287 lines
9.0 KiB
Python
287 lines
9.0 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from typing import cast
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
from uuid import UUID, uuid4
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
|
|
from core.auth.models import CurrentUser
|
|
from models.inbox_messages import InboxMessage, InboxMessageType
|
|
from models.schedule_items import ScheduleItem
|
|
from v1.auth.schemas import UserByEmailResponse
|
|
from v1.schedule_items.repository import ScheduleItemRepository
|
|
from v1.schedule_items.schemas import ScheduleItemShareRequest
|
|
from v1.schedule_items.service import ScheduleItemService
|
|
|
|
|
|
def test_share_request_schema() -> None:
|
|
request = ScheduleItemShareRequest(
|
|
email="friend@example.com",
|
|
permission_view=True,
|
|
permission_edit=True,
|
|
permission_invite=False,
|
|
)
|
|
assert request.email == "friend@example.com"
|
|
assert request.permission_view is True
|
|
|
|
|
|
def test_permission_bits_calculation() -> None:
|
|
request = ScheduleItemShareRequest(
|
|
email="friend@example.com",
|
|
permission_view=True,
|
|
permission_edit=True,
|
|
permission_invite=False,
|
|
)
|
|
assert request._permission_value() == 5
|
|
|
|
|
|
def _build_item(item_id: UUID, owner_id: UUID) -> ScheduleItem:
|
|
item = MagicMock(spec=ScheduleItem)
|
|
item.id = item_id
|
|
item.owner_id = owner_id
|
|
item.title = "test"
|
|
item.description = None
|
|
item.start_at = datetime(2026, 2, 28, 16, 0, 0, tzinfo=timezone.utc)
|
|
item.end_at = None
|
|
item.timezone = "UTC"
|
|
item.extra_metadata = {}
|
|
item.created_at = datetime(2026, 2, 28, 10, 0, 0, tzinfo=timezone.utc)
|
|
item.updated_at = datetime(2026, 2, 28, 10, 0, 0, tzinfo=timezone.utc)
|
|
return item
|
|
|
|
|
|
class ShareRepo:
|
|
def __init__(self, item: ScheduleItem | None) -> None:
|
|
self._item = item
|
|
|
|
async def get_by_id(self, item_id: UUID) -> ScheduleItem | None:
|
|
if self._item and self._item.id == item_id:
|
|
return self._item
|
|
return None
|
|
|
|
async def get_subscription(self, item_id: UUID, subscriber_id: UUID) -> None:
|
|
return None
|
|
|
|
async def create_subscription(self, data: dict[str, object]) -> None:
|
|
return None
|
|
|
|
|
|
class AuthGatewayStub:
|
|
async def get_user_by_email(self, email: str) -> UserByEmailResponse:
|
|
return UserByEmailResponse(
|
|
id="00000000-0000-0000-0000-000000000222",
|
|
email=email,
|
|
created_at="2026-02-28T10:00:00Z",
|
|
email_confirmed_at=None,
|
|
)
|
|
|
|
|
|
class InboxRepoStub:
|
|
async def create(self, data: dict[str, object]) -> InboxMessage:
|
|
return InboxMessage(
|
|
id=uuid4(),
|
|
recipient_id=UUID("00000000-0000-0000-0000-000000000222"),
|
|
sender_id=UUID("00000000-0000-0000-0000-000000000001"),
|
|
message_type=InboxMessageType.CALENDAR,
|
|
schedule_item_id=uuid4(),
|
|
content='{"type": "invite", "permission": 1, "action": "pending"}',
|
|
created_by=UUID("00000000-0000-0000-0000-000000000001"),
|
|
)
|
|
|
|
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
|
|
|
|
async def get_pending_calendar_invite(
|
|
self, schedule_item_id: UUID, recipient_id: UUID
|
|
) -> InboxMessage | None:
|
|
return None
|
|
|
|
async def get_calendar_invite(
|
|
self, schedule_item_id: UUID, recipient_id: UUID
|
|
) -> InboxMessage | None:
|
|
return None
|
|
|
|
|
|
class AuthGatewayInvalidIdStub:
|
|
async def get_user_by_email(self, email: str) -> UserByEmailResponse:
|
|
return UserByEmailResponse(
|
|
id="not-a-uuid",
|
|
email=email,
|
|
created_at="2026-02-28T10:00:00Z",
|
|
email_confirmed_at=None,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_share_forbidden_when_not_owner() -> None:
|
|
owner_id = UUID("00000000-0000-0000-0000-000000000001")
|
|
requester_id = UUID("00000000-0000-0000-0000-000000000002")
|
|
item_id = uuid4()
|
|
service = ScheduleItemService(
|
|
repository=cast(
|
|
ScheduleItemRepository,
|
|
ShareRepo(_build_item(item_id=item_id, owner_id=owner_id)),
|
|
),
|
|
session=AsyncMock(),
|
|
current_user=CurrentUser(id=requester_id),
|
|
auth_gateway=AuthGatewayStub(),
|
|
inbox_repository=InboxRepoStub(),
|
|
)
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await service.share(
|
|
item_id,
|
|
ScheduleItemShareRequest(
|
|
email="friend@example.com",
|
|
permission_view=True,
|
|
permission_edit=False,
|
|
permission_invite=False,
|
|
),
|
|
)
|
|
|
|
assert exc_info.value.status_code == 403
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_share_success_creates_calendar_invitation_message() -> None:
|
|
owner_id = UUID("00000000-0000-0000-0000-000000000001")
|
|
item_id = uuid4()
|
|
session = AsyncMock()
|
|
session.add = MagicMock()
|
|
service = ScheduleItemService(
|
|
repository=cast(
|
|
ScheduleItemRepository,
|
|
ShareRepo(_build_item(item_id=item_id, owner_id=owner_id)),
|
|
),
|
|
session=session,
|
|
current_user=CurrentUser(id=owner_id),
|
|
auth_gateway=AuthGatewayStub(),
|
|
inbox_repository=InboxRepoStub(),
|
|
)
|
|
|
|
result = await service.share(
|
|
item_id,
|
|
ScheduleItemShareRequest(
|
|
email="friend@example.com",
|
|
permission_view=True,
|
|
permission_edit=True,
|
|
permission_invite=False,
|
|
),
|
|
)
|
|
|
|
assert result.message == "Calendar invitation sent"
|
|
session.add.assert_called_once()
|
|
message = session.add.call_args.args[0]
|
|
assert isinstance(message, InboxMessage)
|
|
assert message.sender_id == owner_id
|
|
assert message.schedule_item_id == item_id
|
|
assert message.message_type == InboxMessageType.CALENDAR
|
|
assert message.content == '{"type": "invite", "permission": 5, "action": "pending"}'
|
|
session.commit.assert_awaited_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_share_returns_not_found_when_item_missing() -> None:
|
|
requester_id = UUID("00000000-0000-0000-0000-000000000002")
|
|
service = ScheduleItemService(
|
|
repository=cast(ScheduleItemRepository, ShareRepo(None)),
|
|
session=AsyncMock(),
|
|
current_user=CurrentUser(id=requester_id),
|
|
auth_gateway=AuthGatewayStub(),
|
|
inbox_repository=InboxRepoStub(),
|
|
)
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await service.share(
|
|
uuid4(),
|
|
ScheduleItemShareRequest(
|
|
email="friend@example.com",
|
|
permission_view=True,
|
|
permission_edit=False,
|
|
permission_invite=False,
|
|
),
|
|
)
|
|
|
|
assert exc_info.value.status_code == 404
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_share_invalid_auth_user_id_returns_503() -> None:
|
|
owner_id = UUID("00000000-0000-0000-0000-000000000001")
|
|
item_id = uuid4()
|
|
session = AsyncMock()
|
|
service = ScheduleItemService(
|
|
repository=cast(
|
|
ScheduleItemRepository,
|
|
ShareRepo(_build_item(item_id=item_id, owner_id=owner_id)),
|
|
),
|
|
session=session,
|
|
current_user=CurrentUser(id=owner_id),
|
|
auth_gateway=AuthGatewayInvalidIdStub(),
|
|
inbox_repository=InboxRepoStub(),
|
|
)
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await service.share(
|
|
item_id,
|
|
ScheduleItemShareRequest(
|
|
email="friend@example.com",
|
|
permission_view=True,
|
|
permission_edit=False,
|
|
permission_invite=False,
|
|
),
|
|
)
|
|
|
|
assert exc_info.value.status_code == 503
|
|
assert exc_info.value.detail == "Auth lookup unavailable"
|
|
session.rollback.assert_awaited_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_share_sqlalchemy_error_rolls_back() -> None:
|
|
owner_id = UUID("00000000-0000-0000-0000-000000000001")
|
|
item_id = uuid4()
|
|
session = AsyncMock()
|
|
session.add = MagicMock(side_effect=SQLAlchemyError("db error"))
|
|
service = ScheduleItemService(
|
|
repository=cast(
|
|
ScheduleItemRepository,
|
|
ShareRepo(_build_item(item_id=item_id, owner_id=owner_id)),
|
|
),
|
|
session=session,
|
|
current_user=CurrentUser(id=owner_id),
|
|
auth_gateway=AuthGatewayStub(),
|
|
inbox_repository=InboxRepoStub(),
|
|
)
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await service.share(
|
|
item_id,
|
|
ScheduleItemShareRequest(
|
|
email="friend@example.com",
|
|
permission_view=True,
|
|
permission_edit=False,
|
|
permission_invite=False,
|
|
),
|
|
)
|
|
|
|
assert exc_info.value.status_code == 503
|
|
assert exc_info.value.detail == "Schedule item store unavailable"
|
|
session.rollback.assert_awaited_once()
|