mirror of
https://github.com/snipe/snipe-it.git
synced 2026-08-18 11:15:42 +00:00
714 lines
31 KiB
PHP
714 lines
31 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Importing\Api;
|
|
|
|
use App\Models\Actionlog as ActivityLog;
|
|
use App\Models\Company;
|
|
use App\Models\Import;
|
|
use App\Models\License;
|
|
use App\Models\LicenseSeat;
|
|
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 test_requires_permission()
|
|
{
|
|
$this->actingAsForApi(User::factory()->create());
|
|
|
|
$this->importFileResponse(['import' => 44])->assertForbidden();
|
|
}
|
|
|
|
#[Test]
|
|
public function user_with_import_assets_permission_can_import_licenses(): void
|
|
{
|
|
$this->actingAsForApi(User::factory()->canImport()->create());
|
|
|
|
$import = Import::factory()->license()->create();
|
|
|
|
$this->importFileResponse(['import' => $import->id])->assertOk();
|
|
}
|
|
|
|
#[Test]
|
|
public function import_licenses(): 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' => ['tally' => ['created' => 1, 'updated' => 0, 'skipped' => 0, 'errored' => 0]],
|
|
'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 will_ignore_unknown_columns_when_file_contains_unknown_columns(): 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 will_not_create_new_license_when_name_and_serial_number_already_exist(): 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 format_attributes(): 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 will_not_create_new_company_when_company_exists(): 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 will_not_create_new_manufacturer_when_manufacturer_exists(): 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 will_not_create_new_category_when_category_exists(): 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 when_required_columns_are_missing_in_import_file(): 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' => ['tally' => ['created' => 0, 'updated' => 0, 'skipped' => 0, 'errored' => 1]],
|
|
'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 update_license_from_import(): 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 update_mode_clears_field_when_csv_column_is_present_but_empty(): void
|
|
{
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
|
|
$initialFile = ImportFileBuilder::new();
|
|
$initialRow = $initialFile->firstRow();
|
|
$initialImport = Import::factory()->license()->create([
|
|
'file_path' => $initialFile->saveToImportsDirectory(),
|
|
]);
|
|
$this->importFileResponse(['import' => $initialImport->id])->assertOk();
|
|
|
|
$license = License::query()
|
|
->where('name', $initialRow['licenseName'])
|
|
->where('serial', $initialRow['serialNumber'])
|
|
->sole();
|
|
|
|
// Sanity: initial import should have populated the fields we are
|
|
// about to clear. Guards against a false negative if the fixture
|
|
// shape changes and the fields default to null.
|
|
$this->assertNotNull($license->expiration_date);
|
|
$this->assertNotEmpty($license->license_email);
|
|
$this->assertNotEmpty($license->notes);
|
|
|
|
// Re-import with these columns present but empty. Empty CSV cells
|
|
// should clear the corresponding DB fields, not preserve them.
|
|
$clearedRow = array_merge($initialRow, [
|
|
'expirationDate' => '',
|
|
'licensedToEmail' => '',
|
|
'notes' => '',
|
|
]);
|
|
$clearFile = new ImportFileBuilder([$clearedRow]);
|
|
$clearImport = Import::factory()->license()->create([
|
|
'file_path' => $clearFile->saveToImportsDirectory(),
|
|
]);
|
|
$this->importFileResponse([
|
|
'import' => $clearImport->id,
|
|
'import-update' => true,
|
|
])->assertOk();
|
|
|
|
$license->refresh();
|
|
$this->assertNull($license->expiration_date);
|
|
$this->assertNull($license->license_email);
|
|
$this->assertNull($license->notes);
|
|
}
|
|
|
|
#[Test]
|
|
public function update_mode_preserves_fields_when_csv_column_is_absent(): void
|
|
{
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
|
|
$initialFile = ImportFileBuilder::new();
|
|
$initialRow = $initialFile->firstRow();
|
|
$initialImport = Import::factory()->license()->create([
|
|
'file_path' => $initialFile->saveToImportsDirectory(),
|
|
]);
|
|
$this->importFileResponse(['import' => $initialImport->id])->assertOk();
|
|
|
|
$license = License::query()
|
|
->where('name', $initialRow['licenseName'])
|
|
->where('serial', $initialRow['serialNumber'])
|
|
->sole();
|
|
|
|
$originalEmail = $license->license_email;
|
|
$originalNotes = $license->notes;
|
|
$originalExpirationDate = $license->expiration_date?->toDateString();
|
|
|
|
// Re-import with a CSV that only has the identity fields plus one
|
|
// updated field. All other columns are absent from the CSV, so
|
|
// their DB values must be preserved.
|
|
// A short static order number keeps the assertion within License's
|
|
// order_number varchar(50) limit regardless of what the initial row
|
|
// faker produced. Prefixing the faker value overflowed on some seeds.
|
|
$newOrderNumber = 'UPDATED-ORDER-123';
|
|
|
|
$partialRow = [
|
|
'licenseName' => $initialRow['licenseName'],
|
|
'serialNumber' => $initialRow['serialNumber'],
|
|
'orderNumber' => $newOrderNumber,
|
|
];
|
|
$partialFile = new ImportFileBuilder([$partialRow]);
|
|
$partialImport = Import::factory()->license()->create([
|
|
'file_path' => $partialFile->saveToImportsDirectory(),
|
|
]);
|
|
$this->importFileResponse([
|
|
'import' => $partialImport->id,
|
|
'import-update' => true,
|
|
])->assertOk();
|
|
|
|
$license->refresh();
|
|
$this->assertEquals($newOrderNumber, $license->order_number);
|
|
$this->assertEquals($originalEmail, $license->license_email);
|
|
$this->assertEquals($originalNotes, $license->notes);
|
|
$this->assertEquals($originalExpirationDate, $license->expiration_date?->toDateString());
|
|
}
|
|
|
|
#[Test]
|
|
public function update_mode_logs_license_update_in_actionlog(): void
|
|
{
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
|
|
$initialFile = ImportFileBuilder::new();
|
|
$initialRow = $initialFile->firstRow();
|
|
|
|
$initialImport = Import::factory()->license()->create([
|
|
'file_path' => $initialFile->saveToImportsDirectory(),
|
|
]);
|
|
|
|
$this->importFileResponse(['import' => $initialImport->id])->assertOk();
|
|
|
|
$license = License::query()
|
|
->where('name', $initialRow['licenseName'])
|
|
->where('serial', $initialRow['serialNumber'])
|
|
->sole();
|
|
|
|
$updatedRow = array_merge($initialRow, [
|
|
'orderNumber' => (string) $initialRow['orderNumber'].'-UPD',
|
|
]);
|
|
|
|
$updateFile = new ImportFileBuilder([$updatedRow]);
|
|
$updateImport = Import::factory()->license()->create([
|
|
'file_path' => $updateFile->saveToImportsDirectory(),
|
|
]);
|
|
|
|
$this->importFileResponse([
|
|
'import' => $updateImport->id,
|
|
'import-update' => true,
|
|
])->assertOk();
|
|
|
|
$license->refresh();
|
|
$this->assertEquals($updatedRow['orderNumber'], $license->order_number);
|
|
|
|
$updateLog = ActivityLog::query()
|
|
->where('item_type', License::class)
|
|
->where('item_id', $license->id)
|
|
->where('action_type', 'update')
|
|
->latest('id')
|
|
->first();
|
|
|
|
$this->assertNotNull($updateLog, 'Expected an update action log entry after license importer update mode.');
|
|
}
|
|
|
|
#[Test]
|
|
public function custom_column_mapping(): 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);
|
|
}
|
|
|
|
#[Test]
|
|
public function import_license_checkout_is_blocked_when_fmcs_companies_differ(): void
|
|
{
|
|
[$companyA, $companyB] = Company::factory()->count(2)->create();
|
|
$user = User::factory()->forCompany($companyB)->create();
|
|
$this->settings->enableMultipleFullCompanySupport();
|
|
|
|
$importFileBuilder = ImportFileBuilder::new([
|
|
'companyName' => $companyA->name,
|
|
'checkoutUsername' => $user->username,
|
|
'seats' => 5,
|
|
]);
|
|
|
|
$import = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
|
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
$this->importFileResponse(['import' => $import->id])->assertOk();
|
|
|
|
$license = License::where('serial', $importFileBuilder->firstRow()['serialNumber'])->sole();
|
|
$checkedOutSeat = LicenseSeat::where('license_id', $license->id)->whereNotNull('assigned_to')->first();
|
|
$this->assertNull($checkedOutSeat, 'License seat should not be checked out when item and user companies differ under FMCS');
|
|
}
|
|
|
|
#[Test]
|
|
public function import_license_checkout_is_allowed_when_fmcs_companies_match(): void
|
|
{
|
|
$company = Company::factory()->create();
|
|
$user = User::factory()->forCompany($company)->create();
|
|
$this->settings->enableMultipleFullCompanySupport();
|
|
|
|
$importFileBuilder = ImportFileBuilder::new([
|
|
'companyName' => $company->name,
|
|
'checkoutUsername' => $user->username,
|
|
'seats' => 5,
|
|
]);
|
|
|
|
$import = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
|
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
$this->importFileResponse(['import' => $import->id])->assertOk();
|
|
|
|
$license = License::where('serial', $importFileBuilder->firstRow()['serialNumber'])->sole();
|
|
$checkedOutSeat = LicenseSeat::where('license_id', $license->id)->where('assigned_to', $user->id)->first();
|
|
$this->assertNotNull($checkedOutSeat, 'License seat should be checked out when companies match under FMCS');
|
|
}
|
|
|
|
#[Test]
|
|
public function import_license_checkout_is_blocked_when_floater_disabled_and_user_has_no_company(): void
|
|
{
|
|
$company = Company::factory()->create();
|
|
$user = User::factory()->withoutCompany()->create();
|
|
$this->settings->enableMultipleFullCompanySupport()->disableFloaterMode();
|
|
|
|
$importFileBuilder = ImportFileBuilder::new([
|
|
'companyName' => $company->name,
|
|
'checkoutUsername' => $user->username,
|
|
'seats' => 5,
|
|
]);
|
|
|
|
$import = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
|
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
$this->importFileResponse(['import' => $import->id])->assertOk();
|
|
|
|
$license = License::where('serial', $importFileBuilder->firstRow()['serialNumber'])->sole();
|
|
$checkedOutSeat = LicenseSeat::where('license_id', $license->id)->whereNotNull('assigned_to')->first();
|
|
$this->assertNull($checkedOutSeat, 'License seat should not be checked out to a no-company user when floater mode is off');
|
|
}
|
|
|
|
#[Test]
|
|
public function import_license_checkout_is_allowed_when_floater_enabled_and_user_has_no_company(): void
|
|
{
|
|
$company = Company::factory()->create();
|
|
$user = User::factory()->withoutCompany()->create();
|
|
$this->settings->enableFloaterMode();
|
|
|
|
$importFileBuilder = ImportFileBuilder::new([
|
|
'companyName' => $company->name,
|
|
'checkoutUsername' => $user->username,
|
|
'seats' => 5,
|
|
]);
|
|
|
|
$import = Import::factory()->license()->create(['file_path' => $importFileBuilder->saveToImportsDirectory()]);
|
|
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
$this->importFileResponse(['import' => $import->id])->assertOk();
|
|
|
|
$license = License::where('serial', $importFileBuilder->firstRow()['serialNumber'])->sole();
|
|
$checkedOutSeat = LicenseSeat::where('license_id', $license->id)->where('assigned_to', $user->id)->first();
|
|
$this->assertNotNull($checkedOutSeat, 'License seat should be checked out to a no-company user when floater mode is on');
|
|
}
|
|
|
|
#[Test]
|
|
public function multiple_rows_without_serial_column_assign_seats_to_a_single_license_in_update_mode(): void
|
|
{
|
|
// Regression from the 8.7.0 importer rewrite. Users importing a
|
|
// seat-assignment CSV (username + license name, no serial column)
|
|
// were seeing every row create a new License in update mode instead
|
|
// of matching to an existing null-serial license. Reason:
|
|
// setItemFromCsvIfPresent skips absent columns so the insert path
|
|
// stored serial as NULL, but the match query looked for serial = ''
|
|
// which never matches NULL in SQL. Match now checks
|
|
// (serial IS NULL OR serial = '').
|
|
[$firstUser, $secondUser] = User::factory()->count(2)->create();
|
|
$existingLicense = License::factory()->create([
|
|
'serial' => null,
|
|
'seats' => 5,
|
|
]);
|
|
|
|
$rowShape = [
|
|
'licenseName' => $existingLicense->name,
|
|
'category' => 'Software',
|
|
'seats' => 5,
|
|
];
|
|
|
|
$file = new ImportFileBuilder([
|
|
array_merge($rowShape, ['checkoutUsername' => $firstUser->username]),
|
|
array_merge($rowShape, ['checkoutUsername' => $secondUser->username]),
|
|
]);
|
|
|
|
$import = Import::factory()->license()->create(['file_path' => $file->saveToImportsDirectory()]);
|
|
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
$this->importFileResponse([
|
|
'import' => $import->id,
|
|
'import-update' => true,
|
|
])->assertOk();
|
|
|
|
// Only the one existing License should exist; both rows should have
|
|
// matched into it. Without the fix, each row created a new license.
|
|
$licenses = License::where('name', $existingLicense->name)->get();
|
|
$this->assertCount(1, $licenses, 'Both rows should have matched the existing null-serial license, not created duplicates.');
|
|
|
|
// Two seats should be checked out, one per row.
|
|
$this->assertSame(2, LicenseSeat::where('license_id', $existingLicense->id)->whereNotNull('assigned_to')->count());
|
|
}
|
|
|
|
#[Test]
|
|
public function import_records_error_when_license_runs_out_of_free_seats(): void
|
|
{
|
|
// Regression for #19467 follow-up. If a seat-assignment CSV exceeds
|
|
// the target license's free-seat count, the trailing rows should
|
|
// surface as errored tally entries with a specific "no free seats"
|
|
// message, not silently be dropped on the floor.
|
|
[$firstUser, $secondUser, $thirdUser] = User::factory()->count(3)->create();
|
|
$existingLicense = License::factory()->create([
|
|
'serial' => null,
|
|
'seats' => 2,
|
|
]);
|
|
|
|
$rowShape = [
|
|
'licenseName' => $existingLicense->name,
|
|
'category' => 'Software',
|
|
'seats' => 2,
|
|
];
|
|
|
|
$file = new ImportFileBuilder([
|
|
array_merge($rowShape, ['checkoutUsername' => $firstUser->username]),
|
|
array_merge($rowShape, ['checkoutUsername' => $secondUser->username]),
|
|
array_merge($rowShape, ['checkoutUsername' => $thirdUser->username]),
|
|
]);
|
|
|
|
$import = Import::factory()->license()->create(['file_path' => $file->saveToImportsDirectory()]);
|
|
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
$response = $this->importFileResponse([
|
|
'import' => $import->id,
|
|
'import-update' => true,
|
|
]);
|
|
|
|
// The API convention for import runs with any errored rows is HTTP
|
|
// 500 (see Api\ImportController::importFile line 352). The interesting
|
|
// assertion is that the tally reflects the failure and the message
|
|
// surfaces the specific "no free seats" reason.
|
|
$response->assertStatus(500);
|
|
$response->assertJson(['status' => 'import-errors']);
|
|
$this->assertSame(1, $response->json('payload.tally.errored'), 'The row that had no free seat should be counted as errored in the tally.');
|
|
$this->assertStringContainsString('no free seats', $response->content());
|
|
|
|
// Two seats successfully assigned (rows 1 and 2), third row's seat
|
|
// assignment failed and was recorded.
|
|
$this->assertSame(2, LicenseSeat::where('license_id', $existingLicense->id)->whereNotNull('assigned_to')->count());
|
|
}
|
|
|
|
#[Test]
|
|
public function update_matches_existing_license_with_null_serial_when_csv_omits_serial(): void
|
|
{
|
|
// Same class of bug as multiple_rows_without_serial_column, on the
|
|
// update path: an existing License with serial NULL in the DB and a
|
|
// CSV row that omits the serial column altogether. The old match
|
|
// query used serial = '' which does not match NULL, so a duplicate
|
|
// was created. Now checks (serial IS NULL OR serial = '').
|
|
$license = License::factory()->create(['serial' => null]);
|
|
|
|
$file = new ImportFileBuilder([[
|
|
'licenseName' => $license->name,
|
|
'category' => 'Software',
|
|
'seats' => 3,
|
|
'notes' => 'Updated via seat-assign CSV',
|
|
]]);
|
|
|
|
$import = Import::factory()->license()->create(['file_path' => $file->saveToImportsDirectory()]);
|
|
|
|
$this->actingAsForApi(User::factory()->superuser()->create());
|
|
$this->importFileResponse([
|
|
'import' => $import->id,
|
|
'import-update' => true,
|
|
])->assertOk();
|
|
|
|
$this->assertSame(1, License::where('name', $license->name)->count(), 'Existing null-serial license should have been matched, not duplicated.');
|
|
$this->assertSame('Updated via seat-assign CSV', $license->fresh()->notes);
|
|
}
|
|
}
|