# 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