slate-test-1770505673/.sdlc/features/user-preferences/qa-plan.md
rdev-worker 9e15946afd
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
build: /create-qa-plan user-preferences
2026-02-07 23:37:49 +00:00

16 KiB
Raw Blame History

QA Plan: User Preferences API

Test Scenarios

Happy Path

ID Scenario Input Expected Output Derived From
HP-1 GET preferences for user with preferences set GET /api/preferences-api/preferences/{user_id} with valid JWT matching user_id; user has theme=dark, language=en, notifications_enabled=true stored 200 OK, {data: {theme: "dark", language: "en", notifications_enabled: "true"}, meta: {request_id, timestamp}} AC-1
HP-2 GET preferences for user with no preferences GET /api/preferences-api/preferences/{user_id} with valid JWT; user has no stored preferences 200 OK, {data: {}, meta: {...}} (empty object, not 404) AC-8
HP-3 PUT create preferences for user (first time) PUT /api/preferences-api/preferences/{user_id} with body {"theme": "dark", "language": "fr"} 200 OK, {data: {"theme": "dark", "language": "fr"}, meta: {...}} AC-2
HP-4 PUT update existing preferences (full set) PUT with body {"theme": "light", "language": "es", "notifications_enabled": "false"} when user already has preferences 200 OK, data reflects all three updated values AC-2, AC-6
HP-5 PUT partial update preserves other keys User has {theme: "dark", language: "en", notifications_enabled: "true"}; PUT {"theme": "light"} 200 OK, {data: {"theme": "light", "language": "en", "notifications_enabled": "true"}} — language and notifications_enabled unchanged AC-7
HP-6 PUT idempotency — same request twice yields same result Send identical PUT {"theme": "dark"} twice Both return 200 OK with identical data; database state identical after both AC-6
HP-7 PUT single preference key — theme PUT {"theme": "system"} 200 OK, theme updated to "system" AC-3
HP-8 PUT single preference key — language PUT {"language": "es"} 200 OK, language updated to "es" AC-3
HP-9 PUT single preference key — notifications_enabled PUT {"notifications_enabled": "false"} 200 OK, notifications_enabled updated to "false" AC-3
HP-10 All valid theme values accepted PUT with theme=light, theme=dark, theme=system (three separate requests) All return 200 OK AC-3
HP-11 Language ISO 639-1 codes accepted PUT with language=en, language=fr, language=de, language=ja All return 200 OK AC-3
HP-12 GET after PUT returns consistent data PUT {"theme": "dark"}, then GET GET response includes theme: "dark" AC-1, AC-2

Edge Cases

ID Scenario Input Expected Output Derived From
EC-1 PUT with empty JSON body {} PUT with body {} 400 Bad Request — "request body is required" or equivalent (no keys to update) AC-2
EC-2 PUT with all three keys at once PUT {"theme": "dark", "language": "en", "notifications_enabled": "true"} 200 OK, all three stored AC-2, AC-3
EC-3 Rapid sequential PUTs (concurrency safety) Two concurrent PUTs: {"theme": "dark"} and {"theme": "light"} Both return 200; final state is deterministic (last write wins); no data corruption AC-6
EC-4 GET with user_id as valid UUID but no data GET /api/preferences-api/preferences/00000000-0000-0000-0000-000000000000 (valid UUID, no user data) 200 OK with {data: {}} — never 404 AC-8
EC-5 Language boundary: two-character lowercase codes PUT {"language": "zz"} (valid format, unusual code) 200 OK — format-valid per regex ^[a-z]{2}$ AC-3
EC-6 PUT same value as currently stored User has theme=dark; PUT {"theme": "dark"} 200 OK, no change, idempotent AC-6
EC-7 Multiple PUTs building up preference set incrementally PUT {"theme": "dark"}, then PUT {"language": "en"}, then PUT {"notifications_enabled": "true"} After third PUT, GET returns all three keys AC-7

Error Cases

ID Scenario Input Expected Output Derived From
ER-1 GET with invalid UUID format GET /api/preferences-api/preferences/not-a-uuid 400 Bad Request, "invalid user ID format" AC-1 (implicit UUID validation)
ER-2 PUT with invalid UUID format PUT /api/preferences-api/preferences/12345 with valid body 400 Bad Request, "invalid user ID format" AC-2 (implicit UUID validation)
ER-3 PUT with unknown preference key PUT {"color": "blue"} 400 Bad Request, descriptive error mentioning unknown key "color" AC-4
ER-4 PUT with multiple keys, one unknown PUT {"theme": "dark", "font_size": "14"} 400 Bad Request, error identifies "font_size" as unknown AC-4
ER-5 PUT with invalid theme value PUT {"theme": "blue"} 400 Bad Request, "invalid value 'blue' for key 'theme': allowed values are [light, dark, system]" AC-5
ER-6 PUT with invalid notifications_enabled value PUT {"notifications_enabled": "yes"} 400 Bad Request, descriptive error about invalid value AC-5
ER-7 PUT with invalid language value — too long PUT {"language": "english"} 400 Bad Request, invalid language format AC-5
ER-8 PUT with invalid language value — uppercase PUT {"language": "EN"} 400 Bad Request, invalid language format (regex: ^[a-z]{2}$) AC-5
ER-9 PUT with invalid language value — digits PUT {"language": "12"} 400 Bad Request, invalid language format AC-5
ER-10 PUT with invalid language value — single char PUT {"language": "e"} 400 Bad Request, invalid language format AC-5
ER-11 GET without JWT (unauthenticated) GET /api/preferences-api/preferences/{user_id} with no Authorization header 401 Unauthorized AC-9
ER-12 PUT without JWT (unauthenticated) PUT /api/preferences-api/preferences/{user_id} with no Authorization header 401 Unauthorized AC-9
ER-13 GET with expired/invalid JWT GET with Authorization: Bearer invalid-token 401 Unauthorized AC-9
ER-14 GET for another user's preferences (ownership violation) JWT subject = user-A, path user_id = user-B 403 Forbidden, "cannot access preferences for another user" AC-10
ER-15 PUT for another user's preferences (ownership violation) JWT subject = user-A, path user_id = user-B, valid body 403 Forbidden, "cannot access preferences for another user" AC-10
ER-16 PUT with mix of valid and invalid values PUT {"theme": "dark", "language": "INVALID"} 400 Bad Request — validation fails before any persistence AC-4, AC-5
ER-17 PUT with non-JSON body PUT with Content-Type: application/json but body is not json 400 Bad Request AC-2

Domain Validation Unit Tests

ID Scenario Input Expected Output Derived From
DV-1 ValidateKey accepts all known keys ValidateKey("theme"), ValidateKey("language"), ValidateKey("notifications_enabled") nil (no error) for all three AC-3
DV-2 ValidateKey rejects unknown keys ValidateKey("color"), ValidateKey(""), ValidateKey("Theme") ErrUnknownKey for all AC-4
DV-3 ValidateValue for theme — valid values ValidateValue("theme", "light"), "dark", "system" nil for all three AC-3
DV-4 ValidateValue for theme — invalid values ValidateValue("theme", "blue"), "DARK", "" ErrInvalidValue for all AC-5
DV-5 ValidateValue for language — valid ISO codes ValidateValue("language", "en"), "fr", "de", "ja" nil for all AC-3
DV-6 ValidateValue for language — invalid formats ValidateValue("language", "english"), "EN", "e", "123", "" ErrInvalidValue for all AC-5
DV-7 ValidateValue for notifications_enabled — valid ValidateValue("notifications_enabled", "true"), "false" nil for both AC-3
DV-8 ValidateValue for notifications_enabled — invalid ValidateValue("notifications_enabled", "yes"), "1", "on", "" ErrInvalidValue for all AC-5

Service Layer Tests

ID Scenario Input Expected Output Derived From
SV-1 Get delegates to repository and returns result Mock repo returns {"theme": "dark"} Service returns {"theme": "dark"}, nil AC-1
SV-2 Get returns empty map for user with no prefs Mock repo returns {} Service returns {}, nil AC-8
SV-3 Get propagates repository errors Mock repo returns error Service returns nil, error AC-11 (DB dependency)
SV-4 Upsert validates all keys before persisting Input: {"theme": "dark", "unknown": "val"} Returns error wrapping ErrUnknownKey; repo.Upsert NOT called AC-4
SV-5 Upsert validates all values before persisting Input: {"theme": "blue"} Returns error wrapping ErrInvalidValue; repo.Upsert NOT called AC-5
SV-6 Upsert calls repo and returns full pref set Input: {"theme": "dark"}; mock repo returns full set after upsert Returns full set including unchanged keys AC-2, AC-7
SV-7 Upsert propagates repository errors Mock repo Upsert returns error Service returns error AC-11 (DB dependency)

Test Data Requirements

Test Users

ID Purpose
550e8400-e29b-41d4-a716-446655440000 Primary test user — owns preferences
660e8400-e29b-41d4-a716-446655440001 Secondary test user — for ownership violation tests

Test Preference Sets

Set Data Used By
Full set {"theme": "dark", "language": "en", "notifications_enabled": "true"} HP-1, HP-4, HP-5
Partial set {"theme": "dark"} HP-3, HP-5, HP-6
Empty set {} HP-2, EC-4

Mock Repository

  • Implements port.PreferenceRepository interface
  • Thread-safe with sync.RWMutex
  • In-memory map[string]map[string]string (userID -> key -> value)
  • Used by service tests and handler tests
  • Must return empty map (not nil) for unknown users

Auth Context Setup

  • Use auth.SetUser(ctx, &auth.User{ID: userID}) to inject authenticated user into request context
  • For unauthenticated tests: omit SetUser call
  • For ownership tests: set user with different ID than path param

Integration Test Plan

Component Boundary Tests

ID Boundary Test Description
IT-1 Handler → Service → Mock Repo Full request/response cycle through handler with mock repository, verifying JSON envelope format, status codes, and error messages
IT-2 Auth Middleware → Handler Verify auth middleware rejects unauthenticated requests before reaching handler (401 response)
IT-3 Handler → Auth Context → Ownership Verify handler extracts JWT subject and compares with path user_id (403 on mismatch)
IT-4 Handler → app.Wrap error mapping Verify that domain errors (ErrUnknownKey, ErrInvalidValue) are correctly mapped to HTTP status codes via app.Wrap
IT-5 Route registration → Handler dispatch Verify GET and PUT routes are correctly registered and dispatch to the right handler methods

Database Integration Tests (if PostgreSQL available)

ID Test Description
DB-1 Migration creates user_preferences table with correct schema (composite PK, index)
DB-2 Adapter GetByUserID returns empty map for nonexistent user
DB-3 Adapter Upsert inserts new preferences and GetByUserID retrieves them
DB-4 Adapter Upsert updates existing preferences (ON CONFLICT behavior)
DB-5 Adapter Upsert is transactional — partial failure rolls back all changes
DB-6 Concurrent Upsert calls don't cause deadlocks or data corruption

End-to-End Smoke Tests (manual or scripted)

ID Test Description
E2E-1 Start service, run migration, PUT preferences, GET preferences — full round trip
E2E-2 Verify health endpoint still works after preference code replaces example code
E2E-3 Verify OpenAPI docs endpoint renders and documents both preference endpoints

Performance Considerations

Latency Budget

  • Target: p99 < 50ms for GET /preferences/{user_id} (per AC-15)
  • Expected: < 5ms — single indexed SELECT on composite PK
  • Measurement: Use httptest with timing assertions or benchmark tests

Benchmark Tests

ID Benchmark Target
BM-1 BenchmarkGetPreferences — service.Get with mock repo Baseline for handler overhead
BM-2 BenchmarkUpsertPreferences — service.Upsert with validation + mock repo Validate validation overhead is minimal

Load Considerations

  • Table growth: 3 rows per user (one per preference key), indexed by user_id
  • No caching needed — direct PK lookups are efficient
  • Connection pool: pkg/database.Pool defaults (25 max open, 5 max idle) are sufficient

Manual Verification Steps

Pre-Implementation Checks

  1. Verify all 8 example scaffold files are deleted (T1 acceptance)
  2. Verify health.go and config.go are untouched after scaffold removal
  3. Verify service compiles after each task (go build ./...)

Post-Implementation Smoke Tests

  1. Start service: cd services/preferences-api && go run ./cmd/server/ — verify it starts without errors
  2. Health check: curl http://localhost:8001/api/preferences-api/health — verify 200 OK
  3. OpenAPI docs: curl http://localhost:8001/api/preferences-api/docs — verify docs render with preference endpoints
  4. PUT preferences (authenticated):
    curl -X PUT http://localhost:8001/api/preferences-api/preferences/<user-id> \
      -H "Authorization: Bearer <valid-jwt>" \
      -H "Content-Type: application/json" \
      -d '{"theme": "dark", "language": "en", "notifications_enabled": "true"}'
    
    Verify: 200 OK, response contains all three preferences in {data, meta} envelope
  5. GET preferences (authenticated):
    curl http://localhost:8001/api/preferences-api/preferences/<user-id> \
      -H "Authorization: Bearer <valid-jwt>"
    
    Verify: 200 OK, returns same preferences set in step 4
  6. Ownership violation: Use JWT for user-A, request user-B's preferences — verify 403
  7. Validation error: PUT {"theme": "blue"} — verify 400 with descriptive message
  8. Unknown key: PUT {"font_size": "14"} — verify 400 with descriptive message

Test Suite Verification

  • cd services/preferences-api && go test -v ./... — all tests pass
  • cd services/preferences-api && go test -race ./... — no race conditions
  • cd services/preferences-api && go vet ./... — no static analysis issues

Acceptance Criteria Coverage Matrix

AC# Description Test IDs
AC-1 GET returns preferences in {data, meta} envelope HP-1, HP-2, HP-12, IT-1
AC-2 PUT creates/updates with upsert semantics HP-3, HP-4, EC-2, SV-6, IT-1
AC-3 Supported keys: theme, language, notifications_enabled HP-7, HP-8, HP-9, HP-10, HP-11, DV-1, DV-3, DV-5, DV-7
AC-4 Unknown keys rejected with 400 ER-3, ER-4, DV-2, SV-4
AC-5 Invalid values rejected with 400 ER-5, ER-6, ER-7, ER-8, ER-9, ER-10, ER-16, DV-4, DV-6, DV-8, SV-5
AC-6 PUT is idempotent HP-6, EC-6
AC-7 PUT supports partial updates HP-5, EC-7, SV-6
AC-8 GET with no preferences returns 200 with {} HP-2, EC-4, SV-2
AC-9 Both endpoints require JWT auth ER-11, ER-12, ER-13, IT-2
AC-10 Ownership check — user_id must match JWT subject ER-14, ER-15, IT-3
AC-11 Preferences persisted in PostgreSQL with migration DB-1, DB-2, DB-3, DB-4, DB-5
AC-12 OpenAPI spec documents both endpoints E2E-3
AC-13 Handler tests cover success, validation, not-found, auth HP-112, ER-117
AC-14 Service-layer tests with mock repository SV-17
AC-15 Response times < 50ms at p99 for reads BM-1, BM-2