15 KiB
15 KiB
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
mockExampleRepositorypattern: thread-safe withsync.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 loggerschi.NewRouter()+httptest.NewRequest/NewRecorderfor HTTP testingcontext.Background()for test contexts- Pre-seed data in mock repos before assertions
Integration Test Plan
Cross-Entity Interactions
- Project → Task lifecycle: Create project → create tasks in project → list tasks → delete project → verify tasks deleted (CASCADE)
- Project → Label lifecycle: Create project → create labels → verify unique name per project → delete project → verify labels deleted
- Task → Assignment lifecycle: Create project → create task → create assignments → verify duplicate rejected → delete task → verify assignments deleted
- Full cascade chain: Create project → create task → create assignment → delete project → verify task and assignment both deleted
Service-Layer Cross-Dependencies
- TaskService validates project exists: Service calls
ProjectRepository.Getbefore creating task — mock returnsErrProjectNotFound→ verify service propagates correctly - LabelService validates project exists: Same pattern as task
- AssignmentService validates task exists: Service calls
TaskRepository.Getbefore creating assignment
Route Registration
- All endpoints reachable: Verify no 404s on correctly-formed paths for all 17 endpoints
- Brace syntax routing: Confirm
{id},{projectId},{taskId}parameters extract correctly viachi.URLParam()
Wiring (main.go)
- Dependency injection: Verify main.go creates all adapters, services, and handlers and wires them correctly
- Database connection + migrations: Verify
database.MustConnect→database.MustRunMigrations→ adapter creation order - Graceful shutdown: Verify
pool.Closeis 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
- Connect to PostgreSQL and verify all 4 tables exist with correct columns, types, and constraints
- Verify
ON DELETE CASCADEbehavior by inserting test data and deleting a parent row - Verify CHECK constraints reject invalid status/priority values
- Verify unique constraints reject duplicate project names and duplicate (project_id, name) for labels
Migration Idempotency
- Run migrations twice — verify second run is a no-op (no errors, no duplicate tables)
- Verify migrations are tracked in
schema_migrationstable
OpenAPI Documentation
- Access Scalar docs endpoint and verify all 17 endpoints are documented
- Verify request/response schemas match the actual DTOs
- Verify tags group endpoints correctly:
Projects,Tasks,Labels,Assignments
Response Envelope
- Verify all GET responses return
{data, meta}envelope - Verify POST responses return
{data}with 201 status - Verify DELETE responses return 204 with no body
Error Response Format
- Verify 400/409/404 errors follow the standard error response format from
httperror - Verify 422 validation errors include field-level detail from
app.BindAndValidate()