feat: add user-vehicle assignment CRUD endpoints #60
feat: add user-vehicle assignment CRUD endpoints #60diveshpatil9104 wants to merge 5 commits intoOneBusAway:mainfrom
Conversation
aaronbrethorst
left a comment
There was a problem hiding this comment.
Divesh, the implementation quality here is impressive — the interface segregation, the compile-time satisfaction checks, the thorough input validation discipline (Content-Type, MaxBytesReader, DisallowUnknownFields, trailing-data rejection), and the 49 tests across unit and integration layers all show real care. The scanAssignments shared helper, the non-nil empty slice initialization, and the pgconn error code detection are all well-executed.
That said, there are a couple of issues that need to be resolved before this can merge.
Critical Issues (2 found)
-
Admin endpoints have no authorization check — any authenticated user can manage assignments (
main.go:84-89). The routes live under/api/v1/admin/but are protected only byauthMiddleware(requireAuth), which validates the JWT but does not check theroleclaim. This means any authenticated driver can create/delete/list assignments for any user. The JWT already includes aroleclaim (seeauth.go), andrequireAdminTokenalready exists in the codebase (used by the GTFS upload endpoint on the same branch). Shipping admin-scoped CRUD endpoints without role-based authorization is a security gap. The TODO comment acknowledges this, but the fix is trivial — either use the existingrequireAdminTokenmiddleware or add an inline role check. This should not ship without authorization. -
Migration number conflict —
000004is already taken onmain(migrations/000004_add_user_vehicles.{up,down}.sql). PR #40 was merged tomainand occupies migration000004_add_vehicle_received_index. This PR also uses000004. When both exist,golang-migratewill fail at startup because it expects unique, sequential migration numbers. You'll need to rebase againstmainand renumber to000005.
Important Issues (4 found)
-
Raw SQL queries instead of sqlc (
assignment_store.go). The existing codebase uses sqlc for all database queries (seedb/query.sql.goandstore.gowhich delegates tos.queries.*). This PR writes raw SQL directly againsts.pooland manually scans results, creating two divergent data access patterns. The sqlc approach catches SQL errors at generation time and keeps the schema definition in sync. Please add the assignment queries todb/query.sqland regenerate. -
FK constraint name mismatch silently converts a 4xx into a 500 (
assignment_store.go:59-66). TheswitchonfkConstraintName(err)matches against hardcoded strings"user_vehicles_user_id_fkey"and"user_vehicles_vehicle_id_fkey". If the constraint name doesn't match either case (e.g., due to a migration rename or explicit naming), the switch falls through and returns a generic error. The handler then maps that to a 500 instead of a 404. Two fixes: (a) explicitly name the constraints in the migration DDL so the names are guaranteed, and (b) add adefaultcase that logs the unrecognized constraint name and returns a 4xx-mappable error rather than silently falling through to 500. -
Missing pagination on list endpoints (
assignment_store.go:90-108). NeitherListAssignmentsByUsernorListAssignmentsByVehiclehas aLIMIT. If a vehicle has thousands of assignments, the response is unbounded. At minimum, add a safety cap (e.g.,LIMIT 1000) to the SQL queries. -
Nil-slice serialization risk in list handlers (
assignment_handlers.go:140,160). The store currently returnsmake([]AssignmentResponse, 0)(non-nil), sojson.Encodeproduces[]. But the handler trusts this — if a future refactor returnsnil, nil, the API silently changes from[]tonull. Add a nil guard in the handler:if assignments == nil { assignments = []AssignmentResponse{} }.
Suggestions (2 found)
-
Inconsistent validation messages across endpoints.
handleCreateAssignmentprovides specific error messages for each vehicle_id validation failure ("vehicle_id is required", "must be at most 50 characters", "must contain only alphanumeric characters..."), buthandleDeleteAssignmentandhandleListVehicleUserscollapse all three checks into a single generic "invalid vehicle id" message. Consider extracting a sharedvalidateVehicleIDfunction. -
Inconsistent 4xx logging.
handleDeleteAssignmentlogs atslog.Warnwhen a delete targets a nonexistent assignment (line 112), but the other handlers don't log 4xx responses. Either remove the Warn from delete-not-found to match, or add equivalent logging to all 4xx paths.
Strengths
- Clean interface segregation (
AssignmentCreator,AssignmentDeleter,AssignmentListerByUser,AssignmentListerByVehicle) with compile-time satisfaction checks - Thorough input validation: Content-Type, MaxBytesReader (1KB), DisallowUnknownFields, trailing-data rejection via
json.RawMessage - 49 tests total — 37 unit tests with mock store covering happy paths, validation, JSON edge cases, FK violations, and duplicates; 12 integration tests with real PostgreSQL including cascade delete verification and explicit timestamp ordering
- Non-nil empty slice initialization ensures
[]notnullin JSON INSERT ... RETURNING created_atuses DB-generated timestamp rather than fabricatingtime.Now()- Proper error wrapping with distinct messages for diagnostic differentiation
- DELETE uses path params instead of body — good REST practice
Recommended Action
- Add admin authorization (use
requireAdminTokenor equivalent) — this is a security blocker - Rebase against
mainand renumber migration to000005 - Address the sqlc consistency, FK constraint naming, and pagination issues
- Re-run review after fixes
a51817f to
8226ab8
Compare
|
All changes addressed:
|
…LIMIT and nil guards
8226ab8 to
cf9032e
Compare
Summary
user_vehiclesjoin table (migration 000008) with composite PK, explicitly named FK constraints, CASCADE deletes, and vehicle_id indexauthMiddleware+adminMiddlewareWhy
Milestone 2 requires admin endpoints to manage many-to-many relationships between users and vehicles (README line 264). The
user_vehiclesjoin table is defined in the README database design but does not exist yet.Without assignment endpoints, admins cannot connect drivers to vehicles — the start-trip endpoint (PR #55) will always fail its assignment check. This PR is the missing link:
What Changed
migrations/000008_add_user_vehicles.up.sql— Createsuser_vehiclestable with PK(user_id, vehicle_id), explicitly named FK constraints withON DELETE CASCADE,created_attimestamp, and index onvehicle_id.assignment_store.go— Store methods using sqlc-generated queries (matchingstore_vehicles.gopattern). Four interfaces for testability. Detects unique violations (duplicate → 409) and FK violations with constraint name switch including default case (user/vehicle not found → 404). List queries capped atLIMIT 1000.assignment_handlers.go— 4 handlers following existing closure pattern. Content-Type validation,MaxBytesReader(1KB),DisallowUnknownFields, trailing data check. Reuses existingvalidateVehicleID(). Nil-slice guards ensure[]notnullin JSON responses.assignment_handlers_test.go— 37 unit tests with mock store: validation, boundary tests (50/51 char vehicle_id), Content-Type, JSON edge cases, FK violations, duplicates. All assert error messages, not just status codes.assignment_store_test.go— 12 integration tests: round-trip, duplicate, delete, FK violations with rollback verification, empty lists, ordering with explicitcreated_atoffsets (notime.Sleep), CASCADE behavior.main.go— Register 4 routes under/api/v1/admin/wrapped withauthMiddleware(adminMiddleware(...)).Routes
/api/v1/admin/assignments/api/v1/admin/users/{userID}/vehicles/{vehicleID}/api/v1/admin/users/{id}/vehicles/api/v1/admin/vehicles/{id}/users