"""The boundary rules that kept breaking, pinned down. Four review rounds produced defects in the same two places: how a timestamp crosses the database boundary, and which evidence a pack claims to cover. Every test here asserts one of those rules directly, so the next change to either has to say so out loud. """ from __future__ import annotations import uuid from datetime import UTC, datetime, timedelta, timezone import pytest from app.core.exceptions import AppException from app.domains.auditor.evidence_query.contracts import ( EvidenceRow, content_hash, persisted_utc, ) from app.domains.auditor.evidence_query.service import ( _period_bounds, run_evidence_query, ) from app.domains.auditor.models import AuditorEvidenceBundle # ── the database boundary ───────────────────────────────────────────── def test_an_offset_param_is_converted_not_preserved(): """``DATETIME`` stores a wall clock and no offset. Writing an aware +08:00 value straight through lands eight hours off, and every later comparison — the evidence-pack window included — is then against the wrong instant. The web UI never produced one, but MCP, the CLI and curl all accept an ISO string with an offset. """ cst = timezone(timedelta(hours=8)) start, end = _period_bounds({ "period_start": "2026-05-01T00:00:00+08:00", "period_end": "2026-05-31T00:00:00+08:00", }) assert start == datetime(2026, 4, 30, 16, 0, tzinfo=UTC) assert start.utcoffset() == timedelta(0) assert end.utcoffset() == timedelta(0) # And the same instant expressed either way agrees. assert start == datetime(2026, 5, 1, tzinfo=cst) def test_a_persisted_timestamp_rehashes_to_the_frozen_digest(): """The reproducibility claim on ``content_hash``, checked. Rows are normalised to UTC before they are written, but the column drops the offset, so they read back NAIVE. Anyone re-hashing the raw persisted values gets a different digest than the one frozen with the bundle — which makes the claim false precisely when someone tries to verify it. ``persisted_utc`` is the documented way back. """ aware = datetime(2026, 5, 1, 12, 0, tzinfo=UTC) row = EvidenceRow( subject_type="person", subject_id="p1", payload={"a": 1}, source_table="t", source_pk="1", occurred_at=aware, ) frozen = content_hash([row]) naive_from_db = aware.replace(tzinfo=None) # what MySQL returns # Hash the raw persisted value the way an external verifier would, # WITHOUT going back through EvidenceRow — which would re-normalise # and make this pass for the wrong reason. def _digest(ts): import hashlib import json return hashlib.sha256(json.dumps( [{"o": ts.isoformat() if ts else None}], sort_keys=True, ).encode()).hexdigest() assert _digest(naive_from_db) != _digest(aware), ( "the naive read-back really does hash differently" ) assert _digest(persisted_utc(naive_from_db)) == _digest(aware), ( "persisted_utc is what closes that gap" ) # And the documented route round-trips to the frozen digest. assert content_hash([EvidenceRow( subject_type="person", subject_id="p1", payload={"a": 1}, source_table="t", source_pk="1", occurred_at=persisted_utc(naive_from_db), )]) == frozen def test_persisted_utc_is_a_no_op_on_an_already_aware_value(): aware = datetime(2026, 5, 1, tzinfo=UTC) assert persisted_utc(aware) is aware assert persisted_utc(None) is None @pytest.mark.asyncio async def test_a_malformed_date_is_a_400_not_a_500(db_session, admin_user): """``_as_datetime`` falls through to ``strptime`` with no guard, so a typo escaped as an uncaught ValueError from inside the handler.""" admin, _ = admin_user with pytest.raises(AppException) as exc: await run_evidence_query( db_session, query_key="software_seat_roster", params={"period_start": "2026-13-45"}, actor_user_id=admin.id, ) assert exc.value.status_code == 400 assert "period_start" in str(exc.value.message) # ── which evidence a pack covers ────────────────────────────────────── class _Pack: def __init__(self, start: datetime, end: datetime) -> None: self.period_start, self.period_end = start, end async def _bundle(db, *, via, created_at, period_start=None, period_end=None): row = AuditorEvidenceBundle( query_key="sso_grant_roster", query_version="v1", params={}, row_count=0, content_hash=uuid.uuid4().hex, generated_via=via, created_at=created_at, period_start=period_start, period_end=period_end or period_start, ) db.add(row) await db.flush() return row @pytest.mark.asyncio async def test_a_may_pack_takes_neither_neighbour_whatever_the_offset( db_session, ): """The picker sends whole UTC days now, so "May" is one window. ``startOf('day')`` alone was the BROWSER's day: UTC+8 shifted the window back (dropping month-end point-in-time evidence) and UTC-7 shifted it forward far enough to pull June in. Both directions are asserted here because only one of them was ever caught. """ from app.domains.auditor.services import EvidencePackService apr = await _bundle( db_session, via="scheduled", created_at=datetime(2026, 5, 2, tzinfo=UTC), period_start=datetime(2026, 4, 1, tzinfo=UTC), period_end=datetime(2026, 4, 30, tzinfo=UTC), ) may = await _bundle( db_session, via="scheduled", created_at=datetime(2026, 6, 2, tzinfo=UTC), period_start=datetime(2026, 5, 1, tzinfo=UTC), period_end=datetime(2026, 5, 31, tzinfo=UTC), ) jun = await _bundle( db_session, via="scheduled", created_at=datetime(2026, 7, 2, tzinfo=UTC), period_start=datetime(2026, 6, 1, tzinfo=UTC), period_end=datetime(2026, 6, 30, tzinfo=UTC), ) section = str(await EvidencePackService._render_access_evidence( db_session, pack=_Pack( datetime(2026, 5, 1, 0, 0, 0, tzinfo=UTC), datetime(2026, 5, 31, 23, 59, 59, 999000, tzinfo=UTC), ), )) assert str(may.id)[:8] in section assert str(apr.id)[:8] not in section, "April leaked into a May pack" assert str(jun.id)[:8] not in section, "June leaked into a May pack" @pytest.mark.asyncio async def test_an_offboarding_archive_files_under_the_leavers_month( db_session, ): """Not the month the paperwork finished. The archive is frozen at offboarding COMPLETION, which routinely lands in the following month — a 29 May leaver completed on 3 June filed into the June pack, where nobody auditing May would look. """ from app.domains.auditor.services import EvidencePackService archive = await _bundle( db_session, via="offboarding", created_at=datetime(2026, 6, 3, 10, 0, tzinfo=UTC), # completion period_start=datetime(2026, 5, 29, tzinfo=UTC), # last day period_end=datetime(2026, 5, 29, tzinfo=UTC), ) may = str(await EvidencePackService._render_access_evidence( db_session, pack=_Pack( datetime(2026, 5, 1, tzinfo=UTC), datetime(2026, 5, 31, 23, 59, 59, tzinfo=UTC), ), )) jun = str(await EvidencePackService._render_access_evidence( db_session, pack=_Pack( datetime(2026, 6, 1, tzinfo=UTC), datetime(2026, 6, 30, 23, 59, 59, tzinfo=UTC), ), )) assert str(archive.id)[:8] in may, "the leaver's own month" assert str(archive.id)[:8] not in jun def test_the_offboarding_cycle_supplies_the_coverage_window(): """Where that period comes from: the archive declares no period of its own, but it does know which departure it belongs to.""" start, end = _period_bounds({ "person_id": "x", "offboarding_cycle": "2026-05-29", }) assert start == datetime(2026, 5, 29, tzinfo=UTC) assert end == start