diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-10 10:13:04 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-10 10:13:04 +0200 |
| commit | 2cdd4dba93b26691a85cd0977294687d5afe0670 (patch) | |
| tree | 6027db6b599b43138a7b1f81a08296f7e91cfa5e /tests | |
| parent | 57dc93c3069ccac583533f4f2d4f9cbc2720c73e (diff) | |
| download | abusectl-2cdd4dba93b26691a85cd0977294687d5afe0670.tar.gz abusectl-2cdd4dba93b26691a85cd0977294687d5afe0670.zip | |
feat: freeze a reported case and detect edited bodies
Content hash rather than mtime, because mtime is wrong in both directions.
Any sent destination freezes the whole case with no override: two desks
holding contradictory accounts of one incident is worse than a stale body.
The marker is tested for PRESENCE rather than truthiness. It is write-once
and monotonic, so an empty or malformed value is a half-written record of a
case a desk may already hold, and the safe direction is to refuse; only an
explicit null counts as absent. A non-mapping marker refuses too rather than
raising AttributeError out of a .get, since cli turns Frozen into a message
and a traceback into a bug report.
The hash pins utf-8 rather than leaving the encoding to the locale: it is
compared against a body read back off disk, possibly under a different LANG,
and for a sent destination it is the record of what was disclosed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LByBnw83xr9YP85nskzkyE
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_report.py | 110 |
1 files changed, 110 insertions, 0 deletions
diff --git a/tests/test_report.py b/tests/test_report.py index 6a340c9..b36252c 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -1,6 +1,7 @@ import copy import email import email.policy +import hashlib import json import unittest @@ -1596,5 +1597,114 @@ class DocumentIdentity(unittest.TestCase): self.assertEqual(addresses[0].display_name, name) +class Freeze(unittest.TestCase): + """A case that has reached a desk is an evidence record, not a draft.""" + + def _frozen(self, marker): + manifest = copy.deepcopy(MANIFEST) + manifest["frozen"] = marker + return manifest + + def test_a_frozen_case_refuses(self): + manifest = self._frozen({"at": "2026-09-07T10:00:00Z", + "by": "abusedb"}) + with self.assertRaises(report.Frozen) as caught: + report.check_regenerable(manifest, modified=[]) + self.assertIn("abusedb", str(caught.exception)) + + def test_a_frozen_case_refuses_even_when_forced(self): + manifest = self._frozen({"at": "2026-09-07T10:00:00Z", + "by": "abusedb"}) + with self.assertRaises(report.Frozen): + report.check_regenerable(manifest, modified=[], force=True) + + def test_a_frozen_case_refuses_before_the_modified_check(self): + """Frozen wins over force for EVERY shape of the second argument. + + The two rules are not independent: an edited body on a frozen case + must still refuse, and must refuse as Frozen rather than as + Modified, because Modified is the one --force can clear. + """ + manifest = self._frozen({"at": "2026-09-07T10:00:00Z", + "by": "abusedb"}) + with self.assertRaises(report.Frozen): + report.check_regenerable(manifest, modified=["email-1"]) + with self.assertRaises(report.Frozen): + report.check_regenerable(manifest, modified=["email-1"], + force=True) + + def test_a_present_but_empty_marker_still_freezes(self): + """Present means frozen. The marker is write-once and monotonic, so + a shape that is present and says nothing useful is a half-written or + corrupted record, and the safe direction is to refuse. Truthiness is + the wrong test: `{}` is present and falsy, and treating it as absent + silently unfreezes a case a desk may already hold. + """ + for marker in ({}, "", 0, []): + with self.subTest(marker=marker): + with self.assertRaises(report.Frozen): + report.check_regenerable(self._frozen(marker), + modified=[], force=True) + + def test_a_malformed_marker_refuses_rather_than_crashing(self): + """A marker that is not a mapping is still a refusal, not an + AttributeError. cli turns Frozen into a message and a traceback into + a bug report. + """ + for marker in ("2026-09-07T10:00:00Z", 1, ["abusedb"]): + with self.subTest(marker=marker): + with self.assertRaises(report.Frozen): + report.check_regenerable(self._frozen(marker), + modified=[]) + + def test_an_explicit_null_marker_is_absent(self): + """JSON has no way to omit a key it wrote as null, and null is the + one present value that unambiguously carries no record. + """ + manifest = self._frozen(None) + report.check_regenerable(manifest, modified=[]) + + def test_a_modified_body_refuses_without_force(self): + with self.assertRaises(report.Modified) as caught: + report.check_regenerable(MANIFEST, modified=["email-1"]) + self.assertIn("email-1", str(caught.exception)) + + def test_a_modified_body_is_allowed_with_force(self): + report.check_regenerable(MANIFEST, modified=["email-1"], force=True) + + def test_an_untouched_case_regenerates(self): + report.check_regenerable(MANIFEST, modified=[]) + + def test_it_does_not_mutate_the_manifest(self): + """It is a check, not a step. case.py is the only writer.""" + manifest = copy.deepcopy(MANIFEST) + before = copy.deepcopy(manifest) + report.check_regenerable(manifest, modified=[]) + self.assertEqual(manifest, before) + + +class Hashes(unittest.TestCase): + def test_the_hash_detects_a_changed_body(self): + first = report.body_hash("one") + self.assertNotEqual(first, report.body_hash("two")) + self.assertEqual(first, report.body_hash("one")) + + def test_it_is_sha256_of_the_utf8_bytes(self): + """The encoding is pinned, not incidental: the hash is compared + against a body read back off disk, and for a sent destination it is + the record of what was disclosed. + """ + text = "a body with a non-ascii org name: Örg\n" + self.assertEqual( + report.body_hash(text), + hashlib.sha256(text.encode("utf-8")).hexdigest(), + ) + + def test_it_hashes_a_real_generated_body(self): + destination = report.email_destinations(MANIFEST["contacts"])[0] + text = report.build(MANIFEST, destination, IDENTITY) + self.assertEqual(len(report.body_hash(text)), 64) + + if __name__ == "__main__": unittest.main() |
