1use std::fs::{File, OpenOptions};
7use std::io::{BufRead, BufReader, Write};
8use std::path::PathBuf;
9
10use crate::error::{EnvelopeError, EnvelopeResult};
11
12use super::entry::AuditEntry;
13
14pub struct AuditLogger {
19 log_path: PathBuf,
21}
22
23impl AuditLogger {
24 pub fn new(log_path: PathBuf) -> Self {
26 Self { log_path }
27 }
28
29 pub fn log(&self, entry: &AuditEntry) -> EnvelopeResult<()> {
34 let mut file = OpenOptions::new()
35 .create(true)
36 .append(true)
37 .open(&self.log_path)
38 .map_err(|e| EnvelopeError::Io(format!("Failed to open audit log: {}", e)))?;
39
40 let json = serde_json::to_string(entry)
41 .map_err(|e| EnvelopeError::Json(format!("Failed to serialize audit entry: {}", e)))?;
42
43 writeln!(file, "{}", json)
44 .map_err(|e| EnvelopeError::Io(format!("Failed to write audit entry: {}", e)))?;
45
46 file.flush()
47 .map_err(|e| EnvelopeError::Io(format!("Failed to flush audit log: {}", e)))?;
48
49 Ok(())
50 }
51
52 pub fn log_batch(&self, entries: &[AuditEntry]) -> EnvelopeResult<()> {
56 if entries.is_empty() {
57 return Ok(());
58 }
59
60 let mut file = OpenOptions::new()
61 .create(true)
62 .append(true)
63 .open(&self.log_path)
64 .map_err(|e| EnvelopeError::Io(format!("Failed to open audit log: {}", e)))?;
65
66 for entry in entries {
67 let json = serde_json::to_string(entry).map_err(|e| {
68 EnvelopeError::Json(format!("Failed to serialize audit entry: {}", e))
69 })?;
70
71 writeln!(file, "{}", json)
72 .map_err(|e| EnvelopeError::Io(format!("Failed to write audit entry: {}", e)))?;
73 }
74
75 file.flush()
76 .map_err(|e| EnvelopeError::Io(format!("Failed to flush audit log: {}", e)))?;
77
78 Ok(())
79 }
80
81 pub fn read_all(&self) -> EnvelopeResult<Vec<AuditEntry>> {
85 if !self.log_path.exists() {
86 return Ok(Vec::new());
87 }
88
89 let file = File::open(&self.log_path)
90 .map_err(|e| EnvelopeError::Io(format!("Failed to open audit log: {}", e)))?;
91
92 let reader = BufReader::new(file);
93 let mut entries = Vec::new();
94
95 for (line_num, line) in reader.lines().enumerate() {
96 let line = line.map_err(|e| {
97 EnvelopeError::Io(format!(
98 "Failed to read audit log line {}: {}",
99 line_num + 1,
100 e
101 ))
102 })?;
103
104 if line.trim().is_empty() {
106 continue;
107 }
108
109 let entry: AuditEntry = serde_json::from_str(&line).map_err(|e| {
110 EnvelopeError::Json(format!(
111 "Failed to parse audit entry at line {}: {}",
112 line_num + 1,
113 e
114 ))
115 })?;
116
117 entries.push(entry);
118 }
119
120 Ok(entries)
121 }
122
123 pub fn read_recent(&self, count: usize) -> EnvelopeResult<Vec<AuditEntry>> {
125 let all_entries = self.read_all()?;
126 let start = all_entries.len().saturating_sub(count);
127 Ok(all_entries[start..].to_vec())
128 }
129
130 pub fn entry_count(&self) -> EnvelopeResult<usize> {
132 if !self.log_path.exists() {
133 return Ok(0);
134 }
135
136 let file = File::open(&self.log_path)
137 .map_err(|e| EnvelopeError::Io(format!("Failed to open audit log: {}", e)))?;
138
139 let reader = BufReader::new(file);
140 let count = reader.lines().filter(|l| l.is_ok()).count();
141
142 Ok(count)
143 }
144
145 pub fn exists(&self) -> bool {
147 self.log_path.exists()
148 }
149
150 pub fn path(&self) -> &PathBuf {
152 &self.log_path
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use crate::audit::entry::{EntityType, Operation};
160 use serde_json::json;
161 use tempfile::TempDir;
162
163 fn create_test_logger() -> (AuditLogger, TempDir) {
164 let temp_dir = TempDir::new().unwrap();
165 let log_path = temp_dir.path().join("audit.log");
166 let logger = AuditLogger::new(log_path);
167 (logger, temp_dir)
168 }
169
170 fn create_test_entry() -> AuditEntry {
171 AuditEntry::create(
172 EntityType::Account,
173 "acc-12345678",
174 Some("Test Account".to_string()),
175 &json!({"name": "Test Account", "balance": 1000}),
176 )
177 }
178
179 #[test]
180 fn test_log_and_read() {
181 let (logger, _temp) = create_test_logger();
182 let entry = create_test_entry();
183
184 logger.log(&entry).unwrap();
186
187 let entries = logger.read_all().unwrap();
189 assert_eq!(entries.len(), 1);
190 assert_eq!(entries[0].operation, Operation::Create);
191 assert_eq!(entries[0].entity_type, EntityType::Account);
192 }
193
194 #[test]
195 fn test_multiple_entries() {
196 let (logger, _temp) = create_test_logger();
197
198 for i in 0..5 {
200 let entry = AuditEntry::create(
201 EntityType::Account,
202 format!("acc-{}", i),
203 Some(format!("Account {}", i)),
204 &json!({"name": format!("Account {}", i)}),
205 );
206 logger.log(&entry).unwrap();
207 }
208
209 assert_eq!(logger.entry_count().unwrap(), 5);
211
212 let entries = logger.read_all().unwrap();
214 assert_eq!(entries.len(), 5);
215 }
216
217 #[test]
218 fn test_log_batch() {
219 let (logger, _temp) = create_test_logger();
220
221 let entries: Vec<AuditEntry> = (0..3)
222 .map(|i| {
223 AuditEntry::create(
224 EntityType::Account,
225 format!("acc-{}", i),
226 None,
227 &json!({"id": i}),
228 )
229 })
230 .collect();
231
232 logger.log_batch(&entries).unwrap();
233
234 let read_entries = logger.read_all().unwrap();
235 assert_eq!(read_entries.len(), 3);
236 }
237
238 #[test]
239 fn test_read_recent() {
240 let (logger, _temp) = create_test_logger();
241
242 for i in 0..10 {
244 let entry = AuditEntry::create(
245 EntityType::Account,
246 format!("acc-{}", i),
247 None,
248 &json!({"index": i}),
249 );
250 logger.log(&entry).unwrap();
251 }
252
253 let recent = logger.read_recent(3).unwrap();
255 assert_eq!(recent.len(), 3);
256 assert_eq!(recent[0].entity_id, "acc-7");
257 assert_eq!(recent[1].entity_id, "acc-8");
258 assert_eq!(recent[2].entity_id, "acc-9");
259 }
260
261 #[test]
262 fn test_empty_log() {
263 let (logger, _temp) = create_test_logger();
264
265 assert!(!logger.exists());
266 assert_eq!(logger.entry_count().unwrap(), 0);
267 assert!(logger.read_all().unwrap().is_empty());
268 }
269
270 #[test]
271 fn test_update_entry_logged() {
272 let (logger, _temp) = create_test_logger();
273
274 let before = json!({"name": "Old Name", "balance": 100});
275 let after = json!({"name": "New Name", "balance": 100});
276
277 let entry = AuditEntry::update(
278 EntityType::Account,
279 "acc-12345678",
280 Some("Account".to_string()),
281 &before,
282 &after,
283 Some("name: \"Old Name\" -> \"New Name\"".to_string()),
284 );
285
286 logger.log(&entry).unwrap();
287
288 let entries = logger.read_all().unwrap();
289 assert_eq!(entries.len(), 1);
290 assert_eq!(entries[0].operation, Operation::Update);
291 assert!(entries[0].before.is_some());
292 assert!(entries[0].after.is_some());
293 }
294
295 #[test]
296 fn test_delete_entry_logged() {
297 let (logger, _temp) = create_test_logger();
298
299 let entity = json!({"name": "Deleted Account"});
300 let entry = AuditEntry::delete(
301 EntityType::Account,
302 "acc-12345678",
303 Some("Deleted Account".to_string()),
304 &entity,
305 );
306
307 logger.log(&entry).unwrap();
308
309 let entries = logger.read_all().unwrap();
310 assert_eq!(entries.len(), 1);
311 assert_eq!(entries[0].operation, Operation::Delete);
312 assert!(entries[0].before.is_some());
313 assert!(entries[0].after.is_none());
314 }
315
316 #[test]
317 fn test_survives_crash_simulation() {
318 let (logger, temp) = create_test_logger();
319
320 let entry = create_test_entry();
322 logger.log(&entry).unwrap();
323
324 let logger2 = AuditLogger::new(temp.path().join("audit.log"));
326
327 let entries = logger2.read_all().unwrap();
329 assert_eq!(entries.len(), 1);
330 }
331}