liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
//! SRV-039 R1 and R2: a server with Lys and no grant stream asks for its root
//! once with the app credential, and says why when it has nothing to ask with.

use super::root_presence::LysRoot;
use super::root_presence_start::{RootQuestion, RootSlot, root_question};
use super::root_presence_tests::{
    Capture, Lines, NONE_HOLDS, ONE_HOLDS, TestResult, issuer, runtime,
};
use std::net::SocketAddr;
use std::sync::Arc;
use tracing_subscriber::prelude::*;

/// `[auth.lys]` with no grant stream, asking the Lys at `api` with the
/// credential in `credential`, or with no credential when it is `None`.
fn lys(
    api: &str,
    credential: Option<&std::path::Path>,
) -> TestResult<crate::config::types::LysConfig> {
    let credential = credential.map_or_else(String::new, |path| {
        format!("app_credential_path = {:?}\n", path.display().to_string())
    });
    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\n[[channels]]\nname = \"a\"\ndurable = false\n\n\
         [auth]\ntoken = \"t\"\n\n[auth.lys]\nissuer = \"https://lys.test\"\n\
         network_id = \"ablative\"\nkeys_path = \"/k\"\napi_url = \"{api}\"\n{credential}"
    );
    let config: crate::config::types::ServerConfig = toml::from_str(&text)?;
    Ok(config
        .auth
        .and_then(|auth| auth.lys)
        .ok_or("no [auth.lys]")?)
}

/// What `/health` reports for the root: `lys_root` and `lys_root_cause`.
type Reported = (&'static str, Option<&'static str>);

/// Asks the start question of the fake Lys at `at` once, as the server does at
/// start, and answers what `/health` then reports with the levels logged.
fn started(at: SocketAddr) -> TestResult<(Reported, Vec<tracing::Level>)> {
    let dir = tempfile::tempdir()?;
    let path = dir.path().join("app-credential");
    std::fs::write(&path, "fixture-app-credential\n")?;
    let runtime = runtime()?;
    let config = lys(&format!("http://{at}/api/"), Some(&path))?;
    let RootQuestion::AtStart(root) = root_question(&config, Some(runtime.handle().clone()))?
    else {
        return Err("a server with no grant stream and a credential asks at start".into());
    };
    assert_eq!(root.answer(), LysRoot::NotAsked);
    let lines = Arc::new(Lines::default());
    let subscriber = tracing_subscriber::registry().with(Capture(Arc::clone(&lines)));
    tracing::subscriber::with_default(subscriber, || root.ask());
    let levels = lines
        .0
        .lock()
        .map_err(|_| "captured lines poisoned")?
        .iter()
        .map(|(level, _)| *level)
        .collect();
    Ok((RootSlot::Asked(root).health(), levels))
}

#[test]
fn a_server_without_a_grant_stream_reports_an_absent_root() -> TestResult {
    let (at, asks) = issuer(vec![(200, NONE_HOLDS)])?;
    let (health, levels) = started(at)?;
    assert_eq!(health, ("absent", None));
    assert!(asks.try_recv()?.starts_with("POST /api/grants/held "));
    assert!(asks.try_recv().is_err(), "exactly one held ask");
    assert_eq!(levels, vec![tracing::Level::ERROR]);
    Ok(())
}

#[test]
fn a_server_without_a_grant_stream_reports_a_held_root_silently() -> TestResult {
    let (at, asks) = issuer(vec![(200, ONE_HOLDS)])?;
    let (health, levels) = started(at)?;
    assert_eq!(health, ("held", None));
    assert!(asks.try_recv().is_ok());
    assert!(asks.try_recv().is_err(), "exactly one held ask");
    assert!(levels.is_empty(), "a held root logs nothing: {levels:?}");
    Ok(())
}

#[test]
fn a_server_with_a_grant_stream_installs_one_root_question() -> TestResult {
    let template = crate::link::certificate::setup::DEVICE_TEMPLATE
        .replace("LYS_HOST", "issuer.example")
        .replace("BROKER_HOST", "broker.example")
        .replace("BROKER_IP", "127.0.0.1")
        .replace("RELAY_HOST", "relay.example")
        .replace("OFFICE_ED25519_PUBLIC_KEY_64_HEX", &"01".repeat(32))
        .replace("ADMISSION_RESOURCE_KIND", "liminal.network")
        .replace("ADMISSION_RESOURCE_ID", "office")
        .replace("ADMISSION_ACTION", "call");
    let config: crate::config::types::ServerConfig = toml::from_str(&template)?;
    let lys = config
        .auth
        .and_then(|auth| auth.lys)
        .ok_or("no [auth.lys]")?;
    assert!(lys.grant_stream.is_some());
    let runtime = runtime()?;
    assert!(matches!(
        root_question(&lys, Some(runtime.handle().clone()))?,
        RootQuestion::Owner
    ));
    Ok(())
}

#[test]
fn health_names_an_unasked_root_with_its_cause() -> TestResult {
    let runtime = runtime()?;
    let question = root_question(
        &lys("https://lys.invalid/api/", None)?,
        Some(runtime.handle().clone()),
    )?;
    let RootQuestion::Unasked(cause) = question else {
        return Err("a server with no credential cannot ask".into());
    };
    assert_eq!(cause, "no_asking_credential");
    assert_eq!(
        RootSlot::Unasked { cause }.health(),
        ("not_asked", Some("no_asking_credential"))
    );
    Ok(())
}