feat: add tests for authorization guards and role-based access control
This commit is contained in:
208
tests/test_authorization_integration.py
Normal file
208
tests/test_authorization_integration.py
Normal file
@@ -0,0 +1,208 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from fastapi import Request, status
|
||||
|
||||
from dependencies import get_auth_session
|
||||
from models import MiningOperationType, Project, User
|
||||
from services.session import AuthSession, SessionTokens
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def auth_session_context(client):
|
||||
"""Allow tests to swap the current auth session for the test app."""
|
||||
|
||||
original_override = client.app.dependency_overrides[get_auth_session]
|
||||
|
||||
@contextmanager
|
||||
def _use(user: User | None):
|
||||
def _override(request: Request) -> AuthSession:
|
||||
if user is None:
|
||||
session = AuthSession.anonymous()
|
||||
else:
|
||||
session = AuthSession(tokens=SessionTokens(
|
||||
access_token="token", refresh_token="refresh"))
|
||||
session.user = user
|
||||
request.state.auth_session = session
|
||||
return session
|
||||
|
||||
client.app.dependency_overrides[get_auth_session] = _override
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
client.app.dependency_overrides[get_auth_session] = original_override
|
||||
|
||||
return _use
|
||||
|
||||
|
||||
def _unique(name: str) -> str:
|
||||
return f"{name}-{uuid4().hex}"
|
||||
|
||||
|
||||
def _create_user(uow, *, roles: tuple[str, ...] = (), is_superuser: bool = False) -> User:
|
||||
assert uow.users and uow.roles
|
||||
uow.ensure_default_roles()
|
||||
user = User(
|
||||
email=f"{_unique('user')}@example.com",
|
||||
username=_unique('user'),
|
||||
password_hash=User.hash_password("password"),
|
||||
is_active=True,
|
||||
is_superuser=is_superuser,
|
||||
)
|
||||
uow.users.create(user)
|
||||
uow.flush()
|
||||
|
||||
for role_name in roles:
|
||||
role = uow.roles.get_by_name(role_name)
|
||||
if role is None: # pragma: no cover - defensive guard
|
||||
raise AssertionError(f"Role {role_name} not found")
|
||||
uow.users.assign_role(user_id=user.id, role_id=role.id)
|
||||
return uow.users.get(user.id, with_roles=True)
|
||||
|
||||
|
||||
def _create_project(uow) -> Project:
|
||||
assert uow.projects
|
||||
project = Project(
|
||||
name=_unique('project'),
|
||||
location="Integration Site",
|
||||
operation_type=MiningOperationType.OPEN_PIT,
|
||||
)
|
||||
uow.projects.create(project)
|
||||
uow.flush()
|
||||
return project
|
||||
|
||||
|
||||
class TestAuthenticationRequirements:
|
||||
def test_api_projects_list_requires_login(self, client, auth_session_context):
|
||||
with auth_session_context(None):
|
||||
response = client.get("/projects")
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert response.json()["detail"] == "Authentication required."
|
||||
|
||||
def test_ui_project_list_requires_login(self, client, auth_session_context):
|
||||
with auth_session_context(None):
|
||||
response = client.get("/projects/ui")
|
||||
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
class TestRoleRestrictions:
|
||||
def test_api_projects_create_forbidden_for_viewer(
|
||||
self,
|
||||
client,
|
||||
auth_session_context,
|
||||
unit_of_work_factory,
|
||||
) -> None:
|
||||
with unit_of_work_factory() as uow:
|
||||
viewer = _create_user(uow, roles=("viewer",))
|
||||
|
||||
payload = {
|
||||
"name": _unique("project"),
|
||||
"location": "Restricted",
|
||||
"operation_type": MiningOperationType.OPEN_PIT.value,
|
||||
"description": "Test restriction",
|
||||
}
|
||||
with auth_session_context(viewer):
|
||||
response = client.post("/projects", json=payload)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.json()[
|
||||
"detail"] == "Insufficient permissions for this action."
|
||||
|
||||
def test_api_projects_create_allows_project_manager(
|
||||
self,
|
||||
client,
|
||||
auth_session_context,
|
||||
unit_of_work_factory,
|
||||
) -> None:
|
||||
with unit_of_work_factory() as uow:
|
||||
manager = _create_user(uow, roles=("project_manager",))
|
||||
|
||||
payload = {
|
||||
"name": _unique("managed-project"),
|
||||
"location": "Permitted",
|
||||
"operation_type": MiningOperationType.OPEN_PIT.value,
|
||||
"description": "Authorized creation",
|
||||
}
|
||||
with auth_session_context(manager):
|
||||
response = client.post("/projects", json=payload)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.json()["name"] == payload["name"]
|
||||
|
||||
def test_api_projects_update_forbidden_for_viewer(
|
||||
self,
|
||||
client,
|
||||
auth_session_context,
|
||||
unit_of_work_factory,
|
||||
) -> None:
|
||||
with unit_of_work_factory() as uow:
|
||||
project = _create_project(uow)
|
||||
viewer = _create_user(uow, roles=("viewer",))
|
||||
|
||||
with auth_session_context(viewer):
|
||||
response = client.put(
|
||||
f"/projects/{project.id}",
|
||||
json={"description": "Updated"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.json()[
|
||||
"detail"] == "Insufficient role permissions for this action."
|
||||
|
||||
def test_api_projects_update_allows_manager(
|
||||
self,
|
||||
client,
|
||||
auth_session_context,
|
||||
unit_of_work_factory,
|
||||
) -> None:
|
||||
with unit_of_work_factory() as uow:
|
||||
project = _create_project(uow)
|
||||
manager = _create_user(uow, roles=("project_manager",))
|
||||
|
||||
updated_description = "Manager updated description"
|
||||
with auth_session_context(manager):
|
||||
response = client.put(
|
||||
f"/projects/{project.id}",
|
||||
json={"description": updated_description},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["description"] == updated_description
|
||||
|
||||
def test_ui_project_edit_forbidden_for_viewer(
|
||||
self,
|
||||
client,
|
||||
auth_session_context,
|
||||
unit_of_work_factory,
|
||||
) -> None:
|
||||
with unit_of_work_factory() as uow:
|
||||
project = _create_project(uow)
|
||||
viewer = _create_user(uow, roles=("viewer",))
|
||||
|
||||
with auth_session_context(viewer):
|
||||
response = client.get(f"/projects/{project.id}/edit")
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.json()[
|
||||
"detail"] == "Insufficient role permissions for this action."
|
||||
|
||||
def test_ui_project_edit_accessible_to_manager(
|
||||
self,
|
||||
client,
|
||||
auth_session_context,
|
||||
unit_of_work_factory,
|
||||
) -> None:
|
||||
with unit_of_work_factory() as uow:
|
||||
project = _create_project(uow)
|
||||
manager = _create_user(uow, roles=("project_manager",))
|
||||
|
||||
with auth_session_context(manager):
|
||||
response = client.get(f"/projects/{project.id}/edit")
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.template.name == "projects/form.html"
|
||||
Reference in New Issue
Block a user