# 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 ## API Test Cases ### TC-API-001: Create Post — Success - **Type:** Positive - **Layer:** API - **File:** `api/test_posts.py::TestCreatePost::test_create_post_success` - **Steps:** 1. POST `/api/v1/posts` with valid payload and user auth - **Expected:** 201 with correct title, content, tags, author_id, generated UUID/slug, published=False - **Last Verified:** 2026-05-10 ### TC-API-002: Create Post — Validation Error - **Type:** Negative - **Layer:** API - **File:** `api/test_posts.py::TestCreatePost::test_create_post_invalid_payload` - **Steps:** 1. POST `/api/v1/posts` with too-short title - **Expected:** 422 validation error - **Last Verified:** 2026-05-10 ### TC-API-004: List Posts — Default - **Type:** Positive - **Layer:** API - **File:** `api/test_posts.py::TestListPosts::test_list_posts_default` - **Steps:** 1. GET `/api/v1/posts` without auth - **Expected:** 200 with `items` and `total` fields - **Last Verified:** 2026-05-10 ### TC-API-005: List Posts — Include Unpublished as Admin - **Type:** Positive - **Layer:** API - **File:** `api/test_posts.py::TestListPosts::test_list_posts_include_unpublished_as_admin` - **Steps:** 1. GET `/api/v1/posts?include_unpublished=true` with admin auth - **Expected:** 200 including unpublished posts - **Last Verified:** 2026-05-10 ### TC-API-006: List Posts — Include Unpublished as User (Forbidden) - **Type:** Policy - **Layer:** API - **File:** `api/test_posts.py::TestListPosts::test_list_posts_include_unpublished_as_user_returns_403` - **Steps:** 1. GET `/api/v1/posts?include_unpublished=true` with user auth - **Expected:** 403 ForbiddenException - **Last Verified:** 2026-05-10 ### TC-API-007: List Posts — Include Unpublished as Guest (Forbidden) - **Type:** Policy - **Layer:** API - **File:** `api/test_posts.py::TestListPosts::test_list_posts_include_unpublished_as_guest_returns_403` - **Steps:** 1. GET `/api/v1/posts?include_unpublished=true` with guest auth - **Expected:** 403 ForbiddenException - **Last Verified:** 2026-05-10 ### TC-API-008: List Published Posts - **Type:** Positive - **Layer:** API - **File:** `api/test_posts.py::TestListPublishedPosts::test_list_published_posts_success` - **Expected:** 200 with published posts only, public endpoint - **Last Verified:** 2026-05-10 ### TC-API-009: Search Posts - **Type:** Positive - **Layer:** API - **File:** `api/test_posts.py::TestSearchPosts::test_search_posts_success` - **Expected:** 200 with matching posts, public endpoint - **Last Verified:** 2026-05-10 ### TC-API-010: Get Posts by Tag - **Type:** Positive - **Layer:** API - **File:** `api/test_posts.py::TestGetPostsByTag::test_get_posts_by_tag_success` - **Expected:** 200 with tagged posts, public endpoint - **Last Verified:** 2026-05-10 ### TC-API-011: Get Posts by Author - **Type:** Positive - **Layer:** API - **File:** `api/test_posts.py::TestGetPostsByAuthor::test_get_posts_by_author_success` - **Expected:** 200 with author's posts, public endpoint - **Last Verified:** 2026-05-10 ### TC-API-012: Get Post by ID — Success - **Type:** Positive - **Layer:** API - **File:** `api/test_posts.py::TestGetPost::test_get_post_by_id_success` - **Expected:** 200 with post data, public endpoint - **Last Verified:** 2026-05-10 ### TC-API-013: Get Post by ID — Not Found - **Type:** Negative - **Layer:** API - **File:** `api/test_posts.py::TestGetPost::test_get_post_by_id_not_found` - **Expected:** 404 NotFoundException - **Last Verified:** 2026-05-10 ### TC-API-014: Get Post by Slug — Success - **Type:** Positive - **Layer:** API - **File:** `api/test_posts.py::TestGetPostBySlug::test_get_post_by_slug_success` - **Expected:** 200 with post data, public endpoint - **Last Verified:** 2026-05-10 ### TC-API-015: Get Post by Slug — Not Found - **Type:** Negative - **Layer:** API - **File:** `api/test_posts.py::TestGetPostBySlug::test_get_post_by_slug_not_found` - **Expected:** 404 NotFoundException - **Last Verified:** 2026-05-10 ### TC-API-016: Update Post — Own Post - **Type:** Positive - **Layer:** API - **File:** `api/test_posts.py::TestUpdatePost::test_update_own_post_success` - **Expected:** 200 with updated fields - **Last Verified:** 2026-05-10 ### TC-API-017: Update Post — Other User's Post (Forbidden) - **Type:** Policy - **Layer:** API - **File:** `api/test_posts.py::TestUpdatePost::test_update_other_user_post_returns_403` - **Expected:** 403 ForbiddenException - **Last Verified:** 2026-05-10 ### TC-API-018: Update Post — No Auth - **Type:** Negative - **Layer:** API - **File:** `api/test_posts.py::TestUpdatePost::test_update_post_no_auth` - **Expected:** 401 UnauthorizedException - **Last Verified:** 2026-05-10 ### TC-API-019: Delete Post — Own Post - **Type:** Positive - **Layer:** API - **File:** `api/test_posts.py::TestDeletePost::test_delete_own_post_success` - **Expected:** 204, post no longer accessible - **Last Verified:** 2026-05-10 ### TC-API-020: Delete Post — Other User's Post (Forbidden) - **Type:** Policy - **Layer:** API - **File:** `api/test_posts.py::TestDeletePost::test_delete_other_user_post_returns_403` - **Expected:** 403 ForbiddenException - **Last Verified:** 2026-05-10 ### TC-API-021: Delete Post — No Auth - **Type:** Negative - **Layer:** API - **File:** `api/test_posts.py::TestDeletePost::test_delete_post_no_auth` - **Expected:** 401 UnauthorizedException - **Last Verified:** 2026-05-10 ### TC-API-022: Publish Post — Own Post - **Type:** Positive - **Layer:** API - **File:** `api/test_posts.py::TestPublishPost::test_publish_own_post_success` - **Expected:** 200 with published=True - **Last Verified:** 2026-05-10 ### TC-API-023: Publish Post — Other User's Post (Forbidden) - **Type:** Policy - **Layer:** API - **File:** `api/test_posts.py::TestPublishPost::test_publish_other_user_post_returns_403` - **Expected:** 403 ForbiddenException - **Last Verified:** 2026-05-10 ### TC-API-024: Unpublish Post — Own Post - **Type:** Positive - **Layer:** API - **File:** `api/test_posts.py::TestUnpublishPost::test_unpublish_own_post_success` - **Expected:** 200 with published=False - **Last Verified:** 2026-05-10 ### TC-API-025: Unpublish Post — Other User's Post (Forbidden) - **Type:** Policy - **Layer:** API - **File:** `api/test_posts.py::TestUnpublishPost::test_unpublish_other_user_post_returns_403` - **Expected:** 403 ForbiddenException - **Last Verified:** 2026-05-10 ## Coverage Summary | Aspect | Coverage | Notes | |--------|----------|-------| | Create post | Unit + API + E2E | TC-API-001, TC-API-101, TC-API-102 | | Read post (by id/slug) | Unit + API | TC-API-012, TC-API-013, TC-API-014, TC-API-015 | | Update post | Unit + API | TC-API-016, TC-API-017, TC-API-018 | | Delete post | Unit + API + E2E | TC-API-019, TC-API-020, TC-API-021 | | Publish / Unpublish | Unit + API | TC-API-022, TC-API-023, TC-API-024, TC-API-025 | | List posts (all filters) | Unit + API | TC-API-004, TC-API-005, TC-API-006, TC-API-007, TC-API-008 | | Search posts | Unit + API | TC-API-009 | ## 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