diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index aa812a9979..00c31f09ba 100644 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -747,9 +747,13 @@ class UsersController extends Controller $assets = $assets->InModelList($model_ids); } - $assets = $assets->get(); + $total = $assets->count(); + $offset = ($request->input('offset') > $total) ? $total : app('api_offset_value'); + $limit = app('api_limit_value'); - return (new AssetsTransformer)->transformAssets($assets, $assets->count(), $request); + $assets = $assets->skip($offset)->take($limit)->get(); + + return (new AssetsTransformer)->transformAssets($assets, $total, $request); } return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found', compact('id')))); @@ -812,15 +816,22 @@ class UsersController extends Controller * * @param $userId */ - public function accessories($id): array + public function accessories(Request $request, $id): array { $this->authorize('view', User::class); $user = User::findOrFail($id); $this->authorize('view', $user); $this->authorize('view', Accessory::class); - $accessories = $user->accessories; - return (new AccessoriesTransformer)->transformAccessories($accessories, $accessories->count()); + $accessories = $user->accessories(); + + $total = $accessories->count(); + $offset = ($request->input('offset') > $total) ? $total : app('api_offset_value'); + $limit = app('api_limit_value'); + + $accessories = $accessories->skip($offset)->take($limit)->get(); + + return (new AccessoriesTransformer)->transformAccessories($accessories, $total); } /** @@ -832,15 +843,21 @@ class UsersController extends Controller * * @param $userId */ - public function licenses($id): JsonResponse|array + public function licenses(Request $request, $id): JsonResponse|array { $this->authorize('view', User::class); $this->authorize('view', License::class); if ($user = User::where('id', $id)->withTrashed()->first()) { - $licenses = $user->licenses()->get(); + $licenses = $user->licenses(); - return (new LicensesTransformer)->transformLicenses($licenses, $licenses->count()); + $total = $licenses->count(); + $offset = ($request->input('offset') > $total) ? $total : app('api_offset_value'); + $limit = app('api_limit_value'); + + $licenses = $licenses->skip($offset)->take($limit)->get(); + + return (new LicensesTransformer)->transformLicenses($licenses, $total); } return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/users/message.user_not_found', compact('id')))); @@ -915,14 +932,20 @@ class UsersController extends Controller * * @author [Godfrey Martinez] [] */ - public function eulas(User $user, ActionlogsTransformer $transformer) + public function eulas(Request $request, User $user, ActionlogsTransformer $transformer) { $this->authorize('view', $user); - $eulas = $user->eulas; + $eulas = $user->eulas(); + + $total = $eulas->count(); + $offset = ($request->input('offset') > $total) ? $total : app('api_offset_value'); + $limit = app('api_limit_value'); + + $eulas = $eulas->skip($offset)->take($limit)->get(); return response()->json( - $transformer->transformActionlogs($eulas, $eulas->count()) + $transformer->transformActionlogs($eulas, $total) ); } diff --git a/tests/Feature/Users/Api/UserItemsPaginationTest.php b/tests/Feature/Users/Api/UserItemsPaginationTest.php new file mode 100644 index 0000000000..9dcf75ea36 --- /dev/null +++ b/tests/Feature/Users/Api/UserItemsPaginationTest.php @@ -0,0 +1,185 @@ +create(); + $bystander = User::factory()->create(); + + Asset::factory()->count(7)->assignedToUser($user)->create(); + Asset::factory()->count(3)->assignedToUser($bystander)->create(); + + $response = $this->actingAsForApi(User::factory()->viewUsers()->viewAssets()->create()) + ->getJson(route('api.users.assetlist', ['user' => $user->id, 'limit' => 3, 'page' => 2])) + ->assertOk() + ->json(); + + $this->assertSame(7, $response['total'], 'total should reflect the users assigned assets only'); + $this->assertCount(3, $response['rows'], 'rows should be paginated to per_page'); + $this->assertSame(3, $response['per_page']); + $this->assertSame(2, $response['current_page']); + $this->assertSame(3, $response['total_pages']); + $this->assertNotNull($response['next_page_url']); + $this->assertNotNull($response['prev_page_url']); + } + + public function test_assets_endpoint_last_page_returns_remainder_and_no_next_url(): void + { + $user = User::factory()->create(); + Asset::factory()->count(7)->assignedToUser($user)->create(); + + $response = $this->actingAsForApi(User::factory()->viewUsers()->viewAssets()->create()) + ->getJson(route('api.users.assetlist', ['user' => $user->id, 'limit' => 3, 'page' => 3])) + ->assertOk() + ->json(); + + $this->assertSame(7, $response['total']); + $this->assertCount(1, $response['rows']); + $this->assertSame(3, $response['current_page']); + $this->assertNull($response['next_page_url']); + $this->assertNotNull($response['prev_page_url']); + } + + public function test_accessories_endpoint_paginates_rows_and_reports_accurate_metadata(): void + { + $user = User::factory()->create(); + $bystander = User::factory()->create(); + + Accessory::factory()->count(5)->checkedOutToUser($user)->create(); + Accessory::factory()->count(2)->checkedOutToUser($bystander)->create(); + + $response = $this->actingAsForApi(User::factory()->viewUsers()->viewAccessories()->create()) + ->getJson(route('api.users.accessorieslist', ['user' => $user->id, 'limit' => 2, 'page' => 2])) + ->assertOk() + ->json(); + + $this->assertSame(5, $response['total']); + $this->assertCount(2, $response['rows']); + $this->assertSame(2, $response['per_page']); + $this->assertSame(2, $response['current_page']); + $this->assertSame(3, $response['total_pages']); + $this->assertNotNull($response['next_page_url']); + $this->assertNotNull($response['prev_page_url']); + } + + public function test_licenses_endpoint_paginates_rows_and_reports_accurate_metadata(): void + { + $user = User::factory()->create(); + $bystander = User::factory()->create(); + + $userLicenses = License::factory()->count(6)->create(); + foreach ($userLicenses as $license) { + LicenseSeat::factory()->for($license)->assignedToUser($user)->create(); + } + + $bystanderLicense = License::factory()->create(); + LicenseSeat::factory()->for($bystanderLicense)->assignedToUser($bystander)->create(); + + $response = $this->actingAsForApi(User::factory()->viewUsers()->viewLicenses()->create()) + ->getJson(route('api.users.licenselist', ['user' => $user->id, 'limit' => 2, 'page' => 2])) + ->assertOk() + ->json(); + + $this->assertSame(6, $response['total']); + $this->assertCount(2, $response['rows']); + $this->assertSame(2, $response['per_page']); + $this->assertSame(2, $response['current_page']); + $this->assertSame(3, $response['total_pages']); + $this->assertNotNull($response['next_page_url']); + $this->assertNotNull($response['prev_page_url']); + } + + public function test_eulas_endpoint_paginates_rows_and_reports_accurate_metadata(): void + { + $user = User::factory()->create(); + $bystander = User::factory()->create(); + + // eulas() filters on target_type=User, action_type=accepted, and + // NOT NULL filename + accept_signature. Bystander rows and rows + // missing either field must be excluded from the total. + Actionlog::factory()->count(4)->create([ + 'target_type' => User::class, + 'target_id' => $user->id, + 'item_type' => Asset::class, + 'action_type' => 'accepted', + 'filename' => 'eula.pdf', + 'accept_signature' => 'sig', + ]); + Actionlog::factory()->create([ + 'target_type' => User::class, + 'target_id' => $bystander->id, + 'item_type' => Asset::class, + 'action_type' => 'accepted', + 'filename' => 'eula.pdf', + 'accept_signature' => 'sig', + ]); + + $response = $this->actingAsForApi(User::factory()->viewUsers()->create()) + ->getJson(route('api.user.eulas', ['user' => $user->id, 'limit' => 2, 'page' => 2])) + ->assertOk() + ->json(); + + $this->assertSame(4, $response['total']); + $this->assertCount(2, $response['rows']); + $this->assertSame(2, $response['per_page']); + $this->assertSame(2, $response['current_page']); + $this->assertSame(2, $response['total_pages']); + $this->assertNull($response['next_page_url']); + $this->assertNotNull($response['prev_page_url']); + } + + public function test_out_of_range_page_returns_empty_rows_but_keeps_total(): void + { + $user = User::factory()->create(); + Asset::factory()->count(2)->assignedToUser($user)->create(); + + $response = $this->actingAsForApi(User::factory()->viewUsers()->viewAssets()->create()) + ->getJson(route('api.users.assetlist', ['user' => $user->id, 'limit' => 10, 'page' => 99])) + ->assertOk() + ->json(); + + $this->assertSame(2, $response['total']); + $this->assertCount(0, $response['rows']); + } + + public function test_default_no_pagination_params_still_returns_paged_response(): void + { + // Without ?limit or ?page, the middleware falls back to config + // app.max_results (150 in this app). The rows should include every + // matching item up to that ceiling, with current_page=1 and no prev + // URL. Guards against a regression where "no page param" bypassed + // the skip/take entirely. + $user = User::factory()->create(); + Asset::factory()->count(4)->assignedToUser($user)->create(); + + $response = $this->actingAsForApi(User::factory()->viewUsers()->viewAssets()->create()) + ->getJson(route('api.users.assetlist', ['user' => $user->id])) + ->assertOk() + ->json(); + + $this->assertSame(4, $response['total']); + $this->assertCount(4, $response['rows']); + $this->assertSame(1, $response['current_page']); + $this->assertNull($response['prev_page_url']); + $this->assertNull($response['next_page_url']); + } +}