from __future__ import annotations from collections.abc import AsyncIterator import httpx import pytest import json @pytest.fixture async def feedback_client(api_base_url: str) -> AsyncIterator[httpx.AsyncClient]: async with httpx.AsyncClient(base_url=api_base_url, timeout=30.0) as client: try: health = await client.get("/health") if health.status_code != 200: pytest.skip(f"API not ready: /health={health.status_code}") except Exception as exc: pytest.skip(f"API unavailable: {exc}") yield client @pytest.fixture async def authed_feedback_client( feedback_client: httpx.AsyncClient, test_identity: dict[str, str], ) -> httpx.AsyncClient: otp_response = await feedback_client.post( "/api/v1/auth/otp", json={"email": test_identity["email"]}, ) if otp_response.status_code not in (200, 204): pytest.skip(f"OTP request failed: {otp_response.status_code}") verify_response = await feedback_client.post( "/api/v1/auth/verify", json={ "email": test_identity["email"], "code": test_identity["code"], }, ) if verify_response.status_code != 200: pytest.skip(f"Auth verify failed: {verify_response.status_code}") token = verify_response.json().get("access_token") or verify_response.json().get( "session", {} ).get("access_token") if not token: pytest.skip("No access token in auth response") feedback_client.headers["Authorization"] = f"Bearer {token}" return feedback_client class TestFeedbackSubmitAnonymous: @pytest.mark.asyncio async def test_submit_feedback_anonymous_success( self, feedback_client: httpx.AsyncClient ): response = await feedback_client.post( "/api/v1/feedback", data={ "feedback_type": "bug", "content": "App crashes when opening settings", "device_info": json.dumps({"platform": "ios", "model": "iPhone 15"}), "app_version": "1.0.0", "os_version": "iOS 17.0", }, ) assert response.status_code == 201 body = response.json() assert "id" in body assert "created_at" in body @pytest.mark.asyncio async def test_submit_feedback_with_auth( self, authed_feedback_client: httpx.AsyncClient ): response = await authed_feedback_client.post( "/api/v1/feedback", data={ "feedback_type": "suggestion", "content": "Please add dark mode", "device_info": json.dumps({"platform": "android", "model": "Pixel 8"}), "app_version": "1.0.0", "os_version": "Android 14", }, ) assert response.status_code == 201 body = response.json() assert "id" in body assert "created_at" in body @pytest.mark.asyncio async def test_submit_feedback_content_empty( self, feedback_client: httpx.AsyncClient ): response = await feedback_client.post( "/api/v1/feedback", data={ "feedback_type": "bug", "content": "", "device_info": json.dumps({"platform": "ios", "model": "iPhone 15"}), "app_version": "1.0.0", "os_version": "iOS 17.0", }, ) assert response.status_code == 400 assert response.json().get("code") == "FEEDBACK_CONTENT_EMPTY" @pytest.mark.asyncio async def test_submit_feedback_content_too_long( self, feedback_client: httpx.AsyncClient ): response = await feedback_client.post( "/api/v1/feedback", data={ "feedback_type": "bug", "content": "x" * 501, "device_info": json.dumps({"platform": "ios", "model": "iPhone 15"}), "app_version": "1.0.0", "os_version": "iOS 17.0", }, ) assert response.status_code == 400 assert response.json().get("code") == "FEEDBACK_CONTENT_TOO_LONG" @pytest.mark.asyncio async def test_submit_feedback_invalid_device_info( self, feedback_client: httpx.AsyncClient ): response = await feedback_client.post( "/api/v1/feedback", data={ "feedback_type": "bug", "content": "Test content", "device_info": "not-json", "app_version": "1.0.0", "os_version": "iOS 17.0", }, ) assert response.status_code == 422 @pytest.mark.asyncio async def test_submit_feedback_with_image(self, feedback_client: httpx.AsyncClient): fake_image = b"\xff\xd8\xff\xe0" + b"\x00" * 100 response = await feedback_client.post( "/api/v1/feedback", data={ "feedback_type": "bug", "content": "Screenshot of the issue", "device_info": json.dumps({"platform": "ios", "model": "iPhone 15"}), "app_version": "1.0.0", "os_version": "iOS 17.0", }, files=[("images", ("screenshot.jpg", fake_image, "image/jpeg"))], ) assert response.status_code == 201 body = response.json() assert "id" in body