API pagination: Fixed #19155 - API not paginating correctly with page=x, added tests

This commit is contained in:
snipe
2026-06-08 14:03:47 +01:00
parent 9bc4efa5ff
commit 1252681d55
10 changed files with 517 additions and 6 deletions
@@ -107,7 +107,7 @@ class AccessoriesController extends Controller
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $accessories->count()) ? $accessories->count() : abs($request->input('offset'));
$offset = ($request->input('offset') > $accessories->count()) ? $accessories->count() : app('api_offset_value');
$limit = app('api_limit_value');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
@@ -133,7 +133,8 @@ class AssetModelsController extends Controller
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $assetmodels->count()) ? $assetmodels->count() : abs($request->input('offset'));
$total = $assetmodels->count();
$offset = ($request->input('offset') > $total) ? $total : app('api_offset_value');
$limit = app('api_limit_value');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
@@ -157,7 +158,6 @@ class AssetModelsController extends Controller
break;
}
$total = $assetmodels->count();
$assetmodels = $assetmodels->skip($offset)->take($limit)->get();
return (new AssetModelsTransformer)->transformAssetModels($assetmodels, $total);
@@ -30,7 +30,7 @@ class MaintenanceTypesController extends Controller
$types->where('name', '=', $request->input('name'));
}
$offset = ($request->input('offset') > $types->count()) ? $types->count() : abs($request->input('offset'));
$offset = ($request->input('offset') > $types->count()) ? $types->count() : app('api_offset_value');
$limit = app('api_limit_value');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), ['id', 'name', 'created_at', 'updated_at']) ? $request->input('sort') : 'name';
@@ -102,7 +102,7 @@ class MaintenancesController extends Controller
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $maintenances->count()) ? $maintenances->count() : abs($request->input('offset'));
$offset = ($request->input('offset') > $maintenances->count()) ? $maintenances->count() : app('api_offset_value');
$limit = app('api_limit_value');
$allowed_columns = [
@@ -52,7 +52,7 @@ class UploadedFilesController extends Controller
$uploads = self::$map_object_type[$object_type]::withTrashed()->find($id)->uploads()
->with('adminuser');
$offset = ($request->input('offset') > $uploads->count()) ? $uploads->count() : abs($request->input('offset'));
$offset = ($request->input('offset') > $uploads->count()) ? $uploads->count() : app('api_offset_value');
$limit = app('api_limit_value');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
@@ -0,0 +1,92 @@
<?php
namespace Tests\Feature\Accessories\Api;
use App\Models\Accessory;
use App\Models\User;
use Tests\TestCase;
class AccessoryPaginationTest extends TestCase
{
private User $user;
protected function setUp(): void
{
parent::setUp();
$this->user = User::factory()->superuser()->create();
}
public function test_page_one_returns_first_items()
{
foreach (range(1, 10) as $i) {
Accessory::factory()->create(['name' => sprintf('PAG-TEST-%03d', $i)]);
}
$names = $this->actingAsForApi($this->user)
->getJson(route('api.accessories.index', ['page' => 1, 'limit' => 5, 'sort' => 'name', 'order' => 'asc']))
->assertOk()
->json('rows.*.name');
$this->assertEquals(
['PAG-TEST-001', 'PAG-TEST-002', 'PAG-TEST-003', 'PAG-TEST-004', 'PAG-TEST-005'],
$names
);
}
public function test_page_two_returns_second_set_of_items()
{
foreach (range(1, 10) as $i) {
Accessory::factory()->create(['name' => sprintf('PAG-TEST-%03d', $i)]);
}
$names = $this->actingAsForApi($this->user)
->getJson(route('api.accessories.index', ['page' => 2, 'limit' => 5, 'sort' => 'name', 'order' => 'asc']))
->assertOk()
->json('rows.*.name');
$this->assertEquals(
['PAG-TEST-006', 'PAG-TEST-007', 'PAG-TEST-008', 'PAG-TEST-009', 'PAG-TEST-010'],
$names
);
}
public function test_offset_returns_correct_items()
{
foreach (range(1, 10) as $i) {
Accessory::factory()->create(['name' => sprintf('PAG-TEST-%03d', $i)]);
}
$names = $this->actingAsForApi($this->user)
->getJson(route('api.accessories.index', ['offset' => 5, 'limit' => 5, 'sort' => 'name', 'order' => 'asc']))
->assertOk()
->json('rows.*.name');
$this->assertEquals(
['PAG-TEST-006', 'PAG-TEST-007', 'PAG-TEST-008', 'PAG-TEST-009', 'PAG-TEST-010'],
$names
);
}
public function test_page_param_respects_limit()
{
Accessory::factory()->count(10)->create();
$response = $this->actingAsForApi($this->user)
->getJson(route('api.accessories.index', ['page' => 1, 'limit' => 4]))
->assertOk();
$this->assertCount(4, $response->json('rows'));
}
public function test_page_beyond_results_returns_empty_rows()
{
Accessory::factory()->count(5)->create();
$response = $this->actingAsForApi($this->user)
->getJson(route('api.accessories.index', ['page' => 99, 'limit' => 5]))
->assertOk();
$this->assertCount(0, $response->json('rows'));
$this->assertEquals(5, $response->json('total'));
}
}
@@ -0,0 +1,92 @@
<?php
namespace Tests\Feature\AssetModels\Api;
use App\Models\AssetModel;
use App\Models\User;
use Tests\TestCase;
class AssetModelPaginationTest extends TestCase
{
private User $user;
protected function setUp(): void
{
parent::setUp();
$this->user = User::factory()->superuser()->create();
}
public function test_page_one_returns_first_items()
{
foreach (range(1, 10) as $i) {
AssetModel::factory()->create(['name' => sprintf('PAG-TEST-%03d', $i)]);
}
$names = $this->actingAsForApi($this->user)
->getJson(route('api.models.index', ['page' => 1, 'limit' => 5, 'sort' => 'name', 'order' => 'asc']))
->assertOk()
->json('rows.*.name');
$this->assertEquals(
['PAG-TEST-001', 'PAG-TEST-002', 'PAG-TEST-003', 'PAG-TEST-004', 'PAG-TEST-005'],
$names
);
}
public function test_page_two_returns_second_set_of_items()
{
foreach (range(1, 10) as $i) {
AssetModel::factory()->create(['name' => sprintf('PAG-TEST-%03d', $i)]);
}
$names = $this->actingAsForApi($this->user)
->getJson(route('api.models.index', ['page' => 2, 'limit' => 5, 'sort' => 'name', 'order' => 'asc']))
->assertOk()
->json('rows.*.name');
$this->assertEquals(
['PAG-TEST-006', 'PAG-TEST-007', 'PAG-TEST-008', 'PAG-TEST-009', 'PAG-TEST-010'],
$names
);
}
public function test_offset_returns_correct_items()
{
foreach (range(1, 10) as $i) {
AssetModel::factory()->create(['name' => sprintf('PAG-TEST-%03d', $i)]);
}
$names = $this->actingAsForApi($this->user)
->getJson(route('api.models.index', ['offset' => 5, 'limit' => 5, 'sort' => 'name', 'order' => 'asc']))
->assertOk()
->json('rows.*.name');
$this->assertEquals(
['PAG-TEST-006', 'PAG-TEST-007', 'PAG-TEST-008', 'PAG-TEST-009', 'PAG-TEST-010'],
$names
);
}
public function test_page_param_respects_limit()
{
AssetModel::factory()->count(10)->create();
$response = $this->actingAsForApi($this->user)
->getJson(route('api.models.index', ['page' => 1, 'limit' => 4]))
->assertOk();
$this->assertCount(4, $response->json('rows'));
}
public function test_page_beyond_results_returns_empty_rows()
{
AssetModel::factory()->count(5)->create();
$response = $this->actingAsForApi($this->user)
->getJson(route('api.models.index', ['page' => 99, 'limit' => 5]))
->assertOk();
$this->assertCount(0, $response->json('rows'));
$this->assertEquals(5, $response->json('total'));
}
}
@@ -0,0 +1,141 @@
<?php
namespace Tests\Feature\Assets\Api;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\User;
use Tests\TestCase;
class UploadedFilesPaginationTest extends TestCase
{
private User $user;
private Asset $asset;
protected function setUp(): void
{
parent::setUp();
$this->user = User::factory()->superuser()->create();
$this->asset = Asset::factory()->create();
}
private function createUploadLog(string $filename): void
{
$log = new Actionlog();
$log->item_id = $this->asset->id;
$log->item_type = Asset::class;
$log->action_type = 'uploaded';
$log->filename = $filename;
$log->created_by = $this->user->id;
$log->save();
}
public function test_page_one_returns_first_items()
{
foreach (range(1, 10) as $i) {
$this->createUploadLog(sprintf('PAG-TEST-%03d.jpg', $i));
}
$filenames = $this->actingAsForApi($this->user)
->getJson(route('api.files.index', [
'object_type' => 'assets',
'id' => $this->asset->id,
'page' => 1,
'limit' => 5,
'sort' => 'filename',
'order' => 'asc',
]))
->assertOk()
->json('rows.*.filename');
$this->assertEquals(
['PAG-TEST-001.jpg', 'PAG-TEST-002.jpg', 'PAG-TEST-003.jpg', 'PAG-TEST-004.jpg', 'PAG-TEST-005.jpg'],
$filenames
);
}
public function test_page_two_returns_second_set_of_items()
{
foreach (range(1, 10) as $i) {
$this->createUploadLog(sprintf('PAG-TEST-%03d.jpg', $i));
}
$filenames = $this->actingAsForApi($this->user)
->getJson(route('api.files.index', [
'object_type' => 'assets',
'id' => $this->asset->id,
'page' => 2,
'limit' => 5,
'sort' => 'filename',
'order' => 'asc',
]))
->assertOk()
->json('rows.*.filename');
$this->assertEquals(
['PAG-TEST-006.jpg', 'PAG-TEST-007.jpg', 'PAG-TEST-008.jpg', 'PAG-TEST-009.jpg', 'PAG-TEST-010.jpg'],
$filenames
);
}
public function test_offset_returns_correct_items()
{
foreach (range(1, 10) as $i) {
$this->createUploadLog(sprintf('PAG-TEST-%03d.jpg', $i));
}
$filenames = $this->actingAsForApi($this->user)
->getJson(route('api.files.index', [
'object_type' => 'assets',
'id' => $this->asset->id,
'offset' => 5,
'limit' => 5,
'sort' => 'filename',
'order' => 'asc',
]))
->assertOk()
->json('rows.*.filename');
$this->assertEquals(
['PAG-TEST-006.jpg', 'PAG-TEST-007.jpg', 'PAG-TEST-008.jpg', 'PAG-TEST-009.jpg', 'PAG-TEST-010.jpg'],
$filenames
);
}
public function test_page_param_respects_limit()
{
foreach (range(1, 10) as $i) {
$this->createUploadLog(sprintf('PAG-TEST-%03d.jpg', $i));
}
$response = $this->actingAsForApi($this->user)
->getJson(route('api.files.index', [
'object_type' => 'assets',
'id' => $this->asset->id,
'page' => 1,
'limit' => 4,
]))
->assertOk();
$this->assertCount(4, $response->json('rows'));
}
public function test_page_beyond_results_returns_empty_rows()
{
foreach (range(1, 5) as $i) {
$this->createUploadLog(sprintf('PAG-TEST-%03d.jpg', $i));
}
$response = $this->actingAsForApi($this->user)
->getJson(route('api.files.index', [
'object_type' => 'assets',
'id' => $this->asset->id,
'page' => 99,
'limit' => 5,
]))
->assertOk();
$this->assertCount(0, $response->json('rows'));
$this->assertEquals(5, $response->json('total'));
}
}
@@ -0,0 +1,92 @@
<?php
namespace Tests\Feature\Maintenances\Api;
use App\Models\Maintenance;
use App\Models\User;
use Tests\TestCase;
class MaintenancePaginationTest extends TestCase
{
private User $user;
protected function setUp(): void
{
parent::setUp();
$this->user = User::factory()->superuser()->create();
}
public function test_page_one_returns_first_items()
{
foreach (range(1, 10) as $i) {
Maintenance::factory()->create(['name' => sprintf('PAG-TEST-%03d', $i)]);
}
$names = $this->actingAsForApi($this->user)
->getJson(route('api.maintenances.index', ['page' => 1, 'limit' => 5, 'sort' => 'name', 'order' => 'asc']))
->assertOk()
->json('rows.*.name');
$this->assertEquals(
['PAG-TEST-001', 'PAG-TEST-002', 'PAG-TEST-003', 'PAG-TEST-004', 'PAG-TEST-005'],
$names
);
}
public function test_page_two_returns_second_set_of_items()
{
foreach (range(1, 10) as $i) {
Maintenance::factory()->create(['name' => sprintf('PAG-TEST-%03d', $i)]);
}
$names = $this->actingAsForApi($this->user)
->getJson(route('api.maintenances.index', ['page' => 2, 'limit' => 5, 'sort' => 'name', 'order' => 'asc']))
->assertOk()
->json('rows.*.name');
$this->assertEquals(
['PAG-TEST-006', 'PAG-TEST-007', 'PAG-TEST-008', 'PAG-TEST-009', 'PAG-TEST-010'],
$names
);
}
public function test_offset_returns_correct_items()
{
foreach (range(1, 10) as $i) {
Maintenance::factory()->create(['name' => sprintf('PAG-TEST-%03d', $i)]);
}
$names = $this->actingAsForApi($this->user)
->getJson(route('api.maintenances.index', ['offset' => 5, 'limit' => 5, 'sort' => 'name', 'order' => 'asc']))
->assertOk()
->json('rows.*.name');
$this->assertEquals(
['PAG-TEST-006', 'PAG-TEST-007', 'PAG-TEST-008', 'PAG-TEST-009', 'PAG-TEST-010'],
$names
);
}
public function test_page_param_respects_limit()
{
Maintenance::factory()->count(10)->create();
$response = $this->actingAsForApi($this->user)
->getJson(route('api.maintenances.index', ['page' => 1, 'limit' => 4]))
->assertOk();
$this->assertCount(4, $response->json('rows'));
}
public function test_page_beyond_results_returns_empty_rows()
{
Maintenance::factory()->count(5)->create();
$response = $this->actingAsForApi($this->user)
->getJson(route('api.maintenances.index', ['page' => 99, 'limit' => 5]))
->assertOk();
$this->assertCount(0, $response->json('rows'));
$this->assertEquals(5, $response->json('total'));
}
}
@@ -0,0 +1,94 @@
<?php
namespace Tests\Feature\Maintenances\Api;
use App\Models\MaintenanceType;
use App\Models\User;
use Tests\TestCase;
class MaintenanceTypesPaginationTest extends TestCase
{
private User $user;
protected function setUp(): void
{
parent::setUp();
$this->user = User::factory()->superuser()->create();
// Remove migration-seeded types so alphabetical sorting is predictable
MaintenanceType::query()->delete();
}
public function test_page_one_returns_first_items()
{
foreach (range(1, 10) as $i) {
MaintenanceType::factory()->create(['name' => sprintf('PAG-TEST-%03d', $i)]);
}
$names = $this->actingAsForApi($this->user)
->getJson(route('api.maintenance-types.index', ['page' => 1, 'limit' => 5, 'sort' => 'name', 'order' => 'asc']))
->assertOk()
->json('rows.*.name');
$this->assertEquals(
['PAG-TEST-001', 'PAG-TEST-002', 'PAG-TEST-003', 'PAG-TEST-004', 'PAG-TEST-005'],
$names
);
}
public function test_page_two_returns_second_set_of_items()
{
foreach (range(1, 10) as $i) {
MaintenanceType::factory()->create(['name' => sprintf('PAG-TEST-%03d', $i)]);
}
$names = $this->actingAsForApi($this->user)
->getJson(route('api.maintenance-types.index', ['page' => 2, 'limit' => 5, 'sort' => 'name', 'order' => 'asc']))
->assertOk()
->json('rows.*.name');
$this->assertEquals(
['PAG-TEST-006', 'PAG-TEST-007', 'PAG-TEST-008', 'PAG-TEST-009', 'PAG-TEST-010'],
$names
);
}
public function test_offset_returns_correct_items()
{
foreach (range(1, 10) as $i) {
MaintenanceType::factory()->create(['name' => sprintf('PAG-TEST-%03d', $i)]);
}
$names = $this->actingAsForApi($this->user)
->getJson(route('api.maintenance-types.index', ['offset' => 5, 'limit' => 5, 'sort' => 'name', 'order' => 'asc']))
->assertOk()
->json('rows.*.name');
$this->assertEquals(
['PAG-TEST-006', 'PAG-TEST-007', 'PAG-TEST-008', 'PAG-TEST-009', 'PAG-TEST-010'],
$names
);
}
public function test_page_param_respects_limit()
{
MaintenanceType::factory()->count(10)->create();
$response = $this->actingAsForApi($this->user)
->getJson(route('api.maintenance-types.index', ['page' => 1, 'limit' => 4]))
->assertOk();
$this->assertCount(4, $response->json('rows'));
}
public function test_page_beyond_results_returns_empty_rows()
{
MaintenanceType::factory()->count(5)->create();
$response = $this->actingAsForApi($this->user)
->getJson(route('api.maintenance-types.index', ['page' => 99, 'limit' => 5]))
->assertOk();
$this->assertCount(0, $response->json('rows'));
$this->assertEquals(5, $response->json('total'));
}
}