1use chrono::Utc;
4use uuid::Uuid;
5
6use crate::audit_log_store::AuditLogStore;
7use crate::entities::{AuditLogEntry, AuditLogFilter, NewAuditLogEntry, Page};
8use crate::store::StoreFuture;
9
10use super::InMemoryStore;
11
12impl AuditLogStore for InMemoryStore {
13 fn append_audit_log(&self, entry: NewAuditLogEntry) -> StoreFuture<'_, AuditLogEntry> {
14 Box::pin(async move {
15 let now = Utc::now();
16 let audit_entry = AuditLogEntry {
17 id: Uuid::now_v7(),
18 event_type: entry.event_type,
19 payload: entry.payload,
20 run_id: entry.run_id,
21 step_id: entry.step_id,
22 user_id: entry.user_id,
23 created_at: now,
24 };
25
26 let mut state = self.state.write().await;
27 state.audit_logs.push(audit_entry.clone());
28 Ok(audit_entry)
29 })
30 }
31
32 fn list_audit_logs(
33 &self,
34 filter: AuditLogFilter,
35 page: u32,
36 per_page: u32,
37 ) -> StoreFuture<'_, Page<AuditLogEntry>> {
38 Box::pin(async move {
39 let state = self.state.read().await;
40
41 let filtered: Vec<&AuditLogEntry> = state
42 .audit_logs
43 .iter()
44 .filter(|e| {
45 if let Some(event_type) = filter.event_type
46 && e.event_type != event_type
47 {
48 return false;
49 }
50 if let Some(run_id) = filter.run_id
51 && e.run_id != Some(run_id)
52 {
53 return false;
54 }
55 if let Some(from) = filter.from
56 && e.created_at < from
57 {
58 return false;
59 }
60 if let Some(to) = filter.to
61 && e.created_at > to
62 {
63 return false;
64 }
65 true
66 })
67 .collect();
68
69 let total = filtered.len() as u64;
70
71 let offset = (page.saturating_sub(1) as usize) * (per_page as usize);
72 let items: Vec<AuditLogEntry> = filtered
73 .into_iter()
74 .rev()
75 .skip(offset)
76 .take(per_page as usize)
77 .cloned()
78 .collect();
79
80 Ok(Page {
81 items,
82 total,
83 page,
84 per_page,
85 })
86 })
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use serde_json::json;
93 use uuid::Uuid;
94
95 use crate::audit_log_store::AuditLogStore;
96 use crate::entities::{AuditLogFilter, EventKind, NewAuditLogEntry};
97 use crate::memory::InMemoryStore;
98
99 fn new_entry(event_type: EventKind, run_id: Option<Uuid>) -> NewAuditLogEntry {
100 NewAuditLogEntry {
101 event_type,
102 payload: json!({"test": true}),
103 run_id,
104 step_id: None,
105 user_id: None,
106 }
107 }
108
109 #[tokio::test]
110 async fn append_returns_entry_with_id() {
111 let store = InMemoryStore::new();
112 let entry = store
113 .append_audit_log(new_entry(EventKind::RunCreated, None))
114 .await
115 .unwrap();
116
117 assert_eq!(entry.event_type, EventKind::RunCreated);
118 assert!(!entry.id.is_nil());
119 }
120
121 #[tokio::test]
122 async fn list_empty_store_returns_empty_page() {
123 let store = InMemoryStore::new();
124 let page = store
125 .list_audit_logs(AuditLogFilter::default(), 1, 20)
126 .await
127 .unwrap();
128
129 assert!(page.items.is_empty());
130 assert_eq!(page.total, 0);
131 }
132
133 #[tokio::test]
134 async fn list_returns_all_entries() {
135 let store = InMemoryStore::new();
136 store
137 .append_audit_log(new_entry(EventKind::RunCreated, None))
138 .await
139 .unwrap();
140 store
141 .append_audit_log(new_entry(EventKind::RunFailed, None))
142 .await
143 .unwrap();
144
145 let page = store
146 .list_audit_logs(AuditLogFilter::default(), 1, 20)
147 .await
148 .unwrap();
149
150 assert_eq!(page.items.len(), 2);
151 assert_eq!(page.total, 2);
152 }
153
154 #[tokio::test]
155 async fn list_newest_first() {
156 let store = InMemoryStore::new();
157 store
158 .append_audit_log(new_entry(EventKind::RunCreated, None))
159 .await
160 .unwrap();
161 store
162 .append_audit_log(new_entry(EventKind::RunFailed, None))
163 .await
164 .unwrap();
165
166 let page = store
167 .list_audit_logs(AuditLogFilter::default(), 1, 20)
168 .await
169 .unwrap();
170
171 assert_eq!(page.items[0].event_type, EventKind::RunFailed);
172 assert_eq!(page.items[1].event_type, EventKind::RunCreated);
173 }
174
175 #[tokio::test]
176 async fn list_filters_by_event_type() {
177 let store = InMemoryStore::new();
178 store
179 .append_audit_log(new_entry(EventKind::RunCreated, None))
180 .await
181 .unwrap();
182 store
183 .append_audit_log(new_entry(EventKind::RunFailed, None))
184 .await
185 .unwrap();
186 store
187 .append_audit_log(new_entry(EventKind::RunCreated, None))
188 .await
189 .unwrap();
190
191 let filter = AuditLogFilter {
192 event_type: Some(EventKind::RunCreated),
193 ..AuditLogFilter::default()
194 };
195 let page = store.list_audit_logs(filter, 1, 20).await.unwrap();
196
197 assert_eq!(page.items.len(), 2);
198 assert_eq!(page.total, 2);
199 assert!(
200 page.items
201 .iter()
202 .all(|e| e.event_type == EventKind::RunCreated)
203 );
204 }
205
206 #[tokio::test]
207 async fn list_filters_by_run_id() {
208 let store = InMemoryStore::new();
209 let target_run = Uuid::now_v7();
210
211 store
212 .append_audit_log(new_entry(EventKind::RunCreated, Some(target_run)))
213 .await
214 .unwrap();
215 store
216 .append_audit_log(new_entry(EventKind::RunCreated, Some(Uuid::now_v7())))
217 .await
218 .unwrap();
219
220 let filter = AuditLogFilter {
221 run_id: Some(target_run),
222 ..AuditLogFilter::default()
223 };
224 let page = store.list_audit_logs(filter, 1, 20).await.unwrap();
225
226 assert_eq!(page.items.len(), 1);
227 assert_eq!(page.items[0].run_id, Some(target_run));
228 }
229
230 #[tokio::test]
231 async fn list_paginates_correctly() {
232 let store = InMemoryStore::new();
233 let kinds = [
234 EventKind::RunCreated,
235 EventKind::RunFailed,
236 EventKind::StepCompleted,
237 EventKind::StepFailed,
238 EventKind::UserSignedIn,
239 ];
240 for kind in kinds {
241 store.append_audit_log(new_entry(kind, None)).await.unwrap();
242 }
243
244 let page1 = store
245 .list_audit_logs(AuditLogFilter::default(), 1, 2)
246 .await
247 .unwrap();
248 assert_eq!(page1.items.len(), 2);
249 assert_eq!(page1.total, 5);
250 assert_eq!(page1.page, 1);
251
252 let page2 = store
253 .list_audit_logs(AuditLogFilter::default(), 2, 2)
254 .await
255 .unwrap();
256 assert_eq!(page2.items.len(), 2);
257
258 let page3 = store
259 .list_audit_logs(AuditLogFilter::default(), 3, 2)
260 .await
261 .unwrap();
262 assert_eq!(page3.items.len(), 1);
263 }
264
265 #[tokio::test]
266 async fn list_filters_by_date_range() {
267 let store = InMemoryStore::new();
268 let entry1 = store
269 .append_audit_log(new_entry(EventKind::RunCreated, None))
270 .await
271 .unwrap();
272
273 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
274
275 let midpoint = chrono::Utc::now();
276
277 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
278
279 let entry2 = store
280 .append_audit_log(new_entry(EventKind::RunFailed, None))
281 .await
282 .unwrap();
283
284 let filter_from = AuditLogFilter {
285 from: Some(midpoint),
286 ..AuditLogFilter::default()
287 };
288 let page = store.list_audit_logs(filter_from, 1, 20).await.unwrap();
289 assert_eq!(page.items.len(), 1);
290 assert_eq!(page.items[0].id, entry2.id);
291
292 let filter_to = AuditLogFilter {
293 to: Some(midpoint),
294 ..AuditLogFilter::default()
295 };
296 let page = store.list_audit_logs(filter_to, 1, 20).await.unwrap();
297 assert_eq!(page.items.len(), 1);
298 assert_eq!(page.items[0].id, entry1.id);
299 }
300
301 #[tokio::test]
302 async fn append_preserves_all_fields() {
303 let store = InMemoryStore::new();
304 let run_id = Uuid::now_v7();
305 let step_id = Uuid::now_v7();
306 let user_id = Uuid::now_v7();
307
308 let entry = store
309 .append_audit_log(NewAuditLogEntry {
310 event_type: EventKind::StepCompleted,
311 payload: json!({"cost": 0.42}),
312 run_id: Some(run_id),
313 step_id: Some(step_id),
314 user_id: Some(user_id),
315 })
316 .await
317 .unwrap();
318
319 assert_eq!(entry.event_type, EventKind::StepCompleted);
320 assert_eq!(entry.run_id, Some(run_id));
321 assert_eq!(entry.step_id, Some(step_id));
322 assert_eq!(entry.user_id, Some(user_id));
323 assert_eq!(entry.payload, json!({"cost": 0.42}));
324 }
325}