haematite-cli 0.21.0

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
use std::net::TcpListener;
use std::path::Path;

use super::{LysSignIn, read_secret};
use crate::door::config::Lys;

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

const CREDENTIAL: &str = "issued-credential-never-shown";

fn lys(issuer: &str, secret_file: &Path) -> Lys {
    Lys {
        issuer: issuer.to_owned(),
        client_id: "lys-client.haematite".to_owned(),
        secret_file: secret_file.to_path_buf(),
        redirect: "https://studio.example/callback".to_owned(),
    }
}

/// A credential written by hand ends with a newline; it is read as its one
/// line, and a second line is refused without showing either.
#[test]
fn a_credential_file_is_read_as_its_one_line() -> TestResult {
    let directory = tempfile::tempdir()?;
    let path = directory.path().join("secret");
    std::fs::write(&path, format!("{CREDENTIAL}\n"))?;
    read_secret(&path)?;
    std::fs::write(&path, format!("{CREDENTIAL}\nsecond\n"))?;
    let refused = read_secret(&path)
        .err()
        .ok_or("a two-line credential was taken")?;
    assert!(refused.contains("not one line"), "{refused}");
    assert!(
        !refused.contains(CREDENTIAL),
        "the credential was shown: {refused}"
    );
    std::fs::write(&path, "x".repeat(5000))?;
    assert!(
        read_secret(&path).is_err(),
        "an over-long credential was taken"
    );
    Ok(())
}

/// A missing credential file is named by its path.
#[test]
fn a_missing_credential_file_is_named() -> TestResult {
    let directory = tempfile::tempdir()?;
    let path = directory.path().join("absent");
    let refused = read_secret(&path).err().ok_or("an absent file was read")?;
    assert!(refused.contains(&path.display().to_string()), "{refused}");
    Ok(())
}

/// With Lys unreachable the door does not start, and says so by name.
#[test]
fn an_unreachable_issuer_is_lys_could_not_be_asked() -> TestResult {
    let directory = tempfile::tempdir()?;
    let path = directory.path().join("secret");
    std::fs::write(&path, CREDENTIAL)?;
    let listener = TcpListener::bind("127.0.0.1:0")?;
    let port = listener.local_addr()?.port();
    drop(listener);
    let refused = LysSignIn::connect(&lys(&format!("http://127.0.0.1:{port}/"), &path))
        .err()
        .ok_or("an unreachable Lys was connected")?;
    assert!(refused.starts_with("lys_could_not_be_asked"), "{refused}");
    assert!(
        !refused.contains(CREDENTIAL),
        "the credential was shown: {refused}"
    );
    Ok(())
}