254 lines
8.4 KiB
Rust
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(®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
|
|
}
|