feat: add membership management tests; implement session and repository stubs for member addition and duplicate checks
Test / test (push) Successful in 15s
Details
Test / test (push) Successful in 15s
Details
This commit is contained in:
parent
1a3f3cc1e2
commit
ce652a7e48
|
|
@ -1,6 +1,7 @@
|
||||||
"""Unit tests for OrganizationService."""
|
"""Unit tests for OrganizationService."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import cast
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest # type: ignore[import-not-found]
|
import pytest # type: ignore[import-not-found]
|
||||||
|
|
@ -14,6 +15,7 @@ from app.services.organization_service import (
|
||||||
OrganizationContext,
|
OrganizationContext,
|
||||||
OrganizationContextMissingError,
|
OrganizationContextMissingError,
|
||||||
OrganizationForbiddenError,
|
OrganizationForbiddenError,
|
||||||
|
OrganizationMemberAlreadyExistsError,
|
||||||
OrganizationService,
|
OrganizationService,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -47,6 +49,40 @@ def make_membership(role: OrganizationRole, *, organization_id: int = 1, user_id
|
||||||
return membership
|
return membership
|
||||||
|
|
||||||
|
|
||||||
|
class SessionStub:
|
||||||
|
"""Minimal async session stub capturing writes."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.added: list[OrganizationMember] = []
|
||||||
|
self.committed: bool = False
|
||||||
|
self.refreshed: list[OrganizationMember] = []
|
||||||
|
|
||||||
|
def add(self, obj: OrganizationMember) -> None:
|
||||||
|
self.added.append(obj)
|
||||||
|
|
||||||
|
async def commit(self) -> None:
|
||||||
|
self.committed = True
|
||||||
|
|
||||||
|
async def refresh(self, obj: OrganizationMember) -> None:
|
||||||
|
self.refreshed.append(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class MembershipRepositoryStub(OrganizationRepository):
|
||||||
|
"""Repository stub that can emulate duplicate checks for add_member."""
|
||||||
|
|
||||||
|
def __init__(self, memberships: dict[tuple[int, int], OrganizationMember] | None = None) -> None:
|
||||||
|
self._session_stub = SessionStub()
|
||||||
|
super().__init__(session=cast(AsyncSession, self._session_stub))
|
||||||
|
self._memberships = memberships or {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def session_stub(self) -> SessionStub:
|
||||||
|
return self._session_stub
|
||||||
|
|
||||||
|
async def get_membership(self, organization_id: int, user_id: int) -> OrganizationMember | None:
|
||||||
|
return self._memberships.get((organization_id, user_id))
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_context_success() -> None:
|
async def test_get_context_success() -> None:
|
||||||
membership = make_membership(OrganizationRole.MANAGER)
|
membership = make_membership(OrganizationRole.MANAGER)
|
||||||
|
|
@ -97,3 +133,62 @@ def test_member_must_own_entity() -> None:
|
||||||
|
|
||||||
# Same owner should pass silently.
|
# Same owner should pass silently.
|
||||||
service.ensure_member_owns_entity(context=context, owner_id=membership.user_id)
|
service.ensure_member_owns_entity(context=context, owner_id=membership.user_id)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_member_succeeds_for_owner() -> None:
|
||||||
|
owner_membership = make_membership(OrganizationRole.OWNER, organization_id=7, user_id=1)
|
||||||
|
organization = owner_membership.organization
|
||||||
|
assert organization is not None
|
||||||
|
context = OrganizationContext(organization=organization, membership=owner_membership)
|
||||||
|
|
||||||
|
repo = MembershipRepositoryStub()
|
||||||
|
service = OrganizationService(repo)
|
||||||
|
|
||||||
|
result = await service.add_member(context=context, user_id=42, role=OrganizationRole.MANAGER)
|
||||||
|
|
||||||
|
assert result.organization_id == organization.id
|
||||||
|
assert result.user_id == 42
|
||||||
|
assert result.role == OrganizationRole.MANAGER
|
||||||
|
|
||||||
|
session_stub = repo.session_stub
|
||||||
|
assert session_stub.committed is True
|
||||||
|
assert session_stub.added and session_stub.added[0] is result
|
||||||
|
assert session_stub.refreshed and session_stub.refreshed[0] is result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_member_rejects_duplicate_membership() -> None:
|
||||||
|
owner_membership = make_membership(OrganizationRole.OWNER, organization_id=5, user_id=10)
|
||||||
|
organization = owner_membership.organization
|
||||||
|
assert organization is not None
|
||||||
|
context = OrganizationContext(organization=organization, membership=owner_membership)
|
||||||
|
|
||||||
|
duplicate_user_id = 55
|
||||||
|
existing = OrganizationMember(
|
||||||
|
organization_id=organization.id,
|
||||||
|
user_id=duplicate_user_id,
|
||||||
|
role=OrganizationRole.MEMBER,
|
||||||
|
)
|
||||||
|
repo = MembershipRepositoryStub({(organization.id, duplicate_user_id): existing})
|
||||||
|
service = OrganizationService(repo)
|
||||||
|
|
||||||
|
with pytest.raises(OrganizationMemberAlreadyExistsError):
|
||||||
|
await service.add_member(context=context, user_id=duplicate_user_id, role=OrganizationRole.MANAGER)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_member_requires_privileged_role() -> None:
|
||||||
|
member_context = make_membership(OrganizationRole.MEMBER, organization_id=3, user_id=77)
|
||||||
|
organization = member_context.organization
|
||||||
|
assert organization is not None
|
||||||
|
context = OrganizationContext(organization=organization, membership=member_context)
|
||||||
|
|
||||||
|
repo = MembershipRepositoryStub()
|
||||||
|
service = OrganizationService(repo)
|
||||||
|
|
||||||
|
with pytest.raises(OrganizationForbiddenError):
|
||||||
|
await service.add_member(context=context, user_id=99, role=OrganizationRole.MANAGER)
|
||||||
|
|
||||||
|
# Ensure DB work not attempted when permissions fail.
|
||||||
|
assert repo.session_stub.committed is False
|
||||||
Loading…
Reference in New Issue