Add 45 API tests covering all 12 post endpoints (CRUD, publish/unpublish) with RBAC policy coverage across guest, user, admin roles. Fix get_keycloak_client() in deps.py to be async - Dishka's async container requires await on get(), without it a coroutine object was returned instead of the actual client.
17 KiB
17 KiB
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:
- Mock
slug_existsto returnFalse - Execute
CreatePostUseCasewith valid DTO
- Mock
- Expected:
- Returns
PostResponseDTOwith correct title and author repository.addcalled oncetransaction_manager.commitcalled once
- Returns
- 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
CreatePostUseCasewith 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/ContentVO 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
PostResponseDTOfor 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
PostResponseDTOfor 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
PostResponseDTOwithpublished=True repository.updateandcommitcalled once
- Returns
- 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
ForbiddenExceptionwhen 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
PostResponseDTOwithpublished=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
ForbiddenExceptionwhen 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_pagefixtures - Steps:
- Generate post data via
PostDataGenerator - Open home page and click "Write a Post"
- Fill form (title, content, tags)
- Click "Publish Post"
- Generate post data via
- 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
- Redirect to
- 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_pagefixtures - Steps:
- User creates a draft post
- User creates and publishes another post
- Check visibility for each role on the home page
- 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_pagefixtures - Steps:
- Generate a random slug that does not exist in the database.
- Navigate to
/web/posts/{fake-slug}as guest and as authenticated user.
- Expected:
- Error page is rendered with
data-testid="error-code"showing404 - Both guest and user see the same 404 response
- Error page is rendered with
- 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_pagefixtures - Steps:
- User creates a draft post and saves it.
- Extract the slug from the detail page URL.
- 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_pagefixture - Steps:
- Generate post data via
PostDataGenerator - Open home page and click "Write a Post"
- Fill form (title, content, tags) and click "Publish Post"
- On post detail page, click "Delete" and accept confirm dialog
- Generate post data via
- Expected:
- Redirect to
/web/ - Post no longer appears on home page
- Redirect to
- 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:
- POST
/api/v1/postswith valid payload and user auth
- POST
- 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:
- POST
/api/v1/postswith too-short title
- POST
- 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:
- GET
/api/v1/postswithout auth
- GET
- Expected: 200 with
itemsandtotalfields - 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:
- GET
/api/v1/posts?include_unpublished=truewith admin auth
- GET
- 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:
- GET
/api/v1/posts?include_unpublished=truewith user auth
- GET
- 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:
- GET
/api/v1/posts?include_unpublished=truewith guest auth
- GET
- 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)
- TC-UNIT-024: UpdatePostUseCase — not found scenario
- TC-UNIT-025: UpdatePostUseCase — content and tags update
- TC-UNIT-026: ListPostsUseCase — pagination edge cases (page boundaries, empty page)
- TC-E2E-003: 404 error page for nonexistent post
- TC-E2E-003a: Edit post via web UI and verify changes (own post)
- TC-E2E-004: Delete post via web UI and verify removal
- TC-E2E-005: Save post as draft and publish via edit, verify visibility change
- TC-E2E-006: Search posts via web UI
- TC-E2E-007: Pagination navigation on home page
- TC-E2E-009: Profile page renders user info and role badge correctly
- TC-E2E-010: Theme toggle switches between light and dark with localStorage persistence