haematite-cli 0.21.1

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
//! ACCESS-001 R2: a served database is put behind Lys by its configuration,
//! and a Lys that cannot be asked at start never holds the start: the
//! service listens and refuses every decided act by name until Lys's keys
//! are read, never serving undecided.

use std::net::TcpListener;
use std::sync::mpsc::channel;
use std::time::Duration;

use super::{LysConfig, lys_gate};
use crate::serve::pass::LYS_KEYS_PENDING;

type TestResult = Result<(), Box<dyn std::error::Error>>;

fn closed_origin() -> Result<String, std::io::Error> {
    let listener = TcpListener::bind("127.0.0.1:0")?;
    let port = listener.local_addr()?.port();
    drop(listener);
    Ok(format!("http://127.0.0.1:{port}/"))
}

#[test]
fn the_lys_section_names_its_issuer_and_database_and_nothing_else() -> TestResult {
    let config: LysConfig =
        serde_json::from_str(r#"{"issuer": "https://lys.example", "database": "wards"}"#)?;
    assert_eq!(
        (config.issuer.as_str(), config.database.as_str()),
        ("https://lys.example", "wards")
    );
    assert!(
        serde_json::from_str::<LysConfig>(
            r#"{"issuer": "https://lys.example", "database": "wards", "writer": "everyone"}"#
        )
        .is_err(),
        "a member granting anything was accepted"
    );
    Ok(())
}

/// A Lys that cannot be asked at start leaves the service listening, every
/// decided act refused by name until its keys are read.
#[test]
fn a_lys_that_cannot_be_asked_at_start_leaves_every_decided_act_refused_by_name() -> TestResult {
    let config = LysConfig {
        issuer: closed_origin()?,
        database: "wards".to_owned(),
        placement_credential_file: None,
    };
    let gate = lys_gate(&config)?;
    let refused = gate
        .present("a-pass", 0)
        .err()
        .ok_or("a pass was verified with no published keys")?;
    assert_eq!(refused.code, LYS_KEYS_PENDING);
    Ok(())
}

/// A Lys that takes the connection and never answers does not hold the
/// start: the gate is made at once, whatever Lys does.
#[test]
fn start_does_not_wait_on_a_lys_that_never_answers() -> TestResult {
    let silent = TcpListener::bind("127.0.0.1:0")?;
    let config = LysConfig {
        issuer: format!("http://{}/", silent.local_addr()?),
        database: "wards".to_owned(),
        placement_credential_file: None,
    };
    let (made, gate) = channel();
    std::thread::spawn(move || drop(made.send(lys_gate(&config).map(drop))));
    gate.recv_timeout(Duration::from_secs(5))
        .map_err(|_| "the gate waited on a Lys that never answers")??;
    drop(silent);
    Ok(())
}

/// A database id is one placed name: `.` separates a database from what it
/// holds, so an id carrying one would name another resource's children, and
/// Lys takes no `/` in any id.
#[test]
fn a_database_id_that_is_empty_or_placed_is_refused_before_lys_is_asked() -> TestResult {
    for database in ["", "wards/patients", "wards.patients"] {
        let config = LysConfig {
            issuer: closed_origin()?,
            database: database.to_owned(),
            placement_credential_file: None,
        };
        let refused = lys_gate(&config)
            .err()
            .ok_or("an invalid database id was served")?;
        assert!(
            refused.starts_with("lys database id"),
            "{database:?} refused as {refused}"
        );
    }
    Ok(())
}