Merge remote-tracking branch 'origin/develop'

This commit is contained in:
snipe 2024-10-10 12:09:29 +01:00
commit 14b8b3b37c
30 changed files with 3294 additions and 30 deletions

View file

@ -97,7 +97,7 @@ API_TOKEN_EXPIRATION_YEARS=40
# --------------------------------------------
# OPTIONAL: SECURITY HEADER SETTINGS
# --------------------------------------------
APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1,172.0.0.0/8
APP_TRUSTED_PROXIES=192.168.1.1,10.0.0.1,172.16.0.0/12
ALLOW_IFRAMING=false
REFERRER_POLICY=same-origin
ENABLE_CSP=false

View file

@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Support\Facades\DB;
/**
* This controller provide the health route for
@ -15,13 +16,35 @@ use Illuminate\Routing\Controller as BaseController;
*/
class HealthController extends BaseController
{
public function __construct()
{
$this->middleware('health');
}
/**
* Returns a fixed JSON content ({ "status": "ok"}) which indicate the app is up and running
*/
public function get()
{
return response()->json([
'status' => 'ok',
]);
try {
if (DB::select('select 2 + 2')) {
return response()->json([
'status' => 'ok',
]);
}
} catch (\Exception $e) {
\Log::error('Could not connect to database');
return response()->json([
'status' => 'database connection failed',
], 500);
}
}
}

View file

@ -53,6 +53,10 @@ class Kernel extends HttpKernel
\App\Http\Middleware\CheckLocale::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'health' => [
],
];
/**
@ -69,5 +73,6 @@ class Kernel extends HttpKernel
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'health' => null,
];
}

View file

@ -7,14 +7,19 @@ use Closure;
class CheckForSetup
{
protected $except = [
'_debugbar*',
'health'
];
public function handle($request, Closure $next, $guard = null)
{
/**
* This is dumb
* @todo Check on removing this, not sure if it's still needed
* Skip this middleware for the debugbar and health check
*/
if ($request->is('_debugbar*')) {
if ($request->is($this->except)) {
return $next($request);
}
@ -25,7 +30,7 @@ class CheckForSetup
return $next($request);
}
} else {
if (! ($request->is('setup*')) && ! ($request->is('.env')) && ! ($request->is('health'))) {
if (! ($request->is('setup*')) && ! ($request->is('.env'))) {
return redirect(config('app.url').'/setup');
}

View file

@ -164,6 +164,7 @@ abstract class Importer
$this->log('------------- Action Summary ----------------');
}
Model::reguard();
});
}

View file

@ -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();
}
/**
@ -368,7 +368,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();
@ -1705,7 +1705,7 @@ class Asset extends Depreciable
});
});
}
/**
* THIS CLUNKY BIT IS VERY IMPORTANT
@ -1726,7 +1726,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
*
*/

View file

@ -2,10 +2,13 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Import extends Model
{
use HasFactory;
protected $casts = [
'header_row' => 'array',
'first_row' => 'array',

View file

@ -0,0 +1,146 @@
<?php
namespace Database\Factories;
use App\Models\Import;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Factories\Factory;
use Tests\Support\Importing;
/**
* @extends Factory<Import>
*/
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'] = 'accessory';
$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::new();
$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;
});
}
}

View file

@ -7,6 +7,9 @@ use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use \Auth;
/**
* @extends Factory<User>
*/
class UserFactory extends Factory
{
/**

View file

@ -34,6 +34,8 @@
'required' => true,
'asset_status_type' => 'RTD',
'select_id' => 'assigned_assets_select',
'asset_selector_div_id' => 'assets_to_checkout_div',
'asset_ids' => old('selected_assets')
])
@ -42,7 +44,7 @@
@include ('partials.forms.checkout-selector', ['user_select' => 'true','asset_select' => 'true', 'location_select' => 'true'])
@include ('partials.forms.edit.user-select', ['translated_name' => trans('general.user'), 'fieldname' => 'assigned_user'])
@include ('partials.forms.edit.asset-select', ['translated_name' => trans('general.asset'), 'fieldname' => 'assigned_asset', 'unselect' => 'true', 'style' => 'display:none;'])
@include ('partials.forms.edit.asset-select', ['translated_name' => trans('general.asset'), 'asset_selector_div_id' => 'assigned_asset', 'fieldname' => 'assigned_asset', 'unselect' => 'true', 'style' => 'display:none;'])
@include ('partials.forms.edit.location-select', ['translated_name' => trans('general.location'), 'fieldname' => 'assigned_location', 'style' => 'display:none;'])
<!-- Checkout/Checkin Date -->

View file

@ -1,5 +1,6 @@
<!-- Asset -->
<div id="assigned_asset" class="form-group{{ $errors->has($fieldname) ? ' has-error' : '' }}"{!! (isset($style)) ? ' style="'.e($style).'"' : '' !!}>
<div id="{{ $asset_selector_div_id ?? "assigned_asset" }}"
class="form-group{{ $errors->has($fieldname) ? ' has-error' : '' }}"{!! (isset($style)) ? ' style="'.e($style).'"' : '' !!}>
{{ Form::label($fieldname, $translated_name, array('class' => 'col-md-3 control-label')) }}
<div class="col-md-7">
<select class="js-data-ajax select2" data-endpoint="hardware" data-placeholder="{{ trans('general.select_asset') }}" aria-label="{{ $fieldname }}" name="{{ $fieldname }}" style="width: 100%" id="{{ (isset($select_id)) ? $select_id : 'assigned_asset_select' }}"{{ (isset($multiple)) ? ' multiple' : '' }}{!! (!empty($asset_status_type)) ? ' data-asset-status-type="' . $asset_status_type . '"' : '' !!}{{ ((isset($required) && ($required =='true'))) ? ' required' : '' }}>
@ -11,6 +12,15 @@
@else
@if(!isset($multiple))
<option value="" role="option">{{ trans('general.select_asset') }}</option>
@else
@if(isset($asset_ids))
@foreach($asset_ids as $asset_id)
<option value="{{ $asset_id }}" selected="selected" role="option" aria-selected="true"
role="option">
{{ (\App\Models\Asset::find($asset_id)) ? \App\Models\Asset::find($asset_id)->present()->fullName : '' }}
</option>
@endforeach
@endif
@endif
@endif
</select>

View file

@ -373,8 +373,11 @@
</table>
@endif
@php
if (!empty($eulas)) $eulas = array_unique($eulas);
@endphp
{{-- This may have been render at the top of the page if we're rendering more than one user... --}}
@if (count($users) === 1)
@if (count($users) === 1 && !empty($eulas))
<p></p>
<div class="pull-right">
<button class="btn btn-default hidden-print" type="button" data-toggle="collapse" data-target=".eula-row" aria-expanded="false" aria-controls="eula-row" title="EULAs">
@ -384,19 +387,16 @@
@endif
<table style="margin-top: 80px;">
@if (!empty($eulas))
<tr class="collapse eula-row">
<td style="padding-right: 10px; vertical-align: top; font-weight: bold;">EULA</td>
<td style="padding-right: 10px; vertical-align: top; padding-bottom: 80px;" colspan="3">
@php
if (!empty($eulas)) $eulas = array_unique($eulas);
@endphp
@if (!empty($eulas))
@foreach ($eulas as $key => $eula)
{!! $eula !!}
@endforeach
@endif
@foreach ($eulas as $key => $eula)
{!! $eula !!}
@endforeach
</td>
</tr>
@endif
<tr>
<td style="padding-right: 10px; vertical-align: top; font-weight: bold;">{{ trans('general.signed_off_by') }}:</td>
<td style="padding-right: 10px; vertical-align: top;">______________________________________</td>

View file

@ -31,7 +31,7 @@
<x-icon type="assets" class="fa-2x" />
</span>
<span class="hidden-xs hidden-sm">{{ trans('general.assets') }}
{!! ($user->assets()->AssetsForShow()->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($user->assets()->AssetsForShow()->count()).'</badge>' : '' !!}
{!! ($user->assets()->AssetsForShow()->count() > 0 ) ? '<badge class="badge badge-secondary">'.number_format($user->assets()->AssetsForShow()->withoutTrashed()->count()).'</badge>' : '' !!}
</span>
</a>
</li>

View file

@ -536,13 +536,16 @@ Route::group(['middleware' => 'web'], function () {
)->name('logout.post');
});
//Auth::routes();
Route::get(
'/health',
/**
* Health check route - skip middleware
*/
Route::withoutMiddleware(['web'])->get(
'/health',
[HealthController::class, 'get']
)->name('health');
Route::middleware(['auth'])->get(
'/',
[DashboardController::class, 'index']

View file

@ -0,0 +1,16 @@
<?php
namespace Tests\Feature\Importing\Api;
use App\Models\User;
class GeneralImportTest extends ImportDataTestCase
{
public function testRequiresExistingImport()
{
$this->actingAsForApi(User::factory()->canImport()->create());
$this->importFileResponse(['import' => 9999, 'import-type' => 'accessory'])
->assertStatusMessageIs('import-errors');
}
}

View file

@ -0,0 +1,420 @@
<?php
namespace Tests\Feature\Importing\Api;
use App\Models\Accessory;
use App\Models\Actionlog;
use App\Models\Import;
use App\Models\User;
use Illuminate\Support\Str;
use PHPUnit\Framework\Attributes\Test;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Arr;
use Illuminate\Testing\TestResponse;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\Support\Importing\AccessoriesImportFileBuilder as ImportFileBuilder;
use Tests\Support\Importing\CleansUpImportFiles;
class ImportAccessoriesTest extends ImportDataTestCase implements TestsPermissionsRequirement
{
use CleansUpImportFiles;
use WithFaker;
protected function importFileResponse(array $parameters = []): TestResponse
{
if (!array_key_exists('import-type', $parameters)) {
$parameters['import-type'] = 'accessory';
}
return parent::importFileResponse($parameters);
}
#[Test]
public function testRequiresPermission()
{
$this->actingAsForApi(User::factory()->create());
$this->importFileResponse(['import' => 44])->assertForbidden();
}
#[Test]
public function userWithImportAccessoryPermissionCanImportAccessories(): void
{
$this->actingAsForApi(User::factory()->canImport()->create());
$import = Import::factory()->accessory()->create();
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function importAccessory(): void
{
$importFileBuilder = ImportFileBuilder::new();
$row = $importFileBuilder->firstRow();
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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('create', $activityLog->action_type);
$this->assertEquals('importer', $activityLog->action_source);
$this->assertEquals($newAccessory->company->id, $activityLog->company_id);
$this->assertEquals($row['itemName'], $newAccessory->name);
$this->assertEquals($row['quantity'], $newAccessory->qty);
$this->assertEquals($row['purchaseDate'], $newAccessory->purchase_date->toDateString());
$this->assertEquals($row['purchaseCost'], $newAccessory->purchase_cost);
$this->assertEquals($row['orderNumber'], $newAccessory->order_number);
$this->assertEquals($row['notes'], $newAccessory->notes);
$this->assertEquals($row['category'], $newAccessory->category->name);
$this->assertEquals('accessory', $newAccessory->category->category_type);
$this->assertEquals($row['manufacturerName'], $newAccessory->manufacturer->name);
$this->assertEquals($row['supplierName'], $newAccessory->supplier->name);
$this->assertEquals($row['location'], $newAccessory->location->name);
$this->assertEquals($row['companyName'], $newAccessory->company->name);
$this->assertEquals($row['modelNumber'], $newAccessory->model_number);
$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(User::factory()->superuser()->create());
$import = Import::factory()->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 = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$accessory = Accessory::query()
->where('name', $importFileBuilder->firstRow()['itemName'])
->sole(['purchase_date']);
$this->assertEquals('2022-10-10', $accessory->purchase_date->toDateString());
}
#[Test]
public function willNotCreateNewCategoryWhenCategoryExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['category' => Str::random()]);
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Accessory::factory()->create(['name' => Str::random()]);
$importFileBuilder = ImportFileBuilder::times(2)->replace(['itemName' => $accessory->name]);
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$probablyNewAccessories = Accessory::query()
->where('name', $importFileBuilder->pluck('itemName'))
->get(['name']);
$this->assertCount(1, $probablyNewAccessories);
$this->assertEquals($accessory->name, $probablyNewAccessories->first()->name);
}
#[Test]
public function willNotCreateNewCompanyWhenCompanyAlreadyExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['companyName' => Str::random()]);
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Accessory::factory()->create(['name' => Str::random()])->refresh();
$importFileBuilder = ImportFileBuilder::new(['itemName' => $accessory->name]);
$row = $importFileBuilder->firstRow();
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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($row['itemName'], $updatedAccessory->name);
$this->assertEquals($row['companyName'], $updatedAccessory->company->name);
$this->assertEquals($row['quantity'], $updatedAccessory->qty);
$this->assertEquals($row['purchaseDate'], $updatedAccessory->purchase_date->toDateString());
$this->assertEquals($row['purchaseCost'], $updatedAccessory->purchase_cost);
$this->assertEquals($row['orderNumber'], $updatedAccessory->order_number);
$this->assertEquals($row['notes'], $updatedAccessory->notes);
$this->assertEquals($row['category'], $updatedAccessory->category->name);
$this->assertEquals('accessory', $updatedAccessory->category->category_type);
$this->assertEquals($row['manufacturerName'], $updatedAccessory->manufacturer->name);
$this->assertEquals($row['supplierName'], $updatedAccessory->supplier->name);
$this->assertEquals($row['location'], $updatedAccessory->location->name);
$this->assertEquals($row['modelNumber'], $updatedAccessory->model_number);
$this->assertEquals(
Arr::except($accessory->attributesToArray(), $updatedAttributes),
Arr::except($updatedAccessory->attributesToArray(), $updatedAttributes),
);
}
#[Test]
public function whenImportFileContainsEmptyValues(): void
{
$accessory = Accessory::factory()->create(['name' => Str::random()]);
$accessory->refresh();
$importFileBuilder = ImportFileBuilder::new([
'companyName' => ' ',
'purchaseDate' => ' ',
'purchaseCost' => '',
'location' => '',
'companyName' => '',
'orderNumber' => '',
'category' => '',
'quantity' => '',
'manufacturerName' => '',
'supplierName' => '',
'notes' => '',
'requestAble' => '',
'minimumAmount' => '',
'modelNumber' => ''
]);
$import = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk();
$updatedAccessory = clone $accessory;
$updatedAccessory->refresh();
$this->assertEquals($accessory->toArray(), $updatedAccessory->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 = Import::factory()->accessory()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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($row['modelNumber'], $newAccessory->name);
$this->assertEquals($row['itemName'], $newAccessory->model_number);
$this->assertEquals($row['quantity'], $newAccessory->qty);
$this->assertEquals($row['notes'], $newAccessory->purchase_date->toDateString());
$this->assertEquals($row['location'], $newAccessory->purchase_cost);
$this->assertEquals($row['companyName'], $newAccessory->order_number);
$this->assertEquals($row['purchaseDate'], $newAccessory->notes);
$this->assertEquals($row['manufacturerName'], $newAccessory->category->name);
$this->assertEquals($row['category'], $newAccessory->manufacturer->name);
$this->assertEquals($row['purchaseCost'], $newAccessory->location->name);
}
}

View file

@ -0,0 +1,595 @@
<?php
namespace Tests\Feature\Importing\Api;
use App\Models\Actionlog as ActionLog;
use App\Models\Asset;
use App\Models\CustomField;
use App\Models\Import;
use App\Models\User;
use App\Notifications\CheckoutAssetNotification;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
use PHPUnit\Framework\Attributes\Test;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\Support\Importing\AssetsImportFileBuilder as ImportFileBuilder;
use Tests\Support\Importing\CleansUpImportFiles;
class ImportAssetsTest extends ImportDataTestCase implements TestsPermissionsRequirement
{
use CleansUpImportFiles;
use WithFaker;
protected function importFileResponse(array $parameters = []): TestResponse
{
if (!array_key_exists('import-type', $parameters)) {
$parameters['import-type'] = 'asset';
}
return parent::importFileResponse($parameters);
}
#[Test]
public function testRequiresPermission()
{
$this->actingAsForApi(User::factory()->create());
$this->importFileResponse(['import' => 44])->assertForbidden();
}
#[Test]
public function userWithImportAssetsPermissionCanImportAssets(): void
{
$this->actingAsForApi(User::factory()->canImport()->create());
$import = Import::factory()->asset()->create();
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function importAsset(): void
{
Notification::fake();
$importFileBuilder = ImportFileBuilder::new();
$row = $importFileBuilder->firstRow();
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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('checkout', $activityLogs[0]->action_type);
$this->assertEquals(Asset::class, $activityLogs[0]->item_type);
$this->assertEquals($assignee->id, $activityLogs[0]->target_id);
$this->assertEquals(User::class, $activityLogs[0]->target_type);
$this->assertEquals('Checkout from CSV Importer', $activityLogs[0]->note);
$this->assertEquals('create', $activityLogs[1]->action_type);
$this->assertNull($activityLogs[1]->target_id);
$this->assertEquals(Asset::class, $activityLogs[1]->item_type);
$this->assertNull($activityLogs[1]->note);
$this->assertNull($activityLogs[1]->target_type);
$this->assertEquals($row['assigneeFullName'], "{$assignee->first_name} {$assignee->last_name}");
$this->assertEquals($row['assigneeEmail'], $assignee->email);
$this->assertEquals($row['assigneeUsername'], $assignee->username);
$this->assertEquals($row['category'], $newAsset->model->category->name);
$this->assertEquals($row['manufacturerName'], $newAsset->model->manufacturer->name);
$this->assertEquals($row['itemName'], $newAsset->name);
$this->assertEquals($row['tag'], $newAsset->asset_tag);
$this->assertEquals($row['model'], $newAsset->model->name);
$this->assertEquals($row['modelNumber'], $newAsset->model->model_number);
$this->assertEquals($row['purchaseDate'], $newAsset->purchase_date->toDateString());
$this->assertNull($newAsset->asset_eol_date);
$this->assertEquals(0, $newAsset->eol_explicit);
$this->assertEquals($newAsset->location_id, $newAsset->rtd_location_id);
$this->assertEquals($row['purchaseCost'], $newAsset->purchase_cost);
$this->assertNull($newAsset->order_number);
$this->assertEquals('', $newAsset->image);
$this->assertNull($newAsset->user_id);
$this->assertEquals(1, $newAsset->physical);
$this->assertEquals($row['status'], $newAsset->assetStatus->name);
$this->assertEquals(0, $newAsset->archived);
$this->assertEquals($row['warrantyInMonths'], $newAsset->warranty_months);
$this->assertNull($newAsset->deprecate);
$this->assertEquals($row['supplierName'], $newAsset->supplier->name);
$this->assertEquals(0, $newAsset->requestable);
$this->assertEquals($row['location'], $newAsset->defaultLoc->name);
$this->assertEquals(null, $newAsset->accepted);
$this->assertEquals(now()->toDateString(), Carbon::parse($newAsset->last_checkout)->toDateString());
$this->assertEquals(0, $newAsset->last_checkin);
$this->assertEquals(0, $newAsset->expected_checkin);
$this->assertEquals($row['companyName'], $newAsset->company->name);
$this->assertEquals(User::class, $newAsset->assigned_type);
$this->assertNull($newAsset->last_audit_date);
$this->assertNull($newAsset->next_audit_date);
$this->assertEquals($row['location'], $newAsset->location->name);
$this->assertEquals(0, $newAsset->checkin_counter);
$this->assertEquals(1, $newAsset->checkout_counter);
$this->assertEquals(0, $newAsset->requests_counter);
$this->assertEquals(0, $newAsset->byod);
//Notes is never read.
// $this->assertEquals($row['notes'], $newAsset->notes);
Notification::assertSentTo($assignee, CheckoutAssetNotification::class);
}
#[Test]
public function willIgnoreUnknownColumnsWhenFileContainsUnknownColumns(): void
{
$row = ImportFileBuilder::new()->definition();
$row['unknownColumnInCsvFile'] = 'foo';
$importFileBuilder = new ImportFileBuilder([$row]);
$this->actingAsForApi(User::factory()->superuser()->create());
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function willNotCreateNewAssetWhenAssetWithSameTagAlreadyExists(): void
{
$asset = Asset::factory()->create(['asset_tag' => $this->faker->uuid]);
$importFileBuilder = ImportFileBuilder::times(4)->replace(['tag' => $asset->asset_tag]);
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newAsset = Asset::query()
->with(['assetStatus'])
->where('serial', $importFileBuilder->firstRow()['serialNumber'])
->sole();
$this->assertEquals('Ready to Deploy', $newAsset->assetStatus->name);
$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 = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newAsset = Asset::query()
->where('serial', $importFileBuilder->firstRow()['serialNumber'])
->sole();
$this->assertEquals(3, $newAsset->warranty_months);
$this->assertEquals('2022-10-10', $newAsset->purchase_date->toDateString());
}
#[Test]
public function whenRequiredColumnsAreMissingInImportFile(): void
{
$importFileBuilder = ImportFileBuilder::times(2)
->forget(['tag'])
->replace(['model' => '']);
$rows = $importFileBuilder->all();
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Asset::factory()->create()->refresh();
$importFileBuilder = ImportFileBuilder::times(1)->replace(['tag' => $asset->asset_tag]);
$row = $importFileBuilder->firstRow();
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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($row['assigneeFullName'], "{$assignee->first_name} {$assignee->last_name}");
$this->assertEquals($row['assigneeEmail'], $assignee->email);
$this->assertEquals($row['assigneeUsername'], $assignee->username);
$this->assertEquals($row['category'], $updatedAsset->model->category->name);
$this->assertEquals($row['manufacturerName'], $updatedAsset->model->manufacturer->name);
$this->assertEquals($row['itemName'], $updatedAsset->name);
$this->assertEquals($row['tag'], $updatedAsset->asset_tag);
$this->assertEquals($row['model'], $updatedAsset->model->name);
$this->assertEquals($row['modelNumber'], $updatedAsset->model->model_number);
$this->assertEquals($row['purchaseDate'], $updatedAsset->purchase_date->toDateString());
$this->assertEquals($row['purchaseCost'], $updatedAsset->purchase_cost);
$this->assertEquals($row['status'], $updatedAsset->assetStatus->name);
$this->assertEquals($row['warrantyInMonths'], $updatedAsset->warranty_months);
$this->assertEquals($row['supplierName'], $updatedAsset->supplier->name);
$this->assertEquals($row['location'], $updatedAsset->defaultLoc->name);
$this->assertEquals($row['companyName'], $updatedAsset->company->name);
$this->assertEquals($row['location'], $updatedAsset->location->name);
$this->assertEquals(1, $updatedAsset->checkout_counter);
$this->assertEquals(user::class, $updatedAsset->assigned_type);
//RequestAble is always updated regardless of initial value.
// $this->assertEquals($asset->requestable, $updatedAsset->requestable);
$this->assertEquals(
Arr::except($asset->attributesToArray(), $updatedAttributes),
Arr::except($updatedAsset->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 = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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($row['warrantyInMonths'], "{$assignee->first_name} {$assignee->last_name}");
$this->assertEquals($row['tag'], $assignee->email);
$this->assertEquals($row['location'], $assignee->username);
$this->assertEquals($row['modelNumber'], $asset->model->category->name);
$this->assertEquals($row['assigneeEmail'], $asset->model->manufacturer->name);
$this->assertEquals($row['model'], $asset->name);
$this->assertEquals($row['serialNumber'], $asset->asset_tag);
$this->assertEquals($row['purchaseCost'], $asset->model->name);
$this->assertEquals($row['itemName'], $asset->model->model_number);
$this->assertEquals($row['supplierName'], $asset->purchase_date->toDateString());
$this->assertEquals($row['companyName'], $asset->purchase_cost);
$this->assertEquals($row['manufacturerName'], $asset->assetStatus->name);
$this->assertEquals($row['status'], $asset->warranty_months);
$this->assertEquals($row['assigneeFullName'], $asset->supplier->name);
$this->assertEquals($row['category'], $asset->defaultLoc->name);
$this->assertEquals($row['purchaseDate'], $asset->company->name);
$this->assertEquals($row['category'], $asset->location->name);
$this->assertEquals($row['notes'], $asset->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(1, $asset->physical);
$this->assertEquals(0, $asset->archived);
$this->assertNull($asset->deprecate);
$this->assertEquals(0, $asset->requestable);
$this->assertEquals(null, $asset->accepted);
$this->assertEquals(now()->toDateString(), Carbon::parse($asset->last_checkout)->toDateString());
$this->assertEquals(0, $asset->last_checkin);
$this->assertEquals(0, $asset->expected_checkin);
$this->assertEquals(User::class, $asset->assigned_type);
$this->assertNull($asset->last_audit_date);
$this->assertNull($asset->next_audit_date);
$this->assertEquals(0, $asset->checkin_counter);
$this->assertEquals(1, $asset->checkout_counter);
$this->assertEquals(0, $asset->requests_counter);
$this->assertEquals(0, $asset->byod);
}
#[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 = CustomField::factory()->macAddress()->create(['db_column' => '_snipeit_mac_address_1']);
}
if ($customField->field_encrypted) {
$customField->field_encrypted = 0;
$customField->save();
}
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newAsset = Asset::query()->where('serial', $importFileBuilder->firstRow()['serialNumber'])->sole();
$this->assertEquals($macAddress, $newAsset->getAttribute($customField->db_column));
}
#[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 = CustomField::factory()->macAddress()->create();
}
if (!$customField->field_encrypted) {
$customField->field_encrypted = 1;
$customField->save();
}
$import = Import::factory()->asset()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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);
}
}

View file

@ -0,0 +1,305 @@
<?php
namespace Tests\Feature\Importing\Api;
use App\Models\Actionlog as ActionLog;
use App\Models\Component;
use App\Models\Import;
use App\Models\User;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
use PHPUnit\Framework\Attributes\Test;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\Support\Importing\CleansUpImportFiles;
use Tests\Support\Importing\ComponentsImportFileBuilder as ImportFileBuilder;
class ImportComponentsTest extends ImportDataTestCase implements TestsPermissionsRequirement
{
use CleansUpImportFiles;
use WithFaker;
protected function importFileResponse(array $parameters = []): TestResponse
{
if (!array_key_exists('import-type', $parameters)) {
$parameters['import-type'] = 'component';
}
return parent::importFileResponse($parameters);
}
#[Test]
public function testRequiresPermission()
{
$this->actingAsForApi(User::factory()->create());
$this->importFileResponse(['import' => 44])->assertForbidden();
}
#[Test]
public function userWithImportAssetsPermissionCanImportComponents(): void
{
$this->actingAsForApi(User::factory()->canImport()->create());
$import = Import::factory()->component()->create();
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function importComponents(): void
{
Notification::fake();
$importFileBuilder = ImportFileBuilder::new();
$row = $importFileBuilder->firstRow();
$import = Import::factory()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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('create', $activityLog->action_type);
$this->assertEquals('importer', $activityLog->action_source);
$this->assertEquals($newComponent->company->id, $activityLog->company_id);
$this->assertEquals($row['itemName'], $newComponent->name);
$this->assertEquals($row['companyName'], $newComponent->company->name);
$this->assertEquals($row['category'], $newComponent->category->name);
$this->assertEquals($row['location'], $newComponent->location->name);
$this->assertNull($newComponent->supplier_id);
$this->assertEquals($row['quantity'], $newComponent->qty);
$this->assertEquals($row['orderNumber'], $newComponent->order_number);
$this->assertEquals($row['purchaseDate'], $newComponent->purchase_date->toDateString());
$this->assertEquals($row['purchaseCost'], $newComponent->purchase_cost);
$this->assertNull($newComponent->min_amt);
$this->assertEquals($row['serialNumber'], $newComponent->serial);
$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(User::factory()->superuser()->create());
$import = Import::factory()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function willNotCreateNewComponentWhenComponentWithNameAndSerialNumberExists(): void
{
$component = Component::factory()->create();
$importFileBuilder = ImportFileBuilder::times(4)->replace([
'itemName' => $component->name,
'serialNumber' => $component->serial
]);
$import = Import::factory()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Component::factory()->create();
$importFileBuilder = ImportFileBuilder::new([
'itemName' => $component->name,
'serialNumber' => $component->serial
]);
$row = $importFileBuilder->firstRow();
$import = Import::factory()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk();
$updatedComponent = Component::query()
->with(['location', 'category'])
->where('serial', $row['serialNumber'])
->sole();
$this->assertEquals($row['itemName'], $updatedComponent->name);
$this->assertEquals($row['category'], $updatedComponent->category->name);
$this->assertEquals($row['location'], $updatedComponent->location->name);
$this->assertEquals($component->supplier_id, $updatedComponent->supplier_id);
$this->assertEquals($row['quantity'], $updatedComponent->qty);
$this->assertEquals($row['orderNumber'], $updatedComponent->order_number);
$this->assertEquals($row['purchaseDate'], $updatedComponent->purchase_date->toDateString());
$this->assertEquals($row['purchaseCost'], $updatedComponent->purchase_cost);
$this->assertEquals($component->min_amt, $updatedComponent->min_amt);
$this->assertEquals($row['serialNumber'], $updatedComponent->serial);
$this->assertEquals($component->image, $updatedComponent->image);
$this->assertEquals($component->notes, $updatedComponent->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 = Import::factory()->component()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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($row['quantity'], $newComponent->name);
$this->assertEquals($row['purchaseCost'], $newComponent->category->name);
$this->assertEquals($row['serialNumber'], $newComponent->location->name);
$this->assertNull($newComponent->supplier_id);
$this->assertEquals($row['companyName'], $newComponent->qty);
$this->assertEquals($row['orderNumber'], $newComponent->order_number);
$this->assertEquals($row['itemName'], $newComponent->purchase_date->toDateString());
$this->assertEquals($row['location'], $newComponent->purchase_cost);
$this->assertNull($newComponent->min_amt);
$this->assertNull($newComponent->image);
$this->assertNull($newComponent->notes);
}
}

View file

@ -0,0 +1,305 @@
<?php
namespace Tests\Feature\Importing\Api;
use App\Models\Actionlog as ActivityLog;
use App\Models\Consumable;
use App\Models\Import;
use App\Models\User;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
use PHPUnit\Framework\Attributes\Test;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\Support\Importing\CleansUpImportFiles;
use Tests\Support\Importing\ConsumablesImportFileBuilder as ImportFileBuilder;
class ImportConsumablesTest extends ImportDataTestCase implements TestsPermissionsRequirement
{
use CleansUpImportFiles;
use WithFaker;
protected function importFileResponse(array $parameters = []): TestResponse
{
if (!array_key_exists('import-type', $parameters)) {
$parameters['import-type'] = 'consumable';
}
return parent::importFileResponse($parameters);
}
#[Test]
public function testRequiresPermission()
{
$this->actingAsForApi(User::factory()->create());
$this->importFileResponse(['import' => 44])->assertForbidden();
}
#[Test]
public function userWithImportAssetsPermissionCanImportConsumables(): void
{
$this->actingAsForApi(User::factory()->canImport()->create());
$import = Import::factory()->consumable()->create();
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function importConsumables(): void
{
Notification::fake();
$importFileBuilder = ImportFileBuilder::new();
$row = $importFileBuilder->firstRow();
$import = Import::factory()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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('create', $activityLog->action_type);
$this->assertEquals('importer', $activityLog->action_source);
$this->assertEquals($newConsumable->company->id, $activityLog->company_id);
$this->assertEquals($row['itemName'], $newConsumable->name);
$this->assertEquals($row['category'], $newConsumable->category->name);
$this->assertEquals($row['location'], $newConsumable->location->name);
$this->assertEquals($row['companyName'], $newConsumable->company->name);
$this->assertNull($newConsumable->supplier_id);
$this->assertFalse($newConsumable->requestable);
$this->assertNull($newConsumable->image);
$this->assertEquals($row['orderNumber'], $newConsumable->order_number);
$this->assertEquals($row['purchaseDate'], $newConsumable->purchase_date->toDateString());
$this->assertEquals($row['purchaseCost'], $newConsumable->purchase_cost);
$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(User::factory()->superuser()->create());
$import = Import::factory()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function willNotCreateNewConsumableWhenConsumableNameAlreadyExist(): void
{
$consumable = Consumable::factory()->create(['name' => Str::random()]);
$importFileBuilder = ImportFileBuilder::new(['itemName' => $consumable->name]);
$import = Import::factory()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Consumable::factory()->create(['name' => Str::random()]);
$importFileBuilder = ImportFileBuilder::new(['itemName' => $consumable->name]);
$row = $importFileBuilder->firstRow();
$import = Import::factory()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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($row['itemName'], $updatedConsumable->name);
$this->assertEquals($row['category'], $updatedConsumable->category->name);
$this->assertEquals($row['location'], $updatedConsumable->location->name);
$this->assertEquals($row['companyName'], $updatedConsumable->company->name);
$this->assertEquals($row['orderNumber'], $updatedConsumable->order_number);
$this->assertEquals($row['purchaseDate'], $updatedConsumable->purchase_date->toDateString());
$this->assertEquals($row['purchaseCost'], $updatedConsumable->purchase_cost);
$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 = Import::factory()->consumable()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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($row['supplier'], $newConsumable->category->name);
$this->assertEquals($row['purchaseCost'], $newConsumable->location->name);
$this->assertEquals($row['purchaseDate'], $newConsumable->company->name);
$this->assertEquals($row['companyName'], $newConsumable->qty);
$this->assertEquals($row['quantity'], $newConsumable->name);
$this->assertNull($newConsumable->supplier_id);
$this->assertFalse($newConsumable->requestable);
$this->assertNull($newConsumable->image);
$this->assertEquals($row['orderNumber'], $newConsumable->order_number);
$this->assertEquals($row['itemName'], $newConsumable->purchase_date->toDateString());
$this->assertEquals($row['location'], $newConsumable->purchase_cost);
$this->assertNull($newConsumable->min_amt);
$this->assertEquals('', $newConsumable->model_number);
$this->assertNull($newConsumable->item_number);
$this->assertNull($newConsumable->manufacturer_id);
$this->assertNull($newConsumable->notes);
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace Tests\Feature\Importing\Api;
use Illuminate\Testing\TestResponse;
use Tests\TestCase;
abstract class ImportDataTestCase extends TestCase
{
protected function importFileResponse(array $parameters = []): TestResponse
{
return $this->postJson(route('api.imports.importFile', $parameters), $parameters);
}
}

View file

@ -0,0 +1,356 @@
<?php
namespace Tests\Feature\Importing\Api;
use App\Models\Actionlog as ActivityLog;
use App\Models\Import;
use App\Models\License;
use App\Models\User;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
use PHPUnit\Framework\Attributes\Test;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\Support\Importing\CleansUpImportFiles;
use Tests\Support\Importing\LicensesImportFileBuilder as ImportFileBuilder;
class ImportLicenseTest extends ImportDataTestCase implements TestsPermissionsRequirement
{
use CleansUpImportFiles;
use WithFaker;
protected function importFileResponse(array $parameters = []): TestResponse
{
if (!array_key_exists('import-type', $parameters)) {
$parameters['import-type'] = 'license';
}
return parent::importFileResponse($parameters);
}
#[Test]
public function testRequiresPermission()
{
$this->actingAsForApi(User::factory()->create());
$this->importFileResponse(['import' => 44])->assertForbidden();
}
#[Test]
public function userWithImportAssetsPermissionCanImportLicenses(): void
{
$this->actingAsForApi(User::factory()->canImport()->create());
$import = Import::factory()->license()->create();
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function importLicenses(): void
{
$importFileBuilder = ImportFileBuilder::new();
$row = $importFileBuilder->firstRow();
$import = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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($row['licenseName'], $newLicense->name);
$this->assertEquals($row['serialNumber'], $newLicense->serial);
$this->assertEquals($row['purchaseDate'], $newLicense->purchase_date->toDateString());
$this->assertEquals($row['purchaseCost'], $newLicense->purchase_cost);
$this->assertEquals($row['orderNumber'], $newLicense->order_number);
$this->assertEquals($row['seats'], $newLicense->seats);
$this->assertEquals($row['notes'], $newLicense->notes);
$this->assertEquals($row['licensedToName'], $newLicense->license_name);
$this->assertEquals($row['licensedToEmail'], $newLicense->license_email);
$this->assertEquals($row['supplierName'], $newLicense->supplier->name);
$this->assertEquals($row['companyName'], $newLicense->company->name);
$this->assertEquals($row['category'], $newLicense->category->name);
$this->assertEquals($row['expirationDate'], $newLicense->expiration_date->toDateString());
$this->assertEquals($row['isMaintained'] === 'TRUE', $newLicense->maintained);
$this->assertEquals($row['isReassignAble'] === 'TRUE', $newLicense->reassignable);
$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(User::factory()->superuser()->create());
$import = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function willNotCreateNewLicenseWhenNameAndSerialNumberAlreadyExist(): void
{
$license = License::factory()->create();
$importFileBuilder = ImportFileBuilder::times(4)->replace([
'itemName' => $license->name,
'serialNumber' => $license->serial
]);
$import = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id])->assertOk();
$newLicense = License::query()
->where('serial', $importFileBuilder->firstRow()['serialNumber'])
->sole();
$this->assertEquals('2022-10-10', $newLicense->expiration_date->toDateString());
}
#[Test]
public function willNotCreateNewCompanyWhenCompanyExists(): void
{
$importFileBuilder = ImportFileBuilder::times(4)->replace(['companyName' => Str::random()]);
$import = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = License::factory()->create();
$importFileBuilder = ImportFileBuilder::new([
'licenseName' => $license->name,
'serialNumber' => $license->serial
]);
$row = $importFileBuilder->firstRow();
$import = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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($row['licenseName'], $updatedLicense->name);
$this->assertEquals($row['serialNumber'], $updatedLicense->serial);
$this->assertEquals($row['purchaseDate'], $updatedLicense->purchase_date->toDateString());
$this->assertEquals($row['purchaseCost'], $updatedLicense->purchase_cost);
$this->assertEquals($row['orderNumber'], $updatedLicense->order_number);
$this->assertEquals($row['seats'], $updatedLicense->seats);
$this->assertEquals($row['notes'], $updatedLicense->notes);
$this->assertEquals($row['licensedToName'], $updatedLicense->license_name);
$this->assertEquals($row['licensedToEmail'], $updatedLicense->license_email);
$this->assertEquals($row['supplierName'], $updatedLicense->supplier->name);
$this->assertEquals($row['companyName'], $updatedLicense->company->name);
$this->assertEquals($row['category'], $updatedLicense->category->name);
$this->assertEquals($row['expirationDate'], $updatedLicense->expiration_date->toDateString());
$this->assertEquals($row['isMaintained'] === 'TRUE', $updatedLicense->maintained);
$this->assertEquals($row['isReassignAble'] === 'TRUE', $updatedLicense->reassignable);
$this->assertEquals($license->purchase_order, $updatedLicense->purchase_order);
$this->assertEquals($license->depreciation_id, $updatedLicense->depreciation_id);
$this->assertEquals($license->termination_date, $updatedLicense->termination_date);
$this->assertEquals($license->deprecate, $updatedLicense->deprecate);
$this->assertEquals($license->min_amt, $updatedLicense->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 = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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($row['licenseName'], $newLicense->name);
$this->assertEquals($row['companyName'], $newLicense->serial);
$this->assertEquals($row['isMaintained'], $newLicense->purchase_date->toDateString());
$this->assertEquals($row['isReassignAble'], $newLicense->purchase_cost);
$this->assertEquals($row['licensedToName'], $newLicense->order_number);
$this->assertEquals($row['expirationDate'], $newLicense->seats);
$this->assertEquals($row['licensedToEmail'], $newLicense->notes);
$this->assertEquals($row['seats'], $newLicense->license_name);
$this->assertEquals($row['serialNumber'], $newLicense->license_email);
$this->assertEquals($row['category'], $newLicense->supplier->name);
$this->assertEquals($row['notes'], $newLicense->company->name);
$this->assertEquals($row['manufacturerName'], $newLicense->category->name);
$this->assertEquals($row['orderNumber'], $newLicense->expiration_date->toDateString());
$this->assertEquals($row['purchaseCost'] === 'TRUE', $newLicense->maintained);
$this->assertEquals($row['purchaseDate'] === 'TRUE', $newLicense->reassignable);
$this->assertEquals('', $newLicense->purchase_order);
$this->assertNull($newLicense->depreciation_id);
$this->assertNull($newLicense->termination_date);
$this->assertNull($newLicense->deprecate);
$this->assertNull($newLicense->min_amt);
}
}

View file

@ -0,0 +1,336 @@
<?php
namespace Tests\Feature\Importing\Api;
use App\Models\Asset;
use App\Models\Import;
use App\Models\Location;
use App\Models\User;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Str;
use Illuminate\Testing\TestResponse;
use PHPUnit\Framework\Attributes\Test;
use Tests\Concerns\TestsPermissionsRequirement;
use Tests\Support\Importing\CleansUpImportFiles;
use Tests\Support\Importing\UsersImportFileBuilder as ImportFileBuilder;
class ImportUsersTest extends ImportDataTestCase implements TestsPermissionsRequirement
{
use CleansUpImportFiles;
use WithFaker;
protected function importFileResponse(array $parameters = []): TestResponse
{
if (!array_key_exists('import-type', $parameters)) {
$parameters['import-type'] = 'user';
}
return parent::importFileResponse($parameters);
}
#[Test]
public function testRequiresPermission()
{
$this->actingAsForApi(User::factory()->create());
$this->importFileResponse(['import' => 44])->assertForbidden();
}
#[Test]
public function userWithImportAssetsPermissionCanImportUsers(): void
{
$this->actingAsForApi(User::factory()->canImport()->create());
$import = Import::factory()->users()->create();
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function importUsers(): void
{
Notification::fake();
$importFileBuilder = ImportFileBuilder::new();
$row = $importFileBuilder->firstRow();
$import = Import::factory()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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($row['email'], $newUser->email);
$this->assertEquals($row['firstName'], $newUser->first_name);
$this->assertEquals($row['lastName'], $newUser->last_name);
$this->assertEquals($row['employeeNumber'], $newUser->employee_num);
$this->assertEquals($row['companyName'], $newUser->company->name);
$this->assertEquals($row['location'], $newUser->location->name);
$this->assertEquals($row['phoneNumber'], $newUser->phone);
$this->assertEquals($row['position'], $newUser->jobtitle);
$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('en-US', $newUser->locale);
$this->assertEquals(1, $newUser->show_in_list);
$this->assertEquals(0, $newUser->two_factor_enrolled);
$this->assertEquals(0, $newUser->two_factor_optin);
$this->assertEquals(0, $newUser->remote);
$this->assertEquals(0, $newUser->autoassign_licenses);
$this->assertEquals(0, $newUser->vip);
$this->assertEquals(0, $newUser->enable_sounds);
$this->assertEquals(0, $newUser->enable_confetti);
$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(0, $newUser->activated);
}
#[Test]
public function willIgnoreUnknownColumnsWhenFileContainsUnknownColumns(): void
{
$row = ImportFileBuilder::new()->definition();
$row['unknownColumnInCsvFile'] = 'foo';
$importFileBuilder = new ImportFileBuilder([$row]);
$this->actingAsForApi(User::factory()->superuser()->create());
$import = Import::factory()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->importFileResponse(['import' => $import->id])->assertOk();
}
#[Test]
public function willNotCreateNewUserWhenUserWithUserNameAlreadyExist(): void
{
$user = User::factory()->create(['username' => Str::random()]);
$importFileBuilder = ImportFileBuilder::times(4)->replace(['username' => $user->username]);
$import = Import::factory()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = Import::factory()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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($generatedUsername, $newUser->username);
}
#[Test]
public function willUpdateLocationOfAllAssetsAssignedToUser(): void
{
$user = User::factory()->create(['username' => Str::random()]);
$assetsAssignedToUser = Asset::factory()->create(['assigned_to' => $user->id, 'assigned_type' => User::class]);
$importFileBuilder = ImportFileBuilder::new(['username' => $user->username]);
$import = Import::factory()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk();
$userLocation = Location::query()->where('name', $importFileBuilder->firstRow()['location'])->sole(['id']);
$this->assertEquals(
$userLocation->id,
$assetsAssignedToUser->refresh()->location_id
);
}
#[Test]
public function whenRequiredColumnsAreMissingInImportFile(): void
{
$importFileBuilder = ImportFileBuilder::new(['firstName' => ''])->forget(['username']);
$import = Import::factory()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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 = User::factory()->create(['username' => Str::random()])->refresh();
$importFileBuilder = ImportFileBuilder::new(['username' => $user->username]);
$row = $importFileBuilder->firstRow();
$import = Import::factory()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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($row['email'], $updatedUser->email);
$this->assertEquals($row['firstName'], $updatedUser->first_name);
$this->assertEquals($row['lastName'], $updatedUser->last_name);
$this->assertEquals($row['employeeNumber'], $updatedUser->employee_num);
$this->assertEquals($row['companyName'], $updatedUser->company->name);
$this->assertEquals($row['location'], $updatedUser->location->name);
$this->assertEquals($row['phoneNumber'], $updatedUser->phone);
$this->assertEquals($row['position'], $updatedUser->jobtitle);
$this->assertTrue(Hash::isHashed($updatedUser->password));
$this->assertEquals(
Arr::except($user->attributesToArray(), $updatedAttributes),
Arr::except($updatedUser->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 = Import::factory()->users()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
$this->actingAsForApi(User::factory()->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($row['position'], $newUser->email);
$this->assertEquals($row['location'], $newUser->first_name);
$this->assertEquals($row['lastName'], $newUser->last_name);
$this->assertEquals($row['email'], $newUser->jobtitle);
$this->assertEquals($row['phoneNumber'], $newUser->employee_num);
$this->assertEquals($row['username'], $newUser->company->name);
$this->assertEquals($row['firstName'], $newUser->location->name);
$this->assertEquals($row['employeeNumber'], $newUser->phone);
$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('en-US', $newUser->locale);
$this->assertEquals(1, $newUser->show_in_list);
$this->assertEquals(0, $newUser->two_factor_enrolled);
$this->assertEquals(0, $newUser->two_factor_optin);
$this->assertEquals(0, $newUser->remote);
$this->assertEquals(0, $newUser->autoassign_licenses);
$this->assertEquals(0, $newUser->vip);
$this->assertEquals(0, $newUser->enable_sounds);
$this->assertEquals(0, $newUser->enable_confetti);
$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(0, $newUser->activated);
}
}

View file

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Tests\Support\Importing;
use Illuminate\Support\Str;
/**
* Build an accessories import file at runtime for testing.
*
* @template Row of array{
* category?: string,
* companyName?: string,
* itemName?: string,
* location?: string,
* manufacturerName?: string,
* modelNumber?: string,
* notes?: string,
* orderNumber?: string,
* purchaseCost?: int,
* purchaseDate?: string,
* quantity?: int,
* supplierName?: string
* }
*
* @extends FileBuilder<Row>
*/
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,
];
}
}

View file

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Tests\Support\Importing;
use Illuminate\Support\Str;
/**
* Build an assets import file at runtime for testing.
*
* @template Row of array{
* assigneeFullName?: string,
* assigneeEmail?: string,
* assigneeUsername?: string,
* category?: string,
* companyName?: string,
* itemName?: string,
* location?: string,
* manufacturerName?: int,
* model?: string,
* modelNumber?: string,
* notes?: string,
* purchaseCost?: int,
* purchaseDate?: string,
* serialNumber?: string,
* supplierName?: string,
* status?: string,
* tag?: string,
* warrantyInMonths?: int,
* }
*
* @extends FileBuilder<Row>
*/
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),
];
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace Tests\Support\Importing;
use App\Models\Import;
use Illuminate\Support\Facades\Storage;
trait CleansUpImportFiles
{
public function setUp(): void
{
parent::setUp();
Import::created(function (Import $import) {
$this->beforeApplicationDestroyed(function () use ($import) {
Storage::delete('private_uploads/imports/' . $import->file_path);
});
});
}
}

View file

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Tests\Support\Importing;
use Illuminate\Support\Str;
/**
* Build a components import file at runtime for testing.
*
* @template Row of array{
* category?: string,
* companyName?: string,
* itemName?: string,
* location?: string,
* orderNumber?: string,
* purchaseCost?: int,
* purchaseDate?: string,
* quantity?: int,
* serialNumber?: string,
* }
*
* @extends FileBuilder<Row>
*/
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(),
];
}
}

View file

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Tests\Support\Importing;
use Illuminate\Support\Str;
/**
* Build a consumables import file at runtime for testing.
*
* @template Row of array{
* category?: string,
* companyName?: string,
* itemName?: string,
* location?: string,
* orderNumber?: string,
* purchaseCost?: int,
* purchaseDate?: string,
* quantity?: int,
* supplier?: string,
* }
*
* @extends FileBuilder<Row>
*/
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}",
];
}
}

View file

@ -0,0 +1,249 @@
<?php
declare(strict_types=1);
namespace Tests\Support\Importing;
use Illuminate\Support\Str;
use Illuminate\Support\Collection;
use League\Csv\Reader;
use OutOfBoundsException;
/**
* @template Row of array
*/
abstract class FileBuilder
{
/**
* The import file rows.
*
* @var Collection<Row>
*/
protected Collection $rows;
/**
* Define the builders default row.
*
* @return Row
*/
abstract public function definition();
/**
* @param array<Row> $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<string,string>
*/
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<Row> $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<string> $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<Row>
*/
public function all(): array
{
return $this->rows->all();
}
}

View file

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Tests\Support\Importing;
use Illuminate\Support\Str;
/**
* Build a consumables import file at runtime for testing.
*
* @template Row of array{
* category?: string,
* companyName?: string,
* expirationDate?: string,
* isMaintained?: bool,
* isReassignAble?: bool,
* licensedToName?: string,
* licensedToEmail?: email,
* licenseName?: string,
* manufacturerName?: string,
* notes?: string,
* orderNumber?: string,
* purchaseCost?: int,
* purchaseDate?: string,
* seats?: int,
* serialNumber?: string,
* supplierName?: string
* }
*
* @extends FileBuilder<Row>
*/
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,
];
}
}

View file

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Tests\Support\Importing;
use Illuminate\Support\Str;
/**
* Build a users import file at runtime for testing.
*
* @template Row of array{
* companyName?: string,
* email?: string,
* employeeNumber?: int,
* firstName?: string,
* lastName?: string,
* location?: string,
* phoneNumber?: string,
* position?: string,
* username?: string,
* }
*
* @extends FileBuilder<Row>
*/
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(),
];
}
}