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.
286 lines
10 KiB
Markdown
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
|