endpoint-validator 0.1.0

Interactive test harness for endpoint-libs WebSocket RPC services: reads the endpoint description generated by endpoint-gen and exercises every endpoint with preset parameters.
Documentation
//! Guards the bug that made this tool unusable: the schema types were vendored
//! by hand, upstream renamed `EnumVariant.comment` to `description`, and every
//! `services.json` produced after that failed with `missing field 'comment'`.
//!
//! The fixture is a real file generated by `endpoint-gen` (api.support.cafe,
//! 26 endpoints across 7 services). Refresh it whenever the format moves — a
//! failure here means this crate needs an `endpoint-libs` bump, not a patch.

use endpoint_libs::model::Type;
use endpoint_validator::parser::models::Services;
use endpoint_validator::parser::services::ConvertValue;

fn fixture() -> Services {
    let raw = include_str!("fixtures/services.json");
    serde_json::from_str(raw).expect("generated services.json must deserialize")
}

#[test]
fn deserializes_a_real_generated_services_json() {
    let services = fixture();
    assert_eq!(services.services.len(), 7);
    assert_eq!(
        services
            .services
            .iter()
            .map(|s| s.endpoints.len())
            .sum::<usize>(),
        26
    );
    assert!(!services.enums.is_empty(), "enums should round-trip");
    assert!(!services.structs.is_empty(), "structs should round-trip");
}

#[test]
fn every_endpoint_exposes_usable_metadata() {
    let services = fixture();
    let (names, data) = services.extract_endpoints();
    assert_eq!(names.len(), 26);
    for name in &names {
        let meta = data.get(name).expect("every name resolves to metadata");
        assert!(meta.method_id > 0, "{name} has no method code");
        assert!(!meta.service_name.is_empty());
    }
}

#[test]
fn enum_variants_convert_to_their_wire_integer() {
    // Enums travel as integers, not names -- `enum_to_schema` upstream emits
    // `type: integer` with a const per variant. The pre-migration code sent the
    // variant name as a string, which a server would reject.
    let services = fixture();
    let enum_ty = services
        .enums
        .iter()
        .find(|t| matches!(t, Type::Enum { .. }))
        .expect("fixture has at least one enum");

    let Type::Enum { variants, .. } = enum_ty else {
        unreachable!()
    };
    let first = &variants[0];

    assert_eq!(
        enum_ty.convert_value(&first.name).unwrap(),
        serde_json::json!(first.value)
    );
    // The numeric spelling is accepted too.
    assert_eq!(
        enum_ty.convert_value(&first.value.to_string()).unwrap(),
        serde_json::json!(first.value)
    );
    assert!(enum_ty.convert_value("NotAVariant").is_err());
}

#[test]
fn scalar_conversions_match_the_wire_types() {
    assert_eq!(
        Type::Int64.convert_value("42").unwrap(),
        serde_json::json!(42)
    );
    assert_eq!(
        Type::Boolean.convert_value("true").unwrap(),
        serde_json::json!(true)
    );
    assert_eq!(
        Type::String.convert_value("hello").unwrap(),
        serde_json::json!("hello")
    );
    // An empty Optional is null, not an empty string.
    assert_eq!(
        Type::Optional(Box::new(Type::String))
            .convert_value("")
            .unwrap(),
        serde_json::Value::Null
    );
    assert!(Type::Int64.convert_value("not-a-number").is_err());
}