import os import time import pytest from fastapi.testclient import TestClient from booth.app import ( build_gallery, classify, create_app, human_dur, is_expired, sweep_once, ) # ---- pure helpers ----------------------------------------------------------- def test_classify(): assert classify("a.PNG") == "image" assert classify("clip.webm") == "video" assert classify("v.mp4") == "video" assert classify("song.mp3") == "audio" assert classify("notes.txt") == "other" assert classify("archive.tar.gz") == "other" def test_human_dur(): assert human_dur(0) == "expired" assert human_dur(-5) == "expired" assert human_dur(30) == "<1m" assert human_dur(90) == "1m" assert human_dur(3600) == "1h" assert human_dur(3660) == "1h 1m" def _touch(path, when=None): path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(b"x") if when is not None: os.utime(path, (when, when)) def test_is_expired_uses_newest_mtime(tmp_path): booth = tmp_path / "b" _touch(booth / "old.png", when=time.time() - 10_000) # freshly touched second file keeps the booth alive despite the old one _touch(booth / "new.png") assert not is_expired(booth, ttl_seconds=3600) old = tmp_path / "stale" t = time.time() - 10_000 _touch(old / "x.png", when=t) os.utime(old, (t, t)) assert is_expired(old, ttl_seconds=3600) def test_sweep_only_removes_expired(tmp_path): fresh = tmp_path / "fresh" _touch(fresh / "a.png") old = tmp_path / "old" t = time.time() - 10_000 _touch(old / "a.png", when=t) os.utime(old, (t, t)) dotdir = tmp_path / ".control" t2 = time.time() - 10_000 dotdir.mkdir() os.utime(dotdir, (t2, t2)) wiped = sweep_once(tmp_path, ttl_seconds=3600) assert wiped == ["old"] assert fresh.exists() assert not old.exists() assert dotdir.exists() # dotfolders are never swept def test_build_gallery_folds_caption_sidecars(tmp_path): booth = tmp_path / "b" _touch(booth / "a.png") (booth / "a.txt").write_text("variant A: cudaMalloc") _touch(booth / "b.png") (booth / "b.png.txt").write_text("variant B: cudaMallocAsync") _touch(booth / "loose.txt") # no media partner -> shown as its own item items = build_gallery(booth) by_name = {it["name"]: it for it in items} assert by_name["a.png"]["caption"] == "variant A: cudaMalloc" assert by_name["b.png"]["caption"] == "variant B: cudaMallocAsync" assert "a.txt" not in by_name and "b.png.txt" not in by_name assert "loose.txt" in by_name # a caption with nothing to caption stays visible # ---- HTTP surface ----------------------------------------------------------- @pytest.fixture def client(tmp_path): app = create_app(tmp_path, ttl_hours=24, start_sweeper=False) return TestClient(app), tmp_path def test_index_empty(client): c, _ = client r = c.get("/") assert r.status_code == 200 assert "No booths yet" in r.text def test_index_lists_booth(client): c, data = client _touch(data / "run1" / "a.png") r = c.get("/") assert r.status_code == 200 assert "run1" in r.text def test_booth_autogallery_renders_media(client): c, data = client _touch(data / "run1" / "shot.png") r = c.get("/b/run1/") assert r.status_code == 200 assert "MY CUSTOM REPORT") r = c.get("/b/custom/") assert r.status_code == 200 assert "MY CUSTOM REPORT" in r.text def test_booth_serves_file(client): c, data = client (data / "run1").mkdir() (data / "run1" / "a.bin").write_bytes(b"\x00\x01payload") r = c.get("/b/run1/a.bin") assert r.status_code == 200 assert r.content == b"\x00\x01payload" def test_missing_booth_404(client): c, _ = client assert c.get("/b/nope/").status_code == 404 def test_traversal_rejected(client): c, data = client (data / "run1").mkdir() # a name with a slash or .. can never resolve to a direct child assert c.get("/b/..%2f..%2fetc/").status_code == 404 assert c.get("/b/run1/../../../etc/passwd").status_code == 404 def test_delete_form_wipes(client): c, data = client _touch(data / "run1" / "a.png") r = c.post("/b/run1/delete", follow_redirects=False) assert r.status_code == 303 assert not (data / "run1").exists() def test_delete_api_wipes(client): c, data = client _touch(data / "run1" / "a.png") r = c.request("DELETE", "/b/run1") assert r.status_code == 200 assert r.json() == {"wiped": "run1"} assert not (data / "run1").exists() def test_healthz(client): c, data = client _touch(data / "run1" / "a.png") r = c.get("/healthz") assert r.status_code == 200 body = r.json() assert body["ok"] is True and body["booths"] == 1