feat(friendships): implement FriendshipService with TDD
- 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
This commit is contained in:
@@ -0,0 +1,380 @@
|
||||
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
|
||||
Reference in New Issue
Block a user