feat: protect production Snipe-IT operations
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
use App\Domain\Identity\Enums\RoleName;
|
||||
use App\Domain\Identity\Models\Role;
|
||||
use App\Domain\Inventory\Models\SnipeAsset;
|
||||
use App\Domain\Operations\Enums\HandoverType;
|
||||
use App\Domain\Operations\Models\HandoverRecord;
|
||||
use App\Domain\Reservations\Enums\ReservationItemStatus;
|
||||
use App\Domain\Reservations\Enums\ReservationStatus;
|
||||
use App\Domain\Reservations\Models\Reservation;
|
||||
@@ -54,6 +56,8 @@ function approvedOperationReservation(User $requester, array $assets, string $re
|
||||
'snipe_asset_id' => $asset->id,
|
||||
'starts_at' => $reservation->starts_at,
|
||||
'ends_at' => $reservation->ends_at,
|
||||
'buffer_starts_at' => $reservation->starts_at,
|
||||
'buffer_ends_at' => $reservation->ends_at,
|
||||
'status' => ReservationItemStatus::Reserved,
|
||||
]);
|
||||
}
|
||||
@@ -61,6 +65,34 @@ function approvedOperationReservation(User $requester, array $assets, string $re
|
||||
return $reservation;
|
||||
}
|
||||
|
||||
/** @param array<int, int> $assignedUsers */
|
||||
function fakeSuccessfulSnipeOperations(array $assignedUsers = []): void
|
||||
{
|
||||
Http::fake(function (Request $request) use ($assignedUsers) {
|
||||
if ($request->method() === 'GET' && preg_match('#/hardware/(\d+)$#', $request->url(), $matches) === 1) {
|
||||
$hardwareId = (int) $matches[1];
|
||||
|
||||
return Http::response(operationSnipeRow($hardwareId, $assignedUsers[$hardwareId] ?? null));
|
||||
}
|
||||
|
||||
return Http::response(['status' => 'success', 'payload' => ['id' => 1]]);
|
||||
});
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
function operationSnipeRow(int $hardwareId, ?int $assignedUserId = null): array
|
||||
{
|
||||
return [
|
||||
'id' => $hardwareId,
|
||||
'asset_tag' => "EQ-{$hardwareId}",
|
||||
'name' => "Equipamento {$hardwareId}",
|
||||
'assigned_to' => $assignedUserId === null ? null : [
|
||||
'id' => $assignedUserId,
|
||||
'type' => 'user',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
it('restringe e apresenta a fila diária aos operadores', function () {
|
||||
$reservation = approvedOperationReservation(
|
||||
User::factory()->create(),
|
||||
@@ -94,7 +126,7 @@ it('prepara e entrega todos os equipamentos com registo auditável', function ()
|
||||
expect($reservation->fresh()->status)->toBe(ReservationStatus::Ready)
|
||||
->and($reservation->items()->firstOrFail()->status)->toBe(ReservationItemStatus::Ready);
|
||||
|
||||
Http::fake(['*' => Http::response(['status' => 'success', 'payload' => ['id' => 501]])]);
|
||||
fakeSuccessfulSnipeOperations();
|
||||
|
||||
$this->actingAs($operator)
|
||||
->post("/operacoes/{$reservation->uuid}/entregar", [
|
||||
@@ -123,24 +155,56 @@ it('prepara e entrega todos os equipamentos com registo auditável', function ()
|
||||
'condition' => 'Sem danos visíveis',
|
||||
])
|
||||
->assertSessionHasErrors('operation');
|
||||
Http::assertSentCount(2);
|
||||
});
|
||||
|
||||
it('não toca num equipamento que já esteja em checkout no Snipe-IT', function () {
|
||||
$operator = inventoryOperator();
|
||||
$asset = SnipeAsset::factory()->create(['snipe_asset_id' => 551, 'status_id' => 2]);
|
||||
$reservation = approvedOperationReservation(User::factory()->create(), [$asset], 'REQ-OPS-REMOTE-ASSIGNED');
|
||||
$reservation->update(['status' => ReservationStatus::Ready]);
|
||||
$reservation->items()->update(['status' => ReservationItemStatus::Ready]);
|
||||
fakeSuccessfulSnipeOperations([551 => 999]);
|
||||
|
||||
$this->actingAs($operator)
|
||||
->post("/operacoes/{$reservation->uuid}/entregar", [
|
||||
'snipe_user_id' => 77,
|
||||
'condition' => 'Sem danos visíveis',
|
||||
])
|
||||
->assertSessionHasErrors('operation');
|
||||
|
||||
expect($reservation->fresh()->status)->toBe(ReservationStatus::Ready)
|
||||
->and($reservation->items()->firstOrFail()->status)->toBe(ReservationItemStatus::Ready);
|
||||
Http::assertSentCount(1);
|
||||
Http::assertSent(fn (Request $request): bool => $request->method() === 'GET');
|
||||
$this->assertDatabaseCount('handover_records', 0);
|
||||
});
|
||||
|
||||
it('regista a devolução no Snipe-IT e conclui o processo', function () {
|
||||
$operator = inventoryOperator();
|
||||
$requester = User::factory()->create(['snipe_user_id' => 77]);
|
||||
$asset = SnipeAsset::factory()->create([
|
||||
'snipe_asset_id' => 601,
|
||||
'status_id' => 2,
|
||||
'location_id' => 9,
|
||||
]);
|
||||
$reservation = approvedOperationReservation(User::factory()->create(), [$asset], 'REQ-OPS-RETURN');
|
||||
$reservation = approvedOperationReservation($requester, [$asset], 'REQ-OPS-RETURN');
|
||||
$reservation->update(['status' => ReservationStatus::Delivered]);
|
||||
$reservation->items()->update([
|
||||
'status' => ReservationItemStatus::Delivered,
|
||||
'delivered_at' => now()->subHours(2),
|
||||
]);
|
||||
|
||||
Http::fake(['*' => Http::response(['status' => 'success', 'payload' => ['id' => 601]])]);
|
||||
$item = $reservation->items()->firstOrFail();
|
||||
HandoverRecord::create([
|
||||
'reservation_item_id' => $item->id,
|
||||
'type' => HandoverType::Checkout,
|
||||
'operator_id' => $operator->id,
|
||||
'recipient_id' => $requester->id,
|
||||
'condition' => 'Bom estado',
|
||||
'snipe_response' => ['status' => 'success'],
|
||||
]);
|
||||
fakeSuccessfulSnipeOperations([601 => 77]);
|
||||
|
||||
$this->actingAs($operator)
|
||||
->post("/operacoes/{$reservation->uuid}/devolver", [
|
||||
@@ -164,6 +228,49 @@ it('regista a devolução no Snipe-IT e conclui o processo', function () {
|
||||
$this->assertDatabaseHas('audit_logs', ['event' => 'reservation.completed']);
|
||||
});
|
||||
|
||||
it('bloqueia check-in sem checkout previamente confirmado pelo portal', function () {
|
||||
$operator = inventoryOperator();
|
||||
$requester = User::factory()->create(['snipe_user_id' => 77]);
|
||||
$asset = SnipeAsset::factory()->create(['snipe_asset_id' => 651, 'status_id' => 2]);
|
||||
$reservation = approvedOperationReservation($requester, [$asset], 'REQ-OPS-UNTRACKED-CHECKIN');
|
||||
$reservation->update(['status' => ReservationStatus::Delivered]);
|
||||
$reservation->items()->update(['status' => ReservationItemStatus::Delivered]);
|
||||
Http::fake();
|
||||
|
||||
$this->actingAs($operator)
|
||||
->post("/operacoes/{$reservation->uuid}/devolver", ['condition' => 'Bom estado'])
|
||||
->assertSessionHasErrors('operation');
|
||||
|
||||
Http::assertNothingSent();
|
||||
expect($reservation->items()->firstOrFail()->status)->toBe(ReservationItemStatus::Delivered);
|
||||
});
|
||||
|
||||
it('bloqueia check-in quando o checkout do portal pertence a outro destinatário', function () {
|
||||
$operator = inventoryOperator();
|
||||
$requester = User::factory()->create(['snipe_user_id' => 77]);
|
||||
$otherRecipient = User::factory()->create(['snipe_user_id' => 88]);
|
||||
$asset = SnipeAsset::factory()->create(['snipe_asset_id' => 652, 'status_id' => 2]);
|
||||
$reservation = approvedOperationReservation($requester, [$asset], 'REQ-OPS-WRONG-RECIPIENT');
|
||||
$reservation->update(['status' => ReservationStatus::Delivered]);
|
||||
$reservation->items()->update(['status' => ReservationItemStatus::Delivered]);
|
||||
HandoverRecord::create([
|
||||
'reservation_item_id' => $reservation->items()->firstOrFail()->id,
|
||||
'type' => HandoverType::Checkout,
|
||||
'operator_id' => $operator->id,
|
||||
'recipient_id' => $otherRecipient->id,
|
||||
'condition' => 'Bom estado',
|
||||
'snipe_response' => ['status' => 'success'],
|
||||
]);
|
||||
Http::fake();
|
||||
|
||||
$this->actingAs($operator)
|
||||
->post("/operacoes/{$reservation->uuid}/devolver", ['condition' => 'Bom estado'])
|
||||
->assertSessionHasErrors('operation');
|
||||
|
||||
Http::assertNothingSent();
|
||||
expect($reservation->items()->firstOrFail()->status)->toBe(ReservationItemStatus::Delivered);
|
||||
});
|
||||
|
||||
it('preserva sucessos e sinaliza incidente quando uma entrega fica parcial', function () {
|
||||
$operator = inventoryOperator();
|
||||
$first = SnipeAsset::factory()->create(['snipe_asset_id' => 701, 'status_id' => 2]);
|
||||
@@ -173,8 +280,11 @@ it('preserva sucessos e sinaliza incidente quando uma entrega fica parcial', fun
|
||||
$reservation->items()->update(['status' => ReservationItemStatus::Ready]);
|
||||
|
||||
Http::fakeSequence()
|
||||
->push(operationSnipeRow(701))
|
||||
->push(['status' => 'success', 'payload' => ['id' => 701]])
|
||||
->push(operationSnipeRow(702))
|
||||
->push(['status' => 'error', 'messages' => 'Asset is not deployable'])
|
||||
->push(operationSnipeRow(702))
|
||||
->push(['status' => 'success', 'payload' => ['id' => 702]]);
|
||||
|
||||
$this->actingAs($operator)
|
||||
|
||||
Reference in New Issue
Block a user