2026-07-15 16:18:50 +02:00
|
|
|
import importlib.util
|
2026-07-15 16:34:06 +02:00
|
|
|
import json
|
2026-07-15 16:18:50 +02:00
|
|
|
from pathlib import Path
|
|
|
|
|
import sys
|
2026-07-15 16:34:06 +02:00
|
|
|
import tempfile
|
|
|
|
|
import unittest
|
2026-07-15 16:18:50 +02:00
|
|
|
|
|
|
|
|
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("<h1>Hello</h1>")
|
|
|
|
|
(root / "assets" / "app.js").write_text("console.log('hello')")
|
|
|
|
|
|
|
|
|
|
|
2026-07-15 16:34:06 +02:00
|
|
|
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), ([], []))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main()
|