Files
snipe-it/tests/Feature/CheckoutAcceptances/Ui/AssetAcceptanceTest.php
T
2026-05-04 11:46:34 +01:00

475 lines
17 KiB
PHP

<?php
namespace Tests\Feature\CheckoutAcceptances\Ui;
use App\Events\CheckoutAccepted;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\CheckoutAcceptance;
use App\Models\CustomField;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\AcceptanceItemAcceptedNotification;
use App\Notifications\AcceptanceItemAcceptedToUserNotification;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;
class AssetAcceptanceTest extends TestCase
{
public function test_asset_checkout_accept_page_renders()
{
$checkoutAcceptance = CheckoutAcceptance::factory()->pending()->create();
$this->actingAs($checkoutAcceptance->assignedTo)
->get(route('account.accept.item', $checkoutAcceptance))
->assertViewIs('account.accept.create');
}
public function test_cannot_accept_asset_already_accepted()
{
Event::fake([CheckoutAccepted::class]);
$checkoutAcceptance = CheckoutAcceptance::factory()->accepted()->create();
$this->assertFalse($checkoutAcceptance->isPending());
$this->actingAs($checkoutAcceptance->assignedTo)
->post(route('account.store-acceptance', $checkoutAcceptance), [
'asset_acceptance' => 'accepted',
'note' => 'my note',
])
->assertRedirectToRoute('account.accept')
->assertSessionHas('error');
Event::assertNotDispatched(CheckoutAccepted::class);
}
public function test_cannot_accept_asset_for_another_user()
{
Event::fake([CheckoutAccepted::class]);
$checkoutAcceptance = CheckoutAcceptance::factory()->pending()->create();
$this->assertTrue($checkoutAcceptance->isPending());
$anotherUser = User::factory()->create();
$this->actingAs($anotherUser)
->post(route('account.store-acceptance', $checkoutAcceptance), [
'asset_acceptance' => 'accepted',
'note' => 'my note',
])
->assertRedirectToRoute('account.accept')
->assertSessionHas('error');
$this->assertTrue($checkoutAcceptance->fresh()->isPending());
Event::assertNotDispatched(CheckoutAccepted::class);
}
public function test_user_can_accept_asset()
{
Event::fake([CheckoutAccepted::class]);
$checkoutAcceptance = CheckoutAcceptance::factory()->pending()->create();
$this->assertTrue($checkoutAcceptance->isPending());
$this->actingAs($checkoutAcceptance->assignedTo)
->post(route('account.store-acceptance', $checkoutAcceptance), [
'asset_acceptance' => 'accepted',
'note' => 'my note',
])
->assertRedirectToRoute('account.accept')
->assertSessionHas('success');
$checkoutAcceptance->refresh();
$this->assertFalse($checkoutAcceptance->isPending());
$this->assertNotNull($checkoutAcceptance->accepted_at);
$this->assertNull($checkoutAcceptance->declined_at);
Event::assertDispatched(CheckoutAccepted::class);
}
public function test_user_can_accept_asset_with_required_signature()
{
if (! function_exists('imagecreatetruecolor')) {
$this->markTestSkipped('GD extension is required for signature image generation.');
}
$settings = Setting::query()->firstOrFail();
$settings->require_accept_signature = 1;
$settings->save();
Setting::$_cache = null;
$checkoutAcceptance = CheckoutAcceptance::factory()->pending()->create();
$canvas = imagecreatetruecolor(24, 12);
$transparent = imagecolorallocatealpha($canvas, 0, 0, 0, 127);
imagefill($canvas, 0, 0, $transparent);
imagesavealpha($canvas, true);
$ink = imagecolorallocate($canvas, 25, 25, 25);
imageline($canvas, 2, 10, 21, 2, $ink);
ob_start();
imagepng($canvas);
$signaturePng = (string) ob_get_clean();
imagedestroy($canvas);
$signatureOutput = 'data:image/png;base64,'.base64_encode($signaturePng);
$this->actingAs($checkoutAcceptance->assignedTo)
->post(route('account.store-acceptance', $checkoutAcceptance), [
'asset_acceptance' => 'accepted',
'note' => 'signed in test',
'signature_output' => $signatureOutput,
])
->assertRedirectToRoute('account.accept')
->assertSessionHas('success');
$checkoutAcceptance->refresh();
$this->assertNotNull($checkoutAcceptance->signature_filename);
$this->assertNotNull($checkoutAcceptance->stored_eula_file);
}
public function test_user_can_decline_asset()
{
Event::fake([CheckoutAccepted::class]);
$checkoutAcceptance = CheckoutAcceptance::factory()->pending()->create();
$this->assertTrue($checkoutAcceptance->isPending());
$this->actingAs($checkoutAcceptance->assignedTo)
->post(route('account.store-acceptance', $checkoutAcceptance), [
'asset_acceptance' => 'declined',
'note' => 'my note',
])
->assertRedirectToRoute('account.accept')
->assertSessionHas('success');
$checkoutAcceptance->refresh();
$this->assertFalse($checkoutAcceptance->isPending());
$this->assertNull($checkoutAcceptance->accepted_at);
$this->assertNotNull($checkoutAcceptance->declined_at);
Event::assertNotDispatched(CheckoutAccepted::class);
}
public function test_action_logged_when_accepting_asset()
{
$checkoutAcceptance = CheckoutAcceptance::factory()->pending()->create();
$this->actingAs($checkoutAcceptance->assignedTo)
->post(route('account.store-acceptance', $checkoutAcceptance), [
'asset_acceptance' => 'accepted',
'note' => 'my note',
]);
$this->assertTrue(Actionlog::query()
->where([
'action_type' => 'accepted',
'target_id' => $checkoutAcceptance->assignedTo->id,
'target_type' => User::class,
'note' => 'my note',
'item_type' => Asset::class,
'item_id' => $checkoutAcceptance->checkoutable->id,
])
->whereNotNull('action_date')
->exists()
);
}
public function test_admin_acceptance_email_contains_custom_fields_marked_show_in_email(): void
{
Notification::fake();
$this->settings->enableAlertEmail();
$customField = CustomField::factory()->create([
'name' => 'Cost Center',
'show_in_email' => '1',
'field_encrypted' => '0',
])->fresh();
$asset = Asset::factory()->hasMultipleCustomFields([$customField])->create();
$asset->{$customField->db_column} = 'ENG-42';
$asset->save();
$checkoutAcceptance = CheckoutAcceptance::factory()
->pending()
->for($asset, 'checkoutable')
->create();
$this->actingAs($checkoutAcceptance->assignedTo)
->post(route('account.store-acceptance', $checkoutAcceptance), [
'asset_acceptance' => 'accepted',
])
->assertSessionHasNoErrors();
Notification::assertSentTo(
$checkoutAcceptance,
function (AcceptanceItemAcceptedNotification $notification) {
$rendered = $notification->toMail()->render();
$this->assertStringContainsString('Cost Center', $rendered);
$this->assertStringContainsString('ENG-42', $rendered);
return true;
}
);
}
public function test_admin_acceptance_email_does_not_contain_encrypted_custom_fields(): void
{
Notification::fake();
$this->settings->enableAlertEmail();
$customField = CustomField::factory()->create([
'name' => 'SSN',
'show_in_email' => '1',
'field_encrypted' => '1',
])->fresh();
$asset = Asset::factory()->hasMultipleCustomFields([$customField])->create();
$asset->{$customField->db_column} = '123-45-6789';
$asset->save();
$checkoutAcceptance = CheckoutAcceptance::factory()
->pending()
->for($asset, 'checkoutable')
->create();
$this->actingAs($checkoutAcceptance->assignedTo)
->post(route('account.store-acceptance', $checkoutAcceptance), [
'asset_acceptance' => 'accepted',
])
->assertSessionHasNoErrors();
Notification::assertSentTo(
$checkoutAcceptance,
function (AcceptanceItemAcceptedNotification $notification) {
$rendered = $notification->toMail()->render();
$this->assertStringNotContainsString('SSN', $rendered);
$this->assertStringNotContainsString('123-45-6789', $rendered);
return true;
}
);
}
public function test_action_logged_when_declining_asset()
{
$checkoutAcceptance = CheckoutAcceptance::factory()->pending()->create();
$this->actingAs($checkoutAcceptance->assignedTo)
->post(route('account.store-acceptance', $checkoutAcceptance), [
'asset_acceptance' => 'declined',
'note' => 'my note',
]);
$this->assertTrue(Actionlog::query()
->where([
'action_type' => 'declined',
'target_id' => $checkoutAcceptance->assignedTo->id,
'target_type' => User::class,
'note' => 'my note',
'item_type' => Asset::class,
'item_id' => $checkoutAcceptance->checkoutable->id,
])
->whereNotNull('action_date')
->exists()
);
}
public function test_admin_can_complete_sign_in_place_acceptance_and_is_redirected_to_selected_destination()
{
Event::fake([CheckoutAccepted::class]);
$assignee = User::factory()->create();
$admin = User::factory()->admin()->create();
$asset = Asset::factory()->create();
$checkoutAcceptance = CheckoutAcceptance::factory()
->pending()
->for($assignee, 'assignedTo')
->for($asset, 'checkoutable')
->create();
$this->actingAs($admin)
->withSession([
'sign_in_place_acceptance_id' => $checkoutAcceptance->id,
'sign_in_place_item_id' => $asset->id,
'sign_in_place_resource_type' => 'Assets',
'redirect_option' => 'target',
'checkout_to_type' => 'user',
])
->post(route('account.store-acceptance', $checkoutAcceptance), [
'asset_acceptance' => 'accepted',
'note' => 'signed in person',
])
->assertRedirect(route('users.show', $assignee));
$checkoutAcceptance->refresh();
$this->assertNotNull($checkoutAcceptance->accepted_at);
$this->assertTrue((bool) $checkoutAcceptance->signed_in_place);
$this->assertSame($admin->id, $checkoutAcceptance->signed_in_place_admin);
Event::assertDispatched(CheckoutAccepted::class);
}
public function test_stale_sign_in_place_post_on_already_accepted_item_redirects_to_intended_destination()
{
$assignee = User::factory()->create();
$admin = User::factory()->admin()->create();
$asset = Asset::factory()->create();
$checkoutAcceptance = CheckoutAcceptance::factory()
->accepted()
->for($assignee, 'assignedTo')
->for($asset, 'checkoutable')
->create();
$this->actingAs($admin)
->withSession([
'sign_in_place' => true,
'redirect_option' => 'target',
'checkout_to_type' => 'user',
])
->post(route('account.store-acceptance', $checkoutAcceptance), [
'asset_acceptance' => 'accepted',
])
->assertRedirectToRoute('account.accept')
->assertSessionHas('error');
}
public function test_stale_sign_in_place_post_with_missing_assignee_does_not_throw_route_error()
{
$admin = User::factory()->admin()->create();
$asset = Asset::factory()->create();
$assignee = User::factory()->create();
$checkoutAcceptance = CheckoutAcceptance::factory()
->accepted()
->for($assignee, 'assignedTo')
->for($asset, 'checkoutable')
->create();
CheckoutAcceptance::whereKey($checkoutAcceptance->id)->update(['assigned_to_id' => null]);
$this->actingAs($admin)
->withSession([
'sign_in_place' => true,
'redirect_option' => 'target',
'checkout_to_type' => 'user',
])
->post(route('account.store-acceptance', $checkoutAcceptance), [
'asset_acceptance' => 'accepted',
])
->assertRedirectToRoute('account.accept')
->assertSessionHas('error');
}
public function test_sign_in_place_acceptance_page_uses_checkout_flow_breadcrumbs()
{
$assignee = User::factory()->create();
$admin = User::factory()->admin()->create();
$asset = Asset::factory()->create();
$checkoutAcceptance = CheckoutAcceptance::factory()
->pending()
->for($assignee, 'assignedTo')
->for($asset, 'checkoutable')
->create();
$response = $this->actingAs($admin)
->withSession([
'sign_in_place_acceptance_id' => $checkoutAcceptance->id,
'sign_in_place_item_id' => $asset->id,
'sign_in_place_resource_type' => 'Assets',
])
->get(route('account.accept.item', $checkoutAcceptance));
$response->assertOk()
->assertSeeInOrder([
trans('general.assets'),
$asset->display_name,
trans('general.checkout'),
sprintf('%s for %s', trans('general.sign_in_place'), $assignee->display_name),
], false)
->assertSee(route('hardware.index'), false)
->assertSee(route('hardware.show', $asset), false)
->assertDontSee(route('users.show', $assignee), false)
->assertDontSee(route('hardware.checkout.create', $asset), false);
}
public function test_acceptance_create_page_shows_email_info_when_always_send_email_enabled()
{
$checkoutAcceptance = CheckoutAcceptance::factory()->pending()->create();
config(['app.always_send_email' => true]);
$response = $this->actingAs($checkoutAcceptance->assignedTo)
->get(route('account.accept.item', $checkoutAcceptance));
$response->assertOk();
$response->assertSee(trans('general.acceptance_email_always_sent'), false);
$response->assertDontSee(trans('mail.send_pdf_copy'), false);
}
public function test_acceptance_create_page_shows_checkbox_when_always_send_email_disabled()
{
$checkoutAcceptance = CheckoutAcceptance::factory()->pending()->create();
config(['app.always_send_email' => false]);
$response = $this->actingAs($checkoutAcceptance->assignedTo)
->get(route('account.accept.item', $checkoutAcceptance));
$response->assertOk();
$response->assertSee(trans('mail.send_pdf_copy'), false);
$response->assertDontSee(trans('general.acceptance_email_always_sent'), false);
}
public function test_acceptance_create_page_hides_checkbox_when_always_send_eula_enabled(): void
{
$checkoutAcceptance = CheckoutAcceptance::factory()->pending()->create();
config([
'app.always_send_email' => false,
'app.always_send_eula' => true,
]);
$response = $this->actingAs($checkoutAcceptance->assignedTo)
->get(route('account.accept.item', $checkoutAcceptance));
$response->assertOk();
$response->assertSee(trans('general.acceptance_email_always_sent'), false);
$response->assertDontSee(trans('mail.send_pdf_copy'), false);
}
public function test_acceptance_always_sends_copy_when_always_send_eula_enabled(): void
{
Notification::fake();
$checkoutAcceptance = CheckoutAcceptance::factory()->pending()->create();
config([
'app.always_send_email' => false,
'app.always_send_eula' => true,
]);
$this->actingAs($checkoutAcceptance->assignedTo)
->post(route('account.store-acceptance', $checkoutAcceptance), [
'asset_acceptance' => 'accepted',
])
->assertSessionHasNoErrors();
Notification::assertSentTo(
$checkoutAcceptance->assignedTo,
AcceptanceItemAcceptedToUserNotification::class
);
}
}