5.2 KiB
5.2 KiB
Code Review: User Preferences API
Summary
Overall Assessment: PASS
The implementation is well-structured, follows the hexagonal architecture pattern correctly, and satisfies all acceptance criteria from the spec. All 18 unit tests pass. The code follows project conventions for error handling, request binding, response envelopes, and URL parameter syntax. No blockers found.
Findings
Blockers
None.
Warnings
internal/api/handlers/preferences.go:63—UpdatedAtuses hardcodedZformat ("2006-01-02T15:04:05Z") instead oftime.RFC3339. If a non-UTC time is ever passed (e.g., from a database driver returning local time), theZsuffix would be incorrect. Currently safe becausetime.Now().UTC()is used consistently and PostgreSQLTIMESTAMPTZstores UTC, buttime.RFC3339would be more robust.internal/service/preferences.go/internal/api/handlers/preferences_test.go— Mock repository is duplicated between service tests and handler tests. Consider extracting to a sharedinternal/testutil/package to avoid maintaining two copies. Low priority since the mock is small and stable.
Suggestions
internal/domain/preferences.go— Domain validation could return all validation errors at once instead of short-circuiting on the first error. This would give clients a better experience when multiple fields are invalid. Low priority for MVP.internal/adapter/postgres/preferences.go— No integration tests for the PostgreSQL adapter. This is acceptable for now since the SQL is straightforward (PK lookup + upsert), but integration tests would increase confidence for future schema changes.
Spec Alignment
The implementation matches the spec completely:
| Acceptance Criterion | Status | Evidence |
|---|---|---|
| GET returns stored preferences | PASS | Handler test returns_stored_preferences |
| GET for unknown user returns defaults (not 404) | PASS | Service test returns_defaults_for_unknown_user, handler test returns_defaults_for_unknown_user |
| PUT creates preferences (upsert) | PASS | Service test creates_new_preferences_from_defaults, handler test creates_preferences_with_full_update |
| PUT merges partial data (deep merge) | PASS | Service test merges_partial_data_with_existing_preferences, handler test creates_preferences_with_partial_update |
| PUT validates theme | PASS | Service test rejects_invalid_theme, handler test returns_400_for_invalid_theme |
| PUT validates language non-empty | PASS | Service test rejects_empty_language |
| PUT validates notifications.digest | PASS | Service test rejects_invalid_digest |
| notifications.email/push are booleans | PASS | Enforced by Go type system (JSON binding) |
| Invalid values return 400 | PASS | mapPreferencesDomainError maps domain errors to httperror.BadRequest |
| Invalid user_id returns 400 | PASS | UUID parse check in both handlers, tested in handler tests |
| Standard {data, meta} envelope | PASS | Uses httpresponse.OK() |
| OpenAPI spec documents endpoints | PASS | spec.go defines GET/PUT with schemas, parameters, and responses |
| Preferences persisted in PostgreSQL | PASS | adapter/postgres/preferences.go with JSONB column |
| Database migration creates table | PASS | migrations/001_create_user_preferences.sql |
| Hexagonal architecture | PASS | domain -> service -> port -> adapter separation is clean |
| Service and handler unit tests | PASS | 10 service tests + 8 handler tests, all passing |
| Example scaffolding removed | PASS | No example files found |
Test Coverage Assessment
Service layer (10 tests):
- GetPreferences: defaults for unknown user, stored for existing user
- UpdatePreferences: create from defaults, partial merge, theme-only, language-only, all fields, invalid theme, invalid digest, empty language
Handler layer (8 tests):
- Get: stored preferences, defaults for unknown, invalid UUID
- Upsert: full update, partial update, invalid UUID, invalid theme, empty body
Coverage gaps (non-critical):
- No test for invalid digest at handler level (covered at service level)
- No test for invalid language at handler level (covered at service level)
- No integration tests for PostgreSQL adapter
- No test for concurrent access patterns (mock uses sync.RWMutex properly)
All acceptance criteria have at least one corresponding test. The test suite is thorough for an MVP.
Code Quality Notes
Strengths:
- Clean hexagonal architecture with proper dependency injection
- Pointer-based partial update pattern correctly distinguishes "not provided" from zero values
- Deep merge logic is simple and correct
- Domain validation is self-contained with clear error types
- Error mapping from domain to HTTP is explicit and maintainable
- Uses
app.Wrap(),app.BindAndValidate(),httpresponse.OK()per project conventions - Uses
{user_id}brace syntax for chi URL parameters (not colon syntax) - Compile-time interface verification (
var _ port.PreferencesRepository = ...) - SQL uses parameterized queries ($1, $2) — no injection risk
- Atomic upsert with
ON CONFLICT DO UPDATE
Minor observations:
toPreferencesResponsehardcodes time format rather than usingtime.RFC3339(warning above)- Mock repository duplicated across test files (warning above)