feat: add comments feature with nested replies and recursive rendering
All checks were successful
ci/woodpecker/pr/pipeline Pipeline was successful
All checks were successful
ci/woodpecker/pr/pipeline Pipeline was successful
Implement full comments system: domain entities (Comment, CommentLike), value objects (CommentContent), use cases (CRUD, like toggle), SQLAlchemy repository, API v1 endpoints, web UI with comment form and nested replies, i18n translations (EN/RU/FR/DE), and E2E tests. Fix nested reply (reply-to-reply) not displaying — the flat reply_comments dict was only queried for top-level comment IDs, so deeply nested replies were saved to DB (incrementing comment count) but never rendered. Switch to a recursive Jinja2 macro that renders any nesting depth.
This commit is contained in:
119
tests/unit/application/test_toggle_comment_like.py
Normal file
119
tests/unit/application/test_toggle_comment_like.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""Tests for ToggleCommentLikeUseCase.
|
||||
|
||||
This module tests the comment like/unlike toggle use case covering
|
||||
first-time like, unlike, and comment-not-found scenarios.
|
||||
"""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from app.application.use_cases.toggle_comment_like import ToggleCommentLikeUseCase
|
||||
from app.domain.entities.comment import Comment
|
||||
from app.domain.entities.comment_like import CommentLike
|
||||
from app.domain.exceptions import NotFoundException
|
||||
|
||||
|
||||
class TestToggleCommentLikeUseCase:
|
||||
"""Tests for ToggleCommentLikeUseCase.
|
||||
|
||||
Covers TC-UNIT-838 through TC-UNIT-840.
|
||||
"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_like_comment_first_time(
|
||||
self,
|
||||
mock_transaction_manager: AsyncMock,
|
||||
) -> None:
|
||||
"""Test liking a comment for the first time.
|
||||
|
||||
TC-UNIT-838: Positive — like first time.
|
||||
"""
|
||||
post_id = uuid4()
|
||||
comment = Comment.create(
|
||||
post_id=post_id,
|
||||
author_id="user-123",
|
||||
content_str="Nice post!",
|
||||
)
|
||||
|
||||
mock_comment_repository = AsyncMock()
|
||||
mock_comment_repository.get_by_id = AsyncMock(return_value=comment)
|
||||
mock_comment_repository.get_like = AsyncMock(return_value=None)
|
||||
mock_comment_repository.add_like = AsyncMock()
|
||||
mock_comment_repository.remove_like = AsyncMock()
|
||||
mock_comment_repository.update = AsyncMock()
|
||||
|
||||
use_case = ToggleCommentLikeUseCase(
|
||||
comment_repo=mock_comment_repository,
|
||||
tx_manager=mock_transaction_manager,
|
||||
)
|
||||
|
||||
result = await use_case.execute(comment.id, "user-456")
|
||||
|
||||
assert result.like_count == 1
|
||||
mock_comment_repository.add_like.assert_called_once()
|
||||
mock_comment_repository.remove_like.assert_not_called()
|
||||
mock_comment_repository.update.assert_called_once()
|
||||
mock_transaction_manager.commit.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unlike_comment_already_liked(
|
||||
self,
|
||||
mock_transaction_manager: AsyncMock,
|
||||
) -> None:
|
||||
"""Test unliking a comment that is already liked.
|
||||
|
||||
TC-UNIT-839: Positive — unlike (already liked).
|
||||
"""
|
||||
post_id = uuid4()
|
||||
comment = Comment.create(
|
||||
post_id=post_id,
|
||||
author_id="user-123",
|
||||
content_str="Nice post!",
|
||||
)
|
||||
existing_like = CommentLike(comment_id=comment.id, liked_by="user-456")
|
||||
|
||||
mock_comment_repository = AsyncMock()
|
||||
mock_comment_repository.get_by_id = AsyncMock(return_value=comment)
|
||||
mock_comment_repository.get_like = AsyncMock(return_value=existing_like)
|
||||
mock_comment_repository.add_like = AsyncMock()
|
||||
mock_comment_repository.remove_like = AsyncMock()
|
||||
mock_comment_repository.update = AsyncMock()
|
||||
|
||||
use_case = ToggleCommentLikeUseCase(
|
||||
comment_repo=mock_comment_repository,
|
||||
tx_manager=mock_transaction_manager,
|
||||
)
|
||||
|
||||
result = await use_case.execute(comment.id, "user-456")
|
||||
|
||||
assert result.like_count == 0
|
||||
mock_comment_repository.remove_like.assert_called_once()
|
||||
mock_comment_repository.add_like.assert_not_called()
|
||||
mock_comment_repository.update.assert_called_once()
|
||||
mock_transaction_manager.commit.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_like_comment_not_found(
|
||||
self,
|
||||
mock_transaction_manager: AsyncMock,
|
||||
) -> None:
|
||||
"""Test liking a non-existent comment.
|
||||
|
||||
TC-UNIT-840: Negative — comment not found.
|
||||
"""
|
||||
mock_comment_repository = AsyncMock()
|
||||
mock_comment_repository.get_by_id = AsyncMock(return_value=None)
|
||||
|
||||
use_case = ToggleCommentLikeUseCase(
|
||||
comment_repo=mock_comment_repository,
|
||||
tx_manager=mock_transaction_manager,
|
||||
)
|
||||
|
||||
with pytest.raises(NotFoundException):
|
||||
await use_case.execute(uuid4(), "user-456")
|
||||
|
||||
mock_comment_repository.add_like.assert_not_called()
|
||||
mock_comment_repository.remove_like.assert_not_called()
|
||||
mock_transaction_manager.commit.assert_not_called()
|
||||
Reference in New Issue
Block a user