diff --git a/app/Http/Controllers/Api/AssetsController.php b/app/Http/Controllers/Api/AssetsController.php index 160f4a120a20..8260cacf1021 100644 --- a/app/Http/Controllers/Api/AssetsController.php +++ b/app/Http/Controllers/Api/AssetsController.php @@ -9,6 +9,7 @@ use App\Models\AccessoryCheckout; use App\Models\CheckoutAcceptance; use App\Models\LicenseSeat; +use App\Rules\FlatArray; use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Crypt; @@ -34,8 +35,6 @@ use Illuminate\Support\Facades\Route; use App\View\Label; use Illuminate\Support\Facades\Storage; -use Illuminate\Support\Facades\Validator; - /** * This class controls all actions related to assets for @@ -57,7 +56,12 @@ class AssetsController extends Controller */ public function index(Request $request, $action = null, $upcoming_status = null) : JsonResponse | array { - + $request->validate([ + 'filter' => [ + 'json', + new FlatArray, + ], + ]); // This handles the legacy audit endpoints :( if ($action == 'audit') { diff --git a/app/Rules/FlatArray.php b/app/Rules/FlatArray.php new file mode 100644 index 000000000000..75537df33603 --- /dev/null +++ b/app/Rules/FlatArray.php @@ -0,0 +1,32 @@ + 'b'] + * Fails: ['a' => ['b', 'c']] + */ +class FlatArray implements ValidationRule +{ + /** + * Run the validation rule. + * + * @param \Closure(string, ?string=): \Illuminate\Translation\PotentiallyTranslatedString $fail + */ + public function validate(string $attribute, mixed $value, Closure $fail): void + { + if (is_string($value)) { + $value = json_decode($value, true); + } + + foreach ($value as $arrayValue) { + if (is_array($arrayValue)) { + $fail(":attribute cannot contain a nested data."); + } + } + } +} diff --git a/tests/Feature/Assets/Api/AssetIndexTest.php b/tests/Feature/Assets/Api/AssetIndexTest.php index 8554edb3d459..d6197e231f0e 100644 --- a/tests/Feature/Assets/Api/AssetIndexTest.php +++ b/tests/Feature/Assets/Api/AssetIndexTest.php @@ -7,6 +7,7 @@ use App\Models\User; use Carbon\Carbon; use Illuminate\Testing\Fluent\AssertableJson; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\TestCase; class AssetIndexTest extends TestCase @@ -126,6 +127,47 @@ public function testAssetApiIndexReturnsDueOrOverdueForExpectedCheckin() ->assertJson(fn(AssertableJson $json) => $json->has('rows', 5)->etc()); } + public static function filterValues() + { + return [ + ['filter%5Bassigned_to%5D'], + ['filter[assigned_to][not]=null'], + ['filter={%22assigned_to%22:{%22$ne%22:null}}'], + ['filter=%5B%22a%22%20%3D%3E%20%22b%22%5D'], + ]; + } + + /** + * [RB-17904] + * [RB-19910] + */ + #[DataProvider('filterValues')] + public function test_handles_non_string_filter($filterString) + { + $this->actingAsForApi(User::factory()->superuser()->create()) + ->getJson(route('api.assets.index') . '?' . $filterString) + ->assertOk() + ->assertStatusMessageIs('error') + ->assertMessagesContains('filter'); + } + + public function test_can_filter_results() + { + Asset::factory()->create(['purchase_date' => '2025-07-01', 'order_number' => '123']); + Asset::factory()->create(['purchase_date' => '2025-07-01', 'order_number' => '123']); + Asset::factory()->create(['purchase_date' => '2025-07-01', 'order_number' => '456']); + + $this->actingAsForApi(User::factory()->superuser()->create()) + ->getJson(route('api.assets.index', [ + 'filter' => json_encode([ + 'order_number' => '123', + 'purchase_date' => '2025-07-01', + ]), + ])) + ->assertOk() + ->assertJson(fn(AssertableJson $json) => $json->has('rows', 2)->etc()); + } + public function testAssetApiIndexAdheresToCompanyScoping() { [$companyA, $companyB] = Company::factory()->count(2)->create();