"""Tests for FastAPI routes and validation.""" from __future__ import annotations from pathlib import Path import pytest from app import config, images, pipeline from app.main import _safe_job_file, _validate_job_id, app from fastapi import HTTPException from fastapi.testclient import TestClient from PIL import Image @pytest.fixture def client(data_dir: Path) -> TestClient: return TestClient(app) def _write_test_png(path: Path, size: tuple[int, int] = (1200, 800)) -> None: path.parent.mkdir(parents=True, exist_ok=True) Image.new("RGB", size, color=(40, 80, 120)).save(path, format="PNG") def test_validate_job_id_sanitizes_input() -> None: assert _validate_job_id("foo@bar") == "foo_bar" assert _validate_job_id("photo01") == "photo01" def test_validate_job_id_rejects_path_like() -> None: with pytest.raises(HTTPException) as exc: _validate_job_id("a/b") assert exc.value.status_code == 400 with pytest.raises(HTTPException): _validate_job_id("../evil") with pytest.raises(HTTPException): _validate_job_id("") def test_safe_job_file_rejects_wrong_prefix(data_dir: Path) -> None: stem = "photo01" paths = pipeline.job_paths(stem) paths.variants.mkdir(parents=True, exist_ok=True) target = paths.variants / f"{stem}_v1.png" target.write_bytes(b"x") with pytest.raises(HTTPException): _safe_job_file(stem, "other_v1.png") def test_index_empty(client: TestClient) -> None: response = client.get("/") assert response.status_code == 200 assert config.SITE_TITLE in response.text def test_job_not_found(client: TestClient) -> None: response = client.get("/jobs/does-not-exist") assert response.status_code == 404 def test_job_file_srcset_resize(client: TestClient, data_dir: Path) -> None: stem = "photo01" paths = pipeline.job_paths(stem) png = paths.variants / f"{stem}_v1.png" _write_test_png(png) pipeline.write_status(paths, "done", created_at="2026-01-01T00:00:00") pipeline.write_manifest( paths, { "variants": [{"id": "v1", "file": png.name}], "created_at": "2026-01-01T00:00:00", }, ) full = client.get(f"/jobs/{stem}/files/{png.name}") assert full.status_code == 200 assert full.headers["content-type"].startswith("image/") bad = client.get(f"/jobs/{stem}/files/{png.name}?w=999") assert bad.status_code == 400 resized = client.get(f"/jobs/{stem}/files/{png.name}?w=320") assert resized.status_code == 200 assert resized.headers["content-type"] == "image/webp" assert len(resized.content) < len(full.content) cached = config.WEB_CACHE_DIR / f"{png.name}.w320.webp" assert cached.is_file() with Image.open(cached) as img: assert img.width == 320 index = client.get("/") assert index.status_code == 200 assert f"/jobs/{stem}/files/{png.name}?w=640" in index.text assert "srcset=" in index.text assert "data-full-src=" in index.text def test_img_attrs_builds_srcset() -> None: attrs = images.img_attrs("job1", "job1_v1.png") assert attrs["src"].endswith("?w=640") assert "320w" in attrs["srcset"] assert attrs["full_src"].endswith("?w=1280")