Files
social-app/backend/tests/unit/v1/schedule_items/test_share.py
T
qzl f0af44d840 refactor(backend): update API routes and service layer
- Update agent router/service/repository with new endpoints
- Update auth routes with phone-based authentication
- Update users service with new phone lookup
- Update schedule_items with new schemas
- Update message schemas with visibility support
- Update settings with new automation scheduler config
- Update CLI with new commands
- Update tests to match new API contracts
2026-03-19 18:42:59 +08:00

287 lines
8.9 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 UserByPhoneResponse
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(
phone="+8613810000000",
permission_view=True,
permission_edit=True,
permission_invite=False,
)
assert request.phone == "+8613810000000"
assert request.permission_view is True
def test_permission_bits_calculation() -> None:
request = ScheduleItemShareRequest(
phone="+8613810000000",
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_phone(self, phone: str) -> UserByPhoneResponse:
return UserByPhoneResponse(
id="00000000-0000-0000-0000-000000000222",
phone=phone,
created_at="2026-02-28T10:00:00Z",
phone_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_phone(self, phone: str) -> UserByPhoneResponse:
return UserByPhoneResponse(
id="not-a-uuid",
phone=phone,
created_at="2026-02-28T10:00:00Z",
phone_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(
phone="+8613810000000",
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(
phone="+8613810000000",
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(
phone="+8613810000000",
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(
phone="+8613810000000",
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(
phone="+8613810000000",
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()