haematite-cli 0.21.7

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
use serde_json::{Value, json};

use super::Config;

type TestResult = Result<(), Box<dyn std::error::Error>>;

fn base() -> Value {
    json!({
        "listen":"127.0.0.1:0",
        "allowed_origins":["https://agent.example"]
    })
}

#[test]
fn a_config_naming_permitted_subjects_or_lys_userinfo_is_refused_so_it_cannot_run_open()
-> TestResult {
    serde_json::from_value::<Config>(base())?.validate()?;
    for (key, field) in [
        ("permitted_subjects", json!(["permitted"])),
        (
            "lys_userinfo",
            json!("https://identity.example/oauth/userinfo"),
        ),
        ("sign_in", json!({"timeout_ms":1000,"max_in_flight":2})),
    ] {
        let mut value = base();
        value[key] = field;
        let refused = serde_json::from_value::<Config>(value)
            .err()
            .ok_or_else(|| format!("a config naming {key} was taken"))?;
        assert!(
            refused
                .to_string()
                .contains(&format!("unknown field `{key}`")),
            "{key}: {refused}"
        );
    }
    Ok(())
}

#[test]
fn io_limits_have_independent_defaults_and_knobs() -> TestResult {
    let mut value = base();
    value["header_timeout_ms"] = json!(100);
    value["body_timeout_ms"] = json!(150);
    value["max_connections"] = json!(8);
    value["resource_metadata_url"] = json!("https://identity.example/metadata");
    let config: Config = serde_json::from_value(value)?;
    config.validate()?;
    assert_eq!(config.header_timeout_ms.get(), 100);
    assert_eq!(config.body_timeout_ms.get(), 150);
    assert_eq!(config.max_connections.get(), 8);
    assert_eq!(
        config.resource_metadata_url.as_deref(),
        Some("https://identity.example/metadata")
    );
    let defaults: Config = serde_json::from_value(base())?;
    defaults.validate()?;
    assert_eq!(defaults.header_timeout_ms.get(), 2000);
    assert_eq!(defaults.body_timeout_ms.get(), 5000);
    assert_eq!(defaults.max_connections.get(), 64);
    assert!(defaults.resource_metadata_url.is_none());
    assert_eq!(defaults.path, "/mcp");
    Ok(())
}

#[test]
fn unsafe_binding_urls_and_limits_refuse_by_name() -> TestResult {
    let mut cases = Vec::new();
    for (key, field) in [
        ("listen", json!("0.0.0.0:9000")),
        ("path", json!("mcp")),
        ("path", json!("/mcp/")),
        ("path", json!("/mcp//studio")),
        ("path", json!("/mcp?studio")),
        ("path", json!("/mcp studio")),
        ("allowed_origins", json!(["https://agent.example/path"])),
        (
            "resource_metadata_url",
            json!("http://identity.example/metadata"),
        ),
        (
            "resource_metadata_url",
            json!("https://user:secret@identity.example/metadata"),
        ),
        (
            "resource_metadata_url",
            json!("https://identity.example/metadata\r\nInjected: true"),
        ),
    ] {
        let mut value = base();
        value[key] = field;
        cases.push(value);
    }
    for value in cases {
        let config: Config = serde_json::from_value(value)?;
        assert_eq!(
            config.validate().err().map(|error| error.code),
            Some("mcp_http_config_invalid")
        );
    }
    for key in ["max_connections", "header_timeout_ms", "body_timeout_ms"] {
        let mut value = base();
        value[key] = json!(0);
        assert!(serde_json::from_value::<Config>(value).is_err());
    }
    Ok(())
}