foundary-test-1770773605/.sdlc/features/data-models/qa-plan.md
rdev-worker 2a3925337d
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /create-qa-plan data-models
2026-02-11 02:15:50 +00:00

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 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

  1. TaskService validates project exists: Service calls ProjectRepository.Get before creating task — mock returns ErrProjectNotFound → verify service propagates correctly
  2. LabelService validates project exists: Same pattern as task
  3. AssignmentService validates task exists: Service calls TaskRepository.Get before creating assignment

Route Registration

  1. All endpoints reachable: Verify no 404s on correctly-formed paths for all 17 endpoints
  2. Brace syntax routing: Confirm {id}, {projectId}, {taskId} parameters extract correctly via chi.URLParam()

Wiring (main.go)

  1. Dependency injection: Verify main.go creates all adapters, services, and handlers and wires them correctly
  2. Database connection + migrations: Verify database.MustConnectdatabase.MustRunMigrations → adapter creation order
  3. 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

  1. Run migrations twice — verify second run is a no-op (no errors, no duplicate tables)
  2. Verify migrations are tracked in schema_migrations table

OpenAPI Documentation

  1. Access Scalar docs endpoint and verify all 17 endpoints are documented
  2. Verify request/response schemas match the actual DTOs
  3. Verify tags group endpoints correctly: Projects, Tasks, Labels, Assignments

Response Envelope

  1. Verify all GET responses return {data, meta} envelope
  2. Verify POST responses return {data} with 201 status
  3. Verify DELETE responses return 204 with no body

Error Response Format

  1. Verify 400/409/404 errors follow the standard error response format from httperror
  2. Verify 422 validation errors include field-level detail from app.BindAndValidate()