"""Tests for ``arautopilot.core.audit``.""" from __future__ import annotations from pathlib import Path import pytest from arautopilot.core.audit import AuditEvent, AuditLog, AuditOutcome def test_event_to_jsonl_is_single_line() -> None: ev = AuditEvent( action="engage_pilot", outcome=AuditOutcome.SUCCESS, user_id="u123", role="user", ) line = ev.to_jsonl() assert "\n" not in line assert line.startswith("{") and line.endswith("}") def test_event_is_immutable() -> None: ev = AuditEvent(action="x", outcome=AuditOutcome.SUCCESS) with pytest.raises((TypeError, ValueError)): ev.action = "y" # type: ignore[misc] def test_append_and_read_round_trip(tmp_path: Path) -> None: log = AuditLog(tmp_path / "audit.jsonl") e1 = AuditEvent(action="login", outcome=AuditOutcome.SUCCESS, user_id="u1") e2 = AuditEvent(action="engage", outcome=AuditOutcome.DENIED, user_id="u2", reason="missing capability") log.append(e1) log.append(e2) read = log.read_all() assert len(read) == 2 assert read[0].action == "login" assert read[1].outcome is AuditOutcome.DENIED def test_len_counts_lines(tmp_path: Path) -> None: log = AuditLog(tmp_path / "audit.jsonl") assert len(log) == 0 log.append(AuditEvent(action="a", outcome=AuditOutcome.SUCCESS)) log.append(AuditEvent(action="b", outcome=AuditOutcome.SUCCESS)) assert len(log) == 2 def test_log_file_is_created_if_missing(tmp_path: Path) -> None: p = tmp_path / "subdir" / "audit.jsonl" log = AuditLog(p) assert p.exists() assert len(log) == 0 def test_corrupt_line_raises(tmp_path: Path) -> None: p = tmp_path / "audit.jsonl" p.write_text( '{"action":"good","outcome":"success","timestamp":"2026-05-18T00:00:00Z"}\n' "this is not json\n", encoding="utf-8", ) log = AuditLog(p) with pytest.raises(ValueError, match="corrupt audit line"): log.read_all() def test_dual_auth_event_carries_secondary_user(tmp_path: Path) -> None: log = AuditLog(tmp_path / "a.jsonl") ev = AuditEvent( action="flash_firmware", outcome=AuditOutcome.SUCCESS, user_id="engineer_1", role="engineer", secondary_user_id="super_admin_alvaro", target="COM7:esp32-dev", ) log.append(ev) read = log.read_all() assert read[0].secondary_user_id == "super_admin_alvaro" assert read[0].target == "COM7:esp32-dev" def test_extra_payload_round_trips(tmp_path: Path) -> None: log = AuditLog(tmp_path / "a.jsonl") ev = AuditEvent( action="mode_change", outcome=AuditOutcome.SUCCESS, extra={"from": "STANDBY", "to": "HEADING_HOLD", "via": "modbus"}, ) log.append(ev) read = log.read_all() assert read[0].extra == {"from": "STANDBY", "to": "HEADING_HOLD", "via": "modbus"} def test_blank_lines_are_skipped(tmp_path: Path) -> None: p = tmp_path / "a.jsonl" p.write_text( '{"action":"a","outcome":"success","timestamp":"2026-05-18T00:00:00Z"}\n' "\n" '{"action":"b","outcome":"denied","timestamp":"2026-05-18T00:00:01Z"}\n', encoding="utf-8", ) log = AuditLog(p) events = log.read_all() assert len(events) == 2