Skip to main content

cratefield_module_guardrails/
fakes.rs

1//! Fakes for every port in this crate, public so tests and other crates can
2//! wire a full engine with no I/O. Each is a cheap cloneable handle over a
3//! shared interior, in the style of `cratefield_testing`'s fakes.
4//!
5//! Interior mutability here records test observations; it is not request
6//! state (ADR 0007) — the scoped `Mutex` allow follows the policy in the
7//! workspace `clippy.toml`.
8
9#![allow(clippy::disallowed_types)]
10// Every accessor locks an unpoisoned fixture mutex; per-method `# Panics`
11// sections would add noise without information.
12#![allow(clippy::missing_panics_doc)]
13
14use async_trait::async_trait;
15use std::collections::BTreeMap;
16use std::sync::{Arc, Mutex};
17use time::OffsetDateTime;
18
19use crate::action::{Chain, Request, TokenKey};
20use crate::audit::{AuditEntry, AuditError, AuditSink};
21use crate::caps::{LedgerError, Prices, PricesError, SpendEntry, SpendLedger};
22use crate::hygiene::{Allowance, AllowanceError, AllowanceSource};
23use crate::kill::{KillSwitch, KillSwitchError, Scope};
24use crate::simulate::{SimError, SimulationReport, Simulator};
25use crate::{SignError, WalletSigner};
26
27// ---------------------------------------------------------------------------
28// MemoryKillSwitch
29
30/// A kill switch in memory. Engagements are a set of scope keys.
31#[derive(Clone, Default)]
32pub struct MemoryKillSwitch {
33    engaged: Arc<Mutex<BTreeMap<String, String>>>,
34}
35
36impl MemoryKillSwitch {
37    /// An unengaged switch.
38    #[must_use]
39    pub fn new() -> Self {
40        Self::default()
41    }
42
43    /// The reason each engaged scope was engaged with, keyed by scope key.
44    #[must_use]
45    pub fn engagements(&self) -> BTreeMap<String, String> {
46        self.engaged.lock().expect("unpoisoned").clone()
47    }
48}
49
50#[async_trait]
51impl KillSwitch for MemoryKillSwitch {
52    async fn engaged(&self, scope: &Scope) -> Result<bool, KillSwitchError> {
53        Ok(self
54            .engaged
55            .lock()
56            .expect("unpoisoned")
57            .contains_key(&scope.key()))
58    }
59
60    async fn engage(&self, scope: Scope, reason: &str) -> Result<(), KillSwitchError> {
61        self.engaged
62            .lock()
63            .expect("unpoisoned")
64            .insert(scope.key(), reason.to_owned());
65        Ok(())
66    }
67
68    async fn release(&self, scope: &Scope) -> Result<(), KillSwitchError> {
69        self.engaged
70            .lock()
71            .expect("unpoisoned")
72            .remove(&scope.key());
73        Ok(())
74    }
75}
76
77// ---------------------------------------------------------------------------
78// ScriptedSimulator
79
80/// What a [`ScriptedSimulator`] answers with.
81#[derive(Debug, Clone)]
82pub enum Script {
83    /// The configured report.
84    Report(SimulationReport),
85    /// The configured error.
86    Fail(SimError),
87}
88
89/// A simulator that answers every call with the configured script and
90/// records the requests it was asked about.
91#[derive(Clone, Default)]
92pub struct ScriptedSimulator {
93    inner: Arc<Mutex<ScriptedSim>>,
94}
95
96#[derive(Default)]
97struct ScriptedSim {
98    script: Option<Script>,
99    calls: Vec<Request>,
100}
101
102impl ScriptedSimulator {
103    /// A simulator with no script: it errors like an unreachable endpoint
104    /// until a script is set.
105    #[must_use]
106    pub fn new() -> Self {
107        Self::default()
108    }
109
110    /// Answers every simulation with `report`.
111    #[must_use]
112    pub fn respond(self, report: SimulationReport) -> Self {
113        self.set(Script::Report(report));
114        self
115    }
116
117    /// Fails every simulation with `error`.
118    #[must_use]
119    pub fn fail(self, error: SimError) -> Self {
120        self.set(Script::Fail(error));
121        self
122    }
123
124    /// Answers with a clean report: success, nothing observed.
125    #[must_use]
126    pub fn clean(self) -> Self {
127        self.respond(SimulationReport::clean())
128    }
129
130    fn set(&self, script: Script) {
131        self.inner.lock().expect("unpoisoned").script = Some(script);
132    }
133
134    /// The requests it has been asked about, in order.
135    #[must_use]
136    pub fn calls(&self) -> Vec<Request> {
137        self.inner.lock().expect("unpoisoned").calls.clone()
138    }
139}
140
141#[async_trait]
142impl Simulator for ScriptedSimulator {
143    async fn simulate(&self, request: &Request) -> Result<SimulationReport, SimError> {
144        let mut inner = self.inner.lock().expect("unpoisoned");
145        inner.calls.push(request.clone());
146        match &inner.script {
147            Some(Script::Report(report)) => Ok(report.clone()),
148            Some(Script::Fail(error)) => Err(error.clone()),
149            None => Err(SimError::Unavailable("no script configured".to_owned())),
150        }
151    }
152}
153
154// ---------------------------------------------------------------------------
155// FixedPrices
156
157/// A price oracle with a fixed table: token key to micro-USD per smallest
158/// unit. A token missing from the table has no quote.
159#[derive(Clone, Default)]
160pub struct FixedPrices {
161    table: Arc<Mutex<BTreeMap<TokenKey, u128>>>,
162}
163
164impl FixedPrices {
165    /// An empty table.
166    #[must_use]
167    pub fn new() -> Self {
168        Self::default()
169    }
170
171    /// Prices one token at `micros_per_unit` micro-USD per smallest unit.
172    #[must_use]
173    pub fn price(self, key: TokenKey, micros_per_unit: u128) -> Self {
174        self.table
175            .lock()
176            .expect("unpoisoned")
177            .insert(key, micros_per_unit);
178        self
179    }
180
181    /// Prices the native coin of `chain`.
182    #[must_use]
183    pub fn native(self, chain: Chain, micros_per_unit: u128) -> Self {
184        self.price(TokenKey::native(chain), micros_per_unit)
185    }
186}
187
188#[async_trait]
189impl Prices for FixedPrices {
190    async fn micro_usd(
191        &self,
192        chain: &Chain,
193        token: &str,
194        raw_amount: u128,
195    ) -> Result<Option<u128>, PricesError> {
196        let key = TokenKey {
197            chain: chain.clone(),
198            token: token.to_owned(),
199        };
200        Ok(self
201            .table
202            .lock()
203            .expect("unpoisoned")
204            .get(&key)
205            .map(|per| per.saturating_mul(raw_amount)))
206    }
207}
208
209// ---------------------------------------------------------------------------
210// MemoryLedger
211
212/// A spend ledger in memory: an append-only list of entries.
213#[derive(Clone, Default)]
214pub struct MemoryLedger {
215    entries: Arc<Mutex<Vec<RecordedSpend>>>,
216}
217
218/// One recorded entry, with its subject and instant.
219#[derive(Debug, Clone, PartialEq, Eq)]
220pub struct RecordedSpend {
221    /// The subject that spent.
222    pub subject: String,
223    /// The token and amount.
224    pub entry: SpendEntry,
225    /// When it was recorded.
226    pub at: OffsetDateTime,
227}
228
229impl MemoryLedger {
230    /// An empty ledger.
231    #[must_use]
232    pub fn new() -> Self {
233        Self::default()
234    }
235
236    /// Every recorded entry, in order.
237    #[must_use]
238    pub fn recorded(&self) -> Vec<RecordedSpend> {
239        self.entries.lock().expect("unpoisoned").clone()
240    }
241}
242
243#[async_trait]
244impl SpendLedger for MemoryLedger {
245    async fn spent(
246        &self,
247        subject: &str,
248        token: &TokenKey,
249        since: OffsetDateTime,
250    ) -> Result<u128, LedgerError> {
251        let entries = self.entries.lock().expect("unpoisoned");
252        Ok(entries
253            .iter()
254            .filter(|r| r.subject == subject && r.entry.key == *token && r.at >= since)
255            .fold(0_u128, |acc, r| acc.saturating_add(r.entry.micro_usd)))
256    }
257
258    async fn record(
259        &self,
260        subject: &str,
261        spend: &[SpendEntry],
262        at: OffsetDateTime,
263    ) -> Result<(), LedgerError> {
264        let mut entries = self.entries.lock().expect("unpoisoned");
265        for entry in spend {
266            entries.push(RecordedSpend {
267                subject: subject.to_owned(),
268                entry: entry.clone(),
269                at,
270            });
271        }
272        Ok(())
273    }
274}
275
276// ---------------------------------------------------------------------------
277// MemoryAudit
278
279/// An audit sink in memory, inspectable by the test.
280#[derive(Clone, Default)]
281pub struct MemoryAudit {
282    entries: Arc<Mutex<Vec<AuditEntry>>>,
283}
284
285impl MemoryAudit {
286    /// An empty sink.
287    #[must_use]
288    pub fn new() -> Self {
289        Self::default()
290    }
291
292    /// Every recorded entry, in order.
293    #[must_use]
294    pub fn entries(&self) -> Vec<AuditEntry> {
295        self.entries.lock().expect("unpoisoned").clone()
296    }
297
298    /// How many entries are recorded.
299    #[must_use]
300    pub fn len(&self) -> usize {
301        self.entries.lock().expect("unpoisoned").len()
302    }
303
304    /// Whether nothing has been recorded.
305    #[must_use]
306    pub fn is_empty(&self) -> bool {
307        self.len() == 0
308    }
309}
310
311#[async_trait]
312impl AuditSink for MemoryAudit {
313    async fn record(&self, entry: &AuditEntry) -> Result<(), AuditError> {
314        self.entries.lock().expect("unpoisoned").push(entry.clone());
315        Ok(())
316    }
317}
318
319// ---------------------------------------------------------------------------
320// MemoryAllowances
321
322/// An allowance source with a fixed list.
323#[derive(Clone, Default)]
324pub struct MemoryAllowances {
325    allowances: Arc<Mutex<Vec<Allowance>>>,
326}
327
328impl MemoryAllowances {
329    /// No open allowances.
330    #[must_use]
331    pub fn new() -> Self {
332        Self::default()
333    }
334
335    /// Serves `allowances` for every (chain, owner).
336    #[must_use]
337    pub fn with(self, allowances: Vec<Allowance>) -> Self {
338        *self.allowances.lock().expect("unpoisoned") = allowances;
339        self
340    }
341}
342
343#[async_trait]
344impl AllowanceSource for MemoryAllowances {
345    async fn open_allowances(
346        &self,
347        _chain: &Chain,
348        _owner: &str,
349    ) -> Result<Vec<Allowance>, AllowanceError> {
350        Ok(self.allowances.lock().expect("unpoisoned").clone())
351    }
352}
353
354// ---------------------------------------------------------------------------
355// RecordingSigner
356
357/// What a [`RecordingSigner`] does when asked to sign.
358#[derive(Debug, Clone)]
359pub enum SignScript {
360    /// Returns the bytes.
361    Sign(Vec<u8>),
362    /// Returns the error, with this message.
363    Fail(String),
364}
365
366/// A wallet signer that records every request it is asked to sign and
367/// answers with the configured script.
368#[derive(Clone, Default)]
369pub struct RecordingSigner {
370    inner: Arc<Mutex<RecordedSigning>>,
371}
372
373#[derive(Default)]
374struct RecordedSigning {
375    script: Option<SignScript>,
376    signed: Vec<Request>,
377}
378
379impl RecordingSigner {
380    /// Signs to `signature`.
381    #[must_use]
382    pub fn signing(self, signature: Vec<u8>) -> Self {
383        self.set(SignScript::Sign(signature));
384        self
385    }
386
387    /// Fails with `message`.
388    #[must_use]
389    pub fn failing(self, message: &str) -> Self {
390        self.set(SignScript::Fail(message.to_owned()));
391        self
392    }
393
394    fn set(&self, script: SignScript) {
395        let mut inner = self.inner.lock().expect("unpoisoned");
396        inner.script = Some(script);
397    }
398
399    /// Every request it has been asked to sign, in order.
400    #[must_use]
401    pub fn signed(&self) -> Vec<Request> {
402        self.inner.lock().expect("unpoisoned").signed.clone()
403    }
404}
405
406#[async_trait]
407impl WalletSigner for RecordingSigner {
408    async fn sign(&self, request: &Request) -> Result<Vec<u8>, SignError> {
409        let mut inner = self.inner.lock().expect("unpoisoned");
410        inner.signed.push(request.clone());
411        match &inner.script {
412            Some(SignScript::Sign(bytes)) => Ok(bytes.clone()),
413            Some(SignScript::Fail(message)) => Err(SignError(message.clone())),
414            None => Err(SignError("no script configured".to_owned())),
415        }
416    }
417}
418
419// ---------------------------------------------------------------------------
420// FailingAudit
421
422/// An audit sink that always errors, for the fail-closed test.
423#[derive(Clone, Copy, Debug, Default)]
424pub struct FailingAudit;
425
426#[async_trait]
427impl AuditSink for FailingAudit {
428    async fn record(&self, _entry: &AuditEntry) -> Result<(), AuditError> {
429        Err(AuditError("sink down".to_owned()))
430    }
431}