"""Live endpoint smoke tests: every registered API endpoint returns the expected status. Uses httpx.AsyncClient (via ASGITransport) – no real HTTP connection needed. Coverage target: 100% of non-docs, non-static endpoints. """ from __future__ import annotations from typing import Any import pytest from httpx import AsyncClient # --------------------------------------------------------------------------- # List of (method, path, payload, auth_required, expected_without_auth, expected_with_auth) # --------------------------------------------------------------------------- # # Rules: # - Endpoints marked JWT → 401 without token, 200/201/204 with valid token # - Public endpoints → 200/201/422 (validation error payload still returns expected) # - We only test that the endpoint *exists* and returns one of the expected codes # # When expected_without_auth is None, the test only sends the request *with* auth. ENDPOINTS: list[dict[str, Any]] = [ # --- health (public) --- {"method": "GET", "path": "/health", "auth_required": False, "expected": [200]}, {"method": "GET", "path": "/metrics", "auth_required": False, "expected": [200, 404]}, # --- auth (public) --- {"method": "POST", "path": "/api/v1/auth/register", "json": {"email": "e2e@t.com", "password": "Test1234!", "name": "E2E"}, "auth_required": False, "expected": [201, 403]}, {"method": "POST", "path": "/api/v1/auth/login", "data": {"username": "x@y.com", "password": "wrong"}, "auth_required": False, "expected": [401]}, {"method": "POST", "path": "/api/v1/auth/refresh", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200]}, {"method": "POST", "path": "/api/v1/auth/logout", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200]}, {"method": "POST", "path": "/api/v1/auth/password-reset/request", "json": {"email": "x@y.com"}, "auth_required": False, "expected": [200, 404, 405]}, # --- users (JWT) --- {"method": "GET", "path": "/api/v1/users/me", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200]}, {"method": "PATCH", "path": "/api/v1/users/me", "json": {"name": "X"}, "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200]}, {"method": "GET", "path": "/api/v1/users/", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200]}, {"method": "POST", "path": "/api/v1/users/", "json": {"email": "u2@t.com", "password": "Test1234!", "name": "U2", "role": "sales_rep"}, "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [201]}, {"method": "PATCH", "path": "/api/v1/users/999999", "json": {"name": "X"}, "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200, 404]}, {"method": "DELETE", "path": "/api/v1/users/999999", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [204, 404]}, # --- accounts (JWT) --- {"method": "POST", "path": "/api/v1/accounts/", "json": {"name": "A"}, "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [201]}, {"method": "GET", "path": "/api/v1/accounts/", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200]}, {"method": "GET", "path": "/api/v1/accounts/999999", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200, 404]}, {"method": "PATCH", "path": "/api/v1/accounts/999999", "json": {"name": "X"}, "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200, 404]}, {"method": "DELETE", "path": "/api/v1/accounts/999999", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [204, 404]}, # --- contacts (JWT) --- {"method": "POST", "path": "/api/v1/contacts/", "json": {"first_name": "F", "last_name": "L"}, "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [201]}, {"method": "GET", "path": "/api/v1/contacts/", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200]}, {"method": "GET", "path": "/api/v1/contacts/999999", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200, 404]}, {"method": "PATCH", "path": "/api/v1/contacts/999999", "json": {"first_name": "X"}, "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200, 404]}, {"method": "DELETE", "path": "/api/v1/contacts/999999", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [204, 404]}, # --- deals (JWT) --- {"method": "POST", "path": "/api/v1/deals/", "json": {"title": "D", "account_id": "999999"}, "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [201, 404]}, {"method": "GET", "path": "/api/v1/deals/", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200]}, {"method": "GET", "path": "/api/v1/deals/pipeline", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200]}, {"method": "GET", "path": "/api/v1/deals/999999", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200, 404]}, {"method": "PATCH", "path": "/api/v1/deals/999999", "json": {"title": "X"}, "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200, 404]}, {"method": "PATCH", "path": "/api/v1/deals/999999/stage", "json": {"stage": "qualified"}, "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200, 404]}, {"method": "DELETE", "path": "/api/v1/deals/999999", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [204, 404]}, # --- activities (JWT) --- {"method": "POST", "path": "/api/v1/activities/", "json": {"type": "task", "subject": "A", "account_id": 999999}, "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [201, 422]}, {"method": "GET", "path": "/api/v1/activities/", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200]}, {"method": "GET", "path": "/api/v1/activities/999999", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200, 404]}, {"method": "PATCH", "path": "/api/v1/activities/999999", "json": {"subject": "X"}, "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200, 404]}, {"method": "PATCH", "path": "/api/v1/activities/999999/complete", "json": {"outcome": "done"}, "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200, 404]}, {"method": "DELETE", "path": "/api/v1/activities/999999", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [204, 404]}, # --- notes (JWT) --- {"method": "POST", "path": "/api/v1/notes/", "json": {"body": "N", "parent_type": "account", "parent_id": "999999"}, "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [201, 404]}, {"method": "GET", "path": "/api/v1/notes/", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200]}, {"method": "GET", "path": "/api/v1/notes/999999", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200, 404]}, {"method": "PATCH", "path": "/api/v1/notes/999999", "json": {"body": "X"}, "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200, 404]}, {"method": "DELETE", "path": "/api/v1/notes/999999", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [204, 404]}, # --- tags (JWT) --- {"method": "POST", "path": "/api/v1/tags/", "json": {"name": "t1"}, "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [201]}, {"method": "GET", "path": "/api/v1/tags/", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200]}, {"method": "POST", "path": "/api/v1/tags/link", "json": {"tag_id": "999999", "parent_type": "account", "parent_id": "999999"}, "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [201, 404]}, {"method": "DELETE", "path": "/api/v1/tags/link", "json": {"tag_id": "999999", "parent_type": "account", "parent_id": "999999"}, "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [204, 404]}, # --- dashboard (JWT) --- {"method": "GET", "path": "/api/v1/dashboard/kpis", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200]}, {"method": "GET", "path": "/api/v1/dashboard/feed", "auth_required": True, "expected_without_auth": 401, "expected_with_auth": [200]}, ] # Derive pytest test IDs from the method+path _ENDPOINT_IDS = [f"{e['method']} {e['path']}" for e in ENDPOINTS] # --------------------------------------------------------------------------- # Tests WITHOUT auth (public endpoints) # --------------------------------------------------------------------------- @pytest.mark.parametrize( "endpoint", [e for e in ENDPOINTS if not e["auth_required"]], ids=[f"{e['method']} {e['path']}" for e in ENDPOINTS if not e["auth_required"]], ) @pytest.mark.asyncio async def test_public_endpoint(client: AsyncClient, endpoint: dict[str, Any]) -> None: """Every public endpoint returns a status in the expected list.""" method = endpoint["method"].upper() path = endpoint["path"] kwargs: dict[str, Any] = {} if "json" in endpoint: kwargs["json"] = endpoint["json"] if "data" in endpoint: kwargs["data"] = endpoint["data"] resp = await client.request(method, path, **kwargs) assert resp.status_code in endpoint["expected"], \ f"{method} {path} → {resp.status_code}, expected one of {endpoint['expected']}. Body: {resp.text[:300]}" # --------------------------------------------------------------------------- # Tests WITHOUT auth → expect 401 for JWT endpoints # --------------------------------------------------------------------------- @pytest.mark.parametrize( "endpoint", [e for e in ENDPOINTS if e["auth_required"]], ids=[f"{e['method']} {e['path']}" for e in ENDPOINTS if e["auth_required"]], ) @pytest.mark.asyncio async def test_endpoint_without_auth_returns_401(client: AsyncClient, endpoint: dict[str, Any]) -> None: """JWT endpoints return 401 when no Authorization header is present.""" method = endpoint["method"].upper() path = endpoint["path"] kwargs: dict[str, Any] = {} if "json" in endpoint: kwargs["json"] = endpoint["json"] if "data" in endpoint: kwargs["data"] = endpoint["data"] resp = await client.request(method, path, **kwargs) # Some endpoints might return 422 if the payload is invalid and auth is also missing. # We accept 401 (no auth) or 422 (validation before auth). In any case, it must NOT return 200/201/204. assert resp.status_code in (401, 422), \ f"{method} {path} (no auth) → {resp.status_code}, expected 401 or 422. Body: {resp.text[:300]}" # --------------------------------------------------------------------------- # Tests WITH auth → expect 200/201/204 for JWT endpoints # --------------------------------------------------------------------------- @pytest.mark.parametrize( "endpoint", [e for e in ENDPOINTS if e["auth_required"]], ids=[f"{e['method']} {e['path']}" for e in ENDPOINTS if e["auth_required"]], ) @pytest.mark.asyncio async def test_endpoint_with_auth_returns_success( client: AsyncClient, registered_user: dict[str, Any], endpoint: dict[str, Any] ) -> None: """JWT endpoints return one of the expected success codes with valid auth.""" method = endpoint["method"].upper() path = endpoint["path"] headers = registered_user["headers"] kwargs: dict[str, Any] = {"headers": headers} if "json" in endpoint: kwargs["json"] = endpoint["json"] if "data" in endpoint: kwargs["data"] = endpoint["data"] resp = await client.request(method, path, **kwargs) expected = endpoint.get("expected_with_auth", [200, 201, 204]) assert resp.status_code in expected, \ f"{method} {path} (with auth) → {resp.status_code}, expected one of {expected}. Body: {resp.text[:300]}"