# Copyright (C) 2026 Danilo M. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Tests for the case directory: creation, manifest round trip, atomic saves.""" import json import pathlib import tempfile import unittest from abusectl import case class TestCaseCreation(unittest.TestCase): def setUp(self): self._tmp = tempfile.TemporaryDirectory() self.root = pathlib.Path(self._tmp.name) def tearDown(self): self._tmp.cleanup() def test_a_case_holds_the_source_and_a_manifest(self): created = case.create(self.root, b"From: \r\n\r\nhi") self.assertTrue((created.path / "source.eml").is_file()) self.assertTrue((created.path / "manifest.json").is_file()) def test_the_source_is_stored_byte_for_byte(self): raw = b"From: \r\n\r\nhi\r\n" created = case.create(self.root, raw) self.assertEqual((created.path / "source.eml").read_bytes(), raw) def test_the_manifest_carries_a_format_version(self): created = case.create(self.root, b"x") manifest = json.loads((created.path / "manifest.json").read_text()) self.assertEqual(manifest["format"], case.FORMAT_VERSION) def test_two_cases_do_not_collide(self): a = case.create(self.root, b"one") b = case.create(self.root, b"two") self.assertNotEqual(a.path, b.path) def test_a_case_id_is_filesystem_safe(self): created = case.create(self.root, b"x") self.assertRegex(created.path.name, r"^\d{4}-\d{2}-\d{2}-[0-9a-f]{4}$") def test_a_manifest_round_trips(self): created = case.create(self.root, b"x") manifest = case.load(created.path) manifest["iocs"] = [{"id": "ioc-1", "type": "ipv4", "value": "203.0.113.1"}] case.save(created.path, manifest) self.assertEqual(case.load(created.path)["iocs"][0]["value"], "203.0.113.1") def test_saving_leaves_no_temporary_file_behind(self): # The manifest is written atomically, temp file plus rename, because a # half-written manifest during a review is a corrupted evidence record. created = case.create(self.root, b"x") case.save(created.path, case.load(created.path)) leftovers = [p.name for p in created.path.iterdir() if p.suffix == ".tmp"] self.assertEqual(leftovers, []) def test_an_unknown_manifest_format_is_refused(self): # A newer writer may mean fields this build would silently drop on the # next save, so refusing is correct rather than cautious. created = case.create(self.root, b"x") manifest = case.load(created.path) manifest["format"] = case.FORMAT_VERSION + 1 (created.path / "manifest.json").write_text(json.dumps(manifest)) with self.assertRaises(ValueError): case.load(created.path) def test_a_failed_save_leaves_the_existing_manifest_intact(self): # An evidence record must not be destroyed by a write that fails. created = case.create(self.root, b"x") good = case.load(created.path) good["iocs"] = [{"id": "ioc-1"}] case.save(created.path, good) class Unserialisable: pass with self.assertRaises(TypeError): case.save(created.path, {"format": case.FORMAT_VERSION, "bad": Unserialisable()}) self.assertEqual(case.load(created.path)["iocs"], [{"id": "ioc-1"}]) leftovers = [p.name for p in created.path.iterdir() if p.suffix == ".tmp"] self.assertEqual(leftovers, []) if __name__ == "__main__": unittest.main()