from __future__ import annotations import json from pathlib import Path import sys import tempfile import unittest from unittest import mock META_ROOT = Path(__file__).resolve().parents[1] RELEASE_TOOLS_ROOT = META_ROOT / "tools" / "release" if str(RELEASE_TOOLS_ROOT) not in sys.path: sys.path.insert(0, str(RELEASE_TOOLS_ROOT)) from govoplan_release.module_directory import ( # noqa: E402 module_directory_payloads, safe_path_part, write_module_directory, ) class ReleaseModuleDirectoryTests(unittest.TestCase): def test_signed_catalog_timestamp_makes_derived_files_reproducible(self) -> None: catalog = { "generated_at": "2026-07-22T12:00:00Z", "sequence": 7, "modules": [], } first = module_directory_payloads( catalog_payload=catalog, keyring_payload={}, channel="stable", ) second = module_directory_payloads( catalog_payload=catalog, keyring_payload={}, channel="stable", ) self.assertEqual(first, second) self.assertEqual( "2026-07-22T12:00:00Z", first[-1][1]["generated_at"], ) def test_dot_segments_and_noncanonical_ids_never_become_paths(self) -> None: for value in (".", "..", "../escape", "/absolute", "module/child"): with self.subTest(value=value), self.assertRaises(ValueError): safe_path_part(value) for module_id in ("..", "../escape", "UPPER", "bad.id"): with self.subTest(module_id=module_id), mock.patch( "govoplan_release.module_directory.module_rows", return_value=[ { "module_id": module_id, "version": "1.2.3", } ], ): with self.assertRaises(ValueError): module_directory_payloads( catalog_payload={}, keyring_payload={}, channel="stable", ) def test_noncanonical_version_and_channel_fail_before_paths(self) -> None: with mock.patch( "govoplan_release.module_directory.module_rows", return_value=[{"module_id": "demo", "version": "../1.2.3"}], ): with self.assertRaises(ValueError): module_directory_payloads( catalog_payload={}, keyring_payload={}, channel="stable" ) with self.assertRaises(ValueError): module_directory_payloads( catalog_payload={}, keyring_payload={}, channel="../stable" ) def test_prune_removes_stale_json_but_keeps_unrelated_assets(self) -> None: catalog = { "generated_at": "2026-08-06T12:00:00Z", "sequence": 8, "modules": [ { "module_id": "files", "name": "Files", "version": "1.2.3", "python_package": "govoplan-files", "source": { "repository": "govoplan-files", "tag": "v1.2.3", "commit": "a" * 40, }, "artifact_integrity": { "python": {"sha256": "b" * 64}, }, } ], } with tempfile.TemporaryDirectory() as value: output_root = Path(value) stale = output_root / "modules" / "legacy" / "0.1.0" / "manifest.json" stale.parent.mkdir(parents=True) stale.write_text("{}\n", encoding="utf-8") unrelated = output_root / "modules" / "README.txt" unrelated.write_text("keep\n", encoding="utf-8") written = write_module_directory( catalog_payload=catalog, keyring_payload={}, output_root=output_root, channel="stable", prune=True, ) self.assertFalse(stale.exists()) self.assertTrue(unrelated.exists()) self.assertEqual(3, len(written)) manifest = json.loads( (output_root / "modules" / "files" / "1.2.3" / "manifest.json").read_text() ) self.assertEqual("govoplan-files", manifest["module"]["repo"]) self.assertEqual("v1.2.3", manifest["module"]["python_tag"]) self.assertEqual("b" * 64, manifest["module"]["artifact_integrity"]["python"]["sha256"]) def test_writer_refuses_nested_symlink_targets(self) -> None: catalog = { "modules": [{"module_id": "files", "version": "1.2.3"}], } with tempfile.TemporaryDirectory() as value: root = Path(value) output_root = root / "public" external = root / "external" (output_root / "modules").mkdir(parents=True) external.mkdir() (output_root / "modules" / "files").symlink_to( external, target_is_directory=True, ) with self.assertRaisesRegex(ValueError, "symlinks"): write_module_directory( catalog_payload=catalog, keyring_payload={}, output_root=output_root, channel="stable", ) self.assertEqual([], list(external.iterdir())) if __name__ == "__main__": unittest.main()