feat(openapi): complete upload preview and UI evidence
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
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::<Value>().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::<Value>().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<admin_api::service::OpenApiImportCreateResponse, admin_api::error::ApiError> {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user