foundary-test-1770784989/.sdlc/features/data-models/qa-plan.md
rdev-worker c05dac1171
Some checks are pending
ci/woodpecker/push/woodpecker Pipeline is pending
build: /create-qa-plan data-models
2026-02-11 05:13:35 +00:00

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.go pattern)
  • 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)

  1. Request → Handler → Service → Response: Verify the full handler pipeline processes requests, calls service methods, and returns correct HTTP status codes and response envelopes
  2. Validation → Error Mapping: Confirm that app.BindAndValidate() errors produce 400 responses and domain errors map to correct HTTP status codes (404, 409)
  3. URL Parameter Extraction: Verify chi.URLParam() correctly extracts {id}, {projectId}, {taskId}, {labelId}, {assignmentId} from route patterns

Cross-Entity Integration (via service tests with mocks)

  1. Task → Project FK validation: TaskService.Create calls ProjectRepository.Get to validate project existence before creating task
  2. AssociationService → Task + Label validation: AttachLabel verifies both task and label exist before creating the association
  3. AssociationService → Task validation for assignments: AssignUser verifies task exists before creating assignment

Database Schema Integration (manual/migration verification)

  1. Migration ordering: Migrations execute in order (001 → 002 → 003) without errors on a fresh database
  2. FK constraint enforcement: Inserting a task with non-existent project_id fails at the DB level
  3. Cascade delete verification: Deleting a project removes all its tasks, their task_labels, and their assignments
  4. Unique constraint enforcement: Inserting duplicate project names or label names fails at the DB level

Route Registration Integration

  1. All endpoints routable: Every endpoint defined in the spec is registered in routes.go and responds (not 404)
  2. Auth middleware on write routes: POST/PUT/DELETE routes have auth middleware applied when AUTH_ENABLED=true
  3. 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.name and labels.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/database pool settings (25 max open, 5 idle) are adequate for expected concurrent requests
  • No additional caching layer required for v1

Manual Verification Steps

  1. Migration on fresh database: Run database.MustRunMigrations() on an empty PostgreSQL instance and verify all tables, constraints, and indexes are created correctly
  2. OpenAPI spec rendering: Verify /docs endpoint renders the Scalar UI with all new schemas and endpoint documentation
  3. Response envelope format: Manually inspect a few responses to confirm {data, meta} envelope structure
  4. Cascade delete end-to-end: Create a project → tasks → labels → attach labels → assign users → delete project → verify all related data is gone
  5. Existing Example entity unaffected: Verify the Example CRUD endpoints still work correctly after all new routes are added
  6. Main.go wiring: Verify the application starts successfully with DATABASE_URL configured, runs migrations, and serves all endpoints
  7. SQL migration idempotency: Run migrations twice and verify no errors on second run (tracked by schema_migrations table)

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