3
0
mirror of https://github.com/snipe/snipe-it.git synced 2026-08-18 11:15:42 +00:00

Fixed #19334 - added pagination to user’s assets API endpoint

This commit is contained in:
snipe
2026-07-21 17:27:30 +01:00
parent 17b0e43961
commit 95eb43650e
2 changed files with 219 additions and 11 deletions

View File

@ -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] [<gmartinez@grokability.com>]
*/
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)
);
}

View File

@ -0,0 +1,185 @@
<?php
namespace Tests\Feature\Users\Api;
use App\Models\Accessory;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\License;
use App\Models\LicenseSeat;
use App\Models\User;
use Tests\TestCase;
/**
* Regression coverage for GitHub #19334: /users/{id}/assets, /accessories,
* /licenses, and /eulas emitted pagination metadata but returned every row,
* ignoring ?limit and ?page. Each test seeds N rows for the target user and a
* few extra rows for a bystander user (so we also catch a "return everything
* across all users" regression), then asserts total, rows count, per_page,
* current_page, and next/prev URL against a specific ?limit/?page query.
*/
class UserItemsPaginationTest extends TestCase
{
public function test_assets_endpoint_paginates_rows_and_reports_accurate_metadata(): void
{
$user = User::factory()->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']);
}
}