set('services.snipe_it', [ 'url' => 'https://inventory.example.test', 'read_token' => 'read-token-for-tests', 'timeout' => 2, 'page_size' => 1, 'retries' => 1, 'retry_delay_ms' => 0, ]); }); it('sincroniza por páginas e desativa apenas ativos ausentes', function () { $stale = SnipeAsset::factory()->create([ 'snipe_asset_id' => 99, 'asset_tag' => 'ANTIGO-99', 'active' => true, 'synced_at' => now()->subDay(), ]); Http::fakeSequence() ->push(['total' => 2, 'rows' => [snipeSyncRow(1, 'EQ-001')]]) ->push(['total' => 2, 'rows' => [snipeSyncRow(2, 'EQ-002')]]); $run = app(AssetSyncService::class)->sync(); expect($run->status)->toBe(SyncRunStatus::Succeeded) ->and($run->records_processed)->toBe(2) ->and($run->records_created)->toBe(2) ->and($run->records_disabled)->toBe(1) ->and($stale->refresh()->active)->toBeFalse(); $this->assertDatabaseHas('snipe_assets', [ 'snipe_asset_id' => 1, 'asset_tag' => 'EQ-001', 'name' => 'Câmara & tripé', 'category_name' => 'Audiovisual', 'requestable' => true, 'active' => true, ]); }); it('não desativa ativos quando uma página remota falha', function () { $existing = SnipeAsset::factory()->create([ 'snipe_asset_id' => 99, 'asset_tag' => 'PRESERVAR-99', 'active' => true, 'synced_at' => now()->subDay(), ]); Http::fakeSequence() ->push(['total' => 2, 'rows' => [snipeSyncRow(1, 'EQ-001')]]) ->push(['message' => 'failure'], 500); expect(fn () => app(AssetSyncService::class)->sync()) ->toThrow(SnipeItApiException::class, 'HTTP 500'); expect($existing->refresh()->active)->toBeTrue(); $run = SyncRun::query()->latest('id')->firstOrFail(); expect($run->status)->toBe(SyncRunStatus::Failed) ->and($run->records_processed)->toBe(1) ->and($run->records_disabled)->toBe(0); }); it('não conta como atualização um ativo cujo conteúdo não mudou', function () { Http::fake([ '*' => Http::response(['total' => 1, 'rows' => [snipeSyncRow(1, 'EQ-001')]]), ]); app(AssetSyncService::class)->sync(); $secondRun = app(AssetSyncService::class)->sync(); expect($secondRun->records_processed)->toBe(1) ->and($secondRun->records_created)->toBe(0) ->and($secondRun->records_updated)->toBe(0) ->and($secondRun->records_disabled)->toBe(0); }); /** @return array */ function snipeSyncRow(int $id, string $tag): array { return [ 'id' => $id, 'asset_tag' => $tag, 'name' => 'Câmara & tripé', 'serial' => "SERIAL-{$id}", 'model' => ['id' => 20, 'name' => 'Lumix'], 'category' => ['id' => 30, 'name' => 'Audiovisual'], 'manufacturer' => ['id' => 40, 'name' => 'Panasonic'], 'status_label' => ['id' => 2, 'name' => 'Pronto a utilizar'], 'rtd_location' => ['id' => 5, 'name' => 'Armazém central'], 'requestable' => true, 'assigned_to' => null, 'image' => 'https://inventory.example.test/uploads/asset.png', 'updated_at' => ['datetime' => '2026-08-04T12:00:00+00:00'], ]; }