fix(api): stop PUT /v1/vehicles/{id} silently discarding the odometer - #301
Merged
Conversation
`odometer` is fillable on the Vehicle model and unrestricted by the request rules, but `VehicleController::vehicleInputFromRequest()` builds its input with `$request->only([...])` and that allowlist had no odometer in it. A caller sending one therefore received a 200 and a response body that looked correct, while the reading went nowhere. Recording mileage is the most common write a driver app makes against a vehicle — it is what a fuel report is checked against — and a silent no-op is the worst of the three possible answers. Accept it or reject it; reporting success for a discarded field leaves the client with no way to tell. Adds `odometer` and `odometer_unit` to the projection, and validates them rather than merely accepting them: the model casts odometer to an integer, so an unchecked string would have been stored as 0, which reads as a vehicle that has never moved rather than as an error. Two tests, using the probe already in the suite for protected helpers: one that the odometer survives the projection, and one that the projection is still an allowlist — adding a field must not turn it into "whatever the caller sent", so company_uuid and uuid must still be dropped.
…wns it The two odometer tests were appended to VehicleControllerHelperContractsTest, whose probe extends Internal\v1\VehicleController. The helper they exercise, vehicleInputFromRequest, lives on Api\v1\VehicleController, so the reflection lookup errored with "method does not exist" and took PHP CI down. Move them to ApiVehicleControllerContractsTest, which already has a probe exposing that helper, and pin the new odometer validation rules in RequestContractsTest alongside the rest of the vehicle request contract.
A local symlink into a sibling checkout slipped into the previous commit and broke `pnpm install` in CI with ENOTDIR. .gitignore's `/node_modules/` has a trailing slash, so it matches the directory but not a symlink of that name.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev-v0.6.61 #301 +/- ##
===============================================
Coverage 100.00% 100.00%
Complexity 9815 9815
===============================================
Files 523 523
Lines 37888 37891 +3
===============================================
+ Hits 37888 37891 +3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
odometeris in theVehiclemodel's$fillableand nothing in the requestrules forbids it — but
VehicleController::vehicleInputFromRequest()builds theinput it applies with
$request->only([...]), and that allowlist has noodometer in it:
So
PUT /v1/vehicles/{id}with{"odometer": 211098, "odometer_unit": "km"}returns 200, hands back a response body that looks entirely correct, and
discards the reading.
Recording mileage is the most common write a driver app makes against a vehicle
— it is the number a fuel report is sanity-checked against. A silent no-op is
the worst of the three available answers: accept it, or reject it, but reporting
success for a field that was dropped leaves the client with no way to find out.
Found while building the Navigator redesign's "My vehicle" screen, which had to
tell drivers that entering an odometer was unavailable. It turned out the
endpoint was there all along and simply threw the value away.
The change
odometerandodometer_unitadded to the projection.odometerto aninteger, so an unchecked string would have been stored as
0— a vehiclereporting that it has never moved, rather than an error.
nullable|numeric|min:0and
nullable|string|max:12. NoRule::inon the unit, because the codebasedefines no vocabulary for it anywhere and inventing one here could reject what
the console already writes.
Tests
Two, using the
FleetOpsVehicleControllerProbealready in the suite forprotected helpers:
into "whatever the caller sent", so
company_uuidanduuidare still droppedI could not run them locally.
server/tests/VehicleControllerHelperContractsTest.phpfatals on this machine with
Trait "Illuminate\Foundation\Auth\Access\AuthorizesRequests" not found— the whole file, not just the new cases, and identically on a clean checkout of
mainat this base commit. It is a missingilluminate/foundationin the localserver_vendor, not anything in this change. CI has the full install. Flaggingit rather than implying a green run I did not see.
Related
Same shape as an issue on the fuel-report and issue list filters, where
driver_uuidis accepted, ignored, and answered with every driver's records.Separate PR.