haematite-cli 0.21.7

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
use super::{Site, answer};
use crate::door::{
    config::SignInBounds,
    http::Request,
    packed::Packed,
    signin::{Door, Issuer, SignedIn, Started},
};
use lys_pass::Error;
use std::collections::BTreeMap;
use std::num::NonZeroU64;
use std::path::PathBuf;

type TestResult = Result<(), Box<dyn std::error::Error>>;
struct ShortSession;
impl Issuer for ShortSession {
    fn bounds(&self) -> SignInBounds {
        SignInBounds {
            session_seconds: NonZeroU64::MIN.saturating_add(1),
            ..SignInBounds::default()
        }
    }
    fn start(&self) -> Result<Started, Error> {
        Ok(Started {
            url: "https://lys.example".into(),
            state: "state".into(),
            browser: "browser".into(),
            verifier: "verifier".into(),
        })
    }
    fn finish(
        &self,
        _code: &str,
        _state: &str,
        browser: &str,
        started: &Started,
    ) -> Result<SignedIn, Error> {
        if browser != started.browser {
            return Err(Error::Invalid("fixture browser mismatch"));
        }
        Ok(SignedIn {
            token: "fixture".into(),
            holder: "fixture".into(),
            expires_at: 1600,
            refresh: Some("refresh".into()),
        })
    }
    fn refresh(&self, _refresh: &str) -> Result<SignedIn, Error> {
        Err(Error::CannotAsk("fixture"))
    }
}

#[test]
fn session_cookie_uses_the_configured_session_lifetime() -> TestResult {
    let door = Door::new(ShortSession);
    door.begin(1000)?;
    let request = Request {
        method: "GET".into(),
        path: "/callback".into(),
        query: "code=code&state=state".into(),
        headers: vec![("cookie".into(), "haem_state=browser".into())],
        body: Vec::new(),
    };
    let site = Site {
        origin: "https://studio.example".into(),
        files: PathBuf::from("/unused"),
        stores: BTreeMap::new(),
        timeout: std::time::Duration::from_secs(1),
        packed: Packed::default(),
        store_children: BTreeMap::new(),
    };
    let mut bytes = Vec::new();
    answer(&door, &site, &request, 1000).write(&mut bytes)?;
    let response = String::from_utf8(bytes)?;
    let cookie = response
        .lines()
        .find(|line| line.starts_with("Set-Cookie: haem_session="))
        .ok_or("session cookie missing")?;
    let lifetime = cookie
        .rsplit_once("Max-Age=")
        .ok_or("cookie lifetime missing")?
        .1;
    assert_eq!(lifetime, "2", "cookie outlived configured session");
    Ok(())
}