This commit is contained in:
parent
39c02c2cc7
commit
2a3925337d
@ -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
|
||||
|
||||
182
.sdlc/features/data-models/qa-plan.md
Normal file
182
.sdlc/features/data-models/qa-plan.md
Normal 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()`
|
||||
Loading…
Reference in New Issue
Block a user