Files
blog.pyaqa.ru/tests/unit/application/test_toggle_like.py
Sergey Vanyushkin 3cf6c94da2 feat: add like/unlike toggle on blog posts with per-user tracking
- 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
2026-05-10 18:24:09 +03:00

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