From 4b5828ca53b3dc314bb0903444c9c7695afd27c3 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Wed, 14 Oct 2026 13:48:14 +0800 Subject: [PATCH 1/2] test: keep the SMS guard off in the harness and give each code lookup its own row Against core-api 1.6.69 the suite hit the real guard with its counters in the shared cache: a second text to the same number inside a file tripped the 60-second cooldown, so driver login tests got a 429 refusal where they expect the transport failure or the refusal they staged themselves. The bootstrap now seeds sms.guard.enabled = false, as core-api's and storefront's do; the tests that exercise refusals raise them from the provider fake. The helper-seams test reused one code for two lookups and omitted the purpose; a code is used up by the lookup that accepts it and the live-code query is per purpose, so each check gets its own row and carries 'for'. --- scripts/pest-bootstrap.php | 4 ++++ .../Http/Api/CustomerControllerHelperSeamsTest.php | 8 +++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/pest-bootstrap.php b/scripts/pest-bootstrap.php index 6ea5bf5ed..95b00ad3a 100644 --- a/scripts/pest-bootstrap.php +++ b/scripts/pest-bootstrap.php @@ -53,6 +53,10 @@ function config(string|array|null $key = null, mixed $default = null): mixed 'fleetops' => [], 'services' => [], 'telematics' => [], + // The core SMS guard is off unless a test turns it on: its counters live in the + // shared cache, so one file's sends would otherwise rate-limit the next's, and + // a second text to the same number inside a test trips its cooldown. + 'sms' => ['guard' => ['enabled' => false]], ])); } diff --git a/server/tests/Feature/Http/Api/CustomerControllerHelperSeamsTest.php b/server/tests/Feature/Http/Api/CustomerControllerHelperSeamsTest.php index bd034d946..d886630d2 100644 --- a/server/tests/Feature/Http/Api/CustomerControllerHelperSeamsTest.php +++ b/server/tests/Feature/Http/Api/CustomerControllerHelperSeamsTest.php @@ -452,14 +452,16 @@ public function parameters() test('verification codes files contacts and customer auth resolve', function () { $connection = fleetopsCustomerHelperBoot(); $connection->table('verification_codes')->insert(['uuid' => 'vc-1', 'subject_uuid' => 'user-1', 'subject_type' => 'user', 'code' => '123456', 'for' => 'customer_verification']); + // A code is used up by the lookup that accepts it, so each check gets its own row. + $connection->table('verification_codes')->insert(['uuid' => 'vc-2', 'subject_uuid' => 'user-1', 'subject_type' => 'user', 'code' => '654321', 'for' => 'customer_verification']); $connection->table('files')->insert(['uuid' => 'file-1', 'public_id' => 'file_custhelp1', 'company_uuid' => 'company-1', 'name' => 'photo.png']); $connection->table('contacts')->insert(['uuid' => 'contact-1', 'public_id' => 'contact_custhelp1', 'company_uuid' => 'company-1', 'name' => 'Casey', 'type' => 'customer']); $probe = new FleetOpsCustomerControllerProbe(); - expect($probe->callHelper('verificationCodeExists', ['code' => '123456', 'for' => 'customer_verification']))->toBeTrue() - ->and($probe->callHelper('findVerificationCode', ['code' => '123456'])?->uuid)->toBe('vc-1') - ->and($probe->callHelper('findVerificationCode', ['code' => '999999']))->toBeNull() + expect($probe->callHelper('verificationCodeExists', ['code' => '654321', 'for' => 'customer_verification']))->toBeTrue() + ->and($probe->callHelper('findVerificationCode', ['code' => '123456', 'for' => 'customer_verification'])?->uuid)->toBe('vc-1') + ->and($probe->callHelper('findVerificationCode', ['code' => '999999', 'for' => 'customer_verification']))->toBeNull() ->and($probe->callHelper('findFileByPublicId', 'file_custhelp1')?->uuid)->toBe('file-1'); expect($probe->callHelper('findCustomerContact', ['uuid' => 'contact-1'])?->name)->toBe('Casey') From 10a5d9167016aeea6bc4028841fd9207a84a8108 Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Wed, 14 Oct 2026 14:56:30 +0800 Subject: [PATCH 2/2] test: cover the order operations branches the coverage gate flagged The gate never ran before: the suite failed first. With the tests passing it reports 57 statements short across the order edit, its event, notifications, policy, tracking publisher, payload sync and the controllers that begin and refuse edits. This covers them: an edit's policy, marked changes, schedule, cash, contact and item detection with the kitchen flag, orders with no or an unsaved payload, the summary's fallbacks; order.changed's customer channel, session editor, missing driver and null place; push builders reaching for the FCM and APN clients; a config that cannot answer, cannot load, and times that do not parse; route changes queued for tracking; caching switched off; the controllers' begin, locked-edit and switched-payload cash paths; replaced drivers told on bulk assignment; and the entity sync's non-array inputs. The older-core branch in ApiCacheThrottle::telemetryWrite() cannot run against the core this package requires and is marked for coverage with the reason. --- server/src/Support/ApiCacheThrottle.php | 4 + .../tests/ApiOrderControllerContractsTest.php | 26 ++++ .../Internal/OrderControllerHelpersTest.php | 12 +- .../OrderChangeNotificationsTest.php | 7 + .../Unit/Support/ApiCacheDependenciesTest.php | 10 ++ .../Unit/Support/OrderEditPolicyTest.php | 28 ++++ .../Unit/Support/OrderOperationsTest.php | 129 +++++++++++++++++- .../Unit/Support/TrackingPublisherTest.php | 6 +- 8 files changed, 213 insertions(+), 9 deletions(-) diff --git a/server/src/Support/ApiCacheThrottle.php b/server/src/Support/ApiCacheThrottle.php index 65b571bf9..d98f542c1 100644 --- a/server/src/Support/ApiCacheThrottle.php +++ b/server/src/Support/ApiCacheThrottle.php @@ -39,9 +39,13 @@ class ApiCacheThrottle */ public static function telemetryWrite(Model $model, \Closure $write, int $seconds = self::LOCATION_WINDOW): mixed { + // @codeCoverageIgnoreStart + // fleetbase/core-api ^1.6.69, which this package requires, always has it; the + // branch only serves an install that pinned an older core by hand. if (!method_exists(ApiModelCache::class, 'withoutInvalidating')) { return $write(); } + // @codeCoverageIgnoreEnd $result = ApiModelCache::withoutInvalidating($write); static::invalidate($model, $model->company_uuid, $seconds); diff --git a/server/tests/ApiOrderControllerContractsTest.php b/server/tests/ApiOrderControllerContractsTest.php index 5b87e94d8..228827ff1 100644 --- a/server/tests/ApiOrderControllerContractsTest.php +++ b/server/tests/ApiOrderControllerContractsTest.php @@ -59,6 +59,13 @@ protected function findOrder(string $id, array $with = [], array $withCount = [] return $this->order; } + public ?FleetOpsApiOrderPayloadFake $switchedPayload = null; + + protected function findPayloadByUuid(?string $uuid): ?Payload + { + return $this->switchedPayload; + } + protected function beginOrderEdit(Order $order): OrderEdit { $this->edit ??= new FleetOpsApiOrderEditFake($order); @@ -825,6 +832,25 @@ public function isString(string $key): bool ->and($payload->removedWaypoints)->toBeTrue(); }); +test('api order controller moves the cash to collect onto the payload it switches to by id', function () { + $order = new FleetOpsApiOrderCrudFake(); + $order->payload = new FleetOpsApiOrderPayloadFake(); + $switched = new FleetOpsApiOrderPayloadFake(); + + $controller = new FleetOpsApiOrderCrudControllerProbe(); + $controller->order = $order; + $controller->switchedPayload = $switched; + + $controller->update('order-public', new FleetOpsApiOrderUpdateRequestFake([ + 'payload' => 'payload_public', + 'cod_amount' => '1800', + ])); + + expect($order->updates[0])->toMatchArray(['payload_uuid' => 'payloads-uuid']) + ->and($switched->getAttributes()['cod_amount'])->toBe(1800) + ->and($switched->savedForTest)->toBeTrue(); +}); + test('api order controller finds deletes and updates distance matrices without database records', function () { $order = new FleetOpsApiOrderCrudFake(); $controller = new FleetOpsApiOrderCrudControllerProbe(); diff --git a/server/tests/Feature/Http/Internal/OrderControllerHelpersTest.php b/server/tests/Feature/Http/Internal/OrderControllerHelpersTest.php index 1760fbc3f..f0b15b9a6 100644 --- a/server/tests/Feature/Http/Internal/OrderControllerHelpersTest.php +++ b/server/tests/Feature/Http/Internal/OrderControllerHelpersTest.php @@ -157,18 +157,20 @@ public function __call($method, $arguments) expect($valid['driver'])->toBe('22222222-2222-4222-8222-222222222222'); }); -test('assign driver to orders updates the assignment column', function () { +test('assign driver to orders updates the assignment column and tells the drivers it replaces', function () { $connection = fleetopsInternalOrderHelpersBoot(); $connection->table('orders')->insert([ - ['uuid' => 'order-1', 'company_uuid' => 'company-1'], - ['uuid' => 'order-2', 'company_uuid' => 'company-1'], + ['uuid' => 'order-1', 'company_uuid' => 'company-1', 'driver_assigned_uuid' => null], + ['uuid' => 'order-2', 'company_uuid' => 'company-1', 'driver_assigned_uuid' => null], + // already with another driver, who no longer exists: nothing to tell, nothing to fail + ['uuid' => 'order-3', 'company_uuid' => 'company-1', 'driver_assigned_uuid' => 'driver-gone'], ]); $driver = new Driver(); $driver->setRawAttributes(['uuid' => 'driver-1'], true); - (new FleetOpsInternalOrderHelpersProbe())->callProtected('assignDriverToOrders', ['order-1', 'order-2'], $driver); + (new FleetOpsInternalOrderHelpersProbe())->callProtected('assignDriverToOrders', ['order-1', 'order-2', 'order-3'], $driver); - expect($connection->table('orders')->where('driver_assigned_uuid', 'driver-1')->count())->toBe(2); + expect($connection->table('orders')->where('driver_assigned_uuid', 'driver-1')->count())->toBe(3); }); test('dispatch domain event helper emits and returns the event', function () { diff --git a/server/tests/Unit/Notifications/OrderChangeNotificationsTest.php b/server/tests/Unit/Notifications/OrderChangeNotificationsTest.php index ae9005236..7fbd15f48 100644 --- a/server/tests/Unit/Notifications/OrderChangeNotificationsTest.php +++ b/server/tests/Unit/Notifications/OrderChangeNotificationsTest.php @@ -40,6 +40,10 @@ function fleetopsOrderChangeNotificationOrder(): Order ->and($notification->toArray())->toMatchArray(['event' => 'order.changed_notification', 'data' => $notification->data]) ->and(array_map(fn ($channel) => $channel->name, $notification->broadcastOn()))->toContain('driver.driver-uuid') ->and((new OrderChanged(fleetopsOrderChangeNotificationOrder()))->message)->toBe('This order was changed, tap for details.'); + + // The push builders reach for the FCM and APN clients, which the harness does not configure + expect(fn () => $notification->toFcm(null))->toThrow(Exception::class) + ->and(fn () => $notification->toApn(null))->toThrow(Error::class); }); test('order unassigned tells the driver the order is no longer theirs', function () { @@ -49,4 +53,7 @@ function fleetopsOrderChangeNotificationOrder(): Order ->and($notification->data)->toBe(['type' => 'order_unassigned', 'id' => 'order_change']) ->and($notification->via(null))->toBe([FcmChannel::class, ApnChannel::class]) ->and($notification->toArray()['event'])->toBe('order.unassigned_notification'); + + expect(fn () => $notification->toFcm(null))->toThrow(Exception::class) + ->and(fn () => $notification->toApn(null))->toThrow(Error::class); }); diff --git a/server/tests/Unit/Support/ApiCacheDependenciesTest.php b/server/tests/Unit/Support/ApiCacheDependenciesTest.php index d721343e0..94135bfca 100644 --- a/server/tests/Unit/Support/ApiCacheDependenciesTest.php +++ b/server/tests/Unit/Support/ApiCacheDependenciesTest.php @@ -296,6 +296,16 @@ public function log(string $description): ?Spatie\Activitylog\Contracts\Activity app()->forgetInstance(Spatie\Activitylog\ActivityLogStatus::class); }); +test('throttled invalidation does nothing while api caching is off', function () { + config(['api.cache.enabled' => false]); + + ApiCacheThrottle::invalidate(new Vehicle(), 'co-1'); + + config(['api.cache.enabled' => true]); + + expect(apiCacheQueryVersion('vehicles', 'co-1'))->toBe(0); +}); + test('a cache outage never fails a throttled location invalidation', function () { Cache::swap(new Repository(new class extends ArrayStore { public function add($key, $value, $seconds) diff --git a/server/tests/Unit/Support/OrderEditPolicyTest.php b/server/tests/Unit/Support/OrderEditPolicyTest.php index 8153865e0..802883f23 100644 --- a/server/tests/Unit/Support/OrderEditPolicyTest.php +++ b/server/tests/Unit/Support/OrderEditPolicyTest.php @@ -126,3 +126,31 @@ function fleetopsEditPolicyOrder(string $status, array $attributes = [], ?OrderC ->and((array) $serialized['restrictions'])->toBe(['items' => 'removal_only', 'schedule' => 'earlier_only']) ->and(fleetopsEditPolicyOrder('ready')->editPolicy())->toBeInstanceOf(OrderEditPolicy::class); }); + +class FleetOpsEditPolicyBrokenConfig extends OrderConfig +{ + public function isTerminalActivityCode(?string $code): bool + { + throw new RuntimeException('activities unavailable'); + } +} + +class FleetOpsEditPolicyOrderWithoutDatabase extends Order +{ + public function orderConfig(): Illuminate\Database\Eloquent\Relations\BelongsTo + { + throw new RuntimeException('no database'); + } +} + +test('a config that cannot answer, a config that cannot load and times that do not parse all fail safe', function () { + $brokenConfig = OrderEditPolicy::for(fleetopsEditPolicyOrder('custom_status', [], new FleetOpsEditPolicyBrokenConfig())); + + $withoutDatabase = new FleetOpsEditPolicyOrderWithoutDatabase(); + $withoutDatabase->setRawAttributes(['uuid' => 'order-uuid', 'public_id' => 'order_policy', 'status' => 'custom_status'], true); + + expect($brokenConfig->isTerminal())->toBeFalse() + ->and(OrderEditPolicy::for($withoutDatabase)->isTerminal())->toBeFalse() + // "earlier only" cannot be judged from times that do not parse, so the move is refused + ->and(OrderEditPolicy::for(fleetopsEditPolicyOrder('ready'))->check('schedule', ['from' => 'soon', 'to' => 'later']))->toBeString(); +}); diff --git a/server/tests/Unit/Support/OrderOperationsTest.php b/server/tests/Unit/Support/OrderOperationsTest.php index 5ac197271..b84999e24 100644 --- a/server/tests/Unit/Support/OrderOperationsTest.php +++ b/server/tests/Unit/Support/OrderOperationsTest.php @@ -16,6 +16,8 @@ function event(...$arguments) } use Fleetbase\FleetOps\Events\OrderChanged; +use Fleetbase\FleetOps\Exceptions\OrderEditLockedException; +use Fleetbase\FleetOps\Models\Contact; use Fleetbase\FleetOps\Models\Driver; use Fleetbase\FleetOps\Models\Entity; use Fleetbase\FleetOps\Models\Order; @@ -73,6 +75,30 @@ public function listen($events, $listener = null) } } +class FleetOpsOrderOpsPayloadControllerProbe extends Fleetbase\FleetOps\Http\Controllers\Api\v1\PayloadController +{ + public function edit(Order $order): OrderEdit + { + return $this->beginOrderEdit($order); + } +} + +class FleetOpsOrderOpsEntityControllerProbe extends Fleetbase\FleetOps\Http\Controllers\Api\v1\EntityController +{ + public function editFor(?string $payloadUuid): ?OrderEdit + { + return $this->orderEditForPayload($payloadUuid); + } +} + +class FleetOpsOrderOpsApiOrderControllerProbe extends Fleetbase\FleetOps\Http\Controllers\Api\v1\OrderController +{ + public function locked(OrderEditLockedException $exception): mixed + { + return $this->lockedEditResponse($exception); + } +} + class FleetOpsOrderOpsDriver extends Driver { public array $notified = []; @@ -111,7 +137,7 @@ public function __call($method, $arguments) } }); Illuminate\Support\Facades\DB::clearResolvedInstance('db'); - class_exists(Fleetbase\FleetOps\Models\Contact::class); + class_exists(Contact::class); foreach (['DNS2D', 'DNS1D'] as $barcode) { app()->instance($barcode, new class { @@ -222,7 +248,11 @@ function fleetopsOrderOpsOrder(SQLiteConnection $connection, array $order = [], ->and($result['updated'])->toBe([$latte->uuid]) ->and($result['deleted'])->toBe([$muffin->uuid]) ->and($result['created'])->toHaveCount(1) - ->and($payload->syncEntities([['uuid' => $latte->uuid, 'name' => 'Latte', 'price' => '500']])['updated'])->toBe([]); + ->and($payload->syncEntities([['uuid' => $latte->uuid, 'name' => 'Latte', 'price' => '500']])['updated'])->toBe([]) + // entries that are not arrays are skipped, and a list that is not an array changes nothing + ->and($payload->syncEntities([['uuid' => $latte->uuid, 'name' => 'Latte', 'price' => '500'], 'junk'])['deleted'])->toBe([]) + ->and($payload->syncEntities('junk'))->toBe(['created' => [], 'updated' => [], 'deleted' => []]) + ->and(Entity::where('payload_uuid', 'payload-1')->count())->toBe(1); }); test('route signatures report changed parts and a not started order restarts at the first stop', function () { @@ -369,7 +399,7 @@ function fleetopsOrderOpsOrder(SQLiteConnection $connection, array $order = [], $edit->guard($edit->requestedChanges(['entities' => [['id' => $latte->public_id]], 'scheduled_at' => '2026-10-13 11:00:00'], OrderEdit::ENTITIES_REPLACE)); expect(fn () => $edit->guard($edit->requestedChanges(['entities' => [['name' => 'Cake']]]))) - ->toThrow(Fleetbase\FleetOps\Exceptions\OrderEditLockedException::class, 'Once the order is ready, items can only be removed.') + ->toThrow(OrderEditLockedException::class, 'Once the order is ready, items can only be removed.') ->and(OrderEdit::entitiesMode('replace'))->toBe('replace') ->and(OrderEdit::entitiesMode('sync'))->toBe('replace') ->and(OrderEdit::entitiesMode(null))->toBe('append'); @@ -387,5 +417,98 @@ function fleetopsOrderOpsOrder(SQLiteConnection $connection, array $order = [], expect($events)->toHaveCount(1) ->and($events[0]->broadcastAs())->toBe('order.driver_unassigned') ->and($events[0]->channelNames)->toBe(['driver.driver_one', 'driver.driver-1', 'company.company-1']) + ->and(array_map(fn ($channel) => $channel->name, $events[0]->broadcastOn()))->toBe($events[0]->channelNames) ->and($events[0]->broadcastWith()['data'])->toBe(['id' => 'order_one', 'driver' => 'driver_one']); }); + +test('an edit exposes its policy and detects schedule cash contact and item changes, flagging the kitchen while preparing', function () { + $connection = fleetopsOrderOpsBoot(); + $connection->table('contacts')->insert(['uuid' => 'contact-1', 'public_id' => 'contact_one', 'company_uuid' => 'company-1', 'name' => 'Ada', 'type' => 'customer']); + $order = fleetopsOrderOpsOrder($connection, [ + 'status' => 'preparing', + 'scheduled_at' => '2026-10-13 12:00:00', + 'notes' => 'Same', + 'customer_uuid' => 'contact-1', + 'customer_type' => Contact::class, + ], ['cod_amount' => '0']); + $edit = OrderEdit::begin($order); + + expect($edit->policy())->toBeInstanceOf(Fleetbase\FleetOps\Support\OrderEditPolicy::class) + ->and($edit->requestedChanges([ + // a time that does not parse is kept as given, and still counts as a change + 'scheduled_at' => 'not a date', + 'notes' => 'Ring twice', + 'cod_amount' => '12.50', + 'customer' => 'contact_two', + ]))->toBe([ + 'schedule' => ['from' => '2026-10-13T12:00:00Z', 'to' => 'not a date'], + 'notes' => true, + 'cod_amount' => true, + 'contact' => true, + ]) + // the customer already on the order, by public id, is not a change; anything but a public id is + ->and($edit->requestedChanges(['customer' => 'contact_one']))->toBe([]) + ->and($edit->requestedChanges(['customer' => 'contact-1']))->toBe([]) + ->and($edit->requestedChanges(['customer' => ['id' => 'contact_one']]))->toBe(['contact' => true]); + + $edit = OrderEdit::begin($order); + $edit->markChanged('total', 'total'); + $connection->table('payloads')->where('uuid', 'payload-1')->update(['cod_amount' => '900']); + $order->payload->setEntities([['name' => 'Latte']]); + $order->scheduled_at = '2026-10-14 09:30:00'; + $order->customer_uuid = 'contact-2'; + $order->save(); + + expect($edit->commit())->toBe(['items', 'cod_amount', 'schedule', 'contact', 'total']) + ->and(fleetopsOrderOpsEvents()->of(OrderChanged::class)[0]->data['summary'])->toContain('Rescheduled to 14 Oct 09:30') + ->and(OrderEdit::summarize(['pickup', 'schedule'], null, null))->toBe('Pickup changed ยท Schedule cleared') + ->and(OrderEdit::summarize(['service_type']))->toBe('Service type changed'); +}); + +test('an order with no payload, or an unsaved one, commits without touching the database', function () { + $connection = fleetopsOrderOpsBoot(); + $connection->table('orders')->insert(['uuid' => 'order-2', 'public_id' => 'order_two', 'company_uuid' => 'company-1', 'status' => 'created']); + + $bare = Order::where('uuid', 'order-2')->first(); + expect(OrderEdit::begin($bare)->commit())->toBe([]); + + $unsaved = Order::where('uuid', 'order-2')->first(); + $unsaved->setRelation('payload', new Payload()); + expect(OrderEdit::begin($unsaved)->commit())->toBe([]) + ->and(fleetopsOrderOpsEvents()->of(OrderChanged::class))->toBe([]); +}); + +test('order changed names the customer channel, reads the editor from the session and copes with a driver that is gone', function () { + $connection = fleetopsOrderOpsBoot(); + $connection->table('contacts')->insert(['uuid' => 'contact-1', 'public_id' => 'contact_one', 'company_uuid' => 'company-1', 'name' => 'Ada', 'type' => 'customer']); + $order = fleetopsOrderOpsOrder($connection, ['driver_assigned_uuid' => 'ghost-driver', 'customer_uuid' => 'contact-1', 'customer_type' => Contact::class]); + // No users table here: the editor's name lookup fails quietly + session(['user' => 'user-1']); + + $event = OrderChanged::dispatchFor($order, ['dropoff'], 'Drop-off changed'); + session(['user' => null]); + + $noDriver = new Order(); + $noDriver->setRawAttributes(['uuid' => 'order-9', 'public_id' => 'order_nine'], true); + + expect($event->channelNames)->toBe(['order.order_one', 'order.order-1', 'driver.ghost-driver', 'customer.contact_one', 'company.company-1']) + ->and(array_map(fn ($channel) => $channel->name, $event->broadcastOn()))->toBe($event->channelNames) + ->and($event->data['changed_by'])->toBeNull() + ->and(OrderChanged::notifyDriver($order, $event))->toBeFalse() + ->and(OrderChanged::assignedDriver($noDriver))->toBeNull() + ->and(OrderChanged::placeData(null))->toBeNull(); +}); + +test('the api controllers begin an order edit for a payload and answer a locked edit with 422', function () { + $connection = fleetopsOrderOpsBoot(); + $order = fleetopsOrderOpsOrder($connection); + + $response = (new FleetOpsOrderOpsApiOrderControllerProbe())->locked(new OrderEditLockedException(['items' => 'Items cannot be changed once a driver has the order.'])); + + expect((new FleetOpsOrderOpsPayloadControllerProbe())->edit($order))->toBeInstanceOf(OrderEdit::class) + ->and((new FleetOpsOrderOpsEntityControllerProbe())->editFor('payload-1'))->toBeInstanceOf(OrderEdit::class) + ->and((new FleetOpsOrderOpsEntityControllerProbe())->editFor('payload-missing'))->toBeNull() + ->and((new FleetOpsOrderOpsEntityControllerProbe())->editFor(null))->toBeNull() + ->and($response->getStatusCode())->toBe(422) + ->and($response->getData(true)['locked'])->toBe(['items' => 'Items cannot be changed once a driver has the order.']); +}); diff --git a/server/tests/Unit/Support/TrackingPublisherTest.php b/server/tests/Unit/Support/TrackingPublisherTest.php index b588b462a..1ef383f22 100644 --- a/server/tests/Unit/Support/TrackingPublisherTest.php +++ b/server/tests/Unit/Support/TrackingPublisherTest.php @@ -267,12 +267,16 @@ function trackingPublisherUseStops(Collection $stops, array|Throwable $tracker = $unsaved = trackingPublisherOrder(['uuid' => null]); $unsaved->status = 'started'; $unsaved->syncChanges(); - expect($publisher->orderChanged($unsaved))->toBeFalse(); + expect($publisher->orderChanged($unsaved))->toBeFalse() + // an edited route queues its own update; an unsaved order has nothing to publish + ->and($publisher->routeChanged($order))->toBeTrue() + ->and($publisher->routeChanged($unsaved))->toBeFalse(); expect(array_map(fn ($dispatch) => [$dispatch['job'], ...$dispatch['arguments']], DispatchRecorder::$dispatched))->toBe([ [PublishTrackingUpdate::class, 'order-started', 'eta', 'mysql'], [PublishTrackingUpdate::class, 'order-started', 'assignment', 'mysql'], [PublishTrackingUpdate::class, 'order-started', 'status', 'mysql'], + [PublishTrackingUpdate::class, 'order-started', 'route', 'mysql'], ]); });