image-captioning-api / backend /app /tests /test_captions.py
apoorvrajdev's picture
test(backend): add FastAPI route tests with fake predictor service
785dbd5
"""Tests for ``POST /v1/captions``.
Covers the route's status-code contract end-to-end:
* 200 β€” happy path, typed ``CaptionResponse`` body
* 400 β€” empty file upload
* 413 β€” payload above ``max_upload_bytes``
* 415 β€” disallowed content type
* 422 β€” bytes that the predictor cannot decode (synthetic)
* 503 β€” predictor not yet loaded
"""
from __future__ import annotations
from fastapi.testclient import TestClient
from app.tests.conftest import FakePredictorService
def _image_field(payload: bytes, content_type: str = "image/jpeg", name: str = "a.jpg"):
return {"image": (name, payload, content_type)}
def test_captions_happy_path_returns_typed_response(
client: TestClient, fake_service: FakePredictorService
) -> None:
response = client.post("/v1/captions", files=_image_field(b"\xff\xd8stub"))
assert response.status_code == 200
body = response.json()
assert body["caption"] == "a test caption"
assert body["model_version"] == "test-v0"
assert body["decode_strategy"] == "greedy"
assert body["latency_ms"] == 1.23
assert body["request_id"]
# Service actually received the upload payload.
assert fake_service.calls == [b"\xff\xd8stub"]
def test_captions_request_id_matches_response_header(client: TestClient) -> None:
response = client.post(
"/v1/captions",
files=_image_field(b"\xff\xd8stub"),
headers={"x-request-id": "trace-123"},
)
assert response.status_code == 200
assert response.headers.get("x-request-id") == "trace-123"
assert response.json()["request_id"] == "trace-123"
def test_captions_rejects_unsupported_content_type(client: TestClient) -> None:
response = client.post(
"/v1/captions",
files=_image_field(b"hello", content_type="text/plain", name="a.txt"),
)
assert response.status_code == 415
assert "Unsupported content type" in response.json()["detail"]
def test_captions_rejects_empty_upload(client: TestClient) -> None:
response = client.post("/v1/captions", files=_image_field(b""))
assert response.status_code == 400
assert "Empty" in response.json()["detail"]
def test_captions_rejects_oversize_upload(client: TestClient) -> None:
# Fake service.max_upload_bytes = 1024
response = client.post("/v1/captions", files=_image_field(b"x" * 2048))
assert response.status_code == 413
assert "limit" in response.json()["detail"].lower()
def test_captions_returns_422_on_decode_failure(build_client) -> None:
bad_service = FakePredictorService(raise_decode_error=True)
with build_client(bad_service) as test_client:
response = test_client.post("/v1/captions", files=_image_field(b"\xff\xd8junk"))
assert response.status_code == 422
assert "synthetic decode failure" in response.json()["detail"]
def test_captions_returns_503_when_predictor_not_loaded(
client_without_service: TestClient,
) -> None:
response = client_without_service.post("/v1/captions", files=_image_field(b"\xff\xd8stub"))
assert response.status_code == 503
assert "not ready" in response.json()["detail"].lower()