use crank_artifacts::ArtifactRef; use super::*; #[tokio::test(flavor = "multi_thread")] #[serial] async fn public_http_contract_is_safe_for_source_failures() { const SOURCE_CANARY: &str = "openapi-apply-source-secret-canary"; let registry = test_registry().await; let storage_root = test_storage_root("openapi_apply_source_errors"); let app = build_test_app(registry.clone(), storage_root.clone()); let server = spawn_admin_api(app).await; let client = authorized_client(&server).await; let workspace_id = WorkspaceId::new("ws_default"); for (marker, expected_code, corrupt) in [ ("missing", "openapi_upload.source_unavailable", false), ("corrupt", "openapi_upload.source_integrity", true), ] { let document = format!("{OPENAPI}\n# {marker} {SOURCE_CANARY}"); let preview_response = client .post(format!("{server}/imports/openapi/preview")) .multipart(Form::new().part( "file", file_part(document.as_bytes(), "openapi.yaml", "application/yaml"), )) .send() .await .unwrap(); assert_eq!(preview_response.status(), reqwest::StatusCode::OK); let preview = preview_response.json::().await.unwrap(); let job_id: crank_registry::ImportJobId = preview["job_id"].as_str().unwrap().into(); let job = registry .get_import_job(&workspace_id, &job_id) .await .unwrap() .unwrap(); let artifact_ref: ArtifactRef = job.preview_payload["source"]["digest"] .as_str() .unwrap() .parse() .unwrap(); let path = artifact_path(&storage_root, &artifact_ref); if corrupt { std::fs::set_permissions(&path, std::os::unix::fs::PermissionsExt::from_mode(0o600)) .unwrap(); std::fs::write(&path, b"corrupt").unwrap(); std::fs::set_permissions(&path, std::os::unix::fs::PermissionsExt::from_mode(0o400)) .unwrap(); } else { std::fs::remove_file(path).unwrap(); } let response = client .post(format!( "{server}/imports/openapi/{}/create", job_id.as_str() )) .json(&serde_json::json!({ "selected_operation_keys": ["GET /health"], "conflict_mode": "skip" })) .send() .await .unwrap(); let status = response.status(); let body = response.json::().await.unwrap(); assert_eq!(status, reqwest::StatusCode::UNPROCESSABLE_ENTITY, "{body}"); assert_eq!(body["error"]["code"], expected_code); assert!(body["error"]["request_id"].is_string()); assert!(body["error"]["trace_id"].is_string()); let rendered = body.to_string(); assert!(!rendered.contains(SOURCE_CANARY)); assert!(!rendered.contains("source_id")); assert!(!rendered.contains("digest")); } assert_eq!( sqlx::query_scalar::<_, i64>("select count(*) from operations where workspace_id = $1") .bind(workspace_id.as_str()) .fetch_one(registry.pool()) .await .unwrap(), 0, "source failures must not create operations" ); } #[tokio::test(flavor = "multi_thread")] #[serial] async fn missing_corrupt_changed_and_foreign_sources_create_no_drafts() { let registry = test_registry().await; let storage_root = test_storage_root("openapi_source_fail_closed"); let service = test_service( registry.clone(), storage_root.clone(), test_auth_settings(), test_secret_crypto(), ); let workspace_id = WorkspaceId::new("ws_default"); let (missing_job, _, missing_ref) = preview_job_source(®istry, &service, &workspace_id, "missing").await; std::fs::remove_file(artifact_path(&storage_root, &missing_ref)).unwrap(); assert!( apply_health_operation(&service, &workspace_id, &missing_job) .await .is_err() ); let (corrupt_job, _, corrupt_ref) = preview_job_source(®istry, &service, &workspace_id, "corrupt").await; let corrupt_path = artifact_path(&storage_root, &corrupt_ref); std::fs::set_permissions( &corrupt_path, std::os::unix::fs::PermissionsExt::from_mode(0o600), ) .unwrap(); std::fs::write(&corrupt_path, b"corrupt-openapi-canary").unwrap(); std::fs::set_permissions( &corrupt_path, std::os::unix::fs::PermissionsExt::from_mode(0o400), ) .unwrap(); assert!( apply_health_operation(&service, &workspace_id, &corrupt_job) .await .is_err() ); let (changed_job, _, _) = preview_job_source(®istry, &service, &workspace_id, "changed").await; sqlx::query( "update import_jobs set preview_payload = jsonb_set( preview_payload, '{source,digest}', to_jsonb($1::text) ) where id = $2", ) .bind(format!("sha256:{}", "0".repeat(64))) .bind(changed_job.as_str()) .execute(registry.pool()) .await .unwrap(); assert!( apply_health_operation(&service, &workspace_id, &changed_job) .await .is_err() ); let now = OffsetDateTime::now_utc(); let foreign_workspace = WorkspaceId::new("ws_openapi_foreign"); registry .create_workspace(CreateWorkspaceRequest { workspace: &Workspace { id: foreign_workspace.clone(), slug: "openapi-foreign".to_owned(), display_name: "OpenAPI Foreign".to_owned(), status: WorkspaceStatus::Active, settings: serde_json::json!({}), created_at: now, updated_at: now, }, }) .await .unwrap(); let (foreign_job, _, _) = preview_job_source(®istry, &service, &workspace_id, "foreign").await; sqlx::query("update import_jobs set workspace_id = $1 where id = $2") .bind(foreign_workspace.as_str()) .bind(foreign_job.as_str()) .execute(registry.pool()) .await .unwrap(); assert!( apply_health_operation(&service, &foreign_workspace, &foreign_job) .await .is_err() ); assert!( service .list_operations(&workspace_id) .await .unwrap() .is_empty() ); assert!( service .list_operations(&foreign_workspace) .await .unwrap() .is_empty() ); } async fn preview_job_source( registry: &crank_registry::PostgresRegistry, service: &admin_api::service::AdminService, workspace_id: &WorkspaceId, marker: &str, ) -> (crank_registry::ImportJobId, ArtifactSourceId, ArtifactRef) { let preview = service .preview_openapi_import( workspace_id, OpenApiUpload { bytes: format!("{OPENAPI}\n# {marker}").into_bytes(), mime_type: "application/yaml".to_owned(), locale: OpenApiUploadLocale::En, }, ) .await .unwrap(); let job_id: crank_registry::ImportJobId = preview.job_id.as_str().into(); let job = registry .get_import_job(workspace_id, &job_id) .await .unwrap() .unwrap(); let source = &job.preview_payload["source"]; ( job_id, ArtifactSourceId::new(source["source_id"].as_str().unwrap()), source["digest"].as_str().unwrap().parse().unwrap(), ) } fn artifact_path(root: &std::path::Path, artifact_ref: &ArtifactRef) -> std::path::PathBuf { root.join("sha256") .join(&artifact_ref.digest_hex()[..2]) .join(artifact_ref.digest_hex()) } async fn apply_health_operation( service: &admin_api::service::AdminService, workspace_id: &WorkspaceId, job_id: &crank_registry::ImportJobId, ) -> Result { service .create_openapi_import( workspace_id, job_id, OpenApiImportCreatePayload { selected_operation_keys: vec!["GET /health".to_owned()], server_url: None, conflict_mode: "skip".to_owned(), }, ) .await }