From 2a3925337de173891270ee3f8f94357ab0657fec Mon Sep 17 00:00:00 2001 From: rdev-worker Date: Wed, 11 Feb 2026 02:15:50 +0000 Subject: [PATCH] build: /create-qa-plan data-models --- .sdlc/features/data-models/manifest.yaml | 2 +- .sdlc/features/data-models/qa-plan.md | 182 +++++++++++++++++++++++ 2 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 .sdlc/features/data-models/qa-plan.md diff --git a/.sdlc/features/data-models/manifest.yaml b/.sdlc/features/data-models/manifest.yaml index fd28c6a..791b773 100644 --- a/.sdlc/features/data-models/manifest.yaml +++ b/.sdlc/features/data-models/manifest.yaml @@ -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 diff --git a/.sdlc/features/data-models/qa-plan.md b/.sdlc/features/data-models/qa-plan.md new file mode 100644 index 0000000..ef64e8c --- /dev/null +++ b/.sdlc/features/data-models/qa-plan.md @@ -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()`