""" Tests for server_app.py API endpoints — FastAPI TestClient against gr.Server. """ import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) import pytest from unittest.mock import patch FIXTURE_PATH = Path(__file__).resolve().parent / "fixtures" / "button_panel_accessible.png" # ── Helpers ────────────────────────────────────────────────────────────────── def image_to_base64(path: Path) -> str: """Encode a fixture image as base64 for JSON POST body.""" import base64 with open(path, "rb") as f: return base64.b64encode(f.read()).decode("utf-8") # ── Tests ───────────────────────────────────────────────────────────────────── class TestGenerateGalleryEndpoint: """POST /api/generate_gallery should return 8 base64 PNGs with labels.""" def test_generate_gallery_endpoint_returns_200(self): """Endpoint should respond with HTTP 200.""" from fastapi.testclient import TestClient from server_app import app client = TestClient(app) b64 = image_to_base64(FIXTURE_PATH) response = client.post("/api/generate_gallery", json={"image": b64}) assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.text}" def test_generate_gallery_endpoint_returns_8_items(self): """Response should contain exactly 8 gallery items.""" from fastapi.testclient import TestClient from server_app import app client = TestClient(app) b64 = image_to_base64(FIXTURE_PATH) response = client.post("/api/generate_gallery", json={"image": b64}) assert response.status_code == 200 data = response.json() assert "items" in data, f"Response missing 'items' key: {list(data.keys())}" assert len(data["items"]) == 8, f"Expected 8 items, got {len(data['items'])}" def test_generate_gallery_endpoint_items_have_image_and_label(self): """Each item should have 'image' (base64 str) and 'label' (str).""" from fastapi.testclient import TestClient from server_app import app client = TestClient(app) b64 = image_to_base64(FIXTURE_PATH) response = client.post("/api/generate_gallery", json={"image": b64}) assert response.status_code == 200 data = response.json() for i, item in enumerate(data["items"]): assert "image" in item, f"Item {i} missing 'image' key: {list(item.keys())}" assert "label" in item, f"Item {i} missing 'label' key: {list(item.keys())}" assert isinstance(item["image"], str), f"Item {i} 'image' not str: {type(item['image'])}" assert isinstance(item["label"], str), f"Item {i} 'label' not str: {type(item['label'])}" assert len(item["image"]) > 0, f"Item {i} 'image' is empty" assert len(item["label"]) > 0, f"Item {i} 'label' is empty" class TestAnalyzeVlmEndpoint: """POST /api/analyze_vlm should accept gallery JSON and return VLM result.""" def test_analyze_vlm_endpoint_returns_200(self): """Endpoint should respond with HTTP 200 when given a valid gallery.""" from fastapi.testclient import TestClient from server_app import app client = TestClient(app) # Send a small synthetic gallery (1 item) — may hit error path since VLM isn't mocked # but should still return 200 with embedded error JSON synthetic_gallery = [ {"image": image_to_base64(FIXTURE_PATH), "label": "Protanopia (red-blind)"} ] response = client.post("/api/analyze_vlm", json={"items": synthetic_gallery}) # 200 even on error path — errors are embedded in JSON assert response.status_code in (200, 400, 422), f"Unexpected status {response.status_code}" @patch("backend_api._analyze_all_perspectives") def test_analyze_vlm_endpoint_with_mocked_vlm(self, mock_analyze): """Mocked VLM should return consistent JSON shape in response.""" from fastapi.testclient import TestClient from server_app import app mock_analyze.return_value = { "findings": [ { "type": "Low Contrast", "wcag_criterion": "1.4.3", "description": "Button has insufficient contrast.", "severity": "serious", "location": "Bottom-right", } ], "summary": "1 issue found.", "passes": False, } client = TestClient(app) # Use real fixture but mock VLM so no network call synthetic_gallery = [ {"image": image_to_base64(FIXTURE_PATH), "label": "Deuteranopia (green-blind)"} ] response = client.post("/api/analyze_vlm", json={"items": synthetic_gallery}) assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.text}" data = response.json() assert "findings" in data, f"Missing 'findings' in response: {list(data.keys())}" assert "passes" in data, f"Missing 'passes' in response: {list(data.keys())}" assert data["passes"] is False def test_analyze_vlm_endpoint_rejects_empty_gallery(self): """Empty gallery list should be handled gracefully (returns error JSON, HTTP 200).""" from fastapi.testclient import TestClient from server_app import app client = TestClient(app) response = client.post("/api/analyze_vlm", json={"items": []}) # Endpoint returns error JSON with HTTP 200 (not 400) — error is in the body assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert "error" in data or data.get("findings") == [], "Empty gallery should return error or empty findings" class TestWcagReportEndpoint: """POST /api/wcag_report should accept VLM JSON and return markdown.""" def test_wcag_report_endpoint_returns_200(self): """Endpoint should respond with HTTP 200 for valid VLM JSON.""" from fastapi.testclient import TestClient from server_app import app vlm_json = { "findings": [ { "type": "Low Contrast", "wcag_criterion": "1.4.3", "description": "Submit button contrast is below minimum.", "severity": "serious", "location": "Bottom-right", } ], "summary": "1 issue.", "passes": False, } client = TestClient(app) response = client.post("/api/wcag_report", json=vlm_json) assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.text}" def test_wcag_report_endpoint_returns_markdown_string(self): """Response should be a markdown string containing report structure.""" from fastapi.testclient import TestClient from server_app import app vlm_json = { "findings": [ { "type": "Low Contrast", "wcag_criterion": "1.4.3", "description": "Submit button contrast ratio is 2.1:1.", "severity": "serious", "location": "Bottom-right", } ], "summary": "1 issue found.", "passes": False, } client = TestClient(app) response = client.post("/api/wcag_report", json=vlm_json) assert response.status_code == 200 body = response.text assert isinstance(body, str), f"Expected str response, got {type(body)}" assert "Overall:" in body, "Markdown should contain 'Overall:'" assert "Fail" in body, "Markdown should indicate failure" assert "Submit button" in body def test_wcag_report_endpoint_handles_error_json(self): """VLM JSON with 'error' key should return warning message in markdown.""" from fastapi.testclient import TestClient from server_app import app vlm_json = { "error": "MiniCPM endpoint timed out (cold-start may need ~90s). Try again.", "findings": [], "passes": False, } client = TestClient(app) response = client.post("/api/wcag_report", json=vlm_json) assert response.status_code == 200 assert "Warning" in response.text or "error" in response.text.lower() class TestCarouselRoute: """GET / should return HTML carousel with an iframe embedding /access.""" def test_root_route_returns_200(self): """GET / should return HTTP 200.""" from fastapi.testclient import TestClient from server_app import app client = TestClient(app) response = client.get("/") assert response.status_code == 200, f"Expected 200, got {response.status_code}" def test_root_route_returns_html(self): """GET / should return Content-Type text/html.""" from fastapi.testclient import TestClient from server_app import app client = TestClient(app) response = client.get("/") assert response.status_code == 200 assert "text/html" in response.headers.get("content-type", ""), \ f"Expected text/html, got {response.headers.get('content-type')}" def test_root_route_contains_iframe_to_access(self): """HTML should contain an iframe src pointing to /access.""" from fastapi.testclient import TestClient from server_app import app client = TestClient(app) response = client.get("/") assert response.status_code == 200 html = response.text assert 'src="/access"' in html or 'src="/access "' in html, \ "HTML should contain