slack5-1770603014/.sdlc/features/user-preferences/qa-plan.md
rdev-worker 408db3e49b
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /create-qa-plan user-preferences
2026-02-09 02:29:12 +00:00

14 KiB

QA Plan: User Preferences API

Test Scenarios

Happy Path

ID Scenario Input Expected Output Derived From
HP-1 GET returns stored preferences for existing user GET /api/preferences-api/preferences/{valid_uuid} (user has saved prefs) 200 with full preferences object in {data, meta} envelope AC-1
HP-2 GET returns default preferences for unknown user GET /api/preferences-api/preferences/{valid_uuid} (no saved prefs) 200 with defaults: theme=system, language=en, notifications={email:true, push:true, digest:weekly} AC-2
HP-3 PUT creates preferences for new user (upsert) PUT /api/preferences-api/preferences/{valid_uuid} with {"preferences":{"theme":"dark","language":"fr","notifications":{"email":false,"push":true,"digest":"daily"}}} 200 with full preference set persisted AC-3
HP-4 PUT merges partial update - theme only PUT with {"preferences":{"theme":"light"}} on user with existing prefs 200 with theme changed, all other fields unchanged AC-4
HP-5 PUT merges partial update - language only PUT with {"preferences":{"language":"es"}} on user with existing prefs 200 with language changed, all other fields unchanged AC-4
HP-6 PUT merges partial update - single nested notification field PUT with {"preferences":{"notifications":{"push":false}}} on user with existing prefs 200 with push changed, email and digest unchanged (deep merge) AC-4
HP-7 PUT merges partial update - multiple nested notification fields PUT with {"preferences":{"notifications":{"email":false,"digest":"daily"}}} 200 with email and digest changed, push unchanged AC-4
HP-8 PUT with all valid theme values PUT with theme="light", "dark", "system" separately 200 for each valid value AC-5
HP-9 PUT with valid language values PUT with language="en", "fr", "es", "de", "zh" 200 for each valid value AC-6
HP-10 PUT with all valid digest values PUT with digest="daily", "weekly", "never" separately 200 for each valid value AC-7
HP-11 PUT with valid boolean notification fields PUT with email=true/false, push=true/false 200 for all boolean combinations AC-8
HP-12 All GET responses use {data, meta} envelope GET /api/preferences-api/preferences/{valid_uuid} Response body has data and meta top-level keys; meta includes request_id and timestamp AC-11
HP-13 All PUT responses use {data, meta} envelope PUT /api/preferences-api/preferences/{valid_uuid} with valid body Response body has data and meta top-level keys AC-11
HP-14 GET then PUT then GET roundtrip GET defaults, PUT partial update, GET returns merged result All three requests succeed; final GET reflects PUT changes AC-1, AC-3, AC-4
HP-15 PUT response contains full merged preferences PUT with partial preferences on existing user Response data contains complete preferences (merged), not just the submitted partial AC-4

Edge Cases

ID Scenario Input Expected Output Derived From
EC-1 PUT with empty preferences object PUT with {"preferences":{}} 200 with no changes to existing preferences (all fields omitted = no merge changes) AC-4
EC-2 PUT with empty notifications object PUT with {"preferences":{"notifications":{}}} 200 with no notification field changes AC-4
EC-3 PUT twice with different partial fields First PUT: {"preferences":{"theme":"dark"}}, second PUT: {"preferences":{"language":"fr"}} Both fields persisted after second PUT (theme=dark, language=fr) AC-4
EC-4 PUT overwrites previous value of same field PUT theme=dark, then PUT theme=light Final theme is light, other fields unchanged AC-4
EC-5 GET with lowercase UUID GET /preferences/{lowercase-uuid} 200 (UUID parsing is case-insensitive) AC-1, AC-10
EC-6 GET with uppercase UUID GET /preferences/{UPPERCASE-UUID} 200 (UUID parsing accepts uppercase) AC-1, AC-10
EC-7 PUT first user then GET second user PUT prefs for user A, then GET for user B (no prefs) User B gets defaults, not user A's prefs AC-2
EC-8 Concurrent PUT requests for same user Two simultaneous PUTs for same user_id Both succeed (INSERT ON CONFLICT is atomic); last write wins AC-3
EC-9 Default preferences have correct values GET for unknown user theme="system", language="en", notifications.email=true, notifications.push=true, notifications.digest="weekly" AC-2
EC-10 Response updated_at reflects latest change PUT, then GET updated_at in GET response >= updated_at from PUT response AC-1
EC-11 PUT on user with no existing prefs merges with defaults PUT {"preferences":{"theme":"dark"}} for new user Response shows theme=dark, language=en (default), notifications all defaults AC-3, AC-4

Error Cases

ID Scenario Input Expected Output Derived From
ER-1 PUT with invalid theme value {"preferences":{"theme":"midnight"}} 400 Bad Request with descriptive error about invalid theme AC-5, AC-9
ER-2 PUT with empty theme string {"preferences":{"theme":""}} 400 Bad Request (empty is not in allowed values) AC-5, AC-9
ER-3 PUT with numeric theme value {"preferences":{"theme":123}} 400 Bad Request (type mismatch) AC-5, AC-9
ER-4 PUT with empty language {"preferences":{"language":""}} 400 Bad Request with descriptive error about invalid language AC-6, AC-9
ER-5 PUT with invalid digest value {"preferences":{"notifications":{"digest":"monthly"}}} 400 Bad Request with descriptive error about invalid digest AC-7, AC-9
ER-6 PUT with non-boolean email value {"preferences":{"notifications":{"email":"yes"}}} 400 Bad Request (type mismatch on boolean field) AC-8, AC-9
ER-7 PUT with non-boolean push value {"preferences":{"notifications":{"push":1}}} 400 Bad Request (type mismatch on boolean field) AC-8, AC-9
ER-8 GET with non-UUID user_id GET /preferences/not-a-uuid 400 Bad Request with "invalid user_id format" AC-10
ER-9 PUT with non-UUID user_id PUT /preferences/not-a-uuid with valid body 400 Bad Request with "invalid user_id format" AC-10
ER-10 GET with empty user_id GET /preferences/ 404 (route not matched) or 400 AC-10
ER-11 PUT with empty request body PUT /preferences/{valid_uuid} with empty body 400 Bad Request ("request body is required") AC-9
ER-12 PUT with malformed JSON body PUT with {not json 400 Bad Request ("invalid request body") AC-9
ER-13 PUT with missing preferences key PUT with {"theme":"dark"} (no wrapper) 400 Bad Request (validation: preferences is required) AC-9
ER-14 PUT with null preferences value PUT with {"preferences":null} 400 Bad Request (validation: preferences is required) AC-9
ER-15 GET with user_id containing SQL injection GET /preferences/'; DROP TABLE user_preferences; -- 400 Bad Request (not a valid UUID), no SQL execution AC-10
ER-16 PUT with oversized request body PUT with very large JSON payload 400 or 413 (framework body size limit) AC-9
ER-17 PUT with invalid theme plus valid language {"preferences":{"theme":"bad","language":"en"}} 400 Bad Request for invalid theme (validation rejects entire request) AC-5, AC-9

Test Data Requirements

Fixtures

Fixture Description
validUUID A well-formed UUID: 550e8400-e29b-41d4-a716-446655440000
validUUID2 A second UUID for multi-user tests: 660e8400-e29b-41d4-a716-446655440001
defaultPreferences {theme: "system", language: "en", notifications: {email: true, push: true, digest: "weekly"}}
fullPreferences {theme: "dark", language: "fr", notifications: {email: false, push: true, digest: "daily"}}
partialThemeOnly {preferences: {theme: "light"}}
partialNotificationOnly {preferences: {notifications: {push: false}}}

Mocks

Mock Purpose Used By
mockPreferencesRepository In-memory implementation of PreferencesRepository port Service unit tests, handler unit tests
Mock must support: Get(ctx, userID) returning nil, nil for unknown users Enables testing default fallback behavior Service tests
Mock must support: Upsert(ctx, prefs) storing preferences by user_id Enables testing persistence roundtrip Service tests

Test Database (Integration)

  • PostgreSQL instance with user_preferences table created via migration 001_create_user_preferences.sql
  • Each integration test should use a clean table state (truncate between tests or use unique UUIDs)

Integration Test Plan

Cross-Layer Integration (Handler → Service → Repository)

ID Scenario Components Verification
IT-1 Full GET flow with mock repo Handler → Service → Mock Repo HTTP 200, correct envelope, default preferences for unknown user
IT-2 Full PUT flow with mock repo Handler → Service → Mock Repo HTTP 200, preferences persisted in mock, response contains merged result
IT-3 PUT then GET roundtrip with mock repo Handler → Service → Mock Repo PUT creates, GET returns what was PUT
IT-4 Deep merge across PUT calls Handler → Service → Mock Repo First PUT sets theme, second PUT sets language, GET returns both
IT-5 Validation error propagation Handler → Service → Domain validation Domain error surfaces as HTTP 400 with descriptive message
IT-6 UUID validation at handler layer Handler (chi URL param extraction) Invalid UUID returns 400 before reaching service layer

Database Integration (requires PostgreSQL)

ID Scenario Components Verification
DB-1 Migration creates table Migration runner → PostgreSQL Table user_preferences exists with correct columns
DB-2 Adapter Get for non-existent user PostgreSQL adapter → DB Returns nil, nil (not error)
DB-3 Adapter Upsert creates new row PostgreSQL adapter → DB Row inserted; SELECT confirms data
DB-4 Adapter Upsert updates existing row PostgreSQL adapter → DB Row updated; updated_at changed
DB-5 JSONB serialization roundtrip Adapter → DB → Adapter Write preferences, read back, all fields match
DB-6 Concurrent upsert doesn't error Two goroutines upsert same user Both succeed (ON CONFLICT handles it)

OpenAPI Spec Verification

ID Scenario Verification
OA-1 OpenAPI spec exports --export-openapi produces valid JSON
OA-2 GET endpoint documented Spec contains GET /api/preferences-api/preferences/{user_id} with response schema
OA-3 PUT endpoint documented Spec contains PUT with request body schema and response schema
OA-4 Schemas match domain types UserPreferences, UpdatePreferencesRequest, PreferencesResponse schemas defined

Performance Considerations

Aspect Expectation Test Method
GET latency < 10ms for single PK lookup (excluding network) Benchmark test: BenchmarkGetPreferences with seeded data
PUT latency < 20ms for upsert (excluding network) Benchmark test: BenchmarkUpdatePreferences
No N+1 queries GET and PUT each execute exactly 1 SQL statement Count queries in integration test (or review adapter code)
JSONB size Preferences JSON < 500 bytes for standard fields Assert serialized size in unit test
Concurrent writes No deadlocks or errors under concurrent PUT Run 10 goroutines doing PUT for same user, assert no errors

Manual Verification Steps

Step Action Expected Result
1 Start service with ./scripts/dev.sh or equivalent Service starts, connects to PostgreSQL, runs migration
2 curl GET /api/preferences-api/preferences/{new-uuid} Returns 200 with default preferences
3 curl PUT /api/preferences-api/preferences/{uuid} with {"preferences":{"theme":"dark"}} Returns 200 with theme=dark, other fields defaulted
4 curl GET /api/preferences-api/preferences/{same-uuid} Returns 200 with theme=dark persisted
5 curl PUT /api/preferences-api/preferences/{same-uuid} with {"preferences":{"language":"fr"}} Returns 200 with theme=dark (retained), language=fr (updated)
6 curl GET /api/preferences-api/preferences/not-a-uuid Returns 400 Bad Request
7 curl PUT /api/preferences-api/preferences/{uuid} with {"preferences":{"theme":"invalid"}} Returns 400 with descriptive error message
8 curl GET /health Returns 200 (health endpoint still works)
9 Visit OpenAPI docs page (Scalar UI) Both endpoints documented with schemas
10 Restart service, repeat GET for same UUID Returns persisted preferences (survived restart = AC-13)

Acceptance Criteria Coverage Matrix

AC Description Test IDs
AC-1 GET returns stored preferences HP-1, HP-14, EC-10
AC-2 GET unknown user returns defaults HP-2, EC-7, EC-9, EC-11
AC-3 PUT creates preferences (upsert) HP-3, EC-11, IT-2, IT-3
AC-4 PUT merges partial update (deep merge) HP-4, HP-5, HP-6, HP-7, HP-15, EC-1, EC-2, EC-3, EC-4, IT-4
AC-5 PUT validates theme HP-8, ER-1, ER-2, ER-3, ER-17
AC-6 PUT validates language non-empty HP-9, ER-4
AC-7 PUT validates digest HP-10, ER-5
AC-8 PUT validates notification booleans HP-11, ER-6, ER-7
AC-9 Invalid values return 400 with details ER-1 through ER-17
AC-10 Invalid user_id returns 400 ER-8, ER-9, ER-10, ER-15, IT-6
AC-11 Standard {data, meta} envelope HP-12, HP-13
AC-12 OpenAPI spec documents endpoints OA-1, OA-2, OA-3, OA-4
AC-13 Persisted in PostgreSQL DB-1 through DB-5, Manual step 10
AC-14 Migration creates table DB-1
AC-15 Hexagonal architecture IT-1 through IT-5 (layer isolation verified by mock injection)
AC-16 Service and handler unit tests All HP, EC, ER tests implemented as unit tests
AC-17 Example scaffolding removed Manual code review; no example.go files remain