Files
blog.pyaqa.ru/tests/FEATURE_COMMENTS.md
Sergey Vanyushkin 7ff3fa0992
All checks were successful
ci/woodpecker/pr/pipeline Pipeline was successful
feat: add comments feature with nested replies and recursive rendering
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.
2026-05-11 15:34:20 +03:00

286 lines
10 KiB
Markdown

# Test Model: Comments
Feature: Add comments to blog posts with Markdown editor, nested replies
(parent_id), and per-comment like/unlike toggle.
## Unit Test Cases
### Domain Entities
#### TC-UNIT-829: Comment entity — valid creation
- **Type:** Positive
- **Layer:** Unit
- **File:** `unit/domain/test_comment_entity.py::TestCommentEntity::test_comment_creation`
- **Preconditions:** Valid post_id, author_id, and content
- **Steps:** Create Comment instance
- **Expected:**
- `post_id` matches input
- `author_id` matches input
- `content` is CommentContent value object
- `id` is a valid UUID
- `parent_id` is None
- `like_count` is 0
- `created_at` is set
- **Last Verified:** —
#### TC-UNIT-830: Comment entity — with parent_id (reply)
- **Type:** Positive
- **Layer:** Unit
- **File:** `unit/domain/test_comment_entity.py::TestCommentEntity::test_comment_with_parent`
- **Preconditions:** Valid post_id, author_id, content, and parent_id
- **Steps:** Create Comment instance with parent_id
- **Expected:**
- `parent_id` matches input
- All other attributes set correctly
- **Last Verified:** —
#### TC-UNIT-831: CommentLike entity — valid creation
- **Type:** Positive
- **Layer:** Unit
- **File:** `unit/domain/test_comment_like_entity.py::TestCommentLikeEntity::test_comment_like_creation`
- **Preconditions:** Valid comment_id and liked_by
- **Steps:** Create CommentLike instance
- **Expected:**
- `comment_id` matches input
- `liked_by` matches input
- `id` is a valid UUID
- `created_at` is set
- **Last Verified:** —
### CreateCommentUseCase
#### TC-UNIT-832: CreateCommentUseCase — on post (top-level)
- **Type:** Positive
- **Layer:** Unit
- **File:** `unit/application/test_create_comment.py::TestCreateCommentUseCase::test_create_comment_on_post`
- **Preconditions:** Post exists
- **Steps:** Execute with post_id, author_id, content, parent_id=None
- **Expected:**
- Comment created with correct post_id and author_id
- `parent_id` is None
- `comment_repo.add` called once
- Transaction committed
- **Last Verified:** —
#### TC-UNIT-833: CreateCommentUseCase — reply to comment
- **Type:** Positive
- **Layer:** Unit
- **File:** `unit/application/test_create_comment.py::TestCreateCommentUseCase::test_create_comment_reply`
- **Preconditions:** Post and parent comment exist
- **Steps:** Execute with post_id, author_id, content, parent_id set
- **Expected:**
- Comment created with correct parent_id
- `comment_repo.add` called once
- **Last Verified:** —
#### TC-UNIT-834: CreateCommentUseCase — post not found
- **Type:** Negative
- **Layer:** Unit
- **File:** `unit/application/test_create_comment.py::TestCreateCommentUseCase::test_create_comment_post_not_found`
- **Preconditions:** Post does not exist
- **Steps:** Execute with non-existent post_id
- **Expected:** `NotFoundException` raised
- **Last Verified:** —
### ListCommentsUseCase
#### TC-UNIT-835: ListCommentsUseCase — returns comments for post
- **Type:** Positive
- **Layer:** Unit
- **File:** `unit/application/test_list_comments.py::TestListCommentsUseCase::test_list_comments_by_post`
- **Preconditions:** Post has multiple comments and replies
- **Steps:** Execute with post_id
- **Expected:** Returns list of CommentResponseDTO with correct post_id
- **Last Verified:** —
### DeleteCommentUseCase
#### TC-UNIT-836: DeleteCommentUseCase — own comment
- **Type:** Positive
- **Layer:** Unit
- **File:** `unit/application/test_delete_comment.py::TestDeleteCommentUseCase::test_delete_own_comment`
- **Preconditions:** Comment exists owned by user
- **Steps:** Execute with comment_id, user_id matching author_id
- **Expected:** `comment_repo.delete` called
- **Last Verified:** —
#### TC-UNIT-837: DeleteCommentUseCase — not found
- **Type:** Negative
- **Layer:** Unit
- **File:** `unit/application/test_delete_comment.py::TestDeleteCommentUseCase::test_delete_comment_not_found`
- **Preconditions:** Comment does not exist
- **Steps:** Execute with non-existent comment_id
- **Expected:** `NotFoundException` raised
- **Last Verified:** —
### ToggleCommentLikeUseCase
#### TC-UNIT-838: ToggleCommentLikeUseCase — like first time
- **Type:** Positive
- **Layer:** Unit
- **File:** `unit/application/test_toggle_comment_like.py::TestToggleCommentLikeUseCase::test_like_comment_first_time`
- **Preconditions:** Comment exists, no existing like for this user
- **Steps:** Execute toggle with comment_id and liked_by
- **Expected:**
- `add_like` called once
- `remove_like` not called
- Response DTO has `like_count=1`
- **Last Verified:** —
#### TC-UNIT-839: ToggleCommentLikeUseCase — unlike (already liked)
- **Type:** Positive
- **Layer:** Unit
- **File:** `unit/application/test_toggle_comment_like.py::TestToggleCommentLikeUseCase::test_unlike_comment_already_liked`
- **Preconditions:** Comment exists, existing like found for this user
- **Steps:** Execute toggle with same comment_id and liked_by
- **Expected:**
- `remove_like` called once
- `add_like` not called
- Response DTO has `like_count=0`
- **Last Verified:** —
#### TC-UNIT-840: ToggleCommentLikeUseCase — comment not found
- **Type:** Negative
- **Layer:** Unit
- **File:** `unit/application/test_toggle_comment_like.py::TestToggleCommentLikeUseCase::test_like_comment_not_found`
- **Preconditions:** Comment does not exist
- **Steps:** Execute toggle with non-existent comment_id
- **Expected:** `NotFoundException` raised
- **Last Verified:** —
## API Test Cases
#### TC-API-118: Create comment — authenticated
- **Type:** Positive
- **Layer:** API
- **File:** `api/test_comments.py::TestCreateComment::test_create_comment_authenticated`
- **Preconditions:** Post exists, user authenticated
- **Steps:** POST `/api/v1/posts/{post_id}/comments` with auth header
- **Expected:**
- Status 201
- Response has comment_id, post_id, content, author_id
- **Last Verified:** —
#### TC-API-119: Create comment — reply to comment
- **Type:** Positive
- **Layer:** API
- **File:** `api/test_comments.py::TestCreateComment::test_create_comment_reply`
- **Preconditions:** Post and comment exist, user authenticated
- **Steps:** POST with `parent_id` set to existing comment
- **Expected:**
- Status 201
- Response has correct `parent_id`
- **Last Verified:** —
#### TC-API-120: Create comment — guest
- **Type:** Negative
- **Layer:** API
- **File:** `api/test_comments.py::TestCreateComment::test_create_comment_as_guest`
- **Preconditions:** Post exists, guest token used
- **Steps:** POST without auth header
- **Expected:** Status 401
- **Last Verified:** —
#### TC-API-121: List comments — by post
- **Type:** Positive
- **Layer:** API
- **File:** `api/test_comments.py::TestListComments::test_list_comments_by_post`
- **Preconditions:** Post exists with comments
- **Steps:** GET `/api/v1/posts/{post_id}/comments`
- **Expected:**
- Status 200
- Response contains list of comments
- **Last Verified:** —
#### TC-API-122: Delete comment — own comment
- **Type:** Positive
- **Layer:** API
- **File:** `api/test_comments.py::TestDeleteComment::test_delete_own_comment`
- **Preconditions:** Comment exists owned by authenticated user
- **Steps:** DELETE `/api/v1/comments/{comment_id}` with auth header
- **Expected:** Status 204
- **Last Verified:** —
#### TC-API-123: Delete comment — not owner
- **Type:** Negative
- **Layer:** API
- **File:** `api/test_comments.py::TestDeleteComment::test_delete_comment_not_owner`
- **Preconditions:** Comment exists owned by different user
- **Steps:** DELETE with another user's auth header
- **Expected:** Status 403
- **Last Verified:** —
#### TC-API-124: Toggle comment like — authenticated
- **Type:** Positive
- **Layer:** API
- **File:** `api/test_comments.py::TestLikeComment::test_like_comment_authenticated`
- **Preconditions:** Comment exists, user authenticated
- **Steps:** POST `/api/v1/comments/{comment_id}/like` with auth header
- **Expected:**
- Status 200
- `like_count == 1`
- **Last Verified:** —
#### TC-API-125: Toggle comment like — guest
- **Type:** Negative
- **Layer:** API
- **File:** `api/test_comments.py::TestLikeComment::test_like_comment_as_guest`
- **Preconditions:** Comment exists, guest token used
- **Steps:** POST without auth header
- **Expected:** Status 401
- **Last Verified:** —
## E2E Test Cases
#### TC-E2E-109: Create comment via web UI
- **Type:** Positive
- **Layer:** E2E
- **File:** `tests/e2e/test_comments.py::test_create_comment`
- **Scenario:** Login → open post → write comment with Markdown → verify display
- **Expected:** Comment displayed on post detail page with rendered Markdown
- **Last Verified:** —
#### TC-E2E-110: Reply to comment
- **Type:** Positive
- **Layer:** E2E
- **File:** `tests/e2e/test_comments.py::test_reply_to_comment`
- **Scenario:** Create top-level comment → click Reply → write reply → verify nesting
- **Expected:** Reply appears below parent comment with indentation
- **Last Verified:** —
#### TC-E2E-111: Like/unlike comment
- **Type:** Positive
- **Layer:** E2E
- **File:** `tests/e2e/test_comments.py::test_like_unlike_comment`
- **Scenario:** Create comment → like → verify count → unlike → verify count
- **Expected:** Count toggles correctly (0→1→0)
- **Last Verified:** —
#### TC-E2E-112: Guest cannot comment
- **Type:** Negative
- **Layer:** E2E
- **File:** `tests/e2e/test_comments.py::test_guest_cannot_comment`
- **Scenario:** Guest opens published post → comment form not visible → cannot post
- **Expected:** Comment form hidden for guests
- **Last Verified:** —
## Coverage Summary
| Component | Cases | Status |
|-----------|-------|--------|
| Domain Entities (Comment, CommentLike) | 3 | ⬜ Planned |
| CreateCommentUseCase | 3 | ⬜ Planned |
| ListCommentsUseCase | 1 | ⬜ Planned |
| DeleteCommentUseCase | 2 | ⬜ Planned |
| ToggleCommentLikeUseCase | 3 | ⬜ Planned |
| API Endpoints | 8 | ⬜ Planned |
| E2E Flows | 4 | ⬜ Planned |
## Gaps (Not Yet Covered)
- [ ] Integration tests for SQLAlchemyCommentRepository
- [ ] Web-only tests (TC-WEB-004+)
- [ ] Admin delete any comment
- [ ] Edit comment
- [ ] Comment pagination with large number of comments