mirror of
https://github.com/snipe/snipe-it.git
synced 2024-12-25 05:34:06 -08:00
Merge pull request #14260 from marcusmoore/chore/sc-24808
Added tests around asset check in and added missing actions to the api controller action
This commit is contained in:
commit
a4941031cb
|
@ -4,6 +4,10 @@ namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
use App\Events\CheckoutableCheckedIn;
|
use App\Events\CheckoutableCheckedIn;
|
||||||
use App\Http\Requests\StoreAssetRequest;
|
use App\Http\Requests\StoreAssetRequest;
|
||||||
|
use App\Http\Traits\MigratesLegacyAssetLocations;
|
||||||
|
use App\Models\CheckoutAcceptance;
|
||||||
|
use App\Models\LicenseSeat;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Support\Facades\Crypt;
|
use Illuminate\Support\Facades\Crypt;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
@ -45,6 +49,8 @@ use Route;
|
||||||
*/
|
*/
|
||||||
class AssetsController extends Controller
|
class AssetsController extends Controller
|
||||||
{
|
{
|
||||||
|
use MigratesLegacyAssetLocations;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns JSON listing of all assets
|
* Returns JSON listing of all assets
|
||||||
*
|
*
|
||||||
|
@ -864,11 +870,9 @@ class AssetsController extends Controller
|
||||||
*/
|
*/
|
||||||
public function checkin(Request $request, $asset_id)
|
public function checkin(Request $request, $asset_id)
|
||||||
{
|
{
|
||||||
$this->authorize('checkin', Asset::class);
|
|
||||||
$asset = Asset::with('model')->findOrFail($asset_id);
|
$asset = Asset::with('model')->findOrFail($asset_id);
|
||||||
$this->authorize('checkin', $asset);
|
$this->authorize('checkin', $asset);
|
||||||
|
|
||||||
|
|
||||||
$target = $asset->assignedTo;
|
$target = $asset->assignedTo;
|
||||||
if (is_null($target)) {
|
if (is_null($target)) {
|
||||||
return response()->json(Helper::formatStandardApiResponse('error', [
|
return response()->json(Helper::formatStandardApiResponse('error', [
|
||||||
|
@ -881,7 +885,6 @@ class AssetsController extends Controller
|
||||||
$asset->expected_checkin = null;
|
$asset->expected_checkin = null;
|
||||||
//$asset->last_checkout = null;
|
//$asset->last_checkout = null;
|
||||||
$asset->last_checkin = now();
|
$asset->last_checkin = now();
|
||||||
$asset->assigned_to = null;
|
|
||||||
$asset->assignedTo()->disassociate($asset);
|
$asset->assignedTo()->disassociate($asset);
|
||||||
$asset->accepted = null;
|
$asset->accepted = null;
|
||||||
|
|
||||||
|
@ -889,10 +892,16 @@ class AssetsController extends Controller
|
||||||
$asset->name = $request->input('name');
|
$asset->name = $request->input('name');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->migrateLegacyLocations($asset);
|
||||||
|
|
||||||
$asset->location_id = $asset->rtd_location_id;
|
$asset->location_id = $asset->rtd_location_id;
|
||||||
|
|
||||||
if ($request->filled('location_id')) {
|
if ($request->filled('location_id')) {
|
||||||
$asset->location_id = $request->input('location_id');
|
$asset->location_id = $request->input('location_id');
|
||||||
|
|
||||||
|
if ($request->input('update_default_location')){
|
||||||
|
$asset->rtd_location_id = $request->input('location_id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->has('status_id')) {
|
if ($request->has('status_id')) {
|
||||||
|
@ -906,12 +915,22 @@ class AssetsController extends Controller
|
||||||
$originalValues['action_date'] = $checkin_at;
|
$originalValues['action_date'] = $checkin_at;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!empty($asset->licenseseats->all())){
|
$asset->licenseseats->each(function (LicenseSeat $seat) {
|
||||||
foreach ($asset->licenseseats as $seat){
|
$seat->update(['assigned_to' => null]);
|
||||||
$seat->assigned_to = null;
|
});
|
||||||
$seat->save();
|
|
||||||
}
|
// Get all pending Acceptances for this asset and delete them
|
||||||
}
|
CheckoutAcceptance::pending()
|
||||||
|
->whereHasMorph(
|
||||||
|
'checkoutable',
|
||||||
|
[Asset::class],
|
||||||
|
function (Builder $query) use ($asset) {
|
||||||
|
$query->where('id', $asset->id);
|
||||||
|
})
|
||||||
|
->get()
|
||||||
|
->map(function ($acceptance) {
|
||||||
|
$acceptance->delete();
|
||||||
|
});
|
||||||
|
|
||||||
if ($asset->save()) {
|
if ($asset->save()) {
|
||||||
event(new CheckoutableCheckedIn($asset, $target, Auth::user(), $request->input('note'), $checkin_at, $originalValues));
|
event(new CheckoutableCheckedIn($asset, $target, Auth::user(), $request->input('note'), $checkin_at, $originalValues));
|
||||||
|
|
|
@ -6,8 +6,10 @@ use App\Events\CheckoutableCheckedIn;
|
||||||
use App\Helpers\Helper;
|
use App\Helpers\Helper;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\AssetCheckinRequest;
|
use App\Http\Requests\AssetCheckinRequest;
|
||||||
|
use App\Http\Traits\MigratesLegacyAssetLocations;
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
use App\Models\CheckoutAcceptance;
|
use App\Models\CheckoutAcceptance;
|
||||||
|
use App\Models\LicenseSeat;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Redirect;
|
use Illuminate\Support\Facades\Redirect;
|
||||||
|
@ -15,6 +17,8 @@ use Illuminate\Support\Facades\View;
|
||||||
|
|
||||||
class AssetCheckinController extends Controller
|
class AssetCheckinController extends Controller
|
||||||
{
|
{
|
||||||
|
use MigratesLegacyAssetLocations;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a view that presents a form to check an asset back into inventory.
|
* Returns a view that presents a form to check an asset back into inventory.
|
||||||
*
|
*
|
||||||
|
@ -69,9 +73,7 @@ class AssetCheckinController extends Controller
|
||||||
$asset->expected_checkin = null;
|
$asset->expected_checkin = null;
|
||||||
//$asset->last_checkout = null;
|
//$asset->last_checkout = null;
|
||||||
$asset->last_checkin = now();
|
$asset->last_checkin = now();
|
||||||
$asset->assigned_to = null;
|
|
||||||
$asset->assignedTo()->disassociate($asset);
|
$asset->assignedTo()->disassociate($asset);
|
||||||
$asset->assigned_type = null;
|
|
||||||
$asset->accepted = null;
|
$asset->accepted = null;
|
||||||
$asset->name = $request->get('name');
|
$asset->name = $request->get('name');
|
||||||
|
|
||||||
|
@ -79,24 +81,7 @@ class AssetCheckinController extends Controller
|
||||||
$asset->status_id = e($request->get('status_id'));
|
$asset->status_id = e($request->get('status_id'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is just meant to correct legacy issues where some user data would have 0
|
$this->migrateLegacyLocations($asset);
|
||||||
// as a location ID, which isn't valid. Later versions of Snipe-IT have stricter validation
|
|
||||||
// rules, so it's necessary to fix this for long-time users. It's kinda gross, but will help
|
|
||||||
// people (and their data) in the long run
|
|
||||||
|
|
||||||
if ($asset->rtd_location_id == '0') {
|
|
||||||
\Log::debug('Manually override the RTD location IDs');
|
|
||||||
\Log::debug('Original RTD Location ID: '.$asset->rtd_location_id);
|
|
||||||
$asset->rtd_location_id = '';
|
|
||||||
\Log::debug('New RTD Location ID: '.$asset->rtd_location_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($asset->location_id == '0') {
|
|
||||||
\Log::debug('Manually override the location IDs');
|
|
||||||
\Log::debug('Original Location ID: '.$asset->location_id);
|
|
||||||
$asset->location_id = '';
|
|
||||||
\Log::debug('New Location ID: '.$asset->location_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
$asset->location_id = $asset->rtd_location_id;
|
$asset->location_id = $asset->rtd_location_id;
|
||||||
|
|
||||||
|
@ -117,12 +102,9 @@ class AssetCheckinController extends Controller
|
||||||
$checkin_at = $request->get('checkin_at');
|
$checkin_at = $request->get('checkin_at');
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!empty($asset->licenseseats->all())){
|
$asset->licenseseats->each(function (LicenseSeat $seat) {
|
||||||
foreach ($asset->licenseseats as $seat){
|
$seat->update(['assigned_to' => null]);
|
||||||
$seat->assigned_to = null;
|
});
|
||||||
$seat->save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all pending Acceptances for this asset and delete them
|
// Get all pending Acceptances for this asset and delete them
|
||||||
$acceptances = CheckoutAcceptance::pending()->whereHasMorph('checkoutable',
|
$acceptances = CheckoutAcceptance::pending()->whereHasMorph('checkoutable',
|
||||||
|
|
33
app/Http/Traits/MigratesLegacyAssetLocations.php
Normal file
33
app/Http/Traits/MigratesLegacyAssetLocations.php
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Traits;
|
||||||
|
|
||||||
|
use App\Models\Asset;
|
||||||
|
|
||||||
|
trait MigratesLegacyAssetLocations
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* This is just meant to correct legacy issues where some user data would have 0
|
||||||
|
* as a location ID, which isn't valid. Later versions of Snipe-IT have stricter validation
|
||||||
|
* rules, so it's necessary to fix this for long-time users. It's kinda gross, but will help
|
||||||
|
* people (and their data) in the long run
|
||||||
|
* @param Asset $asset
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function migrateLegacyLocations(Asset $asset): void
|
||||||
|
{
|
||||||
|
if ($asset->rtd_location_id == '0') {
|
||||||
|
\Log::debug('Manually override the RTD location IDs');
|
||||||
|
\Log::debug('Original RTD Location ID: ' . $asset->rtd_location_id);
|
||||||
|
$asset->rtd_location_id = '';
|
||||||
|
\Log::debug('New RTD Location ID: ' . $asset->rtd_location_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($asset->location_id == '0') {
|
||||||
|
\Log::debug('Manually override the location IDs');
|
||||||
|
\Log::debug('Original Location ID: ' . $asset->location_id);
|
||||||
|
$asset->location_id = '';
|
||||||
|
\Log::debug('New Location ID: ' . $asset->location_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,6 @@ use App\Models\Location;
|
||||||
use App\Models\Statuslabel;
|
use App\Models\Statuslabel;
|
||||||
use App\Models\Supplier;
|
use App\Models\Supplier;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Carbon\Carbon;
|
|
||||||
use Carbon\CarbonImmutable;
|
use Carbon\CarbonImmutable;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
@ -289,12 +288,13 @@ class AssetFactory extends Factory
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function assignedToUser()
|
public function assignedToUser(User $user = null)
|
||||||
{
|
{
|
||||||
return $this->state(function () {
|
return $this->state(function () use ($user) {
|
||||||
return [
|
return [
|
||||||
'assigned_to' => User::factory(),
|
'assigned_to' => $user->id ?? User::factory(),
|
||||||
'assigned_type' => User::class,
|
'assigned_type' => User::class,
|
||||||
|
'last_checkout' => now()->subDay(),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -352,4 +352,19 @@ class AssetFactory extends Factory
|
||||||
{
|
{
|
||||||
return $this->state(['requestable' => false]);
|
return $this->state(['requestable' => false]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This allows bypassing model level validation if you want to purposefully
|
||||||
|
* create an asset in an invalid state. Validation is turned back on
|
||||||
|
* after the model is created via the factory.
|
||||||
|
* @return AssetFactory
|
||||||
|
*/
|
||||||
|
public function canBeInvalidUponCreation()
|
||||||
|
{
|
||||||
|
return $this->afterMaking(function (Asset $asset) {
|
||||||
|
$asset->setValidating(false);
|
||||||
|
})->afterCreating(function (Asset $asset) {
|
||||||
|
$asset->setValidating(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
namespace Database\Factories;
|
namespace Database\Factories;
|
||||||
|
|
||||||
use App\Models\License;
|
use App\Models\License;
|
||||||
|
use App\Models\User;
|
||||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
class LicenseSeatFactory extends Factory
|
class LicenseSeatFactory extends Factory
|
||||||
|
@ -13,4 +14,13 @@ class LicenseSeatFactory extends Factory
|
||||||
'license_id' => License::factory(),
|
'license_id' => License::factory(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function assignedToUser(User $user = null)
|
||||||
|
{
|
||||||
|
return $this->state(function () use ($user) {
|
||||||
|
return [
|
||||||
|
'assigned_to' => $user->id ?? User::factory(),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,15 @@
|
||||||
|
|
||||||
namespace Tests\Feature\Api\Assets;
|
namespace Tests\Feature\Api\Assets;
|
||||||
|
|
||||||
|
use App\Events\CheckoutableCheckedIn;
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
|
use App\Models\CheckoutAcceptance;
|
||||||
|
use App\Models\LicenseSeat;
|
||||||
|
use App\Models\Location;
|
||||||
|
use App\Models\Statuslabel;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
use Tests\Support\InteractsWithSettings;
|
use Tests\Support\InteractsWithSettings;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
@ -11,20 +18,150 @@ class AssetCheckinTest extends TestCase
|
||||||
{
|
{
|
||||||
use InteractsWithSettings;
|
use InteractsWithSettings;
|
||||||
|
|
||||||
public function testLastCheckInFieldIsSetOnCheckin()
|
public function testCheckingInAssetRequiresCorrectPermission()
|
||||||
{
|
{
|
||||||
$admin = User::factory()->superuser()->create();
|
$this->actingAsForApi(User::factory()->create())
|
||||||
$asset = Asset::factory()->create(['last_checkin' => null]);
|
->postJson(route('api.asset.checkin', Asset::factory()->assignedToUser()->create()))
|
||||||
|
->assertForbidden();
|
||||||
|
}
|
||||||
|
|
||||||
$asset->checkOut(User::factory()->create(), $admin, now());
|
public function testCannotCheckInNonExistentAsset()
|
||||||
|
{
|
||||||
|
$this->actingAsForApi(User::factory()->checkinAssets()->create())
|
||||||
|
->postJson(route('api.asset.checkin', ['id' => 'does-not-exist']))
|
||||||
|
->assertStatusMessageIs('error');
|
||||||
|
}
|
||||||
|
|
||||||
$this->actingAsForApi($admin)
|
public function testCannotCheckInAssetThatIsNotCheckedOut()
|
||||||
->postJson(route('api.asset.checkin', $asset))
|
{
|
||||||
|
$this->actingAsForApi(User::factory()->checkinAssets()->create())
|
||||||
|
->postJson(route('api.asset.checkin', Asset::factory()->create()->id))
|
||||||
|
->assertStatusMessageIs('error');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAssetCanBeCheckedIn()
|
||||||
|
{
|
||||||
|
Event::fake([CheckoutableCheckedIn::class]);
|
||||||
|
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$location = Location::factory()->create();
|
||||||
|
$status = Statuslabel::factory()->create();
|
||||||
|
$asset = Asset::factory()->assignedToUser($user)->create([
|
||||||
|
'expected_checkin' => now()->addDay(),
|
||||||
|
'last_checkin' => null,
|
||||||
|
'accepted' => 'accepted',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertTrue($asset->assignedTo->is($user));
|
||||||
|
|
||||||
|
$currentTimestamp = now();
|
||||||
|
|
||||||
|
$this->actingAsForApi(User::factory()->checkinAssets()->create())
|
||||||
|
->postJson(route('api.asset.checkin', $asset), [
|
||||||
|
'name' => 'Changed Name',
|
||||||
|
'status_id' => $status->id,
|
||||||
|
'location_id' => $location->id,
|
||||||
|
])
|
||||||
->assertOk();
|
->assertOk();
|
||||||
|
|
||||||
$this->assertNotNull(
|
$this->assertNull($asset->refresh()->assignedTo);
|
||||||
$asset->fresh()->last_checkin,
|
$this->assertNull($asset->expected_checkin);
|
||||||
'last_checkin field should be set on checkin'
|
$this->assertNull($asset->last_checkout);
|
||||||
);
|
$this->assertNotNull($asset->last_checkin);
|
||||||
|
$this->assertNull($asset->assignedTo);
|
||||||
|
$this->assertNull($asset->assigned_type);
|
||||||
|
$this->assertNull($asset->accepted);
|
||||||
|
$this->assertEquals('Changed Name', $asset->name);
|
||||||
|
$this->assertEquals($status->id, $asset->status_id);
|
||||||
|
$this->assertTrue($asset->location()->is($location));
|
||||||
|
|
||||||
|
Event::assertDispatched(function (CheckoutableCheckedIn $event) use ($currentTimestamp) {
|
||||||
|
// this could be better mocked but is ok for now.
|
||||||
|
return Carbon::parse($event->action_date)->diffInSeconds($currentTimestamp) < 2;
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLocationIsSetToRTDLocationByDefaultUponCheckin()
|
||||||
|
{
|
||||||
|
$rtdLocation = Location::factory()->create();
|
||||||
|
$asset = Asset::factory()->assignedToUser()->create([
|
||||||
|
'location_id' => Location::factory()->create()->id,
|
||||||
|
'rtd_location_id' => $rtdLocation->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAsForApi(User::factory()->checkinAssets()->create())
|
||||||
|
->postJson(route('api.asset.checkin', $asset->id));
|
||||||
|
|
||||||
|
$this->assertTrue($asset->refresh()->location()->is($rtdLocation));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefaultLocationCanBeUpdatedUponCheckin()
|
||||||
|
{
|
||||||
|
$location = Location::factory()->create();
|
||||||
|
$asset = Asset::factory()->assignedToUser()->create();
|
||||||
|
|
||||||
|
$this->actingAsForApi(User::factory()->checkinAssets()->create())
|
||||||
|
->postJson(route('api.asset.checkin', $asset), [
|
||||||
|
'location_id' => $location->id,
|
||||||
|
'update_default_location' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertTrue($asset->refresh()->defaultLoc()->is($location));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAssetsLicenseSeatsAreClearedUponCheckin()
|
||||||
|
{
|
||||||
|
$asset = Asset::factory()->assignedToUser()->create();
|
||||||
|
LicenseSeat::factory()->assignedToUser()->for($asset)->create();
|
||||||
|
|
||||||
|
$this->assertNotNull($asset->licenseseats->first()->assigned_to);
|
||||||
|
|
||||||
|
$this->actingAsForApi(User::factory()->checkinAssets()->create())
|
||||||
|
->postJson(route('api.asset.checkin', $asset));
|
||||||
|
|
||||||
|
$this->assertNull($asset->refresh()->licenseseats->first()->assigned_to);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLegacyLocationValuesSetToZeroAreUpdated()
|
||||||
|
{
|
||||||
|
$asset = Asset::factory()->canBeInvalidUponCreation()->assignedToUser()->create([
|
||||||
|
'rtd_location_id' => 0,
|
||||||
|
'location_id' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAsForApi(User::factory()->checkinAssets()->create())
|
||||||
|
->postJson(route('api.asset.checkin', $asset));
|
||||||
|
|
||||||
|
$this->assertNull($asset->refresh()->rtd_location_id);
|
||||||
|
$this->assertEquals($asset->location_id, $asset->rtd_location_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPendingCheckoutAcceptancesAreClearedUponCheckin()
|
||||||
|
{
|
||||||
|
$asset = Asset::factory()->assignedToUser()->create();
|
||||||
|
|
||||||
|
$acceptance = CheckoutAcceptance::factory()->for($asset, 'checkoutable')->pending()->create();
|
||||||
|
|
||||||
|
$this->actingAsForApi(User::factory()->checkinAssets()->create())
|
||||||
|
->postJson(route('api.asset.checkin', $asset));
|
||||||
|
|
||||||
|
$this->assertFalse($acceptance->exists(), 'Acceptance was not deleted');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCheckinTimeAndActionLogNoteCanBeSet()
|
||||||
|
{
|
||||||
|
Event::fake();
|
||||||
|
|
||||||
|
$this->actingAsForApi(User::factory()->checkinAssets()->create())
|
||||||
|
->postJson(route('api.asset.checkin', Asset::factory()->assignedToUser()->create()), [
|
||||||
|
// time is appended to the provided date in controller
|
||||||
|
'checkin_at' => '2023-01-02',
|
||||||
|
'note' => 'hi there',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Event::assertDispatched(function (CheckoutableCheckedIn $event) {
|
||||||
|
return Carbon::parse('2023-01-02')->isSameDay(Carbon::parse($event->action_date))
|
||||||
|
&& $event->note === 'hi there';
|
||||||
|
}, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Assets;
|
|
||||||
|
|
||||||
use App\Models\Asset;
|
|
||||||
use App\Models\User;
|
|
||||||
use Tests\Support\InteractsWithSettings;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class AssetCheckinTest extends TestCase
|
|
||||||
{
|
|
||||||
use InteractsWithSettings;
|
|
||||||
|
|
||||||
public function testLastCheckInFieldIsSetOnCheckin()
|
|
||||||
{
|
|
||||||
$admin = User::factory()->superuser()->create();
|
|
||||||
$asset = Asset::factory()->create(['last_checkin' => null]);
|
|
||||||
|
|
||||||
$asset->checkOut(User::factory()->create(), $admin, now());
|
|
||||||
|
|
||||||
$this->actingAs($admin)
|
|
||||||
->post(route('hardware.checkin.store', [
|
|
||||||
'assetId' => $asset->id,
|
|
||||||
]))
|
|
||||||
->assertRedirect();
|
|
||||||
|
|
||||||
$this->assertNotNull(
|
|
||||||
$asset->fresh()->last_checkin,
|
|
||||||
'last_checkin field should be set on checkin'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
167
tests/Feature/Checkins/AssetCheckinTest.php
Normal file
167
tests/Feature/Checkins/AssetCheckinTest.php
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Checkins;
|
||||||
|
|
||||||
|
use App\Events\CheckoutableCheckedIn;
|
||||||
|
use App\Models\Asset;
|
||||||
|
use App\Models\CheckoutAcceptance;
|
||||||
|
use App\Models\LicenseSeat;
|
||||||
|
use App\Models\Location;
|
||||||
|
use App\Models\Statuslabel;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Tests\Support\InteractsWithSettings;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class AssetCheckinTest extends TestCase
|
||||||
|
{
|
||||||
|
use InteractsWithSettings;
|
||||||
|
|
||||||
|
public function testCheckingInAssetRequiresCorrectPermission()
|
||||||
|
{
|
||||||
|
$this->actingAs(User::factory()->create())
|
||||||
|
->post(route('hardware.checkin.store', [
|
||||||
|
'assetId' => Asset::factory()->assignedToUser()->create()->id,
|
||||||
|
]))
|
||||||
|
->assertForbidden();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCannotCheckInAssetThatIsNotCheckedOut()
|
||||||
|
{
|
||||||
|
$this->actingAs(User::factory()->checkinAssets()->create())
|
||||||
|
->post(route('hardware.checkin.store', ['assetId' => Asset::factory()->create()->id]))
|
||||||
|
->assertSessionHas('error')
|
||||||
|
->assertRedirect(route('hardware.index'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAssetCanBeCheckedIn()
|
||||||
|
{
|
||||||
|
Event::fake([CheckoutableCheckedIn::class]);
|
||||||
|
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$location = Location::factory()->create();
|
||||||
|
$status = Statuslabel::first() ?? Statuslabel::factory()->create();
|
||||||
|
$asset = Asset::factory()->assignedToUser($user)->create([
|
||||||
|
'expected_checkin' => now()->addDay(),
|
||||||
|
'last_checkin' => null,
|
||||||
|
'accepted' => 'accepted',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertTrue($asset->assignedTo->is($user));
|
||||||
|
|
||||||
|
$currentTimestamp = now();
|
||||||
|
|
||||||
|
$this->actingAs(User::factory()->checkinAssets()->create())
|
||||||
|
->post(
|
||||||
|
route('hardware.checkin.store', ['assetId' => $asset->id, 'backto' => 'user']),
|
||||||
|
[
|
||||||
|
'name' => 'Changed Name',
|
||||||
|
'status_id' => $status->id,
|
||||||
|
'location_id' => $location->id,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
->assertRedirect(route('users.show', $user));
|
||||||
|
|
||||||
|
$this->assertNull($asset->refresh()->assignedTo);
|
||||||
|
$this->assertNull($asset->expected_checkin);
|
||||||
|
$this->assertNull($asset->last_checkout);
|
||||||
|
$this->assertNotNull($asset->last_checkin);
|
||||||
|
$this->assertNull($asset->assignedTo);
|
||||||
|
$this->assertNull($asset->assigned_type);
|
||||||
|
$this->assertNull($asset->accepted);
|
||||||
|
$this->assertEquals('Changed Name', $asset->name);
|
||||||
|
$this->assertEquals($status->id, $asset->status_id);
|
||||||
|
$this->assertTrue($asset->location()->is($location));
|
||||||
|
|
||||||
|
Event::assertDispatched(function (CheckoutableCheckedIn $event) use ($currentTimestamp) {
|
||||||
|
// this could be better mocked but is ok for now.
|
||||||
|
return Carbon::parse($event->action_date)->diffInSeconds($currentTimestamp) < 2;
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLocationIsSetToRTDLocationByDefaultUponCheckin()
|
||||||
|
{
|
||||||
|
$rtdLocation = Location::factory()->create();
|
||||||
|
$asset = Asset::factory()->assignedToUser()->create([
|
||||||
|
'location_id' => Location::factory()->create()->id,
|
||||||
|
'rtd_location_id' => $rtdLocation->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs(User::factory()->checkinAssets()->create())
|
||||||
|
->post(route('hardware.checkin.store', ['assetId' => $asset->id]));
|
||||||
|
|
||||||
|
$this->assertTrue($asset->refresh()->location()->is($rtdLocation));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDefaultLocationCanBeUpdatedUponCheckin()
|
||||||
|
{
|
||||||
|
$location = Location::factory()->create();
|
||||||
|
$asset = Asset::factory()->assignedToUser()->create();
|
||||||
|
|
||||||
|
$this->actingAs(User::factory()->checkinAssets()->create())
|
||||||
|
->post(route('hardware.checkin.store', ['assetId' => $asset->id]), [
|
||||||
|
'location_id' => $location->id,
|
||||||
|
'update_default_location' => 0
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertTrue($asset->refresh()->defaultLoc()->is($location));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAssetsLicenseSeatsAreClearedUponCheckin()
|
||||||
|
{
|
||||||
|
$asset = Asset::factory()->assignedToUser()->create();
|
||||||
|
LicenseSeat::factory()->assignedToUser()->for($asset)->create();
|
||||||
|
|
||||||
|
$this->assertNotNull($asset->licenseseats->first()->assigned_to);
|
||||||
|
|
||||||
|
$this->actingAs(User::factory()->checkinAssets()->create())
|
||||||
|
->post(route('hardware.checkin.store', ['assetId' => $asset->id]));
|
||||||
|
|
||||||
|
$this->assertNull($asset->refresh()->licenseseats->first()->assigned_to);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLegacyLocationValuesSetToZeroAreUpdated()
|
||||||
|
{
|
||||||
|
$asset = Asset::factory()->canBeInvalidUponCreation()->assignedToUser()->create([
|
||||||
|
'rtd_location_id' => 0,
|
||||||
|
'location_id' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs(User::factory()->checkinAssets()->create())
|
||||||
|
->post(route('hardware.checkin.store', ['assetId' => $asset->id]));
|
||||||
|
|
||||||
|
$this->assertNull($asset->refresh()->rtd_location_id);
|
||||||
|
$this->assertEquals($asset->location_id, $asset->rtd_location_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPendingCheckoutAcceptancesAreClearedUponCheckin()
|
||||||
|
{
|
||||||
|
$asset = Asset::factory()->assignedToUser()->create();
|
||||||
|
|
||||||
|
$acceptance = CheckoutAcceptance::factory()->for($asset, 'checkoutable')->pending()->create();
|
||||||
|
|
||||||
|
$this->actingAs(User::factory()->checkinAssets()->create())
|
||||||
|
->post(route('hardware.checkin.store', ['assetId' => $asset->id]));
|
||||||
|
|
||||||
|
$this->assertFalse($acceptance->exists(), 'Acceptance was not deleted');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCheckinTimeAndActionLogNoteCanBeSet()
|
||||||
|
{
|
||||||
|
Event::fake([CheckoutableCheckedIn::class]);
|
||||||
|
|
||||||
|
$this->actingAs(User::factory()->checkinAssets()->create())
|
||||||
|
->post(route(
|
||||||
|
'hardware.checkin.store',
|
||||||
|
['assetId' => Asset::factory()->assignedToUser()->create()->id]
|
||||||
|
), [
|
||||||
|
'checkin_at' => '2023-01-02',
|
||||||
|
'note' => 'hello'
|
||||||
|
]);
|
||||||
|
|
||||||
|
Event::assertDispatched(function (CheckoutableCheckedIn $event) {
|
||||||
|
return $event->action_date === '2023-01-02' && $event->note === 'hello';
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -108,6 +108,54 @@ class AssetWebhookTest extends TestCase
|
||||||
Notification::assertNotSentTo(new AnonymousNotifiable, CheckinAssetNotification::class);
|
Notification::assertNotSentTo(new AnonymousNotifiable, CheckinAssetNotification::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testCheckInEmailSentToUserIfSettingEnabled()
|
||||||
|
{
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$asset = Asset::factory()->assignedToUser($user)->create();
|
||||||
|
|
||||||
|
$asset->model->category->update(['checkin_email' => true]);
|
||||||
|
|
||||||
|
event(new CheckoutableCheckedIn(
|
||||||
|
$asset,
|
||||||
|
$user,
|
||||||
|
User::factory()->checkinAssets()->create(),
|
||||||
|
''
|
||||||
|
));
|
||||||
|
|
||||||
|
Notification::assertSentTo(
|
||||||
|
[$user],
|
||||||
|
function (CheckinAssetNotification $notification, $channels) {
|
||||||
|
return in_array('mail', $channels);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCheckInEmailNotSentToUserIfSettingDisabled()
|
||||||
|
{
|
||||||
|
Notification::fake();
|
||||||
|
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$asset = Asset::factory()->assignedToUser($user)->create();
|
||||||
|
|
||||||
|
$asset->model->category->update(['checkin_email' => false]);
|
||||||
|
|
||||||
|
event(new CheckoutableCheckedIn(
|
||||||
|
$asset,
|
||||||
|
$user,
|
||||||
|
User::factory()->checkinAssets()->create(),
|
||||||
|
''
|
||||||
|
));
|
||||||
|
|
||||||
|
Notification::assertNotSentTo(
|
||||||
|
[$user],
|
||||||
|
function (CheckinAssetNotification $notification, $channels) {
|
||||||
|
return in_array('mail', $channels);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private function createAsset()
|
private function createAsset()
|
||||||
{
|
{
|
||||||
return Asset::factory()->laptopMbp()->create();
|
return Asset::factory()->laptopMbp()->create();
|
||||||
|
|
Loading…
Reference in a new issue