"""Offline packaging tests using a minimal, independently generated repository.""" import importlib.util import json import tarfile import tempfile import unittest import zipfile from pathlib import Path SCRIPT_PATH = Path(__file__).with_name("package_release.py") SPEC = importlib.util.spec_from_file_location("package_release", SCRIPT_PATH) PACKER = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(PACKER) class PackagingTests(unittest.TestCase): VERSION = "v28.0.0" COMMIT = "a" * 40 THEME = "demo" LANGUAGE = "en-US" ROOT_DOCUMENTS = ( "LICENSE", "AGENTS.md", "README.md", "CONTRIBUTING.md", "COMPATIBILITY.md", "THIRD_PARTY_NOTICES.md", "docs/README.zh-CN.md", "docs/CONTRIBUTING.zh-CN.md", "preview/index.html", "preview/lib/html2canvas/html2canvas.min.js", "preview/lib/html2canvas/LICENSE", "preview/lib/html2canvas/README.md", "docs/images/README.md", ) def setUp(self): temporary_directory = tempfile.TemporaryDirectory() self.addCleanup(temporary_directory.cleanup) self.root = Path(temporary_directory.name) self.output = self.root / "artifacts" self.create_theme() self.create_snapshot() self.create_preview() for name in self.ROOT_DOCUMENTS: self.write(name, b"test") # Fixture helpers keep test bodies focused on the behavior being checked. def write(self, name, data): path = self.root / name path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(data) def write_json(self, name, data): self.write(name, json.dumps(data).encode("utf-8")) def create_theme(self): theme_data = json.dumps({"name": self.THEME, "mode": "shared"}).encode("utf-8") css_data = b"a { color:blue; }" self.write("themes/demo/theme.json", theme_data) self.write("themes/demo/theme.css", css_data) generated_files = { "mail/base/head.tmpl": b"
footer
", } metadata = { "theme": self.THEME, "mode": "shared", "gitea_tag": self.VERSION, "gitea_commit": self.COMMIT, "sources": { "theme.json": PACKER.digest(theme_data), "theme.css": PACKER.digest(css_data), }, "files": { path: PACKER.digest(data) for path, data in generated_files.items() }, } for path, data in generated_files.items(): self.write("build/themes/demo/" + path, data) self.write_json("build/themes/demo/build.json", metadata) def create_snapshot(self): self.write_json("gitea.lock.json", { "tag": self.VERSION, "commit": self.COMMIT, "files": { "LICENSE": PACKER.digest(b"MIT"), "options/locale/locale_en-US.json": PACKER.digest(b"{}"), }, }) self.write("build/upstream/LICENSE", b"MIT") def create_preview(self): manifest = { "upstream": self.VERSION, "upstream_commit": self.COMMIT, "themes": [self.THEME], "languages": [self.LANGUAGE], "registry": {"activate": {}}, } bundle = {self.THEME: {"activate": ""}} self.write("preview/rendered.js", ( "window.__MAIL_PREVIEW__ = " + json.dumps(manifest) + ";" ).encode("utf-8")) self.write("preview/rendered/en-US.js", ( 'window.__MAIL_PREVIEW_DATA__["en-US"] = ' + json.dumps(bundle) + ";" ).encode("utf-8")) # Archives: both formats contain the verified bytes, never stale build files. def test_archives_contain_only_verified_outputs(self): self.write("build/themes/stale/mail/stale.tmpl", b"stale") expected_files = PACKER.collect(self.root, self.VERSION) prefix = f"gitea-mail-templates-{self.VERSION}/" expected_members = { prefix + name: data for name, data in expected_files.items() } PACKER.package(self.root, self.VERSION, self.output) with zipfile.ZipFile(self.output / f"gitea-mail-templates-{self.VERSION}.zip") as archive: zipped = {name: archive.read(name) for name in archive.namelist()} with tarfile.open(self.output / f"gitea-mail-templates-{self.VERSION}.tar.gz") as archive: tarred = { member.name: archive.extractfile(member).read() for member in archive.getmembers() } self.assertTrue(all(member.mode == 0o644 for member in archive.getmembers())) self.assertEqual(expected_members, zipped) self.assertEqual(expected_members, tarred) self.assertIn(prefix + "themes/demo/mail/base/head.tmpl", zipped) for name in ("html2canvas.min.js", "LICENSE", "README.md"): self.assertIn(prefix + "preview/lib/html2canvas/" + name, zipped) self.assertFalse(any("stale" in name or "/build/" in name for name in zipped)) def test_missing_screenshot_library_is_rejected(self): (self.root / "preview/lib/html2canvas/html2canvas.min.js").unlink() with self.assertRaisesRegex(ValueError, "Expected regular file"): PACKER.collect(self.root, self.VERSION) def test_existing_archives_are_not_overwritten(self): PACKER.package(self.root, self.VERSION, self.output) original_archives = { path.name: path.read_bytes() for path in self.output.iterdir() } with self.assertRaisesRegex(ValueError, "Refusing to overwrite"): PACKER.package(self.root, self.VERSION, self.output) self.assertEqual(original_archives, { path.name: path.read_bytes() for path in self.output.iterdir() }) # Validation: release identity, source checksums and generated checksums. def test_wrong_version_is_rejected(self): with self.assertRaisesRegex(ValueError, "must match the locked Gitea tag"): PACKER.collect(self.root, "v28.1.0") def test_changed_source_is_rejected(self): self.write("themes/demo/theme.css", b"changed") with self.assertRaisesRegex(ValueError, "Source changed"): PACKER.collect(self.root, self.VERSION) def test_missing_language_bundle_is_rejected(self): (self.root / "preview/rendered/en-US.js").unlink() with self.assertRaisesRegex(ValueError, "Expected regular file"): PACKER.collect(self.root, self.VERSION) def test_modified_generated_files_are_rejected(self): self.write("build/themes/demo/mail/base/head.tmpl", b"modified") with self.assertRaisesRegex(ValueError, "checksum mismatch"): PACKER.collect(self.root, self.VERSION) # Documentation: recursive inclusion and optional root files. def test_nested_documentation_and_release_notes_are_included(self): documents = { "docs/INDEX.zh-CN.md": "中文索引".encode("utf-8"), "docs/images/README.zh-CN.md": "截图指南".encode("utf-8"), "docs/release-notes/v28.0.0.zh-CN.md": "发布说明".encode("utf-8"), ".github/release-notes/v28.0.0.md": b"English release notes", } for name, data in documents.items(): self.write(name, data) self.write("docs/private.tmp", b"not documentation") files = PACKER.collect(self.root, self.VERSION) for name, data in documents.items(): with self.subTest(document=name): self.assertEqual(data, files[name]) self.assertIn("AGENTS.md", files) self.assertNotIn("docs/private.tmp", files) def test_optional_root_documents_may_be_absent(self): for name in ("AGENTS.md", "THIRD_PARTY_NOTICES.md"): (self.root / name).unlink() files = PACKER.collect(self.root, self.VERSION) self.assertNotIn("AGENTS.md", files) self.assertNotIn("THIRD_PARTY_NOTICES.md", files) self.assertIn("GITEA-LICENSE", files) if __name__ == "__main__": unittest.main()