from types import SimpleNamespace import pytest from fastapi.testclient import TestClient import app.main as main from app import __version__ class DummySettings(SimpleNamespace): @property def auth_enabled(self): return bool(self.auth_token) @pytest.fixture(autouse=True) def reset_settings(monkeypatch): monkeypatch.setattr( main, "settings", DummySettings( env="test", docs_enabled=True, test_ui_enabled=True, auth_token="", auth_header_name="X-API-Key", max_upload_bytes=1024 * 1024, allowed_mime_types=("image/jpeg", "image/png", "image/webp", "image/gif"), ), ) @pytest.fixture() def client(): return TestClient(main.app) def test_health_reflects_settings(client): resp = client.get("/health") assert resp.status_code == 200 assert resp.json() == { "ok": True, "env": "test", "version": __version__, "docs_enabled": True, "test_ui_enabled": True, "auth_enabled": False, "auth_header": None, "max_upload_bytes": 1024 * 1024, "allowed_mime_types": ["image/jpeg", "image/png", "image/webp", "image/gif"], } def test_index_shows_disabled_message_when_ui_off(monkeypatch): monkeypatch.setattr( main, "settings", DummySettings( env="test", docs_enabled=True, test_ui_enabled=False, auth_token="", auth_header_name="X-API-Key", max_upload_bytes=1024 * 1024, allowed_mime_types=("image/jpeg", "image/png", "image/webp", "image/gif"), ), ) resp = TestClient(main.app).get("/") assert resp.status_code == 200 assert "Test UI is disabled." in resp.text assert resp.headers["cache-control"] == "no-store" def test_focus_accepts_bearer_auth_and_returns_payload(monkeypatch, client): monkeypatch.setattr( main, "settings", DummySettings( env="test", docs_enabled=True, test_ui_enabled=True, auth_token="secret", auth_header_name="X-API-Key", max_upload_bytes=1024 * 1024, allowed_mime_types=("image/jpeg", "image/png", "image/webp", "image/gif"), ), ) def fake_process_image(image_bytes, filename, buffer_ratio=0.15, detector="subject"): assert image_bytes == b"image-bytes" assert filename == "sample.jpg" assert buffer_ratio == 0.2 assert detector == "face" return { "filename": filename, "detector": detector, "method": "face_cascade", "buffer_ratio": buffer_ratio, "detected_bbox": {"x": 1, "y": 2, "w": 3, "h": 4}, "square_bbox": {"x": 0, "y": 0, "w": 8, "h": 8}, "source_size": {"width": 10, "height": 12}, "crop_data_url": "data:image/jpeg;base64,AAA=", "annotated_data_url": "data:image/jpeg;base64,BBB=", "mime_type": "image/jpeg", "crop_bytes": b"crop", } monkeypatch.setattr("app.vision.process_image", fake_process_image) resp = client.post( "/api/focus", headers={"Authorization": "Bearer secret"}, files={"file": ("sample.jpg", b"image-bytes", "image/jpeg")}, data={"buffer_ratio": "0.2", "detector": "face"}, ) assert resp.status_code == 200 assert resp.json()["method"] == "face_cascade" assert resp.json()["detected_bbox"] == {"x": 1, "y": 2, "w": 3, "h": 4} assert resp.json()["mime_type"] == "image/jpeg" def test_focus_rejects_unauthorized_request(monkeypatch, client): monkeypatch.setattr( main, "settings", DummySettings( env="test", docs_enabled=True, test_ui_enabled=True, auth_token="secret", auth_header_name="X-API-Key", max_upload_bytes=1024 * 1024, allowed_mime_types=("image/jpeg", "image/png", "image/webp", "image/gif"), ), ) resp = client.post( "/api/focus", files={"file": ("sample.jpg", b"image-bytes", "image/jpeg")}, ) assert resp.status_code == 401 assert resp.json() == {"detail": "unauthorized"} def test_focus_rejects_unsupported_content_type(monkeypatch, client): monkeypatch.setattr( main, "settings", DummySettings( env="test", docs_enabled=True, test_ui_enabled=True, auth_token="", auth_header_name="X-API-Key", max_upload_bytes=1024 * 1024, allowed_mime_types=("image/jpeg",), ), ) resp = client.post( "/api/focus", files={"file": ("sample.txt", b"image-bytes", "text/plain")}, ) assert resp.status_code == 415 assert resp.json() == {"detail": "unsupported content type: text/plain"} def test_focus_rejects_large_upload(monkeypatch, client): monkeypatch.setattr( main, "settings", DummySettings( env="test", docs_enabled=True, test_ui_enabled=True, auth_token="", auth_header_name="X-API-Key", max_upload_bytes=4, allowed_mime_types=("image/jpeg",), ), ) resp = client.post( "/api/focus", files={"file": ("sample.jpg", b"image-bytes", "image/jpeg")}, ) assert resp.status_code == 413 assert resp.json() == {"detail": "upload exceeds FACE_LOCK_MAX_UPLOAD_BYTES (4 bytes)"} def test_focus_image_returns_jpeg_stream(monkeypatch, client): def fake_process_image(image_bytes, filename, buffer_ratio=0.15, detector="subject"): return { "filename": filename, "detector": detector, "method": "subject_contour", "buffer_ratio": buffer_ratio, "detected_bbox": {"x": 1, "y": 2, "w": 3, "h": 4}, "square_bbox": {"x": 0, "y": 0, "w": 8, "h": 8}, "source_size": {"width": 10, "height": 12}, "crop_data_url": "data:image/jpeg;base64,AAA=", "annotated_data_url": "data:image/jpeg;base64,BBB=", "mime_type": "image/jpeg", "crop_bytes": b"crop-bytes", } monkeypatch.setattr("app.vision.process_image", fake_process_image) resp = client.post( "/api/focus/image", files={"file": ("sample.jpg", b"image-bytes", "image/jpeg")}, ) assert resp.status_code == 200 assert resp.headers["content-type"] == "image/jpeg" assert resp.headers["content-disposition"] == 'inline; filename="sample-crop.jpg"' assert resp.content == b"crop-bytes"