3
0
mirror of https://github.com/snipe/snipe-it.git synced 2026-08-24 22:29:27 +00:00
Files
snipe-it/tests/Feature/Requests/Ui/AccessoryRequestTest.php
2026-08-21 16:06:41 +01:00

240 lines
9.0 KiB
PHP

<?php
namespace Tests\Feature\Requests\Ui;
use App\Models\Accessory;
use App\Models\CheckoutRequest;
use App\Models\User;
use Tests\TestCase;
class AccessoryRequestTest extends TestCase
{
public function test_requestable_index_lists_requestable_accessories(): void
{
// The accessories tab on /account/requestable is now
// API-backed via api.accessories.requestable. Shell page still
// needs a non-empty $accessories view var so the tab-badge
// count renders (0 hides the tab entirely), and the API row
// shape has to include the requestable one + omit the
// non-requestable one.
$requestableAccessory = Accessory::factory()->create(['requestable' => true]);
$nonRequestable = Accessory::factory()->create(['requestable' => false]);
$this->actingAs(User::factory()->create())
->get(route('account.requestable'))
->assertOk()
->assertViewHas('counts', fn ($counts) => ($counts['accessories'] ?? 0) > 0);
$rows = $this->actingAsForApi(User::factory()->create())
->getJson(route('api.accessories.requestable'))
->assertOk()
->json('rows');
$ids = collect($rows)->pluck('id')->all();
$this->assertContains($requestableAccessory->id, $ids);
$this->assertNotContains($nonRequestable->id, $ids);
}
public function test_user_can_request_a_requestable_accessory(): void
{
$accessory = Accessory::factory()->create(['requestable' => true]);
$user = User::factory()->create();
$this->actingAs($user)
->post(route('account/request-item', [
'itemType' => 'accessory',
'itemId' => $accessory->id,
]), ['request-quantity' => 3])
->assertRedirect();
$request = CheckoutRequest::where('requestable_id', $accessory->id)
->where('requestable_type', Accessory::class)
->where('user_id', $user->id)
->first();
$this->assertNotNull($request, 'A checkout request should have been created for the accessory.');
$this->assertEquals(3, $request->quantity, 'The requested quantity should be persisted.');
$this->assertNull($request->canceled_at);
}
public function test_user_cannot_request_an_accessory_that_is_not_requestable(): void
{
$accessory = Accessory::factory()->create(['requestable' => false]);
$user = User::factory()->create();
$this->actingAs($user)
->post(route('account/request-item', [
'itemType' => 'accessory',
'itemId' => $accessory->id,
]))
->assertRedirect()
->assertSessionHas('error');
$this->assertNull(
CheckoutRequest::where('requestable_id', $accessory->id)
->where('requestable_type', Accessory::class)
->first()
);
}
public function test_user_can_cancel_their_own_accessory_request(): void
{
$accessory = Accessory::factory()->create(['requestable' => true]);
$user = User::factory()->create();
CheckoutRequest::factory()->create([
'requestable_id' => $accessory->id,
'requestable_type' => Accessory::class,
'user_id' => $user->id,
]);
$this->actingAs($user)
->post(route('account/request-item', [
'itemType' => 'accessory',
'itemId' => $accessory->id,
]))
->assertRedirect();
$this->assertNotNull(
CheckoutRequest::where('requestable_id', $accessory->id)
->where('requestable_type', Accessory::class)
->where('user_id', $user->id)
->whereNotNull('canceled_at')
->first()
);
}
public function test_user_can_request_an_accessory_with_a_reservation_window(): void
{
// Reservation dates are optional (persist the "start blank ->
// no window" story), but when supplied both should land on the
// CheckoutRequest row so the future calendar-events pipeline
// has something to hydrate from.
$accessory = Accessory::factory()->create(['requestable' => true]);
$user = User::factory()->create();
$this->actingAs($user)
->post(route('account/request-item', [
'itemType' => 'accessory',
'itemId' => $accessory->id,
]), [
'request-quantity' => 2,
'start_date' => '2026-09-01',
'end_date' => '2026-09-05',
])
->assertRedirect();
$request = CheckoutRequest::where('requestable_id', $accessory->id)
->where('requestable_type', Accessory::class)
->where('user_id', $user->id)
->first();
$this->assertNotNull($request);
$this->assertSame('2026-09-01', $request->start_date->toDateString());
$this->assertSame('2026-09-05', $request->end_date->toDateString());
}
public function test_user_can_attach_notes_to_an_accessory_request(): void
{
// Notes are optional but persist end-to-end when supplied: on
// the row for admin-queue rendering and via the notification
// for the fulfillment email/slack.
$accessory = Accessory::factory()->create(['requestable' => true]);
$user = User::factory()->create();
$this->actingAs($user)
->post(route('account/request-item', [
'itemType' => 'accessory',
'itemId' => $accessory->id,
]), [
'request-quantity' => 1,
'notes' => 'Needed for the Q3 offsite',
])
->assertRedirect();
$request = CheckoutRequest::where('requestable_id', $accessory->id)
->where('requestable_type', Accessory::class)
->where('user_id', $user->id)
->first();
$this->assertNotNull($request);
$this->assertSame('Needed for the Q3 offsite', $request->notes);
}
public function test_blank_notes_do_not_persist_as_empty_string(): void
{
// An untouched textarea shouldn't create an empty "Additional
// Notes" block in the admin's email. Controller coerces the
// empty string to null.
$accessory = Accessory::factory()->create(['requestable' => true]);
$user = User::factory()->create();
$this->actingAs($user)
->post(route('account/request-item', [
'itemType' => 'accessory',
'itemId' => $accessory->id,
]), [
'request-quantity' => 1,
'notes' => '',
])
->assertRedirect();
$request = CheckoutRequest::where('requestable_id', $accessory->id)
->where('requestable_type', Accessory::class)
->where('user_id', $user->id)
->first();
$this->assertNotNull($request);
$this->assertNull($request->notes);
}
public function test_request_is_rejected_when_end_date_precedes_start_date(): void
{
$accessory = Accessory::factory()->create(['requestable' => true]);
$user = User::factory()->create();
$this->actingAs($user)
->post(route('account/request-item', [
'itemType' => 'accessory',
'itemId' => $accessory->id,
]), [
'request-quantity' => 1,
'start_date' => '2026-09-10',
'end_date' => '2026-09-05',
])
->assertRedirect()
->assertSessionHasErrors('end_date');
$this->assertNull(
CheckoutRequest::where('requestable_id', $accessory->id)
->where('requestable_type', Accessory::class)
->first(),
'A request with an inverted date range must not persist.'
);
}
public function test_admin_requested_api_endpoint_includes_accessory_requests(): void
{
// The admin "Requested" queue is a polymorphic list of all
// checkout requests. It renders from GET /api/v1/requests
// (the /hardware/requested Blade page is now a shell that
// hydrates from the same endpoint), so this test asserts on
// the API row rather than the server-rendered HTML.
$accessory = Accessory::factory()->create(['requestable' => true]);
$request = CheckoutRequest::factory()->create([
'requestable_id' => $accessory->id,
'requestable_type' => Accessory::class,
'user_id' => User::factory()->create()->id,
]);
$rows = $this->actingAsForApi(User::factory()->checkoutAccessories()->create())
->getJson(route('api.requests.index'))
->assertOk()
->json('rows');
$row = collect($rows)->firstWhere('id', $request->id);
$this->assertNotNull($row);
$this->assertSame($accessory->name, $row['requestable']['name']);
$this->assertSame('Accessory', $row['requestable']['type']);
}
}