"""Tests for AuditLog.""" from __future__ import annotations import json import os from datetime import datetime from pathlib import Path import pytest from atm.audit import AuditLog def _dt(s: str) -> datetime: return datetime.fromisoformat(s) def test_writes_jsonl(tmp_path: Path) -> None: clock_dt = _dt("2026-04-15T10:00:00") log = AuditLog(tmp_path / "logs", clock=lambda: clock_dt) events = [ {"msg": "first", "ts": "2026-04-15T10:00:00"}, {"msg": "second", "ts": "2026-04-15T10:01:00"}, {"msg": "third", "ts": "2026-04-15T10:02:00"}, ] for e in events: log.log(e) log.close() lines = (tmp_path / "logs" / "2026-04-15.jsonl").read_text().splitlines() assert len(lines) == 3 for original, line in zip(events, lines): parsed = json.loads(line) assert parsed["msg"] == original["msg"] assert parsed["ts"] == original["ts"] def test_daily_rotation(tmp_path: Path) -> None: base = tmp_path / "logs" times = [ _dt("2026-04-15T23:59:59"), # just before midnight _dt("2026-04-16T00:00:01"), # just after midnight ] idx = 0 def clock() -> datetime: return times[min(idx, len(times) - 1)] log = AuditLog(base, clock=clock) log.log({"msg": "before"}) idx = 1 log.log({"msg": "after"}) log.close() file_15 = base / "2026-04-15.jsonl" file_16 = base / "2026-04-16.jsonl" assert file_15.exists(), "File for Apr 15 should exist" assert file_16.exists(), "File for Apr 16 should exist" lines_15 = [json.loads(l) for l in file_15.read_text().splitlines()] lines_16 = [json.loads(l) for l in file_16.read_text().splitlines()] assert lines_15[0]["msg"] == "before" assert lines_16[0]["msg"] == "after" def test_line_buffered(tmp_path: Path) -> None: base = tmp_path / "logs" clock_dt = _dt("2026-04-15T12:00:00") log = AuditLog(base, clock=lambda: clock_dt) log.log({"msg": "hello", "ts": "2026-04-15T12:00:00"}) # Do NOT call close() — file should already have content due to line buffering path = base / "2026-04-15.jsonl" assert os.stat(path).st_size > 0 log.close() def test_adds_ts_when_missing(tmp_path: Path) -> None: clock_dt = _dt("2026-04-15T09:30:00") log = AuditLog(tmp_path / "logs", clock=lambda: clock_dt) log.log({"msg": "hi"}) log.close() lines = (tmp_path / "logs" / "2026-04-15.jsonl").read_text().splitlines() parsed = json.loads(lines[0]) assert "ts" in parsed assert parsed["ts"] == "2026-04-15T09:30:00" def test_preserves_existing_ts(tmp_path: Path) -> None: clock_dt = _dt("2026-04-15T09:30:00") log = AuditLog(tmp_path / "logs", clock=lambda: clock_dt) log.log({"ts": "2026-01-01T00:00:00", "msg": "hi"}) log.close() lines = (tmp_path / "logs" / "2026-04-15.jsonl").read_text().splitlines() parsed = json.loads(lines[0]) assert parsed["ts"] == "2026-01-01T00:00:00" def test_close_idempotent(tmp_path: Path) -> None: clock_dt = _dt("2026-04-15T10:00:00") log = AuditLog(tmp_path / "logs", clock=lambda: clock_dt) log.log({"msg": "x", "ts": "2026-04-15T10:00:00"}) log.close() log.close() # should not raise