feat: add comments feature with nested replies and recursive rendering
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:
2026-05-11 15:34:20 +03:00
parent 63da25174e
commit 7ff3fa0992
40 changed files with 3161 additions and 44 deletions

View File

@@ -0,0 +1,98 @@
"""Tests for Comment domain entity.
This module tests the Comment entity creation, parent_id support,
and BaseEntity integration.
"""
from uuid import UUID, uuid4
from app.domain.entities.comment import Comment
from app.domain.value_objects.comment_content import CommentContent
class TestCommentEntity:
"""Tests for the Comment domain entity.
Covers TC-UNIT-829 and TC-UNIT-830.
"""
def test_comment_creation(self) -> None:
"""Test creating a top-level Comment with valid attributes.
TC-UNIT-829: Positive — create Comment instance.
Expected:
- post_id matches input
- author_id matches input
- content is CommentContent with correct value
- id is a valid UUID
- parent_id is None
- like_count is 0
- created_at is set
"""
post_id = UUID("00000000-0000-0000-0000-000000000001")
author_id = "user-123"
content_text = "This is a comment with **Markdown** support."
comment = Comment.create(
post_id=post_id,
author_id=author_id,
content_str=content_text,
)
assert comment.post_id == post_id
assert comment.author_id == author_id
assert isinstance(comment.content, CommentContent)
assert comment.content.value == content_text
assert isinstance(comment.id, UUID)
assert comment.parent_id is None
assert comment.like_count == 0
assert comment.created_at is not None
def test_comment_with_parent(self) -> None:
"""Test creating a reply Comment with parent_id.
TC-UNIT-830: Positive — create Comment with parent_id.
Expected:
- parent_id matches the provided parent comment ID.
- All other attributes set correctly.
"""
post_id = UUID("00000000-0000-0000-0000-000000000001")
parent_id = uuid4()
author_id = "user-456"
content_text = "This is a reply to another comment."
comment = Comment.create(
post_id=post_id,
author_id=author_id,
content_str=content_text,
parent_id=parent_id,
)
assert comment.parent_id == parent_id
assert comment.post_id == post_id
assert comment.author_id == author_id
assert comment.content.value == content_text
assert comment.like_count == 0
def test_comment_to_dict(self) -> None:
"""Test Comment to_dict serialization."""
post_id = UUID("00000000-0000-0000-0000-000000000001")
author_id = "user-123"
content_text = "Comment with serialization test."
comment = Comment.create(
post_id=post_id,
author_id=author_id,
content_str=content_text,
)
data = comment.to_dict()
assert data["post_id"] == str(post_id)
assert data["author_id"] == author_id
assert data["content"] == content_text
assert "id" in data
assert "created_at" in data
assert data["parent_id"] is None
assert data["like_count"] == 0

View File

@@ -0,0 +1,50 @@
"""Tests for CommentLike domain entity.
This module tests the CommentLike entity creation, attributes,
and BaseEntity integration.
"""
from uuid import UUID
from app.domain.entities.comment_like import CommentLike
class TestCommentLikeEntity:
"""Tests for the CommentLike domain entity.
Covers TC-UNIT-831.
"""
def test_comment_like_creation(self) -> None:
"""Test creating a CommentLike with valid attributes.
TC-UNIT-831: Positive — create CommentLike instance.
Expected:
- comment_id matches input
- liked_by matches input
- id is a valid UUID
- created_at is set
"""
comment_id = UUID("00000000-0000-0000-0000-000000000001")
liked_by = "user-123"
like = CommentLike(comment_id=comment_id, liked_by=liked_by)
assert like.comment_id == comment_id
assert like.liked_by == liked_by
assert isinstance(like.id, UUID)
assert like.created_at is not None
def test_comment_like_to_dict(self) -> None:
"""Test CommentLike to_dict serialization."""
comment_id = UUID("00000000-0000-0000-0000-000000000001")
liked_by = "device-abc-123"
like = CommentLike(comment_id=comment_id, liked_by=liked_by)
data = like.to_dict()
assert data["comment_id"] == str(comment_id)
assert data["liked_by"] == liked_by
assert "id" in data
assert "created_at" in data