3
0
mirror of https://github.com/snipe/snipe-it.git synced 2026-08-18 11:15:42 +00:00
Files
snipe-it/tests/Feature/Users/Api/IndexUsersTest.php
2026-07-10 14:41:47 +01:00

206 lines
8.3 KiB
PHP

<?php
namespace Tests\Feature\Users\Api;
use App\Models\Asset;
use App\Models\Company;
use App\Models\Location;
use App\Models\Maintenance;
use App\Models\User;
use Illuminate\Testing\Fluent\AssertableJson;
use Tests\TestCase;
class IndexUsersTest extends TestCase
{
public function test_requires_permission()
{
$this->actingAsForApi(User::factory()->create())
->getJson(route('api.users.index'))
->assertForbidden();
}
public function test_returns_managed_users_count_correctly()
{
$manager = User::factory()->create(['first_name' => 'Manages Users']);
User::factory()->create(['first_name' => 'Does Not Manage Users']);
User::factory()->create(['manager_id' => $manager->id]);
User::factory()->create(['manager_id' => $manager->id]);
$response = $this->actingAsForApi(User::factory()->viewUsers()->create())
->getJson(route('api.users.index', [
'manages_users_count' => 2,
]))
->assertOk();
$response->assertJson(function (AssertableJson $json) {
$json->has('rows', 1)
->where('rows.0.first_name', 'Manages Users')
->etc();
});
}
public function test_returns_managed_locations_count_correctly()
{
$manager = User::factory()->create(['first_name' => 'Manages Locations']);
User::factory()->create(['first_name' => 'Does Not Manage Locations']);
Location::factory()->create(['manager_id' => $manager->id]);
Location::factory()->create(['manager_id' => $manager->id]);
$response = $this->actingAsForApi(User::factory()->viewUsers()->create())
->getJson(route('api.users.index', [
'manages_locations_count' => 2,
]))
->assertOk();
$response->assertJson(function (AssertableJson $json) {
$json->has('rows', 1)
->where('rows.0.first_name', 'Manages Locations')
->etc();
});
}
public function test_returns_assigned_maintenances_count_correctly()
{
// The withCount/has filter both target the new assignedMaintenances
// morphMany. checked_out_to_* on a maintenance is populated by
// MaintenanceObserver::creating() from the asset's assigned_to —
// so seed via "asset checked out to user, then maintenance on
// that asset" rather than passing checked_out_to_* directly.
$busy = User::factory()->create(['first_name' => 'Busy Breaker']);
User::factory()->create(['first_name' => 'Quiet User']);
$busyAsset = Asset::factory()->assignedToUser($busy)->create();
Maintenance::factory()->create(['asset_id' => $busyAsset->id]);
Maintenance::factory()->create([
'asset_id' => $busyAsset->id,
'completed_at' => now(),
]);
$response = $this->actingAsForApi(User::factory()->viewUsers()->create())
->getJson(route('api.users.index', [
'assigned_maintenances_count' => 2,
]))
->assertOk();
$response->assertJson(function (AssertableJson $json) {
$json->has('rows', 1)
->where('rows.0.first_name', 'Busy Breaker')
->where('rows.0.assigned_maintenances_count', 2)
->etc();
});
}
public function test_can_sort_users_index_by_assigned_maintenances_count()
{
$most = User::factory()->create();
$some = User::factory()->create();
$mostAsset = Asset::factory()->assignedToUser($most)->create();
Maintenance::factory()->count(3)->create(['asset_id' => $mostAsset->id]);
$someAsset = Asset::factory()->assignedToUser($some)->create();
Maintenance::factory()->create(['asset_id' => $someAsset->id]);
$response = $this->actingAsForApi(User::factory()->viewUsers()->create())
->getJson(route('api.users.index', [
'sort' => 'assigned_maintenances_count',
'order' => 'desc',
]))
->assertOk();
// Filter to just the two users we created with maintenances; everyone
// else seeded by factories has count=0 and their relative order is
// not what we're testing here. Position of $most must precede $some.
$orderedIds = collect($response->json('rows'))->pluck('id');
$mostPos = $orderedIds->search($most->id);
$somePos = $orderedIds->search($some->id);
$this->assertNotFalse($mostPos, 'User with 3 maintenances should appear in the result');
$this->assertNotFalse($somePos, 'User with 1 maintenance should appear in the result');
$this->assertLessThan(
$somePos,
$mostPos,
'User with 3 maintenances must sort before user with 1 when ordering desc by assigned_maintenances_count',
);
}
public function test_gracefully_handles_malformed_filter()
{
$this->actingAsForApi(User::factory()->viewUsers()->create())
->getJson(route('api.users.index', [
// filter should be a json encoded array and not a string
'filter' => 'email:an-email-address@example.com',
]))
->assertStatusMessageIs('error')
->assertJson(function (AssertableJson $json) {
$json->has('messages.filter')->etc();
});
}
/**
* FMCS + floaters on: a company-scoped, non-superuser caller sees
* their own pivot companies AND null-company (floater) users, but
* NOT users pivoted to a different company.
*
* Regression pin for support ticket 56305. Root cause was that
* `orWhereDoesntHave('companies')` in the floater branch had the
* companies-table CompanyableScope applied recursively to its
* subquery, filtering the JOIN to the caller's own companies. A user
* pivoted only to OUT-OF-SCOPE companies then looked pivot-less and
* slipped through as an apparent floater. Fix reads the company_user
* pivot directly. See the same pin at tests/Unit/CompanyScopingTest.php.
*/
public function test_users_index_hides_other_companies_users_but_includes_floaters_for_company_scoped_caller()
{
[$companyA, $companyB] = Company::factory()->count(2)->create();
$companyACaller = $companyA->users()->save(User::factory()->viewUsers()->make());
$companyAPeer = $companyA->users()->save(User::factory()->make(['first_name' => 'PeerInA']));
$companyBUser = $companyB->users()->save(User::factory()->make(['first_name' => 'UserInB']));
$floater = User::factory()->withoutCompany()->create(['first_name' => 'FloaterUser']);
$this->settings->enableFloaterMode();
$rows = $this->actingAsForApi($companyACaller)
->getJson(route('api.users.index'))
->assertOk()
->json('rows');
$visibleIds = collect($rows)->pluck('id')->all();
$this->assertContains($companyACaller->id, $visibleIds);
$this->assertContains($companyAPeer->id, $visibleIds);
$this->assertContains($floater->id, $visibleIds, 'Floater user should be visible per docs.');
$this->assertNotContains($companyBUser->id, $visibleIds, 'Company B user leaked to a company A caller.');
}
public function test_returns_result_via_filter()
{
User::factory()->count(3)->create(['first_name' => 'Awesome', 'last_name' => 'Admin', 'email' => 'awesome@example.org']);
$this->actingAsForApi(User::factory()->viewUsers()->create())
->getJson(route('api.users.index', [
'filter' => '{"first_name":"Awesome","last_name":"Admin","email":"awesome@example.org"}',
]))
->assertOk()
->assertJsonStructure([
'total',
'rows',
])
->assertJson(fn (AssertableJson $json) => $json->has('rows', 3)->etc());
$this->actingAsForApi(User::factory()->viewUsers()->create())
->getJson(route('api.users.index', [
'filter' => '{"first_name":"Not Awesome"}',
]))
->assertOk()
->assertJsonStructure([
'total',
'rows',
])
->assertJson(fn (AssertableJson $json) => $json->has('rows', 0)->etc());
}
}