"""Unit tests for TaskService.""" from __future__ import annotations import uuid from collections.abc import AsyncGenerator from datetime import datetime, timedelta, timezone import pytest import pytest_asyncio from app.models.activity import Activity, ActivityType from app.models.base import Base from app.models.contact import Contact from app.models.deal import Deal from app.models.organization import Organization from app.models.organization_member import OrganizationMember, OrganizationRole from app.models.task import TaskCreate from app.models.user import User from app.repositories.activity_repo import ActivityRepository from app.repositories.task_repo import TaskRepository from app.services.organization_service import OrganizationContext from app.services.task_service import ( TaskDueDateError, TaskForbiddenError, TaskService, TaskUpdateData, ) from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.pool import StaticPool @pytest_asyncio.fixture() async def session() -> AsyncGenerator[AsyncSession, None]: engine = create_async_engine( "sqlite+aiosqlite:///:memory:", future=True, poolclass=StaticPool, ) async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) session_factory = async_sessionmaker(engine, expire_on_commit=False) async with session_factory() as session: yield session await engine.dispose() def _make_user(suffix: str) -> User: return User( email=f"user-{suffix}@example.com", hashed_password="hashed", name="Test User", is_active=True, ) async def _setup_environment( session: AsyncSession, *, role: OrganizationRole = OrganizationRole.MANAGER, context_user: User | None = None, owner_user: User | None = None, ) -> tuple[OrganizationContext, User, User, int, TaskRepository, ActivityRepository]: org = Organization(name=f"Org-{uuid.uuid4()}"[:8]) owner = owner_user or _make_user("owner") ctx_user = context_user or owner session.add_all([org, owner]) if ctx_user is not owner: session.add(ctx_user) await session.flush() contact = Contact( organization_id=org.id, owner_id=owner.id, name="John Doe", email="john@example.com", ) session.add(contact) await session.flush() deal = Deal( organization_id=org.id, contact_id=contact.id, owner_id=owner.id, title="Implementation", amount=None, ) session.add(deal) await session.flush() membership = OrganizationMember(organization_id=org.id, user_id=ctx_user.id, role=role) context = OrganizationContext(organization=org, membership=membership) task_repo = TaskRepository(session=session) activity_repo = ActivityRepository(session=session) return context, owner, ctx_user, deal.id, task_repo, activity_repo @pytest.mark.asyncio async def test_create_task_logs_activity(session: AsyncSession) -> None: context, owner, _, deal_id, task_repo, activity_repo = await _setup_environment(session) service = TaskService(task_repository=task_repo, activity_repository=activity_repo) due_date = datetime.now(timezone.utc) + timedelta(days=2) task = await service.create_task( TaskCreate( deal_id=deal_id, title="Follow up", description="Call client", due_date=due_date, ), context=context, ) result = await session.scalars(select(Activity).where(Activity.deal_id == deal_id)) activities = result.all() assert len(activities) == 1 assert activities[0].type == ActivityType.TASK_CREATED assert activities[0].payload["task_id"] == task.id assert activities[0].payload["title"] == task.title @pytest.mark.asyncio async def test_member_cannot_create_task_for_foreign_deal(session: AsyncSession) -> None: owner = _make_user("owner") member = _make_user("member") context, _, _, deal_id, task_repo, activity_repo = await _setup_environment( session, role=OrganizationRole.MEMBER, context_user=member, owner_user=owner, ) service = TaskService(task_repository=task_repo, activity_repository=activity_repo) with pytest.raises(TaskForbiddenError): await service.create_task( TaskCreate( deal_id=deal_id, title="Follow up", description=None, due_date=datetime.now(timezone.utc) + timedelta(days=1), ), context=context, ) @pytest.mark.asyncio async def test_due_date_cannot_be_in_past(session: AsyncSession) -> None: context, _, _, deal_id, task_repo, activity_repo = await _setup_environment(session) service = TaskService(task_repository=task_repo, activity_repository=activity_repo) with pytest.raises(TaskDueDateError): await service.create_task( TaskCreate( deal_id=deal_id, title="Late", description=None, due_date=datetime.now(timezone.utc) - timedelta(days=1), ), context=context, ) @pytest.mark.asyncio async def test_member_cannot_update_foreign_task(session: AsyncSession) -> None: # First create a task as the owner owner = _make_user("owner") context_owner, _, _, deal_id, task_repo, activity_repo = await _setup_environment( session, context_user=owner, owner_user=owner, ) service = TaskService(task_repository=task_repo, activity_repository=activity_repo) task = await service.create_task( TaskCreate( deal_id=deal_id, title="Prepare deck", description=None, due_date=datetime.now(timezone.utc) + timedelta(days=5), ), context=context_owner, ) # Attempt to update it as another member member = _make_user("member") session.add(member) await session.flush() membership = OrganizationMember( organization_id=context_owner.organization_id, user_id=member.id, role=OrganizationRole.MEMBER, ) member_context = OrganizationContext( organization=context_owner.organization, membership=membership, ) with pytest.raises(TaskForbiddenError): await service.update_task( task.id, TaskUpdateData(is_done=True), context=member_context, )