use crank_import::rest::model::{CanonicalSourceNode, CanonicalSourceValue, CoverageDisposition}; use crank_import::rest::{ ImportFindingSeverity, ImportParseError, NormalizationConfig, NormalizedFinding, NormalizedIr, SourceDigest, SourceLocation, normalize_verified_document, validate_normalized_ir, }; fn normalize(document: &str) -> NormalizedIr { normalize_verified_document( document, SourceDigest::parse("c".repeat(64)).unwrap(), &NormalizationConfig::default(), ) .unwrap() } fn valid_source() -> &'static str { r#" openapi: 3.1.0 info: { title: Coverage, version: v1 } paths: /items: get: operationId: listItems tags: [items] responses: '200': description: ok content: application/json: schema: { type: object, properties: { id: { type: string } } } "# } fn assert_invalid(ir: &NormalizedIr) { assert_eq!( validate_normalized_ir(ir), Err(ImportParseError::InvalidDocument) ); } #[test] fn source_digest_deserialization_preserves_the_validated_invariant() { assert!(serde_json::from_str::(&format!("\"{}\"", "a".repeat(64))).is_ok()); assert!(serde_json::from_str::(&format!("\"{}\"", "A".repeat(64))).is_err()); assert!(serde_json::from_str::("\"not-a-digest\"").is_err()); } #[test] fn rejects_tampered_coverage_location_disposition_and_orphan_finding() { let ir = normalize(valid_source()); let operation_id = ir.operations[0].stable_id.clone(); let mut wrong_location = ir.clone(); wrong_location .coverage .iter_mut() .find(|entry| entry.construct_id == operation_id) .unwrap() .location .pointer = "/paths/~1other/get".to_owned(); assert_invalid(&wrong_location); let mut wrong_disposition = ir.clone(); wrong_disposition .coverage .iter_mut() .find(|entry| entry.construct_id == operation_id) .unwrap() .disposition = CoverageDisposition::Finding; assert_invalid(&wrong_disposition); let mut duplicate = ir.clone(); duplicate.coverage.push(duplicate.coverage[0].clone()); assert_invalid(&duplicate); let mut orphan = ir; orphan.findings.push(NormalizedFinding { code: "forged".to_owned(), severity: ImportFindingSeverity::Warning, message: "forged".to_owned(), construct_id: "orphan".to_owned(), location: SourceLocation { pointer: String::new(), }, operation_key: None, }); assert_invalid(&orphan); } #[test] fn rejects_tampered_contract_operation_identity_and_source_tree() { let ir = normalize(valid_source()); let mut normalizer = ir.clone(); normalizer.normalizer_version = "future-normalizer".to_owned(); assert_invalid(&normalizer); let mut projection = ir.clone(); projection.projection_version = "future-projection".to_owned(); assert_invalid(&projection); let mut source_version = ir.clone(); source_version.source.version = Some("3.0.3".to_owned()); assert_invalid(&source_version); let mut stable_id = ir.clone(); stable_id.operations[0].stable_id.push_str(":forged"); assert_invalid(&stable_id); let mut operation_location = ir.clone(); operation_location.operations[0].location.pointer = "/paths/~1items/post".to_owned(); assert_invalid(&operation_location); let mut operation_key = ir.clone(); operation_key.operations[0].key = "POST /items".to_owned(); assert_invalid(&operation_key); let mut schema_description = ir.clone(); schema_description.operations[0] .response_schema .as_mut() .unwrap() .description = Some("forged description".to_owned()); assert_invalid(&schema_description); let mut source_tree = ir; source_tree.source_tree.location.pointer = "/forged".to_owned(); assert_invalid(&source_tree); } #[test] fn validates_every_recursive_source_tree_pointer_and_construct_id() { let mut ir = normalize(valid_source()); let CanonicalSourceValue::Object(root) = &mut ir.source_tree.value else { panic!("source root must be an object"); }; let CanonicalSourceValue::Object(info) = &mut root.get_mut("info").unwrap().value else { panic!("info must be an object"); }; let title = info.get_mut("title").unwrap(); title.construct_id = "source:/info/wrong".to_owned(); assert_invalid(&ir); let mut ir = normalize(valid_source()); let title = source_node_mut(&mut ir.source_tree, "/info/title").unwrap(); title.location.pointer = "/info/wrong".to_owned(); assert_invalid(&ir); } #[test] fn accepts_virtual_missing_operation_id_with_exact_finding_coverage() { let ir = normalize( "openapi: 3.1.0\ninfo: { title: Missing ID }\npaths: { /ok: { get: { responses: { '200': { description: ok } } } } }", ); validate_normalized_ir(&ir).unwrap(); let operation = &ir.operations[0]; let id = format!("{}:operation_id", operation.stable_id); let expected_pointer = format!("{}/operationId", operation.location.pointer); assert!(source_node(&ir.source_tree, &expected_pointer).is_none()); assert!(operation.findings.iter().any(|finding| { finding.code == "missing_operation_id" && finding.construct_id == id && finding.location.pointer == expected_pointer })); assert!(ir.coverage.iter().any(|entry| { entry.construct_id == id && entry.location.pointer == expected_pointer && entry.disposition == CoverageDisposition::Finding })); let mut missing_finding = ir; missing_finding.operations[0] .findings .retain(|finding| finding.code != "missing_operation_id"); assert_invalid(&missing_finding); } #[test] fn deduplicates_shared_path_parameter_and_schema_coverage_exactly() { let ir = normalize( r#" openapi: 3.1.0 info: { title: Shared } paths: /items/{id}: parameters: - { name: id, in: path, required: true, schema: { type: string } } get: operationId: getItem responses: { '200': { description: ok } } post: operationId: updateItem responses: { '200': { description: ok } } "#, ); validate_normalized_ir(&ir).unwrap(); assert_eq!(ir.operations.len(), 2); assert_eq!(ir.operations[0].parameters, ir.operations[1].parameters); let parameter_id = &ir.operations[0].parameters[0].construct_id; let schema_id = &ir.operations[0].parameters[0] .schema .as_ref() .unwrap() .construct_id; assert_eq!( ir.coverage .iter() .filter(|entry| &entry.construct_id == parameter_id) .count(), 1 ); assert_eq!( ir.coverage .iter() .filter(|entry| &entry.construct_id == schema_id) .count(), 1 ); } #[test] fn unsupported_method_has_exact_finding_pointer_and_coverage() { let ir = normalize( "openapi: 3.1.0\ninfo: { title: Unsupported }\npaths: { /ok: { get: { responses: { '200': { description: ok } } }, head: { responses: {} } } }", ); validate_normalized_ir(&ir).unwrap(); let pointer = "/paths/~1ok/head"; let construct_id = format!("3.1.0:{pointer}"); assert!(ir.findings.iter().any(|finding| { finding.code == "unsupported_http_method" && finding.construct_id == construct_id && finding.location.pointer == pointer })); assert!(ir.coverage.iter().any(|entry| { entry.construct_id == construct_id && entry.location.pointer == pointer && entry.disposition == CoverageDisposition::Finding })); } fn source_node<'a>( node: &'a CanonicalSourceNode, pointer: &str, ) -> Option<&'a CanonicalSourceNode> { if node.location.pointer == pointer { return Some(node); } match &node.value { CanonicalSourceValue::Array(items) => { items.iter().find_map(|child| source_node(child, pointer)) } CanonicalSourceValue::Object(items) => { items.values().find_map(|child| source_node(child, pointer)) } _ => None, } } fn source_node_mut<'a>( node: &'a mut CanonicalSourceNode, pointer: &str, ) -> Option<&'a mut CanonicalSourceNode> { if node.location.pointer == pointer { return Some(node); } match &mut node.value { CanonicalSourceValue::Array(items) => items .iter_mut() .find_map(|child| source_node_mut(child, pointer)), CanonicalSourceValue::Object(items) => items .values_mut() .find_map(|child| source_node_mut(child, pointer)), _ => None, } }