ruststream 0.7.0-rc.10

Async messaging framework for Rust: broker-agnostic traits, router, codecs, and a conformance harness for broker authors.
Documentation
use super::*;

#[test]
fn security_accumulates_schemes_in_order() {
    let spec = ServerSpec::new("kafka.example.com:9093", "kafka")
        .security(SecurityScheme::scram_sha512())
        .security(SecurityScheme::x509());
    assert_eq!(spec.security.len(), 2);
    assert_eq!(spec.security[0].kind, SecuritySchemeKind::ScramSha512);
    assert_eq!(spec.security[1].kind, SecuritySchemeKind::X509);
}

#[test]
fn in_process_spec_starts_without_security() {
    let spec = ServerSpec::in_process("memory");
    assert!(spec.security.is_empty());
}

#[test]
fn the_host_survives_a_scheme_userinfo_a_path_and_a_query() {
    let cases = [
        ("amqp://localhost:5672", "localhost:5672"),
        ("amqp://user:pass@broker:5672", "broker:5672"),
        ("amqps://broker:5671/vhost", "broker:5671"),
        ("nats://broker:4222/?tls=true", "broker:4222"),
        ("redis://:secret@cache:6379", "cache:6379"),
        // No scheme at all: a bare address is already the coordinate.
        ("broker:5672", "broker:5672"),
        // A password may contain '@', so inside the authority the boundary is the last one.
        ("amqp://user:p@ss@broker:5672", "broker:5672"),
        // A host without a port stays a host.
        ("mqtt://user:pass@broker", "broker"),
        // The authority ends before the path, the query and the fragment, so an '@' past it
        // separates nothing. Cutting on '@' before cutting the path reports a host of "b".
        ("nats://broker:4222/a@b", "broker:4222"),
        ("nats://broker:4222/?token=a@b", "broker:4222"),
        ("nats://broker:4222#a@b", "broker:4222"),
        // A token carries no colon and no user name, and is userinfo all the same.
        ("nats://s3cret-token@broker:4222", "broker:4222"),
    ];
    for (url, expected) in cases {
        assert_eq!(ServerSpec::host_from_url(url), expected, "url {url:?}");
    }
}

/// The document a service generates is meant to be published, so a URL that authenticates the
/// connection must not describe the server it reaches.
#[test]
fn a_url_carrying_credentials_describes_a_server_without_them() {
    let spec = ServerSpec::from_url("amqp://svc:secret@broker.example.com:5672/prod", "amqp");

    assert_eq!(spec.host.as_deref(), Some("broker.example.com:5672"));
    assert_eq!(spec.protocol, "amqp");

    let host = spec.host.expect("a networked broker describes a host");
    assert!(!host.contains("secret"), "the description leaked {host:?}");
    assert!(!host.contains("svc"), "the description leaked {host:?}");
    assert!(!host.contains('@'), "the description leaked {host:?}");
}

#[test]
fn constructors_produce_their_kind() {
    let cases = [
        (
            SecurityScheme::user_password(),
            SecuritySchemeKind::UserPassword,
        ),
        (
            SecurityScheme::api_key(ApiKeyLocation::User),
            SecuritySchemeKind::ApiKey {
                location: ApiKeyLocation::User,
            },
        ),
        (SecurityScheme::x509(), SecuritySchemeKind::X509),
        (SecurityScheme::plain(), SecuritySchemeKind::Plain),
        (
            SecurityScheme::scram_sha256(),
            SecuritySchemeKind::ScramSha256,
        ),
        (
            SecurityScheme::scram_sha512(),
            SecuritySchemeKind::ScramSha512,
        ),
        (SecurityScheme::gssapi(), SecuritySchemeKind::Gssapi),
        (
            SecurityScheme::http("bearer"),
            SecuritySchemeKind::Http {
                scheme: "bearer".into(),
            },
        ),
        (
            SecurityScheme::http_api_key("X-Api-Key", HttpApiKeyLocation::Header),
            SecuritySchemeKind::HttpApiKey {
                name: "X-Api-Key".into(),
                location: HttpApiKeyLocation::Header,
            },
        ),
        (
            SecurityScheme::open_id_connect("https://idp.example.com/.well-known"),
            SecuritySchemeKind::OpenIdConnect {
                url: "https://idp.example.com/.well-known".into(),
            },
        ),
    ];
    for (scheme, kind) in cases {
        assert_eq!(scheme.kind, kind);
        assert_eq!(scheme.description, None);
    }
}

#[cfg(feature = "json")]
#[test]
fn json_backed_constructors_serialize_their_payload() {
    let oauth2 = SecurityScheme::oauth2(serde_json::json!({ "clientCredentials": {} }));
    assert_eq!(
        oauth2.kind,
        SecuritySchemeKind::Oauth2 {
            flows: r#"{"clientCredentials":{}}"#.into(),
        }
    );

    let custom = SecurityScheme::custom(serde_json::json!({ "type": "symmetricEncryption" }));
    assert_eq!(
        custom.kind,
        SecuritySchemeKind::Custom {
            object: r#"{"type":"symmetricEncryption"}"#.into(),
        }
    );
}

#[test]
fn the_description_reaches_the_scheme() {
    let scheme = SecurityScheme::plain().description("SASL over TLS");
    assert_eq!(scheme.description.as_deref(), Some("SASL over TLS"));
}

#[test]
fn api_key_locations_map_to_document_values() {
    assert_eq!(ApiKeyLocation::User.as_api(), "user");
    assert_eq!(ApiKeyLocation::Password.as_api(), "password");
}

#[test]
fn http_api_key_locations_map_to_document_values() {
    assert_eq!(HttpApiKeyLocation::Query.as_api(), "query");
    assert_eq!(HttpApiKeyLocation::Header.as_api(), "header");
    assert_eq!(HttpApiKeyLocation::Cookie.as_api(), "cookie");
}