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::sync::Mutex;

use lys_pass::Error;

use super::{Door, Issuer, SignedIn, Started};

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

const NOW: u64 = 1_000;

/// A double of lys-pass's sign-in helper. It answers one sign-in per code,
/// refuses a browser cookie that is not the one it started, and refreshes
/// only while `reachable` holds.
struct Lys {
    reachable: Mutex<bool>,
    refreshed: Mutex<u32>,
}

impl Lys {
    const fn new() -> Self {
        Self {
            reachable: Mutex::new(true),
            refreshed: Mutex::new(0),
        }
    }

    fn cut(&self) -> Result<(), Box<dyn std::error::Error>> {
        *self.reachable.lock().map_err(|_| "poisoned")? = false;
        Ok(())
    }
}

impl Issuer for Lys {
    fn start(&self) -> Result<Started, Error> {
        Ok(Started {
            url: "https://lys.example/oauth/authorize?state=s1".to_owned(),
            state: "s1".to_owned(),
            verifier: "v1".to_owned(),
            browser: "b1".to_owned(),
        })
    }

    fn finish(
        &self,
        code: &str,
        _state: &str,
        browser: &str,
        started: &Started,
    ) -> Result<SignedIn, Error> {
        if browser != started.browser {
            return Err(Error::Invalid("the state was started in another browser"));
        }
        Ok(SignedIn {
            token: format!("pass-for-{code}"),
            holder: "clinician".to_owned(),
            expires_at: NOW + 600,
            refresh: Some("refresh-1".to_owned()),
        })
    }

    fn refresh(&self, _refresh: &str) -> Result<SignedIn, Error> {
        if !*self
            .reachable
            .lock()
            .map_err(|_| Error::CannotAsk("poisoned"))?
        {
            return Err(Error::CannotAsk("the double is unreachable"));
        }
        let mut count = self
            .refreshed
            .lock()
            .map_err(|_| Error::CannotAsk("poisoned"))?;
        *count += 1;
        let token = format!("refreshed-{count}");
        drop(count);
        Ok(SignedIn {
            token,
            holder: "clinician".to_owned(),
            expires_at: NOW + 1_200,
            refresh: Some("refresh-2".to_owned()),
        })
    }
}

/// The browser holds only an opaque session id: the pass stays server-side.
#[test]
fn a_sign_in_keeps_the_pass_server_side() -> TestResult {
    let door = Door::new(Lys::new());
    let begun = door.begin(NOW)?;
    assert_eq!(begun.browser, "b1");
    let session = door.callback("c1", "s1", "b1", NOW)?;
    assert!(!session.contains("pass"), "the session id carries the pass");
    assert_eq!(session.len(), 64);
    assert_eq!(door.pass(&session, NOW)?, "pass-for-c1");
    Ok(())
}

/// A state is used once, and only from the browser that started it.
#[test]
fn a_state_is_used_once_and_only_by_its_browser() -> TestResult {
    let door = Door::new(Lys::new());
    door.begin(NOW)?;
    let other = door
        .callback("c1", "s1", "b2", NOW)
        .err()
        .ok_or("another browser finished the sign-in")?;
    assert_eq!(other.code, "contract_refused");
    let again = door
        .callback("c1", "s1", "b1", NOW)
        .err()
        .ok_or("a state was used twice")?;
    assert_eq!(again.code, "sign_in_state_unknown");
    Ok(())
}

/// R2: a pass about to expire is refreshed. With Lys unreachable a pass
/// still valid is carried, and an expired one is refused by name rather than
/// carried or dropped silently.
#[test]
fn a_pass_is_refreshed_before_it_expires_and_refused_once_lys_cannot_be_asked() -> TestResult {
    let lys = Lys::new();
    let door = Door::new(lys);
    door.begin(NOW)?;
    let session = door.callback("c1", "s1", "b1", NOW)?;
    assert_eq!(door.pass(&session, NOW + 100)?, "pass-for-c1");
    assert_eq!(door.pass(&session, NOW + 590)?, "refreshed-1");
    door.issuer().cut()?;
    assert_eq!(door.pass(&session, NOW + 1_190)?, "refreshed-1");
    let refused = door
        .pass(&session, NOW + 1_200)
        .err()
        .ok_or("an expired pass was carried")?;
    assert_eq!(refused.code, "lys_could_not_be_asked");
    Ok(())
}

/// A signed-out session is forgotten with its pass: it carries nothing
/// afterwards, and a second sign-out finds nothing to forget.
#[test]
fn a_signed_out_session_is_forgotten() -> TestResult {
    let door = Door::new(Lys::new());
    door.begin(NOW)?;
    let session = door.callback("c1", "s1", "b1", NOW)?;
    assert!(door.sign_out(&session)?, "the session was not held");
    let refused = door
        .pass(&session, NOW)
        .err()
        .ok_or("a signed-out session still carried its pass")?;
    assert_eq!(refused.code, "session_unknown");
    assert!(!door.sign_out(&session)?, "a session was forgotten twice");
    Ok(())
}

#[test]
fn an_unknown_session_is_refused() {
    let door = Door::new(Lys::new());
    let refused = door.pass("nobody", NOW).err();
    assert_eq!(
        refused.map(|failure| failure.code),
        Some("session_unknown".to_owned())
    );
}