Updated tests and transformers

This commit is contained in:
snipe
2026-05-20 16:17:02 +01:00
parent f03b27ec88
commit 6ae09e15fb
17 changed files with 313 additions and 33 deletions
@@ -22,6 +22,7 @@ use App\Models\Asset;
use App\Models\Company;
use App\Models\Consumable;
use App\Models\License;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\CurrentInventory;
use App\Notifications\WelcomeNotification;
@@ -38,13 +38,11 @@ class LicenseSeatsTransformer
'tag_color' => $seat->user->department->tag_color ? e($seat->user->department->tag_color) : null,
] : null,
'company' => ($seat->user->companies->isNotEmpty()) ?
[
'id' => (int) $seat->user->companies->first()->id,
'name' => e($seat->user->companies->first()->name),
'tag_color' => $seat->user->companies->first()->tag_color ? e($seat->user->companies->first()->tag_color) : null,
] : null,
'companies' => $seat->user->companies->map(fn ($c) => [
'id' => (int) $c->id,
'name' => e($c->name),
'tag_color' => $c->tag_color ? e($c->tag_color) : null,
])->values(),
'created_at' => Helper::getFormattedDateObject($seat->created_at, 'datetime'),
] : null,
'assigned_asset' => ($seat->asset) ? [
@@ -150,6 +150,11 @@ class UsersTransformer
'last_name' => e($user->last_name),
'username' => e($user->username),
'display_name' => e($user->display_name),
'companies' => $user->companies->map(fn ($c) => [
'id' => (int) $c->id,
'name' => e($c->name),
'tag_color' => $c->tag_color ? e($c->tag_color) : null,
])->values(),
'created_by' => $user->adminuser ? [
'id' => (int) $user->adminuser->id,
'name' => e($user->adminuser->present()->fullName),
+2 -1
View File
@@ -146,7 +146,8 @@ class AccessoryCheckout extends Model
$search_str = '%'.$term.'%';
$query->where('first_name', 'like', $search_str)
->orWhere('last_name', 'like', $search_str)
->orWhere('note', 'like', $search_str);
->orWhere('note', 'like', $search_str)
->orWhereHas('companies', fn ($q) => $q->where('companies.name', 'like', $search_str));
}
}
)->select('id');
+42 -11
View File
@@ -16,6 +16,8 @@ class UserObserver
{
// ONLY allow these fields to be stored
// NOTE: company_id is intentionally excluded — company membership changes are logged
// via User::syncCompaniesWithLogging() against the pivot table instead.
$allowed_fields = [
'email',
'activated',
@@ -31,7 +33,6 @@ class UserObserver
'employee_num',
'username',
'notes',
'company_id',
'ldap_import',
'locale',
'two_factor_enrolled',
@@ -58,18 +59,44 @@ class UserObserver
// Make sure the info is in the allow fields array
if (in_array($key, $allowed_fields)) {
// Check and see if the value changed
if ($user->getRawOriginal()[$key] != $user->getAttributes()[$key]) {
$oldValue = $user->getRawOriginal()[$key];
$newValue = $user->getAttributes()[$key];
$changed[$key]['old'] = $user->getRawOriginal()[$key];
$changed[$key]['new'] = $user->getAttributes()[$key];
// Do not store the hashed password in changes
if ($key == 'password') {
$changed['password']['old'] = '*************';
$changed['password']['new'] = '*************';
if ($key === 'permissions') {
// Compare decoded to avoid spurious diffs from key reordering or type coercion.
$oldDecoded = json_decode($oldValue ?? '{}', true) ?: [];
$newDecoded = json_decode($newValue ?? '{}', true) ?: [];
if ($oldDecoded == $newDecoded) {
continue;
}
// Only log the permission keys that actually changed.
$diffOld = [];
$diffNew = [];
foreach (array_unique(array_merge(array_keys($oldDecoded), array_keys($newDecoded))) as $permKey) {
$oldPerm = $oldDecoded[$permKey] ?? null;
$newPerm = $newDecoded[$permKey] ?? null;
if ($oldPerm != $newPerm) {
$diffOld[$permKey] = $oldPerm;
$diffNew[$permKey] = $newPerm;
}
}
$changed['permissions']['old'] = json_encode($diffOld);
$changed['permissions']['new'] = json_encode($diffNew);
continue;
}
if ($oldValue == $newValue) {
continue;
}
$changed[$key]['old'] = $oldValue;
$changed[$key]['new'] = $newValue;
// Do not store the hashed password in changes
if ($key == 'password') {
$changed['password']['old'] = '*************';
$changed['password']['new'] = '*************';
}
}
@@ -79,12 +106,16 @@ class UserObserver
$logAction = new Actionlog;
$logAction->item_type = User::class;
$logAction->item_id = $user->id;
$logAction->target_type = User::class; // can we instead say $logAction->item = $asset ?
$logAction->target_type = User::class;
$logAction->target_id = $user->id;
$logAction->created_at = date('Y-m-d H:i:s');
$logAction->created_by = auth()->id();
$logAction->log_meta = json_encode($changed);
$logAction->logaction('update');
// Let syncCompaniesWithLogging() merge company changes into this entry
// rather than creating a separate log row for the same edit session.
$user->currentUpdateLogId = $logAction->id;
}
}
+9
View File
@@ -218,6 +218,15 @@ class AccessoryPresenter extends Presenter
'visible' => true,
'formatter' => 'polymorphicItemFormatter',
],
[
'field' => 'assigned_to.companies',
'searchable' => true,
'sortable' => false,
'switchable' => true,
'title' => trans('general.companies'),
'visible' => true,
'formatter' => 'companiesArrayLinkFormatter',
],
[
'field' => 'note',
'searchable' => false,
+3 -3
View File
@@ -268,13 +268,13 @@ class LicensePresenter extends Presenter
'formatter' => 'emailFormatter',
],
[
'field' => 'assigned_user.company',
'field' => 'assigned_user.companies',
'searchable' => false,
'sortable' => false,
'switchable' => true,
'title' => trans('general.company'),
'title' => trans('general.companies'),
'visible' => true,
'formatter' => 'companiesLinkObjFormatter',
'formatter' => 'companiesArrayLinkFormatter',
],
[
'field' => 'assigned_user.department',
+1 -1
View File
@@ -93,7 +93,7 @@
<tr>
<td>{{ $counter }}</td>
<td>{{ (($user) && ($user->company)) ? $user->company->name : '' }}</td>
<td>{{ ($user) ? $user->companies->pluck('name')->implode(', ') : '' }}</td>
<td>{{ ($user) ? $user->first_name .' '. $user->last_name : '' }}</td>
<td>{{ ($user) ? $user->employee_num : '' }}</td>
<td>{{ (($user) && ($user->department)) ? $user->department->name : '' }}</td>
+2 -2
View File
@@ -16,8 +16,8 @@
</div>
<!-- Setup of default company, taken from asset creator if scoped locations are activated in the settings -->
@if (($snipeSettings->scope_locations_fmcs == '1') && ($user->company))
<input type="hidden" name="company_id" id='modal-company' value='{{ $user->company->id }}' class="form-control">
@if (($snipeSettings->scope_locations_fmcs == '1') && ($user->companies->isNotEmpty()))
<input type="hidden" name="company_id" id='modal-company' value='{{ $user->companies->first()->id }}' class="form-control">
@endif
<!-- Select company, only for users with multicompany access - replace default company -->
@@ -81,4 +81,33 @@ class IndexAccessoryCheckoutsTest extends TestCase implements TestsFullMultipleC
->assertJsonPath('rows.0.assigned_to.id', $userB->id)
->assertJsonPath('rows.1.assigned_to.id', $userC->id);
}
public function test_checkout_search_by_company_name_returns_matching_users()
{
$company = Company::factory()->create(['name' => 'Jedi Order']);
$jedi = User::factory()->create();
$company->users()->attach($jedi);
$sith = User::factory()->create();
$accessory = Accessory::factory()->checkedOutToUsers([$jedi, $sith])->create();
$this->actingAsForApi(User::factory()->viewAccessories()->create())
->getJson(route('api.accessories.checkedout', ['accessory' => $accessory->id, 'search' => 'Jedi Order']))
->assertOk()
->assertJsonPath('total', 1)
->assertJsonPath('rows.0.assigned_to.id', $jedi->id);
}
public function test_checkout_search_by_company_name_does_not_return_users_in_other_companies()
{
Company::factory()->create(['name' => 'Jedi Order']);
$sith = User::factory()->create();
$accessory = Accessory::factory()->checkedOutToUsers([$sith])->create();
$this->actingAsForApi(User::factory()->viewAccessories()->create())
->getJson(route('api.accessories.checkedout', ['accessory' => $accessory->id, 'search' => 'Jedi Order']))
->assertOk()
->assertJsonPath('total', 0);
}
}
@@ -70,4 +70,23 @@ class AssetsForSelectListTest extends TestCase
->assertResponseDoesNotContainInResults($assetA)
->assertResponseContainsInResults($assetB);
}
public function test_assets_are_filtered_by_multiple_comma_separated_company_ids_when_full_company_support_is_enabled()
{
$this->settings->enableMultipleFullCompanySupport();
[$companyA, $companyB, $companyC] = Company::factory()->count(3)->create();
$assetA = Asset::factory()->for($companyA)->create(['asset_tag' => 'A001']);
$assetB = Asset::factory()->for($companyB)->create(['asset_tag' => 'B001']);
$assetC = Asset::factory()->for($companyC)->create(['asset_tag' => 'C001']);
$actor = User::factory()->superuser()->create();
$this->actingAsForApi($actor)
->getJson(route('assets.selectlist', ['companyId' => $companyA->id.','.$companyB->id]))
->assertResponseContainsInResults($assetA)
->assertResponseContainsInResults($assetB)
->assertResponseDoesNotContainInResults($assetC);
}
}
@@ -228,4 +228,51 @@ class AccessoryCheckoutTest extends TestCase implements TestsPermissionsRequirem
$this->assertEquals(1, $accessoryInCompanyA->fresh()->numRemaining());
}
public function test_user_in_same_company_can_checkout_accessory_when_full_company_support_is_enabled()
{
$this->settings->enableMultipleFullCompanySupport();
$company = Company::factory()->create();
$accessory = Accessory::factory()->for($company)->create(['qty' => 5]);
$target = $company->users()->save(User::factory()->make());
$actor = User::factory()->superuser()->create();
$this->actingAsForApi($actor)
->postJson(route('api.accessories.checkout', $accessory), [
'assigned_user' => $target->id,
'checkout_to_type' => 'user',
])
->assertOk()
->assertStatusMessageIs('success');
}
public function test_user_in_multiple_companies_can_checkout_accessory_from_any_of_their_companies_when_full_company_support_is_enabled()
{
$this->settings->enableMultipleFullCompanySupport();
[$companyA, $companyB] = Company::factory()->count(2)->create();
$target = User::factory()->create();
$target->companies()->sync([$companyA->id, $companyB->id]);
$accessoryInA = Accessory::factory()->for($companyA)->create(['qty' => 5]);
$accessoryInB = Accessory::factory()->for($companyB)->create(['qty' => 5]);
$actor = User::factory()->superuser()->create();
$this->actingAsForApi($actor)
->postJson(route('api.accessories.checkout', $accessoryInA), [
'assigned_user' => $target->id,
'checkout_to_type' => 'user',
])
->assertOk()
->assertStatusMessageIs('success');
$this->actingAsForApi($actor)
->postJson(route('api.accessories.checkout', $accessoryInB), [
'assigned_user' => $target->id,
'checkout_to_type' => 'user',
])
->assertOk()
->assertStatusMessageIs('success');
}
}
@@ -152,4 +152,48 @@ class ConsumableCheckoutTest extends TestCase
$this->assertEquals(1, $consumableInCompanyA->fresh()->numRemaining());
}
public function test_user_in_same_company_can_checkout_consumable_when_full_company_support_is_enabled()
{
$this->settings->enableMultipleFullCompanySupport();
$company = Company::factory()->create();
$consumable = Consumable::factory()->for($company)->create(['qty' => 5]);
$target = $company->users()->save(User::factory()->make());
$actor = User::factory()->superuser()->create();
$this->actingAsForApi($actor)
->postJson(route('api.consumables.checkout', $consumable), [
'assigned_to' => $target->id,
])
->assertOk()
->assertStatusMessageIs('success');
}
public function test_user_in_multiple_companies_can_checkout_consumable_from_any_of_their_companies_when_full_company_support_is_enabled()
{
$this->settings->enableMultipleFullCompanySupport();
[$companyA, $companyB] = Company::factory()->count(2)->create();
$target = User::factory()->create();
$target->companies()->sync([$companyA->id, $companyB->id]);
$consumableInA = Consumable::factory()->for($companyA)->create(['qty' => 5]);
$consumableInB = Consumable::factory()->for($companyB)->create(['qty' => 5]);
$actor = User::factory()->superuser()->create();
$this->actingAsForApi($actor)
->postJson(route('api.consumables.checkout', $consumableInA), [
'assigned_to' => $target->id,
])
->assertOk()
->assertStatusMessageIs('success');
$this->actingAsForApi($actor)
->postJson(route('api.consumables.checkout', $consumableInB), [
'assigned_to' => $target->id,
])
->assertOk()
->assertStatusMessageIs('success');
}
}
@@ -69,7 +69,7 @@ class ImportUsersTest extends ImportDataTestCase implements TestsPermissionsRequ
]);
$newUser = User::query()
->with(['company', 'location'])
->with(['companies', 'location'])
->where('username', $row['username'])
->sole();
@@ -80,7 +80,7 @@ class ImportUsersTest extends ImportDataTestCase implements TestsPermissionsRequ
$this->assertEquals($row['lastName'], $newUser->last_name);
$this->assertEquals($row['displayName'], $newUser->display_name);
$this->assertEquals($row['employeeNumber'], $newUser->employee_num);
$this->assertEquals($row['companyName'], $newUser->company->name);
$this->assertEquals($row['companyName'], $newUser->companies->first()->name);
$this->assertEquals($row['location'], $newUser->location->name);
$this->assertEquals($row['phoneNumber'], $newUser->phone);
$this->assertEquals($row['position'], $newUser->jobtitle);
@@ -229,16 +229,14 @@ class ImportUsersTest extends ImportDataTestCase implements TestsPermissionsRequ
$this->actingAsForApi(User::factory()->superuser()->create());
$this->importFileResponse(['import' => $import->id, 'import-update' => true])->assertOk();
$updatedUser = User::query()->with(['company', 'location'])->find($user->id);
$updatedUser = User::query()->with(['companies', 'location'])->find($user->id);
$updatedAttributes = [
'first_name',
'display_name',
'email',
'last_name',
'employee_num',
'company',
'location_id',
'company_id',
'updated_at',
'phone',
'jobtitle',
@@ -249,7 +247,7 @@ class ImportUsersTest extends ImportDataTestCase implements TestsPermissionsRequ
$this->assertEquals($row['displayName'], $updatedUser->display_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['companyName'], $updatedUser->companies->first()->name);
$this->assertEquals($row['location'], $updatedUser->location->name);
$this->assertEquals($row['phoneNumber'], $updatedUser->phone);
$this->assertEquals($row['position'], $updatedUser->jobtitle);
@@ -346,7 +344,7 @@ class ImportUsersTest extends ImportDataTestCase implements TestsPermissionsRequ
->json();
$newUser = User::query()
->with(['company', 'location'])
->with(['companies', 'location'])
->where('username', $row['companyName'])
->sole();
@@ -356,7 +354,7 @@ class ImportUsersTest extends ImportDataTestCase implements TestsPermissionsRequ
$this->assertEquals($row['dumbName'], $newUser->display_name);
$this->assertEquals($row['email'], $newUser->jobtitle);
$this->assertEquals($row['phoneNumber'], $newUser->employee_num);
$this->assertEquals($row['username'], $newUser->company->name);
$this->assertEquals($row['username'], $newUser->companies->first()->name);
$this->assertEquals($row['firstName'], $newUser->location->name);
$this->assertEquals($row['employeeNumber'], $newUser->phone);
$this->assertFalse(Hash::isHashed($newUser->password));
@@ -485,6 +485,50 @@ class LicenseSeatUpdateTest extends TestCase
]);
}
public function test_user_in_same_company_can_be_assigned_license_seat_when_full_company_support_is_enabled()
{
$this->settings->enableMultipleFullCompanySupport();
$company = Company::factory()->create();
$license = License::factory()->for($company)->create();
$seat = LicenseSeat::factory()->create(['license_id' => $license->id, 'assigned_to' => null, 'asset_id' => null]);
$target = $company->users()->save(User::factory()->make());
$actor = User::factory()->superuser()->create();
$this->actingAsForApi($actor)
->patchJson($this->route($seat), ['assigned_to' => $target->id])
->assertOk()
->assertStatusMessageIs('success');
$this->assertEquals($target->id, $seat->fresh()->assigned_to);
}
public function test_user_in_multiple_companies_can_be_assigned_license_from_any_of_their_companies_when_full_company_support_is_enabled()
{
$this->settings->enableMultipleFullCompanySupport();
[$companyA, $companyB] = Company::factory()->count(2)->create();
$target = User::factory()->create();
$target->companies()->sync([$companyA->id, $companyB->id]);
$actor = User::factory()->superuser()->create();
$licenseInA = License::factory()->for($companyA)->create();
$seatInA = LicenseSeat::factory()->create(['license_id' => $licenseInA->id, 'assigned_to' => null, 'asset_id' => null]);
$licenseInB = License::factory()->for($companyB)->create();
$seatInB = LicenseSeat::factory()->create(['license_id' => $licenseInB->id, 'assigned_to' => null, 'asset_id' => null]);
$this->actingAsForApi($actor)
->patchJson($this->route($seatInA), ['assigned_to' => $target->id])
->assertOk()
->assertStatusMessageIs('success');
$this->actingAsForApi($actor)
->patchJson($this->route($seatInB), ['assigned_to' => $target->id])
->assertOk()
->assertStatusMessageIs('success');
}
private function route(LicenseSeat $licenseSeat)
{
return route('api.licenses.seats.update', [$licenseSeat->license->id, $licenseSeat->id]);
@@ -107,4 +107,54 @@ class UsersForSelectListTest extends TestCase
$response = $this->getJson(route('api.users.selectlist', ['search' => 'dvader']))->assertOk();
$this->assertEquals(0, collect($response->json('results'))->count());
}
public function test_users_are_filtered_by_company_id_parameter_when_full_company_support_is_enabled()
{
$this->settings->enableMultipleFullCompanySupport();
[$companyA, $companyB] = Company::factory()->count(2)->create();
$userInA = User::factory()->create(['first_name' => 'Luke', 'last_name' => 'Skywalker', 'username' => 'lskywalker_fmcs1']);
$companyA->users()->attach($userInA);
$userInB = User::factory()->create(['first_name' => 'Darth', 'last_name' => 'Vader', 'username' => 'dvader_fmcs1']);
$companyB->users()->attach($userInB);
$actor = User::factory()->superuser()->create();
$response = $this->actingAsForApi($actor)
->getJson(route('api.users.selectlist', ['companyId' => $companyA->id]))
->assertOk();
$results = collect($response->json('results'));
$this->assertTrue($results->pluck('text')->contains(fn ($t) => str_contains($t, 'Luke')));
$this->assertFalse($results->pluck('text')->contains(fn ($t) => str_contains($t, 'Darth')));
}
public function test_users_are_filtered_by_multiple_comma_separated_company_ids_when_full_company_support_is_enabled()
{
$this->settings->enableMultipleFullCompanySupport();
[$companyA, $companyB, $companyC] = Company::factory()->count(3)->create();
$userInA = User::factory()->create(['first_name' => 'Luke', 'last_name' => 'Skywalker', 'username' => 'lskywalker_fmcs2']);
$companyA->users()->attach($userInA);
$userInB = User::factory()->create(['first_name' => 'Obi-Wan', 'last_name' => 'Kenobi', 'username' => 'okenobi_fmcs2']);
$companyB->users()->attach($userInB);
$userInC = User::factory()->create(['first_name' => 'Darth', 'last_name' => 'Vader', 'username' => 'dvader_fmcs2']);
$companyC->users()->attach($userInC);
$actor = User::factory()->superuser()->create();
$response = $this->actingAsForApi($actor)
->getJson(route('api.users.selectlist', ['companyId' => $companyA->id.','.$companyB->id]))
->assertOk();
$results = collect($response->json('results'));
$this->assertTrue($results->pluck('text')->contains(fn ($t) => str_contains($t, 'Luke')));
$this->assertTrue($results->pluck('text')->contains(fn ($t) => str_contains($t, 'Obi-Wan')));
$this->assertFalse($results->pluck('text')->contains(fn ($t) => str_contains($t, 'Darth')));
}
}
@@ -337,6 +337,10 @@ class UpdateUserTest extends TestCase
'id' => $id,
'first_name' => 'test',
'username' => 'test',
]);
$this->assertDatabaseHas('company_user', [
'user_id' => $id,
'company_id' => $companyB->id,
]);
}