17551d662b
- Add send_request(), accept_request(), decline_request(), cancel_request() - Add get_inbox(), get_outgoing_requests(), get_friends_list(), remove_friend() - Add unit tests for all service methods (14 tests) - Update FriendRequestResponse schema to include 'canceled' status - Follow async SQLAlchemy patterns and BaseService conventions
381 lines
12 KiB
Python
381 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import uuid
|
|
from typing import Any
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from models.friendships import Friendship, FriendshipStatus
|
|
from models.inbox_messages import InboxMessage, InboxMessageStatus, InboxMessageType
|
|
|
|
|
|
class FakeFriendshipRepository:
|
|
"""Fake implementation for testing."""
|
|
|
|
def __init__(self) -> None:
|
|
self.friendships: dict[uuid.UUID, Friendship] = {}
|
|
self.inbox_messages: dict[uuid.UUID, InboxMessage] = {}
|
|
|
|
async def create_request(
|
|
self,
|
|
initiator_id: uuid.UUID,
|
|
recipient_id: uuid.UUID,
|
|
) -> tuple[Friendship, InboxMessage]:
|
|
raise NotImplementedError
|
|
|
|
async def get_friendship_between_users(
|
|
self, user_id_1: uuid.UUID, user_id_2: uuid.UUID
|
|
) -> Friendship | None:
|
|
raise NotImplementedError
|
|
|
|
async def get_pending_inbox_for_recipient(
|
|
self, recipient_id: uuid.UUID, friendship_id: uuid.UUID
|
|
) -> InboxMessage | None:
|
|
raise NotImplementedError
|
|
|
|
async def get_friendship_by_id(self, friendship_id: uuid.UUID) -> Friendship | None:
|
|
raise NotImplementedError
|
|
|
|
async def get_inbox_messages_for_user(
|
|
self, user_id: uuid.UUID, status: InboxMessageStatus | None = None
|
|
) -> list[InboxMessage]:
|
|
raise NotImplementedError
|
|
|
|
async def get_outgoing_requests(self, user_id: uuid.UUID) -> list[Friendship]:
|
|
raise NotImplementedError
|
|
|
|
async def get_friends_list(self, user_id: uuid.UUID) -> list[Friendship]:
|
|
raise NotImplementedError
|
|
|
|
|
|
class TestFriendshipRepository:
|
|
"""Tests for FriendshipRepository."""
|
|
|
|
@pytest.fixture
|
|
def mock_session(self) -> MagicMock:
|
|
session = MagicMock()
|
|
session.execute = AsyncMock()
|
|
session.flush = AsyncMock()
|
|
session.add = MagicMock()
|
|
session.commit = AsyncMock()
|
|
return session
|
|
|
|
@pytest.fixture
|
|
def repository(self, mock_session: MagicMock) -> Any:
|
|
from v1.friendships.repository import SQLAlchemyFriendshipRepository
|
|
|
|
return SQLAlchemyFriendshipRepository(mock_session)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_request_creates_friendship_and_inbox(
|
|
self, repository: Any, mock_session: MagicMock
|
|
) -> None:
|
|
initiator_id = uuid.uuid4()
|
|
recipient_id = uuid.uuid4()
|
|
|
|
result_friendship = Friendship(
|
|
id=uuid.uuid4(),
|
|
user_low_id=min(initiator_id, recipient_id),
|
|
user_high_id=max(initiator_id, recipient_id),
|
|
initiator_id=initiator_id,
|
|
status=FriendshipStatus.PENDING,
|
|
requested_at=uuid.uuid4(),
|
|
)
|
|
result_inbox = InboxMessage(
|
|
id=uuid.uuid4(),
|
|
recipient_id=recipient_id,
|
|
sender_id=initiator_id,
|
|
message_type=InboxMessageType.FRIEND_REQUEST,
|
|
status=InboxMessageStatus.PENDING,
|
|
)
|
|
|
|
mock_execute = AsyncMock(
|
|
return_value=MagicMock(scalar_one_or_none=MagicMock(return_value=None))
|
|
)
|
|
mock_session.execute = mock_execute
|
|
mock_session.add = MagicMock()
|
|
|
|
class MockExecuteResult:
|
|
def __init__(self, returning):
|
|
self._returning = returning
|
|
|
|
def scalar_one_or_none(self):
|
|
return self._returning
|
|
|
|
async def mock_execute_func(stmt):
|
|
if "INSERT INTO friendships" in str(stmt):
|
|
return MockExecuteResult(result_friendship)
|
|
elif "INSERT INTO inbox_messages" in str(stmt):
|
|
return MockExecuteResult(result_inbox)
|
|
return MockExecuteResult(None)
|
|
|
|
mock_session.execute = AsyncMock(side_effect=mock_execute_func)
|
|
|
|
friendship, inbox = await repository.create_request(initiator_id, recipient_id)
|
|
|
|
assert friendship is not None
|
|
assert inbox is not None
|
|
assert friendship.initiator_id == initiator_id
|
|
assert inbox.recipient_id == recipient_id
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_friendship_between_users_returns_friendship(
|
|
self, repository: Any, mock_session: MagicMock
|
|
) -> None:
|
|
user_id_1 = uuid.uuid4()
|
|
user_id_2 = uuid.uuid4()
|
|
expected_friendship = Friendship(
|
|
id=uuid.uuid4(),
|
|
user_low_id=min(user_id_1, user_id_2),
|
|
user_high_id=max(user_id_1, user_id_2),
|
|
initiator_id=user_id_1,
|
|
status=FriendshipStatus.ACCEPTED,
|
|
)
|
|
|
|
class MockExecuteResult:
|
|
def scalar_one_or_none(self):
|
|
return expected_friendship
|
|
|
|
mock_session.execute = AsyncMock(return_value=MockExecuteResult())
|
|
|
|
result = await repository.get_friendship_between_users(user_id_1, user_id_2)
|
|
|
|
assert result is not None
|
|
assert result.id == expected_friendship.id
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_friendship_between_users_returns_none(
|
|
self, repository: Any, mock_session: MagicMock
|
|
) -> None:
|
|
user_id_1 = uuid.uuid4()
|
|
user_id_2 = uuid.uuid4()
|
|
|
|
class MockExecuteResult:
|
|
def scalar_one_or_none(self):
|
|
return None
|
|
|
|
mock_session.execute = AsyncMock(return_value=MockExecuteResult())
|
|
|
|
result = await repository.get_friendship_between_users(user_id_1, user_id_2)
|
|
|
|
assert result is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_pending_inbox_for_recipient_returns_inbox(
|
|
self, repository: Any, mock_session: MagicMock
|
|
) -> None:
|
|
recipient_id = uuid.uuid4()
|
|
friendship_id = uuid.uuid4()
|
|
expected_inbox = InboxMessage(
|
|
id=uuid.uuid4(),
|
|
recipient_id=recipient_id,
|
|
sender_id=uuid.uuid4(),
|
|
message_type=InboxMessageType.FRIEND_REQUEST,
|
|
friendship_id=friendship_id,
|
|
status=InboxMessageStatus.PENDING,
|
|
)
|
|
|
|
class MockExecuteResult:
|
|
def scalar_one_or_none(self):
|
|
return expected_inbox
|
|
|
|
mock_session.execute = AsyncMock(return_value=MockExecuteResult())
|
|
|
|
result = await repository.get_pending_inbox_for_recipient(
|
|
recipient_id, friendship_id
|
|
)
|
|
|
|
assert result is not None
|
|
assert result.id == expected_inbox.id
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_pending_inbox_for_recipient_returns_none(
|
|
self, repository: Any, mock_session: MagicMock
|
|
) -> None:
|
|
recipient_id = uuid.uuid4()
|
|
friendship_id = uuid.uuid4()
|
|
|
|
class MockExecuteResult:
|
|
def scalar_one_or_none(self):
|
|
return None
|
|
|
|
mock_session.execute = AsyncMock(return_value=MockExecuteResult())
|
|
|
|
result = await repository.get_pending_inbox_for_recipient(
|
|
recipient_id, friendship_id
|
|
)
|
|
|
|
assert result is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_friendship_by_id_returns_friendship(
|
|
self, repository: Any, mock_session: MagicMock
|
|
) -> None:
|
|
friendship_id = uuid.uuid4()
|
|
expected_friendship = Friendship(
|
|
id=friendship_id,
|
|
user_low_id=uuid.uuid4(),
|
|
user_high_id=uuid.uuid4(),
|
|
initiator_id=uuid.uuid4(),
|
|
status=FriendshipStatus.ACCEPTED,
|
|
)
|
|
|
|
class MockExecuteResult:
|
|
def scalar_one_or_none(self):
|
|
return expected_friendship
|
|
|
|
mock_session.execute = AsyncMock(return_value=MockExecuteResult())
|
|
|
|
result = await repository.get_friendship_by_id(friendship_id)
|
|
|
|
assert result is not None
|
|
assert result.id == friendship_id
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_friendship_by_id_returns_none(
|
|
self, repository: Any, mock_session: MagicMock
|
|
) -> None:
|
|
friendship_id = uuid.uuid4()
|
|
|
|
class MockExecuteResult:
|
|
def scalar_one_or_none(self):
|
|
return None
|
|
|
|
mock_session.execute = AsyncMock(return_value=MockExecuteResult())
|
|
|
|
result = await repository.get_friendship_by_id(friendship_id)
|
|
|
|
assert result is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_inbox_messages_for_user_returns_messages(
|
|
self, repository: Any, mock_session: MagicMock
|
|
) -> None:
|
|
user_id = uuid.uuid4()
|
|
expected_messages = [
|
|
InboxMessage(
|
|
id=uuid.uuid4(),
|
|
recipient_id=user_id,
|
|
sender_id=uuid.uuid4(),
|
|
message_type=InboxMessageType.FRIEND_REQUEST,
|
|
status=InboxMessageStatus.PENDING,
|
|
),
|
|
InboxMessage(
|
|
id=uuid.uuid4(),
|
|
recipient_id=user_id,
|
|
sender_id=uuid.uuid4(),
|
|
message_type=InboxMessageType.FRIEND_REQUEST,
|
|
status=InboxMessageStatus.ACCEPTED,
|
|
),
|
|
]
|
|
|
|
class MockScalars:
|
|
def all(self):
|
|
return expected_messages
|
|
|
|
class MockExecuteResult:
|
|
def scalars(self):
|
|
return MockScalars()
|
|
|
|
mock_session.execute = AsyncMock(return_value=MockExecuteResult())
|
|
|
|
result = await repository.get_inbox_messages_for_user(user_id)
|
|
|
|
assert len(result) == 2
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_inbox_messages_for_user_filters_by_status(
|
|
self, repository: Any, mock_session: MagicMock
|
|
) -> None:
|
|
user_id = uuid.uuid4()
|
|
expected_messages = [
|
|
InboxMessage(
|
|
id=uuid.uuid4(),
|
|
recipient_id=user_id,
|
|
sender_id=uuid.uuid4(),
|
|
message_type=InboxMessageType.FRIEND_REQUEST,
|
|
status=InboxMessageStatus.PENDING,
|
|
),
|
|
]
|
|
|
|
class MockScalars:
|
|
def all(self):
|
|
return expected_messages
|
|
|
|
class MockExecuteResult:
|
|
def scalars(self):
|
|
return MockScalars()
|
|
|
|
mock_session.execute = AsyncMock(return_value=MockExecuteResult())
|
|
|
|
result = await repository.get_inbox_messages_for_user(
|
|
user_id, status=InboxMessageStatus.PENDING
|
|
)
|
|
|
|
assert len(result) == 1
|
|
assert result[0].status == InboxMessageStatus.PENDING
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_outgoing_requests_returns_pending_requests(
|
|
self, repository: Any, mock_session: MagicMock
|
|
) -> None:
|
|
user_id = uuid.uuid4()
|
|
expected_requests = [
|
|
Friendship(
|
|
id=uuid.uuid4(),
|
|
user_low_id=user_id,
|
|
user_high_id=uuid.uuid4(),
|
|
initiator_id=user_id,
|
|
status=FriendshipStatus.PENDING,
|
|
),
|
|
]
|
|
|
|
class MockScalars:
|
|
def all(self):
|
|
return expected_requests
|
|
|
|
class MockExecuteResult:
|
|
def scalars(self):
|
|
return MockScalars()
|
|
|
|
mock_session.execute = AsyncMock(return_value=MockExecuteResult())
|
|
|
|
result = await repository.get_outgoing_requests(user_id)
|
|
|
|
assert len(result) == 1
|
|
assert result[0].initiator_id == user_id
|
|
assert result[0].status == FriendshipStatus.PENDING
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_friends_list_returns_accepted_friends(
|
|
self, repository: Any, mock_session: MagicMock
|
|
) -> None:
|
|
user_id = uuid.uuid4()
|
|
friend_id = uuid.uuid4()
|
|
expected_friends = [
|
|
Friendship(
|
|
id=uuid.uuid4(),
|
|
user_low_id=min(user_id, friend_id),
|
|
user_high_id=max(user_id, friend_id),
|
|
initiator_id=user_id,
|
|
status=FriendshipStatus.ACCEPTED,
|
|
accepted_at=uuid.uuid4(),
|
|
),
|
|
]
|
|
|
|
class MockScalars:
|
|
def all(self):
|
|
return expected_friends
|
|
|
|
class MockExecuteResult:
|
|
def scalars(self):
|
|
return MockScalars()
|
|
|
|
mock_session.execute = AsyncMock(return_value=MockExecuteResult())
|
|
|
|
result = await repository.get_friends_list(user_id)
|
|
|
|
assert len(result) == 1
|
|
assert result[0].status == FriendshipStatus.ACCEPTED
|