"""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 "