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:
128
tests/unit/application/test_create_comment.py
Normal file
128
tests/unit/application/test_create_comment.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Tests for CreateCommentUseCase.
|
||||
|
||||
This module tests comment creation use case covering top-level comments,
|
||||
replies to existing comments, and post-not-found scenarios.
|
||||
"""
|
||||
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from app.application.use_cases.create_comment import CreateCommentUseCase
|
||||
from app.domain.entities import Post
|
||||
from app.domain.exceptions import NotFoundException
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_post() -> Post:
|
||||
"""Create a test post for comment tests."""
|
||||
return Post.create(
|
||||
title_str="Commentable Post",
|
||||
content_str="This post will receive comments. Enough length here.",
|
||||
author_id="user-123",
|
||||
tags=["test"],
|
||||
)
|
||||
|
||||
|
||||
class TestCreateCommentUseCase:
|
||||
"""Tests for CreateCommentUseCase.
|
||||
|
||||
Covers TC-UNIT-832 through TC-UNIT-834.
|
||||
"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_comment_on_post(
|
||||
self,
|
||||
mock_post_repository: Mock,
|
||||
mock_transaction_manager: Mock,
|
||||
test_post: Post,
|
||||
) -> None:
|
||||
"""Test creating a top-level comment on a post.
|
||||
|
||||
TC-UNIT-832: Positive — create top-level comment.
|
||||
"""
|
||||
mock_comment_repository = AsyncMock()
|
||||
mock_post_repository.get_by_id = AsyncMock(return_value=test_post)
|
||||
|
||||
use_case = CreateCommentUseCase(
|
||||
post_repo=mock_post_repository,
|
||||
comment_repo=mock_comment_repository,
|
||||
tx_manager=mock_transaction_manager,
|
||||
)
|
||||
|
||||
result = await use_case.execute(
|
||||
post_id=test_post.id,
|
||||
author_id="user-456",
|
||||
content="Great post! Thanks for sharing.",
|
||||
)
|
||||
|
||||
assert result.post_id == test_post.id
|
||||
assert result.author_id == "user-456"
|
||||
assert result.content == "Great post! Thanks for sharing."
|
||||
assert result.parent_id is None
|
||||
assert result.like_count == 0
|
||||
mock_comment_repository.add.assert_called_once()
|
||||
mock_transaction_manager.commit.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_comment_reply(
|
||||
self,
|
||||
mock_post_repository: Mock,
|
||||
mock_transaction_manager: Mock,
|
||||
test_post: Post,
|
||||
) -> None:
|
||||
"""Test creating a reply to an existing comment.
|
||||
|
||||
TC-UNIT-833: Positive — reply to comment with parent_id.
|
||||
"""
|
||||
mock_comment_repository = AsyncMock()
|
||||
mock_post_repository.get_by_id = AsyncMock(return_value=test_post)
|
||||
parent_id = uuid4()
|
||||
|
||||
use_case = CreateCommentUseCase(
|
||||
post_repo=mock_post_repository,
|
||||
comment_repo=mock_comment_repository,
|
||||
tx_manager=mock_transaction_manager,
|
||||
)
|
||||
|
||||
result = await use_case.execute(
|
||||
post_id=test_post.id,
|
||||
author_id="user-456",
|
||||
content="This is a reply.",
|
||||
parent_id=parent_id,
|
||||
)
|
||||
|
||||
assert result.parent_id == parent_id
|
||||
assert result.post_id == test_post.id
|
||||
mock_comment_repository.add.assert_called_once()
|
||||
mock_transaction_manager.commit.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_comment_post_not_found(
|
||||
self,
|
||||
mock_post_repository: Mock,
|
||||
mock_transaction_manager: Mock,
|
||||
) -> None:
|
||||
"""Test creating a comment on a non-existent post.
|
||||
|
||||
TC-UNIT-834: Negative — post not found.
|
||||
"""
|
||||
mock_comment_repository = AsyncMock()
|
||||
mock_post_repository.get_by_id = AsyncMock(return_value=None)
|
||||
|
||||
use_case = CreateCommentUseCase(
|
||||
post_repo=mock_post_repository,
|
||||
comment_repo=mock_comment_repository,
|
||||
tx_manager=mock_transaction_manager,
|
||||
)
|
||||
|
||||
with pytest.raises(NotFoundException):
|
||||
await use_case.execute(
|
||||
post_id=uuid4(),
|
||||
author_id="user-456",
|
||||
content="Comment on missing post.",
|
||||
)
|
||||
|
||||
mock_comment_repository.add.assert_not_called()
|
||||
mock_transaction_manager.commit.assert_not_called()
|
||||
Reference in New Issue
Block a user