getProperty('cachedDhlConfig'); $property->setAccessible(true); $property->setValue(null, [ 'api_key' => 'cached-test-api-key-1234', 'use_queue' => false, ]); DhlTrackingService::clearQuotaPause(); DhlTrackingService::setCallIntervalSeconds(0); }); afterEach(function () { SettingController::flushDhlConfigCache(); DhlTrackingService::clearQuotaPause(); DhlTrackingService::setCallIntervalSeconds(5); Mockery::close(); }); /** * @return DhlShipment&\Mockery\MockInterface */ function makeFakeShipment(int $id, string $trackingNumber, ?Carbon\Carbon $lastTrackedAt = null) { /** @var DhlShipment&\Mockery\MockInterface $shipment */ $shipment = Mockery::mock(DhlShipment::class)->makePartial(); $shipment->id = $id; $shipment->dhl_shipment_no = $trackingNumber; $shipment->last_tracked_at = $lastTrackedAt; return $shipment; } it('does not update last_tracked_at when the Unified API returns 401', function () { Http::fake([ 'api-eu.dhl.com/track/shipments*' => Http::response([], 401), ]); $shipment = makeFakeShipment(123, '00340434292135100148', now()->subDays(2)); // The sync path must never call ->update() on the shipment when an auth // error occurs. Asserting "never called" is the strongest contract we // can express here without a database. $shipment->shouldNotReceive('update'); $service = new DhlTrackingService; $result = $service->updateTracking($shipment, ['auto_retrack' => false]); expect($result) ->toHaveKey('success', false) ->toHaveKey('auth_error', true) ->toHaveKey('http_status', 401); }); it('does update last_tracked_at when DHL says the shipment is not found', function () { Http::fake([ 'api-eu.dhl.com/track/shipments*' => Http::response(['shipments' => []], 200), ]); $shipment = makeFakeShipment(123, '00340434292135100148'); $shipment->shouldReceive('update') ->once() ->withArgs(function (array $payload) { return array_keys($payload) === ['last_tracked_at'] && ! array_key_exists('tracking_status', $payload) && ! array_key_exists('status', $payload); }) ->andReturnTrue(); $service = new DhlTrackingService; $result = $service->updateTracking($shipment, ['auto_retrack' => false]); expect($result) ->toHaveKey('success', false) ->toHaveKey('auth_error', false); }); it('stops the batch tracker on the first auth error to avoid burning the API quota', function () { Http::fake([ 'api-eu.dhl.com/track/shipments*' => Http::response([], 401), ]); $shipments = new Collection([ makeFakeShipment(1, 'A1'), makeFakeShipment(2, 'B2'), makeFakeShipment(3, 'C3'), ]); foreach ($shipments as $shipment) { $shipment->shouldNotReceive('update'); } $service = new DhlTrackingService; $stats = $service->updateTrackingBatch($shipments); // Since Phase 13 the batch tracker fires one HTTP call per shipment and // aborts on the *first* auth error. So exactly one shipment is marked // failed (the one we tried), the other two are skipped entirely. expect($stats) ->toHaveKey('updated', 0) ->toHaveKey('failed', 1); expect($stats['results'])->toHaveCount(1); expect($stats['results'][0]) ->toHaveKey('success', false) ->toHaveKey('auth_error', true); Http::assertSentCount(1); });