| """ |
| Functional tests — POST /generate/async endpoint |
| ================================================== |
| Tests for the async (RQ-queue) document generation endpoint. |
| |
| Key behaviours exercised: |
| • Schema validation (422) for missing / bad fields |
| • 404 when request_id not in Supabase |
| • 503 behaviour when Redis queue is unavailable (if applicable) |
| • Response contract when request is queued successfully |
| • Same prompt_params validation as /generate/pdf (shared schema) |
| """ |
| import copy |
| import pytest |
| import requests |
| from tests.conftest import ( |
| BASE_URL, TIMEOUT, SEED_IMAGE_URL, |
| NONEXISTENT_REQUEST_ID, MINIMAL_GENERATE_PAYLOAD, |
| ) |
|
|
| ENDPOINT = f"{BASE_URL}/generate/async" |
|
|
|
|
| def make_payload(**overrides): |
| payload = copy.deepcopy(MINIMAL_GENERATE_PAYLOAD) |
| payload.update(overrides) |
| return payload |
|
|
|
|
| def make_prompt_override(**kw): |
| pp = copy.deepcopy(MINIMAL_GENERATE_PAYLOAD["prompt_params"]) |
| pp.update(kw) |
| return pp |
|
|
|
|
| |
| |
| |
|
|
| class TestGenerateAsyncInputValidation: |
| """FastAPI must reject malformed requests before any business logic.""" |
|
|
| def test_missing_request_id_returns_422(self, http): |
| payload = { |
| "seed_images": [SEED_IMAGE_URL], |
| "prompt_params": MINIMAL_GENERATE_PAYLOAD["prompt_params"], |
| } |
| r = http.post(ENDPOINT, json=payload, timeout=TIMEOUT) |
| assert r.status_code == 422 |
|
|
| def test_empty_seed_images_returns_422(self, http): |
| r = http.post(ENDPOINT, json=make_payload(seed_images=[]), timeout=TIMEOUT) |
| assert r.status_code == 422 |
|
|
| def test_too_many_seed_images_returns_422(self, http): |
| r = http.post(ENDPOINT, |
| json=make_payload(seed_images=[SEED_IMAGE_URL] * 11), |
| timeout=TIMEOUT) |
| assert r.status_code == 422 |
|
|
| def test_invalid_seed_image_url_returns_422(self, http): |
| r = http.post(ENDPOINT, |
| json=make_payload(seed_images=["not-a-url"]), |
| timeout=TIMEOUT) |
| assert r.status_code == 422 |
|
|
| def test_num_solutions_below_min_returns_422(self, http): |
| pp = make_prompt_override(num_solutions=0) |
| r = http.post(ENDPOINT, json=make_payload(prompt_params=pp), timeout=TIMEOUT) |
| assert r.status_code == 422 |
|
|
| def test_num_solutions_above_max_returns_422(self, http): |
| pp = make_prompt_override(num_solutions=6) |
| r = http.post(ENDPOINT, json=make_payload(prompt_params=pp), timeout=TIMEOUT) |
| assert r.status_code == 422 |
|
|
| def test_empty_body_returns_422(self, http): |
| r = http.post(ENDPOINT, json={}, timeout=TIMEOUT) |
| assert r.status_code == 422 |
|
|
|
|
| |
| |
| |
|
|
| class TestGenerateAsyncBusinessLogic: |
| """ |
| With a valid schema but nonexistent request_id the API should: |
| • Return 404 if Redis is available (request_id lookup fails first), OR |
| • Return 503 if Redis is unavailable (queue not initialised) |
| Both are acceptable non-422 responses. |
| """ |
|
|
| def test_nonexistent_request_id_is_not_422(self, http): |
| r = http.post(ENDPOINT, json=MINIMAL_GENERATE_PAYLOAD, timeout=TIMEOUT) |
| assert r.status_code != 422, ( |
| f"Valid schema must not produce 422, got {r.status_code}" |
| ) |
|
|
| def test_nonexistent_request_id_returns_404_or_503(self, http): |
| r = http.post(ENDPOINT, json=MINIMAL_GENERATE_PAYLOAD, timeout=TIMEOUT) |
| assert r.status_code in (404, 503), ( |
| f"Expected 404 (no request) or 503 (no Redis), got {r.status_code}: {r.text}" |
| ) |
|
|
| def test_error_response_is_json(self, http): |
| r = http.post(ENDPOINT, json=MINIMAL_GENERATE_PAYLOAD, timeout=TIMEOUT) |
| assert "application/json" in r.headers.get("Content-Type", "") |
|
|
| def test_error_response_has_detail(self, http): |
| r = http.post(ENDPOINT, json=MINIMAL_GENERATE_PAYLOAD, timeout=TIMEOUT) |
| body = r.json() |
| assert "detail" in body, f"Error body must have 'detail'. Got: {body}" |
|
|
| def test_swagger_string_tokens_not_422(self, http): |
| payload = make_payload( |
| google_drive_token="string", |
| google_drive_refresh_token="string", |
| ) |
| r = http.post(ENDPOINT, json=payload, timeout=TIMEOUT) |
| assert r.status_code != 422 |
|
|
| def test_none_google_tokens_accepted(self, http): |
| payload = make_payload(google_drive_token=None, google_drive_refresh_token=None) |
| r = http.post(ENDPOINT, json=payload, timeout=TIMEOUT) |
| assert r.status_code != 422 |
|
|
| def test_num_solutions_boundary_values_schema_valid(self, http): |
| for n in [1, 5]: |
| pp = make_prompt_override(num_solutions=n) |
| r = http.post(ENDPOINT, json=make_payload(prompt_params=pp), timeout=TIMEOUT) |
| assert r.status_code != 422, ( |
| f"num_solutions={n} should be schema-valid" |
| ) |
|
|
| def test_missing_prompt_params_uses_defaults(self, http): |
| payload = {"request_id": NONEXISTENT_REQUEST_ID} |
| r = http.post(ENDPOINT, json=payload, timeout=TIMEOUT) |
| assert r.status_code != 422 |
|
|