| """Tests for the FastAPI backend.""" |
|
|
| from __future__ import annotations |
|
|
| import base64 |
| import io |
|
|
| import numpy as np |
| import pytest |
| from fastapi.testclient import TestClient |
| from PIL import Image |
|
|
| from signbridge.backend import app |
|
|
|
|
| @pytest.fixture() |
| def client() -> TestClient: |
| return TestClient(app) |
|
|
|
|
| def _frame_b64(rgb: tuple[int, int, int] = (128, 128, 128), size: int = 64) -> str: |
| arr = np.full((size, size, 3), rgb, dtype=np.uint8) |
| img = Image.fromarray(arr) |
| buf = io.BytesIO() |
| img.save(buf, format="JPEG", quality=80) |
| return base64.b64encode(buf.getvalue()).decode("ascii") |
|
|
|
|
| class TestHealth: |
| def test_healthz_returns_ok(self, client: TestClient) -> None: |
| r = client.get("/healthz") |
| assert r.status_code == 200 |
| assert r.json() == {"status": "ok"} |
|
|
| def test_info_returns_provider_block(self, client: TestClient) -> None: |
| r = client.get("/info") |
| assert r.status_code == 200 |
| body = r.json() |
| for key in ("provider", "composer_model", "vlm_model", "tts_model", "recognizer_mode"): |
| assert key in body |
|
|
|
|
| class TestRecognize: |
| def test_empty_frame_rejected(self, client: TestClient) -> None: |
| r = client.post("/recognize", json={"frame": ""}) |
| assert r.status_code in (400, 422) |
|
|
| def test_invalid_base64_rejected(self, client: TestClient) -> None: |
| r = client.post("/recognize", json={"frame": "%%%not-base64%%%"}) |
| assert r.status_code == 400 |
|
|
| def test_valid_frame_no_provider_returns_empty_token(self, client: TestClient) -> None: |
| |
| |
| r = client.post("/recognize", json={"frame": _frame_b64()}) |
| assert r.status_code == 200 |
| body = r.json() |
| assert body == {"token": "", "confidence": 0.0} |
|
|
| def test_data_url_prefix_tolerated(self, client: TestClient) -> None: |
| b64 = _frame_b64() |
| r = client.post( |
| "/recognize", json={"frame": f"data:image/jpeg;base64,{b64}"} |
| ) |
| assert r.status_code == 200 |
|
|
|
|
| class TestCompose: |
| def test_empty_signs(self, client: TestClient) -> None: |
| r = client.post("/compose", json={"signs": []}) |
| assert r.status_code == 200 |
| assert r.json() == {"sentence": ""} |
|
|
| def test_fingerspelled_word(self, client: TestClient) -> None: |
| r = client.post( |
| "/compose", json={"signs": ["L", "U", "C", "A", "S"]} |
| ) |
| assert r.status_code == 200 |
| |
| assert "Lucas" in r.json()["sentence"] |
|
|
| def test_glosses(self, client: TestClient) -> None: |
| r = client.post( |
| "/compose", json={"signs": ["hello", "thank_you"]} |
| ) |
| assert r.status_code == 200 |
| out = r.json()["sentence"].lower() |
| assert "hello" in out |
| assert "thank you" in out |
|
|
|
|
| class TestSpeak: |
| def test_empty_text_rejected(self, client: TestClient) -> None: |
| r = client.post("/speak", json={"text": ""}) |
| assert r.status_code == 400 |
|
|
| def test_returns_audio(self, client: TestClient) -> None: |
| r = client.post("/speak", json={"text": "hello"}) |
| assert r.status_code == 200 |
| assert r.headers["content-type"].startswith("audio/") |
| assert len(r.content) > 0 |
|
|
|
|
| class TestRecognizeFrames: |
| def test_empty_frames_rejected(self, client: TestClient) -> None: |
| r = client.post("/recognize", json={"frames": []}) |
| |
| |
| assert r.status_code in (400, 422) |
|
|
| def test_single_frame_in_list_rejected(self, client: TestClient) -> None: |
| |
| b64 = _frame_b64() |
| r = client.post("/recognize", json={"frames": [b64]}) |
| assert r.status_code == 400 |
| detail = r.json().get("detail", "").lower() |
| assert "at least 2" in detail or "2 frames" in detail or "frames" in detail |
|
|
| def test_valid_multi_frame_no_provider(self, client: TestClient) -> None: |
| b64 = _frame_b64() |
| r = client.post("/recognize", json={"frames": [b64, b64, b64, b64]}) |
| assert r.status_code == 200 |
| assert r.json() == {"token": "", "confidence": 0.0} |
|
|
| def test_too_many_frames_rejected(self, client: TestClient) -> None: |
| b64 = _frame_b64() |
| r = client.post("/recognize", json={"frames": [b64] * 100}) |
| assert r.status_code in (400, 422) |
|
|
| def test_oversized_frame_rejected(self, client: TestClient) -> None: |
| |
| big = "A" * (6 * 1024 * 1024) |
| r = client.post("/recognize", json={"frame": big}) |
| assert r.status_code in (400, 422) |
|
|
| def test_both_frame_and_frames_rejected(self, client: TestClient) -> None: |
| b64 = _frame_b64() |
| r = client.post( |
| "/recognize", |
| json={"frame": b64, "frames": [b64, b64]}, |
| ) |
| assert r.status_code in (400, 422) |
|
|
| def test_neither_frame_nor_frames_rejected(self, client: TestClient) -> None: |
| r = client.post("/recognize", json={}) |
| assert r.status_code in (400, 422) |
|
|