Files
compras/tests/Feature/ReplenishmentHealthTest.php
2026-11-03 11:51:44 +02:00

112 lines
6.9 KiB
PHP

<?php
use App\Models\User;
use App\Services\ReplenishmentHeartbeat;
use App\Services\ReplenishmentQueue;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
use Symfony\Component\HttpKernel\Exception\HttpException;
require_once __DIR__.'/../Support/ReceiptFixture.php';
beforeEach(function () {
$this->freezeTime();
$this->f = receiptFixture();
$this->originalStorage = app()->storagePath();
$this->temporaryStorage = $this->originalStorage.'/framework/testing/health-'.Str::uuid();
File::ensureDirectoryExists($this->temporaryStorage.'/framework');
app()->useStoragePath($this->temporaryStorage);
auth()->guard()->logout();
});
afterEach(function () {
app()->useStoragePath($this->originalStorage);
File::deleteDirectory($this->temporaryStorage);
});
test('replenishment health is read only and emits success without creating work purchases or telemetry', function () {
app(ReplenishmentHeartbeat::class)->record(now()->toIso8601String(), 0);
$path = storage_path('framework/replenishment-command-status.json');
$beforeFile = File::get($path);
$before = [];
foreach (['requisitions', 'stock_movements', 'audit_logs', 'replenishment_work', 'supply_records'] as $table) {
$before[$table] = DB::table($table)->orderBy('id')->get()->toArray();
}
$result = app(ReplenishmentQueue::class)->health();
expect($result['ok'])->toBeTrue()->and($result['reasons'])->toBe([])->and($result['work'])->toBe(['total' => 0, 'pending' => 0, 'failed' => 0]);
$this->artisan('sgc:replenishment-health', ['--json' => true])->expectsOutput(json_encode($result, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR))->assertExitCode(0);
expect(auth()->check())->toBeFalse()->and(File::get($path))->toBe($beforeFile);
foreach ($before as $table => $rows) {
expect(DB::table($table)->orderBy('id')->get()->toArray())->toEqual($rows);
}
});
test('replenishment health reports missing stale damaged or unsuccessful observation without certifying execution', function () {
expect(app(ReplenishmentQueue::class)->health()['reasons'])->toContain('not_recent', 'last_run_not_successful');
File::put(storage_path('framework/replenishment-command-status.json'), '{broken');
expect(app(ReplenishmentQueue::class)->health()['ok'])->toBeFalse();
File::put(storage_path('framework/replenishment-command-status.json'), json_encode(['result' => 'passed', 'finished_at' => now()->subHour()->toIso8601String(), 'installation' => app(ReplenishmentHeartbeat::class)->fingerprint()], JSON_THROW_ON_ERROR));
expect(app(ReplenishmentQueue::class)->health()['reasons'])->toBe(['not_recent']);
app(ReplenishmentHeartbeat::class)->record(now()->toIso8601String(), 1);
expect(app(ReplenishmentQueue::class)->health()['reasons'])->toBe(['last_run_not_successful']);
$this->artisan('sgc:replenishment-health')->expectsOutput('Reposição: verificação técnica requer atenção.')->expectsOutput('Última passagem sem sucesso confirmado.')->assertExitCode(1);
});
test('replenishment health aggregates pending failures while the workdesk stays tenant scoped and unchanged', function () {
$queue = app(ReplenishmentQueue::class);
$this->actingAs($this->f['receiver']);
$first = $queue->enqueue($this->f['receiver'], $this->f['stock']->id, $this->f['warehouse']->id);
$foreign = receiptFixture();
$this->actingAs($foreign['receiver']);
$second = $queue->enqueue($foreign['receiver'], $foreign['stock']->id, $foreign['warehouse']->id);
DB::table('replenishment_work')->where('id', $second)->update(['failures' => 1, 'last_error' => 'PRIVATE-WORK-DETAIL']);
$before = DB::table('replenishment_work')->orderBy('id')->get()->toArray();
app(ReplenishmentHeartbeat::class)->record(now()->toIso8601String(), 0);
auth()->guard()->logout();
$result = $queue->health();
expect($result['ok'])->toBeFalse()->and($result['reasons'])->toBe(['pending_signals', 'failed_work'])
->and($result['work'])->toBe(['total' => 2, 'pending' => 2, 'failed' => 1]);
expect(json_encode($result))->not->toContain('PRIVATE-WORK-DETAIL')->not->toContain('tenant_id')->not->toContain('responsible_id');
expect(DB::table('replenishment_work')->orderBy('id')->get()->toArray())->toEqual($before);
$this->actingAs($this->f['receiver']);
expect((int) $queue->status($this->f['receiver'])['work']->total)->toBe(1);
expect(DB::table('replenishment_work')->where('id', $first)->value('processed_generation'))->toBe(0);
$unassigned = User::factory()->create(['tenant_id' => null]);
$this->actingAs($unassigned);
expect((int) $queue->status($unassigned)['work']->total)->toBe(0);
});
test('replenishment health rejects interactive identity and unavailable composition without processing work', function () {
$this->actingAs($this->f['receiver']);
try {
app(ReplenishmentQueue::class)->health();
test()->fail('Interactive identity must not be used for technical health');
} catch (HttpException $error) {
expect($error->getStatusCode())->toBe(403);
}
auth()->guard()->logout();
config(['work_context.composition' => 'sgf']);
$result = app(ReplenishmentQueue::class)->health();
expect($result['ok'])->toBeFalse()->and($result['recovery_ready'])->toBeFalse()->and($result['reasons'])->toContain('not_installed')->and($result['work'])->toBeNull();
});
test('replenishment health command returns generic unavailability without exposing an exception', function () {
$this->mock(ReplenishmentQueue::class)->shouldReceive('health')->once()->andThrow(new RuntimeException('PRIVATE-CONNECTION-DETAIL'));
$this->artisan('sgc:replenishment-health', ['--json' => true])->expectsOutput(json_encode(['checked_at' => now()->toIso8601String(), 'ok' => false, 'reasons' => ['unavailable']], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR))->assertExitCode(1);
});
test('replenishment health requires a bound observation and rejects another installation without leaking its identity', function () {
$path = storage_path('framework/replenishment-command-status.json');
File::put($path, json_encode(['result' => 'passed', 'finished_at' => now()->toIso8601String()], JSON_THROW_ON_ERROR));
expect(app(ReplenishmentQueue::class)->health()['reasons'])->toBe(['unbound_observation']);
File::put($path, json_encode(['result' => 'passed', 'finished_at' => now()->toIso8601String(), 'installation' => str_repeat('a', 64)], JSON_THROW_ON_ERROR));
expect(app(ReplenishmentQueue::class)->health()['ok'])->toBeFalse();
$this->actingAs($this->f['receiver']);
expect(app(ReplenishmentQueue::class)->status($this->f['receiver'])['schedulerRecent'])->toBeFalse();
auth()->guard()->logout();
app(ReplenishmentHeartbeat::class)->record(now()->toIso8601String(), 0);
expect(app(ReplenishmentQueue::class)->health()['ok'])->toBeTrue();
expect(json_encode(app(ReplenishmentQueue::class)->health()))->not->toContain(app(ReplenishmentHeartbeat::class)->fingerprint());
});