slack5-1770574304/.sdlc/features/user-preferences/review.md
rdev-worker 3576d4e4de
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /review-feature user-preferences
2026-02-09 01:51:02 +00:00

7.3 KiB

Code Review: User Preferences API

Summary

Overall Assessment: PASS

The implementation is well-structured, follows the hexagonal architecture pattern correctly, and satisfies the spec requirements. Build succeeds, all 16 tests pass, and go vet reports no issues. No example scaffold files remain. The code follows project conventions (handler pattern, error wrapping, response envelope, auth middleware, brace URL params). Security is sound with proper authorization checks, parameterized SQL, strict JSON binding, and auth middleware. A few minor improvements are recommended but nothing blocks shipping.

Findings

Blockers

None.

Warnings

  • services/preferences-api/internal/service/preferences.go:55 Re-fetch after upsert may return nil with in-memory adapter -- The UpdatePreferences method calls s.repo.Get(ctx, userID) after Upsert to return the saved result. With the in-memory adapter, the returned UpdatedAt will be the Go-set value (line 42), not a DB-generated timestamp. With PostgreSQL, the NOW() value from the DB is returned. This inconsistency is minor (tests use in-memory, production uses PostgreSQL) but could cause confusion if the in-memory adapter is ever used for integration testing that checks timestamps. Consider having Upsert return the saved entity directly to avoid the extra round-trip.

  • services/preferences-api/internal/domain/preferences.go No domain-level unit tests -- The spec states "Unit tests cover domain validation, service logic, and handler request/response mapping." The Validate() and DefaultPreferences() functions have no direct unit tests. They are indirectly tested via service tests, but dedicated domain tests would catch regressions if the domain layer changes independently.

  • services/preferences-api/internal/api/handlers/preferences_test.go:73-75 Weak assertion for updated_at on defaults -- The test for default preferences checks updated_at but the assertion is effectively a no-op (the comment says "may be present but empty for defaults" and the condition does nothing). This should assert that updated_at is either absent or an empty string for default preferences, matching the spec behavior.

  • services/preferences-api/internal/api/handlers/preferences_test.go No PUT-then-GET roundtrip test -- The QA plan specifies a roundtrip test (HP-10) verifying that data persisted via PUT can be retrieved via GET. No such test exists in the handler test suite. This is an important integration-level assertion.

Suggestions

  • services/preferences-api/internal/api/handlers/preferences_test.go Missing handler-level tests for invalid language and invalid digest -- Handler tests only cover invalid theme (TestPreference_Update_InvalidThemeValue). Invalid language and invalid digest are tested at the service layer but not at the handler HTTP level. Adding these would verify the full error mapping path end-to-end.

  • services/preferences-api/internal/api/routes.go:36 Auth middleware is conditional on AUTH_ENABLED -- The spec says "Both endpoints require authentication via the existing auth.Middleware()" unconditionally. The implementation makes auth optional via an environment variable. This is likely intentional for local development, but should be documented to avoid security concerns in production deployments.

  • services/preferences-api/internal/api/handlers/preferences.go:34-37 Request DTO has redundant validation with domain layer -- The struct tags on UpdatePreferencesRequest (oneof=light dark system, etc.) duplicate the same validation rules in domain.Validate(). The domain validation will never fire in the handler path since struct tag validation catches invalid values first. This is defensive but means the domain Validate() is effectively untested via the handler path. Consider keeping validation in one canonical location.

  • services/preferences-api/cmd/server/main.go No db.Ping() to verify database connectivity at startup -- The sql.Open call only validates arguments, it doesn't actually connect. Adding db.PingContext(ctx) after opening would catch connection issues at startup rather than on the first request.

Spec Alignment

The implementation matches the spec well:

Requirement Status
GET /api/preferences-api/preferences/{user_id} Implemented correctly
PUT /api/preferences-api/preferences/{user_id} Implemented correctly
Auth required on both endpoints Implemented (conditionally via AUTH_ENABLED)
Self-access enforcement (JWT match) Implemented correctly
Admin read access Implemented correctly
Admin write denied Implemented correctly
Theme/language/digest validation Implemented at both struct-tag and domain levels
GET returns defaults (200, not 404) Implemented correctly
Default values match spec Correct: theme=system, language=en, email=true, push=true, digest=weekly
Unknown keys return 400 Implemented via app.BindAndValidateStrict
Invalid values return 400 Implemented via struct tags and domain validation
PostgreSQL persistence Implemented with parameterized queries
Hexagonal architecture Correctly follows domain -> service -> port -> adapter
OpenAPI spec updated Complete with schemas, security, and error responses
Example scaffold removed Fully removed, no remnants

Minor gap: The spec says "PUT with invalid preference values returns 400 Bad Request with per-field validation errors." The struct tag validation does produce per-field details via httperror.WithDetails, which satisfies this. However, domain-level validation errors (from Validate()) produce a single error message rather than per-field details. Since struct tags catch these first, this is not a practical issue.

Test Coverage Assessment

Acceptance Criterion Has Test? Test Name
GET returns preferences in envelope Yes TestPreference_Get_SelfAccess
PUT creates/replaces (upsert) Yes TestPreference_Update_SelfAccess
Auth required Partial Auth is bypassed in tests (set via context); no 401 test
JWT user match enforced Yes TestPreference_Get_Forbidden, TestPreference_Update_Forbidden
Admin read allowed Yes TestPreference_Get_AdminAccess
Admin write denied Yes TestPreference_Update_AdminForbidden
Theme validation Yes TestPreference_Update_InvalidThemeValue (handler), TestPreferenceService_UpdatePreferences_InvalidTheme (service)
Language validation Partial Service test only, no handler test
Digest validation Partial Service test only, no handler test
GET returns defaults (not 404) Yes TestPreference_Get_SelfAccess, TestPreferenceService_GetPreferences_Defaults
Default values correct Yes TestPreferenceService_GetPreferences_Defaults
Unknown keys return 400 Yes TestPreference_Update_UnknownFields
Missing fields return 400 Yes TestPreference_Update_InvalidBody
PUT-then-GET roundtrip No Missing
Domain validation standalone No No domain-level tests
GET existing (non-default) prefs Yes TestPreference_Get_ExistingPreferences

16 total tests, all passing. Coverage is good for the critical paths. The main gaps are the roundtrip test and domain-level unit tests, which are classified as warnings above.