slack5-1770606136/.sdlc/features/user-preferences/qa-plan.md
rdev-worker 4ed372c740
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /create-qa-plan user-preferences
2026-02-09 03:20:13 +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 returns stored preferences for a user GET /api/preferences-api/preferences/{valid_uuid} where user has preferences {theme: "dark", language: "en", notifications_enabled: true} 200 with {data: {user_id, preferences: {theme: "dark", language: "en", notifications_enabled: true}, updated_at}, meta} AC-1
HP-2 GET returns empty preferences for user with no stored preferences GET /api/preferences-api/preferences/{valid_uuid} where user has no row 200 with {data: {user_id, preferences: {}, updated_at}, meta} AC-2
HP-3 PUT creates preferences for new user (upsert - insert path) PUT /api/preferences-api/preferences/{valid_uuid} with body {preferences: {theme: "light"}} where no row exists 200 with {data: {user_id, preferences: {theme: "light"}, updated_at}, meta} AC-4, AC-5, AC-6
HP-4 PUT updates preferences for existing user (upsert - update path) PUT /api/preferences-api/preferences/{valid_uuid} with body {preferences: {theme: "dark"}} where user already has {theme: "light", language: "en"} 200 with merged preferences {theme: "dark", language: "en"} AC-4, AC-6
HP-5 PUT with all known preference keys valid PUT with {preferences: {theme: "system", language: "ja", notifications_enabled: false}} 200 with all preferences stored correctly AC-7
HP-6 PUT with unknown preference keys accepted PUT with {preferences: {custom_key: "custom_value", another: 42}} 200 with unknown keys stored as-is AC-9
HP-7 PUT with mix of known and unknown keys PUT with {preferences: {theme: "dark", custom_flag: true}} 200 with both keys stored AC-7, AC-9
HP-8 Preferences survive service restart PUT preferences, restart service, GET same user GET returns previously stored preferences AC-10
HP-9 All responses follow envelope pattern Any successful GET or PUT Response has {data, meta} structure with meta containing request_id and timestamp AC-11
HP-10 PUT validates theme "light" accepted PUT with {preferences: {theme: "light"}} 200 success AC-7
HP-11 PUT validates theme "dark" accepted PUT with {preferences: {theme: "dark"}} 200 success AC-7
HP-12 PUT validates theme "system" accepted PUT with {preferences: {theme: "system"}} 200 success AC-7
HP-13 PUT validates various BCP-47 language tags PUT with {preferences: {language: "en"}}, then "fr", "es", "de", "ja" 200 for each AC-7
HP-14 PUT validates notifications_enabled true PUT with {preferences: {notifications_enabled: true}} 200 success AC-7
HP-15 PUT validates notifications_enabled false PUT with {preferences: {notifications_enabled: false}} 200 success AC-7
HP-16 PUT is idempotent Same PUT request sent twice Both return 200 with identical preferences; second call does not create duplicate AC-4

Edge Cases

ID Scenario Input Expected Output Derived From
EC-1 GET with UUID that has never had preferences GET /api/preferences-api/preferences/{new_uuid} 200 with empty preferences {} (not 404) AC-2
EC-2 PUT with empty preferences object PUT with {preferences: {}} 200 success, no preferences changed (or empty stored) AC-5
EC-3 PUT preserves existing keys not in request User has {theme: "dark", language: "en"}, PUT with {preferences: {theme: "light"}} Stored preferences: {theme: "light", language: "en"} - language preserved AC-4 (merge behavior from design)
EC-4 PUT with unknown key containing complex JSON value PUT with {preferences: {custom: {nested: {deeply: true}}}} 200 with nested value stored as-is AC-9
EC-5 PUT with unknown key containing array value PUT with {preferences: {tags: ["a", "b", "c"]}} 200 with array stored AC-9
EC-6 PUT with unknown key containing null value PUT with {preferences: {optional_field: null}} 200 with null stored AC-9
EC-7 GET with lowercase UUID GET with {550e8400-e29b-41d4-a716-446655440000} 200 success AC-1
EC-8 GET with uppercase UUID GET with {550E8400-E29B-41D4-A716-446655440000} 200 success (UUIDs are case-insensitive) AC-1
EC-9 PUT with large number of preference keys PUT with 100 different key-value pairs 200 success, all keys stored AC-9
EC-10 PUT with unicode string values for unknown keys PUT with {preferences: {greeting: "こんにちは"}} 200 with unicode preserved AC-9
EC-11 PUT with numeric values for unknown keys PUT with {preferences: {max_items: 50, ratio: 3.14}} 200 with numeric types preserved AC-9
EC-12 Sequential PUTs accumulate preferences PUT {theme: "dark"}, then PUT {language: "fr"} GET returns {theme: "dark", language: "fr"} AC-4, merge behavior
EC-13 PUT with boolean-like string for notifications_enabled PUT with {preferences: {notifications_enabled: "true"}} (string, not bool) 400 validation error - must be boolean 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 AC-3
ER-2 GET with empty string as user_id GET /api/preferences-api/preferences/ 404 (route not matched) or 400 AC-3
ER-3 PUT with invalid UUID format PUT /api/preferences-api/preferences/abc123 400 Bad Request AC-3
ER-4 PUT with invalid theme value PUT with {preferences: {theme: "midnight"}} 400 with details: {theme: "must be one of: light, dark, system"} AC-7, AC-8
ER-5 PUT with invalid language tag PUT with {preferences: {language: "not-a-language-!!"}} 400 with details: {language: "must be a valid BCP-47 language tag"} AC-7, AC-8
ER-6 PUT with invalid notifications_enabled type PUT with {preferences: {notifications_enabled: "yes"}} 400 with details: {notifications_enabled: "must be a boolean"} AC-7, AC-8
ER-7 PUT with multiple validation errors PUT with {preferences: {theme: "neon", language: "???", notifications_enabled: 42}} 400 with details containing all three field errors AC-8
ER-8 PUT with missing request body PUT with empty body 400 Bad Request AC-5
ER-9 PUT with missing preferences field PUT with {} (empty JSON object) 400 Bad Request AC-5
ER-10 PUT with malformed JSON body PUT with {not json 400 Bad Request AC-5
ER-11 PUT with preferences as non-object type PUT with {preferences: "string"} 400 Bad Request AC-5
ER-12 PUT with preferences as array PUT with {preferences: [1, 2, 3]} 400 Bad Request AC-5
ER-13 PUT with integer theme value PUT with {preferences: {theme: 123}} 400 with details for theme AC-7, AC-8
ER-14 PUT with null theme value PUT with {preferences: {theme: null}} 400 with details for theme AC-7, AC-8
ER-15 Database unavailable on GET Database connection dropped 500 Internal Server Error Design: Error Handling
ER-16 Database unavailable on PUT Database connection dropped 500 Internal Server Error Design: Error Handling
ER-17 GET non-existent route GET /api/preferences-api/nonexistent 404 Not Found Standard routing
ER-18 POST to preferences endpoint (wrong method) POST /api/preferences-api/preferences/{uuid} 405 Method Not Allowed Standard routing
ER-19 DELETE to preferences endpoint (wrong method) DELETE /api/preferences-api/preferences/{uuid} 405 Method Not Allowed Standard routing

Test Data Requirements

Fixtures

  • Valid UUIDs: Generate at least 3 unique UUIDs for test isolation (e.g., 550e8400-e29b-41d4-a716-446655440000, 660e8400-e29b-41d4-a716-446655440001, 770e8400-e29b-41d4-a716-446655440002)
  • Pre-seeded preferences row: One user with {theme: "dark", language: "en", notifications_enabled: true} for GET and merge tests
  • Empty database state: Tests must handle both seeded and unseeded user_ids

Mocks

  • Mock PreferenceRepository: For handler and service unit tests, providing controllable Get and Upsert behavior
  • Mock returning nil: Simulates user with no preferences
  • Mock returning error: Simulates database failures (connection errors, query errors)
  • Mock tracking calls: Verifies correct arguments passed to repository methods

Test Data Values

Key Valid Values Invalid Values
theme "light", "dark", "system" "midnight", 123, null, "", true
language "en", "fr", "es", "de", "ja", "zh-Hans" "not-a-language-!!", 123, null, ""
notifications_enabled true, false "true", "false", 1, 0, "yes", null

Integration Test Plan

Component Boundary Tests

ID Test Components What to Verify
IT-1 Handler → Service → Repository (GET, no preferences) Handler, Service, Mock Repo Full request/response cycle; empty preferences returned for unknown user
IT-2 Handler → Service → Repository (GET, with preferences) Handler, Service, Mock Repo Full request/response cycle; stored preferences returned
IT-3 Handler → Service → Repository (PUT, create) Handler, Service, Mock Repo Request binding, validation, upsert delegation, response mapping
IT-4 Handler → Service → Repository (PUT, update with merge) Handler, Service, Mock Repo Merge logic: existing keys preserved, new keys added, changed keys updated
IT-5 Handler → Service (PUT, validation failure) Handler, Service Validation errors mapped to structured HTTP error response with per-field details
IT-6 Service → PostgreSQL Adapter (GET) Service, Postgres Adapter, PostgreSQL Real database round-trip for GET (requires running PostgreSQL)
IT-7 Service → PostgreSQL Adapter (Upsert) Service, Postgres Adapter, PostgreSQL Real database round-trip for INSERT and UPDATE paths
IT-8 Full stack: HTTP → Handler → Service → Postgres (GET) All layers End-to-end GET via HTTP with real database
IT-9 Full stack: HTTP → Handler → Service → Postgres (PUT then GET) All layers End-to-end PUT followed by GET, verifying persistence
IT-10 Migration creates correct schema Migration, PostgreSQL Table structure, column types, index existence

Database Integration Tests (require PostgreSQL)

ID Test Verification
DB-1 Adapter Get returns nil for non-existent user Get returns nil, nil (not error)
DB-2 Adapter Upsert inserts new row Row exists after Upsert, Get returns it
DB-3 Adapter Upsert updates existing row updated_at changes, preferences updated
DB-4 Adapter Upsert is atomic (ON CONFLICT) Concurrent Upserts don't produce duplicate rows
DB-5 JSONB marshaling round-trip Complex nested JSON preserved through write→read cycle
DB-6 Migration is idempotent Running migration twice doesn't error

Performance Considerations

Latency Budgets

Operation Target P50 Target P99 Rationale
GET preferences < 5ms < 20ms Single primary key lookup, no joins
PUT preferences (new user) < 10ms < 50ms Single INSERT with conflict check
PUT preferences (existing user) < 10ms < 50ms Single UPDATE on primary key

Load Expectations

  • Read:Write ratio: ~10:1 (preferences read on every page load, written on settings changes)
  • Expected QPS: Low to moderate; preferences are per-user, not global
  • Connection pool: Default 25 open / 5 idle connections should be sufficient

Benchmarks to Run

Benchmark What to Measure
BenchmarkServiceValidation Time to validate known preference keys (no I/O)
BenchmarkJSONBMarshal Time to marshal/unmarshal preference maps to JSONB
BenchmarkGetEndpoint End-to-end GET latency with real database
BenchmarkPutEndpoint End-to-end PUT latency with real database

Scalability Notes

  • Primary key index ensures O(1) lookups regardless of table size
  • No table scans in either GET or PUT queries
  • JSONB stored in decomposed binary format; efficient for full-object reads/writes
  • updated_at index not used by current queries but supports future cleanup/analytics

Manual Verification Steps

OpenAPI Documentation

  1. Start the service locally (./scripts/dev.sh or direct go run)
  2. Navigate to the Scalar docs UI (typically at /api/preferences-api/docs)
  3. Verify both GET and PUT endpoints are documented
  4. Verify request/response schemas match the spec
  5. Verify parameter descriptions for user_id are present
  6. Try executing sample requests from the docs UI

Database Migration

  1. Start with a clean database (no preferences table)
  2. Start the service — migration should run automatically
  3. Verify table exists: \d preferences in psql
  4. Verify columns: user_id UUID PK, preferences JSONB, created_at TIMESTAMPTZ, updated_at TIMESTAMPTZ
  5. Verify index: \di idx_preferences_updated_at
  6. Restart the service — migration should be idempotent (no errors)

End-to-End Smoke Test

  1. Start the service with a clean database
  2. GET /api/preferences-api/preferences/{uuid} — expect 200 with empty preferences
  3. PUT /api/preferences-api/preferences/{uuid} with {preferences: {theme: "dark"}} — expect 200
  4. GET /api/preferences-api/preferences/{uuid} — expect 200 with {theme: "dark"}
  5. PUT /api/preferences-api/preferences/{uuid} with {preferences: {language: "fr"}} — expect 200 with merged {theme: "dark", language: "fr"}
  6. PUT /api/preferences-api/preferences/{uuid} with {preferences: {theme: "invalid"}} — expect 400 with validation details
  7. GET /api/preferences-api/preferences/not-a-uuid — expect 400

Health Check

  1. GET /api/preferences-api/health — expect 200 (verify health endpoint still works after route changes)

Acceptance Criteria Coverage Matrix

AC Description Test IDs
AC-1 GET returns all preferences as key-value pairs HP-1, HP-7, HP-8
AC-2 GET returns 200 with empty preferences for no stored prefs HP-2, EC-1
AC-3 GET returns 404 (400) for invalid UUID format ER-1, ER-2, ER-3
AC-4 PUT creates or updates (upsert) HP-3, HP-4, HP-16, EC-3, EC-12
AC-5 PUT accepts JSON body with preferences object HP-3, HP-5, EC-2, ER-8, ER-9, ER-10, ER-11, ER-12
AC-6 PUT returns 200 with updated preferences HP-3, HP-4, HP-5
AC-7 PUT validates known keys (theme, language, notifications_enabled) HP-5, HP-10HP-15, EC-13, ER-4, ER-5, ER-6, ER-13, ER-14
AC-8 PUT returns 400 with details on validation failure ER-4, ER-5, ER-6, ER-7
AC-9 Unknown keys accepted and stored HP-6, HP-7, EC-4, EC-5, EC-6, EC-9, EC-10, EC-11
AC-10 Preferences persisted in PostgreSQL HP-8, IT-6IT-9, DB-2, DB-3
AC-11 All responses follow {data, meta} envelope HP-9 (verified across all HP tests)
AC-12 OpenAPI spec documents both endpoints Manual: OpenAPI verification
AC-13 Handler tests cover success and error cases HP-1HP-16, ER-1ER-19
AC-14 Service-layer tests cover business logic IT-1IT-5, HP tests via service unit tests
AC-15 Database migration creates preferences table IT-10, DB-6, Manual: Migration