112 lines
6.9 KiB
PHP
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());
|
|
});
|