diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml index d44e5ee..9d2de22 100644 --- a/.forgejo/workflows/test.yml +++ b/.forgejo/workflows/test.yml @@ -10,4 +10,5 @@ jobs: runs-on: docker steps: - uses: https://data.forgejo.org/actions/checkout@v6 - - run: python3 -m unittest discover -s tests + - run: python3 -m pip install pytest + - run: python3 -m pytest -q diff --git a/README.md b/README.md index 6752ea5..7c044c1 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,6 @@ A small, dependency-free Forgejo Action that synchronizes a completed static website build to a dedicated Bunny Storage Zone. -The runner needs Bash and Python 3.11 or newer. The action itself installs no -packages and executes no code from the Gandalf repository. - The deployer requires `index.html`, rejects symbolic links, compares files with a checksum manifest, uploads changed assets before HTML, deletes stale files only after successful uploads, and publishes the new manifest last. @@ -60,7 +57,7 @@ Set `BUNNY_STORAGE_ENDPOINT` when the Storage Zone is outside Frankfurt. ## Development ```bash -python3 -m unittest discover -s tests +python3 -m pytest tests ``` The canonical source lives in the private Gandalf infrastructure repository diff --git a/tests/test_static_site.py b/tests/test_static_site.py index 9487054..094b799 100644 --- a/tests/test_static_site.py +++ b/tests/test_static_site.py @@ -1,9 +1,8 @@ import importlib.util -import json from pathlib import Path import sys -import tempfile -import unittest + +import pytest MODULE_PATH = Path(__file__).resolve().parents[1] / "static_site.py" SPEC = importlib.util.spec_from_file_location("static_site_deploy", MODULE_PATH) @@ -25,58 +24,47 @@ def write_site(root: Path) -> None: (root / "assets" / "app.js").write_text("console.log('hello')") -class StaticSiteTests(unittest.TestCase): - def test_collect_files_requires_index(self) -> None: - with tempfile.TemporaryDirectory() as directory: - with self.assertRaisesRegex(DeployError, "index.html"): - collect_files(Path(directory)) - - def test_collect_files_rejects_symlinks(self) -> None: - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - write_site(root) - (root / "linked").symlink_to(root / "index.html") - with self.assertRaisesRegex(DeployError, "symbolic links"): - collect_files(root) - - def test_manifest_round_trip(self) -> None: - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - write_site(root) - files = collect_files(root) - manifest = manifest_for(files) - parsed = parse_manifest(json.dumps(manifest).encode()) - self.assertEqual( - parsed["index.html"]["sha256"], files["index.html"].sha256 - ) - - def test_manifest_rejects_unsafe_remote_path(self) -> None: - raw = b'{"version": 1, "files": {"../other-site/index.html": {}}}' - with self.assertRaisesRegex(DeployError, "unsafe path"): - parse_manifest(raw) - - def test_bunny_plan_uploads_assets_before_html_and_removes_stale(self) -> None: - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - write_site(root) - files = collect_files(root) - remote = { - "index.html": {"sha256": "old", "size": 1}, - "assets/app.js": {"sha256": "old", "size": 1}, - "old.css": {"sha256": "old", "size": 1}, - } - upload, delete = bunny_plan(files, remote) - self.assertEqual(upload, ["assets/app.js", "index.html"]) - self.assertEqual(delete, ["old.css"]) - - def test_bunny_plan_skips_unchanged_files(self) -> None: - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - write_site(root) - files = collect_files(root) - remote = manifest_for(files)["files"] - self.assertEqual(bunny_plan(files, remote), ([], [])) +def test_collect_files_requires_index(tmp_path): + with pytest.raises(DeployError, match="index.html"): + collect_files(tmp_path) -if __name__ == "__main__": - unittest.main() +def test_collect_files_rejects_symlinks(tmp_path): + write_site(tmp_path) + (tmp_path / "linked").symlink_to(tmp_path / "index.html") + with pytest.raises(DeployError, match="symbolic links"): + collect_files(tmp_path) + + +def test_manifest_round_trip(tmp_path): + write_site(tmp_path) + files = collect_files(tmp_path) + manifest = manifest_for(files) + parsed = parse_manifest(__import__("json").dumps(manifest).encode()) + assert parsed["index.html"]["sha256"] == files["index.html"].sha256 + + +def test_manifest_rejects_unsafe_remote_path(): + raw = b'{"version": 1, "files": {"../other-site/index.html": {}}}' + with pytest.raises(DeployError, match="unsafe path"): + parse_manifest(raw) + + +def test_bunny_plan_uploads_assets_before_html_and_removes_stale(tmp_path): + write_site(tmp_path) + files = collect_files(tmp_path) + remote = { + "index.html": {"sha256": "old", "size": 1}, + "assets/app.js": {"sha256": "old", "size": 1}, + "old.css": {"sha256": "old", "size": 1}, + } + upload, delete = bunny_plan(files, remote) + assert upload == ["assets/app.js", "index.html"] + assert delete == ["old.css"] + + +def test_bunny_plan_skips_unchanged_files(tmp_path): + write_site(tmp_path) + files = collect_files(tmp_path) + remote = manifest_for(files)["files"] + assert bunny_plan(files, remote) == ([], [])