20 KiB
20 KiB
QA Plan: Core Data Models & Persistence
Test Scenarios
Happy Path
| ID | Scenario | Input | Expected Output | Derived From |
|---|---|---|---|---|
| HP-1 | Create a project with valid name and description | POST /projects {"name":"Alpha","description":"My project"} |
201 Created; {data: {id: UUID, name: "Alpha", description: "My project", created_at, updated_at}} |
AC: Projects CRUD |
| HP-2 | List all projects | GET /projects (with 2+ projects created) |
200 OK; {data: [{...}, {...}]} |
AC: Projects CRUD |
| HP-3 | Get a project by ID | GET /projects/{id} |
200 OK; {data: {id, name, description, created_at, updated_at}} |
AC: Projects CRUD |
| HP-4 | Update a project | PUT /projects/{id} {"name":"Beta","description":"Updated"} |
200 OK; updated fields reflected | AC: Projects CRUD |
| HP-5 | Delete a project | DELETE /projects/{id} |
204 No Content | AC: Projects CRUD |
| HP-6 | Create a task in a project | POST /projects/{projectId}/tasks {"title":"Fix bug","status":"open","priority":"high"} |
201 Created; {data: {id, project_id, title, status: "open", priority: "high", ...}} |
AC: Tasks CRUD |
| HP-7 | List tasks in a project | GET /projects/{projectId}/tasks |
200 OK; {data: [tasks...]} |
AC: Tasks CRUD |
| HP-8 | Get a task by ID | GET /tasks/{id} |
200 OK; {data: {id, project_id, title, ...}} |
AC: Tasks CRUD |
| HP-9 | Update a task | PUT /tasks/{id} {"title":"Updated","status":"done","priority":"low"} |
200 OK; updated fields reflected | AC: Tasks CRUD |
| HP-10 | Delete a task | DELETE /tasks/{id} |
204 No Content | AC: Tasks CRUD |
| HP-11 | Filter tasks by status | GET /projects/{projectId}/tasks?status=open |
200 OK; only tasks with status "open" | AC: Tasks filtering |
| HP-12 | Filter tasks by priority | GET /projects/{projectId}/tasks?priority=high |
200 OK; only tasks with priority "high" | AC: Tasks filtering |
| HP-13 | Filter tasks by status AND priority | GET /projects/{projectId}/tasks?status=open&priority=critical |
200 OK; tasks matching both filters | AC: Tasks filtering |
| HP-14 | Create a label | POST /labels {"name":"Bug","color":"#FF5733"} |
201 Created; {data: {id, name: "Bug", color: "#FF5733", created_at}} |
AC: Labels CRUD |
| HP-15 | Create a label without color | POST /labels {"name":"Feature"} |
201 Created; color is empty/null | AC: Labels color optional |
| HP-16 | List all labels | GET /labels |
200 OK; {data: [labels...]} |
AC: Labels CRUD |
| HP-17 | Get a label by ID | GET /labels/{id} |
200 OK; {data: {id, name, color, created_at}} |
AC: Labels CRUD |
| HP-18 | Update a label | PUT /labels/{id} {"name":"Enhancement","color":"#00FF00"} |
200 OK; updated fields reflected | AC: Labels CRUD |
| HP-19 | Delete a label | DELETE /labels/{id} |
204 No Content | AC: Labels CRUD |
| HP-20 | Attach a label to a task | POST /tasks/{taskId}/labels/{labelId} |
201 Created or 204 No Content | AC: Task-Label join |
| HP-21 | List labels for a task | GET /tasks/{taskId}/labels |
200 OK; {data: [labels...]} |
AC: Task-Label join |
| HP-22 | Detach a label from a task | DELETE /tasks/{taskId}/labels/{labelId} |
204 No Content | AC: Task-Label join |
| HP-23 | Assign a user to a task | POST /tasks/{taskId}/assignments {"user_id":"user-123"} |
201 Created; {data: {id, task_id, user_id: "user-123", assigned_at}} |
AC: Assignments CRUD |
| HP-24 | List assignments for a task | GET /tasks/{taskId}/assignments |
200 OK; {data: [assignments...]} |
AC: Assignments CRUD |
| HP-25 | Remove an assignment | DELETE /tasks/{taskId}/assignments/{assignmentId} |
204 No Content | AC: Assignments CRUD |
| HP-26 | Task defaults status=open, priority=medium | POST /projects/{projectId}/tasks {"title":"No defaults"} (omit status/priority) |
201 Created; status="open", priority="medium" | AC: Tasks status/priority enums |
| HP-27 | All task statuses accepted | POST with each of open, in_progress, done, cancelled |
201 Created for each | AC: Tasks status enum |
| HP-28 | All task priorities accepted | POST with each of low, medium, high, critical |
201 Created for each | AC: Tasks priority enum |
Edge Cases
| ID | Scenario | Input | Expected Output | Derived From |
|---|---|---|---|---|
| EC-1 | Project name exactly 1 character | POST /projects {"name":"A"} |
201 Created | AC: Project name 1-100 chars |
| EC-2 | Project name exactly 100 characters | POST /projects {"name":"<100 chars>"} |
201 Created | AC: Project name 1-100 chars |
| EC-3 | Project description exactly 500 characters | POST /projects {"name":"X","description":"<500 chars>"} |
201 Created | AC: Description max 500 |
| EC-4 | Task title exactly 1 character | POST /projects/{id}/tasks {"title":"X"} |
201 Created | AC: Task title 1-200 chars |
| EC-5 | Task title exactly 200 characters | POST /projects/{id}/tasks {"title":"<200 chars>"} |
201 Created | AC: Task title 1-200 chars |
| EC-6 | Task description exactly 2000 characters | POST /projects/{id}/tasks {"title":"T","description":"<2000 chars>"} |
201 Created | AC: Description max 2000 |
| EC-7 | Label name exactly 1 character | POST /labels {"name":"X"} |
201 Created | AC: Label name 1-50 chars |
| EC-8 | Label name exactly 50 characters | POST /labels {"name":"<50 chars>"} |
201 Created | AC: Label name 1-50 chars |
| EC-9 | Label color with lowercase hex | POST /labels {"name":"L","color":"#ff5733"} |
201 Created | AC: Color hex format |
| EC-10 | Label color with uppercase hex | POST /labels {"name":"L2","color":"#FF5733"} |
201 Created | AC: Color hex format |
| EC-11 | Label color with mixed case hex | POST /labels {"name":"L3","color":"#aAbBcC"} |
201 Created | AC: Color hex format |
| EC-12 | Delete project cascades to tasks | Create project → create task → delete project → GET task | 404 Not Found for the task | AC: Cascade delete |
| EC-13 | Delete project cascades to task-labels | Create project → task → label → attach → delete project → GET task labels | 404 or empty | AC: Cascade delete |
| EC-14 | Delete project cascades to assignments | Create project → task → assign → delete project → GET assignments | 404 or empty | AC: Cascade delete |
| EC-15 | Delete label cascades to task-labels | Create label → attach to task → delete label → list task labels | Label no longer in list | AC: Label cascade |
| EC-16 | Delete task cascades to assignments | Create task → assign user → delete task → GET assignment | 404 | AC: Implicit cascade |
| EC-17 | Delete task cascades to task-labels | Create task → attach label → delete task → check task-label | Association removed | AC: Implicit cascade |
| EC-18 | List tasks with no filters returns all | GET /projects/{id}/tasks (no query params) |
All tasks for project | AC: Tasks filtering |
| EC-19 | List tasks with non-matching filter | GET /projects/{id}/tasks?status=cancelled (no cancelled tasks) |
200 OK; {data: []} |
AC: Tasks filtering |
| EC-20 | List projects when empty | GET /projects (no projects created) |
200 OK; {data: []} |
AC: Projects list |
| EC-21 | List labels when empty | GET /labels (no labels created) |
200 OK; {data: []} |
AC: Labels list |
| EC-22 | Assign user with opaque string ID | POST /tasks/{id}/assignments {"user_id":"user@email.com"} |
201 Created | AC: user_id is opaque string |
| EC-23 | Assign user with UUID-format user_id | POST /tasks/{id}/assignments {"user_id":"550e8400-e29b-41d4-a716-446655440000"} |
201 Created | AC: user_id flexibility |
| EC-24 | Multiple labels on same task | Attach 3 different labels to one task → list | 200 OK; all 3 labels returned | AC: Task-Label many-to-many |
| EC-25 | Same label on multiple tasks | Attach label to 3 different tasks | All 3 attachments succeed | AC: Labels are global |
| EC-26 | Multiple users assigned to same task | Assign 3 users to one task → list | 200 OK; all 3 assignments returned | AC: Assignments |
| EC-27 | Project with empty description | POST /projects {"name":"No Desc"} (omit description) |
201 Created; description is empty/null | AC: Description optional |
| EC-28 | Task with empty description | POST /projects/{id}/tasks {"title":"T"} (omit description) |
201 Created; description is empty/null | AC: Description optional |
Error Cases
| ID | Scenario | Input | Expected Output | Derived From |
|---|---|---|---|---|
| ER-1 | Create project with empty name | POST /projects {"name":""} |
400 Bad Request | AC: Project name required |
| ER-2 | Create project with name > 100 chars | POST /projects {"name":"<101 chars>"} |
400 Bad Request | AC: Project name max 100 |
| ER-3 | Create project with description > 500 chars | POST /projects {"name":"X","description":"<501 chars>"} |
400 Bad Request | AC: Description max 500 |
| ER-4 | Create project with duplicate name | POST /projects {"name":"Existing"} (name already exists) |
409 Conflict | AC: Duplicate project name |
| ER-5 | Update project with duplicate name | PUT /projects/{id} with name matching another project |
409 Conflict | AC: Duplicate project name |
| ER-6 | Get non-existent project | GET /projects/{random-uuid} |
404 Not Found | AC: Projects CRUD |
| ER-7 | Update non-existent project | PUT /projects/{random-uuid} |
404 Not Found | AC: Projects CRUD |
| ER-8 | Delete non-existent project | DELETE /projects/{random-uuid} |
404 Not Found | AC: Projects CRUD |
| ER-9 | Get project with invalid ID format | GET /projects/not-a-uuid |
400 Bad Request | AC: UUID primary keys |
| ER-10 | Create task with empty title | POST /projects/{id}/tasks {"title":""} |
400 Bad Request | AC: Task title required |
| ER-11 | Create task with title > 200 chars | POST /projects/{id}/tasks {"title":"<201 chars>"} |
400 Bad Request | AC: Task title max 200 |
| ER-12 | Create task with description > 2000 chars | POST /projects/{id}/tasks {"title":"T","description":"<2001 chars>"} |
400 Bad Request | AC: Description max 2000 |
| ER-13 | Create task with invalid status | POST /projects/{id}/tasks {"title":"T","status":"invalid"} |
400 Bad Request | AC: Status enum |
| ER-14 | Create task with invalid priority | POST /projects/{id}/tasks {"title":"T","priority":"invalid"} |
400 Bad Request | AC: Priority enum |
| ER-15 | Create task for non-existent project | POST /projects/{random-uuid}/tasks {"title":"T"} |
404 Not Found | AC: Task project_id validated |
| ER-16 | Get non-existent task | GET /tasks/{random-uuid} |
404 Not Found | AC: Tasks CRUD |
| ER-17 | Update non-existent task | PUT /tasks/{random-uuid} |
404 Not Found | AC: Tasks CRUD |
| ER-18 | Delete non-existent task | DELETE /tasks/{random-uuid} |
404 Not Found | AC: Tasks CRUD |
| ER-19 | Create label with empty name | POST /labels {"name":""} |
400 Bad Request | AC: Label name required |
| ER-20 | Create label with name > 50 chars | POST /labels {"name":"<51 chars>"} |
400 Bad Request | AC: Label name max 50 |
| ER-21 | Create label with invalid color format | POST /labels {"name":"L","color":"red"} |
400 Bad Request | AC: Color hex format |
| ER-22 | Create label with invalid hex (too short) | POST /labels {"name":"L","color":"#FFF"} |
400 Bad Request | AC: Color hex format |
| ER-23 | Create label with invalid hex (no #) | POST /labels {"name":"L","color":"FF5733"} |
400 Bad Request | AC: Color hex format |
| ER-24 | Create label with duplicate name | POST /labels {"name":"Existing"} (name already exists) |
409 Conflict | AC: Duplicate label name |
| ER-25 | Update label with duplicate name | PUT /labels/{id} with name matching another label |
409 Conflict | AC: Duplicate label name |
| ER-26 | Get non-existent label | GET /labels/{random-uuid} |
404 Not Found | AC: Labels CRUD |
| ER-27 | Attach non-existent label to task | POST /tasks/{taskId}/labels/{random-uuid} |
404 Not Found | AC: Assign non-existent label |
| ER-28 | Attach label to non-existent task | POST /tasks/{random-uuid}/labels/{labelId} |
404 Not Found | AC: Assign non-existent task |
| ER-29 | Duplicate task-label association | Attach same label to same task twice | 409 Conflict | AC: Duplicate task-label |
| ER-30 | Detach label not attached to task | DELETE /tasks/{taskId}/labels/{labelId} (not attached) |
404 Not Found | AC: Task-Label CRUD |
| ER-31 | Assign user to non-existent task | POST /tasks/{random-uuid}/assignments {"user_id":"u1"} |
404 Not Found | AC: Assign non-existent task |
| ER-32 | Duplicate user assignment to same task | Assign same user_id to same task twice | 409 Conflict | AC: Duplicate assignment |
| ER-33 | Remove non-existent assignment | DELETE /tasks/{taskId}/assignments/{random-uuid} |
404 Not Found | AC: Assignments CRUD |
| ER-34 | Create project with missing body | POST /projects (no body or {}) |
400 Bad Request | AC: Name required |
| ER-35 | Create task with missing body | POST /projects/{id}/tasks (no body or {}) |
400 Bad Request | AC: Title required |
| ER-36 | Create assignment with missing user_id | POST /tasks/{id}/assignments {} |
400 Bad Request | AC: user_id required |
| ER-37 | Create project with invalid JSON | POST /projects {invalid json} |
400 Bad Request | AC: Request binding |
| ER-38 | Write endpoint without auth (when enabled) | POST /projects without auth token (AUTH_ENABLED=true) |
401 Unauthorized | AC: Auth on writes |
Test Data Requirements
Fixtures
- Seed project: A pre-created project with known UUID for task creation tests
- Seed tasks: 3-5 tasks with varying status/priority for filter testing
- Seed labels: 2-3 labels for attachment testing
- Seed assignments: 1-2 assignments for listing/removal tests
Mocks
- Mock repositories: Each service test uses local mock structs implementing port interfaces (following
example_test.gopattern) - Mock services: Each handler test uses mock service implementations
- No-op logger: All tests use
logging.Nop()for structured logger
Test Isolation
- Each unit test creates its own mock instances — no shared mutable state
- Handler tests use
httptest.NewRecorder()and fresh chi routers per test case - Domain tests are pure functions with no external dependencies
Integration Test Plan
Cross-Layer Integration (via handler tests with mocks)
- Request → Handler → Service → Response: Verify the full handler pipeline processes requests, calls service methods, and returns correct HTTP status codes and response envelopes
- Validation → Error Mapping: Confirm that
app.BindAndValidate()errors produce 400 responses and domain errors map to correct HTTP status codes (404, 409) - URL Parameter Extraction: Verify
chi.URLParam()correctly extracts{id},{projectId},{taskId},{labelId},{assignmentId}from route patterns
Cross-Entity Integration (via service tests with mocks)
- Task → Project FK validation: TaskService.Create calls ProjectRepository.Get to validate project existence before creating task
- AssociationService → Task + Label validation: AttachLabel verifies both task and label exist before creating the association
- AssociationService → Task validation for assignments: AssignUser verifies task exists before creating assignment
Database Schema Integration (manual/migration verification)
- Migration ordering: Migrations execute in order (001 → 002 → 003) without errors on a fresh database
- FK constraint enforcement: Inserting a task with non-existent project_id fails at the DB level
- Cascade delete verification: Deleting a project removes all its tasks, their task_labels, and their assignments
- Unique constraint enforcement: Inserting duplicate project names or label names fails at the DB level
Route Registration Integration
- All endpoints routable: Every endpoint defined in the spec is registered in
routes.goand responds (not 404) - Auth middleware on write routes: POST/PUT/DELETE routes have auth middleware applied when AUTH_ENABLED=true
- Public read routes: GET routes are accessible without authentication
Performance Considerations
Load Expectations (v1)
- Small data volumes expected (no pagination per spec)
- Target: < 50ms response time for all CRUD operations under normal load
- Target: < 100ms for filtered task listings
Index Verification
- Confirm indexes exist on:
tasks.project_id,tasks.status,tasks.priority,task_labels.label_id,assignments.task_id,assignments.user_id - Verify unique indexes on
projects.nameandlabels.name(implied by UNIQUE constraint)
Benchmarks to Consider
- Task listing with status filter across 100+ tasks in a project (verify index usage)
- Cascade delete of a project with 50+ tasks, each with labels and assignments
Connection Pool
- Verify
pkg/databasepool settings (25 max open, 5 idle) are adequate for expected concurrent requests - No additional caching layer required for v1
Manual Verification Steps
- Migration on fresh database: Run
database.MustRunMigrations()on an empty PostgreSQL instance and verify all tables, constraints, and indexes are created correctly - OpenAPI spec rendering: Verify
/docsendpoint renders the Scalar UI with all new schemas and endpoint documentation - Response envelope format: Manually inspect a few responses to confirm
{data, meta}envelope structure - Cascade delete end-to-end: Create a project → tasks → labels → attach labels → assign users → delete project → verify all related data is gone
- Existing Example entity unaffected: Verify the Example CRUD endpoints still work correctly after all new routes are added
- Main.go wiring: Verify the application starts successfully with
DATABASE_URLconfigured, runs migrations, and serves all endpoints - SQL migration idempotency: Run migrations twice and verify no errors on second run (tracked by
schema_migrationstable)
Acceptance Criteria Coverage Matrix
| Acceptance Criterion | Test IDs |
|---|---|
| Project CRUD endpoints | HP-1, HP-2, HP-3, HP-4, HP-5 |
| Project name validation (1-100 chars) | EC-1, EC-2, ER-1, ER-2 |
| Project description validation (max 500) | EC-3, ER-3 |
| Duplicate project name → 409 | ER-4, ER-5 |
| Project cascade delete | EC-12, EC-13, EC-14 |
| Task CRUD endpoints | HP-6, HP-7, HP-8, HP-9, HP-10 |
| Task status enum | HP-27, ER-13 |
| Task priority enum | HP-28, ER-14 |
| Task title validation (1-200 chars) | EC-4, EC-5, ER-10, ER-11 |
| Task description validation (max 2000) | EC-6, ER-12 |
| Task default status/priority | HP-26 |
| Task project FK validation | ER-15 |
| Task filtering by status/priority | HP-11, HP-12, HP-13, EC-18, EC-19 |
| Label CRUD endpoints | HP-14, HP-15, HP-16, HP-17, HP-18, HP-19 |
| Label name validation (1-50 chars, unique) | EC-7, EC-8, ER-19, ER-20, ER-24, ER-25 |
| Label color validation (optional, hex) | EC-9, EC-10, EC-11, HP-15, ER-21, ER-22, ER-23 |
| Label cascade delete | EC-15 |
| Task-Label association CRUD | HP-20, HP-21, HP-22 |
| Task-Label duplicate → 409 | ER-29 |
| Task-Label non-existent → 404 | ER-27, ER-28 |
| Assignment CRUD | HP-23, HP-24, HP-25 |
| Assignment duplicate → 409 | ER-32 |
| Assignment non-existent task → 404 | ER-31 |
| UUID primary keys | ER-9 |
| Response envelope pattern | HP-1 through HP-28 (all verify envelope) |
| Error types (httperror) | ER-1 through ER-38 |
| Auth on write endpoints | ER-38 |
| Domain entity constructors/validation | HP-26, HP-27, HP-28 + all ER validation cases |
| Unit tests pass | All HP/EC/ER scenarios implemented as Go tests |