Files
mimic-big/backend/tests/unit/test_user_schemas.py
knacky 4bade795fd test(backend): sprint 2 unit + integration coverage
Unit (`tests/unit/test_user_schemas.py`):
- 4 tests on `UserCreate` (happy path, password min length, email
  validation, invalid type).
- 2 tests on `UserUpdate` (all-optional, password validation when set).
- 3 tests on `EngagementMemberCreate` (default `"member"`, explicit role,
  max-length 40).
- 4 tests on `PageQuery` (defaults, offset arithmetic, page_size cap,
  page lower bound).

Integration (`tests/integration/test_user_mgmt_e2e.py`, marked
`integration`):
- The critical MA6-in-practice flow: rt_lead creates rt_operator, assigns
  to engagement A, the operator signs in, lists engagements and sees only
  A, `GET /engagements/B` returns 404 (anti-leak), `GET /engagements/B/members`
  returns 404 too, `/engagements/A/members` is reachable, `GET /users` is
  forbidden for the operator.
- `USER_MANAGE` gate: anonymous → 401, operator session → 403,
  lead session → 200.
- 409 `email_taken` on duplicate `POST /users`.
- `/audit/log` is lead-only, paginates with `page_size`, filters by
  `?action=`.
- Disabling a user blocks subsequent logins (same uniform
  `invalid_credentials` envelope as for bad passwords — no enumeration
  leak of "this account was disabled").

74 unit tests pass (61 sprint 1 + 13 sprint 2); integration tests run on
the testcontainers Postgres fixture in CI.
2026-05-23 15:53:35 +02:00

95 lines
3.1 KiB
Python

"""User/member DTO validation (no DB)."""
from __future__ import annotations
from uuid import uuid4
import pytest
from pydantic import ValidationError
from mimic.db.types import UserType
from mimic.schemas import EngagementMemberCreate, UserCreate, UserUpdate
from mimic.schemas.pagination import MAX_PAGE_SIZE, PageQuery
class TestUserCreate:
def test_minimal_valid_payload(self) -> None:
u = UserCreate.model_validate(
{
"email": "alice@example.org",
"password": "longenough",
"type": "rt_operator",
}
)
assert u.email == "alice@example.org"
assert u.type is UserType.RT_OPERATOR
assert u.display_name is None
def test_password_min_length(self) -> None:
with pytest.raises(ValidationError):
UserCreate.model_validate(
{"email": "a@b.c", "password": "short", "type": "rt_operator"}
)
def test_invalid_email_rejected(self) -> None:
with pytest.raises(ValidationError):
UserCreate.model_validate(
{"email": "not-an-email", "password": "longenough", "type": "rt_operator"}
)
def test_invalid_type_rejected(self) -> None:
with pytest.raises(ValidationError):
UserCreate.model_validate(
{"email": "a@b.c", "password": "longenough", "type": "not-a-role"}
)
class TestUserUpdate:
def test_all_optional(self) -> None:
u = UserUpdate.model_validate({})
assert u.display_name is None
assert u.password is None
assert u.type is None
def test_password_validates_when_provided(self) -> None:
with pytest.raises(ValidationError):
UserUpdate.model_validate({"password": "tiny"})
class TestEngagementMemberCreate:
def test_default_role(self) -> None:
member = EngagementMemberCreate.model_validate({"user_id": str(uuid4())})
assert member.role == "member"
def test_explicit_role(self) -> None:
member = EngagementMemberCreate.model_validate(
{"user_id": str(uuid4()), "role": "lead-on-mission"}
)
assert member.role == "lead-on-mission"
def test_role_max_length(self) -> None:
with pytest.raises(ValidationError):
EngagementMemberCreate.model_validate({"user_id": str(uuid4()), "role": "x" * 41})
class TestPageQuery:
def test_defaults(self) -> None:
page = PageQuery.model_validate({})
assert page.page == 1
assert page.page_size == 50
assert page.offset == 0
assert page.limit == 50
def test_offset_arithmetic(self) -> None:
page = PageQuery.model_validate({"page": 4, "page_size": 25})
assert page.offset == 75
assert page.limit == 25
def test_page_size_clamped(self) -> None:
with pytest.raises(ValidationError):
PageQuery.model_validate({"page_size": MAX_PAGE_SIZE + 1})
def test_page_lower_bound(self) -> None:
with pytest.raises(ValidationError):
PageQuery.model_validate({"page": 0})