@fleetbase/fleetops-engine 0.6.61 → 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 +23 -8
- package/addon/services/map-settings.js +4 -1
- package/composer.json +1 -1
- package/extension.json +1 -1
- package/package.json +1 -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 +93 -57
- 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/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 +134 -35
- package/server/src/Http/Controllers/Api/v1/ZoneController.php +1 -1
- 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/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 -31
- 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/Vehicle.php +46 -4
- package/server/src/Models/Driver.php +2 -2
- package/server/src/routes.php +8 -0
- package/server/tests/ApiDriverControllerContractsTest.php +284 -13
- 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/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 +175 -8
- 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/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/FleetHierarchyResourceTest.php +56 -8
- package/server/tests/RequestContractsTest.php +100 -7
- package/server/tests/Unit/Http/Resources/FleetResourceTest.php +50 -6
- package/server/tests/VehicleControllerHelperContractsTest.php +1 -1
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
use Fleetbase\FleetOps\Http\Controllers\Api\v1\FleetController;
|
|
4
|
+
use Fleetbase\FleetOps\Models\Driver;
|
|
5
|
+
use Fleetbase\FleetOps\Models\Fleet;
|
|
6
|
+
use Fleetbase\FleetOps\Models\FleetDriver;
|
|
7
|
+
use Fleetbase\FleetOps\Models\FleetVehicle;
|
|
8
|
+
use Fleetbase\FleetOps\Models\Vehicle;
|
|
9
|
+
use Illuminate\Database\ConnectionResolver;
|
|
10
|
+
use Illuminate\Database\Eloquent\Model as EloquentModel;
|
|
11
|
+
use Illuminate\Database\SQLiteConnection;
|
|
12
|
+
use Illuminate\Support\Facades\DB;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The membership pivots carry soft deletes, so "assign, remove, assign again"
|
|
16
|
+
* has to reach a real table to prove anything: the interesting cases are a row
|
|
17
|
+
* that already exists, a row that was removed and is being restored, and a
|
|
18
|
+
* removal of something that was never there. All three are invisible to a fake.
|
|
19
|
+
*/
|
|
20
|
+
function fleetopsMembershipDatabase(): SQLiteConnection
|
|
21
|
+
{
|
|
22
|
+
$connection = new SQLiteConnection(new PDO('sqlite::memory:'));
|
|
23
|
+
$resolver = new ConnectionResolver(['default' => $connection, 'mysql' => $connection]);
|
|
24
|
+
$resolver->setDefaultConnection('mysql');
|
|
25
|
+
EloquentModel::setConnectionResolver($resolver);
|
|
26
|
+
|
|
27
|
+
// A dispatcher is required, not optional: without one the uuid hooks never
|
|
28
|
+
// fire and — more importantly here — `FleetVehicle::creating()` silently
|
|
29
|
+
// registers nothing, so a race test would pass without ever racing.
|
|
30
|
+
// Memoised, because a fresh dispatcher drops the hooks of models already
|
|
31
|
+
// booted in this process.
|
|
32
|
+
if (!EloquentModel::getEventDispatcher()) {
|
|
33
|
+
EloquentModel::setEventDispatcher(new Illuminate\Events\Dispatcher());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// The dispatcher brings the lifecycle observers with it — the HTTP cache
|
|
37
|
+
// observer calls the `responsecache` binding on every create and delete —
|
|
38
|
+
// so satisfy it rather than letting an unrelated facade fail the assertion.
|
|
39
|
+
if (!app()->bound('responsecache')) {
|
|
40
|
+
app()->instance('responsecache', new class {
|
|
41
|
+
public function clear(): void
|
|
42
|
+
{
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public function forget($uris): void
|
|
46
|
+
{
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public function __call($method, $arguments)
|
|
50
|
+
{
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
config()->set('activitylog.enabled', false);
|
|
57
|
+
app()->bind(Illuminate\Contracts\Config\Repository::class, fn () => config());
|
|
58
|
+
|
|
59
|
+
app()->instance('db', new class($connection) {
|
|
60
|
+
public function __construct(public SQLiteConnection $c)
|
|
61
|
+
{
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public function connection($name = null): SQLiteConnection
|
|
65
|
+
{
|
|
66
|
+
return $this->c;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public function __call($method, $arguments)
|
|
70
|
+
{
|
|
71
|
+
return $this->c->{$method}(...$arguments);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
DB::clearResolvedInstance('db');
|
|
75
|
+
|
|
76
|
+
$schema = $connection->getSchemaBuilder();
|
|
77
|
+
|
|
78
|
+
foreach (['fleet_vehicles' => 'vehicle_uuid', 'fleet_drivers' => 'driver_uuid'] as $table => $subjectColumn) {
|
|
79
|
+
$schema->create($table, function ($blueprint) use ($table, $subjectColumn) {
|
|
80
|
+
$blueprint->increments('id');
|
|
81
|
+
$blueprint->string('uuid')->nullable();
|
|
82
|
+
$blueprint->string('_key')->nullable();
|
|
83
|
+
$blueprint->string('fleet_uuid')->nullable();
|
|
84
|
+
$blueprint->string($subjectColumn)->nullable();
|
|
85
|
+
$blueprint->timestamps();
|
|
86
|
+
$blueprint->timestamp('deleted_at')->nullable();
|
|
87
|
+
|
|
88
|
+
// The same composite index the migration adds. Soft-deleted rows are
|
|
89
|
+
// deliberately inside the key: a removed membership is restored on
|
|
90
|
+
// re-assignment rather than replaced, so its key must stay taken.
|
|
91
|
+
$blueprint->unique(['fleet_uuid', $subjectColumn], $table . '_pair_unique');
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return $connection;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function fleetopsMembershipController(): FleetController
|
|
99
|
+
{
|
|
100
|
+
return new FleetController();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function fleetopsMembershipInvoke(string $method, ...$arguments)
|
|
104
|
+
{
|
|
105
|
+
$controller = fleetopsMembershipController();
|
|
106
|
+
$reflection = new ReflectionMethod(FleetController::class, $method);
|
|
107
|
+
$reflection->setAccessible(true);
|
|
108
|
+
|
|
109
|
+
return $reflection->invoke($controller, ...$arguments);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function fleetopsMembershipFleet(string $uuid, string $publicId): Fleet
|
|
113
|
+
{
|
|
114
|
+
$fleet = new Fleet();
|
|
115
|
+
$fleet->setRawAttributes(['uuid' => $uuid, 'public_id' => $publicId], true);
|
|
116
|
+
|
|
117
|
+
return $fleet;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function fleetopsMembershipVehicle(string $uuid, string $publicId): Vehicle
|
|
121
|
+
{
|
|
122
|
+
$vehicle = new Vehicle();
|
|
123
|
+
$vehicle->setRawAttributes(['uuid' => $uuid, 'public_id' => $publicId], true);
|
|
124
|
+
|
|
125
|
+
return $vehicle;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function fleetopsMembershipDriver(string $uuid, string $publicId, ?string $vehicleUuid = null): Driver
|
|
129
|
+
{
|
|
130
|
+
$driver = new Driver();
|
|
131
|
+
$driver->setRawAttributes([
|
|
132
|
+
'uuid' => $uuid,
|
|
133
|
+
'public_id' => $publicId,
|
|
134
|
+
'vehicle_uuid' => $vehicleUuid,
|
|
135
|
+
], true);
|
|
136
|
+
|
|
137
|
+
return $driver;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
test('assigning a vehicle to a fleet is idempotent and creates no duplicate membership', function () {
|
|
141
|
+
$connection = fleetopsMembershipDatabase();
|
|
142
|
+
$fleet = fleetopsMembershipFleet('fleet-uuid-1', 'fleet_123');
|
|
143
|
+
$vehicle = fleetopsMembershipVehicle('vehicle-uuid-1', 'vehicle_123');
|
|
144
|
+
|
|
145
|
+
fleetopsMembershipInvoke('assignVehicleToFleet', $fleet, $vehicle);
|
|
146
|
+
fleetopsMembershipInvoke('assignVehicleToFleet', $fleet, $vehicle);
|
|
147
|
+
fleetopsMembershipInvoke('assignVehicleToFleet', $fleet, $vehicle);
|
|
148
|
+
|
|
149
|
+
$rows = $connection->table('fleet_vehicles')->get();
|
|
150
|
+
|
|
151
|
+
expect($rows)->toHaveCount(1)
|
|
152
|
+
->and($rows[0]->fleet_uuid)->toBe('fleet-uuid-1')
|
|
153
|
+
->and($rows[0]->vehicle_uuid)->toBe('vehicle-uuid-1')
|
|
154
|
+
->and($rows[0]->deleted_at)->toBeNull();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('removing a vehicle from a fleet is a safe no-op when repeated', function () {
|
|
158
|
+
$connection = fleetopsMembershipDatabase();
|
|
159
|
+
$fleet = fleetopsMembershipFleet('fleet-uuid-1', 'fleet_123');
|
|
160
|
+
$vehicle = fleetopsMembershipVehicle('vehicle-uuid-1', 'vehicle_123');
|
|
161
|
+
|
|
162
|
+
// Removing before anything was ever assigned must not raise.
|
|
163
|
+
fleetopsMembershipInvoke('removeVehicleFromFleet', $fleet, $vehicle);
|
|
164
|
+
|
|
165
|
+
fleetopsMembershipInvoke('assignVehicleToFleet', $fleet, $vehicle);
|
|
166
|
+
fleetopsMembershipInvoke('removeVehicleFromFleet', $fleet, $vehicle);
|
|
167
|
+
fleetopsMembershipInvoke('removeVehicleFromFleet', $fleet, $vehicle);
|
|
168
|
+
|
|
169
|
+
expect(FleetVehicle::where(['fleet_uuid' => 'fleet-uuid-1', 'vehicle_uuid' => 'vehicle-uuid-1'])->count())->toBe(0)
|
|
170
|
+
->and($connection->table('fleet_vehicles')->whereNotNull('deleted_at')->count())->toBe(1);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('a soft deleted vehicle membership is restored rather than duplicated', function () {
|
|
174
|
+
$connection = fleetopsMembershipDatabase();
|
|
175
|
+
$fleet = fleetopsMembershipFleet('fleet-uuid-1', 'fleet_123');
|
|
176
|
+
$vehicle = fleetopsMembershipVehicle('vehicle-uuid-1', 'vehicle_123');
|
|
177
|
+
|
|
178
|
+
fleetopsMembershipInvoke('assignVehicleToFleet', $fleet, $vehicle);
|
|
179
|
+
fleetopsMembershipInvoke('removeVehicleFromFleet', $fleet, $vehicle);
|
|
180
|
+
fleetopsMembershipInvoke('assignVehicleToFleet', $fleet, $vehicle);
|
|
181
|
+
|
|
182
|
+
// One row, live again — not a second row shadowing a tombstone.
|
|
183
|
+
expect($connection->table('fleet_vehicles')->count())->toBe(1)
|
|
184
|
+
->and(FleetVehicle::where(['fleet_uuid' => 'fleet-uuid-1', 'vehicle_uuid' => 'vehicle-uuid-1'])->count())->toBe(1);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('assigning a driver to a fleet is idempotent and restores a removed membership', function () {
|
|
188
|
+
$connection = fleetopsMembershipDatabase();
|
|
189
|
+
$fleet = fleetopsMembershipFleet('fleet-uuid-1', 'fleet_123');
|
|
190
|
+
$driver = fleetopsMembershipDriver('driver-uuid-1', 'driver_123');
|
|
191
|
+
|
|
192
|
+
fleetopsMembershipInvoke('assignDriverToFleet', $fleet, $driver);
|
|
193
|
+
fleetopsMembershipInvoke('assignDriverToFleet', $fleet, $driver);
|
|
194
|
+
fleetopsMembershipInvoke('removeDriverFromFleet', $fleet, $driver);
|
|
195
|
+
fleetopsMembershipInvoke('removeDriverFromFleet', $fleet, $driver);
|
|
196
|
+
fleetopsMembershipInvoke('assignDriverToFleet', $fleet, $driver);
|
|
197
|
+
|
|
198
|
+
expect($connection->table('fleet_drivers')->count())->toBe(1)
|
|
199
|
+
->and(FleetDriver::where(['fleet_uuid' => 'fleet-uuid-1', 'driver_uuid' => 'driver-uuid-1'])->count())->toBe(1);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('a fleet membership change leaves the driver its vehicle and its other fleets', function () {
|
|
203
|
+
$connection = fleetopsMembershipDatabase();
|
|
204
|
+
$driver = fleetopsMembershipDriver('driver-uuid-1', 'driver_123', 'vehicle-uuid-9');
|
|
205
|
+
$fleetOne = fleetopsMembershipFleet('fleet-uuid-1', 'fleet_one');
|
|
206
|
+
$fleetTwo = fleetopsMembershipFleet('fleet-uuid-2', 'fleet_two');
|
|
207
|
+
|
|
208
|
+
fleetopsMembershipInvoke('assignDriverToFleet', $fleetOne, $driver);
|
|
209
|
+
fleetopsMembershipInvoke('assignDriverToFleet', $fleetTwo, $driver);
|
|
210
|
+
fleetopsMembershipInvoke('removeDriverFromFleet', $fleetOne, $driver);
|
|
211
|
+
|
|
212
|
+
$live = FleetDriver::where('driver_uuid', 'driver-uuid-1')->pluck('fleet_uuid')->all();
|
|
213
|
+
|
|
214
|
+
expect($live)->toBe(['fleet-uuid-2'])
|
|
215
|
+
// The pivot is the only thing touched: the driver's current vehicle is
|
|
216
|
+
// untouched and the driver itself is still there.
|
|
217
|
+
->and($driver->vehicle_uuid)->toBe('vehicle-uuid-9')
|
|
218
|
+
->and($connection->table('fleet_drivers')->count())->toBe(2);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('a vehicle stays in its other fleets when removed from one', function () {
|
|
222
|
+
fleetopsMembershipDatabase();
|
|
223
|
+
$vehicle = fleetopsMembershipVehicle('vehicle-uuid-1', 'vehicle_123');
|
|
224
|
+
$fleetOne = fleetopsMembershipFleet('fleet-uuid-1', 'fleet_one');
|
|
225
|
+
$fleetTwo = fleetopsMembershipFleet('fleet-uuid-2', 'fleet_two');
|
|
226
|
+
|
|
227
|
+
fleetopsMembershipInvoke('assignVehicleToFleet', $fleetOne, $vehicle);
|
|
228
|
+
fleetopsMembershipInvoke('assignVehicleToFleet', $fleetTwo, $vehicle);
|
|
229
|
+
fleetopsMembershipInvoke('removeVehicleFromFleet', $fleetTwo, $vehicle);
|
|
230
|
+
|
|
231
|
+
expect(FleetVehicle::where('vehicle_uuid', 'vehicle-uuid-1')->pluck('fleet_uuid')->all())->toBe(['fleet-uuid-1']);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('the composite index refuses a duplicate membership pair outright', function () {
|
|
235
|
+
$connection = fleetopsMembershipDatabase();
|
|
236
|
+
|
|
237
|
+
$connection->table('fleet_vehicles')->insert([
|
|
238
|
+
'uuid' => 'pivot-1',
|
|
239
|
+
'fleet_uuid' => 'fleet-uuid-1',
|
|
240
|
+
'vehicle_uuid' => 'vehicle-uuid-1',
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
// The controller's read-then-write is idempotent only against itself. This
|
|
244
|
+
// is the guarantee underneath it: even a direct insert cannot produce a
|
|
245
|
+
// second logical membership.
|
|
246
|
+
expect(fn () => $connection->table('fleet_vehicles')->insert([
|
|
247
|
+
'uuid' => 'pivot-2',
|
|
248
|
+
'fleet_uuid' => 'fleet-uuid-1',
|
|
249
|
+
'vehicle_uuid' => 'vehicle-uuid-1',
|
|
250
|
+
]))->toThrow(Illuminate\Database\UniqueConstraintViolationException::class);
|
|
251
|
+
|
|
252
|
+
// A tombstone still occupies the key, which is what forces the restore path.
|
|
253
|
+
$connection->table('fleet_vehicles')->where('uuid', 'pivot-1')->update(['deleted_at' => now()]);
|
|
254
|
+
|
|
255
|
+
expect(fn () => $connection->table('fleet_vehicles')->insert([
|
|
256
|
+
'uuid' => 'pivot-3',
|
|
257
|
+
'fleet_uuid' => 'fleet-uuid-1',
|
|
258
|
+
'vehicle_uuid' => 'vehicle-uuid-1',
|
|
259
|
+
]))->toThrow(Illuminate\Database\UniqueConstraintViolationException::class);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('a competing request that wins the race is adopted rather than reported as an error', function () {
|
|
263
|
+
$connection = fleetopsMembershipDatabase();
|
|
264
|
+
$fleet = fleetopsMembershipFleet('fleet-uuid-1', 'fleet_123');
|
|
265
|
+
$vehicle = fleetopsMembershipVehicle('vehicle-uuid-1', 'vehicle_123');
|
|
266
|
+
|
|
267
|
+
// The row appears between this request reading an empty table and writing to
|
|
268
|
+
// it — the interleaving a retrying importer actually produces. Inserted
|
|
269
|
+
// through the query builder so no model event recurses.
|
|
270
|
+
$inserted = false;
|
|
271
|
+
FleetVehicle::creating(function () use ($connection, &$inserted) {
|
|
272
|
+
if ($inserted) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
$inserted = true;
|
|
277
|
+
$connection->table('fleet_vehicles')->insert([
|
|
278
|
+
'uuid' => 'winner',
|
|
279
|
+
'fleet_uuid' => 'fleet-uuid-1',
|
|
280
|
+
'vehicle_uuid' => 'vehicle-uuid-1',
|
|
281
|
+
'deleted_at' => now(),
|
|
282
|
+
]);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
fleetopsMembershipInvoke('assignVehicleToFleet', $fleet, $vehicle);
|
|
287
|
+
} finally {
|
|
288
|
+
FleetVehicle::flushEventListeners();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// The loser adopts the winner's row and restores it, so both callers get the
|
|
292
|
+
// same successful answer and there is still exactly one membership.
|
|
293
|
+
expect($connection->table('fleet_vehicles')->count())->toBe(1)
|
|
294
|
+
->and(FleetVehicle::where([
|
|
295
|
+
'fleet_uuid' => 'fleet-uuid-1',
|
|
296
|
+
'vehicle_uuid' => 'vehicle-uuid-1',
|
|
297
|
+
])->count())->toBe(1);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('a duplicate key that is not this membership is not swallowed', function () {
|
|
301
|
+
$connection = fleetopsMembershipDatabase();
|
|
302
|
+
$connection->statement('create unique index fleet_vehicles_uuid_unique on fleet_vehicles (uuid)');
|
|
303
|
+
|
|
304
|
+
$fleet = fleetopsMembershipFleet('fleet-uuid-1', 'fleet_123');
|
|
305
|
+
$vehicle = fleetopsMembershipVehicle('vehicle-uuid-1', 'vehicle_123');
|
|
306
|
+
|
|
307
|
+
// A violation on a different key means something else is wrong. Reporting it
|
|
308
|
+
// as a successful assignment would hide a real failure behind a 200.
|
|
309
|
+
$collided = false;
|
|
310
|
+
FleetVehicle::creating(function ($membership) use ($connection, &$collided) {
|
|
311
|
+
if ($collided) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
$collided = true;
|
|
316
|
+
|
|
317
|
+
// Pin the uuid and take it first, so the save collides on the uuid index
|
|
318
|
+
// rather than on the membership pair.
|
|
319
|
+
$membership->uuid = 'collides-on-uuid';
|
|
320
|
+
$connection->table('fleet_vehicles')->insert([
|
|
321
|
+
'uuid' => 'collides-on-uuid',
|
|
322
|
+
'fleet_uuid' => 'fleet-uuid-9',
|
|
323
|
+
'vehicle_uuid' => 'vehicle-uuid-9',
|
|
324
|
+
]);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
expect(fn () => fleetopsMembershipInvoke('assignVehicleToFleet', $fleet, $vehicle))
|
|
329
|
+
->toThrow(Illuminate\Database\UniqueConstraintViolationException::class);
|
|
330
|
+
} finally {
|
|
331
|
+
FleetVehicle::flushEventListeners();
|
|
332
|
+
}
|
|
333
|
+
});
|