build: /create-qa-plan data-models
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
rdev-worker 2026-02-11 02:15:50 +00:00
parent 39c02c2cc7
commit 2a3925337d
2 changed files with 183 additions and 1 deletions

View File

@ -18,7 +18,7 @@ artifacts:
approved_by: user
approved_at: 2026-02-11T01:59:14.649064247Z
qa_plan:
status: pending
status: draft
path: qa-plan.md
qa_results:
status: pending

View File

@ -0,0 +1,182 @@
# QA Plan: Core Data Models & Persistence
## Test Scenarios
### Happy Path
| ID | Scenario | Input | Expected Output | Derived From |
|----|----------|-------|-----------------|--------------|
| HP-1 | Create project with valid data | `{"name": "My Project", "description": "Desc"}` | 201, project returned with UUID, status=active, timestamps set | AC: Handler Layer, Domain Layer |
| HP-2 | List projects returns all projects | GET `/projects` with 2+ projects seeded | 200, `{data: [...], meta}` array of all projects | AC: Handler Layer |
| HP-3 | Get project by valid UUID | GET `/projects/{id}` with existing ID | 200, single project in `{data}` envelope | AC: Handler Layer |
| HP-4 | Update project name and description | PUT `/projects/{id}` with `{"name": "New", "description": "New"}` | 200, updated project, `updated_at` changed | AC: Handler Layer, Domain Layer |
| HP-5 | Update project status to archived | PUT `/projects/{id}` with `{"status": "archived"}` | 200, status=archived | AC: Domain Layer (free status transitions) |
| HP-6 | Delete project | DELETE `/projects/{id}` with existing ID | 204, no content | AC: Handler Layer |
| HP-7 | Create task in project | `{"title": "Task 1", "priority": "high"}` to `/projects/{projectId}/tasks` | 201, task returned with project_id set, status=todo, priority=high | AC: Handler Layer, Service Layer |
| HP-8 | List tasks by project | GET `/projects/{projectId}/tasks` | 200, array of tasks belonging to project | AC: Handler Layer |
| HP-9 | Get task by ID | GET `/tasks/{id}` | 200, single task in envelope | AC: Handler Layer |
| HP-10 | Update task fields | PUT `/tasks/{id}` with title, status, priority, position | 200, updated task, `updated_at` bumped | AC: Handler Layer, Domain Layer |
| HP-11 | Delete task | DELETE `/tasks/{id}` | 204 | AC: Handler Layer |
| HP-12 | Create label in project | `{"name": "Bug", "color": "#FF5733"}` to `/projects/{projectId}/labels` | 201, label with project_id, name, color | AC: Handler Layer |
| HP-13 | List labels by project | GET `/projects/{projectId}/labels` | 200, array of labels for project | AC: Handler Layer |
| HP-14 | Get label by ID | GET `/labels/{id}` | 200, single label | AC: Handler Layer |
| HP-15 | Update label name and color | PUT `/labels/{id}` with new name and color | 200, updated label | AC: Handler Layer |
| HP-16 | Delete label | DELETE `/labels/{id}` | 204 | AC: Handler Layer |
| HP-17 | Create assignment | `{"assignee": "user@example.com"}` to `/tasks/{taskId}/assignments` | 201, assignment with task_id, assignee, created_at | AC: Handler Layer |
| HP-18 | List assignments by task | GET `/tasks/{taskId}/assignments` | 200, array of assignments | AC: Handler Layer |
| HP-19 | Delete assignment | DELETE `/assignments/{id}` | 204 | AC: Handler Layer |
| HP-20 | Create project with no description | `{"name": "Minimal"}` | 201, description defaults to empty string | AC: Domain Layer |
| HP-21 | Create task with defaults | `{"title": "Simple Task"}` | 201, status=todo, priority=medium, position=0 | AC: Domain Layer |
| HP-22 | Multiple assignments on same task | Two different assignees on one task | Both 201, both returned in list | AC: Domain Layer (Assignment entity) |
| HP-23 | List returns empty array when no records | GET list on empty collection | 200, `{data: []}` (not null) | AC: Handler Layer |
### Edge Cases
| ID | Scenario | Input | Expected Output | Derived From |
|----|----------|-------|-----------------|--------------|
| EC-1 | Project name at max length (200 chars) | Name with exactly 200 characters | 201, project created | AC: Domain Layer (1-200 chars) |
| EC-2 | Project name at min length (1 char) | `{"name": "A"}` | 201, project created | AC: Domain Layer (1-200 chars) |
| EC-3 | Project description at max (1000 chars) | Description with 1000 characters | 201, project created | AC: Domain Layer |
| EC-4 | Task title at max length (300 chars) | Title with 300 characters | 201, task created | AC: Domain Layer (1-300 chars) |
| EC-5 | Task description at max (5000 chars) | Description with 5000 characters | 201, task created | AC: Domain Layer |
| EC-6 | Label name at max length (50 chars) | Name with 50 characters | 201, label created | AC: Domain Layer (1-50 chars) |
| EC-7 | Label color with lowercase hex | `{"color": "#ff5733"}` | 201, accepted | AC: Domain Layer (hex color) |
| EC-8 | Label color with uppercase hex | `{"color": "#FF5733"}` | 201, accepted | AC: Domain Layer (hex color) |
| EC-9 | Assignee at max length (200 chars) | Assignee string with 200 characters | 201, assignment created | AC: Domain Layer (1-200 chars) |
| EC-10 | Task with position=0 (default) | Create task without position field | 201, position=0 | AC: Domain Layer |
| EC-11 | Task with large position value | `{"title": "T", "position": 999999}` | 201, position preserved | AC: Domain Layer |
| EC-12 | Delete project cascades to tasks | Delete project with tasks, then get tasks | 204 on delete; tasks no longer found | AC: Technical Constraints (CASCADE) |
| EC-13 | Delete project cascades to labels | Delete project with labels | 204; labels removed | AC: Technical Constraints (CASCADE) |
| EC-14 | Delete project cascades transitively to assignments | Delete project → tasks → assignments | 204; assignments removed | AC: Technical Constraints (CASCADE) |
| EC-15 | Delete task cascades to assignments | Delete task with assignments | 204; assignments removed | AC: Technical Constraints (CASCADE) |
| EC-16 | Same label name in different projects | Create label "Bug" in project A and project B | Both 201 (unique per project, not global) | AC: Domain Layer, Migration (unique per project_id) |
| EC-17 | Update project name to current name | PUT with same name as existing | 200, no error (no-op update) | AC: Service Layer |
| EC-18 | All task status transitions | Change todo→in_progress→done, done→todo, etc. | 200 for all valid transitions (free transitions) | AC: Design Decision #4 |
| EC-19 | All task priority values | Create/update with low, medium, high | 201/200 for each | AC: Domain Layer |
| EC-20 | Project status active→archived→active | Toggle status back and forth | 200 each time | AC: Design Decision #4 |
### Error Cases
| ID | Scenario | Input | Expected Output | Derived From |
|----|----------|-------|-----------------|--------------|
| ER-1 | Create project with duplicate name | Two projects with same name | 409 Conflict on second create | AC: Domain Layer (ErrDuplicateProjectName) |
| ER-2 | Create project with empty name | `{"name": ""}` | 400/422 validation error | AC: Domain Layer (required, 1-200) |
| ER-3 | Create project with name > 200 chars | Name with 201 characters | 400/422 validation error | AC: Domain Layer (max 200) |
| ER-4 | Create project with description > 1000 chars | Description with 1001 characters | 400/422 validation error | AC: Domain Layer (max 1000) |
| ER-5 | Get project with non-existent UUID | Valid UUID format, not in DB | 404 Not Found | AC: Domain Layer (ErrProjectNotFound) |
| ER-6 | Get project with invalid UUID format | `GET /projects/not-a-uuid` | 400 Bad Request | AC: Handler Layer (UUID validation) |
| ER-7 | Update project with duplicate name | Change name to one that already exists | 409 Conflict | AC: Service Layer (duplicate name check) |
| ER-8 | Update project with invalid status | `{"status": "deleted"}` | 400 Bad Request | AC: Domain Layer (ErrInvalidStatus) |
| ER-9 | Delete non-existent project | DELETE with unknown UUID | 404 Not Found | AC: Domain Layer (ErrProjectNotFound) |
| ER-10 | Create task with empty title | `{"title": ""}` | 400/422 validation error | AC: Domain Layer (required, 1-300) |
| ER-11 | Create task with title > 300 chars | Title with 301 characters | 400/422 validation error | AC: Domain Layer (max 300) |
| ER-12 | Create task in non-existent project | POST to `/projects/{badId}/tasks` | 404 Not Found (project not found) | AC: Service Layer (validate project exists) |
| ER-13 | Create task with invalid priority | `{"title": "T", "priority": "urgent"}` | 400 Bad Request | AC: Domain Layer (ErrInvalidPriority) |
| ER-14 | Update task with invalid status | `{"status": "cancelled"}` | 400 Bad Request | AC: Domain Layer (ErrInvalidStatus) |
| ER-15 | Get non-existent task | Valid UUID, not in DB | 404 Not Found | AC: Domain Layer (ErrTaskNotFound) |
| ER-16 | Create label with empty name | `{"name": "", "color": "#FF5733"}` | 400/422 validation error | AC: Domain Layer (required, 1-50) |
| ER-17 | Create label with name > 50 chars | Name with 51 characters | 400/422 validation error | AC: Domain Layer (max 50) |
| ER-18 | Create label with invalid color | `{"name": "Bug", "color": "red"}` | 400 Bad Request | AC: Domain Layer (hex color format) |
| ER-19 | Create label with color missing # | `{"name": "Bug", "color": "FF5733"}` | 400 Bad Request | AC: Domain Layer (hex format `#XXXXXX`) |
| ER-20 | Create duplicate label name in same project | Two labels with same name in one project | 409 Conflict | AC: Domain Layer (ErrDuplicateLabelName) |
| ER-21 | Create label in non-existent project | POST to `/projects/{badId}/labels` | 404 Not Found | AC: Service Layer (validate project exists) |
| ER-22 | Create assignment with empty assignee | `{"assignee": ""}` | 400/422 validation error | AC: Domain Layer (required, 1-200) |
| ER-23 | Create duplicate assignment | Same assignee on same task twice | 409 Conflict | AC: Domain Layer (ErrDuplicateAssignment) |
| ER-24 | Create assignment on non-existent task | POST to `/tasks/{badId}/assignments` | 404 Not Found | AC: Service Layer (validate task exists) |
| ER-25 | Delete non-existent assignment | DELETE with unknown UUID | 404 Not Found | AC: Domain Layer (ErrAssignmentNotFound) |
| ER-26 | Create project with missing body | Empty request body | 400/422 | AC: Handler Layer (BindAndValidate) |
| ER-27 | Create project with malformed JSON | `{invalid json` | 400 | AC: Handler Layer (Bind) |
| ER-28 | Get task with invalid UUID | `GET /tasks/not-a-uuid` | 400 Bad Request | AC: Handler Layer |
| ER-29 | Create task with description > 5000 chars | Description with 5001 characters | 400/422 validation error | AC: Domain Layer (max 5000) |
| ER-30 | Update non-existent task | PUT to unknown task UUID | 404 Not Found | AC: Domain Layer (ErrTaskNotFound) |
| ER-31 | Update non-existent label | PUT to unknown label UUID | 404 Not Found | AC: Domain Layer (ErrLabelNotFound) |
| ER-32 | Update label to duplicate name in project | Rename to name already used in same project | 409 Conflict | AC: Service Layer |
| ER-33 | List tasks for non-existent project | GET `/projects/{badId}/tasks` | 200 empty list or 404 | AC: Handler Layer |
## Test Data Requirements
### Fixtures
- **Projects:** At least 3 projects with different names, statuses (active/archived)
- **Tasks:** Multiple tasks per project with varied statuses (todo/in_progress/done) and priorities (low/medium/high)
- **Labels:** Multiple labels per project, including labels with same name in different projects
- **Assignments:** Multiple assignments per task, plus tasks with no assignments
### Mock Repositories
- In-memory mock implementations for each repository interface (`ProjectRepository`, `TaskRepository`, `LabelRepository`, `AssignmentRepository`)
- Follow the existing `mockExampleRepository` pattern: thread-safe with `sync.RWMutex`, map-based storage, copy semantics
- Interface compliance assertions: `var _ port.XxxRepository = (*mockXxxRepository)(nil)`
- Mock repositories return domain errors for not-found and duplicate scenarios
### Test Setup
- `newTestHandler()` pattern for each handler, returning `(handler, mockRepo)` or `(handler, mockRepos...)`
- `logging.Nop()` for all test loggers
- `chi.NewRouter()` + `httptest.NewRequest/NewRecorder` for HTTP testing
- `context.Background()` for test contexts
- Pre-seed data in mock repos before assertions
## Integration Test Plan
### Cross-Entity Interactions
1. **Project → Task lifecycle:** Create project → create tasks in project → list tasks → delete project → verify tasks deleted (CASCADE)
2. **Project → Label lifecycle:** Create project → create labels → verify unique name per project → delete project → verify labels deleted
3. **Task → Assignment lifecycle:** Create project → create task → create assignments → verify duplicate rejected → delete task → verify assignments deleted
4. **Full cascade chain:** Create project → create task → create assignment → delete project → verify task and assignment both deleted
### Service-Layer Cross-Dependencies
5. **TaskService validates project exists:** Service calls `ProjectRepository.Get` before creating task — mock returns `ErrProjectNotFound` → verify service propagates correctly
6. **LabelService validates project exists:** Same pattern as task
7. **AssignmentService validates task exists:** Service calls `TaskRepository.Get` before creating assignment
### Route Registration
8. **All endpoints reachable:** Verify no 404s on correctly-formed paths for all 17 endpoints
9. **Brace syntax routing:** Confirm `{id}`, `{projectId}`, `{taskId}` parameters extract correctly via `chi.URLParam()`
### Wiring (main.go)
10. **Dependency injection:** Verify main.go creates all adapters, services, and handlers and wires them correctly
11. **Database connection + migrations:** Verify `database.MustConnect``database.MustRunMigrations` → adapter creation order
12. **Graceful shutdown:** Verify `pool.Close` is called on shutdown
## Performance Considerations
### Load Expectations
- Developer-facing studio tool with low concurrency
- Typical dataset: <100 projects, <1000 tasks, <500 labels, <2000 assignments
- No pagination in v1 — acceptable for expected volumes
### Benchmarks to Consider
- List operations with 100+ records: verify response time < 100ms
- Cascade delete of project with 50+ tasks and 100+ assignments: verify completes without timeout
### Query Efficiency
- Verify indexes used: `idx_tasks_project_id`, `idx_tasks_status`, `idx_labels_project_id`, `idx_assignments_task_id`
- All list queries scoped by single FK — ensure no full table scans on unindexed columns
### No Dedicated Performance Tests in v1
- Given the low-traffic nature of the tool, formal load testing is not required
- Monitor query performance via structured logging if issues arise
## Manual Verification Steps
### Database Schema Verification
1. Connect to PostgreSQL and verify all 4 tables exist with correct columns, types, and constraints
2. Verify `ON DELETE CASCADE` behavior by inserting test data and deleting a parent row
3. Verify CHECK constraints reject invalid status/priority values
4. Verify unique constraints reject duplicate project names and duplicate (project_id, name) for labels
### Migration Idempotency
5. Run migrations twice — verify second run is a no-op (no errors, no duplicate tables)
6. Verify migrations are tracked in `schema_migrations` table
### OpenAPI Documentation
7. Access Scalar docs endpoint and verify all 17 endpoints are documented
8. Verify request/response schemas match the actual DTOs
9. Verify tags group endpoints correctly: `Projects`, `Tasks`, `Labels`, `Assignments`
### Response Envelope
10. Verify all GET responses return `{data, meta}` envelope
11. Verify POST responses return `{data}` with 201 status
12. Verify DELETE responses return 204 with no body
### Error Response Format
13. Verify 400/409/404 errors follow the standard error response format from `httperror`
14. Verify 422 validation errors include field-level detail from `app.BindAndValidate()`