"""Unit tests for OrganizationService.""" from __future__ import annotations from typing import cast from unittest.mock import MagicMock import pytest from app.models.organization import Organization from app.models.organization_member import OrganizationMember, OrganizationRole from app.repositories.org_repo import OrganizationRepository from app.services.organization_service import ( OrganizationAccessDeniedError, OrganizationContext, OrganizationContextMissingError, OrganizationForbiddenError, OrganizationMemberAlreadyExistsError, OrganizationService, ) from sqlalchemy.ext.asyncio import AsyncSession class StubOrganizationRepository(OrganizationRepository): """Simple in-memory stand-in for OrganizationRepository.""" def __init__(self, membership: OrganizationMember | None) -> None: super().__init__(session=MagicMock(spec=AsyncSession)) self._membership = membership async def get_membership( self, organization_id: int, user_id: int, ) -> OrganizationMember | None: # pragma: no cover - helper if ( self._membership and self._membership.organization_id == organization_id and self._membership.user_id == user_id ): return self._membership return None def make_membership( role: OrganizationRole, *, organization_id: int = 1, user_id: int = 10, ) -> OrganizationMember: organization = Organization(name="Acme Inc") organization.id = organization_id membership = OrganizationMember( organization_id=organization_id, user_id=user_id, role=role, ) membership.organization = organization 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 async def test_get_context_success() -> None: membership = make_membership(OrganizationRole.MANAGER) service = OrganizationService(StubOrganizationRepository(membership)) context = await service.get_context( user_id=membership.user_id, organization_id=membership.organization_id, ) assert context.organization_id == membership.organization_id assert context.role == OrganizationRole.MANAGER @pytest.mark.asyncio async def test_get_context_missing_header() -> None: service = OrganizationService(StubOrganizationRepository(None)) with pytest.raises(OrganizationContextMissingError): await service.get_context(user_id=1, organization_id=None) @pytest.mark.asyncio async def test_get_context_access_denied() -> None: service = OrganizationService(StubOrganizationRepository(None)) with pytest.raises(OrganizationAccessDeniedError): await service.get_context(user_id=1, organization_id=99) def test_ensure_can_manage_settings_blocks_manager() -> None: membership = make_membership(OrganizationRole.MANAGER) organization = membership.organization assert organization is not None context = OrganizationContext(organization=organization, membership=membership) service = OrganizationService(StubOrganizationRepository(membership)) with pytest.raises(OrganizationForbiddenError): service.ensure_can_manage_settings(context) def test_member_must_own_entity() -> None: membership = make_membership(OrganizationRole.MEMBER) organization = membership.organization assert organization is not None context = OrganizationContext(organization=organization, membership=membership) service = OrganizationService(StubOrganizationRepository(membership)) with pytest.raises(OrganizationForbiddenError): service.ensure_member_owns_entity(context=context, owner_id=999) # Same owner should pass silently. 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