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()); } }