liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
use super::validate_templates;
use crate::config::types::ServerConfig;

/// A server configuration with `templates` as its `[[auth.lys.templates]]`.
fn config(templates: &str, persistence: bool) -> ServerConfig {
    let persistence = if persistence {
        "persistence_path = \"/tmp/liminal-templates-test\"\n"
    } else {
        ""
    };
    let text = format!(
        "listen_address = \"127.0.0.1:1\"\nhealth_listen_address = \"127.0.0.1:2\"\ndrain_timeout_ms = 5000\n\
         routing_rules = []\n{persistence}\n[[channels]]\nname = \"a\"\ndurable = false\n\n\
         [auth]\ntoken = \"t\"\n\n[auth.lys]\nissuer = \"https://lys.test\"\n\
         network_id = \"n\"\nkeys_path = \"/k\"\napp_credential_path = \"/c\"\n\n{templates}"
    );
    toml::from_str(&text).unwrap_or_else(|error| unreachable!("fixture config parses: {error}"))
}

fn errors_of(config: &ServerConfig) -> Vec<String> {
    let mut errors = Vec::new();
    validate_templates(config, &mut errors);
    errors
}

const SH: &str = "[[auth.lys.templates]]\nname = \"echo\"\ncommand = \"/bin/sh\"\n\
                  args = [\"-c\", \"true\"]\nsubjects = [\"echo\"]\nstop_timeout_ms = 500\n";

#[test]
fn config_reads_a_template_whole() {
    let config = config(SH, true);
    assert_eq!(errors_of(&config), Vec::<String>::new());
    let lys = config.auth.as_ref().and_then(|auth| auth.lys.as_ref());
    let template = lys.and_then(|lys| lys.templates.first());
    assert_eq!(template.map(|t| t.name.as_str()), Some("echo"));
    assert_eq!(template.map(|t| t.args.len()), Some(2));
    assert_eq!(template.map(|t| t.stop_timeout_ms), Some(500));
}

#[test]
fn config_refuses_a_relative_template_command() {
    let errors = errors_of(&config(&SH.replace("/bin/sh", "sh"), true));
    assert!(
        errors
            .iter()
            .any(|e| e.contains("'echo'") && e.contains("absolute path")),
        "{errors:?}"
    );
}

#[test]
fn config_refuses_a_command_that_is_not_executable() {
    let errors = errors_of(&config(&SH.replace("/bin/sh", "/etc/hosts"), true));
    assert!(
        errors.iter().any(|e| e.contains("not an executable file")),
        "{errors:?}"
    );
}

#[test]
fn config_refuses_a_template_without_subjects() {
    let errors = errors_of(&config(&SH.replace("[\"echo\"]", "[]"), true));
    assert!(
        errors.iter().any(|e| e.contains("at least one subject")),
        "{errors:?}"
    );
}

#[test]
fn config_refuses_two_templates_sharing_a_subject() {
    let second = SH.replace("name = \"echo\"", "name = \"other\"");
    let errors = errors_of(&config(&format!("{SH}\n{second}"), true));
    assert!(
        errors
            .iter()
            .any(|e| e.contains("'other'") && e.contains("another template's")),
        "{errors:?}"
    );
}

#[test]
fn config_refuses_templates_without_a_registry_path() {
    let errors = errors_of(&config(SH, false));
    assert!(
        errors.iter().any(|e| e.contains("persistence_path")),
        "{errors:?}"
    );
}

#[test]
fn config_refuses_a_zero_stop_timeout_and_a_wildcard_subject() {
    let bad = SH
        .replace("stop_timeout_ms = 500", "stop_timeout_ms = 0")
        .replace("[\"echo\"]", "[\"echo.*\"]");
    let errors = errors_of(&config(&bad, true));
    assert!(
        errors.iter().any(|e| e.contains("stop_timeout_ms")),
        "{errors:?}"
    );
    assert!(
        errors.iter().any(|e| e.contains("no wildcard")),
        "{errors:?}"
    );
}