Skip to main content

aa_core/storage/
conformance.rs

1//! Reusable trait-conformance harness for driver crates.
2//!
3//! Downstream backend crates (Epic B: Postgres, Redis, memory; Epic E: the
4//! Enterprise gateway driver) import this module to prove their implementation
5//! honors the trait contract, exercising it through a `&dyn` reference so
6//! object-safety is checked at the same time.
7//!
8//! The harness functions panic on the first violated invariant, so they are
9//! meant to be called from within a `#[test]` (driven by the caller's own async
10//! runtime).
11
12use super::{
13    AgentId, AuditEntry, AuditSink, CredentialStore, LifecycleStore, PolicyStore, RateLimitCounter, SessionRecord,
14    SessionStore, StorageError,
15};
16
17/// Assert that a [`PolicyStore`] implementation honors the trait contract.
18///
19/// Drives `store` through `&dyn PolicyStore` and checks:
20///
21/// - `get_policy(present)` resolves to a policy
22/// - `get_policy(absent)` returns [`StorageError::NotFound`]
23/// - `invalidate(present)` succeeds
24/// - `invalidate(absent)` succeeds (invalidation is idempotent)
25///
26/// `present` must be an agent the store has a policy for; `absent` must be one it
27/// does not. Panics with a descriptive message on the first violated invariant.
28pub async fn assert_policy_store_conformance(store: &dyn PolicyStore, present: &AgentId, absent: &AgentId) {
29    store
30        .get_policy(present)
31        .await
32        .expect("get_policy(present) should resolve to a policy");
33
34    match store.get_policy(absent).await {
35        Err(StorageError::NotFound(_)) => {}
36        other => panic!("get_policy(absent) should return NotFound, got {other:?}"),
37    }
38
39    store
40        .invalidate(present)
41        .await
42        .expect("invalidate(present) should succeed");
43
44    store
45        .invalidate(absent)
46        .await
47        .expect("invalidate(absent) should be idempotent");
48}
49
50/// Assert that an [`AuditSink`] implementation honors the trait contract.
51///
52/// Drives `sink` through `&dyn AuditSink` and checks that emitting an entry
53/// succeeds — the sink is append-only, so a successful `emit` is the only
54/// observable invariant the trait guarantees. Panics on failure.
55pub async fn assert_audit_sink_conformance(sink: &dyn AuditSink, event: AuditEntry) {
56    sink.emit(event).await.expect("emit should persist the entry");
57}
58
59/// Assert that a [`SessionStore`] implementation honors the trait contract.
60///
61/// Drives `store` through `&dyn SessionStore` and checks:
62///
63/// - `save` then `load` round-trips the record unchanged
64/// - `delete` removes it, after which `load` returns [`StorageError::NotFound`]
65/// - a second `delete` of the now-absent id still succeeds (idempotent)
66///
67/// `record` must use a session id the store does not already hold. Panics with a
68/// descriptive message on the first violated invariant.
69pub async fn assert_session_store_conformance(store: &dyn SessionStore, record: SessionRecord) {
70    let id = record.session_id;
71
72    store.save(record.clone()).await.expect("save(new) should succeed");
73
74    let loaded = store.load(&id).await.expect("load(present) should return the record");
75    assert_eq!(loaded, record, "loaded record should equal the saved record");
76
77    store.delete(&id).await.expect("delete(present) should succeed");
78
79    match store.load(&id).await {
80        Err(StorageError::NotFound(_)) => {}
81        other => panic!("load(deleted) should return NotFound, got {other:?}"),
82    }
83
84    store.delete(&id).await.expect("delete(absent) should be idempotent");
85}
86
87/// Assert that a [`CredentialStore`] implementation honors the trait contract.
88///
89/// Drives `store` through `&dyn CredentialStore` and checks:
90///
91/// - `put_secret` then `get_secret` round-trips the bytes unchanged
92/// - `delete_secret` removes it, after which `get_secret` returns [`StorageError::NotFound`]
93/// - a second `delete_secret` of the now-absent key still succeeds (idempotent)
94///
95/// `key` must be one the store does not already hold. Panics with a descriptive
96/// message on the first violated invariant.
97pub async fn assert_credential_store_conformance(store: &dyn CredentialStore, key: &str, value: Vec<u8>) {
98    store
99        .put_secret(key, value.clone())
100        .await
101        .expect("put_secret(new) should succeed");
102
103    let got = store
104        .get_secret(key)
105        .await
106        .expect("get_secret(present) should return the value");
107    assert_eq!(got, value, "round-tripped secret bytes should match");
108
109    store
110        .delete_secret(key)
111        .await
112        .expect("delete_secret(present) should succeed");
113
114    match store.get_secret(key).await {
115        Err(StorageError::NotFound(_)) => {}
116        other => panic!("get_secret(deleted) should return NotFound, got {other:?}"),
117    }
118
119    store
120        .delete_secret(key)
121        .await
122        .expect("delete_secret(absent) should be idempotent");
123}
124
125/// Assert that a [`RateLimitCounter`] implementation honors the trait contract.
126///
127/// Drives `counter` through `&dyn RateLimitCounter` and checks:
128///
129/// - a fresh key reads `0`
130/// - `increment` returns the accumulating window total
131/// - `current` reflects the accumulated total without modifying it
132/// - `reset` returns the counter to `0` and is idempotent
133///
134/// `key` must be one the counter has never seen. A long window is used so the
135/// assertions never race a window rollover. Panics on the first violated
136/// invariant.
137pub async fn assert_rate_limit_counter_conformance(counter: &dyn RateLimitCounter, key: &str) {
138    const WINDOW_SECS: u64 = 3600;
139
140    assert_eq!(
141        counter.current(key).await.expect("current(fresh) should succeed"),
142        0,
143        "a key that was never incremented reads 0"
144    );
145
146    assert_eq!(
147        counter
148            .increment(key, 5, WINDOW_SECS)
149            .await
150            .expect("increment should succeed"),
151        5,
152        "first increment returns the amount added"
153    );
154
155    assert_eq!(
156        counter
157            .increment(key, 3, WINDOW_SECS)
158            .await
159            .expect("increment should succeed"),
160        8,
161        "second increment accumulates within the window"
162    );
163
164    assert_eq!(
165        counter.current(key).await.expect("current should succeed"),
166        8,
167        "current reflects the accumulated total"
168    );
169
170    counter.reset(key).await.expect("reset should succeed");
171
172    assert_eq!(
173        counter.current(key).await.expect("current after reset should succeed"),
174        0,
175        "reset returns the counter to 0"
176    );
177
178    counter.reset(key).await.expect("reset(absent) should be idempotent");
179}
180
181/// Assert that a [`LifecycleStore`] implementation honors the trait contract.
182///
183/// Drives `store` through `&dyn LifecycleStore` and checks:
184///
185/// - `register` then `heartbeat` succeeds for a registered agent
186/// - `heartbeat` on an unregistered agent returns [`StorageError::NotFound`]
187/// - `deregister` removes the registration, after which `heartbeat` returns
188///   [`StorageError::NotFound`]
189/// - `deregister` is idempotent for both registered and absent agents
190///
191/// `present` is registered and deregistered by the harness; `absent` must be an
192/// agent the store never holds. Panics on the first violated invariant.
193pub async fn assert_lifecycle_store_conformance(store: &dyn LifecycleStore, present: &AgentId, absent: &AgentId) {
194    store.register(present).await.expect("register should succeed");
195
196    store
197        .heartbeat(present)
198        .await
199        .expect("heartbeat(registered) should succeed");
200
201    match store.heartbeat(absent).await {
202        Err(StorageError::NotFound(_)) => {}
203        other => panic!("heartbeat(unregistered) should return NotFound, got {other:?}"),
204    }
205
206    store
207        .deregister(present)
208        .await
209        .expect("deregister(registered) should succeed");
210
211    match store.heartbeat(present).await {
212        Err(StorageError::NotFound(_)) => {}
213        other => panic!("heartbeat(deregistered) should return NotFound, got {other:?}"),
214    }
215
216    store
217        .deregister(present)
218        .await
219        .expect("deregister(present) should be idempotent");
220
221    store
222        .deregister(absent)
223        .await
224        .expect("deregister(absent) should be idempotent");
225}