requestable()->create(); $nonRequestable = Asset::factory()->nonrequestable()->create(); $this->actingAsForApi(User::factory()->create()) ->post(route('api.assets.requests.store', $requestable->id)) ->assertStatusMessageIs('success'); $this->actingAsForApi(User::factory()->create()) ->post(route('api.assets.requests.store', $nonRequestable->id)) ->assertStatusMessageIs('error'); $this->assertHasTheseActionLogs($requestable, ['create', 'requested', 'update']); // FIXME - is this right?! } public function test_checking_out_asset_requires_correct_permission() { $this->actingAsForApi(User::factory()->create()) ->postJson(route('api.asset.checkout', Asset::factory()->create()), [ 'checkout_to_type' => 'user', 'assigned_user' => User::factory()->create()->id, ]) ->assertForbidden(); } public function test_non_existent_asset_cannot_be_checked_out() { $this->actingAsForApi(User::factory()->checkoutAssets()->create()) ->postJson(route('api.asset.checkout', 1000), [ 'checkout_to_type' => 'user', 'assigned_user' => User::factory()->create()->id, ]) ->assertStatusMessageIs('error'); } public function test_asset_not_available_for_checkout_cannot_be_checked_out() { $assetAlreadyCheckedOut = Asset::factory()->assignedToUser()->create(); $this->actingAsForApi(User::factory()->checkoutAssets()->create()) ->postJson(route('api.asset.checkout', $assetAlreadyCheckedOut), [ 'checkout_to_type' => 'user', 'assigned_user' => User::factory()->create()->id, ]) ->assertStatusMessageIs('error'); } public function test_asset_cannot_be_checked_out_to_itself() { $asset = Asset::factory()->create(); $this->actingAsForApi(User::factory()->checkoutAssets()->create()) ->postJson(route('api.asset.checkout', $asset), [ 'checkout_to_type' => 'asset', 'assigned_asset' => $asset->id, ]) ->assertStatusMessageIs('error'); } public function test_validation_when_checking_out_asset() { $this->actingAsForApi(User::factory()->checkoutAssets()->create()) ->postJson(route('api.asset.checkout', Asset::factory()->create()), []) ->assertStatusMessageIs('error'); Event::assertNotDispatched(CheckoutableCheckedOut::class); } public function test_cannot_checkout_across_companies_when_full_company_support_enabled() { $this->settings->enableMultipleFullCompanySupport(); [$companyA, $companyB] = Company::factory()->count(2)->create(); $actorInCompanyA = User::factory()->checkoutAssets()->for($companyA)->create(); $assetInCompanyA = Asset::factory()->for($companyA)->create(); $userInCompanyB = User::factory()->for($companyB)->create(); $this->actingAsForApi($actorInCompanyA) ->postJson(route('api.asset.checkout', $assetInCompanyA), [ 'checkout_to_type' => 'user', 'assigned_user' => $userInCompanyB->id, ]) ->assertOk() ->assertStatusMessageIs('error') ->assertMessagesAre(trans('general.error_user_company')); $assetInCompanyA->refresh(); $this->assertNull($assetInCompanyA->assigned_to); $this->assertNull($assetInCompanyA->assigned_type); $this->assertEquals(0, $assetInCompanyA->checkout_counter); $this->assertDatabaseMissing('action_logs', [ 'created_by' => $actorInCompanyA->id, 'action_type' => 'checkout', 'target_type' => User::class, 'target_id' => $userInCompanyB->id, 'item_type' => Asset::class, 'item_id' => $assetInCompanyA->id, ]); } public function test_checkout_by_tag_cannot_checkout_across_companies_when_full_company_support_enabled() { $this->settings->enableMultipleFullCompanySupport(); [$companyA, $companyB] = Company::factory()->count(2)->create(); $actorInCompanyA = User::factory()->checkoutAssets()->for($companyA)->create(); $assetInCompanyA = Asset::factory()->for($companyA)->create(); $userInCompanyB = User::factory()->for($companyB)->create(); $this->actingAsForApi($actorInCompanyA) ->postJson(route('api.assets.checkout.bytag', $assetInCompanyA->asset_tag), [ 'checkout_to_type' => 'user', 'assigned_user' => $userInCompanyB->id, ]) ->assertOk() ->assertStatusMessageIs('error') ->assertMessagesAre(trans('general.error_user_company')); $assetInCompanyA->refresh(); $this->assertNull($assetInCompanyA->assigned_to); $this->assertNull($assetInCompanyA->assigned_type); $this->assertEquals(0, $assetInCompanyA->checkout_counter); $this->assertDatabaseMissing('action_logs', [ 'created_by' => $actorInCompanyA->id, 'action_type' => 'checkout', 'target_type' => User::class, 'target_id' => $userInCompanyB->id, 'item_type' => Asset::class, 'item_id' => $assetInCompanyA->id, ]); } /** * This data provider contains checkout targets along with the * asset's expected location after the checkout process. */ public static function checkoutTargets(): array { return [ 'Checkout to User' => [ function () { $userLocation = Location::factory()->create(); $user = User::factory()->for($userLocation)->create(); return [ 'checkout_type' => 'user', 'target' => $user, 'expected_location' => $userLocation, ]; }, ], 'Checkout to User without location set' => [ function () { $userLocation = Location::factory()->create(); $user = User::factory()->for($userLocation)->create(['location_id' => null]); return [ 'checkout_type' => 'user', 'target' => $user, 'expected_location' => null, ]; }, ], 'Checkout to Asset with location set' => [ function () { $rtdLocation = Location::factory()->create(); $location = Location::factory()->create(); $asset = Asset::factory()->for($location)->for($rtdLocation, 'defaultLoc')->create(); return [ 'checkout_type' => 'asset', 'target' => $asset, 'expected_location' => $location, ]; }, ], 'Checkout to Asset without location set' => [ function () { $rtdLocation = Location::factory()->create(); $asset = Asset::factory()->for($rtdLocation, 'defaultLoc')->create(['location_id' => null]); return [ 'checkout_type' => 'asset', 'target' => $asset, 'expected_location' => null, ]; }, ], 'Checkout to Location' => [ function () { $location = Location::factory()->create(); return [ 'checkout_type' => 'location', 'target' => $location, 'expected_location' => $location, ]; }, ], ]; } #[DataProvider('checkoutTargets')] public function test_asset_can_be_checked_out($data) { ['checkout_type' => $type, 'target' => $target, 'expected_location' => $expectedLocation] = $data(); $newStatus = Statuslabel::factory()->readyToDeploy()->create(); $asset = Asset::factory()->forLocation()->create(); $admin = User::factory()->checkoutAssets()->create(); $this->actingAsForApi($admin) ->postJson(route('api.asset.checkout', $asset), [ 'checkout_to_type' => $type, 'assigned_'.$type => $target->id, 'status_id' => $newStatus->id, 'checkout_at' => '2024-04-01', 'expected_checkin' => '2024-04-08', 'name' => 'Changed Name', 'note' => 'Here is a cool note!', ]) ->assertOk(); $asset->refresh(); $this->assertTrue($asset->assignedTo()->is($target)); $this->assertEquals('Changed Name', $asset->name); $this->assertTrue($asset->status->is($newStatus)); $this->assertEquals('2024-04-01 00:00:00', $asset->last_checkout); $this->assertEquals('2024-04-08 00:00:00', (string) $asset->expected_checkin); $expectedLocation ? $this->assertTrue($asset->location->is($expectedLocation)) : $this->assertNull($asset->location); Event::assertDispatched(CheckoutableCheckedOut::class, 1); Event::assertDispatched(function (CheckoutableCheckedOut $event) use ($admin, $asset, $target) { $this->assertTrue($event->checkoutable->is($asset)); $this->assertTrue($event->checkedOutTo->is($target)); $this->assertTrue($event->checkedOutBy->is($admin)); $this->assertEquals('Here is a cool note!', $event->note); return true; }); } public function test_asset_can_be_checked_out_to_user_in_same_company_via_pivot_when_fmcs_enabled() { // Regression: company check used to compare asset company_id to user's primary company_id only. // Users assigned to multiple companies via the pivot table must be able to receive assets // from any of their companies — not just their first/primary one. $this->settings->enableMultipleFullCompanySupport(); [$companyA, $companyB, $companyC] = Company::factory()->count(3)->create(); // Actor is in companyC (same as the asset) so FMCS scoping lets them see and checkout it. $actor = User::factory()->checkoutAssets()->for($companyC)->create(); $assetInCompanyC = Asset::factory()->for($companyC)->create(); // Target user's primary company is A, but they also belong to C via pivot. $target = User::factory()->for($companyA)->create(); $target->companies()->sync([$companyA->id, $companyB->id, $companyC->id]); $this->actingAsForApi($actor) ->postJson(route('api.asset.checkout', $assetInCompanyC), [ 'checkout_to_type' => 'user', 'assigned_user' => $target->id, ]) ->assertOk() ->assertStatusMessageIs('success'); $assetInCompanyC->refresh(); $this->assertEquals($target->id, $assetInCompanyC->assigned_to); } public function test_asset_cannot_be_checked_out_to_user_whose_companies_exclude_asset_company_when_fmcs_enabled() { $this->settings->enableMultipleFullCompanySupport(); [$companyA, $companyB, $companyC] = Company::factory()->count(3)->create(); // Actor is in companyC (same as the asset). $actor = User::factory()->checkoutAssets()->for($companyC)->create(); $assetInCompanyC = Asset::factory()->for($companyC)->create(); // Target belongs to A and B — not C. Checkout to them should be blocked. $target = User::factory()->for($companyA)->create(); $target->companies()->sync([$companyA->id, $companyB->id]); $this->actingAsForApi($actor) ->postJson(route('api.asset.checkout', $assetInCompanyC), [ 'checkout_to_type' => 'user', 'assigned_user' => $target->id, ]) ->assertOk() ->assertStatusMessageIs('error') ->assertMessagesAre(trans('general.error_user_company')); $assetInCompanyC->refresh(); $this->assertNull($assetInCompanyC->assigned_to); } public function test_asset_can_be_checked_out_to_user_with_no_company_when_fmcs_enabled() { // In floater mode, users with no company associations can receive items from any company. $this->settings->enableFloaterMode(); $company = Company::factory()->create(); // Actor is in the same company as the asset. $actor = User::factory()->checkoutAssets()->for($company)->create(); $assetInCompany = Asset::factory()->for($company)->create(); $target = User::factory()->create(['company_id' => null]); $target->companies()->sync([]); $this->actingAsForApi($actor) ->postJson(route('api.asset.checkout', $assetInCompany), [ 'checkout_to_type' => 'user', 'assigned_user' => $target->id, ]) ->assertOk() ->assertStatusMessageIs('success'); $assetInCompany->refresh(); $this->assertEquals($target->id, $assetInCompany->assigned_to); } public function test_license_seats_are_assigned_to_user_upon_checkout() { $this->markTestIncomplete('This is not implemented'); } public function test_last_checkout_uses_current_date_if_not_provided() { $asset = Asset::factory()->create(['last_checkout' => now()->subMonth()]); $this->actingAsForApi(User::factory()->checkoutAssets()->create()) ->postJson(route('api.asset.checkout', $asset), [ 'checkout_to_type' => 'user', 'assigned_user' => User::factory()->create()->id, ]); $asset->refresh(); $this->assertTrue((int) Carbon::parse($asset->last_checkout)->diffInSeconds(now(), true) < 2); } public function test_api_checkout_can_update_requestable_when_field_is_passed() { $asset = Asset::factory()->create(['requestable' => 1]); $targetUser = User::factory()->create(); $this->actingAsForApi(User::factory()->checkoutAssets()->create()) ->postJson(route('api.asset.checkout', $asset), [ 'checkout_to_type' => 'user', 'assigned_user' => $targetUser->id, 'requestable' => 0, ]) ->assertStatusMessageIs('success'); $this->assertFalse((bool) $asset->fresh()->requestable); } public function test_api_checkout_leaves_requestable_unchanged_when_field_is_omitted() { $asset = Asset::factory()->create(['requestable' => 1]); $targetUser = User::factory()->create(); $this->actingAsForApi(User::factory()->checkoutAssets()->create()) ->postJson(route('api.asset.checkout', $asset), [ 'checkout_to_type' => 'user', 'assigned_user' => $targetUser->id, ]) ->assertStatusMessageIs('success'); $this->assertTrue((bool) $asset->fresh()->requestable); } public function test_null_company_asset_cannot_be_checked_out_to_companied_user_when_fmcs_enabled_without_floater() { $this->settings->enableMultipleFullCompanySupport(); $this->settings->disableFloaterMode(); $company = Company::factory()->create(); $actor = User::factory()->superuser()->create(); $nullCompanyAsset = Asset::factory()->create(['company_id' => null]); $companiedUser = User::factory()->for($company)->create(); $this->actingAsForApi($actor) ->postJson(route('api.asset.checkout', $nullCompanyAsset), [ 'checkout_to_type' => 'user', 'assigned_user' => $companiedUser->id, ]) ->assertOk() ->assertStatusMessageIs('error') ->assertMessagesAre(trans('general.error_user_company')); $this->assertNull($nullCompanyAsset->fresh()->assigned_to); } public function test_null_company_asset_can_be_checked_out_to_companied_user_when_floater_enabled() { $this->settings->enableFloaterMode(); $company = Company::factory()->create(); $actor = User::factory()->superuser()->create(); $nullCompanyAsset = Asset::factory()->create(['company_id' => null]); $companiedUser = User::factory()->for($company)->create(); $this->actingAsForApi($actor) ->postJson(route('api.asset.checkout', $nullCompanyAsset), [ 'checkout_to_type' => 'user', 'assigned_user' => $companiedUser->id, ]) ->assertOk() ->assertStatusMessageIs('success'); $this->assertEquals($companiedUser->id, $nullCompanyAsset->fresh()->assigned_to); } }