Files
crank/apps/admin-api/tests/integration/openapi_source/apply_failures.rs
T

254 lines
8.4 KiB
Rust

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(&registry, &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(&registry, &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(&registry, &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(&registry, &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
}