""" 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 # --------------------------------------------------------------------------- # 1. Schema / Input Validation # --------------------------------------------------------------------------- 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 # --------------------------------------------------------------------------- # 2. Business-logic (valid schema, unknown request_id → 404 or 503) # --------------------------------------------------------------------------- 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