- PostLike domain entity (post_id, liked_by) with BaseEntity integration
- Post entity: add like_count field (default 0) and to_dict serialization
- PostRepository interface: add get_like, add_like, remove_like methods
- TogglePostLikeUseCase: toggle logic (like → unlike, unlike → like)
- PostResponseDTO/PostResponseSchema: add like_count field
- PostLikeORM model with FK to posts and cascade delete
- SQLAlchemyPostRepository: implement like query/add/remove with ORM mapping
- DI provider registration for TogglePostLikeUseCase
- API endpoint POST /api/v1/posts/{id}/like (auth required)
- Unit tests: PostLike entity, Post.like_count, TogglePostLikeUseCase (7 tests)
- API tests: POST /api/v1/posts/{id}/like (4 tests)
- Test model files: FEATURE_LIKES.md, TEST_MODEL.md updated
210 lines
7.1 KiB
Markdown
210 lines
7.1 KiB
Markdown
# Test Model: Post Likes
|
||
|
||
Feature: Like/unlike toggle on blog posts with per-user tracking, session-based
|
||
guest identification, and anti-bot protection via JS-only POST.
|
||
|
||
## Unit Test Cases
|
||
|
||
### TogglePostLikeUseCase
|
||
|
||
#### TC-UNIT-822: TogglePostLikeUseCase — Like first time
|
||
- **Type:** Positive
|
||
- **Layer:** Unit
|
||
- **File:** `unit/application/test_toggle_like.py::TestTogglePostLikeUseCase::test_like_post_first_time`
|
||
- **Preconditions:** Post exists, no existing like for this user
|
||
- **Steps:** Execute toggle with valid post_id and liked_by
|
||
- **Expected:**
|
||
- `add_like` called once
|
||
- `remove_like` not called
|
||
- Response DTO has `like_count=1`
|
||
- **Last Verified:** 2026-05-10
|
||
|
||
#### TC-UNIT-823: TogglePostLikeUseCase — Unlike (already liked)
|
||
- **Type:** Positive
|
||
- **Layer:** Unit
|
||
- **File:** `unit/application/test_toggle_like.py::TestTogglePostLikeUseCase::test_unlike_post_already_liked`
|
||
- **Preconditions:** Post exists, existing like found for this user
|
||
- **Steps:** Execute toggle with same post_id and liked_by
|
||
- **Expected:**
|
||
- `remove_like` called once
|
||
- `add_like` not called
|
||
- Response DTO has `like_count=0`
|
||
- **Last Verified:** 2026-05-10
|
||
|
||
#### TC-UNIT-824: TogglePostLikeUseCase — Post not found
|
||
- **Type:** Negative
|
||
- **Layer:** Unit
|
||
- **File:** `unit/application/test_toggle_like.py::TestTogglePostLikeUseCase::test_like_post_not_found`
|
||
- **Preconditions:** Repository returns None for post lookup
|
||
- **Steps:** Execute toggle with non-existent post_id
|
||
- **Expected:** `NotFoundException` raised
|
||
- **Last Verified:** 2026-05-10
|
||
|
||
#### TC-UNIT-825: TogglePostLikeUseCase — Guest via device_id
|
||
- **Type:** Positive
|
||
- **Layer:** Unit
|
||
- **File:** `unit/application/test_toggle_like.py::TestTogglePostLikeUseCase::test_like_as_guest_with_device_id`
|
||
- **Preconditions:** Post exists, no existing like, liked_by set to device_id
|
||
- **Steps:** Execute toggle with device_id instead of user_id
|
||
- **Expected:**
|
||
- Like created with `liked_by == device_id`
|
||
- Response DTO has `like_count=1`
|
||
- **Last Verified:** 2026-05-10
|
||
|
||
#### TC-UNIT-828: TogglePostLikeUseCase — Identity isolation
|
||
- **Type:** Positive
|
||
- **Layer:** Unit
|
||
- **File:** `unit/application/test_toggle_like.py::TestTogglePostLikeUseCase::test_two_users_can_both_like`
|
||
- **Preconditions:** Post exists, user1 likes first
|
||
- **Steps:** User2 toggles like on same post
|
||
- **Expected:**
|
||
- User2's like added (separate identity)
|
||
- `like_count=2`
|
||
- **Last Verified:** 2026-05-10
|
||
|
||
### Domain Entities
|
||
|
||
#### TC-UNIT-826: PostLike entity — valid creation
|
||
- **Type:** Positive
|
||
- **Layer:** Unit
|
||
- **File:** `unit/domain/test_like_entity.py::TestPostLikeEntity::test_post_like_creation`
|
||
- **Preconditions:** Valid post_id and liked_by values
|
||
- **Steps:** Create PostLike instance
|
||
- **Expected:**
|
||
- `post_id` matches input
|
||
- `liked_by` matches input
|
||
- `id` is a valid UUID
|
||
- `created_at` is set
|
||
- **Last Verified:** 2026-05-10
|
||
|
||
#### TC-UNIT-827: Post entity — like_count default 0
|
||
- **Type:** Positive
|
||
- **Layer:** Unit
|
||
- **File:** `unit/domain/test_post_entity.py::TestPostEntity::test_like_count_defaults_to_zero`
|
||
- **Preconditions:** —
|
||
- **Steps:** Create Post via `Post.create()`
|
||
- **Expected:** `post.like_count == 0`
|
||
- **Last Verified:** 2026-05-10
|
||
|
||
## API Test Cases
|
||
|
||
#### TC-API-114: Like Post — authenticated toggle on
|
||
- **Type:** Positive
|
||
- **Layer:** API
|
||
- **File:** `api/test_likes.py::TestLikePost::test_like_post_authenticated`
|
||
- **Preconditions:** Post exists, user authenticated
|
||
- **Steps:** POST `/api/v1/posts/{id}/like` with auth header
|
||
- **Expected:**
|
||
- Status 200
|
||
- `like_count == 1`
|
||
- **Last Verified:** 2026-05-10
|
||
|
||
#### TC-API-115: Like Post — authenticated toggle off
|
||
- **Type:** Positive
|
||
- **Layer:** API
|
||
- **File:** `api/test_likes.py::TestLikePost::test_unlike_post_authenticated`
|
||
- **Preconditions:** Post exists, user already liked it
|
||
- **Steps:** POST `/api/v1/posts/{id}/like` second time
|
||
- **Expected:**
|
||
- Status 200
|
||
- `like_count == 0`
|
||
- **Last Verified:** 2026-05-10
|
||
|
||
#### TC-API-116: Like Post — guest via device_id
|
||
- **Type:** Positive
|
||
- **Layer:** API
|
||
- **File:** `api/test_likes.py::TestLikePost::test_like_post_as_guest`
|
||
- **Preconditions:** Post exists, guest token used
|
||
- **Steps:** POST `/api/v1/posts/{id}/like` with guest token
|
||
- **Expected:**
|
||
- Status 200
|
||
- `like_count == 1`
|
||
- **Last Verified:** 2026-05-10
|
||
|
||
#### TC-API-117: Like Post — not found
|
||
- **Type:** Negative
|
||
- **Layer:** API
|
||
- **File:** `api/test_likes.py::TestLikePost::test_like_post_not_found`
|
||
- **Preconditions:** Post does not exist
|
||
- **Steps:** POST `/api/v1/posts/{id}/like` with auth header
|
||
- **Expected:**
|
||
- Status 404
|
||
- **Last Verified:** 2026-05-10
|
||
|
||
## Web Test Cases
|
||
|
||
#### TC-WEB-001: Like count on post list
|
||
- **Type:** Positive
|
||
- **Layer:** Web
|
||
- **File:** `tests/web/test_likes.py::TestLikeDisplay::test_like_count_on_homepage`
|
||
- **Preconditions:** Posts exist with known like counts
|
||
- **Steps:** GET `/web/`
|
||
- **Expected:**
|
||
- Each post card shows like count
|
||
- `data-testid="like-count-{post.id}"` present
|
||
- **Last Verified:** 2026-05-10
|
||
|
||
#### TC-WEB-002: Like button on post detail
|
||
- **Type:** Positive
|
||
- **Layer:** Web
|
||
- **File:** `tests/web/test_likes.py::TestLikeDisplay::test_like_button_on_detail`
|
||
- **Preconditions:** Post exists
|
||
- **Steps:** GET `/web/posts/{slug}`
|
||
- **Expected:**
|
||
- Like count displayed
|
||
- `data-testid="like-button"` present
|
||
- **Last Verified:** 2026-05-10
|
||
|
||
#### TC-WEB-003: Like toggle via POST
|
||
- **Type:** Positive
|
||
- **Layer:** Web
|
||
- **File:** `tests/web/test_likes.py::TestLikeToggle::test_like_toggle_via_web`
|
||
- **Preconditions:** Post exists
|
||
- **Steps:** POST `/web/posts/{slug}/like` redirects back
|
||
- **Expected:**
|
||
- 303 redirect to post detail
|
||
- Like count incremented
|
||
- **Last Verified:** 2026-05-10
|
||
|
||
## E2E Test Cases
|
||
|
||
#### TC-E2E-106: Like/Unlike flow via web UI
|
||
- **Type:** Positive
|
||
- **Layer:** E2E
|
||
- **File:** `tests/e2e/test_likes.py::test_like_unlike_flow`
|
||
- **Scenario:** Create post → like → verify count → unlike → verify count
|
||
- **Expected:** Count toggles correctly
|
||
- **Last Verified:** 2026-05-10
|
||
|
||
#### TC-E2E-107: Separate users can both like
|
||
- **Type:** Positive
|
||
- **Layer:** E2E
|
||
- **File:** `tests/e2e/test_likes.py::test_multiple_users_can_like`
|
||
- **Scenario:** User1 likes → count=1 → User2 likes → count=2
|
||
- **Expected:** Count increments per user
|
||
- **Last Verified:** 2026-05-10
|
||
|
||
#### TC-E2E-108: Guest like with different sessions
|
||
- **Type:** Positive
|
||
- **Layer:** E2E
|
||
- **File:** `tests/e2e/test_likes.py::test_guest_like_different_sessions`
|
||
- **Scenario:** Guest1 likes → count=1 → different device context
|
||
- **Expected:** Different guests count separately
|
||
- **Last Verified:** 2026-05-10
|
||
|
||
## Coverage Summary
|
||
|
||
| Component | Cases | Status |
|
||
|-----------|-------|--------|
|
||
| TogglePostLikeUseCase | 5 | ✅ Verified |
|
||
| Domain Entities (PostLike, Post) | 2 | ✅ Verified |
|
||
| API Endpoints | 4 | ✅ Verified |
|
||
| Web Display | 3 | ⬜ Planned |
|
||
| E2E Flows | 3 | ⬜ Planned |
|
||
|
||
## Gaps (Not Yet Covered)
|
||
|
||
- [ ] Web tests (TC-WEB-001–003) — test infrastructure pending
|
||
- [ ] E2E tests (TC-E2E-106–108) — test infrastructure pending
|
||
- [ ] Full device_id middleware for guest like support
|