# tests/test_image_api.py — Image serving & viewer API (roadmap #108-B/C) import base64 from pathlib import Path import pytest PNG_1x1 = base64.b64decode( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAen63NgAAAAASUVORK5CYII=" ) SVG_DOC = b'' def _write_image(vault_dir: str, name: str, content: bytes) -> None: (Path(vault_dir) / name).write_bytes(content) # ═══════════════════════════════════════════════════════════════════ # /api/image — byte serving (BUG fixed in #108-B1) # ═══════════════════════════════════════════════════════════════════ class TestImageEndpoint: def test_serves_bytes_and_mime(self, client, test_vault_dir): _write_image(test_vault_dir, "pic.png", PNG_1x1) resp = client.get("/api/image/TestVault", params={"path": "pic.png"}) assert resp.status_code == 200 assert resp.headers["content-type"].startswith("image/png") assert resp.content == PNG_1x1 def test_missing_image_404(self, client): resp = client.get("/api/image/TestVault", params={"path": "nope.png"}) assert resp.status_code == 404 def test_svg_gets_sandbox_header(self, client, test_vault_dir): _write_image(test_vault_dir, "vector.svg", SVG_DOC) resp = client.get("/api/image/TestVault", params={"path": "vector.svg"}) assert resp.status_code == 200 assert resp.headers.get("content-security-policy") == "sandbox" assert resp.headers.get("x-content-type-options") == "nosniff" assert resp.content == SVG_DOC def test_png_keeps_the_global_csp(self, client, test_vault_dir): _write_image(test_vault_dir, "pic2.png", PNG_1x1) resp = client.get("/api/image/TestVault", params={"path": "pic2.png"}) csp = resp.headers.get("content-security-policy", "") assert csp != "sandbox" assert "default-src" in csp # ═══════════════════════════════════════════════════════════════════ # /api/file — standalone image view points at /api/image, not /raw # ═══════════════════════════════════════════════════════════════════ class TestImageViewMetadata: def test_file_view_uses_image_endpoint(self, client): resp = client.get("/api/file/TestVault", params={"path": "chatScreenshot.png"}) assert resp.status_code == 200 data = resp.json() assert data["is_image"] is True assert data["image_mime"] == "image/png" assert data["size_bytes"] > 0 assert "/api/image/TestVault?path=chatScreenshot.png" in data["html"] # The JSON raw endpoint must NOT be used as an source. assert "/raw?path=" not in data["html"] def test_file_view_url_encoded(self, client, test_vault_dir): _write_image(test_vault_dir, "café image.png", PNG_1x1) resp = client.get("/api/file/TestVault", params={"path": "café image.png"}) assert resp.status_code == 200 html = resp.json()["html"] assert "/api/image/TestVault?path=caf%C3%A9%20image.png" in html # ═══════════════════════════════════════════════════════════════════ # /api/media/{vault}/thumb — thumbnails (roadmap #108-C) # ═══════════════════════════════════════════════════════════════════ class TestThumbnailEndpoint: def test_png_thumbnail_is_webp(self, client, tmp_path, monkeypatch): pytest.importorskip("PIL") monkeypatch.setenv("OBSIGATE_DATA_DIR", str(tmp_path / "data")) resp = client.get( "/api/media/TestVault/thumb", params={"path": "chatScreenshot.png", "size": 64}, ) assert resp.status_code == 200 assert resp.headers["content-type"].startswith("image/webp") def test_svg_thumbnail_falls_back_to_original(self, client, tmp_path, monkeypatch): monkeypatch.setenv("OBSIGATE_DATA_DIR", str(tmp_path / "data")) resp = client.get( "/api/media/TestVault/thumb", params={"path": "vector-icon.svg"}, ) assert resp.status_code == 200 assert "svg" in resp.headers["content-type"] def test_thumb_rejects_non_image(self, client): resp = client.get( "/api/media/TestVault/thumb", params={"path": "config.json"}, ) assert resp.status_code == 400 def test_missing_thumb_404(self, client): resp = client.get( "/api/media/TestVault/thumb", params={"path": "nope.png"}, ) assert resp.status_code == 404