liminal-server 0.27.3

Standalone server for the liminal messaging bus
Documentation
use super::*;
use crate::services::lifecycle::templates::TemplateSources;

// ---- ACCESS-012 R1: templates at the top level, one source of truth ----

const BOTH: &str = "templates are written once: at the top level or under [auth.lys], not both";

const ECHO: &str = "name = \"echo\"\ncommand = \"/bin/sh\"\nargs = [\"-c\", \"true\"]\n\
                    subjects = [\"echo\"]\nstop_timeout_ms = 500\n";

/// The `[[templates]]` array as the configuration reads it.
fn templates(place: &str) -> Result<TemplateSources, Box<dyn std::error::Error>> {
    #[derive(serde::Deserialize)]
    struct Wrap {
        templates: TemplateSources,
    }
    let wrapped: Wrap = toml::from_str(&format!("[[templates]]\n{place}"))?;
    Ok(wrapped.templates)
}

/// A gated exchange's `[auth]` with Lys and an app credential, with or
/// without its own templates.
fn lys_auth(templates: &str) -> Result<AuthConfig, Box<dyn std::error::Error>> {
    Ok(toml::from_str(&format!(
        "token = \"s3cr3t\"\n[lys]\nissuer = \"https://lys.test\"\nnetwork_id = \"n\"\n\
         keys_path = \"/k\"\napp_credential_path = \"/c\"\n{templates}"
    ))?)
}

#[test]
fn an_open_exchange_with_top_level_templates_validates() -> Result<(), Box<dyn std::error::Error>> {
    let mut config = sample_config()?;
    config.auth = None;
    config.cluster = None;
    config.templates = templates(ECHO)?;
    let message = config_validation_message(validate(&mut config, None));
    assert_eq!(message, "", "an open exchange with [[templates]] validates");
    assert_eq!(config.template_sources().len(), 1);
    assert_eq!(config.templates_place(), "templates");
    Ok(())
}

#[test]
fn templates_in_both_places_are_refused_by_the_exact_words()
-> Result<(), Box<dyn std::error::Error>> {
    let mut config = sample_config()?;
    config.auth = Some(lys_auth(&format!("[[lys.templates]]\n{ECHO}"))?);
    config.templates = templates(ECHO)?;
    let message = config_validation_message(validate(&mut config, None));
    assert!(
        message.lines().any(|line| line == BOTH),
        "missing {BOTH:?} in {message:?}"
    );
    Ok(())
}

#[test]
fn a_gated_exchange_with_top_level_templates_alone_validates()
-> Result<(), Box<dyn std::error::Error>> {
    let mut config = sample_config()?;
    config.auth = Some(lys_auth("")?);
    config.templates = templates(ECHO)?;
    let message = config_validation_message(validate(&mut config, None));
    assert_eq!(
        message, "",
        "a gated exchange with [[templates]] alone validates"
    );
    assert_eq!(config.template_sources().len(), 1);
    assert_eq!(config.templates_place(), "templates");
    Ok(())
}

#[test]
fn an_open_exchange_with_top_level_templates_needs_a_store()
-> Result<(), Box<dyn std::error::Error>> {
    let mut config = sample_config()?;
    config.auth = None;
    config.cluster = None;
    config.persistence_path = None;
    config.templates = templates(ECHO)?;
    let message = config_validation_message(validate(&mut config, None));
    let expected = "templates: a service registry needs persistence_path to be kept";
    assert!(
        message.lines().any(|line| line == expected),
        "missing {expected:?} in {message:?}"
    );
    Ok(())
}

#[test]
fn a_token_only_exchange_with_templates_is_told_where_services_are_placed()
-> Result<(), Box<dyn std::error::Error>> {
    let mut config = sample_config()?;
    config.auth = Some(toml::from_str("token = \"s3cr3t\"\n")?);
    config.templates = templates(ECHO)?;
    let message = config_validation_message(validate(&mut config, None));
    let expected =
        "templates: a gated exchange places services in Lys, write [auth.lys] or remove [auth]";
    assert!(
        message.lines().any(|line| line == expected),
        "missing {expected:?} in {message:?}"
    );
    Ok(())
}

#[test]
fn the_lys_place_still_answers_when_the_top_level_is_empty()
-> Result<(), Box<dyn std::error::Error>> {
    let mut config = sample_config()?;
    config.auth = Some(lys_auth(&format!("[[lys.templates]]\n{ECHO}"))?);
    let message = config_validation_message(validate(&mut config, None));
    assert_eq!(message, "");
    assert_eq!(config.template_sources().len(), 1);
    assert_eq!(config.templates_place(), "auth.lys.templates");
    Ok(())
}