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
//! A gate listens before Lys's keys are read: every decided act is refused
//! as `lys_keys_pending` until they arrive, and decided by them after.

use std::sync::mpsc::{Receiver, channel};
use std::sync::{Arc, Mutex, PoisonError};
use std::time::{Duration, Instant};

use lys_pass::{Error, KeySet};

use super::tests::{ISSUER, NOW, keys, pass, role};
use super::{Gate, LYS_KEYS_PENDING, Unreachable};

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

/// Long enough for the reading thread to be scheduled on a loaded machine.
const ARRIVAL: Duration = Duration::from_secs(20);

/// A fetch answering each ask with the next answer sent to it, and failing
/// by name once the sender is gone.
fn answering(answers: Receiver<Result<KeySet, Error>>) -> impl Fn() -> Result<KeySet, Error> {
    let answers = Mutex::new(answers);
    move || {
        answers
            .lock()
            .unwrap_or_else(PoisonError::into_inner)
            .recv()
            .unwrap_or(Err(Error::CannotAsk("the test sends no more answers")))
    }
}

#[test]
fn every_decided_act_is_refused_as_pending_until_the_keys_arrive_and_decided_after() -> TestResult {
    let (send, answers) = channel();
    let gate = Gate::fetching(answering(answers), ISSUER, "wards", Arc::new(Unreachable))?;
    let reader = pass("reader", &role(&["read"], "reader-grant"));
    let pending = gate
        .present(&reader, NOW)
        .err()
        .ok_or("a pass was verified before any keys were read")?;
    assert_eq!(pending.code, LYS_KEYS_PENDING);

    send.send(Err(Error::CannotAsk("Lys is not answering yet")))?;
    send.send(Ok(keys()?))?;
    let started = Instant::now();
    let presented = loop {
        match gate.present(&reader, NOW) {
            Ok(presented) => break presented,
            Err(failure) if failure.code == LYS_KEYS_PENDING && started.elapsed() < ARRIVAL => {
                std::thread::sleep(Duration::from_millis(10));
            }
            Err(failure) => return Err(format!("refused as {}", failure.code).into()),
        }
    };
    assert_eq!(presented["holder"], "reader");
    Ok(())
}