//! Audit writer edge cases found in the M3a review: a log with no complete record, a file whose //! name only looks like a log, and the lock file outliving the writer. #[path = "support/audit_dir.rs"] mod audit_dir; use audit_dir::{D1, D2, TempDir, denied, lines, ts}; use brokerd::audit::{Writer, verify_dir}; use proto::{AuditRecord, Hash32}; /// A kill between creating the day's file and writing its first record leaves it empty. It must /// open like an empty log, not panic. #[test] fn one_zero_length_log_file_opens_as_an_empty_log() { let dir = TempDir::unmade("zero-length"); std::fs::create_dir_all(&dir.path).unwrap(); std::fs::write(dir.path.join(D1), b"").unwrap(); let opened = Writer::open(&dir.path, false).unwrap(); assert!(!opened.recovered); let mut writer = opened.writer; assert_eq!(writer.next_seq(), 0); writer .append(ts("2026-09-17T08:00:00.000Z"), denied(1)) .unwrap(); let record: AuditRecord = serde_json::from_str(&lines(&dir.path, D1)[0]).unwrap(); assert_eq!((record.seq, record.prev), (0, Hash32::ZERO)); let report = verify_dir(&dir.path).unwrap(); assert_eq!(report.failure, None); assert_eq!(report.records, 1); } /// The same crash, a day later: the older file holds records and the new one is empty. #[test] fn a_zero_length_file_after_records_continues_the_chain() { let dir = TempDir::unmade("zero-length-later"); let mut writer = Writer::open(&dir.path, false).unwrap().writer; writer .append(ts("2026-09-17T08:00:00.000Z"), denied(1)) .unwrap(); drop(writer); std::fs::write(dir.path.join(D2), b"").unwrap(); let mut writer = Writer::open(&dir.path, false).unwrap().writer; assert_eq!(writer.next_seq(), 1); writer .append(ts("2026-09-18T08:00:00.000Z"), denied(2)) .unwrap(); let report = verify_dir(&dir.path).unwrap(); assert_eq!(report.failure, None); assert_eq!(report.records, 2); } /// A crash part-way through the very first record: one file, one torn line, nothing else. #[test] fn a_single_torn_first_record_is_recovered() { let dir = TempDir::unmade("torn-only"); std::fs::create_dir_all(&dir.path).unwrap(); std::fs::write(dir.path.join(D1), br#"{"seq":0,"time":"2026-09-17T08:"#).unwrap(); let opened = Writer::open(&dir.path, false).unwrap(); assert!(opened.recovered); let report = verify_dir(&dir.path).unwrap(); assert_eq!(report.failure, None); assert_eq!(report.recoveries.len(), 1); } /// `2026-0x-19.jsonl` is not a log file, for `brokerd` as for `bxctl audit verify`: it is neither /// verified nor written to, and the real log goes on in its own files. #[test] fn a_file_that_only_looks_like_a_log_is_not_part_of_it() { let dir = TempDir::case("good", None); let odd = dir.path.join("2026-0x-19.jsonl"); std::fs::write(&odd, b"not a record\n").unwrap(); let mut writer = Writer::open(&dir.path, false) .expect("a stray file does not break the chain") .writer; writer .append(ts("2026-09-18T09:00:00.000Z"), denied(9)) .unwrap(); assert_eq!( std::fs::read(&odd).unwrap(), b"not a record\n", "never written" ); let report = verify_dir(&dir.path).unwrap(); assert_eq!(report.failure, None); assert_eq!( report.records, 11, "the ten fixture records and the new one" ); } /// The lock file is never removed: a `brokerd` holding the old file open could otherwise lock it /// while a new one creates and locks a fresh file, and both would write. #[test] fn the_lock_file_outlives_the_writer() { let dir = TempDir::unmade("lock-stays"); let writer = Writer::open(&dir.path, false).unwrap().writer; drop(writer); assert!(dir.path.join(".lock").exists()); Writer::open(&dir.path, false).expect("the lock was released with the writer"); } /// A torn last line in one file, then an empty later file (created, never written). The torn line /// is ended in its own file and the recovery goes on the chain after it, so the log verifies and /// the next start is an ordinary one. Found by the independent review of task 23. #[test] fn a_torn_line_before_an_empty_later_file_is_recovered_in_place() { let dir = TempDir::unmade("torn-then-empty"); let mut writer = Writer::open(&dir.path, false).unwrap().writer; writer .append(ts("2026-09-17T08:00:00.000Z"), denied(1)) .unwrap(); drop(writer); let mut d1 = std::fs::read(dir.path.join(D1)).unwrap(); d1.extend_from_slice(br#"{"seq":1,"time":"2026-09-17T09"#); std::fs::write(dir.path.join(D1), &d1).unwrap(); std::fs::write(dir.path.join(D2), b"").unwrap(); let opened = Writer::open(&dir.path, false).unwrap(); assert!(opened.recovered); drop(opened); let report = verify_dir(&dir.path).unwrap(); assert_eq!(report.failure, None, "{:?}", report.failure); assert_eq!(report.recoveries.len(), 1); assert!(std::fs::read(dir.path.join(D1)).unwrap().ends_with(b"\n")); let again = Writer::open(&dir.path, false).expect("the next start is an ordinary one"); assert!(!again.recovered); }