diff --git a/app/Models/Asset.php b/app/Models/Asset.php index aff06b6689..35cea17265 100644 --- a/app/Models/Asset.php +++ b/app/Models/Asset.php @@ -43,16 +43,16 @@ class Asset extends Depreciable /** * Run after the checkout acceptance was declined by the user - * + * * @param User $acceptedBy * @param string $signature - */ + */ public function declinedCheckout(User $declinedBy, $signature) { $this->assigned_to = null; $this->assigned_type = null; - $this->accepted = null; - $this->save(); + $this->accepted = null; + $this->save(); } /** @@ -369,7 +369,7 @@ class Asset extends Depreciable if ($this->save()) { if (is_int($admin)) { $checkedOutBy = User::findOrFail($admin); - } elseif (get_class($admin) === \App\Models\User::class) { + } elseif ($admin && get_class($admin) === \App\Models\User::class) { $checkedOutBy = $admin; } else { $checkedOutBy = auth()->user(); @@ -1695,7 +1695,7 @@ class Asset extends Depreciable }); }); } - + /** * THIS CLUNKY BIT IS VERY IMPORTANT @@ -1716,7 +1716,7 @@ class Asset extends Depreciable * assets.location would fail, as that field doesn't exist -- plus we're already searching * against those relationships earlier in this method. * - * - snipe + * - snipe * */ diff --git a/database/factories/ImportFactory.php b/database/factories/ImportFactory.php new file mode 100644 index 0000000000..4fafc96216 --- /dev/null +++ b/database/factories/ImportFactory.php @@ -0,0 +1,146 @@ + + */ +class ImportFactory extends Factory +{ + /** + * @inheritdoc + */ + protected $model = Import::class; + + /** + * @inheritdoc + */ + public function definition() + { + return [ + 'name' => $this->faker->company, + 'file_path' => Str::random().'.csv', + 'filesize' => $this->faker->randomDigitNotNull(), + 'field_map' => null, + ]; + } + + /** + * Create an accessory import type. + * + * @return static + */ + public function accessory() + { + return $this->state(function (array $attributes) { + $fileBuilder = Importing\AccessoriesImportFileBuilder::new(); + + $attributes['name'] = "{$attributes['name']} Accessories"; + $attributes['import_type'] = 'Accessories'; + $attributes['header_row'] = $fileBuilder->toCsv()[0]; + $attributes['first_row'] = $fileBuilder->firstRow(); + + return $attributes; + }); + } + + /** + * Create an asset import type. + * + * @return static + */ + public function asset() + { + return $this->state(function (array $attributes) { + $fileBuilder = Importing\AssetsImportFileBuilder::new(); + + $attributes['name'] = "{$attributes['name']} Assets"; + $attributes['import_type'] = 'asset'; + $attributes['header_row'] = $fileBuilder->toCsv()[0]; + $attributes['first_row'] = $fileBuilder->firstRow(); + + return $attributes; + }); + } + + /** + * Create a component import type. + * + * @return static + */ + public function component() + { + return $this->state(function (array $attributes) { + $fileBuilder = Importing\ComponentsImportFileBuilder::times(); + + $attributes['name'] = "{$attributes['name']} Components"; + $attributes['import_type'] = 'component'; + $attributes['header_row'] = $fileBuilder->toCsv()[0]; + $attributes['first_row'] = $fileBuilder->firstRow(); + + return $attributes; + }); + } + + /** + * Create a consumable import type. + * + * @return static + */ + public function consumable() + { + return $this->state(function (array $attributes) { + $fileBuilder = Importing\ConsumablesImportFileBuilder::new(); + + $attributes['name'] = "{$attributes['name']} Consumables"; + $attributes['import_type'] = 'consumable'; + $attributes['header_row'] = $fileBuilder->toCsv()[0]; + $attributes['first_row'] = $fileBuilder->firstRow(); + + return $attributes; + }); + } + + /** + * Create a license import type. + * + * @return static + */ + public function license() + { + return $this->state(function (array $attributes) { + $fileBuilder = Importing\LicensesImportFileBuilder::new(); + + $attributes['name'] = "{$attributes['name']} Licenses"; + $attributes['import_type'] = 'license'; + $attributes['header_row'] = $fileBuilder->toCsv()[0]; + $attributes['first_row'] = $fileBuilder->firstRow(); + + return $attributes; + }); + } + + /** + * Create a users import type. + * + * @return static + */ + public function users() + { + return $this->state(function (array $attributes) { + $fileBuilder = Importing\UsersImportFileBuilder::new(); + + $attributes['name'] = "{$attributes['name']} Employees"; + $attributes['import_type'] = 'user'; + $attributes['header_row'] = $fileBuilder->toCsv()[0]; + $attributes['first_row'] = $fileBuilder->firstRow(); + + return $attributes; + }); + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index b375142196..d5ef8093bd 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -7,6 +7,9 @@ use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; use \Auth; +/** + * @extends Factory + */ class UserFactory extends Factory { /** diff --git a/tests/Feature/Importing/Api/ImportAccessoriesTest.php b/tests/Feature/Importing/Api/ImportAccessoriesTest.php new file mode 100644 index 0000000000..cf7597c314 --- /dev/null +++ b/tests/Feature/Importing/Api/ImportAccessoriesTest.php @@ -0,0 +1,426 @@ +map(fn (string $permission) => [$permission => '1']) + ->toJson(); + + $this->actingAsForApi(UserFactory::new()->create(['permissions' => $permissions])); + + $this->importFileResponse(['import' => 44])->assertForbidden(); + } + + #[Test] + public function userWithImportAccessoryPermissionCanImportAccessories(): void + { + $this->actingAsForApi(UserFactory::new()->canImport()->create()); + + $import = ImportFactory::new()->accessory()->create(); + + $this->importFileResponse(['import' => $import->id])->assertOk(); + } + + #[Test] + public function importAccessory(): void + { + $importFileBuilder = ImportFileBuilder::new(); + $row = $importFileBuilder->firstRow(); + $import = ImportFactory::new()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id]) + ->assertOk() + ->assertExactJson([ + 'payload' => null, + 'status' => 'success', + 'messages' => [ + 'redirect_url' => route('accessories.index') + ] + ]); + + $newAccessory = Accessory::query() + ->with(['location', 'category', 'manufacturer', 'supplier', 'company']) + ->where('name', $row['itemName']) + ->sole(); + + $activityLog = Actionlog::query() + ->where('item_type', Accessory::class) + ->where('item_id', $newAccessory->id) + ->sole(); + + $this->assertEquals($activityLog->action_type, 'create'); + $this->assertEquals($activityLog->action_source, 'importer'); + $this->assertEquals($activityLog->company_id, $newAccessory->company->id); + + $this->assertEquals($newAccessory->name, $row['itemName']); + $this->assertEquals($newAccessory->qty, $row['quantity']); + $this->assertEquals($newAccessory->purchase_date->toDateString(), $row['purchaseDate']); + $this->assertEquals($newAccessory->purchase_cost, $row['purchaseCost']); + $this->assertEquals($newAccessory->order_number, $row['orderNumber']); + $this->assertEquals($newAccessory->notes, $row['notes']); + $this->assertEquals($newAccessory->category->name, $row['category']); + $this->assertEquals($newAccessory->category->category_type, 'accessory'); + $this->assertEquals($newAccessory->manufacturer->name, $row['manufacturerName']); + $this->assertEquals($newAccessory->supplier->name, $row['supplierName']); + $this->assertEquals($newAccessory->location->name, $row['location']); + $this->assertEquals($newAccessory->company->name, $row['companyName']); + $this->assertEquals($newAccessory->model_number, $row['modelNumber']); + $this->assertFalse($newAccessory->requestable); + $this->assertNull($newAccessory->min_amt); + $this->assertNull($newAccessory->user_id); + } + + #[Test] + public function whenImportFileContainsUnknownColumns(): void + { + $row = ImportFileBuilder::new()->definition(); + $row['unknownColumn'] = $this->faker->word; + + $importFileBuilder = new ImportFileBuilder([$row]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + + $import = ImportFactory::new()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->importFileResponse(['import' => $import->id])->assertOk(); + } + + #[Test] + public function willFormatDate(): void + { + $importFileBuilder = ImportFileBuilder::new(['purchaseDate' => '2022/10/10']); + $import = ImportFactory::new()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $accessory = Accessory::query() + ->where('name', $importFileBuilder->firstRow()['itemName']) + ->sole(['purchase_date']); + + $this->assertEquals($accessory->purchase_date->toDateString(), '2022-10-10'); + } + + #[Test] + public function willNotCreateNewCategoryWhenCategoryExists(): void + { + $importFileBuilder = ImportFileBuilder::times(4)->replace(['category' => Str::random()]); + $import = ImportFactory::new()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newAccessories = Accessory::query() + ->whereIn('name', $importFileBuilder->pluck('itemName')) + ->get(); + + $this->assertCount(1, $newAccessories->pluck('category_id')->unique()->all()); + } + + #[Test] + public function willNotCreateNewAccessoryWhenAccessoryWithNameExists(): void + { + $accessory = AccessoryFactory::new()->create(['name' => Str::random()]); + $importFileBuilder = ImportFileBuilder::times(2)->replace(['itemName' => $accessory->name]); + $import = ImportFactory::new()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $probablyNewAccessories = Accessory::query() + ->where('name', $importFileBuilder->pluck('itemName')) + ->get(['name']); + + $this->assertCount(1, $probablyNewAccessories); + $this->assertEquals($probablyNewAccessories->first()->name, $accessory->name); + } + + #[Test] + public function willNotCreateNewCompanyWhenCompanyAlreadyExists(): void + { + $importFileBuilder = ImportFileBuilder::times(4)->replace(['companyName' => Str::random()]); + $import = ImportFactory::new()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newAccessories = Accessory::query() + ->where('name', $importFileBuilder->pluck('itemName')) + ->get(['company_id']); + + $this->assertCount(1, $newAccessories->pluck('company_id')->unique()->all()); + } + + #[Test] + public function willNotCreateNewLocationWhenLocationAlreadyExists(): void + { + $importFileBuilder = ImportFileBuilder::times(4)->replace(['location' => Str::random()]); + $import = ImportFactory::new()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newAccessories = Accessory::query() + ->where('name', $importFileBuilder->pluck('itemName')) + ->get(['location_id']); + + $this->assertCount(1, $newAccessories->pluck('location_id')->unique()->all()); + } + + #[Test] + public function willNotCreateNewManufacturerWhenManufacturerAlreadyExists(): void + { + $importFileBuilder = ImportFileBuilder::times(4)->replace(['manufacturerName' => $this->faker->company]); + $import = ImportFactory::new()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newAccessories = Accessory::query() + ->where('name', $importFileBuilder->pluck('itemName')) + ->get(['manufacturer_id']); + + $this->assertCount(1, $newAccessories->pluck('manufacturer_id')->unique()->all()); + } + + #[Test] + public function willNotCreateNewSupplierWhenSupplierAlreadyExists(): void + { + $importFileBuilder = ImportFileBuilder::times(4)->replace(['supplierName' => $this->faker->company]); + $import = ImportFactory::new()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newAccessories = Accessory::query() + ->where('name', $importFileBuilder->pluck('itemName')) + ->get(['supplier_id']); + + $this->assertCount(1, $newAccessories->pluck('supplier_id')->unique()->all()); + } + + #[Test] + public function whenColumnsAreMissingInImportFile(): void + { + $importFileBuilder = ImportFileBuilder::new()->forget(['minimumAmount', 'purchaseCost', 'purchaseDate']); + $import = ImportFactory::new()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newAccessory = Accessory::query() + ->where('name', $importFileBuilder->firstRow()['itemName']) + ->sole(); + + $this->assertNull($newAccessory->min_amt); + $this->assertNull($newAccessory->purchase_date); + $this->assertNull($newAccessory->purchase_cost); + } + + #[Test] + public function whenRequiredColumnsAreMissingInImportFile(): void + { + $importFileBuilder = ImportFileBuilder::new()->forget(['itemName', 'quantity', 'category']); + $import = ImportFactory::new()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id]) + ->assertInternalServerError() + ->assertExactJson([ + 'status' => 'import-errors', + 'payload' => null, + 'messages' => [ + '' => [ + 'Accessory' => [ + 'name' => ['The name field is required.'], + 'qty' => ['The qty field must be at least 1.'], + 'category_id' => ['The category id field is required.'] + ] + ] + ] + ]); + } + + #[Test] + public function updateAccessoryFromImport(): void + { + $accessory = AccessoryFactory::new()->create(['name' => Str::random()])->refresh(); + $importFileBuilder = ImportFileBuilder::new(['itemName' => $accessory->name]); + $row = $importFileBuilder->firstRow(); + $import = ImportFactory::new()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk(); + + $updatedAccessory = Accessory::query()->find($accessory->id); + $updatedAttributes = [ + 'name', 'company_id', 'qty', 'purchase_date', 'purchase_cost', + 'order_number', 'notes', 'category_id', 'manufacturer_id', 'supplier_id', + 'location_id', 'model_number', 'updated_at' + ]; + + $this->assertEquals($updatedAccessory->name, $row['itemName']); + $this->assertEquals($row['companyName'], $updatedAccessory->company->name); + $this->assertEquals($updatedAccessory->qty, $row['quantity']); + $this->assertEquals($updatedAccessory->purchase_date->toDateString(), $row['purchaseDate']); + $this->assertEquals($updatedAccessory->purchase_cost, $row['purchaseCost']); + $this->assertEquals($updatedAccessory->order_number, $row['orderNumber']); + $this->assertEquals($updatedAccessory->notes, $row['notes']); + $this->assertEquals($updatedAccessory->category->name, $row['category']); + $this->assertEquals($updatedAccessory->category->category_type, 'accessory'); + $this->assertEquals($updatedAccessory->manufacturer->name, $row['manufacturerName']); + $this->assertEquals($updatedAccessory->supplier->name, $row['supplierName']); + $this->assertEquals($updatedAccessory->location->name, $row['location']); + $this->assertEquals($updatedAccessory->model_number, $row['modelNumber']); + + $this->assertEquals( + Arr::except($updatedAccessory->attributesToArray(), $updatedAttributes), + Arr::except($accessory->attributesToArray(), $updatedAttributes), + ); + } + + #[Test] + public function whenImportFileContainsEmptyValues(): void + { + $accessory = AccessoryFactory::new()->create(['name' => Str::random()]); + $accessory->refresh(); + + $importFileBuilder = ImportFileBuilder::new([ + 'companyName' => ' ', + 'purchaseDate' => ' ', + 'purchaseCost' => '', + 'location' => '', + 'companyName' => '', + 'orderNumber' => '', + 'category' => '', + 'quantity' => '', + 'manufacturerName' => '', + 'supplierName' => '', + 'notes' => '', + 'requestAble' => '', + 'minimumAmount' => '', + 'modelNumber' => '' + ]); + + $import = ImportFactory::new()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id]) + ->assertInternalServerError() + ->assertExactJson([ + 'status' => 'import-errors', + 'payload' => null, + 'messages' => [ + $importFileBuilder->firstRow()['itemName'] => [ + 'Accessory' => [ + 'qty' => ['The qty field must be at least 1.'], + 'category_id' => ['The category id field is required.'] + ] + ] + ] + ]); + + $importFileBuilder->replace(['itemName' => $accessory->name]); + + $import = ImportFactory::new()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk(); + + $updatedAccessory = clone $accessory; + $updatedAccessory->refresh(); + + $this->assertEquals($updatedAccessory->toArray(), $accessory->toArray()); + } + + #[Test] + public function customColumnMapping(): void + { + $faker = ImportFileBuilder::new()->definition(); + $row = [ + 'itemName' => $faker['modelNumber'], + 'purchaseDate' => $faker['notes'], + 'purchaseCost' => $faker['location'], + 'location' => $faker['purchaseCost'], + 'companyName' => $faker['orderNumber'], + 'orderNumber' => $faker['companyName'], + 'category' => $faker['manufacturerName'], + 'manufacturerName' => $faker['category'], + 'notes' => $faker['purchaseDate'], + 'minimumAmount' => $faker['supplierName'], + 'modelNumber' => $faker['itemName'], + 'quantity' => $faker['quantity'] + ]; + + $importFileBuilder = new ImportFileBuilder([$row]); + $import = ImportFactory::new()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse([ + 'import' => $import->id, + 'column-mappings' => [ + 'Item Name' => 'model_number', + 'Purchase Date' => 'notes', + 'Purchase Cost' => 'location', + 'Location' => 'purchase_cost', + 'Company' => 'order_number', + 'Order Number' => 'company', + 'Category' => 'manufacturer', + 'Manufacturer' => 'category', + 'Supplier' => 'min_amt', + 'Notes' => 'purchase_date', + 'Min QTY' => 'supplier', + 'Model Number' => 'item_name', + 'Quantity' => 'quantity' + ] + ])->assertOk(); + + $newAccessory = Accessory::query() + ->with(['location', 'category', 'manufacturer', 'supplier']) + ->where('name', $row['modelNumber']) + ->sole(); + + $this->assertEquals($newAccessory->name, $row['modelNumber']); + $this->assertEquals($newAccessory->model_number, $row['itemName']); + $this->assertEquals($newAccessory->qty, $row['quantity']); + $this->assertEquals($newAccessory->purchase_date->toDateString(), $row['notes']); + $this->assertEquals($newAccessory->purchase_cost, $row['location']); + $this->assertEquals($newAccessory->order_number, $row['companyName']); + $this->assertEquals($newAccessory->notes, $row['purchaseDate']); + $this->assertEquals($newAccessory->category->name, $row['manufacturerName']); + $this->assertEquals($newAccessory->manufacturer->name, $row['category']); + $this->assertEquals($newAccessory->location->name, $row['purchaseCost']); + } +} diff --git a/tests/Feature/Importing/Api/ImportAssetsTest.php b/tests/Feature/Importing/Api/ImportAssetsTest.php new file mode 100644 index 0000000000..b7c6298d32 --- /dev/null +++ b/tests/Feature/Importing/Api/ImportAssetsTest.php @@ -0,0 +1,601 @@ +map(fn (string $permission) => [$permission => '1']) + ->toJson(); + + $this->actingAsForApi(UserFactory::new()->create(['permissions' => $permissions])); + + $this->importFileResponse(['import' => 44])->assertForbidden(); + } + + #[Test] + public function userWithImportAssetsPermissionCanImportAssets(): void + { + $this->actingAsForApi(UserFactory::new()->canImport()->create()); + + $import = ImportFactory::new()->asset()->create(); + + $this->importFileResponse(['import' => $import->id])->assertOk(); + } + + #[Test] + public function importAsset(): void + { + Notification::fake(); + + $importFileBuilder = ImportFileBuilder::new(); + $row = $importFileBuilder->firstRow(); + $import = ImportFactory::new()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id]) + ->assertOk() + ->assertExactJson([ + 'payload' => null, + 'status' => 'success', + 'messages' => ['redirect_url' => route('hardware.index')] + ]); + + $newAsset = Asset::query() + ->with(['location', 'supplier', 'company', 'assignedAssets', 'defaultLoc', 'assetStatus', 'model.category', 'model.manufacturer']) + ->where('serial', $row['serialNumber']) + ->sole(); + + $assignee = User::query()->find($newAsset->assigned_to, ['id', 'first_name', 'last_name', 'email', 'username']); + + $activityLogs = ActionLog::query() + ->where('item_type', Asset::class) + ->where('item_id', $newAsset->id) + ->get(); + + $this->assertCount(2, $activityLogs); + + $this->assertEquals($activityLogs[0]->action_type, 'checkout'); + $this->assertEquals($activityLogs[0]->item_type, Asset::class); + $this->assertEquals($activityLogs[0]->target_id, $assignee->id); + $this->assertEquals($activityLogs[0]->target_type, User::class); + $this->assertEquals($activityLogs[0]->note, 'Checkout from CSV Importer'); + + $this->assertEquals($activityLogs[1]->action_type, 'create'); + $this->assertNull($activityLogs[1]->target_id); + $this->assertEquals($activityLogs[1]->item_type, Asset::class); + $this->assertNull($activityLogs[1]->note); + $this->assertNull($activityLogs[1]->target_type); + + $this->assertEquals("{$assignee->first_name} {$assignee->last_name}", $row['assigneeFullName']); + $this->assertEquals($assignee->email, $row['assigneeEmail']); + $this->assertEquals($assignee->username, $row['assigneeUsername']); + + $this->assertEquals($newAsset->model->category->name, $row['category']); + $this->assertEquals($newAsset->model->manufacturer->name, $row['manufacturerName']); + $this->assertEquals($newAsset->name, $row['itemName']); + $this->assertEquals($newAsset->asset_tag, $row['tag']); + $this->assertEquals($newAsset->model->name, $row['model']); + $this->assertEquals($newAsset->model->model_number, $row['modelNumber']); + $this->assertEquals($newAsset->purchase_date->toDateString(), $row['purchaseDate']); + $this->assertNull($newAsset->asset_eol_date); + $this->assertEquals(0, $newAsset->eol_explicit); + $this->assertEquals($newAsset->location_id, $newAsset->rtd_location_id); + $this->assertEquals($newAsset->purchase_cost, $row['purchaseCost']); + $this->assertNull($newAsset->order_number); + $this->assertEquals($newAsset->image, ''); + $this->assertNull($newAsset->user_id); + $this->assertEquals($newAsset->physical, 1); + $this->assertEquals($newAsset->assetStatus->name, $row['status']); + $this->assertEquals($newAsset->archived, 0); + $this->assertEquals($newAsset->warranty_months, $row['warrantyInMonths']); + $this->assertNull($newAsset->deprecate); + $this->assertEquals($newAsset->supplier->name, $row['supplierName']); + $this->assertEquals($newAsset->requestable, 0); + $this->assertEquals($newAsset->defaultLoc->name, $row['location']); + $this->assertEquals($newAsset->accepted, null); + $this->assertEquals(Carbon::parse($newAsset->last_checkout)->toDateString(), now()->toDateString()); + $this->assertEquals($newAsset->last_checkin, 0); + $this->assertEquals($newAsset->expected_checkin, 0); + $this->assertEquals($newAsset->company->name, $row['companyName']); + $this->assertEquals($newAsset->assigned_type, User::class); + $this->assertNull($newAsset->last_audit_date); + $this->assertNull($newAsset->next_audit_date); + $this->assertEquals($newAsset->location->name, $row['location']); + $this->assertEquals($newAsset->checkin_counter, 0); + $this->assertEquals($newAsset->checkout_counter, 1); + $this->assertEquals($newAsset->requests_counter, 0); + $this->assertEquals($newAsset->byod, 0); + + //Notes is never read. + //$this->assertEquals($asset->notes, $row['notes']); + + Notification::assertSentTo($assignee, CheckoutAssetNotification::class); + } + + #[Test] + public function willIgnoreUnknownColumnsWhenFileContainsUnknownColumns(): void + { + $row = ImportFileBuilder::new()->definition(); + $row['unknownColumnInCsvFile'] = 'foo'; + + $importFileBuilder = new ImportFileBuilder([$row]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + + $import = ImportFactory::new()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->importFileResponse(['import' => $import->id])->assertOk(); + } + + #[Test] + public function willNotCreateNewAssetWhenAssetWithSameTagAlreadyExists(): void + { + $asset = AssetFactory::new()->create(['asset_tag' => $this->faker->uuid]); + $importFileBuilder = ImportFileBuilder::times(4)->replace(['tag' => $asset->asset_tag]); + $import = ImportFactory::new()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id]) + ->assertInternalServerError() + ->assertExactJson([ + 'status' => 'import-errors', + 'payload' => null, + 'messages' => [ + '' => [ + 'asset_tag' => [ + 'asset_tag' => [ + "An asset with the asset tag {$asset->asset_tag} already exists and an update was not requested. No change was made." + ] + ] + ] + ] + ]); + + $assetsWithSameTag = Asset::query()->where('asset_tag', $asset->asset_tag)->get(); + + $this->assertCount(1, $assetsWithSameTag); + } + + #[Test] + public function willNotCreateNewCompanyWhenCompanyExists(): void + { + $importFileBuilder = ImportFileBuilder::times(4)->replace(['companyName' => Str::random()]); + $import = ImportFactory::new()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newAssets = Asset::query() + ->whereIn('serial', $importFileBuilder->pluck('serialNumber')) + ->get(); + + $this->assertCount(1, $newAssets->pluck('company_id')->unique()->all()); + } + + #[Test] + public function willNotCreateNewLocationWhenLocationExists(): void + { + $importFileBuilder = ImportFileBuilder::times(4)->replace(['location' => Str::random()]); + $import = ImportFactory::new()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newAssets = Asset::query() + ->whereIn('serial', $importFileBuilder->pluck('serialNumber')) + ->get(); + + $this->assertCount(1, $newAssets->pluck('location_id')->unique()->all()); + } + + #[Test] + public function willNotCreateNewSupplierWhenSupplierExists(): void + { + $importFileBuilder = ImportFileBuilder::times(4)->replace(['supplierName' => $this->faker->company]); + $import = ImportFactory::new()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newAssets = Asset::query() + ->whereIn('serial', $importFileBuilder->pluck('serialNumber')) + ->get(['supplier_id']); + + $this->assertCount(1, $newAssets->pluck('supplier_id')->unique()->all()); + } + + #[Test] + public function willNotCreateNewManufacturerWhenManufacturerExists(): void + { + $importFileBuilder = ImportFileBuilder::times(4)->replace(['manufacturerName' => $this->faker->company]); + $import = ImportFactory::new()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newAssets = Asset::query() + ->with('model.manufacturer') + ->whereIn('serial', $importFileBuilder->pluck('serialNumber')) + ->get(); + + $this->assertCount(1, $newAssets->pluck('model.manufacturer_id')->unique()->all()); + } + + #[Test] + public function willNotCreateCategoryWhenCategoryExists(): void + { + $importFileBuilder = ImportFileBuilder::times(4)->replace(['category' => $this->faker->company]); + $import = ImportFactory::new()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newAssets = Asset::query() + ->with('model.category') + ->whereIn('serial', $importFileBuilder->pluck('serialNumber')) + ->get(); + + $this->assertCount(1, $newAssets->pluck('model.category_id')->unique()->all()); + } + + #[Test] + public function willNotCreateNewAssetModelWhenAssetModelExists(): void + { + $importFileBuilder = ImportFileBuilder::times(4)->replace(['model' => Str::random()]); + $import = ImportFactory::new()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newAssets = Asset::query() + ->with('model') + ->whereIn('serial', $importFileBuilder->pluck('serialNumber')) + ->get(); + + $this->assertCount(1, $newAssets->pluck('model.name')->unique()->all()); + } + + #[Test] + public function whenColumnsAreMissingInImportFile(): void + { + $importFileBuilder = ImportFileBuilder::times()->forget([ + 'purchaseCost', + 'purchaseDate', + 'status' + ]); + + $import = ImportFactory::new()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newAsset = Asset::query() + ->with(['assetStatus']) + ->where('serial', $importFileBuilder->firstRow()['serialNumber']) + ->sole(); + + $this->assertEquals($newAsset->assetStatus->name, 'Ready to Deploy'); + $this->assertNull($newAsset->purchase_date); + $this->assertNull($newAsset->purchase_cost); + } + + #[Test] + public function willFormatValues(): void + { + $importFileBuilder = ImportFileBuilder::new([ + 'warrantyInMonths' => '3 months', + 'purchaseDate' => '2022/10/10' + ]); + + $import = ImportFactory::new()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newAsset = Asset::query() + ->where('serial', $importFileBuilder->firstRow()['serialNumber']) + ->sole(); + + $this->assertEquals($newAsset->warranty_months, 3); + $this->assertEquals($newAsset->purchase_date->toDateString(), '2022-10-10'); + } + + #[Test] + public function whenRequiredColumnsAreMissingInImportFile(): void + { + $importFileBuilder = ImportFileBuilder::times(2) + ->forget(['tag']) + ->replace(['model' => '']); + + $rows = $importFileBuilder->all(); + $import = ImportFactory::new()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id]) + ->assertInternalServerError() + ->assertJson([ + 'status' => 'import-errors', + 'payload' => null, + 'messages' => [ + $rows[0]['itemName'] => [ + "Asset \"{$rows[0]['itemName']}\"" => [ + 'asset_tag' => [ + 'The asset tag field must be at least 1 characters.', + ], + 'model_id' => [ + 'The model id field is required.' + ] + ] + ], + $rows[1]['itemName'] => [ + "Asset \"{$rows[1]['itemName']}\"" => [ + 'asset_tag' => [ + 'The asset tag field must be at least 1 characters.', + ], + 'model_id' => [ + 'The model id field is required.' + ] + ] + ] + ] + ]); + + $newAssets = Asset::query() + ->whereIn('serial', Arr::pluck($rows, 'serialNumber')) + ->get(); + + $this->assertCount(0, $newAssets); + } + + #[Test] + public function updateAssetFromImport(): void + { + $asset = AssetFactory::new()->create()->refresh(); + $importFileBuilder = ImportFileBuilder::times(1)->replace(['tag' => $asset->asset_tag]); + $row = $importFileBuilder->firstRow(); + $import = ImportFactory::new()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk(); + + $updatedAsset = Asset::query() + ->with(['location', 'supplier', 'company', 'defaultLoc', 'assetStatus', 'model.category', 'model.manufacturer']) + ->find($asset->id); + + $assignee = User::query()->find($updatedAsset->assigned_to, ['id', 'first_name', 'last_name', 'email', 'username']); + + $updatedAttributes = [ + 'category', 'manufacturer_id', 'name', 'tag', 'model_id', + 'model_number', 'purchase_date', 'purchase_cost', 'warranty_months', 'supplier_id', + 'location_id', 'company_id', 'serial', 'assigned_to', 'status_id', 'rtd_location_id', + 'last_checkout', 'requestable', 'updated_at', 'checkout_counter', 'assigned_type' + ]; + + $this->assertEquals("{$assignee->first_name} {$assignee->last_name}", $row['assigneeFullName']); + $this->assertEquals($assignee->email, $row['assigneeEmail']); + $this->assertEquals($assignee->username, $row['assigneeUsername']); + + $this->assertEquals($updatedAsset->model->category->name, $row['category']); + $this->assertEquals($updatedAsset->model->manufacturer->name, $row['manufacturerName']); + $this->assertEquals($updatedAsset->name, $row['itemName']); + $this->assertEquals($updatedAsset->asset_tag, $row['tag']); + $this->assertEquals($updatedAsset->model->name, $row['model']); + $this->assertEquals($updatedAsset->model->model_number, $row['modelNumber']); + $this->assertEquals($updatedAsset->purchase_date->toDateString(), $row['purchaseDate']); + $this->assertEquals($updatedAsset->purchase_cost, $row['purchaseCost']); + $this->assertEquals($updatedAsset->assetStatus->name, $row['status']); + $this->assertEquals($updatedAsset->warranty_months, $row['warrantyInMonths']); + $this->assertEquals($updatedAsset->supplier->name, $row['supplierName']); + $this->assertEquals($updatedAsset->defaultLoc->name, $row['location']); + $this->assertEquals($updatedAsset->company->name, $row['companyName']); + $this->assertEquals($updatedAsset->location->name, $row['location']); + $this->assertEquals($updatedAsset->checkout_counter, 1); + $this->assertEquals($updatedAsset->assigned_type, user::class); + + //RequestAble is always updated regardless of initial value. + //$this->assertEquals($updatedAsset->requestable, $asset->requestable); + + $this->assertEquals( + Arr::except($updatedAsset->attributesToArray(), $updatedAttributes), + Arr::except($asset->attributesToArray(), $updatedAttributes), + ); + } + + #[Test] + public function customColumnMapping(): void + { + $faker = ImportFileBuilder::new()->definition(); + $row = [ + 'assigneeFullName' => $faker['supplierName'], + 'assigneeEmail' => $faker['manufacturerName'], + 'assigneeUsername' => $faker['serialNumber'], + 'category' => $faker['location'], + 'companyName' => $faker['purchaseCost'], + 'itemName' => $faker['modelNumber'], + 'location' => $faker['assigneeUsername'], + 'manufacturerName' => $faker['status'], + 'model' => $faker['itemName'], + 'modelNumber' => $faker['category'], + 'notes' => $faker['notes'], + 'purchaseCost' => $faker['model'], + 'purchaseDate' => $faker['companyName'], + 'serialNumber' => $faker['tag'], + 'supplierName' => $faker['purchaseDate'], + 'status' => $faker['warrantyInMonths'], + 'tag' => $faker['assigneeEmail'], + 'warrantyInMonths' => $faker['assigneeFullName'], + ]; + + $importFileBuilder = new ImportFileBuilder([$row]); + $import = ImportFactory::new()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + + $this->importFileResponse([ + 'import' => $import->id, + 'column-mappings' => [ + 'Asset Tag' => 'email', + 'Category' => 'location', + 'Company' => 'purchase_cost', + 'Email' => 'manufacturer', + 'Full Name' => 'supplier', + 'Item Name' => 'model_number', + 'Location' => 'username', + 'Manufacturer' => 'status', + 'Model name' => 'item_name', + 'Model Number' => 'category', + 'Notes' => 'asset_notes', + 'Purchase Cost' => 'asset_model', + 'Purchase Date' => 'company', + 'Serial number' => 'asset_tag', + 'Status' => 'warranty_months', + 'Supplier' => 'purchase_date', + 'Username' => 'serial', + 'Warranty' => 'full_name', + ] + ])->assertOk(); + + $asset = Asset::query() + ->with(['location', 'supplier', 'company', 'assignedAssets', 'defaultLoc', 'assetStatus', 'model.category', 'model.manufacturer']) + ->where('serial', $row['assigneeUsername']) + ->sole(); + + $assignee = User::query()->find($asset->assigned_to, ['id', 'first_name', 'last_name', 'email', 'username']); + + $this->assertEquals("{$assignee->first_name} {$assignee->last_name}", $row['warrantyInMonths']); + $this->assertEquals($assignee->email, $row['tag']); + $this->assertEquals($assignee->username, $row['location']); + + $this->assertEquals($asset->model->category->name, $row['modelNumber']); + $this->assertEquals($asset->model->manufacturer->name, $row['assigneeEmail']); + $this->assertEquals($asset->name, $row['model']); + $this->assertEquals($asset->asset_tag, $row['serialNumber']); + $this->assertEquals($asset->model->name, $row['purchaseCost']); + $this->assertEquals($asset->model->model_number, $row['itemName']); + $this->assertEquals($asset->purchase_date->toDateString(), $row['supplierName']); + $this->assertEquals($asset->purchase_cost, $row['companyName']); + $this->assertEquals($asset->assetStatus->name, $row['manufacturerName']); + $this->assertEquals($asset->warranty_months, $row['status']); + $this->assertEquals($asset->supplier->name, $row['assigneeFullName']); + $this->assertEquals($asset->defaultLoc->name, $row['category']); + $this->assertEquals($asset->company->name, $row['purchaseDate']); + $this->assertEquals($asset->location->name, $row['category']); + $this->assertEquals($asset->notes, $row['notes']); + $this->assertNull($asset->asset_eol_date); + $this->assertEquals(0, $asset->eol_explicit); + $this->assertNull($asset->order_number); + $this->assertEquals($asset->image, ''); + $this->assertNull($asset->user_id); + $this->assertEquals($asset->physical, 1); + $this->assertEquals($asset->archived, 0); + $this->assertNull($asset->deprecate); + $this->assertEquals($asset->requestable, 0); + $this->assertEquals($asset->accepted, null); + $this->assertEquals(Carbon::parse($asset->last_checkout)->toDateString(), now()->toDateString()); + $this->assertEquals($asset->last_checkin, 0); + $this->assertEquals($asset->expected_checkin, 0); + $this->assertEquals($asset->assigned_type, User::class); + $this->assertNull($asset->last_audit_date); + $this->assertNull($asset->next_audit_date); + $this->assertEquals($asset->checkin_counter, 0); + $this->assertEquals($asset->checkout_counter, 1); + $this->assertEquals($asset->requests_counter, 0); + $this->assertEquals($asset->byod, 0); + } + + #[Test] + public function customFields(): void + { + $macAddress = $this->faker->macAddress; + + $row = ImportFileBuilder::new()->definition(); + $row['Mac Address'] = $macAddress; + + $importFileBuilder = new ImportFileBuilder([$row]); + $customField = CustomField::query()->where('name', 'Mac Address')->firstOrNew(); + + if (!$customField->exists) { + $customField = CustomFieldFactory::new()->macAddress()->create(['db_column' => '_snipeit_mac_address_1']); + } + + if ($customField->field_encrypted) { + $customField->field_encrypted = 0; + $customField->save(); + } + + $import = ImportFactory::new()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newAsset = Asset::query()->where('serial', $importFileBuilder->firstRow()['serialNumber'])->sole(); + + $this->assertEquals($newAsset->getAttribute($customField->db_column), $macAddress); + } + + #[Test] + public function willEncryptCustomFields(): void + { + $macAddress = $this->faker->macAddress; + $row = ImportFileBuilder::new()->definition(); + + $row['Mac Address'] = $macAddress; + + $importFileBuilder = new ImportFileBuilder([$row]); + $customField = CustomField::query()->where('name', 'Mac Address')->firstOrNew(); + + if (!$customField->exists) { + $customField = CustomFieldFactory::new()->macAddress()->create(); + } + + if (!$customField->field_encrypted) { + $customField->field_encrypted = 1; + $customField->save(); + } + + $import = ImportFactory::new()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $asset = Asset::query()->where('serial', $importFileBuilder->firstRow()['serialNumber'])->sole(); + $encryptedMacAddress = $asset->getAttribute($customField->db_column); + + $this->assertNotEquals($encryptedMacAddress, $macAddress); + } +} diff --git a/tests/Feature/Importing/Api/ImportComponentsTest.php b/tests/Feature/Importing/Api/ImportComponentsTest.php new file mode 100644 index 0000000000..56c40ba462 --- /dev/null +++ b/tests/Feature/Importing/Api/ImportComponentsTest.php @@ -0,0 +1,309 @@ +map(fn (string $permission) => [$permission => '1']) + ->toJson(); + + $this->actingAsForApi(UserFactory::new()->create(['permissions' => $permissions])); + + $this->importFileResponse(['import' => 44])->assertForbidden(); + } + + #[Test] + public function userWithImportAssetsPermissionCanImportComponents(): void + { + $this->actingAsForApi(UserFactory::new()->canImport()->create()); + + $import = ImportFactory::new()->component()->create(); + + $this->importFileResponse(['import' => $import->id])->assertOk(); + } + + #[Test] + public function importComponents(): void + { + Notification::fake(); + + $importFileBuilder = ImportFileBuilder::new(); + $row = $importFileBuilder->firstRow(); + $import = ImportFactory::new()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id]) + ->assertOk() + ->assertExactJson([ + 'payload' => null, + 'status' => 'success', + 'messages' => ['redirect_url' => route('components.index')] + ]); + + $newComponent = Component::query() + ->with(['location', 'category', 'company']) + ->where('name', $row['itemName']) + ->sole(); + + $activityLog = ActionLog::query() + ->where('item_type', Component::class) + ->where('item_id', $newComponent->id) + ->sole(); + + $this->assertEquals($activityLog->action_type, 'create'); + $this->assertEquals($activityLog->action_source, 'importer'); + $this->assertEquals($activityLog->company_id, $newComponent->company->id); + + $this->assertEquals($newComponent->name, $row['itemName']); + $this->assertEquals($newComponent->company->name, $row['companyName']); + $this->assertEquals($newComponent->category->name, $row['category']); + $this->assertEquals($newComponent->location->name, $row['location']); + $this->assertNull($newComponent->supplier_id); + $this->assertEquals($newComponent->qty, $row['quantity']); + $this->assertEquals($newComponent->order_number, $row['orderNumber']); + $this->assertEquals($newComponent->purchase_date->toDateString(), $row['purchaseDate']); + $this->assertEquals($newComponent->purchase_cost, $row['purchaseCost']); + $this->assertNull($newComponent->min_amt); + $this->assertEquals($newComponent->serial, $row['serialNumber']); + $this->assertNull($newComponent->image); + $this->assertNull($newComponent->notes); + } + + #[Test] + public function willIgnoreUnknownColumnsWhenFileContainsUnknownColumns(): void + { + $row = ImportFileBuilder::new()->firstRow(); + $row['unknownColumnInCsvFile'] = 'foo'; + + $importFileBuilder = new ImportFileBuilder([$row]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + + $import = ImportFactory::new()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->importFileResponse(['import' => $import->id])->assertOk(); + } + + #[Test] + public function willNotCreateNewComponentWhenComponentWithNameAndSerialNumberExists(): void + { + $component = ComponentFactory::new()->create(); + + $importFileBuilder = ImportFileBuilder::times(4)->replace([ + 'itemName' => $component->name, + 'serialNumber' => $component->serial + ]); + + $import = ImportFactory::new()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $probablyNewComponents = Component::query() + ->where('name', $component->name) + ->where('serial', $component->serial) + ->get(['id']); + + $this->assertCount(1, $probablyNewComponents); + $this->assertEquals($component->id, $probablyNewComponents->sole()->id); + } + + #[Test] + public function willNotCreateNewCompanyWhenCompanyExists(): void + { + $importFileBuilder = ImportFileBuilder::times(4)->replace(['companyName' => Str::random()]); + $import = ImportFactory::new()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newComponents = Component::query() + ->whereIn('serial', $importFileBuilder->pluck('serialNumber')) + ->get(['company_id']); + + $this->assertCount(1, $newComponents->pluck('company_id')->unique()->all()); + } + + #[Test] + public function willNotCreateNewLocationWhenLocationExists(): void + { + $importFileBuilder = ImportFileBuilder::times(4)->replace(['location' => Str::random()]); + $import = ImportFactory::new()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newComponents = Component::query() + ->whereIn('serial', $importFileBuilder->pluck('serialNumber')) + ->get(['location_id']); + + $this->assertCount(1, $newComponents->pluck('location_id')->unique()->all()); + } + + #[Test] + public function willNotCreateNewCategoryWhenCategoryExists(): void + { + $importFileBuilder = ImportFileBuilder::times(4)->replace(['category' => $this->faker->company]); + $import = ImportFactory::new()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newComponents = Component::query() + ->whereIn('serial', $importFileBuilder->pluck('serialNumber')) + ->get(['category_id']); + + $this->assertCount(1, $newComponents->pluck('category_id')->unique()->all()); + } + + #[Test] + public function whenRequiredColumnsAreMissingInImportFile(): void + { + $importFileBuilder = ImportFileBuilder::new() + ->replace(['category' => '']) + ->forget(['quantity']); + + $row = $importFileBuilder->firstRow(); + $import = ImportFactory::new()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + + $this->importFileResponse(['import' => $import->id]) + ->assertInternalServerError() + ->assertExactJson([ + 'status' => 'import-errors', + 'payload' => null, + 'messages' => [ + $row['itemName'] => [ + 'Component' => [ + 'qty' => ['The qty field must be at least 1.'], + 'category_id' => ['The category id field is required.'] + ] + ] + ] + ]); + + $newComponents = Component::query() + ->whereIn('serial', $importFileBuilder->pluck('serialNumber')) + ->get(); + + $this->assertCount(0, $newComponents); + } + + #[Test] + public function updateComponentFromImport(): void + { + $component = ComponentFactory::new()->create(); + $importFileBuilder = ImportFileBuilder::new([ + 'itemName' => $component->name, + 'serialNumber' => $component->serial + ]); + + $row = $importFileBuilder->firstRow(); + $import = ImportFactory::new()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk(); + + $updatedComponent = Component::query() + ->with(['location', 'category']) + ->where('serial', $row['serialNumber']) + ->sole(); + + $this->assertEquals($updatedComponent->name, $row['itemName']); + $this->assertEquals($updatedComponent->category->name, $row['category']); + $this->assertEquals($updatedComponent->location->name, $row['location']); + $this->assertEquals($updatedComponent->supplier_id, $component->supplier_id); + $this->assertEquals($updatedComponent->qty, $row['quantity']); + $this->assertEquals($updatedComponent->order_number, $row['orderNumber']); + $this->assertEquals($updatedComponent->purchase_date->toDateString(), $row['purchaseDate']); + $this->assertEquals($updatedComponent->purchase_cost, $row['purchaseCost']); + $this->assertEquals($updatedComponent->min_amt, $component->min_amt); + $this->assertEquals($updatedComponent->serial, $row['serialNumber']); + $this->assertEquals($updatedComponent->image, $component->image); + $this->assertEquals($updatedComponent->notes, $component->notes); + } + + #[Test] + public function customColumnMapping(): void + { + $faker = ImportFileBuilder::new()->definition(); + $row = [ + 'category' => $faker['serialNumber'], + 'companyName' => $faker['quantity'], + 'itemName' => $faker['purchaseDate'], + 'location' => $faker['purchaseCost'], + 'orderNumber' => $faker['orderNumber'], + 'purchaseCost' => $faker['category'], + 'purchaseDate' => $faker['companyName'], + 'quantity' => $faker['itemName'], + 'serialNumber' => $faker['location'] + ]; + + $importFileBuilder = new ImportFileBuilder([$row]); + $import = ImportFactory::new()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + + $this->importFileResponse([ + 'import' => $import->id, + 'column-mappings' => [ + 'Category' => 'serial', + 'Company' => 'quantity', + 'item Name' => 'purchase_date', + 'Location' => 'purchase_cost', + 'Order Number' => 'order_number', + 'Purchase Cost' => 'category', + 'Purchase Date' => 'company', + 'Quantity' => 'item_name', + 'Serial number' => 'location', + ] + ])->assertOk(); + + $newComponent = Component::query() + ->with(['location', 'category']) + ->where('serial', $importFileBuilder->firstRow()['category']) + ->sole(); + + $this->assertEquals($newComponent->name, $row['quantity']); + $this->assertEquals($newComponent->category->name, $row['purchaseCost']); + $this->assertEquals($newComponent->location->name, $row['serialNumber']); + $this->assertNull($newComponent->supplier_id); + $this->assertEquals($newComponent->qty, $row['companyName']); + $this->assertEquals($newComponent->order_number, $row['orderNumber']); + $this->assertEquals($newComponent->purchase_date->toDateString(), $row['itemName']); + $this->assertEquals($newComponent->purchase_cost, $row['location']); + $this->assertNull($newComponent->min_amt); + $this->assertNull($newComponent->image); + $this->assertNull($newComponent->notes); + } +} diff --git a/tests/Feature/Importing/Api/ImportConsumablesTest.php b/tests/Feature/Importing/Api/ImportConsumablesTest.php new file mode 100644 index 0000000000..7591ee9711 --- /dev/null +++ b/tests/Feature/Importing/Api/ImportConsumablesTest.php @@ -0,0 +1,309 @@ +map(fn (string $permission) => [$permission => '1']) + ->toJson(); + + $this->actingAsForApi(UserFactory::new()->create(['permissions' => $permissions])); + + $this->importFileResponse(['import' => 44])->assertForbidden(); + } + + #[Test] + public function userWithImportAssetsPermissionCanImportConsumables(): void + { + $this->actingAsForApi(UserFactory::new()->canImport()->create()); + + $import = ImportFactory::new()->consumable()->create(); + + $this->importFileResponse(['import' => $import->id])->assertOk(); + } + + #[Test] + public function importConsumables(): void + { + Notification::fake(); + + $importFileBuilder = ImportFileBuilder::new(); + $row = $importFileBuilder->firstRow(); + $import = ImportFactory::new()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id]) + ->assertOk() + ->assertExactJson([ + 'payload' => null, + 'status' => 'success', + 'messages' => ['redirect_url' => route('consumables.index')] + ]); + + $newConsumable = Consumable::query() + ->with(['location', 'category', 'company']) + ->where('name', $row['itemName']) + ->sole(); + + $activityLog = ActivityLog::query() + ->where('item_type', Consumable::class) + ->where('item_id', $newConsumable->id) + ->sole(); + + $this->assertEquals($activityLog->action_type, 'create'); + $this->assertEquals($activityLog->action_source, 'importer'); + $this->assertEquals($activityLog->company_id, $newConsumable->company->id); + + $this->assertEquals($newConsumable->name, $row['itemName']); + $this->assertEquals($newConsumable->category->name, $row['category']); + $this->assertEquals($newConsumable->location->name, $row['location']); + $this->assertEquals($newConsumable->company->name, $row['companyName']); + $this->assertNull($newConsumable->supplier_id); + $this->assertFalse($newConsumable->requestable); + $this->assertNull($newConsumable->image); + $this->assertEquals($newConsumable->order_number, $row['orderNumber']); + $this->assertEquals($newConsumable->purchase_date->toDateString(), $row['purchaseDate']); + $this->assertEquals($newConsumable->purchase_cost, $row['purchaseCost']); + $this->assertNull($newConsumable->min_amt); + $this->assertEquals($newConsumable->model_number, ''); + $this->assertNull($newConsumable->item_number); + $this->assertNull($newConsumable->manufacturer_id); + $this->assertNull($newConsumable->notes); + } + + #[Test] + public function willIgnoreUnknownColumnsWhenFileContainsUnknownColumns(): void + { + $row = ImportFileBuilder::new()->definition(); + $row['unknownColumnInCsvFile'] = 'foo'; + + $importFileBuilder = new ImportFileBuilder([$row]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + + $import = ImportFactory::new()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->importFileResponse(['import' => $import->id])->assertOk(); + } + + #[Test] + public function willNotCreateNewConsumableWhenConsumableNameAlreadyExist(): void + { + $consumable = ConsumableFactory::new()->create(['name' => Str::random()]); + $importFileBuilder = ImportFileBuilder::new(['itemName' => $consumable->name]); + $import = ImportFactory::new()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $probablyNewConsumables = Consumable::query() + ->where('name', $consumable->name) + ->get(); + + $this->assertCount(1, $probablyNewConsumables); + $this->assertEquals($consumable->id, $probablyNewConsumables->sole()->id); + } + + #[Test] + public function willNotCreateNewCompanyWhenCompanyExists(): void + { + $importFileBuilder = ImportFileBuilder::times(4)->replace(['companyName' => Str::random()]); + $import = ImportFactory::new()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newConsumables = Consumable::query() + ->whereIn('name', $importFileBuilder->pluck('itemName')) + ->get(['company_id']); + + $this->assertCount(1, $newConsumables->pluck('company_id')->unique()->all()); + } + + #[Test] + public function willNotCreateNewLocationWhenLocationExists(): void + { + $importFileBuilder = ImportFileBuilder::times(4)->replace(['location' => Str::random()]); + $import = ImportFactory::new()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newConsumables = Consumable::query() + ->whereIn('name', $importFileBuilder->pluck('itemName')) + ->get(['location_id']); + + $this->assertCount(1, $newConsumables->pluck('location_id')->unique()->all()); + } + + #[Test] + public function willNotCreateNewCategoryWhenCategoryExists(): void + { + $importFileBuilder = ImportFileBuilder::times(4)->replace(['category' => Str::random()]); + $import = ImportFactory::new()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newConsumables = Consumable::query() + ->whereIn('name', $importFileBuilder->pluck('itemName')) + ->get(['category_id']); + + $this->assertCount(1, $newConsumables->pluck('category_id')->unique()->all()); + } + + #[Test] + public function whenRequiredColumnsAreMissingInImportFile(): void + { + $importFileBuilder = ImportFileBuilder::new(['category' => ''])->forget(['quantity', 'name']); + + $row = $importFileBuilder->firstRow(); + $import = ImportFactory::new()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + + $this->importFileResponse(['import' => $import->id]) + ->assertInternalServerError() + ->assertExactJson([ + 'status' => 'import-errors', + 'payload' => null, + 'messages' => [ + $row['itemName'] => [ + 'Consumable' => [ + 'category_id' => ['The category id field is required.'] + ] + ] + ] + ]); + + $newConsumables = Consumable::query() + ->whereIn('name', $importFileBuilder->pluck('itemName')) + ->get(['id']); + + $this->assertCount(0, $newConsumables); + } + + #[Test] + public function updateConsumableFromImport(): void + { + $consumable = ConsumableFactory::new()->create(['name' => Str::random()]); + $importFileBuilder = ImportFileBuilder::new(['itemName' => $consumable->name]); + + $row = $importFileBuilder->firstRow(); + $import = ImportFactory::new()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk(); + + $updatedConsumable = Consumable::query() + ->with(['location', 'category', 'company']) + ->where('name', $importFileBuilder->firstRow()['itemName']) + ->sole(); + + $this->assertEquals($updatedConsumable->name, $row['itemName']); + $this->assertEquals($updatedConsumable->category->name, $row['category']); + $this->assertEquals($updatedConsumable->location->name, $row['location']); + $this->assertEquals($updatedConsumable->company->name, $row['companyName']); + $this->assertEquals($updatedConsumable->order_number, $row['orderNumber']); + $this->assertEquals($updatedConsumable->purchase_date->toDateString(), $row['purchaseDate']); + $this->assertEquals($updatedConsumable->purchase_cost, $row['purchaseCost']); + + $this->assertEquals($consumable->supplier_id, $updatedConsumable->supplier_id); + $this->assertEquals($consumable->requestable, $updatedConsumable->requestable); + $this->assertEquals($consumable->min_amt, $updatedConsumable->min_amt); + $this->assertEquals($consumable->model_number, $updatedConsumable->model_number); + $this->assertEquals($consumable->item_number, $updatedConsumable->item_number); + $this->assertEquals($consumable->manufacturer_id, $updatedConsumable->manufacturer_id); + $this->assertEquals($consumable->notes, $updatedConsumable->notes); + $this->assertEquals($consumable->item_number, $updatedConsumable->item_number); + } + + #[Test] + public function customColumnMapping(): void + { + $faker = ImportFileBuilder::new()->definition(); + $row = [ + 'category' => $faker['supplier'], + 'companyName' => $faker['quantity'], + 'itemName' => $faker['purchaseDate'], + 'location' => $faker['purchaseCost'], + 'orderNumber' => $faker['orderNumber'], + 'purchaseCost' => $faker['location'], + 'purchaseDate' => $faker['companyName'], + 'quantity' => $faker['itemName'], + 'supplier' => $faker['category'] + ]; + + $importFileBuilder = new ImportFileBuilder([$row]); + + $import = ImportFactory::new()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + + $this->importFileResponse([ + 'import' => $import->id, + 'column-mappings' => [ + 'Category' => 'supplier', + 'Company' => 'quantity', + 'item Name' => 'purchase_date', + 'Location' => 'purchase_cost', + 'Order Number' => 'order_number', + 'Purchase Cost' => 'location', + 'Purchase Date' => 'company', + 'Quantity' => 'item_name', + 'Supplier' => 'category', + ] + ])->assertOk(); + + $newConsumable = Consumable::query() + ->with(['location', 'category', 'company']) + ->where('name', $importFileBuilder->firstRow()['quantity']) + ->sole(); + + $this->assertEquals($newConsumable->category->name, $row['supplier']); + $this->assertEquals($newConsumable->location->name, $row['purchaseCost']); + $this->assertEquals($newConsumable->company->name, $row['purchaseDate']); + $this->assertEquals($newConsumable->qty, $row['companyName']); + $this->assertEquals($newConsumable->name, $row['quantity']); + $this->assertNull($newConsumable->supplier_id); + $this->assertFalse($newConsumable->requestable); + $this->assertNull($newConsumable->image); + $this->assertEquals($newConsumable->order_number, $row['orderNumber']); + $this->assertEquals($newConsumable->purchase_date->toDateString(), $row['itemName']); + $this->assertEquals($newConsumable->purchase_cost, $row['location']); + $this->assertNull($newConsumable->min_amt); + $this->assertEquals($newConsumable->model_number, ''); + $this->assertNull($newConsumable->item_number); + $this->assertNull($newConsumable->manufacturer_id); + $this->assertNull($newConsumable->notes); + } +} diff --git a/tests/Feature/Importing/Api/ImportDataTestCase.php b/tests/Feature/Importing/Api/ImportDataTestCase.php new file mode 100644 index 0000000000..ac39fab677 --- /dev/null +++ b/tests/Feature/Importing/Api/ImportDataTestCase.php @@ -0,0 +1,36 @@ +postJson(route('api.imports.importFile', $parameters), $parameters); + } + + /** + * @todo Add more permissions. + */ + public static function permissionsTestData(): array + { + return [ + '`admin`' => ['admin'], + '`reports.view`' => ['reports.view'], + 'only `assets` permission' => [ + 'assets.view', + 'assets.create', + 'assets.edit', + 'assets.delete', + 'assets.checkout', + 'assets.checkin', + 'assets.audit', + 'assets.view.requestable', + 'assets.view.encrypted_custom_fields' + ] + ]; + } +} diff --git a/tests/Feature/Importing/Api/ImportLicenseTest.php b/tests/Feature/Importing/Api/ImportLicenseTest.php new file mode 100644 index 0000000000..a5147d55ae --- /dev/null +++ b/tests/Feature/Importing/Api/ImportLicenseTest.php @@ -0,0 +1,360 @@ +map(fn (string $permission) => [$permission => '1']) + ->toJson(); + + $this->actingAsForApi(UserFactory::new()->create(['permissions' => $permissions])); + + $this->importFileResponse(['import' => 44])->assertForbidden(); + } + + #[Test] + public function userWithImportAssetsPermissionCanImportLicenses(): void + { + $this->actingAsForApi(UserFactory::new()->canImport()->create()); + + $import = ImportFactory::new()->license()->create(); + + $this->importFileResponse(['import' => $import->id])->assertOk(); + } + + #[Test] + public function importLicenses(): void + { + $importFileBuilder = ImportFileBuilder::new(); + $row = $importFileBuilder->firstRow(); + $import = ImportFactory::new()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id]) + ->assertOk() + ->assertExactJson([ + 'payload' => null, + 'status' => 'success', + 'messages' => ['redirect_url' => route('licenses.index')] + ]); + + $newLicense = License::query() + ->withCasts(['reassignable' => 'bool']) + ->with(['category', 'company', 'manufacturer', 'supplier']) + ->where('serial', $row['serialNumber']) + ->sole(); + + $activityLogs = ActivityLog::query() + ->where('item_type', License::class) + ->where('item_id', $newLicense->id) + ->get(); + + $this->assertCount(2, $activityLogs); + + $this->assertEquals($newLicense->name, $row['licenseName']); + $this->assertEquals($newLicense->serial, $row['serialNumber']); + $this->assertEquals($newLicense->purchase_date->toDateString(), $row['purchaseDate']); + $this->assertEquals($newLicense->purchase_cost, $row['purchaseCost']); + $this->assertEquals($newLicense->order_number, $row['orderNumber']); + $this->assertEquals($newLicense->seats, $row['seats']); + $this->assertEquals($newLicense->notes, $row['notes']); + $this->assertEquals($newLicense->license_name, $row['licensedToName']); + $this->assertEquals($newLicense->license_email, $row['licensedToEmail']); + $this->assertEquals($newLicense->supplier->name, $row['supplierName']); + $this->assertEquals($newLicense->company->name, $row['companyName']); + $this->assertEquals($newLicense->category->name, $row['category']); + $this->assertEquals($newLicense->expiration_date->toDateString(), $row['expirationDate']); + $this->assertEquals($newLicense->maintained, $row['isMaintained'] === 'TRUE'); + $this->assertEquals($newLicense->reassignable, $row['isReassignAble'] === 'TRUE'); + $this->assertEquals($newLicense->purchase_order, ''); + $this->assertNull($newLicense->depreciation_id); + $this->assertNull($newLicense->termination_date); + $this->assertNull($newLicense->deprecate); + $this->assertNull($newLicense->min_amt); + } + + #[Test] + public function willIgnoreUnknownColumnsWhenFileContainsUnknownColumns(): void + { + $row = ImportFileBuilder::new()->definition(); + $row['unknownColumnInCsvFile'] = 'foo'; + + $importFileBuilder = new ImportFileBuilder([$row]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + + $import = ImportFactory::new()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->importFileResponse(['import' => $import->id])->assertOk(); + } + + #[Test] + public function willNotCreateNewLicenseWhenNameAndSerialNumberAlreadyExist(): void + { + $license = LicenseFactory::new()->create(); + + $importFileBuilder = ImportFileBuilder::times(4)->replace([ + 'itemName' => $license->name, + 'serialNumber' => $license->serial + ]); + + $import = ImportFactory::new()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $probablyNewLicenses = License::query() + ->where('name', $license->name) + ->where('serial', $license->serial) + ->get(); + + $this->assertCount(1, $probablyNewLicenses); + } + + #[Test] + public function formatAttributes(): void + { + $importFileBuilder = ImportFileBuilder::new([ + 'expirationDate' => '2022/10/10' + ]); + + $import = ImportFactory::new()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newLicense = License::query() + ->where('serial', $importFileBuilder->firstRow()['serialNumber']) + ->sole(); + + $this->assertEquals($newLicense->expiration_date->toDateString(), '2022-10-10'); + } + + #[Test] + public function willNotCreateNewCompanyWhenCompanyExists(): void + { + $importFileBuilder = ImportFileBuilder::times(4)->replace(['companyName' => Str::random()]); + $import = ImportFactory::new()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newLicenses = License::query() + ->whereIn('serial', $importFileBuilder->pluck('serialNumber')) + ->get(['company_id']); + + $this->assertCount(1, $newLicenses->pluck('company_id')->unique()->all()); + } + + #[Test] + public function willNotCreateNewManufacturerWhenManufacturerExists(): void + { + $importFileBuilder = ImportFileBuilder::times(4)->replace(['manufacturerName' => Str::random()]); + $import = ImportFactory::new()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newLicenses = License::query() + ->whereIn('serial', $importFileBuilder->pluck('serialNumber')) + ->get(['manufacturer_id']); + + $this->assertCount(1, $newLicenses->pluck('manufacturer_id')->unique()->all()); + } + + #[Test] + public function willNotCreateNewCategoryWhenCategoryExists(): void + { + $importFileBuilder = ImportFileBuilder::times(4)->replace(['category' => $this->faker->company]); + $import = ImportFactory::new()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newLicenses = License::query() + ->whereIn('serial', $importFileBuilder->pluck('serialNumber')) + ->get(['category_id']); + + $this->assertCount(1, $newLicenses->pluck('category_id')->unique()->all()); + } + + #[Test] + public function whenRequiredColumnsAreMissingInImportFile(): void + { + $importFileBuilder = ImportFileBuilder::times() + ->replace(['name' => '']) + ->forget(['seats']); + + $row = $importFileBuilder->firstRow(); + $import = ImportFactory::new()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + + $this->importFileResponse(['import' => $import->id]) + ->assertInternalServerError() + ->assertExactJson([ + 'status' => 'import-errors', + 'payload' => null, + 'messages' => [ + $row['licenseName'] => [ + "License \"{$row['licenseName']}\"" => [ + 'seats' => ['The seats field is required.'], + ] + ] + ] + ]); + + $newLicenses = License::query() + ->where('serial', $row['serialNumber']) + ->get(); + + $this->assertCount(0, $newLicenses); + } + + #[Test] + public function updateLicenseFromImport(): void + { + $license = LicenseFactory::new()->create(); + $importFileBuilder = ImportFileBuilder::new([ + 'licenseName' => $license->name, + 'serialNumber' => $license->serial + ]); + + $row = $importFileBuilder->firstRow(); + $import = ImportFactory::new()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk(); + + $updatedLicense = License::query() + ->with(['manufacturer', 'category', 'supplier']) + ->where('serial', $row['serialNumber']) + ->sole(); + + $this->assertEquals($updatedLicense->name, $row['licenseName']); + $this->assertEquals($updatedLicense->serial, $row['serialNumber']); + $this->assertEquals($updatedLicense->purchase_date->toDateString(), $row['purchaseDate']); + $this->assertEquals($updatedLicense->purchase_cost, $row['purchaseCost']); + $this->assertEquals($updatedLicense->order_number, $row['orderNumber']); + $this->assertEquals($updatedLicense->seats, $row['seats']); + $this->assertEquals($updatedLicense->notes, $row['notes']); + $this->assertEquals($updatedLicense->license_name, $row['licensedToName']); + $this->assertEquals($updatedLicense->license_email, $row['licensedToEmail']); + $this->assertEquals($updatedLicense->supplier->name, $row['supplierName']); + $this->assertEquals($updatedLicense->company->name, $row['companyName']); + $this->assertEquals($updatedLicense->category->name, $row['category']); + $this->assertEquals($updatedLicense->expiration_date->toDateString(), $row['expirationDate']); + $this->assertEquals($updatedLicense->maintained, $row['isMaintained'] === 'TRUE'); + $this->assertEquals($updatedLicense->reassignable, $row['isReassignAble'] === 'TRUE'); + $this->assertEquals($updatedLicense->purchase_order, $license->purchase_order); + $this->assertEquals($updatedLicense->depreciation_id, $license->depreciation_id); + $this->assertEquals($updatedLicense->termination_date, $license->termination_date); + $this->assertEquals($updatedLicense->deprecate, $license->deprecate); + $this->assertEquals($updatedLicense->min_amt, $license->min_amt); + } + + #[Test] + public function customColumnMapping(): void + { + $faker = ImportFileBuilder::times()->definition(); + $row = [ + 'category' => $faker['supplierName'], + 'companyName' => $faker['serialNumber'], + 'expirationDate' => $faker['seats'], + 'isMaintained' => $faker['purchaseDate'], + 'isReassignAble' => $faker['purchaseCost'], + 'licensedToName' => $faker['orderNumber'], + 'licensedToEmail' => $faker['notes'], + 'licenseName' => $faker['licenseName'], + 'manufacturerName' => $faker['category'], + 'notes' => $faker['companyName'], + 'orderNumber' => $faker['expirationDate'], + 'purchaseCost' => $faker['isMaintained'], + 'purchaseDate' => $faker['isReassignAble'], + 'seats' => $faker['licensedToName'], + 'serialNumber' => $faker['licensedToEmail'], + 'supplierName' => $faker['manufacturerName'] + ]; + + $importFileBuilder = new ImportFileBuilder([$row]); + $import = ImportFactory::new()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + + $this->importFileResponse([ + 'import' => $import->id, + 'column-mappings' => [ + 'Category' => 'supplier', + 'Company' => 'serial', + 'expiration date' => 'seats', + 'maintained' => 'purchase_date', + 'reassignable' => 'purchase_cost', + 'Licensed To Name' => 'order_number', + 'Licensed To Email' => 'notes', + 'licenseName' => 'name', + 'manufacturer' => 'category', + 'Notes' => 'company', + 'Serial number' => 'license_email', + 'Order Number' => 'expiration_date', + 'purchase Cost' => 'maintained', + 'purchase Date' => 'reassignable', + 'seats' => 'license_name', + 'supplier' => 'manufacturer' + ] + ])->assertOk(); + + $newLicense = License::query() + ->with(['category', 'company', 'manufacturer', 'supplier']) + ->where('serial', $row['companyName']) + ->sole(); + + $this->assertEquals($newLicense->name, $row['licenseName']); + $this->assertEquals($newLicense->serial, $row['companyName']); + $this->assertEquals($newLicense->purchase_date->toDateString(), $row['isMaintained']); + $this->assertEquals($newLicense->purchase_cost, $row['isReassignAble']); + $this->assertEquals($newLicense->order_number, $row['licensedToName']); + $this->assertEquals($newLicense->seats, $row['expirationDate']); + $this->assertEquals($newLicense->notes, $row['licensedToEmail']); + $this->assertEquals($newLicense->license_name, $row['seats']); + $this->assertEquals($newLicense->license_email, $row['serialNumber']); + $this->assertEquals($newLicense->supplier->name, $row['category']); + $this->assertEquals($newLicense->company->name, $row['notes']); + $this->assertEquals($newLicense->category->name, $row['manufacturerName']); + $this->assertEquals($newLicense->expiration_date->toDateString(), $row['orderNumber']); + $this->assertEquals($newLicense->maintained, $row['purchaseCost'] === 'TRUE'); + $this->assertEquals($newLicense->reassignable, $row['purchaseDate'] === 'TRUE'); + $this->assertEquals($newLicense->purchase_order, ''); + $this->assertNull($newLicense->depreciation_id); + $this->assertNull($newLicense->termination_date); + $this->assertNull($newLicense->deprecate); + $this->assertNull($newLicense->min_amt); + } +} diff --git a/tests/Feature/Importing/Api/ImportUsersTest.php b/tests/Feature/Importing/Api/ImportUsersTest.php new file mode 100644 index 0000000000..4415da3f12 --- /dev/null +++ b/tests/Feature/Importing/Api/ImportUsersTest.php @@ -0,0 +1,340 @@ +map(fn (string $permission) => [$permission => '1']) + ->toJson(); + + $this->actingAsForApi(UserFactory::new()->create(['permissions' => $permissions])); + + $this->importFileResponse(['import' => 44])->assertForbidden(); + } + + #[Test] + public function userWithImportAssetsPermissionCanImportUsers(): void + { + $this->actingAsForApi(UserFactory::new()->canImport()->create()); + + $import = ImportFactory::new()->users()->create(); + + $this->importFileResponse(['import' => $import->id])->assertOk(); + } + + #[Test] + public function importUsers(): void + { + Notification::fake(); + + $importFileBuilder = ImportFileBuilder::new(); + $row = $importFileBuilder->firstRow(); + $import = ImportFactory::new()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id, 'send-welcome' => 1]) + ->assertOk() + ->assertExactJson([ + 'payload' => null, + 'status' => 'success', + 'messages' => ['redirect_url' => route('users.index')] + ]); + + $newUser = User::query() + ->with(['company', 'location']) + ->where('username', $row['username']) + ->sole(); + + Notification::assertNothingSent(); + + $this->assertEquals($newUser->email, $row['email']); + $this->assertEquals($newUser->first_name, $row['firstName']); + $this->assertEquals($newUser->last_name, $row['lastName']); + $this->assertEquals($newUser->employee_num, $row['employeeNumber']); + $this->assertEquals($newUser->company->name, $row['companyName']); + $this->assertEquals($newUser->location->name, $row['location']); + $this->assertEquals($newUser->phone, $row['phoneNumber']); + $this->assertEquals($newUser->jobtitle, $row['position']); + $this->assertTrue(Hash::isHashed($newUser->password)); + $this->assertEquals($newUser->website, ''); + $this->assertEquals($newUser->country, ''); + $this->assertEquals($newUser->address, ''); + $this->assertEquals($newUser->city, ''); + $this->assertEquals($newUser->state, ''); + $this->assertEquals($newUser->zip, ''); + $this->assertNull($newUser->permissions); + $this->assertNull($newUser->avatar); + $this->assertNull($newUser->notes); + $this->assertNull($newUser->skin); + $this->assertNull($newUser->department_id); + $this->assertNull($newUser->two_factor_secret); + $this->assertNull($newUser->idap_import); + $this->assertEquals($newUser->locale, 'en-US'); + $this->assertEquals($newUser->show_in_list, 1); + $this->assertEquals($newUser->two_factor_enrolled, 0); + $this->assertEquals($newUser->two_factor_optin, 0); + $this->assertEquals($newUser->remote, 0); + $this->assertEquals($newUser->autoassign_licenses, 0); + $this->assertEquals($newUser->vip, 0); + $this->assertEquals($newUser->enable_sounds, 0); + $this->assertEquals($newUser->enable_confetti, 0); + $this->assertNull($newUser->created_by); + $this->assertNull($newUser->start_date); + $this->assertNull($newUser->end_date); + $this->assertNull($newUser->scim_externalid); + $this->assertNull($newUser->manager_id); + $this->assertNull($newUser->activation_code); + $this->assertNull($newUser->last_login); + $this->assertNull($newUser->persist_code); + $this->assertNull($newUser->reset_password_code); + $this->assertEquals($newUser->activated, 0); + } + + #[Test] + public function willIgnoreUnknownColumnsWhenFileContainsUnknownColumns(): void + { + $row = ImportFileBuilder::new()->definition(); + $row['unknownColumnInCsvFile'] = 'foo'; + + $importFileBuilder = new ImportFileBuilder([$row]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + + $import = ImportFactory::new()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->importFileResponse(['import' => $import->id])->assertOk(); + } + + #[Test] + public function willNotCreateNewUserWhenUserWithUserNameAlreadyExist(): void + { + $user = UserFactory::new()->create(['username' => Str::random()]); + $importFileBuilder = ImportFileBuilder::times(4)->replace(['username' => $user->username]); + $import = ImportFactory::new()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $probablyNewUsers = User::query() + ->where('username', $user->username) + ->get(); + + $this->assertCount(1, $probablyNewUsers); + } + + #[Test] + public function willGenerateUsernameWhenUsernameFieldIsMissing(): void + { + $importFileBuilder = ImportFileBuilder::new()->forget('username'); + $row = $importFileBuilder->firstRow(); + $import = ImportFactory::new()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id])->assertOk(); + + $newUser = User::query() + ->where('email', $row['email']) + ->sole(); + + $generatedUsername = User::generateFormattedNameFromFullName("{$row['firstName']} {$row['lastName']}")['username']; + + $this->assertEquals($newUser->username, $generatedUsername); + } + + #[Test] + public function willUpdateLocationOfAllAssetsAssignedToUser(): void + { + $user = UserFactory::new()->create(['username' => Str::random()]); + $assetsAssignedToUser = AssetFactory::new()->create(['assigned_to' => $user->id, 'assigned_type' => User::class]); + $importFileBuilder = ImportFileBuilder::new(['username' => $user->username]); + $import = ImportFactory::new()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk(); + + $userLocation = Location::query()->where('name', $importFileBuilder->firstRow()['location'])->sole(['id']); + + $this->assertEquals( + $assetsAssignedToUser->refresh()->location_id, + $userLocation->id + ); + } + + #[Test] + public function whenRequiredColumnsAreMissingInImportFile(): void + { + $importFileBuilder = ImportFileBuilder::new(['firstName' => ''])->forget(['username']); + $import = ImportFactory::new()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + + $this->importFileResponse(['import' => $import->id]) + ->assertInternalServerError() + ->assertExactJson([ + 'status' => 'import-errors', + 'payload' => null, + 'messages' => [ + '' => [ + 'User' => [ + 'first_name' => ['The first name field is required.'], + ] + ] + ] + ]); + + $newUsers = User::query() + ->where('email', $importFileBuilder->firstRow()['email']) + ->get(); + + $this->assertCount(0, $newUsers); + } + + #[Test] + public function updateUserFromImport(): void + { + $user = UserFactory::new()->create(['username' => Str::random()])->refresh(); + $importFileBuilder = ImportFileBuilder::new(['username' => $user->username]); + + $row = $importFileBuilder->firstRow(); + $import = ImportFactory::new()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + $this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk(); + + $updatedUser = User::query()->with(['company', 'location'])->find($user->id); + $updatedAttributes = [ + 'first_name', 'email', 'last_name', 'employee_num', 'company', + 'location_id', 'company_id', 'updated_at', 'phone', 'jobtitle' + ]; + + $this->assertEquals($updatedUser->email, $row['email']); + $this->assertEquals($updatedUser->first_name, $row['firstName']); + $this->assertEquals($updatedUser->last_name, $row['lastName']); + $this->assertEquals($updatedUser->employee_num, $row['employeeNumber']); + $this->assertEquals($updatedUser->company->name, $row['companyName']); + $this->assertEquals($updatedUser->location->name, $row['location']); + $this->assertEquals($updatedUser->phone, $row['phoneNumber']); + $this->assertEquals($updatedUser->jobtitle, $row['position']); + $this->assertTrue(Hash::isHashed($updatedUser->password)); + + $this->assertEquals( + Arr::except($updatedUser->attributesToArray(), $updatedAttributes), + Arr::except($user->attributesToArray(), $updatedAttributes), + ); + } + + #[Test] + public function customColumnMapping(): void + { + $faker = ImportFileBuilder::new()->definition(); + $row = [ + 'companyName' => $faker['username'], + 'email' => $faker['position'], + 'employeeNumber' => $faker['phoneNumber'], + 'firstName' => $faker['location'], + 'lastName' => $faker['lastName'], + 'location' => $faker['firstName'], + 'phoneNumber' => $faker['employeeNumber'], + 'position' => $faker['email'], + 'username' => $faker['companyName'], + ]; + + $importFileBuilder = new ImportFileBuilder([$row]); + $import = ImportFactory::new()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]); + + $this->actingAsForApi(UserFactory::new()->superuser()->create()); + + $this->importFileResponse([ + 'import' => $import->id, + 'column-mappings' => [ + 'Company' => 'username', + 'email' => 'jobtitle', + 'Employee Number' => 'phone_number', + 'First Name' => 'location', + 'Last Name' => 'last_name', + 'Location' => 'first_name', + 'Phone Number' => 'employee_num', + 'Job Title' => 'email', + 'Username' => 'company', + ] + ])->assertOk(); + + $newUser = User::query() + ->with(['company', 'location']) + ->where('username', $row['companyName']) + ->sole(); + + $this->assertEquals($newUser->email, $row['position']); + $this->assertEquals($newUser->first_name, $row['location']); + $this->assertEquals($newUser->last_name, $row['lastName']); + $this->assertEquals($newUser->jobtitle, $row['email']); + $this->assertEquals($newUser->employee_num, $row['phoneNumber']); + $this->assertEquals($newUser->company->name, $row['username']); + $this->assertEquals($newUser->location->name, $row['firstName']); + $this->assertEquals($newUser->phone, $row['employeeNumber']); + $this->assertTrue(Hash::isHashed($newUser->password)); + $this->assertEquals($newUser->website, ''); + $this->assertEquals($newUser->country, ''); + $this->assertEquals($newUser->address, ''); + $this->assertEquals($newUser->city, ''); + $this->assertEquals($newUser->state, ''); + $this->assertEquals($newUser->zip, ''); + $this->assertNull($newUser->permissions); + $this->assertNull($newUser->avatar); + $this->assertNull($newUser->notes); + $this->assertNull($newUser->skin); + $this->assertNull($newUser->department_id); + $this->assertNull($newUser->two_factor_secret); + $this->assertNull($newUser->idap_import); + $this->assertEquals($newUser->locale, 'en-US'); + $this->assertEquals($newUser->show_in_list, 1); + $this->assertEquals($newUser->two_factor_enrolled, 0); + $this->assertEquals($newUser->two_factor_optin, 0); + $this->assertEquals($newUser->remote, 0); + $this->assertEquals($newUser->autoassign_licenses, 0); + $this->assertEquals($newUser->vip, 0); + $this->assertEquals($newUser->enable_sounds, 0); + $this->assertEquals($newUser->enable_confetti, 0); + $this->assertNull($newUser->created_by); + $this->assertNull($newUser->start_date); + $this->assertNull($newUser->end_date); + $this->assertNull($newUser->scim_externalid); + $this->assertNull($newUser->manager_id); + $this->assertNull($newUser->activation_code); + $this->assertNull($newUser->last_login); + $this->assertNull($newUser->persist_code); + $this->assertNull($newUser->reset_password_code); + $this->assertEquals($newUser->activated, 0); + } +} diff --git a/tests/Support/Importing/AccessoriesImportFileBuilder.php b/tests/Support/Importing/AccessoriesImportFileBuilder.php new file mode 100644 index 0000000000..488c91fdc4 --- /dev/null +++ b/tests/Support/Importing/AccessoriesImportFileBuilder.php @@ -0,0 +1,74 @@ + + */ +class AccessoriesImportFileBuilder extends FileBuilder +{ + /** + * @inheritdoc + */ + protected function getDictionary(): array + { + return [ + 'category' => 'Category', + 'companyName' => 'Company', + 'itemName' => 'Item Name', + 'location' => 'Location', + 'manufacturerName' => 'Manufacturer', + 'modelNumber' => 'Model Number', + 'notes' => 'Notes', + 'orderNumber' => 'Order Number', + 'purchaseCost' => 'Purchase Cost', + 'purchaseDate' => 'Purchase Date', + 'quantity' => 'Quantity', + 'supplierName' => 'Supplier', + ]; + } + + /** + * @inheritdoc + */ + public function definition(): array + { + $faker = fake(); + + return [ + 'category' => Str::random(), + 'companyName' => Str::random(), + 'itemName' => Str::random(), + 'location' => "{$faker->city}, {$faker->country}", + 'manufacturerName' => $faker->company, + 'modelNumber' => Str::random(), + 'notes' => $faker->sentence, + 'orderNumber' => Str::random(), + 'purchaseDate' => $faker->date(), + 'purchaseCost' => rand(1, 100), + 'quantity' => rand(1, 100), + 'supplierName' => $faker->company, + ]; + } +} diff --git a/tests/Support/Importing/AssetsImportFileBuilder.php b/tests/Support/Importing/AssetsImportFileBuilder.php new file mode 100644 index 0000000000..df573536c5 --- /dev/null +++ b/tests/Support/Importing/AssetsImportFileBuilder.php @@ -0,0 +1,92 @@ + + */ +class AssetsImportFileBuilder extends FileBuilder +{ + /** + * @inheritdoc + */ + protected function getDictionary(): array + { + return [ + 'assigneeFullName' => 'Full Name', + 'assigneeEmail' => 'Email', + 'assigneeUsername' => 'Username', + 'category' => 'Category', + 'companyName' => 'Company', + 'itemName' => 'item Name', + 'location' => 'Location', + 'manufacturerName' => 'Manufacturer', + 'model' => 'Model name', + 'modelNumber' => 'Model Number', + 'notes' => 'Notes', + 'purchaseCost' => 'Purchase Cost', + 'purchaseDate' => 'Purchase Date', + 'serialNumber' => 'Serial number', + 'supplierName' => 'Supplier', + 'status' => 'Status', + 'tag' => 'Asset Tag', + 'warrantyInMonths' => 'Warranty', + ]; + } + + /** + * @inheritdoc + */ + public function definition(): array + { + $faker = fake(); + + return [ + 'assigneeFullName' => $faker->name, + 'assigneeEmail' => $faker->email, + 'assigneeUsername' => $faker->userName, + 'category' => Str::random(), + 'companyName' => Str::random() . " {$faker->companySuffix}", + 'itemName' => Str::random(), + 'location' => "{$faker->country},{$faker->city}", + 'manufacturerName' => $faker->company, + 'model' => Str::random(), + 'modelNumber' => Str::random(), + 'notes' => $faker->sentence(5), + 'purchaseCost' => rand(1, 100_000), + 'purchaseDate' => $faker->date, + 'serialNumber' => $faker->uuid, + 'supplierName' => $faker->company, + 'status' => $faker->randomElement(['Ready to Deploy', 'Archived', 'Pending']), + 'tag' => Str::random(), + 'warrantyInMonths' => rand(1, 12), + ]; + } +} diff --git a/tests/Support/Importing/ComponentsImportFileBuilder.php b/tests/Support/Importing/ComponentsImportFileBuilder.php new file mode 100644 index 0000000000..6d0b163419 --- /dev/null +++ b/tests/Support/Importing/ComponentsImportFileBuilder.php @@ -0,0 +1,65 @@ + + */ +class ComponentsImportFileBuilder extends FileBuilder +{ + /** + * @inheritdoc + */ + protected function getDictionary(): array + { + return [ + 'category' => 'Category', + 'companyName' => 'Company', + 'itemName' => 'item Name', + 'location' => 'Location', + 'orderNumber' => 'Order Number', + 'purchaseCost' => 'Purchase Cost', + 'purchaseDate' => 'Purchase Date', + 'quantity' => 'Quantity', + 'serialNumber' => 'Serial number', + ]; + } + + /** + * @inheritdoc + */ + public function definition(): array + { + $faker = fake(); + + return [ + 'category' => Str::random(), + 'companyName' => Str::random() . " {$faker->companySuffix}", + 'itemName' => Str::random(), + 'location' => "{$faker->city}, {$faker->country}", + 'orderNumber' => "ON:COM:{$faker->uuid}", + 'purchaseCost' => rand(1, 100_000), + 'purchaseDate' => $faker->date, + 'quantity' => rand(1, 100_000), + 'serialNumber' => 'SN:COM:' . Str::random(), + ]; + } +} diff --git a/tests/Support/Importing/ConsumablesImportFileBuilder.php b/tests/Support/Importing/ConsumablesImportFileBuilder.php new file mode 100644 index 0000000000..16722a2f05 --- /dev/null +++ b/tests/Support/Importing/ConsumablesImportFileBuilder.php @@ -0,0 +1,65 @@ + + */ +class ConsumablesImportFileBuilder extends FileBuilder +{ + /** + * @inheritdoc + */ + protected function getDictionary(): array + { + return [ + 'category' => 'Category', + 'companyName' => 'Company', + 'itemName' => 'item Name', + 'location' => 'Location', + 'orderNumber' => 'Order Number', + 'purchaseCost' => 'Purchase Cost', + 'purchaseDate' => 'Purchase Date', + 'quantity' => 'Quantity', + 'supplier' => 'Supplier', + ]; + } + + /** + * @inheritdoc + */ + public function definition(): array + { + $faker = fake(); + + return [ + 'category' => Str::random(), + 'companyName' => Str::random() . " {$faker->companySuffix}", + 'itemName' => Str::random(), + 'location' => "{$faker->city}, {$faker->country}", + 'orderNumber' => "ON:CON:{$faker->uuid}", + 'purchaseCost' => rand(1, 100_000), + 'purchaseDate' => $faker->date, + 'quantity' => rand(1, 100_000), + 'supplier' => Str::random() . " {$faker->companySuffix}", + ]; + } +} diff --git a/tests/Support/Importing/FileBuilder.php b/tests/Support/Importing/FileBuilder.php new file mode 100644 index 0000000000..fad40054b4 --- /dev/null +++ b/tests/Support/Importing/FileBuilder.php @@ -0,0 +1,249 @@ + + */ + protected Collection $rows; + + /** + * Define the builders default row. + * + * @return Row + */ + abstract public function definition(); + + /** + * @param array $rows + */ + public function __construct(array $rows = []) + { + $this->rows = new Collection($rows); + } + + /** + * Get a new file builder instance. + * + * @param Row $attributes + * + * @return static + */ + public static function new(array $attributes = []) + { + $instance = new static; + + return $instance->push($instance->definition())->replace($attributes); + } + + /** + * Get a new file builder instance from an import file. + * + * @return static + */ + public static function fromFile(string $filepath) + { + $instance = new static; + + $reader = Reader::createFromPath($filepath); + $importFileHeaders = $reader->first(); + $dictionary = array_flip($instance->getDictionary()); + + foreach ($reader->getRecords() as $key => $record) { + $row = []; + + //Skip header. + if ($key === 0) { + continue; + } + + foreach ($record as $index => $value) { + $columnNameInImportFile = $importFileHeaders[$index]; + + //Try to map the value to a dictionary or use the file's + //column if the key is not defined in the dictionary. + $row[$dictionary[$columnNameInImportFile] ?? $columnNameInImportFile] = $value; + } + + $instance->push($row); + } + + return $instance; + } + + /** + * Get a new builder instance for the given number of rows. + * + * @return static + */ + public static function times(int $amountOfRows = 1) + { + $instance = new static; + + for ($i = 1; $i <= $amountOfRows; $i++) { + $instance->push($instance->definition()); + } + + return $instance; + } + + /** + * The the dictionary for mapping row keys to the corresponding import file headers. + * + * @return array + */ + protected function getDictionary(): array + { + return []; + } + + /** + * Add a new row. + * + * @param Row $row + * + * @return $this + */ + public function push(array $row) + { + if (!empty($row)) { + $this->rows->push($row); + } + + return $this; + } + + /** + * Pluck an array of values from the rows. + */ + public function pluck(string $key): array + { + return $this->rows->pluck($key)->all(); + } + + /** + * Replace the keys in each row with the values of the given replacement if they exist. + * + * @param array $replacement + * + * @return $this + */ + public function replace(array $replacement) + { + $this->rows = $this->rows->map(function (array $row) use ($replacement) { + foreach ($replacement as $key => $value) { + if (!array_key_exists($key, $row)) { + continue; + } + + $row[$key] = $value; + } + + return $row; + }); + + return $this; + } + + /** + * Remove the the given keys from all rows. + * + * @param string|array $keys + * + * @return $this + */ + public function forget(array|string $keys) + { + $keys = (array) $keys; + + $this->rows = $this->rows->map(function (array $row) use ($keys) { + foreach ($keys as $key) { + unset($row[$key]); + } + + return $row; + }); + + return $this; + } + + public function toCsv(): array + { + if ($this->rows->isEmpty()) { + return []; + } + + $headers = []; + $rows = $this->rows; + $dictionary = $this->getDictionary(); + + foreach (array_keys($rows->first()) as $key) { + $headers[] = $dictionary[$key] ?? $key; + } + + return $rows + ->map(fn (array $row) => array_values(array_combine($headers, $row))) + ->prepend($headers) + ->all(); + } + + /** + * Save the rows to the imports folder as a csv file. + * + * @return string The filename. + */ + public function saveToImportsDirectory(?string $filename = null): string + { + $filename ??= Str::random(40) . '.csv'; + + try { + $stream = fopen(config('app.private_uploads') . "/imports/{$filename}", 'w'); + + foreach ($this->toCsv() as $row) { + fputcsv($stream, $row); + } + + return $filename; + } finally { + if (is_resource($stream)) { + fclose($stream); + } + } + } + + /** + * Get the first row of the import file. + * + * @throws OutOfBoundsException + * + * @return Row + */ + public function firstRow(): array + { + return $this->rows->first(null, fn () => throw new OutOfBoundsException('Could not retrieve row from collection.')); + } + + /** + * Get the all the rows of the import file. + * + * @return array + */ + public function all(): array + { + return $this->rows->all(); + } +} diff --git a/tests/Support/Importing/LicensesImportFileBuilder.php b/tests/Support/Importing/LicensesImportFileBuilder.php new file mode 100644 index 0000000000..2b5e5c01ee --- /dev/null +++ b/tests/Support/Importing/LicensesImportFileBuilder.php @@ -0,0 +1,86 @@ + + */ +class LicensesImportFileBuilder extends FileBuilder +{ + /** + * @inheritdoc + */ + protected function getDictionary(): array + { + return [ + 'category' => 'Category', + 'companyName' => 'Company', + 'expirationDate' => 'expiration date', + 'isMaintained' => 'maintained', + 'isReassignAble' => 'reassignable', + 'licensedToName' => 'Licensed To Name', + 'licensedToEmail' => 'Licensed to Email', + 'licenseName' => 'Item name', + 'manufacturerName' => 'manufacturer', + 'notes' => 'notes', + 'orderNumber' => 'Order Number', + 'purchaseCost' => 'Purchase Cost', + 'purchaseDate' => 'Purchase Date', + 'seats' => 'seats', + 'serialNumber' => 'Serial number', + 'supplierName' => 'supplier', + ]; + } + + /** + * @inheritdoc + */ + public function definition(): array + { + $faker = fake(); + + return [ + 'category' => Str::random(), + 'companyName' => Str::random() . " {$faker->companySuffix}", + 'expirationDate' => $faker->date, + 'isMaintained' => $faker->randomElement(['TRUE', 'FALSE']), + 'isReassignAble' => $faker->randomElement(['TRUE', 'FALSE']), + 'licensedToName' => $faker->name, + 'licensedToEmail' => $faker->email, + 'licenseName' => $faker->company, + 'manufacturerName' => $faker->company, + 'notes' => $faker->sentence, + 'orderNumber' => "ON:LIC:{$faker->uuid}", + 'purchaseCost' => rand(1, 100_000), + 'purchaseDate' => $faker->date, + 'seats' => rand(1, 10), + 'serialNumber' => 'SN:LIC:' . Str::random(), + 'supplierName' => $faker->company, + ]; + } +} diff --git a/tests/Support/Importing/UsersImportFileBuilder.php b/tests/Support/Importing/UsersImportFileBuilder.php new file mode 100644 index 0000000000..0a00c995c2 --- /dev/null +++ b/tests/Support/Importing/UsersImportFileBuilder.php @@ -0,0 +1,65 @@ + + */ +class UsersImportFileBuilder extends FileBuilder +{ + /** + * @inheritdoc + */ + protected function getDictionary(): array + { + return [ + 'companyName' => 'Company', + 'email' => 'email', + 'employeeNumber' => 'Employee Number', + 'firstName' => 'First Name', + 'lastName' => 'Last Name', + 'location' => 'Location', + 'phoneNumber' => 'Phone Number', + 'position' => 'Job Title', + 'username' => 'Username', + ]; + } + + /** + * @inheritdoc + */ + public function definition(): array + { + $faker = fake(); + + return [ + 'companyName' => $faker->company, + 'email' => Str::random(32) . "@{$faker->freeEmailDomain}", + 'employeeNumber' => $faker->uuid, + 'firstName' => $faker->firstName, + 'lastName' => $faker->lastName, + 'location' => "{$faker->city}, {$faker->country}", + 'phoneNumber' => $faker->phoneNumber, + 'position' => $faker->jobTitle, + 'username' => Str::random(), + ]; + } +}