279 lines
11 KiB
Markdown
279 lines
11 KiB
Markdown
# Test Model: Post Lifecycle
|
|
|
|
Feature: Create, read, update, delete, publish, and unpublish blog posts.
|
|
Covers both API use cases and web UI end-to-end flows.
|
|
|
|
## Unit Test Cases
|
|
|
|
### TC-UNIT-001: CreatePostUseCase — Success
|
|
- **Type:** Positive
|
|
- **Layer:** Unit
|
|
- **File:** `unit/application/test_use_cases.py::TestCreatePostUseCase::test_create_post_success`
|
|
- **Preconditions:** Mock repository, mock transaction manager
|
|
- **Steps:**
|
|
1. Mock `slug_exists` to return `False`
|
|
2. Execute `CreatePostUseCase` with valid DTO
|
|
- **Expected:**
|
|
- Returns `PostResponseDTO` with correct title and author
|
|
- `repository.add` called once
|
|
- `transaction_manager.commit` called once
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
### TC-UNIT-002: CreatePostUseCase — Duplicate Slug
|
|
- **Type:** Negative
|
|
- **Layer:** Unit
|
|
- **File:** `unit/application/test_use_cases.py::TestCreatePostUseCase::test_create_post_slug_exists`
|
|
- **Preconditions:** Mock repository returns `slug_exists=True`
|
|
- **Steps:** Execute `CreatePostUseCase` with DTO that would collide
|
|
- **Expected:** Raises `AlreadyExistsException`, no DB write
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
### TC-UNIT-003: CreatePostUseCase — Validation Error (implied by VO tests)
|
|
- **Type:** Negative
|
|
- **Layer:** Unit
|
|
- **File:** Covered indirectly via `Title` / `Content` VO tests
|
|
- **Gap Note:** No explicit use-case-level validation error test exists.
|
|
|
|
### TC-UNIT-004: DeletePostUseCase — Success
|
|
- **Type:** Positive
|
|
- **Layer:** Unit
|
|
- **File:** `unit/application/test_use_cases.py::TestDeletePostUseCase`
|
|
- **Expected:** Post removed, commit called
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
### TC-UNIT-005: GetPostUseCase — By ID Success
|
|
- **Type:** Positive
|
|
- **Layer:** Unit
|
|
- **File:** `unit/application/test_use_cases.py::TestGetPostUseCase`
|
|
- **Expected:** Returns `PostResponseDTO` for existing post
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
### TC-UNIT-006: GetPostUseCase — By Slug Success
|
|
- **Type:** Positive
|
|
- **Layer:** Unit
|
|
- **File:** `unit/application/test_use_cases.py::TestGetPostUseCase`
|
|
- **Expected:** Returns `PostResponseDTO` for existing slug
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
### TC-UNIT-007: GetPostUseCase — Not Found
|
|
- **Type:** Negative
|
|
- **Layer:** Unit
|
|
- **File:** `unit/application/test_use_cases.py::TestGetPostUseCase`
|
|
- **Expected:** Raises `NotFoundException`
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
### TC-UNIT-008: UpdatePostUseCase — Success
|
|
- **Type:** Positive
|
|
- **Layer:** Unit
|
|
- **File:** `unit/application/test_use_cases.py::TestUpdatePostUseCase`
|
|
- **Expected:** Post updated, commit called
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
### TC-UNIT-009: UpdatePostUseCase — Forbidden (other author)
|
|
- **Type:** Policy
|
|
- **Layer:** Unit
|
|
- **File:** `unit/application/test_use_cases.py::TestUpdatePostUseCase`
|
|
- **Expected:** Raises `ForbiddenException`
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
### TC-UNIT-010: PublishPostUseCase — Publish Success
|
|
- **Type:** Positive
|
|
- **Layer:** Unit
|
|
- **File:** `unit/application/test_publish_post.py::TestPublishPost::test_publish_success`
|
|
- **Preconditions:** Mock repository returns unpublished post
|
|
- **Steps:** Call `publish(post_id, author_id)`
|
|
- **Expected:**
|
|
- Returns `PostResponseDTO` with `published=True`
|
|
- `repository.update` and `commit` called once
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
### TC-UNIT-011: PublishPostUseCase — Publish Not Found
|
|
- **Type:** Negative
|
|
- **Layer:** Unit
|
|
- **File:** `unit/application/test_publish_post.py::TestPublishPost::test_publish_not_found`
|
|
- **Expected:** Raises `NotFoundException`
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
### TC-UNIT-012: PublishPostUseCase — Publish Forbidden
|
|
- **Type:** Policy
|
|
- **Layer:** Unit
|
|
- **File:** `unit/application/test_publish_post.py::TestPublishPost::test_publish_forbidden`
|
|
- **Expected:** Raises `ForbiddenException` when caller is not the author
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
### TC-UNIT-013: PublishPostUseCase — Unpublish Success
|
|
- **Type:** Positive
|
|
- **Layer:** Unit
|
|
- **File:** `unit/application/test_publish_post.py::TestUnpublishPost::test_unpublish_success`
|
|
- **Expected:** Returns `PostResponseDTO` with `published=False`
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
### TC-UNIT-014: PublishPostUseCase — Unpublish Not Found
|
|
- **Type:** Negative
|
|
- **Layer:** Unit
|
|
- **File:** `unit/application/test_publish_post.py::TestUnpublishPost::test_unpublish_not_found`
|
|
- **Expected:** Raises `NotFoundException`
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
### TC-UNIT-015: PublishPostUseCase — Unpublish Forbidden
|
|
- **Type:** Policy
|
|
- **Layer:** Unit
|
|
- **File:** `unit/application/test_publish_post.py::TestUnpublishPost::test_unpublish_forbidden`
|
|
- **Expected:** Raises `ForbiddenException` when caller is not the author
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
### TC-UNIT-016: ListPostsUseCase — All Posts
|
|
- **Type:** Positive
|
|
- **Layer:** Unit
|
|
- **File:** `unit/application/test_list_posts.py::TestAllPosts::test_all_posts`
|
|
- **Expected:** Returns all posts as DTOs
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
### TC-UNIT-017: ListPostsUseCase — Published Posts
|
|
- **Type:** Positive
|
|
- **Layer:** Unit
|
|
- **File:** `unit/application/test_list_posts.py::TestPublishedPosts::test_published_posts`
|
|
- **Expected:** Returns only published posts
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
### TC-UNIT-018: ListPostsUseCase — Published Posts with Pagination
|
|
- **Type:** Edge
|
|
- **Layer:** Unit
|
|
- **File:** `unit/application/test_list_posts.py::TestPublishedPosts::test_published_posts_with_limit_offset`
|
|
- **Expected:** Repository called with correct limit/offset
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
### TC-UNIT-019: ListPostsUseCase — By Author
|
|
- **Type:** Positive
|
|
- **Layer:** Unit
|
|
- **File:** `unit/application/test_list_posts.py::TestByAuthor::test_by_author`
|
|
- **Expected:** Returns posts filtered by author_id
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
### TC-UNIT-020: ListPostsUseCase — By Tag
|
|
- **Type:** Positive
|
|
- **Layer:** Unit
|
|
- **File:** `unit/application/test_list_posts.py::TestByTag::test_by_tag`
|
|
- **Expected:** Returns posts containing the tag
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
### TC-UNIT-021: ListPostsUseCase — Search
|
|
- **Type:** Positive
|
|
- **Layer:** Unit
|
|
- **File:** `unit/application/test_list_posts.py::TestSearch::test_search`
|
|
- **Expected:** Returns posts matching the query
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
### TC-UNIT-022: ListPostsUseCase — Search No Results
|
|
- **Type:** Edge
|
|
- **Layer:** Unit
|
|
- **File:** `unit/application/test_list_posts.py::TestSearch::test_search_no_results`
|
|
- **Expected:** Returns empty list
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
## E2E Test Cases
|
|
|
|
### TC-E2E-001: Positive — Create and Publish Post
|
|
- **Type:** Positive
|
|
- **Layer:** E2E
|
|
- **File:** `e2e/test_post_lifecycle.py::test_user_creates_and_publishes_post_visible_to_guest_and_admin`
|
|
- **Preconditions:** Dev server running, `user_page`, `guest_page`, `admin_page` fixtures
|
|
- **Steps:**
|
|
1. Generate post data via `PostDataGenerator`
|
|
2. Open home page and click "Write a Post"
|
|
3. Fill form (title, content, tags)
|
|
4. Click "Publish Post"
|
|
- **Expected:**
|
|
- Redirect to `/web/posts/{slug}`
|
|
- Status badge shows "Published"
|
|
- Post visible on home page for user, guest, and admin
|
|
- Post detail accessible to guest and admin
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
### TC-E2E-002: Policy — Draft Visibility Across Roles
|
|
- **Type:** Policy
|
|
- **Layer:** E2E
|
|
- **File:** `e2e/test_post_lifecycle.py::test_post_visibility_policies_across_users`
|
|
- **Preconditions:** Dev server running, `user_page`, `user2_page`, `guest_page`, `admin_page` fixtures
|
|
- **Steps:**
|
|
1. User creates a draft post
|
|
2. User creates and publishes another post
|
|
3. Check visibility for each role on the home page
|
|
4. Attempt direct access to draft by user2
|
|
- **Expected:**
|
|
- User sees both posts
|
|
- User2 and guest see only the published post
|
|
- Admin sees both posts
|
|
- User2 receives 404 when accessing draft directly
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
### TC-E2E-003: Negative — 404 for Nonexistent Post
|
|
- **Type:** Negative
|
|
- **Layer:** E2E
|
|
- **File:** `e2e/test_errors.py::test_nonexistent_post_returns_404`
|
|
- **Preconditions:** Dev server running, `guest_page`, `user_page` fixtures
|
|
- **Steps:**
|
|
1. Generate a random slug that does not exist in the database.
|
|
2. Navigate to `/web/posts/{fake-slug}` as guest and as authenticated user.
|
|
- **Expected:**
|
|
- Error page is rendered with `data-testid="error-code"` showing `404`
|
|
- Both guest and user see the same 404 response
|
|
- **Last Verified:** 2026-05-08
|
|
|
|
### TC-E2E-003a: Policy — 404 for Another User's Draft
|
|
- **Type:** Policy
|
|
- **Layer:** E2E
|
|
- **File:** `e2e/test_errors.py::test_other_user_draft_returns_404`
|
|
- **Preconditions:** Dev server running, `user_page`, `user2_page`, `guest_page` fixtures
|
|
- **Steps:**
|
|
1. User creates a draft post and saves it.
|
|
2. Extract the slug from the detail page URL.
|
|
3. User2 and guest navigate to `/web/posts/{slug}`.
|
|
- **Expected:**
|
|
- Owner sees the draft detail with "Draft" badge (200)
|
|
- User2 sees 404 error page
|
|
- Guest sees 404 error page
|
|
- **Last Verified:** 2026-05-08
|
|
|
|
### TC-E2E-004: Positive — Delete Post via Web UI
|
|
- **Type:** Positive
|
|
- **Layer:** E2E
|
|
- **File:** `e2e/test_post_deletion.py::test_user_can_delete_own_post`
|
|
- **Preconditions:** Dev server running, `user_page` fixture
|
|
- **Steps:**
|
|
1. Generate post data via `PostDataGenerator`
|
|
2. Open home page and click "Write a Post"
|
|
3. Fill form (title, content, tags) and click "Publish Post"
|
|
4. On post detail page, click "Delete" and accept confirm dialog
|
|
- **Expected:**
|
|
- Redirect to `/web/`
|
|
- Post no longer appears on home page
|
|
- **Last Verified:** 2026-05-07
|
|
|
|
## Coverage Summary
|
|
|
|
| Aspect | Coverage | Notes |
|
|
|--------|----------|-------|
|
|
| Create post | Unit + E2E | Both happy path and duplicate slug covered |
|
|
| Read post (by id/slug) | Unit | E2E implicitly via detail page |
|
|
| Update post | Unit | No dedicated E2E |
|
|
| Delete post | Unit + E2E | Own-post and admin-delete covered |
|
|
| Publish / Unpublish | Unit + E2E | Draft-to-publish flow covered via edit |
|
|
| List posts (all filters) | Unit | Pagination arguments passed but not edge-case tested |
|
|
| Search posts | Unit | No E2E search flow |
|
|
|
|
## Gaps (Not Yet Covered)
|
|
|
|
- [ ] TC-UNIT-023: CreatePostUseCase — explicit validation error (title too short, content empty)
|
|
- [x] TC-UNIT-024: UpdatePostUseCase — not found scenario
|
|
- [x] TC-UNIT-025: UpdatePostUseCase — content and tags update
|
|
- [ ] TC-UNIT-026: ListPostsUseCase — pagination edge cases (page boundaries, empty page)
|
|
- [x] TC-E2E-003: 404 error page for nonexistent post
|
|
- [x] TC-E2E-003a: Edit post via web UI and verify changes (own post)
|
|
- [x] TC-E2E-004: Delete post via web UI and verify removal
|
|
- [x] TC-E2E-005: Save post as draft and publish via edit, verify visibility change
|
|
- [ ] TC-E2E-006: Search posts via web UI
|
|
- [x] TC-E2E-007: Pagination navigation on home page
|
|
- [x] TC-E2E-009: Profile page renders user info and role badge correctly
|
|
- [x] TC-E2E-010: Theme toggle switches between light and dark with localStorage persistence
|