@fleetbase/fleetops-engine 0.6.60 → 0.6.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/RELEASE.md +21 -17
- package/addon/components/ai/route-preview-map.js +1 -5
- package/addon/components/customer/orders.js +2 -1
- package/addon/components/driver/card.hbs +49 -0
- package/addon/components/driver/card.js +30 -0
- package/addon/components/map/leaflet-live-map.js +4 -19
- package/addon/components/modals/place-details.hbs +1 -1
- package/addon/components/modals/point-map.hbs +1 -1
- package/addon/components/orchestrator-workbench.js +2 -1
- package/addon/components/order/form/route.js +3 -0
- package/addon/components/order-tracking-lookup.js +2 -1
- package/addon/components/place/details.hbs +1 -1
- package/addon/components/positions-replay.js +4 -1
- package/addon/components/service-area/details.hbs +1 -1
- package/addon/components/widget/live-fleet.js +7 -7
- package/addon/components/zone/details.hbs +1 -1
- package/addon/controllers/management/drivers/index.js +22 -1
- package/addon/controllers/settings/map.js +16 -0
- package/addon/helpers/leaflet-tile-url.js +19 -0
- package/addon/services/map-settings.js +26 -1
- package/addon/templates/management/drivers/index.hbs +67 -33
- package/addon/templates/settings/map.hbs +10 -0
- package/addon/utils/leaflet-tile-url.js +31 -0
- package/addon/utils/prepare-place-for-save.js +13 -0
- package/app/components/driver/card.js +1 -0
- package/app/helpers/leaflet-tile-url.js +1 -0
- package/app/utils/leaflet-tile-url.js +1 -0
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/index.js +11 -0
- package/package.json +2 -1
- package/server/migrations/2026_09_05_000001_add_unique_indexes_to_fleet_membership_pivots.php +129 -0
- package/server/src/Exceptions/PublicRelationNotFoundException.php +47 -0
- package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesFleetOpsApiResources.php +42 -7
- package/server/src/Http/Controllers/Api/v1/Concerns/ResolvesPublicExpansions.php +187 -0
- package/server/src/Http/Controllers/Api/v1/DriverController.php +312 -59
- package/server/src/Http/Controllers/Api/v1/EntityController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/FleetController.php +370 -33
- package/server/src/Http/Controllers/Api/v1/FuelReportController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/IssueController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/ManifestController.php +250 -0
- package/server/src/Http/Controllers/Api/v1/OrderController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/ServiceAreaController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/TrackingStatusController.php +1 -1
- package/server/src/Http/Controllers/Api/v1/VehicleController.php +139 -34
- package/server/src/Http/Controllers/Api/v1/ZoneController.php +1 -1
- package/server/src/Http/Controllers/Internal/v1/SettingController.php +22 -0
- package/server/src/Http/Filter/Concerns/ResolvesPublicRelationUuids.php +54 -0
- package/server/src/Http/Filter/DriverFilter.php +71 -15
- package/server/src/Http/Filter/FleetFilter.php +32 -33
- package/server/src/Http/Filter/FuelReportFilter.php +29 -0
- package/server/src/Http/Filter/IssueFilter.php +29 -0
- package/server/src/Http/Filter/VehicleFilter.php +57 -19
- package/server/src/Http/Requests/Concerns/ScopesPublicRelationRules.php +48 -0
- package/server/src/Http/Requests/CreateDriverRequest.php +100 -9
- package/server/src/Http/Requests/CreateFleetRequest.php +30 -2
- package/server/src/Http/Requests/CreateServiceRateRequest.php +7 -2
- package/server/src/Http/Requests/CreateVehicleRequest.php +150 -26
- package/server/src/Http/Resources/v1/Concerns/ResolvesPublicRelationFields.php +110 -0
- package/server/src/Http/Resources/v1/Driver.php +22 -2
- package/server/src/Http/Resources/v1/Fleet.php +38 -25
- package/server/src/Http/Resources/v1/Manifest.php +52 -0
- package/server/src/Http/Resources/v1/ManifestStop.php +47 -0
- package/server/src/Http/Resources/v1/OrderConfig.php +51 -4
- package/server/src/Http/Resources/v1/Vehicle.php +46 -4
- package/server/src/Models/Driver.php +2 -2
- package/server/src/Models/Place.php +1 -1
- package/server/src/routes.php +29 -0
- package/server/tests/ApiDriverControllerContractsTest.php +317 -13
- package/server/tests/ApiDriverPasswordContractsTest.php +457 -0
- package/server/tests/ApiEntityControllerContractsTest.php +1 -1
- package/server/tests/ApiEquipmentControllerContractsTest.php +2 -2
- package/server/tests/ApiFleetControllerContractsTest.php +354 -21
- package/server/tests/ApiFuelReportControllerContractsTest.php +1 -1
- package/server/tests/ApiIssueControllerContractsTest.php +1 -1
- package/server/tests/ApiManifestControllerContractsTest.php +599 -0
- package/server/tests/ApiOrderControllerContractsTest.php +1 -1
- package/server/tests/ApiPartControllerContractsTest.php +2 -2
- package/server/tests/ApiServiceAreaZoneControllerContractsTest.php +2 -2
- package/server/tests/ApiTrackingStatusControllerContractsTest.php +1 -1
- package/server/tests/ApiVehicleControllerContractsTest.php +210 -8
- package/server/tests/CompactResourceSerializationTest.php +13 -7
- package/server/tests/ControllerFilterContractsTest.php +132 -13
- package/server/tests/ControllerHelperContractsTest.php +6 -3
- package/server/tests/DriverFilterExecutionTest.php +130 -6
- package/server/tests/ExceptionContractsTest.php +15 -0
- package/server/tests/Feature/Http/Api/DeviceControllerContractsTest.php +1 -1
- package/server/tests/Feature/Http/Api/DriverControllerSessionHelpersTest.php +61 -0
- package/server/tests/Feature/Http/Api/FleetMembershipMigrationTest.php +140 -0
- package/server/tests/Feature/Http/Api/FleetMembershipTest.php +333 -0
- package/server/tests/Feature/Http/Api/FleetPublicContractTest.php +439 -0
- package/server/tests/Feature/Http/Api/FuelTransactionControllerContractsTest.php +2 -2
- package/server/tests/Feature/Http/Api/NullRelationshipInputTest.php +191 -0
- package/server/tests/Feature/Http/Api/PublicIdentifierFilterTest.php +293 -0
- package/server/tests/Feature/Http/Api/SmallApiControllerHelpersTest.php +7 -1
- package/server/tests/Feature/Http/Api/VehicleControllerTrackingTest.php +3 -1
- package/server/tests/Feature/Http/Api/VehiclePublicContractTest.php +454 -0
- package/server/tests/Feature/Http/Api/WorkOrderControllerContractsTest.php +1 -1
- package/server/tests/Feature/Http/Internal/PlaceControllerTest.php +45 -0
- package/server/tests/FleetHierarchyResourceTest.php +56 -8
- package/server/tests/RequestContractsTest.php +105 -7
- package/server/tests/SettingControllerContractsTest.php +33 -0
- package/server/tests/Unit/Http/Filter/FuelReportFilterTest.php +135 -0
- package/server/tests/Unit/Http/Filter/IssueFilterTest.php +43 -0
- package/server/tests/Unit/Http/Resources/FleetResourceTest.php +50 -6
- package/server/tests/Unit/Http/Resources/ManifestResourceTest.php +129 -0
- package/server/tests/Unit/Http/Resources/OrderConfigResourceTest.php +91 -0
- package/server/tests/VehicleControllerHelperContractsTest.php +1 -1
- package/translations/en-us.yaml +4 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* illuminate/foundation is not installed locally — CI installs it — so the
|
|
5
|
+
* controller's base class cannot load without these. Guarded, so they do
|
|
6
|
+
* nothing where the real ones exist.
|
|
7
|
+
*/
|
|
8
|
+
if (!trait_exists('Illuminate\Foundation\Auth\Access\AuthorizesRequests')) {
|
|
9
|
+
eval('namespace Illuminate\Foundation\Auth\Access; trait AuthorizesRequests {}');
|
|
10
|
+
}
|
|
11
|
+
if (!trait_exists('Illuminate\Foundation\Bus\DispatchesJobs')) {
|
|
12
|
+
eval('namespace Illuminate\Foundation\Bus; trait DispatchesJobs {}');
|
|
13
|
+
}
|
|
14
|
+
if (!trait_exists('Illuminate\Foundation\Validation\ValidatesRequests')) {
|
|
15
|
+
eval('namespace Illuminate\Foundation\Validation; trait ValidatesRequests {}');
|
|
16
|
+
}
|
|
17
|
+
if (!class_exists('Illuminate\Foundation\Auth\User')) {
|
|
18
|
+
eval('namespace Illuminate\Foundation\Auth; class User extends \Illuminate\Database\Eloquent\Model {}');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!function_exists('Fleetbase\Traits\config')) {
|
|
22
|
+
eval('namespace Fleetbase\Traits; function config($key = null, $default = null) { return $key === "api.cache.enabled" ? false : $default; }');
|
|
23
|
+
}
|
|
24
|
+
if (!function_exists('Fleetbase\Models\config')) {
|
|
25
|
+
eval('namespace Fleetbase\Models; function config($key = null, $default = null) { return $default; }');
|
|
26
|
+
}
|
|
27
|
+
if (!function_exists('Fleetbase\FleetOps\Models\config')) {
|
|
28
|
+
eval('namespace Fleetbase\FleetOps\Models; function config($key = null, $default = null) { return $default; }');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!class_exists('FleetOpsPasswordTestResponse')) {
|
|
32
|
+
class FleetOpsPasswordTestResponse
|
|
33
|
+
{
|
|
34
|
+
public function __construct(public mixed $payload = null, public int $status = 200)
|
|
35
|
+
{
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public function getStatusCode(): int
|
|
39
|
+
{
|
|
40
|
+
return $this->status;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public function getData(): mixed
|
|
44
|
+
{
|
|
45
|
+
return $this->payload;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class FleetOpsPasswordTestResponseFactory
|
|
50
|
+
{
|
|
51
|
+
public function apiError(string $message, int $status = 400): FleetOpsPasswordTestResponse
|
|
52
|
+
{
|
|
53
|
+
return new FleetOpsPasswordTestResponse(['error' => $message], $status);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public function json(mixed $payload = null, int $status = 200): FleetOpsPasswordTestResponse
|
|
57
|
+
{
|
|
58
|
+
return new FleetOpsPasswordTestResponse($payload, $status);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!function_exists('Fleetbase\FleetOps\Http\Controllers\Api\v1\response')) {
|
|
64
|
+
eval('namespace Fleetbase\FleetOps\Http\Controllers\Api\v1; function response() { return new \FleetOpsPasswordTestResponseFactory(); }');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
use Fleetbase\FleetOps\Http\Controllers\Api\v1\DriverController;
|
|
68
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
69
|
+
use Fleetbase\Models\User;
|
|
70
|
+
use Illuminate\Http\Request;
|
|
71
|
+
|
|
72
|
+
/** A user whose password writes and token operations are observable. */
|
|
73
|
+
class FleetOpsPasswordUserFake extends User
|
|
74
|
+
{
|
|
75
|
+
public array $saved = [];
|
|
76
|
+
public bool $tokensDeleted = false;
|
|
77
|
+
public ?string $issuedTokenFor = null;
|
|
78
|
+
|
|
79
|
+
/** core-api hashes on write; hashing is not wired in this harness. */
|
|
80
|
+
public function setPasswordAttribute($value): void
|
|
81
|
+
{
|
|
82
|
+
$this->attributes['password'] = 'hashed:' . $value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public function save(array $options = []): bool
|
|
86
|
+
{
|
|
87
|
+
$this->saved[] = $this->attributes['password'] ?? null;
|
|
88
|
+
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public function tokens()
|
|
93
|
+
{
|
|
94
|
+
$user = $this;
|
|
95
|
+
|
|
96
|
+
return new class($user) {
|
|
97
|
+
public function __construct(private FleetOpsPasswordUserFake $user)
|
|
98
|
+
{
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public function delete(): bool
|
|
102
|
+
{
|
|
103
|
+
$this->user->tokensDeleted = true;
|
|
104
|
+
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public function createToken($name, array $abilities = ['*'], $expiresAt = null)
|
|
111
|
+
{
|
|
112
|
+
$this->issuedTokenFor = is_string($name) ? $name : 'unknown';
|
|
113
|
+
|
|
114
|
+
return new class {
|
|
115
|
+
public string $plainTextToken = 'fresh-token';
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A driver that answers with the user the test supplied. */
|
|
121
|
+
class FleetOpsPasswordDriverFake extends Driver
|
|
122
|
+
{
|
|
123
|
+
public ?User $userForTest = null;
|
|
124
|
+
|
|
125
|
+
public function getUser(): ?User
|
|
126
|
+
{
|
|
127
|
+
return $this->userForTest;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
class FleetOpsPasswordControllerProbe extends DriverController
|
|
132
|
+
{
|
|
133
|
+
public static ?Driver $driver = null;
|
|
134
|
+
public static bool $driverMissing = false;
|
|
135
|
+
public static ?User $identityUser = null;
|
|
136
|
+
public static mixed $resetCode = null;
|
|
137
|
+
public static array $sentCodes = [];
|
|
138
|
+
public static bool $sendThrows = false;
|
|
139
|
+
|
|
140
|
+
public function findDriver(string $id, array $with = []): Driver
|
|
141
|
+
{
|
|
142
|
+
if (static::$driverMissing || !static::$driver) {
|
|
143
|
+
throw new Illuminate\Database\Eloquent\ModelNotFoundException();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return static::$driver;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
protected static function findDriverUserByIdentity(string $identity): ?User
|
|
150
|
+
{
|
|
151
|
+
return static::$identityUser;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
protected static function findDriverUserRecord(Driver $driver): ?User
|
|
155
|
+
{
|
|
156
|
+
return $driver->getUser();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
protected static function findResetCode(User $user, string $code)
|
|
160
|
+
{
|
|
161
|
+
return static::$resetCode;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Hashing is not wired in this harness, and asserting that bcrypt works is
|
|
166
|
+
* not what these tests are for — the question is whether a mismatch is
|
|
167
|
+
* refused.
|
|
168
|
+
*/
|
|
169
|
+
protected static function passwordMatches(User $user, string $plain): bool
|
|
170
|
+
{
|
|
171
|
+
return $plain === static::$correctPassword;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public static string $correctPassword = 'correct-horse';
|
|
175
|
+
|
|
176
|
+
protected static function debugEnabled(): bool
|
|
177
|
+
{
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
protected static function sendResetCode(User $user, string $identity): void
|
|
182
|
+
{
|
|
183
|
+
if (static::$sendThrows) {
|
|
184
|
+
throw new RuntimeException('sms gateway down');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
static::$sentCodes[] = $identity;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function fleetopsPasswordUser(string $plainPassword = 'correct-horse'): FleetOpsPasswordUserFake
|
|
192
|
+
{
|
|
193
|
+
FleetOpsPasswordControllerProbe::$correctPassword = $plainPassword;
|
|
194
|
+
|
|
195
|
+
$user = new FleetOpsPasswordUserFake();
|
|
196
|
+
$user->setRawAttributes(['uuid' => 'user-uuid', 'password' => 'stored-hash'], true);
|
|
197
|
+
|
|
198
|
+
return $user;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
beforeEach(function () {
|
|
202
|
+
FleetOpsPasswordControllerProbe::$driver = null;
|
|
203
|
+
FleetOpsPasswordControllerProbe::$driverMissing = false;
|
|
204
|
+
FleetOpsPasswordControllerProbe::$identityUser = null;
|
|
205
|
+
FleetOpsPasswordControllerProbe::$resetCode = null;
|
|
206
|
+
FleetOpsPasswordControllerProbe::$sentCodes = [];
|
|
207
|
+
FleetOpsPasswordControllerProbe::$sendThrows = false;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('change password requires both the current and the new one', function () {
|
|
211
|
+
$response = (new FleetOpsPasswordControllerProbe())->changePassword(new Request(['password' => 'newpassword']), 'driver_a');
|
|
212
|
+
|
|
213
|
+
expect($response->getStatusCode())->toBe(400);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('change password refuses a new password that is too short to be worth having', function () {
|
|
217
|
+
$response = (new FleetOpsPasswordControllerProbe())->changePassword(
|
|
218
|
+
new Request(['current_password' => 'correct-horse', 'password' => 'short']),
|
|
219
|
+
'driver_a'
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
expect($response->getStatusCode())->toBe(400);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('change password refuses a confirmation that does not match', function () {
|
|
226
|
+
$response = (new FleetOpsPasswordControllerProbe())->changePassword(
|
|
227
|
+
new Request(['current_password' => 'correct-horse', 'password' => 'newpassword', 'password_confirmation' => 'newpassward']),
|
|
228
|
+
'driver_a'
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
expect($response->getStatusCode())->toBe(422);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('change password reports a missing driver', function () {
|
|
235
|
+
FleetOpsPasswordControllerProbe::$driverMissing = true;
|
|
236
|
+
|
|
237
|
+
$response = (new FleetOpsPasswordControllerProbe())->changePassword(
|
|
238
|
+
new Request(['current_password' => 'correct-horse', 'password' => 'newpassword']),
|
|
239
|
+
'driver_missing'
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
expect($response->getStatusCode())->toBe(404);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('change password refuses a driver with no user account', function () {
|
|
246
|
+
$driver = new FleetOpsPasswordDriverFake();
|
|
247
|
+
$driver->userForTest = null;
|
|
248
|
+
FleetOpsPasswordControllerProbe::$driver = $driver;
|
|
249
|
+
|
|
250
|
+
$response = (new FleetOpsPasswordControllerProbe())->changePassword(
|
|
251
|
+
new Request(['current_password' => 'correct-horse', 'password' => 'newpassword']),
|
|
252
|
+
'driver_a'
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
expect($response->getStatusCode())->toBe(422);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('change password refuses when the current password is wrong, and changes nothing', function () {
|
|
259
|
+
/*
|
|
260
|
+
* The whole point of the endpoint. Without this check a borrowed handset or
|
|
261
|
+
* a leaked token is a permanent account takeover.
|
|
262
|
+
*/
|
|
263
|
+
$user = fleetopsPasswordUser('correct-horse');
|
|
264
|
+
$driver = new FleetOpsPasswordDriverFake();
|
|
265
|
+
$driver->userForTest = $user;
|
|
266
|
+
FleetOpsPasswordControllerProbe::$driver = $driver;
|
|
267
|
+
|
|
268
|
+
$response = (new FleetOpsPasswordControllerProbe())->changePassword(
|
|
269
|
+
new Request(['current_password' => 'guessing', 'password' => 'newpassword']),
|
|
270
|
+
'driver_a'
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
expect($response->getStatusCode())->toBe(422)
|
|
274
|
+
->and($user->saved)->toBeEmpty()
|
|
275
|
+
->and($user->tokensDeleted)->toBeFalse();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('change password sets it, ends other sessions, and hands back a fresh token', function () {
|
|
279
|
+
$user = fleetopsPasswordUser('correct-horse');
|
|
280
|
+
$driver = new FleetOpsPasswordDriverFake();
|
|
281
|
+
$driver->userForTest = $user;
|
|
282
|
+
FleetOpsPasswordControllerProbe::$driver = $driver;
|
|
283
|
+
|
|
284
|
+
$response = (new FleetOpsPasswordControllerProbe())->changePassword(
|
|
285
|
+
new Request(['current_password' => 'correct-horse', 'password' => 'newpassword', 'device_name' => 'navigator']),
|
|
286
|
+
'driver_a'
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
expect($response->getStatusCode())->toBe(200)
|
|
290
|
+
->and($response->getData()['token'])->toBe('fresh-token')
|
|
291
|
+
->and($user->saved)->toHaveCount(1)
|
|
292
|
+
// The old sessions die; the caller keeps working.
|
|
293
|
+
->and($user->tokensDeleted)->toBeTrue()
|
|
294
|
+
->and($user->issuedTokenFor)->toBe('navigator');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('forgot password requires an identity', function () {
|
|
298
|
+
$response = (new FleetOpsPasswordControllerProbe())->forgotPassword(new Request());
|
|
299
|
+
|
|
300
|
+
expect($response->getStatusCode())->toBe(400);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('forgot password answers the same for an unknown identity, and sends nothing', function () {
|
|
304
|
+
/*
|
|
305
|
+
* A reset endpoint that 404s on an unknown number is a way to enumerate a
|
|
306
|
+
* company's drivers.
|
|
307
|
+
*/
|
|
308
|
+
FleetOpsPasswordControllerProbe::$identityUser = null;
|
|
309
|
+
|
|
310
|
+
$response = (new FleetOpsPasswordControllerProbe())->forgotPassword(new Request(['identity' => 'nobody@example.test']));
|
|
311
|
+
|
|
312
|
+
expect($response->getStatusCode())->toBe(200)
|
|
313
|
+
->and(FleetOpsPasswordControllerProbe::$sentCodes)->toBeEmpty();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('forgot password sends a code to a driver that exists', function () {
|
|
317
|
+
FleetOpsPasswordControllerProbe::$identityUser = fleetopsPasswordUser();
|
|
318
|
+
|
|
319
|
+
$response = (new FleetOpsPasswordControllerProbe())->forgotPassword(new Request(['identity' => 'driver@example.test']));
|
|
320
|
+
|
|
321
|
+
expect($response->getStatusCode())->toBe(200)
|
|
322
|
+
->and(FleetOpsPasswordControllerProbe::$sentCodes)->toBe(['driver@example.test']);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('forgot password reports a delivery failure rather than pretending it sent', function () {
|
|
326
|
+
FleetOpsPasswordControllerProbe::$identityUser = fleetopsPasswordUser();
|
|
327
|
+
FleetOpsPasswordControllerProbe::$sendThrows = true;
|
|
328
|
+
|
|
329
|
+
$response = (new FleetOpsPasswordControllerProbe())->forgotPassword(new Request(['identity' => '+6581000001']));
|
|
330
|
+
|
|
331
|
+
expect($response->getStatusCode())->toBe(400);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('reset password requires identity, code and a new password', function () {
|
|
335
|
+
$response = (new FleetOpsPasswordControllerProbe())->resetPassword(new Request(['identity' => 'a@b.test']));
|
|
336
|
+
|
|
337
|
+
expect($response->getStatusCode())->toBe(400);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('reset password refuses a password too short to be worth having', function () {
|
|
341
|
+
$response = (new FleetOpsPasswordControllerProbe())->resetPassword(
|
|
342
|
+
new Request(['identity' => 'a@b.test', 'code' => '123456', 'password' => 'short'])
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
expect($response->getStatusCode())->toBe(400);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('reset password gives one answer for an unknown identity and a bad code alike', function () {
|
|
349
|
+
// Neither may be used as an oracle.
|
|
350
|
+
$unknownIdentity = (new FleetOpsPasswordControllerProbe())->resetPassword(
|
|
351
|
+
new Request(['identity' => 'nobody@example.test', 'code' => '123456', 'password' => 'newpassword'])
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
FleetOpsPasswordControllerProbe::$identityUser = fleetopsPasswordUser();
|
|
355
|
+
FleetOpsPasswordControllerProbe::$resetCode = null;
|
|
356
|
+
$badCode = (new FleetOpsPasswordControllerProbe())->resetPassword(
|
|
357
|
+
new Request(['identity' => 'driver@example.test', 'code' => 'wrong', 'password' => 'newpassword'])
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
expect($unknownIdentity->getStatusCode())->toBe(422)
|
|
361
|
+
->and($badCode->getStatusCode())->toBe(422)
|
|
362
|
+
->and($unknownIdentity->getData())->toBe($badCode->getData());
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test('reset password sets the password, spends the code, and ends every session', function () {
|
|
366
|
+
$user = fleetopsPasswordUser();
|
|
367
|
+
FleetOpsPasswordControllerProbe::$identityUser = $user;
|
|
368
|
+
|
|
369
|
+
$deleted = false;
|
|
370
|
+
FleetOpsPasswordControllerProbe::$resetCode = new class($deleted) {
|
|
371
|
+
public function __construct(public bool &$deleted)
|
|
372
|
+
{
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
public function delete(): bool
|
|
376
|
+
{
|
|
377
|
+
$this->deleted = true;
|
|
378
|
+
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
$response = (new FleetOpsPasswordControllerProbe())->resetPassword(
|
|
384
|
+
new Request(['identity' => 'driver@example.test', 'code' => '123456', 'password' => 'newpassword'])
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
expect($response->getStatusCode())->toBe(200)
|
|
388
|
+
->and($user->saved)->toHaveCount(1)
|
|
389
|
+
->and($deleted)->toBeTrue()
|
|
390
|
+
// A reset is a recovery from losing control; nothing keeps working.
|
|
391
|
+
->and($user->tokensDeleted)->toBeTrue();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Reaches the real helpers, which the probe above replaces.
|
|
396
|
+
*
|
|
397
|
+
* Each is a one-line delegation — to Eloquent, to the verification-code
|
|
398
|
+
* generator, to the hasher, to the application. What matters is that they
|
|
399
|
+
* delegate: a password comparison that quietly returned true, or a code sender
|
|
400
|
+
* that quietly did nothing, would each turn a security control into decoration.
|
|
401
|
+
*/
|
|
402
|
+
class FleetOpsPasswordRealHelperProbe extends DriverController
|
|
403
|
+
{
|
|
404
|
+
public static function callFindUser(string $identity)
|
|
405
|
+
{
|
|
406
|
+
return parent::findDriverUserByIdentity($identity);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
public static function callFindResetCode(User $user, string $code)
|
|
410
|
+
{
|
|
411
|
+
return parent::findResetCode($user, $code);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
public static function callSendResetCode(User $user, string $identity): void
|
|
415
|
+
{
|
|
416
|
+
parent::sendResetCode($user, $identity);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
public static function callPasswordMatches(User $user, string $plain): bool
|
|
420
|
+
{
|
|
421
|
+
return parent::passwordMatches($user, $plain);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
public static function callDebugEnabled(): bool
|
|
425
|
+
{
|
|
426
|
+
return parent::debugEnabled();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
test('driver password helpers delegate rather than deciding for themselves', function () {
|
|
431
|
+
/*
|
|
432
|
+
* Whether this environment has a database, a mailer or a hasher or not, the
|
|
433
|
+
* call must reach them. Either outcome is accepted; what is asserted is
|
|
434
|
+
* that these are real delegations and not stubs that would quietly approve
|
|
435
|
+
* a wrong password or silently drop a reset code.
|
|
436
|
+
*/
|
|
437
|
+
$user = fleetopsPasswordUser();
|
|
438
|
+
|
|
439
|
+
$reached = function (callable $call): bool {
|
|
440
|
+
try {
|
|
441
|
+
$call();
|
|
442
|
+
} catch (Throwable $e) {
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return true;
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
expect($reached(fn () => FleetOpsPasswordRealHelperProbe::callFindUser('driver@example.test')))->toBeTrue()
|
|
450
|
+
->and($reached(fn () => FleetOpsPasswordRealHelperProbe::callFindResetCode($user, '123456')))->toBeTrue()
|
|
451
|
+
// Both delivery branches: an identity that looks like an email, and one
|
|
452
|
+
// that looks like a phone number.
|
|
453
|
+
->and($reached(fn () => FleetOpsPasswordRealHelperProbe::callSendResetCode($user, 'driver@example.test')))->toBeTrue()
|
|
454
|
+
->and($reached(fn () => FleetOpsPasswordRealHelperProbe::callSendResetCode($user, '+6581000001')))->toBeTrue()
|
|
455
|
+
->and($reached(fn () => FleetOpsPasswordRealHelperProbe::callPasswordMatches($user, 'correct-horse')))->toBeTrue()
|
|
456
|
+
->and($reached(fn () => FleetOpsPasswordRealHelperProbe::callDebugEnabled()))->toBeTrue();
|
|
457
|
+
});
|
|
@@ -43,7 +43,7 @@ class FleetOpsApiEntityControllerProbe extends EntityController
|
|
|
43
43
|
return $value === 'contacts' ? 'contact' : null;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
protected function findPayloadByPublicId(string $publicId): ?Payload
|
|
46
|
+
protected function findPayloadByPublicId(?string $publicId): ?Payload
|
|
47
47
|
{
|
|
48
48
|
$this->payload?->setAttribute('lookup_id', $publicId);
|
|
49
49
|
|
|
@@ -26,7 +26,7 @@ class FleetOpsApiEquipmentControllerProbe extends EquipmentController
|
|
|
26
26
|
return $equipment;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
protected function resolveModel(string $modelClass, string $id): Illuminate\Database\Eloquent\Model
|
|
29
|
+
protected function resolveModel(string $modelClass, string $id, ?string $companyUuid = null): Illuminate\Database\Eloquent\Model
|
|
30
30
|
{
|
|
31
31
|
if ($this->equipmentNotFound) {
|
|
32
32
|
throw new ModelNotFoundException();
|
|
@@ -37,7 +37,7 @@ class FleetOpsApiEquipmentControllerProbe extends EquipmentController
|
|
|
37
37
|
return $this->equipment;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
protected function resolveUuid(string $modelClass, ?string $id): ?string
|
|
40
|
+
protected function resolveUuid(string $modelClass, ?string $id, ?string $companyUuid = null): ?string
|
|
41
41
|
{
|
|
42
42
|
$this->resolvedUuids[] = [$modelClass, $id];
|
|
43
43
|
|