diff --git a/app/Http/Controllers/Api/AssetsController.php b/app/Http/Controllers/Api/AssetsController.php index 24152670ee..fcba0195e3 100644 --- a/app/Http/Controllers/Api/AssetsController.php +++ b/app/Http/Controllers/Api/AssetsController.php @@ -3,15 +3,15 @@ namespace App\Http\Controllers\Api; use App\Events\CheckoutableCheckedIn; +use App\Http\Requests\StoreAssetRequest; +use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Gate; use App\Helpers\Helper; use App\Http\Controllers\Controller; use App\Http\Requests\AssetCheckoutRequest; use App\Http\Transformers\AssetsTransformer; -use App\Http\Transformers\DepreciationReportTransformer; use App\Http\Transformers\LicensesTransformer; use App\Http\Transformers\SelectlistTransformer; -use App\Models\Actionlog; use App\Models\Asset; use App\Models\AssetModel; use App\Models\Company; @@ -20,11 +20,12 @@ use App\Models\License; use App\Models\Location; use App\Models\Setting; use App\Models\User; -use Auth; +use \Illuminate\Support\Facades\Auth; use Carbon\Carbon; use DB; use Illuminate\Http\Request; use App\Http\Requests\ImageUploadRequest; +use Illuminate\Support\Facades\Log; use Input; use Paginator; use Slack; @@ -533,35 +534,17 @@ class AssetsController extends Controller * @since [v4.0] * @return \Illuminate\Http\JsonResponse */ - public function store(ImageUploadRequest $request) + public function store(StoreAssetRequest $request) { - $this->authorize('create', Asset::class); - $asset = new Asset(); $asset->model()->associate(AssetModel::find((int) $request->get('model_id'))); - $asset->name = $request->get('name'); - $asset->serial = $request->get('serial'); - $asset->company_id = Company::getIdForCurrentUser($request->get('company_id')); - $asset->model_id = $request->get('model_id'); - $asset->order_number = $request->get('order_number'); - $asset->notes = $request->get('notes'); - $asset->asset_tag = $request->get('asset_tag', Asset::autoincrement_asset()); + $asset->fill($request->validated()); + $asset->user_id = Auth::id(); $asset->archived = '0'; $asset->physical = '1'; $asset->depreciate = '0'; - $asset->status_id = $request->get('status_id', 0); - $asset->warranty_months = $request->get('warranty_months', null); - $asset->purchase_cost = $request->get('purchase_cost'); - $asset->asset_eol_date = $request->get('asset_eol_date', $asset->present()->eol_date()); - $asset->purchase_date = $request->get('purchase_date', null); - $asset->assigned_to = $request->get('assigned_to', null); - $asset->supplier_id = $request->get('supplier_id'); - $asset->requestable = $request->get('requestable', 0); - $asset->rtd_location_id = $request->get('rtd_location_id', null); - $asset->location_id = $request->get('rtd_location_id', null); - /** * this is here just legacy reasons. Api\AssetController @@ -585,22 +568,22 @@ class AssetsController extends Controller // If input value is null, use custom field's default value if ($field_val == null) { - \Log::debug('Field value for '.$field->db_column.' is null'); + Log::debug('Field value for '.$field->db_column.' is null'); $field_val = $field->defaultValue($request->get('model_id')); - \Log::debug('Use the default fieldset value of '.$field->defaultValue($request->get('model_id'))); + Log::debug('Use the default fieldset value of '.$field->defaultValue($request->get('model_id'))); } // if the field is set to encrypted, make sure we encrypt the value if ($field->field_encrypted == '1') { - \Log::debug('This model field is encrypted in this fieldset.'); + Log::debug('This model field is encrypted in this fieldset.'); if (Gate::allows('admin')) { // If input value is null, use custom field's default value if (($field_val == null) && ($request->has('model_id') != '')) { - $field_val = \Crypt::encrypt($field->defaultValue($request->get('model_id'))); + $field_val = Crypt::encrypt($field->defaultValue($request->get('model_id'))); } else { - $field_val = \Crypt::encrypt($request->input($field->db_column)); + $field_val = Crypt::encrypt($request->input($field->db_column)); } } } diff --git a/app/Http/Requests/StoreAssetRequest.php b/app/Http/Requests/StoreAssetRequest.php index 254895f134..0730a19a37 100644 --- a/app/Http/Requests/StoreAssetRequest.php +++ b/app/Http/Requests/StoreAssetRequest.php @@ -3,7 +3,7 @@ namespace App\Http\Requests; use App\Models\Asset; -use Illuminate\Foundation\Http\FormRequest; +use App\Models\Company; use Illuminate\Support\Facades\Gate; class StoreAssetRequest extends ImageUploadRequest @@ -20,7 +20,10 @@ class StoreAssetRequest extends ImageUploadRequest public function prepareForValidation(): void { - // + $this->merge([ + 'asset_tag' => $this->asset_tag ?? Asset::autoincrement_asset(), + 'company_id' => Company::getIdForCurrentUser($this->company_id), + ]); } /** diff --git a/app/Models/Asset.php b/app/Models/Asset.php index 4e5a25974e..cc2803f8aa 100644 --- a/app/Models/Asset.php +++ b/app/Models/Asset.php @@ -92,7 +92,7 @@ class Asset extends Depreciable 'name' => 'max:255|nullable', 'model_id' => 'required|integer|exists:models,id,deleted_at,NULL|not_array', 'status_id' => 'required|integer|exists:status_labels,id', - 'company_id' => 'integer|nullable', + 'company_id' => 'integer|nullable|exists:companies,id', 'warranty_months' => 'numeric|nullable|digits_between:0,240', 'physical' => 'numeric|max:1|nullable', 'last_checkout' => 'date_format:Y-m-d H:i:s|nullable', @@ -107,6 +107,10 @@ class Asset extends Depreciable 'asset_eol_date' => 'date|nullable', 'eol_explicit' => 'boolean|nullable', 'byod' => 'boolean', + 'order_number' => 'max:255|nullable', + 'notes' => 'max:65535|nullable', + 'assigned_to' => 'nullable|integer|exists:users,id', + 'requestable' => 'boolean', ]; diff --git a/tests/Feature/Api/Assets/AssetStoreTest.php b/tests/Feature/Api/Assets/AssetStoreTest.php index 5a68aebccb..eff1ed34c3 100644 --- a/tests/Feature/Api/Assets/AssetStoreTest.php +++ b/tests/Feature/Api/Assets/AssetStoreTest.php @@ -2,7 +2,14 @@ namespace Tests\Feature\Api\Assets; +use App\Models\Asset; +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 Carbon\Carbon; use Tests\Support\InteractsWithSettings; use Tests\TestCase; @@ -16,4 +23,223 @@ class AssetStoreTest extends TestCase ->postJson(route('api.assets.store')) ->assertForbidden(); } + + public function testAllAssetAttributesAreStored() + { + $company = Company::factory()->create(); + $location = Location::factory()->create(); + $model = AssetModel::factory()->create(); + $rtdLocation = Location::factory()->create(); + $status = Statuslabel::factory()->create(); + $supplier = Supplier::factory()->create(); + $user = User::factory()->createAssets()->create(); + $userAssigned = User::factory()->create(); + + $response = $this->actingAsForApi($user) + ->postJson(route('api.assets.store'), [ + 'archived' => true, + 'asset_eol_date' => '2024-06-02', + 'asset_tag' => 'random_string', + 'assigned_to' => $userAssigned->id, + 'company_id' => $company->id, + 'depreciate' => true, + 'last_audit_date' => '2023-09-03', + '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(); + + $asset = Asset::find($response['payload']['id']); + + $this->assertTrue($asset->adminuser->is($user)); + + // @todo: this is explicitly set 0 in the controller but they docs say they are customizable + // $this->assertTrue($asset->archived); + // @todo: This isn't in the docs but it's in the controller + $this->assertEquals('2024-06-02', $asset->asset_eol_date); + $this->assertEquals('random_string', $asset->asset_tag); + // @todo: This isn't in the docs but it's in the controller (should it be removed?) + $this->assertEquals($userAssigned->id, $asset->assigned_to); + // @todo: This is not in the docs but it's in the controller + $this->assertTrue($asset->company->is($company)); + // @todo: this is explicitly set 0 in the controller but they docs say they are customizable + // $this->assertTrue($asset->depreciate); + // @todo: this is in the docs but not the controller + // $this->assertEquals('2023-09-03', $asset->last_audit_date); + // @todo: this is set to rtd_location_id in the controller but customizable in the docs + // $this->assertTrue($asset->location->is($location)); + $this->assertTrue($asset->model->is($model)); + $this->assertEquals('A New Asset', $asset->name); + $this->assertEquals('Some notes', $asset->notes); + $this->assertEquals('5678', $asset->order_number); + $this->assertEquals('123.45', $asset->purchase_cost); + $this->assertTrue($asset->purchase_date->is('2023-09-02')); + $this->assertEquals('1', $asset->requestable); + $this->assertTrue($asset->defaultLoc->is($rtdLocation)); + $this->assertEquals('1234567890', $asset->serial); + $this->assertTrue($asset->assetstatus->is($status)); + $this->assertTrue($asset->supplier->is($supplier)); + $this->assertEquals(10, $asset->warranty_months); + } + + public function testAssetGetsAssetTagWithAutoIncrement() + { + $model = AssetModel::factory()->create(); + $status = Statuslabel::factory()->create(); + + $this->settings->enableAutoIncrement(); + $response = $this->actingAsForApi(User::factory()->superuser()->create()) + ->postJson(route('api.assets.store'), [ + 'model_id' => $model->id, + 'status_id' => $status->id, + ]) + ->assertOk() + ->assertStatusMessageIs('success'); + $asset = Asset::find($response->json()['payload']['id']); + $this->assertNotNull($asset->asset_tag); + } + + public function testAssetCreationFailsWithNoAssetTagOrAutoIncrement() + { + $model = AssetModel::factory()->create(); + $status = Statuslabel::factory()->create(); + + $this->settings->disableAutoIncrement(); + + $this->actingAsForApi(User::factory()->superuser()->create()) + ->postJson(route('api.assets.store'), [ + 'model_id' => $model->id, + 'status_id' => $status->id, + ]) + ->assertOk() + ->assertStatusMessageIs('error'); + } + + public function testUniqueSerialNumbersIsEnforcedWhenEnabled() + { + $model = AssetModel::factory()->create(); + $status = Statuslabel::factory()->create(); + $serial = '1234567890'; + + $this->settings->enableAutoIncrement(); + $this->settings->enableUniqueSerialNumbers(); + + $this->actingAsForApi(User::factory()->superuser()->create()) + ->postJson(route('api.assets.store'), [ + 'model_id' => $model->id, + 'status_id' => $status->id, + 'serial' => $serial, + ]) + ->assertOk() + ->assertStatusMessageIs('success'); + + $this->actingAsForApi(User::factory()->superuser()->create()) + ->postJson(route('api.assets.store'), [ + 'model_id' => $model->id, + 'status_id' => $status->id, + 'serial' => $serial, + ]) + ->assertOk() + ->assertStatusMessageIs('error'); + } + + public function testUniqueSerialNumbersIsNotEnforcedWhenDisabled() + { + $model = AssetModel::factory()->create(); + $status = Statuslabel::factory()->create(); + $serial = '1234567890'; + + $this->settings->enableAutoIncrement(); + $this->settings->disableUniqueSerialNumbers(); + + $this->actingAsForApi(User::factory()->superuser()->create()) + ->postJson(route('api.assets.store'), [ + 'model_id' => $model->id, + 'status_id' => $status->id, + 'serial' => $serial, + ]) + ->assertOk() + ->assertStatusMessageIs('success'); + + $this->actingAsForApi(User::factory()->superuser()->create()) + ->postJson(route('api.assets.store'), [ + 'model_id' => $model->id, + 'status_id' => $status->id, + 'serial' => $serial, + ]) + ->assertOk() + ->assertStatusMessageIs('success'); + } + + public function testAssetTagsMustBeUniqueWhenUndeleted() + { + $model = AssetModel::factory()->create(); + $status = Statuslabel::factory()->create(); + $asset_tag = '1234567890'; + + $this->settings->disableAutoIncrement(); + + $this->actingAsForApi(User::factory()->superuser()->create()) + ->postJson(route('api.assets.store'), [ + 'asset_tag' => $asset_tag, + 'model_id' => $model->id, + 'status_id' => $status->id, + 'deleted_at' => null, + ]) + ->assertOk() + ->assertStatusMessageIs('success'); + + $this->actingAsForApi(User::factory()->superuser()->create()) + ->postJson(route('api.assets.store'), [ + 'asset_tag' => $asset_tag, + 'model_id' => $model->id, + 'status_id' => $status->id, + 'deleted_at' => null, + ]) + ->assertOk() + ->assertStatusMessageIs('error'); + } + + public function testAssetTagsCanBeDuplicatedIfDeleted() + { + $model = AssetModel::factory()->create(); + $status = Statuslabel::factory()->create(); + $asset_tag = '1234567890'; + + $this->settings->disableAutoIncrement(); + + $response1 = $this->actingAsForApi(User::factory()->superuser()->create()) + ->postJson(route('api.assets.store'), [ + 'asset_tag' => $asset_tag, + 'model_id' => $model->id, + 'status_id' => $status->id, + ]) + ->assertOk() + ->assertStatusMessageIs('success') + ->json(); + + $asset1 = Asset::find($response1['payload']['id'])->delete(); + + $response2 = $this->actingAsForApi(User::factory()->superuser()->create()) + ->postJson(route('api.assets.store'), [ + 'asset_tag' => $asset_tag, + 'model_id' => $model->id, + 'status_id' => $status->id, + ]) + ->assertOk() + ->assertStatusMessageIs('success'); + } } diff --git a/tests/Support/CustomTestMacros.php b/tests/Support/CustomTestMacros.php index 4242b28653..555a20bf2d 100644 --- a/tests/Support/CustomTestMacros.php +++ b/tests/Support/CustomTestMacros.php @@ -74,5 +74,18 @@ trait CustomTestMacros return $this; } ); + + TestResponse::macro( + 'assertStatusMessageIs', + function (string $message) { + Assert::assertEquals( + $message, + $this['status'], + "Response status message was not {$message}" + ); + + return $this; + } + ); } } diff --git a/tests/Support/Settings.php b/tests/Support/Settings.php index 17f8af23d2..288a3ec8a8 100644 --- a/tests/Support/Settings.php +++ b/tests/Support/Settings.php @@ -64,7 +64,26 @@ class Settings 'next_auto_tag_base' => '123', 'zerofill_count' => 5 ]); + } + public function disableAutoIncrement(): Settings + { + return $this->update([ + 'auto_increment_assets' => 0, + 'auto_increment_prefix' => '', + 'next_auto_tag_base' => '', + 'zerofill_count' => 0 + ]); + } + + public function enableUniqueSerialNumbers(): Settings + { + return $this->update(['unique_serial' => 1]); + } + + public function disableUniqueSerialNumbers(): Settings + { + return $this->update(['unique_serial' => 0]); } /**