mockforge-contracts 0.3.240

Contract testing, drift detection, and incident management for MockForge
Documentation
use crate::fidelity::{compare_mock_spec, compare_mock_spec_with_budget};
use serde_json::{json, Value};

fn v2() -> Value {
    json!({"asyncapi":"2.6.0","info":{"title":"events","version":"1"},"defaultContentType":"application/json",
        "servers":{"local":{"url":"localhost:1883","protocol":"mqtt","security":[{"auth":[]}]}},
        "components":{"securitySchemes":{"auth":{"type":"userPassword"}},"schemas":{"Order":{"type":"object","required":["id"],"properties":{"id":{"type":"integer"},"description":{"type":"string"},"traits":{"type":"string"}}}},"messages":{"Order":{"payload":{"$ref":"#/components/schemas/Order"},"headers":{"type":"object","properties":{"trace":{"type":"string"}}}}}},
        "channels":{"orders":{"publish":{"message":{"$ref":"#/components/messages/Order"}},"bindings":{"mqtt":{"qos":1}}}}})
}

fn v3() -> Value {
    json!({"asyncapi":"3.0.0","info":{"title":"events","version":"1"},"defaultContentType":"application/json",
        "servers":{"local":{"host":"localhost:1883","protocol":"mqtt"}},
        "channels":{"orders":{"address":"orders/created","messages":{"description":{"payload":{"type":"object","required":["id"],"properties":{"id":{"type":"integer"}}}}}}},
        "operations":{"receiveOrder":{"action":"receive","channel":{"$ref":"#/channels/orders"},"messages":[{"$ref":"#/channels/orders/messages/description"}]}}})
}

#[test]
fn identical_asyncapi_documents_and_unreferenced_components_are_verified() {
    for mut source in [v2(), v3()] {
        let mock = source.clone();
        source["components"]["unused"] = json!({"type":"string"});
        let report = compare_mock_spec(&mock, &source).unwrap();
        assert_eq!(report.routes_with_drift, 0);
        assert_eq!(report.routes.len(), 2);
    }
}

#[test]
fn v2_publish_means_receive_and_subscribe_means_send() {
    let mut source = v2();
    source["channels"]["orders"]["subscribe"] = source["channels"]["orders"]["publish"].clone();
    let report = compare_mock_spec(&source, &source).unwrap();
    assert!(report.routes.iter().any(|r| r.endpoint == "RECEIVE orders"));
    assert!(report.routes.iter().any(|r| r.endpoint == "SEND orders"));
}

#[test]
fn v3_operations_on_same_channel_are_never_overwritten() {
    let mut source = v3();
    source["operations"]["another"] = source["operations"]["receiveOrder"].clone();
    source["operations"]["sendOrder"] = source["operations"]["receiveOrder"].clone();
    source["operations"]["sendOrder"]["action"] = json!("send");
    let report = compare_mock_spec(&source, &source).unwrap();
    assert_eq!(report.routes.len(), 4);
    let mock = source.clone();
    source["operations"].as_object_mut().unwrap().remove("another");
    let report = compare_mock_spec(&mock, &source).unwrap();
    assert!(report
        .routes
        .iter()
        .any(|r| r.change == "removed" && r.endpoint.contains("[another]")));
}

#[test]
fn nested_schema_headers_and_protocol_bindings_produce_field_deltas() {
    let mock = v2();
    for pointer in [
        "/components/schemas/Order/properties/id/type",
        "/components/schemas/Order/properties/description/type",
        "/components/schemas/Order/properties/traits/type",
        "/components/messages/Order/headers/properties/trace/type",
    ] {
        let mut source = mock.clone();
        *source.pointer_mut(pointer).unwrap() = json!("boolean");
        let report = compare_mock_spec(&mock, &source).unwrap();
        assert!(report
            .routes
            .iter()
            .any(|r| r.deltas.iter().any(|d| d.location.ends_with("/type"))));
    }
    let mut source = mock.clone();
    source["channels"]["orders"]["bindings"]["mqtt"]["qos"] = json!(2);
    assert!(compare_mock_spec(&mock, &source).unwrap().routes_with_drift > 0);
}

#[test]
fn server_security_changes_are_not_hidden_behind_unchanged_references() {
    let mock = v2();
    let mut source = mock.clone();
    source["components"]["securitySchemes"]["auth"]["type"] = json!("plain");
    assert!(compare_mock_spec(&mock, &source).unwrap().routes_with_drift > 0);
    let mock = v3();
    let mut source = mock.clone();
    source["components"]["securitySchemes"]["auth"] = json!({"type":"userPassword"});
    source["components"]["operationTraits"]["secure"] =
        json!({"security":[{"$ref":"#/components/securitySchemes/auth"}]});
    source["operations"]["receiveOrder"]["traits"] =
        json!([{"$ref":"#/components/operationTraits/secure"}]);
    let mock = source.clone();
    source["components"]["securitySchemes"]["auth"]["type"] = json!("plain");
    assert!(compare_mock_spec(&mock, &source).unwrap().routes_with_drift > 0);
}

#[test]
fn message_traits_fill_missing_fields_and_explicit_fields_win() {
    let mut source = v2();
    source["components"]["messageTraits"]["typed"] =
        json!({"contentType":"text/plain","headers":{"type":"object","required":["trace"]}});
    source["components"]["messages"]["Order"]["traits"] =
        json!([{"$ref":"#/components/messageTraits/typed"}]);
    source["components"]["messages"]["Order"]["contentType"] = json!("application/json");
    let mut mock = source.clone();
    mock["components"]["messageTraits"]["typed"]["contentType"] = json!("application/xml");
    assert_eq!(compare_mock_spec(&mock, &source).unwrap().routes_with_drift, 0);
    mock["components"]["messageTraits"]["typed"]["headers"]["required"] = json!(["other"]);
    assert!(compare_mock_spec(&mock, &source).unwrap().routes_with_drift > 0);
}

#[test]
fn schema_literal_traits_fields_are_never_applied_as_asyncapi_traits() {
    let mut source = v2();
    source["components"]["schemas"]["Order"]["properties"]["data"] =
        json!({"type":"object","default":{"traits":[{"x":1}],"x":2}});
    let mock = source.clone();
    source["components"]["schemas"]["Order"]["properties"]["data"]["default"]["traits"][0]["x"] =
        json!(9);
    assert!(compare_mock_spec(&mock, &source).unwrap().routes_with_drift > 0);
}

#[test]
fn content_type_inheritance_and_server_subset_are_effective_contracts() {
    let source = v2();
    let mut mock = source.clone();
    mock["components"]["messages"]["Order"]["contentType"] = json!("application/json");
    mock["servers"]["unused"] = json!({"url":"other:1883","protocol":"mqtt"});
    mock["channels"]["orders"]["servers"] = json!(["local"]);
    assert_eq!(compare_mock_spec(&mock, &source).unwrap().routes_with_drift, 0);
    mock["defaultContentType"] = json!("text/plain");
    assert_eq!(compare_mock_spec(&mock, &source).unwrap().routes_with_drift, 0);
    mock["components"]["messages"]["Order"]
        .as_object_mut()
        .unwrap()
        .remove("contentType");
    assert!(compare_mock_spec(&mock, &source).unwrap().routes_with_drift > 0);
}

#[test]
fn missing_cyclic_external_and_invalid_operation_references_fail_closed() {
    for reference in [
        "#/missing",
        "#/components/messages/Loop",
        "file:///never-read",
        "https://example.invalid/never-fetch",
    ] {
        let mut source = v2();
        source["components"]["messages"]["Loop"] = json!({"$ref":"#/components/messages/Loop"});
        source["channels"]["orders"]["publish"]["message"] = json!({"$ref":reference});
        assert!(compare_mock_spec(&source, &source).is_err());
    }
    let mut source = v3();
    source["operations"]["receiveOrder"]["messages"] =
        json!([{"$ref":"#/channels/orders/messages/description/payload"}]);
    assert!(compare_mock_spec(&source, &source).is_err());
}

#[test]
fn metadata_schema_order_and_comparison_budget_remain_consistent() {
    let source = v2();
    let mut mock = source.clone();
    mock["info"]["title"] = json!("Different documentation");
    mock["channels"]["orders"]["description"] = json!("Only prose");
    mock["components"]["schemas"]["Order"]["required"] = json!(["id", "description"]);
    let mut source = source;
    source["components"]["schemas"]["Order"]["required"] = json!(["description", "id"]);
    assert_eq!(compare_mock_spec(&mock, &source).unwrap().routes_with_drift, 0);
    assert!(compare_mock_spec_with_budget(&mock, &source, 100).is_err());
    assert!(compare_mock_spec(&mock, &json!({"openapi":"3.0.3","paths":{}})).is_err());
}

#[test]
fn x_prefixed_channel_names_are_preserved_as_real_topics() {
    let mut source = v2();
    source["channels"]["x-events"] = source["channels"]["orders"].take();
    source["channels"].as_object_mut().unwrap().remove("orders");
    let report = compare_mock_spec(&source, &source).unwrap();
    assert!(report.routes.iter().any(|r| r.endpoint == "RECEIVE x-events"));
    let mock = source.clone();
    source["channels"]["x-events"]["bindings"]["mqtt"]["qos"] = json!(2);
    assert!(compare_mock_spec(&mock, &source).unwrap().routes_with_drift > 0);
}

#[test]
fn v3_default_message_set_equals_an_explicit_reordered_selection() {
    let mut source = v3();
    source["channels"]["orders"]["messages"]["another"] = json!({"payload":{"type":"string"}});
    source["operations"]["receiveOrder"].as_object_mut().unwrap().remove("messages");
    let mut mock = source.clone();
    mock["operations"]["receiveOrder"]["messages"] = json!([
        {"$ref":"#/channels/orders/messages/description"},
        {"$ref":"#/channels/orders/messages/another"}
    ]);
    assert_eq!(compare_mock_spec(&mock, &source).unwrap().routes_with_drift, 0);
}

#[test]
fn protocol_binding_schemas_resolve_components_and_preserve_schema_fields() {
    let mut mock = v3();
    mock["components"]["schemas"]["Query"] =
        json!({"type":"object","properties":{"description":{"type":"string"}}});
    mock["channels"]["orders"]["bindings"] =
        json!({"ws":{"method":"GET","query":{"$ref":"#/components/schemas/Query"}}});
    let mut source = mock.clone();
    source["components"]["schemas"]["Query"]["properties"]["description"]["type"] =
        json!("integer");
    assert!(compare_mock_spec(&mock, &source).unwrap().routes_with_drift > 0);
    source["channels"]["orders"]["bindings"]["ws"]["query"] =
        json!({"$ref":"https://example.invalid/binding-schema"});
    assert!(compare_mock_spec(&source, &source).is_err());
}

#[test]
fn dynamic_channel_addresses_normalize_absence_and_null() {
    let mut mock = v3();
    mock["channels"]["orders"].as_object_mut().unwrap().remove("address");
    let mut source = mock.clone();
    source["channels"]["orders"]["address"] = Value::Null;
    assert_eq!(compare_mock_spec(&mock, &source).unwrap().routes_with_drift, 0);
    source["channels"]["orders"]["address"] = json!(12);
    assert!(compare_mock_spec(&source, &source).is_err());
}

#[test]
fn operation_trait_message_selections_are_not_overwritten_by_channel_defaults() {
    let mut mock = v3();
    mock["channels"]["orders"]["messages"]["other"] = json!({"payload":{"type":"integer"}});
    mock["operations"]["receiveOrder"].as_object_mut().unwrap().remove("messages");
    mock["components"]["operationTraits"]["selection"] =
        json!({"messages":[{"$ref":"#/channels/orders/messages/description"}]});
    mock["operations"]["receiveOrder"]["traits"] =
        json!([{"$ref":"#/components/operationTraits/selection"}]);
    let mut source = mock.clone();
    source["components"]["operationTraits"]["selection"]["messages"] =
        json!([{"$ref":"#/channels/orders/messages/other"}]);
    let report = compare_mock_spec(&mock, &source).unwrap();
    assert!(report
        .routes
        .iter()
        .any(|r| r.endpoint.contains("[receiveOrder]") && r.change == "changed"));
}

#[test]
fn null_constraints_survive_inline_contract_normalization() {
    let mut mock = v2();
    mock["channels"]["orders"]["publish"]["message"] = json!({"payload":{"const":null}});
    let mut source = mock.clone();
    source["channels"]["orders"]["publish"]["message"]["payload"] = json!({});
    assert!(
        compare_mock_spec(&mock, &source).unwrap().routes_with_drift > 0,
        "an explicit null constraint was erased"
    );
}

#[test]
fn server_variables_named_like_metadata_preserve_defaults_and_enums() {
    for document in [v2(), v3()] {
        for name in [
            "description",
            "title",
            "summary",
            "example",
            "examples",
            "operationId",
            "tags",
        ] {
            let mut mock = document.clone();
            mock["servers"]["local"]["variables"] =
                json!({name: {"default":"before", "enum":["before","after"]}});
            let mut source = mock.clone();
            source["servers"]["local"]["variables"][name]["default"] = json!("after");
            assert!(
                compare_mock_spec(&mock, &source).unwrap().routes_with_drift > 0,
                "server variable {name} default was discarded"
            );
            source = mock.clone();
            source["servers"]["local"]["variables"][name]["enum"] = json!(["before"]);
            assert!(
                compare_mock_spec(&mock, &source).unwrap().routes_with_drift > 0,
                "server variable {name} enum was discarded"
            );
            source = mock.clone();
            source["servers"]["local"]["variables"][name]["description"] =
                json!("documentation only");
            assert_eq!(compare_mock_spec(&mock, &source).unwrap().routes_with_drift, 0);
        }
    }
}