use admin_api::service::{OpenApiImportCreatePayload, OpenApiUpload, OpenApiUploadLocale}; use crank_core::WorkspaceId; use crank_registry::ImportJobStatus; use serial_test::serial; use super::common::{ test_auth_settings, test_registry, test_secret_crypto, test_service, test_storage_root, }; const OPENAPI3: &str = r#" openapi: 3.0.3 info: title: Frankfurter API servers: - url: https://api.frankfurter.dev paths: /v2/latest: get: operationId: latestRates summary: Получить последние курсы валют description: Курсы. tags: [currency] parameters: - name: base in: query required: true schema: { type: string } responses: '200': description: OK content: application/json: schema: type: object properties: base: { type: string } rates: type: object "#; #[tokio::test] #[serial] async fn previews_openapi_and_creates_draft_operations() { let registry = test_registry().await; let service = test_service( registry.clone(), test_storage_root("openapi_import"), test_auth_settings(), test_secret_crypto(), ); let workspace_id = WorkspaceId::new("ws_default"); let preview = service .preview_openapi_import(&workspace_id, openapi_upload()) .await .unwrap(); assert_eq!(preview.preview.groups.len(), 1); assert_eq!( preview.preview.groups[0].operations[0].suggested_name, "latest_rates" ); let public_response = serde_json::to_string(&preview).unwrap(); assert!(!public_response.contains("normalizer_version")); assert!(!public_response.contains("projection_version")); assert!(!public_response.contains("ir_fingerprint")); assert!(!public_response.contains("source_identity")); let preview_job = registry .get_import_job(&workspace_id, &preview.job_id.as_str().into()) .await .unwrap() .unwrap(); assert_eq!(preview_job.status, ImportJobStatus::Pending); assert_eq!( preview_job.preview_payload["normalization"]["normalizer_version"], "normalized-ir-v2" ); assert_eq!( preview_job.preview_payload["normalization"]["projection_version"], "preview-v2" ); assert!(preview_job.preview_payload["normalization"]["ir_fingerprint"].is_string()); let created = service .create_openapi_import( &workspace_id, &preview.job_id.as_str().into(), OpenApiImportCreatePayload { selected_operation_keys: vec!["GET /v2/latest".to_owned()], server_url: Some("https://api.frankfurter.dev".to_owned()), conflict_mode: "skip".to_owned(), }, ) .await .unwrap(); assert_eq!(created.created.len(), 1); assert_eq!(created.created[0].operation_key, "GET /v2/latest"); assert_eq!(created.created[0].name, "latest_rates"); assert!(created.skipped.is_empty()); let replayed = service .create_openapi_import( &workspace_id, &preview.job_id.as_str().into(), OpenApiImportCreatePayload { selected_operation_keys: vec!["GET /v2/latest".to_owned()], server_url: Some("https://api.frankfurter.dev".to_owned()), conflict_mode: "skip".to_owned(), }, ) .await .unwrap(); assert_eq!(replayed.created.len(), 1); assert_eq!( replayed.created[0].operation_id, created.created[0].operation_id ); let operations = service.list_operations(&workspace_id).await.unwrap(); assert!( operations .iter() .any(|operation| operation.name == "latest_rates") ); let imported = operations .iter() .find(|operation| operation.name == "latest_rates") .unwrap(); let version = service .get_operation_version( &workspace_id, &imported.id.as_str().into(), imported.current_draft_version, ) .await .unwrap(); let import_findings = &version .snapshot .wizard_state .as_ref() .unwrap() .import_findings; assert!( import_findings .iter() .any(|finding| finding.code == "openapi_import.weak_tool_description") ); let skip_preview = service .preview_openapi_import(&workspace_id, openapi_upload()) .await .unwrap(); let skipped = service .create_openapi_import( &workspace_id, &skip_preview.job_id.as_str().into(), OpenApiImportCreatePayload { selected_operation_keys: vec!["GET /v2/latest".to_owned()], server_url: Some("https://api.frankfurter.dev".to_owned()), conflict_mode: "skip".to_owned(), }, ) .await .unwrap(); assert!(skipped.created.is_empty()); assert_eq!(skipped.skipped.len(), 1); assert_eq!(skipped.skipped[0].name, "latest_rates"); assert_eq!(skipped.findings[0].code, "operation_name_conflict"); let rename_preview = service .preview_openapi_import(&workspace_id, openapi_upload()) .await .unwrap(); let renamed = service .create_openapi_import( &workspace_id, &rename_preview.job_id.as_str().into(), OpenApiImportCreatePayload { selected_operation_keys: vec!["GET /v2/latest".to_owned()], server_url: Some("https://api.frankfurter.dev".to_owned()), conflict_mode: "rename".to_owned(), }, ) .await .unwrap(); assert_eq!(renamed.created.len(), 1); assert_eq!(renamed.created[0].name, "latest_rates_2"); assert_eq!(renamed.findings[0].code, "operation_name_renamed"); } #[tokio::test] #[serial] async fn unknown_selected_operations_are_persisted_in_the_canonical_replay() { let registry = test_registry().await; let service = test_service( registry, test_storage_root("openapi_import_unknown_replay"), test_auth_settings(), test_secret_crypto(), ); let workspace_id = WorkspaceId::new("ws_default"); let preview = service .preview_openapi_import(&workspace_id, openapi_upload()) .await .unwrap(); let payload = OpenApiImportCreatePayload { selected_operation_keys: vec!["GET /v2/latest".to_owned(), "GET /missing".to_owned()], server_url: Some("https://api.frankfurter.dev".to_owned()), conflict_mode: "skip".to_owned(), }; let first = service .create_openapi_import( &workspace_id, &preview.job_id.as_str().into(), payload.clone(), ) .await .unwrap(); let replayed = service .create_openapi_import(&workspace_id, &preview.job_id.as_str().into(), payload) .await .unwrap(); assert_eq!(first.created, replayed.created); assert_eq!(first.skipped, replayed.skipped); assert_eq!(first.skipped.len(), 1); assert_eq!(first.skipped[0].operation_key, "GET /missing"); } #[tokio::test] #[serial] async fn concurrent_openapi_import_replays_the_same_atomic_result() { let registry = test_registry().await; let service = test_service( registry, test_storage_root("openapi_import_replay"), test_auth_settings(), test_secret_crypto(), ); let workspace_id = WorkspaceId::new("ws_default"); let preview = service .preview_openapi_import(&workspace_id, openapi_upload()) .await .unwrap(); let job_id = preview.job_id.as_str().into(); let payload = OpenApiImportCreatePayload { selected_operation_keys: vec!["GET /v2/latest".to_owned()], server_url: Some("https://api.frankfurter.dev".to_owned()), conflict_mode: "rename".to_owned(), }; let (first, second) = tokio::join!( service.create_openapi_import(&workspace_id, &job_id, payload.clone()), service.create_openapi_import(&workspace_id, &job_id, payload), ); let first = first.unwrap(); let second = second.unwrap(); assert_eq!(first.created.len(), 1); assert_eq!(second.created.len(), 1); assert_eq!( first.created[0].operation_id, second.created[0].operation_id ); assert_eq!(first.created[0].name, second.created[0].name); assert_eq!( service.list_operations(&workspace_id).await.unwrap().len(), 1 ); let conflicting_replay = service .create_openapi_import( &workspace_id, &job_id, OpenApiImportCreatePayload { selected_operation_keys: vec!["GET /v2/latest".to_owned()], server_url: Some("https://api.frankfurter.dev".to_owned()), conflict_mode: "skip".to_owned(), }, ) .await; assert!(conflicting_replay.is_err()); } #[tokio::test] #[serial] async fn apply_fails_closed_when_the_preview_parser_contract_drifts() { let registry = test_registry().await; let service = test_service( registry.clone(), test_storage_root("openapi_import_parser_drift"), test_auth_settings(), test_secret_crypto(), ); let workspace_id = WorkspaceId::new("ws_default"); let preview = service .preview_openapi_import(&workspace_id, openapi_upload()) .await .unwrap(); let job_id: crank_registry::ImportJobId = preview.job_id.as_str().into(); sqlx::query( "update import_jobs set preview_payload = jsonb_set(preview_payload, '{preview_digest}', to_jsonb($1::text)) where id = $2", ) .bind("0".repeat(64)) .bind(job_id.as_str()) .execute(registry.pool()) .await .unwrap(); let result = service .create_openapi_import( &workspace_id, &job_id, OpenApiImportCreatePayload { selected_operation_keys: vec!["GET /v2/latest".to_owned()], server_url: Some("https://api.frankfurter.dev".to_owned()), conflict_mode: "rename".to_owned(), }, ) .await; assert!(result.is_err()); assert!( service .list_operations(&workspace_id) .await .unwrap() .is_empty() ); } #[tokio::test] #[serial] async fn apply_fails_closed_for_each_normalization_contract_field_and_digest_shape() { let registry = test_registry().await; let service = test_service( registry.clone(), test_storage_root("openapi_import_contract_fields"), test_auth_settings(), test_secret_crypto(), ); let workspace_id = WorkspaceId::new("ws_default"); for case in [ "normalizer_version", "projection_version", "ir_fingerprint", "missing_preview_digest", "non_string_preview_digest", ] { let preview = service .preview_openapi_import(&workspace_id, openapi_upload()) .await .unwrap(); let job_id: crank_registry::ImportJobId = preview.job_id.as_str().into(); let query = match case { "normalizer_version" => sqlx::query( "update import_jobs set preview_payload = jsonb_set(preview_payload, '{normalization,normalizer_version}', to_jsonb('future'::text)) where id = $1", ), "projection_version" => sqlx::query( "update import_jobs set preview_payload = jsonb_set(preview_payload, '{normalization,projection_version}', to_jsonb('future'::text)) where id = $1", ), "ir_fingerprint" => sqlx::query( "update import_jobs set preview_payload = jsonb_set(preview_payload, '{normalization,ir_fingerprint}', to_jsonb('bad'::text)) where id = $1", ), "missing_preview_digest" => sqlx::query( "update import_jobs set preview_payload = preview_payload - 'preview_digest' where id = $1", ), "non_string_preview_digest" => sqlx::query( "update import_jobs set preview_payload = jsonb_set(preview_payload, '{preview_digest}', '42'::jsonb) where id = $1", ), _ => unreachable!(), }; query .bind(job_id.as_str()) .execute(registry.pool()) .await .unwrap(); assert!( service .create_openapi_import( &workspace_id, &job_id, OpenApiImportCreatePayload { selected_operation_keys: vec!["GET /v2/latest".to_owned()], server_url: Some("https://api.frankfurter.dev".to_owned()), conflict_mode: "rename".to_owned(), } ) .await .is_err() ); assert!( service .list_operations(&workspace_id) .await .unwrap() .is_empty() ); } } #[tokio::test] #[serial] async fn legacy_job_without_normalization_contract_replays_until_expiry() { let registry = test_registry().await; let service = test_service( registry.clone(), test_storage_root("openapi_import_legacy_replay"), test_auth_settings(), test_secret_crypto(), ); let workspace_id = WorkspaceId::new("ws_default"); let preview = service .preview_openapi_import(&workspace_id, openapi_upload()) .await .unwrap(); let job_id: crank_registry::ImportJobId = preview.job_id.as_str().into(); sqlx::query( "update import_jobs set preview_payload = preview_payload - 'normalization' where id = $1", ) .bind(job_id.as_str()) .execute(registry.pool()) .await .unwrap(); let result = service .create_openapi_import( &workspace_id, &job_id, OpenApiImportCreatePayload { selected_operation_keys: vec!["GET /v2/latest".to_owned()], server_url: Some("https://api.frankfurter.dev".to_owned()), conflict_mode: "skip".to_owned(), }, ) .await .unwrap(); assert_eq!(result.created.len(), 1); } #[tokio::test] #[serial] async fn blocker_finding_keeps_valid_preview_but_prevents_draft_mutation() { let registry = test_registry().await; let service = test_service( registry, test_storage_root("openapi_import_blocker"), test_auth_settings(), test_secret_crypto(), ); let workspace_id = WorkspaceId::new("ws_default"); let upload = OpenApiUpload { bytes: br#" openapi: 3.0.3 info: { title: Partial } paths: /valid: get: responses: { '200': { description: ok } } /broken: not-a-path-item "# .to_vec(), mime_type: "application/yaml".to_owned(), locale: OpenApiUploadLocale::En, }; let preview = service .preview_openapi_import(&workspace_id, upload) .await .unwrap(); assert_eq!(preview.preview.groups[0].operations.len(), 1); assert!( preview .preview .findings .iter() .any(|finding| finding.severity == crank_import::rest::ImportFindingSeverity::Error) ); assert!( service .create_openapi_import( &workspace_id, &preview.job_id.as_str().into(), OpenApiImportCreatePayload { selected_operation_keys: vec!["GET /valid".to_owned()], server_url: None, conflict_mode: "skip".to_owned(), }, ) .await .is_err() ); assert!( service .list_operations(&workspace_id) .await .unwrap() .is_empty() ); } #[tokio::test] #[serial] async fn operation_level_reference_blocker_prevents_draft_mutation() { let registry = test_registry().await; let service = test_service( registry, test_storage_root("openapi_import_operation_blocker"), test_auth_settings(), test_secret_crypto(), ); let workspace_id = WorkspaceId::new("ws_default"); let upload = OpenApiUpload { bytes: br#" openapi: 3.0.3 info: { title: References } paths: /referenced: get: operationId: referenced responses: '200': description: ok content: application/json: schema: { $ref: '#/components/schemas/Result' } components: schemas: Result: { type: object, properties: { id: { type: string } } } "# .to_vec(), mime_type: "application/yaml".to_owned(), locale: OpenApiUploadLocale::En, }; let preview = service .preview_openapi_import(&workspace_id, upload) .await .unwrap(); assert!( preview.preview.findings.iter().all(|finding| { finding.severity != crank_import::rest::ImportFindingSeverity::Error }) ); assert!( preview.preview.groups[0].operations[0] .findings .iter() .any(|finding| { finding.code == "unresolved_reference" && finding.severity == crank_import::rest::ImportFindingSeverity::Error }) ); assert!( service .create_openapi_import( &workspace_id, &preview.job_id.as_str().into(), OpenApiImportCreatePayload { selected_operation_keys: vec!["GET /referenced".to_owned()], server_url: None, conflict_mode: "skip".to_owned(), }, ) .await .is_err() ); assert!( service .list_operations(&workspace_id) .await .unwrap() .is_empty() ); } fn openapi_upload() -> OpenApiUpload { OpenApiUpload { bytes: OPENAPI3.as_bytes().to_vec(), mime_type: "application/yaml".to_owned(), locale: OpenApiUploadLocale::En, } }