160 lines
5.3 KiB
Python
160 lines
5.3 KiB
Python
|
|
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
|