Skip to main content

ironflow_store/memory/
audit_log_store.rs

1//! [`AuditLogStore`] trait implementation for [`InMemoryStore`].
2
3use 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}