mempill_core/application/
audit.rs1#![allow(missing_docs)]
2use std::sync::Arc;
7
8use mempill_types::TransactionTime;
9
10use crate::{
11 engine::audit_ledger::{self, AuditQuery},
12 error::MemError,
13 ports::PersistencePort,
14};
15
16use super::dto::{AuditQueryRequest, AuditQueryResponse};
17
18pub struct AuditUseCase<P>
20where
21 P: PersistencePort + Send + Sync + 'static,
22{
23 persistence: Arc<P>,
24}
25
26impl<P> AuditUseCase<P>
27where
28 P: PersistencePort + Send + Sync + 'static,
29{
30 pub fn new(persistence: Arc<P>) -> Self {
31 Self { persistence }
32 }
33
34 pub fn execute(&self, req: AuditQueryRequest) -> Result<AuditQueryResponse, MemError> {
36 let from_tx_time = req.from_tx_time.map(TransactionTime);
38
39 let query = AuditQuery {
40 agent_id: req.agent_id,
41 claim_ref: req.claim_ref,
42 from_tx_time,
43 limit: req.limit,
44 };
45
46 let result = audit_ledger::query_ledger(&*self.persistence, &query)
47 .map_err(|e| MemError::Persistence { source: Box::new(e) })?;
48
49 Ok(AuditQueryResponse { entries: result.entries })
50 }
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56 use crate::ports::persistence::Txn;
57 use mempill_types::{
58 AgentId, Claim, ClaimEdge, ClaimRef, Disposition, LedgerEntry, LedgerEventKind,
59 TransactionTime, ValidityAssertion,
60 };
61 use std::sync::Mutex;
62 use chrono::Utc;
63
64 struct MockTxn(AgentId);
65 impl Txn for MockTxn {
66 fn agent_id(&self) -> &AgentId { &self.0 }
67 }
68
69 #[derive(Debug, thiserror::Error)]
70 #[error("mock")]
71 struct MockErr;
72
73 struct MockStore {
74 ledger: Mutex<Vec<LedgerEntry>>,
75 }
76
77 impl MockStore {
78 fn with_entries(entries: Vec<LedgerEntry>) -> Self {
79 Self { ledger: Mutex::new(entries) }
80 }
81 }
82
83 impl PersistencePort for MockStore {
84 type Transaction = MockTxn;
85 type Error = MockErr;
86 fn begin_atomic(&self, aid: &AgentId) -> Result<MockTxn, MockErr> { Ok(MockTxn(aid.clone())) }
87 fn append_claim(&self, _t: &mut MockTxn, c: &Claim) -> Result<ClaimRef, MockErr> { Ok(c.claim_ref().clone()) }
88 fn append_validity_assertion(&self, _t: &mut MockTxn, _a: &ValidityAssertion) -> Result<(), MockErr> { Ok(()) }
89 fn append_ledger_entry(&self, _t: &mut MockTxn, _e: &LedgerEntry) -> Result<(), MockErr> { Ok(()) }
90 fn append_claim_edge(&self, _t: &mut MockTxn, _e: &ClaimEdge) -> Result<(), MockErr> { Ok(()) }
91 fn commit(&self, _t: MockTxn) -> Result<(), MockErr> { Ok(()) }
92 fn rollback(&self, _t: MockTxn) -> Result<(), MockErr> { Ok(()) }
93 fn load_subject_line(&self, _a: &AgentId, _s: &str, _p: &str) -> Result<Vec<Claim>, MockErr> { Ok(vec![]) }
94 fn load_claim(&self, _a: &AgentId, _r: &ClaimRef) -> Result<Option<Claim>, MockErr> { Ok(None) }
95 fn load_validity_assertions_for(&self, _a: &AgentId, _r: &ClaimRef) -> Result<Vec<ValidityAssertion>, MockErr> { Ok(vec![]) }
96 fn load_ledger(&self, _a: &AgentId, _f: Option<&TransactionTime>, limit: usize) -> Result<Vec<LedgerEntry>, MockErr> {
97 let mut entries = self.ledger.lock().unwrap().clone();
99 entries.reverse(); entries.truncate(limit);
101 Ok(entries)
102 }
103 fn load_ledger_for_claims(&self, _a: &AgentId, _refs: &[ClaimRef]) -> Result<Vec<LedgerEntry>, MockErr> { Ok(vec![]) }
104 fn load_edges_for(&self, _a: &AgentId, _r: &ClaimRef) -> Result<Vec<ClaimEdge>, MockErr> { Ok(vec![]) }
105 fn load_injected_claims(&self, _a: &AgentId) -> Result<Vec<ClaimRef>, MockErr> { Ok(vec![]) }
106 fn load_lineage(&self, _a: &AgentId, _r: &ClaimRef) -> Result<Vec<ClaimEdge>, MockErr> { Ok(vec![]) }
107 }
108
109 fn make_entry(agent_id: &AgentId, at: chrono::DateTime<Utc>) -> LedgerEntry {
110 LedgerEntry {
111 entry_id: uuid::Uuid::new_v4(),
112 agent_id: agent_id.clone(),
113 claim_ref: ClaimRef::new_random(),
114 event_kind: LedgerEventKind::ClaimCommitted,
115 disposition: Disposition::CommittedCheap,
116 rationale: None,
117 recorded_at: TransactionTime(at),
118 }
119 }
120
121 #[test]
122 fn audit_empty_store_returns_empty() {
123 let store = Arc::new(MockStore::with_entries(vec![]));
124 let uc = AuditUseCase::new(Arc::clone(&store));
125 let resp = uc.execute(AuditQueryRequest {
126 agent_id: AgentId("a".into()),
127 claim_ref: None,
128 from_tx_time: None,
129 limit: 100,
130 }).unwrap();
131 assert!(resp.entries.is_empty());
132 }
133
134 #[test]
135 fn audit_returns_entries_in_chronological_asc_order() {
136 let agent = AgentId("a".into());
137 let t1 = chrono::Utc::now();
138 let t2 = t1 + chrono::Duration::seconds(10);
139 let e1 = make_entry(&agent, t1);
140 let e2 = make_entry(&agent, t2);
141 let store = Arc::new(MockStore::with_entries(vec![e1.clone(), e2.clone()]));
143 let uc = AuditUseCase::new(Arc::clone(&store));
144 let resp = uc.execute(AuditQueryRequest {
145 agent_id: agent,
146 claim_ref: None,
147 from_tx_time: None,
148 limit: 100,
149 }).unwrap();
150 assert_eq!(resp.entries.len(), 2);
151 assert!(resp.entries[0].recorded_at.0 <= resp.entries[1].recorded_at.0,
153 "entries must be in chronological order (ASC)");
154 }
155}