Files
blog.pyaqa.ru/tests/FEATURE_POST_LIFECYCLE.md
Sergey Vanyushkin c9b380c601 test(api): add full API test suite with get_keycloak_client async fix
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.
2026-05-10 11:21:58 +00:00

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:
    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
  • 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)
  • 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