test(api): add full API test suite with get_keycloak_client async fix
All checks were successful
ci/woodpecker/pr/pipeline Pipeline was successful

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.
This commit is contained in:
2026-05-10 14:08:23 +03:00
parent c790b6edc6
commit e9271c850a
7 changed files with 1216 additions and 18 deletions

View File

@@ -250,17 +250,199 @@ Covers both API use cases and web UI end-to-end flows.
- 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 + 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 |
| 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)