feat: protect production Snipe-IT operations

This commit is contained in:
2026-08-05 12:37:12 +01:00
parent 26330b75f7
commit 8c69331118
13 changed files with 289 additions and 26 deletions

View File

@@ -63,14 +63,18 @@ it('recusa erros transportados num HTTP 200', function () {
});
it('regista uma entrega com o token operacional e o destinatário Snipe-IT', function () {
Http::fake([
'*' => Http::response(['status' => 'success', 'payload' => ['id' => 41]], 200),
]);
Http::fakeSequence()
->push(snipeClientRow(41))
->push(['status' => 'success', 'payload' => ['id' => 41]]);
$payload = app(SnipeItClient::class)->checkoutHardware(41, 2, 73, '2026-08-10', 'Reserva RR-001');
expect($payload['status'])->toBe('success');
Http::assertSent(fn (Request $request): bool => $request->method() === 'GET'
&& $request->url() === 'https://inventory.example.test/api/v1/hardware/41'
&& $request->hasHeader('Authorization', 'Bearer read-token-for-tests'));
Http::assertSent(function (Request $request): bool {
return $request->method() === 'POST'
&& $request->url() === 'https://inventory.example.test/api/v1/hardware/41/checkout'
@@ -85,12 +89,24 @@ it('regista uma entrega com o token operacional e o destinatário Snipe-IT', fun
});
});
it('regista uma devolução com estado e localização no Snipe-IT', function () {
it('bloqueia checkout quando o equipamento já está atribuído sem enviar escrita', function () {
Http::fake([
'*' => Http::response(['status' => 'success', 'payload' => ['id' => 41]], 200),
'*' => Http::response(snipeClientRow(41, 99)),
]);
app(SnipeItClient::class)->checkinHardware(41, 2, 9, 'Devolvido sem danos');
expect(fn () => app(SnipeItClient::class)->checkoutHardware(41, 2, 73, '2026-08-10'))
->toThrow(SnipeItApiException::class, 'já está atribuído');
Http::assertSentCount(1);
Http::assertSent(fn (Request $request): bool => $request->method() === 'GET');
});
it('regista uma devolução com estado e localização no Snipe-IT', function () {
Http::fakeSequence()
->push(snipeClientRow(41, 73))
->push(['status' => 'success', 'payload' => ['id' => 41]]);
app(SnipeItClient::class)->checkinHardware(41, 2, 73, 9, 'Devolvido sem danos');
Http::assertSent(function (Request $request): bool {
return $request->method() === 'POST'
@@ -103,21 +119,37 @@ it('regista uma devolução com estado e localização no Snipe-IT', function ()
});
});
it('recusa erros operacionais transportados num HTTP 200', function () {
it('bloqueia check-in se a atribuição remota não corresponder ao destinatário', function () {
Http::fake([
'*' => Http::response(['status' => 'error', 'messages' => 'Asset is not deployable'], 200),
'*' => Http::response(snipeClientRow(41, 99)),
]);
expect(fn () => app(SnipeItClient::class)->checkinHardware(41, 2, 73, 9))
->toThrow(SnipeItApiException::class, 'não está atribuído ao destinatário esperado');
Http::assertSentCount(1);
Http::assertSent(fn (Request $request): bool => $request->method() === 'GET');
});
it('recusa erros operacionais transportados num HTTP 200', function () {
Http::fakeSequence()
->push(snipeClientRow(41))
->push(['status' => 'error', 'messages' => 'Asset is not deployable']);
expect(fn () => app(SnipeItClient::class)->checkoutHardware(41, 2, 73, '2026-08-10'))
->toThrow(SnipeItApiException::class, 'recusou a entrega');
});
/** @return array<string, mixed> */
function snipeClientRow(int $id): array
function snipeClientRow(int $id, ?int $assignedUserId = null): array
{
return [
'id' => $id,
'asset_tag' => "EQ-{$id}",
'name' => "Equipamento {$id}",
'assigned_to' => $assignedUserId === null ? null : [
'id' => $assignedUserId,
'type' => 'user',
],
];
}