slack5-1770529463/.sdlc/features/user-preferences/qa-plan.md
rdev-worker 3951ff5ed7
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /create-qa-plan user-preferences
2026-02-08 06:01:23 +00:00

17 KiB

QA Plan: User Preferences API

Test Scenarios

Happy Path

ID Scenario Input Expected Output Derived From
HP-1 GET preferences for user with stored values GET /api/preferences-api/preferences/550e8400-e29b-41d4-a716-446655440000 (user has theme=dark, language=fr stored) 200 with {"data": {"user_id": "550e...", "preferences": {"theme": "dark", "language": "fr", "notifications_enabled": true}}, "meta": {...}} AC-1, AC-9
HP-2 GET preferences for new user returns defaults GET /api/preferences-api/preferences/660e8400-e29b-41d4-a716-446655440000 (no stored prefs) 200 with {"data": {"user_id": "660e...", "preferences": {"theme": "system", "language": "en", "notifications_enabled": true}}, "meta": {...}} AC-2, AC-6
HP-3 PUT creates preferences for new user PUT /api/preferences-api/preferences/550e8400-... with {"preferences": {"theme": "dark"}} 200 with full merged preferences (theme=dark, others at defaults) AC-3
HP-4 PUT updates existing preferences PUT /api/preferences-api/preferences/550e8400-... with {"preferences": {"language": "fr"}} (user already has theme=dark) 200 with merged result: theme=dark, language=fr, notifications_enabled=true AC-3
HP-5 PUT is idempotent Call PUT with {"preferences": {"theme": "dark"}} twice for the same user Both return 200 with identical response body AC-4
HP-6 PUT multiple keys at once PUT with {"preferences": {"theme": "light", "language": "de", "notifications_enabled": false}} 200 with all three keys updated AC-3, AC-6
HP-7 PUT with boolean notifications_enabled=true PUT with {"preferences": {"notifications_enabled": true}} 200 with notifications_enabled: true in response (boolean, not string) AC-6
HP-8 PUT with boolean notifications_enabled=false PUT with {"preferences": {"notifications_enabled": false}} 200 with notifications_enabled: false in response AC-6
HP-9 GET returns typed values (boolean for notifications) GET after storing notifications_enabled=true Response has "notifications_enabled": true (boolean), not "true" (string) AC-6, AC-9
HP-10 Valid theme values accepted PUT with theme = "light", "dark", or "system" separately All return 200 AC-6
HP-11 Valid BCP 47 language tags accepted PUT with language = "en", "fr", "zh-Hans", "en-US" All return 200 AC-6
HP-12 OpenAPI docs endpoint accessible GET /api/preferences-api/docs or configured docs path Returns OpenAPI/Scalar documentation page AC-10
HP-13 Health endpoint still works GET /api/preferences-api/health 200 OK AC-13

Edge Cases

ID Scenario Input Expected Output Derived From
EC-1 PUT with empty preferences object PUT with {"preferences": {}} 200 with current/default preferences unchanged AC-3
EC-2 PUT partial update preserves unmentioned keys User has theme=dark. PUT with {"preferences": {"language": "de"}} 200 with theme=dark preserved, language=de updated, notifications_enabled=true (default) AC-3
EC-3 GET for different user_ids returns independent data Set theme=dark for user A, theme=light for user B GET user A returns dark, GET user B returns light AC-1, AC-5
EC-4 PUT immediately followed by GET returns consistent data PUT to set theme=dark, then GET GET returns theme=dark AC-3, AC-4, AC-5
EC-5 Language tag with subtags PUT with language = "zh-Hans-CN" 200 accepted (valid BCP 47) AC-6
EC-6 Boundary language tag - minimum length PUT with language = "en" 200 accepted AC-6
EC-7 Boundary language tag - 3-letter code PUT with language = "deu" 200 accepted (3-letter ISO 639) AC-6
EC-8 user_id as different valid UUID formats GET /api/preferences-api/preferences/550e8400-e29b-41d4-a716-446655440000 200 (standard UUID with hyphens accepted) AC-8
EC-9 Default values match specification GET for a brand new user Exactly: theme=system, language=en, notifications_enabled=true AC-2, AC-6
EC-10 PUT same value as default PUT with {"preferences": {"theme": "system"}} 200 stored successfully, subsequent GET returns system AC-3

Error Cases

ID Scenario Input Expected Output Derived From
ER-1 PUT with unknown preference key PUT with {"preferences": {"font_size": "14"}} 400 Bad Request with message mentioning font_size AC-7
ER-2 PUT with mix of valid and unknown keys PUT with {"preferences": {"theme": "dark", "font_size": "14"}} 400 Bad Request (reject entire request, not partial) AC-7
ER-3 GET with invalid user_id (not UUID) GET /api/preferences-api/preferences/not-a-uuid 400 Bad Request AC-8
ER-4 PUT with invalid user_id (not UUID) PUT /api/preferences-api/preferences/abc123 with valid body 400 Bad Request AC-8
ER-5 GET with empty user_id GET /api/preferences-api/preferences/ 404 (route not matched) or 400 AC-8
ER-6 PUT with invalid theme value PUT with {"preferences": {"theme": "midnight"}} 400 Bad Request AC-6
ER-7 PUT with invalid language tag PUT with {"preferences": {"language": "123"}} 400 Bad Request AC-6
ER-8 PUT with invalid notifications_enabled value PUT with {"preferences": {"notifications_enabled": "maybe"}} 400 Bad Request AC-6
ER-9 PUT with malformed JSON body PUT with {invalid json 400 Bad Request AC-3
ER-10 PUT with wrong content type PUT with Content-Type: text/plain 400 Bad Request AC-3
ER-11 PUT with null preferences PUT with {"preferences": null} 400 Bad Request AC-3
ER-12 PUT with missing preferences field PUT with {} 400 Bad Request or 200 with no changes (implementation-dependent) AC-3
ER-13 PUT with wrong value type for theme PUT with {"preferences": {"theme": 123}} 400 Bad Request (type mismatch) AC-6
ER-14 PUT with wrong value type for notifications_enabled PUT with {"preferences": {"notifications_enabled": "true"}} (string instead of bool) 400 Bad Request or handled gracefully AC-6
ER-15 PUT with empty string theme PUT with {"preferences": {"theme": ""}} 400 Bad Request AC-6
ER-16 PUT with empty string language PUT with {"preferences": {"language": ""}} 400 Bad Request AC-6
ER-17 user_id with SQL injection attempt GET /api/preferences-api/preferences/'; DROP TABLE user_preferences; -- 400 Bad Request (UUID validation rejects) AC-8
ER-18 PUT with extremely long value PUT with {"preferences": {"language": " + 10000 char string + "}} 400 Bad Request (fails BCP 47 validation) AC-6

Test Data Requirements

Fixtures

Fixture Description Usage
valid-uuid-1 550e8400-e29b-41d4-a716-446655440000 Primary test user for CRUD operations
valid-uuid-2 660e8400-e29b-41d4-a716-446655440001 Secondary user for isolation tests
no-prefs-uuid 770e8400-e29b-41d4-a716-446655440002 User with no stored preferences (defaults only)
invalid-uuid not-a-uuid For UUID validation error tests

Mock Repository (Unit Tests)

  • Service tests: Mock PreferenceRepository interface with configurable return values
    • Returns empty slice for users with no stored preferences
    • Returns partial preference rows for partial-stored tests
    • Returns all preference rows for fully-stored tests
    • Returns error for database failure simulation

Database State (Integration Tests)

  • Clean user_preferences table before each test
  • Seed data: insert known rows for valid-uuid-1 with theme=dark, language=fr
  • Leave no-prefs-uuid with no rows (test default behavior)

Integration Test Plan

Component Integration Tests

ID Test Components Under Test Verification
IT-1 GET flows through handler → service → repo → DB All layers Response matches expected defaults for new user
IT-2 PUT flows through handler → service → repo → DB and persists All layers Subsequent GET returns updated values
IT-3 PUT upsert updates existing rows without duplication Service → adapter → DB SELECT count shows no duplicate (user_id, key) rows
IT-4 Migration creates table with correct schema Migration runner → DB Table exists, columns have correct types, PK and index present
IT-5 Database connection failure handled gracefully Handler → service → adapter 500 Internal Server Error (not panic, not leak connection details)
IT-6 Multiple concurrent PUT requests for same user Handler → service → adapter → DB No deadlocks, final state is consistent, ON CONFLICT handles races

Cross-Boundary Tests

ID Test Boundary Verification
CB-1 Domain validation errors map to HTTP 400 Domain → Service → Handler ErrUnknownPreferenceKey → 400, ErrInvalidPreferenceValue → 400, ErrInvalidUserID → 400
CB-2 Boolean serialization round-trip DB (TEXT) → Domain → API (JSON) "true" in DB → true (boolean) in JSON response
CB-3 OpenAPI spec matches actual API behavior Spec → Handler All documented endpoints respond as specified
CB-4 Auth middleware integration (when enabled) Auth middleware → Handler Unauthenticated requests rejected when AUTH_ENABLED=true

Unit Test Plan

Domain Layer Tests (internal/domain/)

ID Test Input Expected
UT-D1 DefaultPreferences returns all keys None Map with theme=system, language=en, notifications_enabled=true
UT-D2 ValidateKey accepts known keys "theme", "language", "notifications_enabled" nil error
UT-D3 ValidateKey rejects unknown keys "font_size", "", "THEME" ErrUnknownPreferenceKey
UT-D4 ValidateValue for theme - valid "light", "dark", "system" nil error
UT-D5 ValidateValue for theme - invalid "midnight", "", "123" ErrInvalidPreferenceValue
UT-D6 ValidateValue for language - valid BCP 47 "en", "fr", "zh-Hans", "en-US" nil error
UT-D7 ValidateValue for language - invalid "123", "", "a", "toolongstring" ErrInvalidPreferenceValue
UT-D8 ValidateValue for notifications_enabled - valid "true", "false" nil error
UT-D9 ValidateValue for notifications_enabled - invalid "yes", "1", "" ErrInvalidPreferenceValue
UT-D10 MergeWithDefaults fills missing keys {theme: "dark"} {theme: "dark", language: "en", notifications_enabled: "true"}
UT-D11 MergeWithDefaults preserves all stored All 3 keys provided All stored values preserved
UT-D12 MergeWithDefaults with empty input {} All defaults
UT-D13 SerializeForResponse converts booleans {notifications_enabled: "true"} {notifications_enabled: true} (bool)
UT-D14 SerializeForResponse preserves strings {theme: "dark"} {theme: "dark"} (string)
UT-D15 ValidateUserID accepts valid UUID "550e8400-e29b-41d4-a716-446655440000" nil error
UT-D16 ValidateUserID rejects non-UUID "not-a-uuid", "", "123" ErrInvalidUserID

Service Layer Tests (internal/service/)

ID Test Setup Input Expected
UT-S1 GetPreferences - no stored prefs Mock returns empty slice Valid UUID Result with all defaults
UT-S2 GetPreferences - partial stored Mock returns [{theme, "dark"}] Valid UUID theme=dark, others defaulted
UT-S3 GetPreferences - all stored Mock returns all 3 rows Valid UUID All stored values in result
UT-S4 GetPreferences - invalid user_id N/A "bad-id" ErrInvalidUserID
UT-S5 GetPreferences - repo error Mock returns error Valid UUID Error propagated
UT-S6 UpdatePreferences - single key Mock Upsert succeeds {theme: "dark"} Upsert called once, returns merged result
UT-S7 UpdatePreferences - multiple keys Mock Upsert succeeds {theme: "dark", language: "fr"} Upsert called twice
UT-S8 UpdatePreferences - unknown key N/A {font_size: "14"} ErrUnknownPreferenceKey
UT-S9 UpdatePreferences - invalid value N/A {theme: "midnight"} ErrInvalidPreferenceValue
UT-S10 UpdatePreferences - invalid user_id N/A user_id="bad" ErrInvalidUserID
UT-S11 UpdatePreferences - boolean handling Mock Upsert succeeds {notifications_enabled: false} Upsert called with "false" string
UT-S12 UpdatePreferences - repo error on upsert Mock Upsert returns error Valid input Error propagated

Handler Layer Tests (internal/api/handlers/)

ID Test Method/Path Request Expected Status Expected Body
UT-H1 GET success with defaults GET /preferences/{uuid} None 200 {data: {user_id, preferences: defaults}, meta}
UT-H2 GET success with stored values GET /preferences/{uuid} None (mock returns data) 200 {data: {user_id, preferences: stored+defaults}, meta}
UT-H3 GET invalid user_id GET /preferences/bad-id None 400 Error body
UT-H4 PUT success PUT /preferences/{uuid} {"preferences":{"theme":"dark"}} 200 Full merged prefs
UT-H5 PUT unknown key PUT /preferences/{uuid} {"preferences":{"font_size":"14"}} 400 Error mentioning unknown key
UT-H6 PUT invalid value PUT /preferences/{uuid} {"preferences":{"theme":"bad"}} 400 Error mentioning invalid value
UT-H7 PUT invalid user_id PUT /preferences/bad-id Valid body 400 Error body
UT-H8 PUT malformed JSON PUT /preferences/{uuid} {broken json 400 Error body
UT-H9 Response envelope shape GET /preferences/{uuid} None 200 Has top-level data and meta keys

Performance Considerations

Latency Expectations

Operation Expected P50 Expected P99 Notes
GET preferences < 5ms < 20ms Single indexed SELECT by user_id
PUT single key < 10ms < 50ms Single INSERT ON CONFLICT
PUT all 3 keys < 15ms < 75ms 3 sequential INSERT ON CONFLICT queries

Load Expectations

  • Expected load: Low frequency (once per page load, once per settings save)
  • No caching required at current scale
  • Connection pool defaults (25 max, 5 idle) are sufficient

Benchmarks to Run

ID Benchmark Target
PF-1 GET preferences with populated data Baseline latency
PF-2 PUT single preference key Baseline latency
PF-3 Concurrent GET requests (10 goroutines) No deadlocks, stable latency
PF-4 Concurrent PUT requests for same user (10 goroutines) No deadlocks, data consistent after all complete

Manual Verification Steps

  1. Database Migration: After deployment, verify user_preferences table exists with correct schema:

    \d user_preferences
    

    Confirm columns, types, primary key, and index match the migration.

  2. Default Values: Call GET /api/preferences-api/preferences/{new-uuid} for a UUID with no data. Verify response contains all three defaults: theme=system, language=en, notifications_enabled=true.

  3. Round-Trip Persistence: PUT a preference, restart the service, GET the same user. Verify data survived the restart (stored in PostgreSQL, not in-memory).

  4. OpenAPI Spec Accuracy: Open the Scalar docs page and verify both endpoints are documented with correct schemas and example responses.

  5. Error Messages: Send a PUT with an unknown key (e.g., font_size) and verify the 400 response includes the offending key name in the error message.

  6. Example Scaffold Removal: Verify GET /api/preferences-api/examples returns 404 (old endpoints removed).

  7. Health Check: Verify GET /api/preferences-api/health still returns 200 OK after all changes.

Acceptance Criteria Coverage Matrix

AC# Acceptance Criterion Test Scenarios
AC-1 GET returns all preferences for user HP-1, HP-9, EC-3
AC-2 GET for user with no prefs returns defaults HP-2, EC-9
AC-3 PUT creates or updates preferences HP-3, HP-4, HP-6, EC-1, EC-2, EC-10
AC-4 PUT is idempotent HP-5
AC-5 Preferences stored in PostgreSQL HP-1, EC-3, EC-4, IT-2, IT-3
AC-6 Supported keys with validation HP-7, HP-8, HP-9, HP-10, HP-11, EC-5, EC-6, EC-7, EC-9, ER-6, ER-7, ER-8, ER-13, ER-14, ER-15, ER-16
AC-7 Unknown keys rejected with 400 ER-1, ER-2
AC-8 user_id validated as UUID ER-3, ER-4, ER-5, ER-17, EC-8
AC-9 All responses use {data, meta} envelope HP-1, HP-2, UT-H9
AC-10 OpenAPI spec updated HP-12, CB-3
AC-11 Migration creates user_preferences table IT-4
AC-12 Example endpoints removed Manual step 6
AC-13 Hexagonal architecture followed All unit tests (mock-based layers)
AC-14 Unit tests cover service logic UT-S1 through UT-S12
AC-15 Handler tests cover HTTP layer UT-H1 through UT-H9