liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
use super::*;

// ---- LP-WS-TRANSPORT R1.1: [websocket] section validation ----

fn sample_websocket() -> Result<crate::config::types::WebSocketConfig, Box<dyn std::error::Error>> {
    Ok(crate::config::types::WebSocketConfig {
        listen_address: socket("127.0.0.1:8082")?,
        path: "/liminal".to_owned(),
        allowed_origins: vec!["https://app.example.com".to_owned()],
        allowed_origin_ports: Vec::new(),
        ping_interval_ms: Some(30_000),
    })
}

#[test]
fn valid_websocket_section_passes_validation() -> Result<(), Box<dyn std::error::Error>> {
    let mut config = sample_config()?;
    config.websocket = Some(sample_websocket()?);
    validate(&mut config, None)?;
    Ok(())
}

#[test]
fn websocket_empty_origin_list_is_valid_fail_closed_configuration()
-> Result<(), Box<dyn std::error::Error>> {
    let mut config = sample_config()?;
    let mut websocket = sample_websocket()?;
    websocket.allowed_origins = Vec::new();
    config.websocket = Some(websocket);
    validate(&mut config, None)?;
    Ok(())
}

#[test]
fn websocket_zero_port_is_a_typed_config_error() -> Result<(), Box<dyn std::error::Error>> {
    let mut config = sample_config()?;
    let mut websocket = sample_websocket()?;
    websocket.listen_address = socket("127.0.0.1:0")?;
    config.websocket = Some(websocket);
    let message = config_validation_message(validate(&mut config, None));
    assert!(
        message.contains("websocket.listen_address"),
        "expected websocket.listen_address error, got: {message}"
    );
    Ok(())
}

#[test]
fn websocket_address_conflicts_are_typed_config_errors() -> Result<(), Box<dyn std::error::Error>> {
    for conflict in ["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:9000"] {
        let mut config = sample_config()?;
        let mut websocket = sample_websocket()?;
        websocket.listen_address = socket(conflict)?;
        config.websocket = Some(websocket);
        let message = config_validation_message(validate(&mut config, None));
        assert!(
            message.contains("websocket.listen_address: must differ from"),
            "expected an address-conflict error for {conflict}, got: {message}"
        );
    }
    Ok(())
}

#[test]
fn websocket_path_rules_are_typed_config_errors() -> Result<(), Box<dyn std::error::Error>> {
    for (bad_path, expectation) in [
        ("", "must not be empty"),
        ("liminal", "must start with '/'"),
        ("/limi nal", "whitespace"),
        ("/liminal?x=1", "query"),
        ("/liminal#frag", "query"),
    ] {
        let mut config = sample_config()?;
        let mut websocket = sample_websocket()?;
        websocket.path = bad_path.to_owned();
        config.websocket = Some(websocket);
        let message = config_validation_message(validate(&mut config, None));
        assert!(
            message.contains("websocket.path") && message.contains(expectation),
            "expected websocket.path '{bad_path}' to report '{expectation}', got: {message}"
        );
    }
    Ok(())
}

#[test]
fn websocket_malformed_origin_entries_are_typed_config_errors()
-> Result<(), Box<dyn std::error::Error>> {
    for (bad_origin, expectation) in [
        ("", "must not be empty"),
        ("app.example.com", "serialized origin"),
        ("https://app.example.com/", "path or trailing slash"),
        ("https://app.example.com/app", "path or trailing slash"),
        ("https:// app.example.com", "whitespace"),
    ] {
        let mut config = sample_config()?;
        let mut websocket = sample_websocket()?;
        websocket.allowed_origins = vec![bad_origin.to_owned()];
        config.websocket = Some(websocket);
        let message = config_validation_message(validate(&mut config, None));
        assert!(
            message.contains("websocket.allowed_origins") && message.contains(expectation),
            "expected origin '{bad_origin}' to report '{expectation}', got: {message}"
        );
    }
    Ok(())
}

#[test]
fn websocket_duplicate_origin_is_a_typed_config_error() -> Result<(), Box<dyn std::error::Error>> {
    let mut config = sample_config()?;
    let mut websocket = sample_websocket()?;
    websocket.allowed_origins = vec![
        "https://app.example.com".to_owned(),
        "https://app.example.com".to_owned(),
    ];
    config.websocket = Some(websocket);
    let message = config_validation_message(validate(&mut config, None));
    assert!(
        message.contains("duplicate origin"),
        "expected a duplicate-origin error, got: {message}"
    );
    Ok(())
}

#[test]
fn websocket_zero_ping_interval_is_a_typed_config_error() -> Result<(), Box<dyn std::error::Error>>
{
    let mut config = sample_config()?;
    let mut websocket = sample_websocket()?;
    websocket.ping_interval_ms = Some(0);
    config.websocket = Some(websocket);
    let message = config_validation_message(validate(&mut config, None));
    assert!(
        message.contains("websocket.ping_interval_ms"),
        "expected a zero-interval error, got: {message}"
    );
    Ok(())
}

#[test]
fn websocket_absent_ping_interval_means_disabled_and_valid()
-> Result<(), Box<dyn std::error::Error>> {
    let mut config = sample_config()?;
    let mut websocket = sample_websocket()?;
    websocket.ping_interval_ms = None;
    config.websocket = Some(websocket);
    validate(&mut config, None)?;
    Ok(())
}

// ---- CON-014 R2: allowed_origin_ports hygiene ----

#[test]
fn con014_a_zero_or_duplicate_origin_port_is_refused_by_name()
-> Result<(), Box<dyn std::error::Error>> {
    for (ports, expectation) in [(vec![0u16], "port 0"), (vec![5178, 5178], "duplicate port")] {
        let mut config = sample_config()?;
        let mut websocket = sample_websocket()?;
        websocket.allowed_origin_ports = ports;
        config.websocket = Some(websocket);
        let message = config_validation_message(validate(&mut config, None));
        assert!(
            message.contains("websocket.allowed_origin_ports") && message.contains(expectation),
            "expected '{expectation}', got: {message}"
        );
    }
    let mut config = sample_config()?;
    let mut websocket = sample_websocket()?;
    websocket.allowed_origin_ports = vec![5178, 5188];
    config.websocket = Some(websocket);
    validate(&mut config, None)?;
    Ok(())
}