//! The pending-approval table: whoever takes an entry answers it, and everyone else finds it //! gone. Do not edit. #[path = "support/build.rs"] mod build; use std::sync::{Arc, Barrier}; use brokerd::approvals::{Table, Verdict}; use brokerd::policy::{Ask, Outcome, SessionState, decide}; use build::{grant, now, read, set, ts}; use proto::{CallId, DataClass, DenyReason, Mode, PendingApproval, SessionId, Timestamp}; fn ask() -> Ask { let grants = set(vec![grant("n", "read_file", Mode::Ask).paths(&["/n"])]); match decide(read("/n/a"), &grants, SessionState::default(), now()) { Outcome::Ask(ask) => ask, other => panic!("the test's call does not ask: {other:?}"), } } fn info(approval: u64, expires: &str) -> PendingApproval { PendingApproval { approval, session: SessionId::new("s1").unwrap(), call: CallId(approval + 100), tool: "read_file".to_string(), arguments: r#"{"path":"/n/a"}"#.to_string(), grant: "n".to_string(), taint: DataClass::Private, created: now(), expires: ts(expires), } } const LATER: &str = "2026-09-18T12:15:00.000Z"; #[test] fn a_new_table_is_empty_and_lists_in_id_order() { let table = Table::new(); assert_eq!(table.list(), []); let _a = table.insert(info(7, LATER), ask()); let _b = table.insert(info(3, LATER), ask()); assert_eq!(table.list(), [info(3, LATER), info(7, LATER)]); } #[test] fn an_entry_can_be_taken_once() { let table = Table::new(); let _rx = table.insert(info(5, LATER), ask()); let entry = table.take(5).expect("the entry is there"); assert_eq!(entry.info, info(5, LATER)); assert_eq!(entry.ask.grant(), "n"); assert!(table.take(5).is_none(), "taken twice"); assert_eq!(table.list(), []); } #[test] fn an_id_never_added_is_not_there() { let table = Table::new(); let _rx = table.insert(info(5, LATER), ask()); assert!(table.take(6).is_none()); assert_eq!(table.list().len(), 1); } #[test] fn the_verdict_goes_to_the_waiting_side() { let table = Table::new(); let rx = table.insert(info(5, LATER), ask()); let entry = table.take(5).unwrap(); entry .reply .send(Verdict::Denied(DenyReason::ApprovalRefused)) .unwrap(); match rx.recv().unwrap() { Verdict::Denied(reason) => assert_eq!(reason, DenyReason::ApprovalRefused), Verdict::Run(_) => panic!("the verdict changed on the way"), } } #[test] fn an_entry_expires_at_its_expiry_and_not_before() { let table = Table::new(); let _a = table.insert(info(9, "2026-09-18T12:00:01.000Z"), ask()); let _b = table.insert(info(2, "2026-09-18T12:00:00.500Z"), ask()); let _c = table.insert(info(4, LATER), ask()); let before = ts("2026-09-18T12:00:00.499Z"); assert!(table.take_expired(before).is_empty()); // Exactly at `expires` is expired. let at = ts("2026-09-18T12:00:00.500Z"); let due: Vec = table .take_expired(at) .iter() .map(|e| e.info.approval) .collect(); assert_eq!(due, [2]); let after = ts("2026-09-18T13:00:00.000Z"); let due: Vec = table .take_expired(after) .iter() .map(|e| e.info.approval) .collect(); assert_eq!(due, [4, 9], "in id order"); assert_eq!(table.list(), []); assert!(table.take_expired(Timestamp::MAX).is_empty()); } #[test] fn two_takers_at_once_one_gets_it() { for round in 0..100 { let table = Arc::new(Table::new()); let _rx = table.insert(info(1, LATER), ask()); let start = Arc::new(Barrier::new(2)); let takers: Vec<_> = (0..2) .map(|_| { let table = Arc::clone(&table); let start = Arc::clone(&start); std::thread::spawn(move || { start.wait(); table.take(1).is_some() }) }) .collect(); let got: Vec = takers.into_iter().map(|t| t.join().unwrap()).collect(); assert_eq!( got.iter().filter(|g| **g).count(), 1, "round {round}: {got:?}" ); } } #[test] fn a_taker_and_the_expiry_at_once_one_gets_it() { for round in 0..100 { let table = Arc::new(Table::new()); let _rx = table.insert(info(1, "2026-09-18T12:00:00.000Z"), ask()); let start = Arc::new(Barrier::new(2)); let t = { let (table, start) = (Arc::clone(&table), Arc::clone(&start)); std::thread::spawn(move || { start.wait(); usize::from(table.take(1).is_some()) }) }; let e = { let (table, start) = (Arc::clone(&table), Arc::clone(&start)); std::thread::spawn(move || { start.wait(); table.take_expired(now()).len() }) }; let total = t.join().unwrap() + e.join().unwrap(); assert_eq!(total, 1, "round {round}"); } }