import importlib.util from pathlib import Path import sys import pytest MODULE_PATH = Path(__file__).resolve().parents[1] / "static_site.py" SPEC = importlib.util.spec_from_file_location("static_site_deploy", MODULE_PATH) assert SPEC and SPEC.loader static_site = importlib.util.module_from_spec(SPEC) sys.modules[SPEC.name] = static_site SPEC.loader.exec_module(static_site) DeployError = static_site.DeployError bunny_plan = static_site.bunny_plan collect_files = static_site.collect_files manifest_for = static_site.manifest_for parse_manifest = static_site.parse_manifest def write_site(root: Path) -> None: (root / "assets").mkdir() (root / "index.html").write_text("

Hello

") (root / "assets" / "app.js").write_text("console.log('hello')") def test_collect_files_requires_index(tmp_path): with pytest.raises(DeployError, match="index.html"): collect_files(tmp_path) 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) == ([], [])