"""Authentication, authorisation and PII masking at the API boundary.""" from __future__ import annotations import pytest from django.urls import reverse from apps.accounts.models import Role, User from tests.conftest import PASSWORD pytestmark = pytest.mark.django_db def test_register_creates_a_viewer(api_client) -> None: # noqa: ANN001 response = api_client.post( reverse("v1:accounts:register"), { "email": "New.User@Example.com", "full_name": "New User", "password": PASSWORD, "password_confirm": PASSWORD, }, format="json", ) assert response.status_code == 201 assert response.data["role"] == Role.VIEWER # E-mails are normalised so "A@x.com" and "a@x.com" cannot both exist. assert User.objects.get(email="new.user@example.com") def test_register_rejects_mismatched_and_weak_passwords(api_client) -> None: # noqa: ANN001 url = reverse("v1:accounts:register") mismatch = api_client.post( url, {"email": "a@example.com", "password": PASSWORD, "password_confirm": "other"}, format="json", ) assert mismatch.status_code == 400 assert mismatch.data["error"]["code"] == "validation_error" weak = api_client.post( url, {"email": "b@example.com", "password": "12345678", "password_confirm": "12345678"}, format="json", ) assert weak.status_code == 400 def test_login_returns_tokens_and_identity(api_client, viewer) -> None: # noqa: ANN001 response = api_client.post( reverse("v1:accounts:login"), {"email": viewer.email, "password": PASSWORD}, format="json", ) assert response.status_code == 200 assert {"access", "refresh", "user"} <= set(response.data) assert response.data["user"]["role"] == Role.VIEWER def test_search_requires_authentication(api_client) -> None: # noqa: ANN001 response = api_client.get(reverse("v1:search:search")) assert response.status_code == 401 assert response.data["error"]["code"] in {"authentication_failed", "not_authenticated"} def test_reindex_is_admin_only( auth_client, viewer, admin_user, monkeypatch ) -> None: # noqa: ANN001 url = reverse("v1:search:reindex") assert auth_client(viewer).post(url).status_code == 403 monkeypatch.setattr( "apps.search.indexing.rebuild_index", lambda *_args, **_kwargs: {"indexed": 0} ) response = auth_client(admin_user).post(url) assert response.status_code in {202, 503} @pytest.mark.parametrize( ("role", "should_be_masked"), [(Role.VIEWER, True), (Role.ANALYST, False), (Role.ADMIN, False)], ) def test_contact_details_are_masked_for_unprivileged_roles( auth_client, make_user, sample_profiles, role: str, should_be_masked: bool ) -> None: # noqa: ANN001 profile = sample_profiles[0] profile.emails = [{"address": "sara.python@acme.com", "type": "professional"}] profile.phone_numbers = ["+19402058928"] profile.save(update_fields=["emails", "phone_numbers"]) user = make_user(f"{role}@example.com", role) response = auth_client(user).get(reverse("v1:profiles:detail", args=[profile.id])) assert response.status_code == 200 assert response.data["pii_masked"] is should_be_masked address = response.data["emails"][0]["address"] assert (address != "sara.python@acme.com") is should_be_masked def test_response_carries_a_request_id_header(api_client) -> None: # noqa: ANN001 response = api_client.get(reverse("v1:core:liveness")) assert response.status_code == 200 assert response["X-Request-ID"] def test_client_supplied_request_id_is_echoed(api_client) -> None: # noqa: ANN001 response = api_client.get(reverse("v1:core:liveness"), HTTP_X_REQUEST_ID="abc-123") assert response["X-Request-ID"] == "abc-123" def test_malicious_request_id_is_replaced(api_client) -> None: # noqa: ANN001 response = api_client.get(reverse("v1:core:liveness"), HTTP_X_REQUEST_ID="") assert "