diff --git a/.env.example b/.env.example index 5c0a0b8abc..426af4ff88 100644 --- a/.env.example +++ b/.env.example @@ -87,6 +87,7 @@ SESSION_LIFETIME=12000 EXPIRE_ON_CLOSE=false ENCRYPT=false COOKIE_NAME=snipeit_session +PASSPORT_COOKIE_NAME='snipeit_passport_token' COOKIE_DOMAIN=null SECURE_COOKIES=false API_TOKEN_EXPIRATION_YEARS=15 diff --git a/app/Http/Controllers/Api/AssetsController.php b/app/Http/Controllers/Api/AssetsController.php index f0ac97b7f9..8361971d5d 100644 --- a/app/Http/Controllers/Api/AssetsController.php +++ b/app/Http/Controllers/Api/AssetsController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api; use App\Events\CheckoutableCheckedIn; use App\Http\Requests\StoreAssetRequest; +use App\Http\Requests\UpdateAssetRequest; use App\Http\Traits\MigratesLegacyAssetLocations; use App\Models\CheckoutAcceptance; use App\Models\LicenseSeat; @@ -651,36 +652,35 @@ class AssetsController extends Controller * Accepts a POST request to update an asset * * @author [A. Gianotto] [] - * @param \App\Http\Requests\ImageUploadRequest $request * @since [v4.0] */ - public function update(ImageUploadRequest $request, $id) : JsonResponse + public function update(UpdateAssetRequest $request, Asset $asset): JsonResponse { - $this->authorize('update', Asset::class); + $asset->fill($request->validated()); - if ($asset = Asset::find($id)) { - $asset->fill($request->all()); + if ($request->has('model_id')) { + $asset->model()->associate(AssetModel::find($request->validated()['model_id'])); + } + if ($request->has('company_id')) { + $asset->company_id = Company::getIdForCurrentUser($request->validated()['company_id']); + } + if ($request->has('rtd_location_id') && !$request->has('location_id')) { + $asset->location_id = $request->validated()['rtd_location_id']; + } + if ($request->input('last_audit_date')) { + $asset->last_audit_date = Carbon::parse($request->input('last_audit_date'))->startOfDay()->format('Y-m-d H:i:s'); + } - ($request->filled('model_id')) ? - $asset->model()->associate(AssetModel::find($request->get('model_id'))) : null; - ($request->filled('rtd_location_id')) ? - $asset->location_id = $request->get('rtd_location_id') : ''; - ($request->filled('company_id')) ? - $asset->company_id = Company::getIdForCurrentUser($request->get('company_id')) : ''; + /** + * this is here just legacy reasons. Api\AssetController + * used image_source once to allow encoded image uploads. + */ + if ($request->has('image_source')) { + $request->offsetSet('image', $request->offsetGet('image_source')); + } - ($request->filled('rtd_location_id')) ? - $asset->location_id = $request->get('rtd_location_id') : null; - - /** - * this is here just legacy reasons. Api\AssetController - * used image_source once to allow encoded image uploads. - */ - if ($request->has('image_source')) { - $request->offsetSet('image', $request->offsetGet('image_source')); - } - - $asset = $request->handleImages($asset); - $model = AssetModel::find($asset->model_id); + $asset = $request->handleImages($asset); + $model = $asset->model; // Update custom fields $problems_updating_encrypted_custom_fields = false; @@ -706,15 +706,13 @@ class AssetsController extends Controller } } } - - if ($asset->save()) { if (($request->filled('assigned_user')) && ($target = User::find($request->get('assigned_user')))) { $location = $target->location_id; } elseif (($request->filled('assigned_asset')) && ($target = Asset::find($request->get('assigned_asset')))) { $location = $target->location_id; - Asset::where('assigned_type', \App\Models\Asset::class)->where('assigned_to', $id) + Asset::where('assigned_type', \App\Models\Asset::class)->where('assigned_to', $asset->id) ->update(['location_id' => $target->location_id]); } elseif (($request->filled('assigned_location')) && ($target = Location::find($request->get('assigned_location')))) { $location = $target->id; @@ -728,17 +726,13 @@ class AssetsController extends Controller $asset->image = $asset->getImageUrl(); } - if ($problems_updating_encrypted_custom_fields) { - return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.encrypted_warning'))); - } else { - return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.success'))); - } + if ($problems_updating_encrypted_custom_fields) { + return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.encrypted_warning'))); + } else { + return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/message.update.success'))); } - - return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200); } - - return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.does_not_exist')), 200); + return response()->json(Helper::formatStandardApiResponse('error', null, $asset->getErrors()), 200); } diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php index ca4563ba2d..d186d42cf4 100755 --- a/app/Http/Controllers/SettingsController.php +++ b/app/Http/Controllers/SettingsController.php @@ -14,7 +14,6 @@ use App\Models\Asset; use App\Models\User; use App\Notifications\FirstAdminNotification; use App\Notifications\MailTest; -use Illuminate\Http\Client\HttpClientException; use Illuminate\Http\Request; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Storage; @@ -129,11 +128,11 @@ class SettingsController extends Controller protected function dotEnvFileIsExposed() : bool { try { - return Http::timeout(10) + return Http::withoutVerifying()->timeout(10) ->accept('*/*') ->get(URL::to('.env')) ->successful(); - } catch (HttpClientException $e) { + } catch (\Exception $e) { Log::debug($e->getMessage()); return true; } diff --git a/app/Http/Middleware/EncryptCookies.php b/app/Http/Middleware/EncryptCookies.php index 5f4c62723a..0d5b37de77 100644 --- a/app/Http/Middleware/EncryptCookies.php +++ b/app/Http/Middleware/EncryptCookies.php @@ -20,5 +20,5 @@ class EncryptCookies extends BaseEncrypter * * @var bool */ - protected static $serialize = true; + protected static $serialize = false; } diff --git a/app/Http/Requests/UpdateAssetRequest.php b/app/Http/Requests/UpdateAssetRequest.php new file mode 100644 index 0000000000..cc23a65c40 --- /dev/null +++ b/app/Http/Requests/UpdateAssetRequest.php @@ -0,0 +1,44 @@ +asset); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + $rules = array_merge( + parent::rules(), + (new Asset)->getRules(), + // this is to overwrite rulesets that include required, and rewrite unique_undeleted + [ + 'model_id' => ['integer', 'exists:models,id,deleted_at,NULL', 'not_array'], + 'status_id' => ['integer', 'exists:status_labels,id'], + 'asset_tag' => [ + 'min:1', 'max:255', 'not_array', + Rule::unique('assets', 'asset_tag')->ignore($this->asset)->withoutTrashed() + ], + ], + ); + + return $rules; + } +} diff --git a/app/Models/Asset.php b/app/Models/Asset.php index 5f5e0cd603..cddca9802c 100644 --- a/app/Models/Asset.php +++ b/app/Models/Asset.php @@ -97,35 +97,38 @@ class Asset extends Depreciable ]; protected $rules = [ - 'model_id' => 'required|integer|exists:models,id,deleted_at,NULL|not_array', - 'status_id' => 'required|integer|exists:status_labels,id', - 'asset_tag' => 'required|min:1|max:255|unique_undeleted:assets,asset_tag|not_array', - 'name' => 'nullable|max:255', - 'company_id' => 'nullable|integer|exists:companies,id', - 'warranty_months' => 'nullable|numeric|digits_between:0,240', - 'last_checkout' => 'nullable|date_format:Y-m-d H:i:s', - 'last_checkin' => 'nullable|date_format:Y-m-d H:i:s', - 'expected_checkin' => 'nullable|date', - 'last_audit_date' => 'nullable|date_format:Y-m-d H:i:s', - // 'next_audit_date' => 'nullable|date|after:last_audit_date', - 'next_audit_date' => 'nullable|date', - 'location_id' => 'nullable|exists:locations,id', - 'rtd_location_id' => 'nullable|exists:locations,id', - 'purchase_date' => 'nullable|date|date_format:Y-m-d', - 'serial' => 'nullable|unique_undeleted:assets,serial', - 'purchase_cost' => 'nullable|numeric|gte:0', - 'supplier_id' => 'nullable|exists:suppliers,id', - 'asset_eol_date' => 'nullable|date', - 'eol_explicit' => 'nullable|boolean', - 'byod' => 'nullable|boolean', - 'order_number' => 'nullable|string|max:191', - 'notes' => 'nullable|string|max:65535', - 'assigned_to' => 'nullable|integer', - 'requestable' => 'nullable|boolean', + 'model_id' => ['required', 'integer', 'exists:models,id,deleted_at,NULL', 'not_array'], + 'status_id' => ['required', 'integer', 'exists:status_labels,id'], + 'asset_tag' => ['required', 'min:1', 'max:255', 'unique_undeleted:assets,asset_tag', 'not_array'], + 'name' => ['nullable', 'max:255'], + 'company_id' => ['nullable', 'integer', 'exists:companies,id'], + 'warranty_months' => ['nullable', 'numeric', 'digits_between:0,240'], + 'last_checkout' => ['nullable', 'date_format:Y-m-d H:i:s'], + 'last_checkin' => ['nullable', 'date_format:Y-m-d H:i:s'], + 'expected_checkin' => ['nullable', 'date'], + 'last_audit_date' => ['nullable', 'date_format:Y-m-d H:i:s'], + 'next_audit_date' => ['nullable', 'date'], + //'after:last_audit_date'], + 'location_id' => ['nullable', 'exists:locations,id'], + 'rtd_location_id' => ['nullable', 'exists:locations,id'], + 'purchase_date' => ['nullable', 'date', 'date_format:Y-m-d'], + 'serial' => ['nullable', 'unique_undeleted:assets,serial'], + 'purchase_cost' => ['nullable', 'numeric', 'gte:0'], + 'supplier_id' => ['nullable', 'exists:suppliers,id'], + 'asset_eol_date' => ['nullable', 'date'], + 'eol_explicit' => ['nullable', 'boolean'], + 'byod' => ['nullable', 'boolean'], + 'order_number' => ['nullable', 'string', 'max:191'], + 'notes' => ['nullable', 'string', 'max:65535'], + 'assigned_to' => ['nullable', 'integer'], + 'requestable' => ['nullable', 'boolean'], + 'assigned_user' => ['nullable', 'exists:users,id,deleted_at,NULL'], + 'assigned_location' => ['nullable', 'exists:locations,id,deleted_at,NULL'], + 'assigned_asset' => ['nullable', 'exists:assets,id,deleted_at,NULL'] ]; - /** + /** * The attributes that are mass assignable. * * @var array diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index e17d667845..a69a6cadf0 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -87,11 +87,11 @@ class AuthServiceProvider extends ServiceProvider ]); $this->registerPolicies(); - //Passport::routes(); //this is no longer required in newer passport versions Passport::tokensExpireIn(Carbon::now()->addYears(config('passport.expiration_years'))); Passport::refreshTokensExpireIn(Carbon::now()->addYears(config('passport.expiration_years'))); Passport::personalAccessTokensExpireIn(Carbon::now()->addYears(config('passport.expiration_years'))); - Passport::withCookieSerialization(); + + Passport::cookie(config('passport.cookie_name')); /** diff --git a/app/Providers/ValidationServiceProvider.php b/app/Providers/ValidationServiceProvider.php index 690af759c7..3d7f489780 100644 --- a/app/Providers/ValidationServiceProvider.php +++ b/app/Providers/ValidationServiceProvider.php @@ -66,7 +66,6 @@ class ValidationServiceProvider extends ServiceProvider * `unique_undeleted:table,fieldname` in your rules out of the box */ Validator::extend('unique_undeleted', function ($attribute, $value, $parameters, $validator) { - if (count($parameters)) { // This is a bit of a shim, but serial doesn't have any other rules around it other than that it's nullable diff --git a/config/passport.php b/config/passport.php index d410cfe20e..8de731b49a 100644 --- a/config/passport.php +++ b/config/passport.php @@ -14,4 +14,5 @@ return [ 'private_key' => env('PASSPORT_PRIVATE_KEY'), 'public_key' => env('PASSPORT_PUBLIC_KEY'), 'expiration_years' => env('API_TOKEN_EXPIRATION_YEARS', 20), + 'cookie_name' => env('PASSPORT_COOKIE_NAME', 'snipeit_passport_token'), ]; diff --git a/database/factories/AssetFactory.php b/database/factories/AssetFactory.php index 8f910b903d..b1255baeee 100644 --- a/database/factories/AssetFactory.php +++ b/database/factories/AssetFactory.php @@ -48,6 +48,7 @@ class AssetFactory extends Factory 'assigned_type' => null, 'next_audit_date' => null, 'last_checkout' => null, + 'asset_eol_date' => null ]; } @@ -354,6 +355,17 @@ class AssetFactory extends Factory return $this->state(['requestable' => false]); } + public function noPurchaseOrEolDate() + { + return $this->afterCreating(function (Asset $asset) { + $asset->update([ + 'purchase_date' => null, + 'asset_eol_date' => null + ]); + }); + } + + public function hasEncryptedCustomField(CustomField $field = null) { return $this->state(function () use ($field) { @@ -372,7 +384,6 @@ class AssetFactory extends Factory }); } - /** * This allows bypassing model level validation if you want to purposefully * create an asset in an invalid state. Validation is turned back on diff --git a/database/factories/LocationFactory.php b/database/factories/LocationFactory.php index abd773010e..3aa0577bd8 100644 --- a/database/factories/LocationFactory.php +++ b/database/factories/LocationFactory.php @@ -25,7 +25,13 @@ class LocationFactory extends Factory 'image' => rand(1, 9).'.jpg', ]; } - + + // one of these can eventuall go away - left temporarily for conflict resolution + public function deleted(): self + { + return $this->state(['deleted_at' => $this->faker->dateTime()]); + } + public function deletedLocation() { return $this->state(function () { diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 5c885666df..656fc8672d 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -309,4 +309,9 @@ class UserFactory extends Factory ]; }); } + + public function deleted(): self + { + return $this->state(['deleted_at' => $this->faker->dateTime()]); + } } diff --git a/resources/lang/en-US/button.php b/resources/lang/en-US/button.php index 51c54bb9b5..8a838e8faf 100644 --- a/resources/lang/en-US/button.php +++ b/resources/lang/en-US/button.php @@ -26,7 +26,7 @@ return [ 'clone' => 'Clone :item_type', 'edit' => 'Edit :item_type', 'delete' => 'Delete :item_type', - 'restore' => 'Delete :item_type', + 'restore' => 'Restore :item_type', 'create' => 'Create New :item_type', 'checkout' => 'Checkout :item_type', 'checkin' => 'Checkin :item_type', diff --git a/routes/api.php b/routes/api.php index 5f122b2a18..b5311aa982 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,7 +1,6 @@ 'v1', 'middleware' => ['api', 'throttle:api']], functi 'destroy' ] )->name('api.assets.files.destroy'); - }); - Route::resource('hardware', + // pulling this out of resource route group to begin normalizing for route-model binding. + // this would probably keep working with the resource route group, but the general practice is for + // the model name to be the parameter - and i think it's a good differentiation in the code while we convert the others. + Route::patch('/hardware/{asset}', [Api\AssetsController::class, 'update'])->name('api.assets.update'); + + Route::resource('hardware', Api\AssetsController::class, ['names' => [ 'index' => 'api.assets.index', 'show' => 'api.assets.show', - 'update' => 'api.assets.update', 'store' => 'api.assets.store', 'destroy' => 'api.assets.destroy', ], - 'except' => ['create', 'edit'], + 'except' => ['create', 'edit', 'update'], 'parameters' => ['asset' => 'asset_id'], ] ); // end assets API routes diff --git a/tests/Feature/Assets/Api/UpdateAssetTest.php b/tests/Feature/Assets/Api/UpdateAssetTest.php index 70ad3aa5b3..db5893b4dc 100644 --- a/tests/Feature/Assets/Api/UpdateAssetTest.php +++ b/tests/Feature/Assets/Api/UpdateAssetTest.php @@ -3,13 +3,268 @@ namespace Tests\Feature\Assets\Api; use App\Models\Asset; -use App\Models\CustomField; +use App\Models\AssetModel; +use App\Models\Company; +use App\Models\Location; +use App\Models\Statuslabel; +use App\Models\Supplier; use App\Models\User; +use App\Models\CustomField; use Illuminate\Support\Facades\Crypt; use Tests\TestCase; class UpdateAssetTest extends TestCase { + public function testThatANonExistentAssetIdReturnsError() + { + $this->actingAsForApi(User::factory()->editAssets()->createAssets()->create()) + ->patchJson(route('api.assets.update', 123456789)) + ->assertStatusMessageIs('error'); + } + + public function testRequiresPermissionToUpdateAsset() + { + $asset = Asset::factory()->create(); + + $this->actingAsForApi(User::factory()->create()) + ->patchJson(route('api.assets.update', $asset->id)) + ->assertForbidden(); + } + + public function testGivenPermissionUpdateAssetIsAllowed() + + { + $asset = Asset::factory()->create(); + + $this->actingAsForApi(User::factory()->editAssets()->create()) + ->patchJson(route('api.assets.update', $asset->id), [ + 'name' => 'test' + ]) + ->assertOk(); + } + + public function testAllAssetAttributesAreStored() + { + $asset = Asset::factory()->create(); + $user = User::factory()->editAssets()->create(); + $userAssigned = User::factory()->create(); + $company = Company::factory()->create(); + $location = Location::factory()->create(); + $model = AssetModel::factory()->create(); + $rtdLocation = Location::factory()->create(); + $status = Statuslabel::factory()->create(); + $supplier = Supplier::factory()->create(); + + $response = $this->actingAsForApi($user) + ->patchJson(route('api.assets.update', $asset->id), [ + 'asset_eol_date' => '2024-06-02', + 'asset_tag' => 'random_string', + 'assigned_user' => $userAssigned->id, + 'company_id' => $company->id, + 'last_audit_date' => '2023-09-03 12:23:45', + 'location_id' => $location->id, + 'model_id' => $model->id, + 'name' => 'A New Asset', + 'notes' => 'Some notes', + 'order_number' => '5678', + 'purchase_cost' => '123.45', + 'purchase_date' => '2023-09-02', + 'requestable' => true, + 'rtd_location_id' => $rtdLocation->id, + 'serial' => '1234567890', + 'status_id' => $status->id, + 'supplier_id' => $supplier->id, + 'warranty_months' => 10, + ]) + ->assertOk() + ->assertStatusMessageIs('success') + ->json(); + + $updatedAsset = Asset::find($response['payload']['id']); + + $this->assertEquals('2024-06-02', $updatedAsset->asset_eol_date); + $this->assertEquals('random_string', $updatedAsset->asset_tag); + $this->assertEquals($userAssigned->id, $updatedAsset->assigned_to); + $this->assertTrue($updatedAsset->company->is($company)); + $this->assertTrue($updatedAsset->location->is($location)); + $this->assertTrue($updatedAsset->model->is($model)); + $this->assertEquals('A New Asset', $updatedAsset->name); + $this->assertEquals('Some notes', $updatedAsset->notes); + $this->assertEquals('5678', $updatedAsset->order_number); + $this->assertEquals('123.45', $updatedAsset->purchase_cost); + $this->assertTrue($updatedAsset->purchase_date->is('2023-09-02')); + $this->assertEquals('1', $updatedAsset->requestable); + $this->assertTrue($updatedAsset->defaultLoc->is($rtdLocation)); + $this->assertEquals('1234567890', $updatedAsset->serial); + $this->assertTrue($updatedAsset->assetstatus->is($status)); + $this->assertTrue($updatedAsset->supplier->is($supplier)); + $this->assertEquals(10, $updatedAsset->warranty_months); + //$this->assertEquals('2023-09-03 00:00:00', $updatedAsset->last_audit_date->format('Y-m-d H:i:s')); + $this->assertEquals('2023-09-03 00:00:00', $updatedAsset->last_audit_date); + } + + public function testAssetEolDateIsCalculatedIfPurchaseDateUpdated() + { + $asset = Asset::factory()->laptopMbp()->noPurchaseOrEolDate()->create(); + + $this->actingAsForApi(User::factory()->editAssets()->create()) + ->patchJson((route('api.assets.update', $asset->id)), [ + 'purchase_date' => '2021-01-01', + ]) + ->assertOk() + ->assertStatusMessageIs('success') + ->json(); + + $asset->refresh(); + + $this->assertEquals('2024-01-01', $asset->asset_eol_date); + } + + public function testAssetEolDateIsNotCalculatedIfPurchaseDateNotSet() + { + $asset = Asset::factory()->laptopMbp()->noPurchaseOrEolDate()->create(); + + $this->actingAsForApi(User::factory()->editAssets()->create()) + ->patchJson(route('api.assets.update', $asset->id), [ + 'name' => 'test asset', + 'asset_eol_date' => '2022-01-01' + ]) + ->assertOk() + ->assertStatusMessageIs('success') + ->json(); + + $asset->refresh(); + + $this->assertEquals('2022-01-01', $asset->asset_eol_date); + } + + public function testAssetEolExplicitIsSetIfAssetEolDateIsExplicitlySet() + { + $asset = Asset::factory()->laptopMbp()->create(); + + $this->actingAsForApi(User::factory()->editAssets()->create()) + ->patchJson(route('api.assets.update', $asset->id), [ + 'asset_eol_date' => '2025-01-01', + ]) + ->assertOk() + ->assertStatusMessageIs('success') + ->json(); + + $asset->refresh(); + + $this->assertEquals('2025-01-01', $asset->asset_eol_date); + $this->assertTrue($asset->eol_explicit); + } + + public function testAssetTagCannotUpdateToNullValue() + { + $asset = Asset::factory()->laptopMbp()->create(); + + $this->actingAsForApi(User::factory()->editAssets()->create()) + ->patchJson(route('api.assets.update', $asset->id), [ + 'asset_tag' => null, + ]) + ->assertOk() + ->assertStatusMessageIs('error'); + } + + public function testAssetTagCannotUpdateToEmptyStringValue() + { + $asset = Asset::factory()->laptopMbp()->create(); + + $this->actingAsForApi(User::factory()->editAssets()->create()) + ->patchJson(route('api.assets.update', $asset->id), [ + 'asset_tag' => "", + ]) + ->assertOk() + ->assertStatusMessageIs('error'); + } + + public function testModelIdCannotUpdateToNullValue() + { + $asset = Asset::factory()->laptopMbp()->create(); + + $this->actingAsForApi(User::factory()->editAssets()->create()) + ->patchJson(route('api.assets.update', $asset->id), [ + 'model_id' => null + ]) + ->assertOk() + ->assertStatusMessageIs('error'); + } + + public function testModelIdCannotUpdateToEmptyStringValue() + { + $asset = Asset::factory()->laptopMbp()->create(); + + $this->actingAsForApi(User::factory()->editAssets()->create()) + ->patchJson(route('api.assets.update', $asset->id), [ + 'model_id' => "" + ]) + ->assertOk() + ->assertStatusMessageIs('error'); + } + + public function testStatusIdCannotUpdateToNullValue() + { + $asset = Asset::factory()->laptopMbp()->create(); + + $this->actingAsForApi(User::factory()->editAssets()->create()) + ->patchJson(route('api.assets.update', $asset->id), [ + 'status_id' => null + ]) + ->assertOk() + ->assertStatusMessageIs('error'); + } + + public function testStatusIdCannotUpdateToEmptyStringValue() + { + $asset = Asset::factory()->laptopMbp()->create(); + + $this->actingAsForApi(User::factory()->editAssets()->create()) + ->patchJson(route('api.assets.update', $asset->id), [ + 'status_id' => "" + ]) + ->assertOk() + ->assertStatusMessageIs('error'); + } + + public function testIfRtdLocationIdIsSetWithoutLocationIdAssetReturnsToDefault() + { + $location = Location::factory()->create(); + $asset = Asset::factory()->laptopMbp()->create([ + 'location_id' => $location->id + ]); + $rtdLocation = Location::factory()->create(); + + $this->actingAsForApi(User::factory()->editAssets()->create()) + ->patchJson(route('api.assets.update', $asset->id), [ + 'rtd_location_id' => $rtdLocation->id + ]); + + $asset->refresh(); + + $this->assertTrue($asset->defaultLoc->is($rtdLocation)); + $this->assertTrue($asset->location->is($rtdLocation)); + } + + public function testIfLocationAndRtdLocationAreSetLocationIdIsLocation() + { + $location = Location::factory()->create(); + $asset = Asset::factory()->laptopMbp()->create(); + $rtdLocation = Location::factory()->create(); + + $this->actingAsForApi(User::factory()->editAssets()->create()) + ->patchJson(route('api.assets.update', $asset->id), [ + 'rtd_location_id' => $rtdLocation->id, + 'location_id' => $location->id + ]); + + $asset->refresh(); + + $this->assertTrue($asset->defaultLoc->is($rtdLocation)); + $this->assertTrue($asset->location->is($location)); + } + public function testEncryptedCustomFieldCanBeUpdated() { $this->markIncompleteIfMySQL('Custom Fields tests do not work on MySQL'); @@ -52,4 +307,151 @@ class UpdateAssetTest extends TestCase $asset->refresh(); $this->assertEquals("encrypted value should not change", Crypt::decrypt($asset->{$field->db_column_name()})); } + + public function testCheckoutToUserOnAssetUpdate() + { + $asset = Asset::factory()->create(); + $user = User::factory()->editAssets()->create(); + $assigned_user = User::factory()->create(); + + $response = $this->actingAsForApi($user) + ->patchJson(route('api.assets.update', $asset->id), [ + 'assigned_user' => $assigned_user->id, + ]) + ->assertOk() + ->assertStatusMessageIs('success') + ->json(); + + $asset->refresh(); + $this->assertEquals($assigned_user->id, $asset->assigned_to); + $this->assertEquals($asset->assigned_type, 'App\Models\User'); + } + + public function testCheckoutToDeletedUserFailsOnAssetUpdate() + { + $asset = Asset::factory()->create(); + $user = User::factory()->editAssets()->create(); + $assigned_user = User::factory()->deleted()->create(); + + $this->actingAsForApi($user) + ->patchJson(route('api.assets.update', $asset->id), [ + 'assigned_user' => $assigned_user->id, + ]) + ->assertOk() + ->assertStatusMessageIs('error') + ->json(); + + $asset->refresh(); + $this->assertNull($asset->assigned_to); + $this->assertNull($asset->assigned_type); + } + + public function testCheckoutToLocationOnAssetUpdate() + { + $asset = Asset::factory()->create(); + $user = User::factory()->editAssets()->create(); + $assigned_location = Location::factory()->create(); + + $this->actingAsForApi($user) + ->patchJson(route('api.assets.update', $asset->id), [ + 'assigned_location' => $assigned_location->id, + ]) + ->assertOk() + ->assertStatusMessageIs('success') + ->json(); + + $asset->refresh(); + $this->assertEquals($assigned_location->id, $asset->assigned_to); + $this->assertEquals($asset->assigned_type, 'App\Models\Location'); + + } + + public function testCheckoutToDeletedLocationFailsOnAssetUpdate() + { + $asset = Asset::factory()->create(); + $user = User::factory()->editAssets()->create(); + $assigned_location = Location::factory()->deleted()->create(); + + $this->actingAsForApi($user) + ->patchJson(route('api.assets.update', $asset->id), [ + 'assigned_location' => $assigned_location->id, + ]) + ->assertOk() + ->assertStatusMessageIs('error') + ->json(); + + $asset->refresh(); + $this->assertNull($asset->assigned_to); + $this->assertNull($asset->assigned_type); + } + + public function testCheckoutAssetOnAssetUpdate() + { + $asset = Asset::factory()->create(); + $user = User::factory()->editAssets()->create(); + $assigned_asset = Asset::factory()->create(); + + $this->actingAsForApi($user) + ->patchJson(route('api.assets.update', $asset->id), [ + 'assigned_asset' => $assigned_asset->id, + 'checkout_to_type' => 'user', + ]) + ->assertOk() + ->assertStatusMessageIs('success') + ->json(); + + $asset->refresh(); + $this->assertEquals($assigned_asset->id, $asset->assigned_to); + $this->assertEquals($asset->assigned_type, 'App\Models\Asset'); + + } + + public function testCheckoutToDeletedAssetFailsOnAssetUpdate() + { + $asset = Asset::factory()->create(); + $user = User::factory()->editAssets()->create(); + $assigned_asset = Asset::factory()->deleted()->create(); + + $this->actingAsForApi($user) + ->patchJson(route('api.assets.update', $asset->id), [ + 'assigned_asset' => $assigned_asset->id, + ]) + ->assertOk() + ->assertStatusMessageIs('error') + ->json(); + + $asset->refresh(); + $this->assertNull($asset->assigned_to); + $this->assertNull($asset->assigned_type); + } + + public function testAssetCannotBeUpdatedByUserInSeparateCompany() + { + $this->settings->enableMultipleFullCompanySupport(); + + $companyA = Company::factory()->create(); + $companyB = Company::factory()->create(); + $userA = User::factory()->editAssets()->create([ + 'company_id' => $companyA->id, + ]); + $userB = User::factory()->editAssets()->create([ + 'company_id' => $companyB->id, + ]); + $asset = Asset::factory()->create([ + 'user_id' => $userA->id, + 'company_id' => $companyA->id, + ]); + + $this->actingAsForApi($userB) + ->patchJson(route('api.assets.update', $asset->id), [ + 'name' => 'test name' + ]) + ->assertStatusMessageIs('error'); + + $this->actingAsForApi($userA) + ->patchJson(route('api.assets.update', $asset->id), [ + 'name' => 'test name' + ]) + ->assertStatusMessageIs('success'); + } } diff --git a/tests/Feature/Settings/ShowSetUpPageTest.php b/tests/Feature/Settings/ShowSetUpPageTest.php index 7f40768db7..929c41c4ef 100644 --- a/tests/Feature/Settings/ShowSetUpPageTest.php +++ b/tests/Feature/Settings/ShowSetUpPageTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature\Settings; +use App\Http\Controllers\SettingsController; use Illuminate\Database\Events\QueryExecuted; use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\Client\Request; @@ -301,4 +302,11 @@ class ShowSetUpPageTest extends TestCase $this->assertSeeDirectoryPermissionError(false); } + + public function testInvalidTLSCertsOkWhenCheckingForEnvFile() + { + //set the weird bad SSL cert place - https://self-signed.badssl.com + $this->markTestIncomplete("Not yet sure how to write this test, it requires messing with .env ..."); + $this->assertTrue((new SettingsController())->dotEnvFileIsExposed()); + } }