- PostLike domain entity (post_id, liked_by) with BaseEntity integration
- Post entity: add like_count field (default 0) and to_dict serialization
- PostRepository interface: add get_like, add_like, remove_like methods
- TogglePostLikeUseCase: toggle logic (like → unlike, unlike → like)
- PostResponseDTO/PostResponseSchema: add like_count field
- PostLikeORM model with FK to posts and cascade delete
- SQLAlchemyPostRepository: implement like query/add/remove with ORM mapping
- DI provider registration for TogglePostLikeUseCase
- API endpoint POST /api/v1/posts/{id}/like (auth required)
- Unit tests: PostLike entity, Post.like_count, TogglePostLikeUseCase (7 tests)
- API tests: POST /api/v1/posts/{id}/like (4 tests)
- Test model files: FEATURE_LIKES.md, TEST_MODEL.md updated
171 lines
5.9 KiB
Python
171 lines
5.9 KiB
Python
"""Tests for TogglePostLikeUseCase.
|
|
|
|
This module tests the like/unlike toggle use case covering
|
|
first-time like, unlike, post-not-found, guest access, and
|
|
identity isolation scenarios.
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock, Mock
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
|
|
from app.application.use_cases.toggle_like import TogglePostLikeUseCase
|
|
from app.domain.entities import Post
|
|
from app.domain.entities.like import PostLike
|
|
from app.domain.exceptions import NotFoundException
|
|
|
|
|
|
@pytest.fixture
|
|
def test_post() -> Post:
|
|
"""Create a test post for like tests."""
|
|
return Post.create(
|
|
title_str="Likeable Post",
|
|
content_str="This post will be liked and unliked. Enough length here.",
|
|
author_id="user-123",
|
|
tags=["test"],
|
|
)
|
|
|
|
|
|
class TestTogglePostLikeUseCase:
|
|
"""Tests for TogglePostLikeUseCase.
|
|
|
|
Covers TC-UNIT-822 through TC-UNIT-825 and TC-UNIT-828.
|
|
"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_like_post_first_time(
|
|
self,
|
|
mock_post_repository: Mock,
|
|
mock_transaction_manager: Mock,
|
|
test_post: Post,
|
|
) -> None:
|
|
"""Test toggling like on a post for the first time.
|
|
|
|
TC-UNIT-822: Positive — like first time.
|
|
"""
|
|
mock_post_repository.get_by_id = AsyncMock(return_value=test_post)
|
|
mock_post_repository.get_like = AsyncMock(return_value=None)
|
|
mock_post_repository.add_like = AsyncMock()
|
|
mock_post_repository.remove_like = AsyncMock()
|
|
mock_post_repository.update = AsyncMock()
|
|
|
|
use_case = TogglePostLikeUseCase(mock_post_repository, mock_transaction_manager)
|
|
|
|
result = await use_case.execute(test_post.id, "user-123")
|
|
|
|
assert result.like_count == 1
|
|
mock_post_repository.add_like.assert_called_once()
|
|
mock_post_repository.remove_like.assert_not_called()
|
|
mock_post_repository.update.assert_called_once()
|
|
mock_transaction_manager.commit.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unlike_post_already_liked(
|
|
self,
|
|
mock_post_repository: Mock,
|
|
mock_transaction_manager: Mock,
|
|
test_post: Post,
|
|
) -> None:
|
|
"""Test toggling like on a post that is already liked.
|
|
|
|
TC-UNIT-823: Positive — unlike (already liked).
|
|
"""
|
|
existing_like = PostLike(post_id=test_post.id, liked_by="user-123")
|
|
|
|
mock_post_repository.get_by_id = AsyncMock(return_value=test_post)
|
|
mock_post_repository.get_like = AsyncMock(return_value=existing_like)
|
|
mock_post_repository.add_like = AsyncMock()
|
|
mock_post_repository.remove_like = AsyncMock()
|
|
mock_post_repository.update = AsyncMock()
|
|
|
|
use_case = TogglePostLikeUseCase(mock_post_repository, mock_transaction_manager)
|
|
|
|
result = await use_case.execute(test_post.id, "user-123")
|
|
|
|
assert result.like_count == 0
|
|
mock_post_repository.remove_like.assert_called_once()
|
|
mock_post_repository.add_like.assert_not_called()
|
|
mock_post_repository.update.assert_called_once()
|
|
mock_transaction_manager.commit.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_like_post_not_found(
|
|
self,
|
|
mock_post_repository: Mock,
|
|
mock_transaction_manager: Mock,
|
|
) -> None:
|
|
"""Test toggling like on a non-existent post.
|
|
|
|
TC-UNIT-824: Negative — post not found.
|
|
"""
|
|
mock_post_repository.get_by_id = AsyncMock(return_value=None)
|
|
|
|
use_case = TogglePostLikeUseCase(mock_post_repository, mock_transaction_manager)
|
|
|
|
with pytest.raises(NotFoundException):
|
|
await use_case.execute(uuid4(), "user-123")
|
|
|
|
mock_post_repository.add_like.assert_not_called()
|
|
mock_post_repository.remove_like.assert_not_called()
|
|
mock_transaction_manager.commit.assert_not_called()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_like_as_guest_with_device_id(
|
|
self,
|
|
mock_post_repository: Mock,
|
|
mock_transaction_manager: Mock,
|
|
test_post: Post,
|
|
) -> None:
|
|
"""Test toggling like as a guest using device_id.
|
|
|
|
TC-UNIT-825: Positive — guest via device_id.
|
|
"""
|
|
device_id = "device-abc-123"
|
|
mock_post_repository.get_by_id = AsyncMock(return_value=test_post)
|
|
mock_post_repository.get_like = AsyncMock(return_value=None)
|
|
mock_post_repository.add_like = AsyncMock()
|
|
mock_post_repository.remove_like = AsyncMock()
|
|
mock_post_repository.update = AsyncMock()
|
|
|
|
use_case = TogglePostLikeUseCase(mock_post_repository, mock_transaction_manager)
|
|
|
|
result = await use_case.execute(test_post.id, device_id)
|
|
|
|
assert result.like_count == 1
|
|
added_like = mock_post_repository.add_like.call_args[0][0]
|
|
assert added_like.liked_by == device_id
|
|
assert added_like.post_id == test_post.id
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_two_users_can_both_like(
|
|
self,
|
|
mock_post_repository: Mock,
|
|
mock_transaction_manager: Mock,
|
|
test_post: Post,
|
|
) -> None:
|
|
"""Test that two different users can both like the same post.
|
|
|
|
TC-UNIT-828: Positive — identity isolation.
|
|
Both likes are counted independently.
|
|
"""
|
|
mock_post_repository.get_by_id = AsyncMock(return_value=test_post)
|
|
mock_post_repository.get_like = AsyncMock(return_value=None)
|
|
mock_post_repository.add_like = AsyncMock()
|
|
mock_post_repository.remove_like = AsyncMock()
|
|
mock_post_repository.update = AsyncMock()
|
|
|
|
use_case = TogglePostLikeUseCase(mock_post_repository, mock_transaction_manager)
|
|
|
|
result1 = await use_case.execute(test_post.id, "user-123")
|
|
assert result1.like_count == 1
|
|
|
|
mock_post_repository.add_like.reset_mock()
|
|
mock_post_repository.update.reset_mock()
|
|
mock_transaction_manager.commit.reset_mock()
|
|
|
|
result2 = await use_case.execute(test_post.id, "user-456")
|
|
assert result2.like_count == 2
|
|
|
|
assert mock_post_repository.add_like.call_count == 1
|