import json from datetime import datetime, timezone from unittest.mock import AsyncMock, MagicMock from uuid import UUID, uuid4 import pytest from fastapi import HTTPException from core.auth.models import CurrentUser from models.inbox_messages import InboxMessage, InboxMessageStatus from models.schedule_items import ( ScheduleItem, ScheduleItemSourceType, ScheduleItemStatus, ) from models.schedule_subscriptions import ScheduleSubscription from v1.schedule_items.service import ScheduleItemService def _create_mock_schedule_item( item_id: UUID = uuid4(), owner_id: UUID = UUID("00000000-0000-0000-0000-000000000001"), title: str = "Test Event", ) -> ScheduleItem: item = MagicMock(spec=ScheduleItem) item.id = item_id item.owner_id = owner_id item.title = title item.description = None item.start_at = datetime(2026, 2, 28, 16, 0, 0, tzinfo=timezone.utc) item.end_at = datetime(2026, 2, 28, 17, 0, 0, tzinfo=timezone.utc) item.timezone = "UTC" item.extra_metadata = {} item.source_type = ScheduleItemSourceType.MANUAL item.status = ScheduleItemStatus.ACTIVE item.created_at = datetime(2026, 2, 27, 10, 0, 0, tzinfo=timezone.utc) item.updated_at = datetime(2026, 2, 27, 10, 0, 0, tzinfo=timezone.utc) item.deleted_at = None return item class FakeInboxRepo: def __init__(self, inbox_message: InboxMessage | None = None) -> None: self._inbox = inbox_message async def get_pending_calendar_invite( self, schedule_item_id: UUID, recipient_id: UUID ) -> InboxMessage | None: if self._inbox: return self._inbox return None async def create(self, data: dict) -> InboxMessage: return MagicMock() async def get_by_id( self, message_id: UUID, recipient_id: UUID ) -> InboxMessage | None: return None async def list_by_recipient( self, recipient_id: UUID, is_read: bool | None = None ) -> list[InboxMessage]: return [] async def mark_as_read( self, message_id: UUID, recipient_id: UUID ) -> InboxMessage | None: return None @pytest.fixture def mock_session() -> AsyncMock: session = AsyncMock() session.commit = AsyncMock() session.rollback = AsyncMock() return session @pytest.fixture def mock_repo() -> MagicMock: repo = MagicMock() repo.create_subscription = AsyncMock(return_value=MagicMock()) return repo @pytest.mark.asyncio async def test_accept_subscription_success( mock_session: AsyncMock, mock_repo: MagicMock ) -> None: user_id = UUID("00000000-0000-0000-0000-000000000001") sender_id = UUID("00000000-0000-0000-0000-000000000002") item_id = uuid4() inbox_message = MagicMock(spec=InboxMessage) inbox_message.id = uuid4() inbox_message.sender_id = sender_id inbox_message.content = {"type": "invite", "permission": 1} inbox_message.status = InboxMessageStatus.PENDING service = ScheduleItemService( repository=mock_repo, session=mock_session, current_user=CurrentUser(id=user_id), inbox_repository=FakeInboxRepo(inbox_message), ) result = await service.accept_subscription(item_id) assert result == {"message": "Subscription accepted"} mock_session.commit.assert_awaited_once() @pytest.mark.asyncio async def test_accept_subscription_not_found( mock_session: AsyncMock, mock_repo: MagicMock ) -> None: user_id = UUID("00000000-0000-0000-0000-000000000001") item_id = uuid4() service = ScheduleItemService( repository=mock_repo, session=mock_session, current_user=CurrentUser(id=user_id), inbox_repository=FakeInboxRepo(None), ) with pytest.raises(HTTPException) as exc_info: await service.accept_subscription(item_id) assert exc_info.value.status_code == 404 assert "No pending invitation found" in exc_info.value.detail @pytest.mark.asyncio async def test_reject_subscription_success( mock_session: AsyncMock, mock_repo: MagicMock ) -> None: user_id = UUID("00000000-0000-0000-0000-000000000001") item_id = uuid4() inbox_message = MagicMock(spec=InboxMessage) inbox_message.id = uuid4() inbox_message.status = InboxMessageStatus.PENDING service = ScheduleItemService( repository=mock_repo, session=mock_session, current_user=CurrentUser(id=user_id), inbox_repository=FakeInboxRepo(inbox_message), ) result = await service.reject_subscription(item_id) assert result == {"message": "Subscription rejected"} mock_session.commit.assert_awaited_once() @pytest.mark.asyncio async def test_reject_subscription_not_found( mock_session: AsyncMock, mock_repo: MagicMock ) -> None: user_id = UUID("00000000-0000-0000-0000-000000000001") item_id = uuid4() service = ScheduleItemService( repository=mock_repo, session=mock_session, current_user=CurrentUser(id=user_id), inbox_repository=FakeInboxRepo(None), ) with pytest.raises(HTTPException) as exc_info: await service.reject_subscription(item_id) assert exc_info.value.status_code == 404 assert "No pending invitation found" in exc_info.value.detail @pytest.mark.asyncio async def test_list_by_date_range_with_subscriptions( mock_session: AsyncMock, mock_repo: MagicMock ) -> None: user_id = UUID("00000000-0000-0000-0000-000000000001") owner_id = UUID("00000000-0000-0000-0000-000000000002") item_id = uuid4() owned_item = _create_mock_schedule_item(item_id=item_id, owner_id=user_id) subscribed_item = _create_mock_schedule_item( item_id=uuid4(), owner_id=owner_id, title="Subscribed Event" ) subscription = MagicMock(spec=ScheduleSubscription) subscription.item_id = subscribed_item.id subscription.permission = 1 subscription.subscriber_id = user_id mock_repo.list_by_date_range = AsyncMock(return_value=[owned_item]) mock_repo.get_user_subscriptions = AsyncMock(return_value=[subscription]) mock_repo.get_by_id = AsyncMock(return_value=subscribed_item) service = ScheduleItemService( repository=mock_repo, session=mock_session, current_user=CurrentUser(id=user_id), inbox_repository=FakeInboxRepo(), ) from v1.schedule_items.schemas import ScheduleItemListRequest request = ScheduleItemListRequest( start_at=datetime(2026, 2, 1, 0, 0, 0, tzinfo=timezone.utc), end_at=datetime(2026, 3, 1, 0, 0, 0, tzinfo=timezone.utc), ) result = await service.list_by_date_range(request) assert len(result) == 2 assert result[0].is_owner is True assert result[1].is_owner is False assert result[1].permission == 1