File size: 3,126 Bytes
785dbd5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
"""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()