| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596 |
- from typing import Any
- from uuid import uuid4
- import pytest
- from httpx import AsyncClient
- from app.main import app
- from app.models import OrgRole
- # Type alias for our route definition: (Method, Endpoint Name, Path Params, JSON Payload)
- RouteSpec = tuple[str, str, dict[str, Any], dict[str, Any] | None]
- class SharedProjectAccessTests:
- """
- A reusable interface for testing cross-cutting concerns (Auth, Permissions, 404s)
- across project-scoped API endpoints.
- HOW TO USE:
- 1. Inherit from this class.
- 2. Apply `@pytest.mark.parametrize("route_spec", YOUR_ROUTES, ids=your_id_formatter)`
- to your child class.
- 3. Override the `resolved_route` fixture to map static placeholders (e.g., "PROJECT_ID")
- to actual database fixture objects at execution time.
- """
- @pytest.fixture
- def resolved_route(self, *args, **kwargs) -> RouteSpec:
- """
- INTERFACE FIXTURE: Child classes MUST override this fixture.
- This fixture is responsible for taking a static `route_spec` and swapping
- out string placeholders for real UUIDs generated by database fixtures.
- Returns:
- Tuple containing:
- - HTTP Method (str)
- - FastAPI endpoint name (str)
- - Path parameters (dict)
- - JSON payload (dict or None)
- """
- raise NotImplementedError(
- "Child classes inheriting from SharedProjectAccessTests must implement "
- "the `resolved_route` fixture to inject real database IDs."
- )
- async def test_requires_auth(self, client: AsyncClient, resolved_route: RouteSpec):
- """Ensures the endpoint returns 401 Unauthorized if no token is provided."""
- method, endpoint, kwargs, payload = resolved_route
- url = app.url_path_for(endpoint, **kwargs)
- req = getattr(client, method.lower())
- response = await req(url, json=payload) if payload else await req(url)
- assert response.status_code == 401, f"{response.status_code} != 401"
- async def test_forbidden_other_org(
- self, client: AsyncClient, other_org_user, resolved_route: RouteSpec
- ):
- """Ensures a user from a DIFFERENT organization receives a 403 Forbidden."""
- method, endpoint, kwargs, payload = resolved_route
- if "project_id" not in kwargs:
- pytest.skip("Not a project-scoped route. Skipping 403 check.")
- url = app.url_path_for(endpoint, **kwargs)
- _, headers = other_org_user
- req = getattr(client, method.lower())
- response = (
- await req(url, json=payload, headers=headers)
- if payload
- else await req(url, headers=headers)
- )
- assert response.status_code == 403, f"{response.status_code} != 403"
- async def test_project_not_found(
- self, client: AsyncClient, make_org_user, resolved_route: RouteSpec
- ):
- """Ensures that if the project does not exist, the API returns a 404."""
- method, endpoint, kwargs, payload = resolved_route
- _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
- # Inject a non-existent project_id
- kwargs_copy = kwargs.copy()
- if "project_id" in kwargs_copy:
- kwargs_copy["project_id"] = str(uuid4())
- else:
- pytest.skip("Not a project-scoped route. Skipping 403 check.")
- url = app.url_path_for(endpoint, **kwargs_copy)
- req = getattr(client, method.lower())
- response = (
- await req(url, json=payload, headers=headers)
- if payload
- else await req(url, headers=headers)
- )
- assert response.status_code == 404
|