Files
alrahma_sunday_school_api/tests/Feature/Api/V1/FullSurface/ApiQueryParameterFuzzingContractTest.php
T
2026-06-09 00:03:03 -04:00

99 lines
3.5 KiB
PHP

<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiQueryParameterFuzzingContractTest extends FullSurfaceE2EContractCase
{
public function test_collection_endpoints_handle_hostile_query_parameters_without_5xx(): void
{
foreach ($this->collectionPaths() as $path) {
foreach ($this->hostileQueries() as $query) {
$response = $this->requestAs($this->actorFor($path), 'GET', $path.'?'.http_build_query($query));
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], "hostile query $path");
$this->assertNoServerError($response, "hostile query $path");
}
}
}
public function test_date_range_endpoints_reject_inverted_or_impossible_ranges_cleanly(): void
{
foreach ($this->dateRangePaths() as $path) {
foreach ($this->badDateRanges() as $query) {
$response = $this->requestAs($this->actorFor($path), 'GET', $path.'?'.http_build_query($query));
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], "bad date range $path");
$this->assertNoServerError($response, "bad date range $path");
}
}
}
public function test_sorting_contract_does_not_allow_raw_sql_fragments(): void
{
foreach ($this->collectionPaths() as $path) {
$response = $this->requestAs($this->actorFor($path), 'GET', $path.'?'.http_build_query([
'sort' => 'id desc; drop table users; --',
'direction' => 'sideways',
]));
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], "SQL-ish sort $path");
$this->assertNoServerError($response, "SQL-ish sort $path");
$this->assertStringNotContainsString('SQLSTATE', $response->getContent(), "$path leaked SQLSTATE for malicious sort.");
}
}
/** @return list<string> */
private function collectionPaths(): array
{
return [
'api/v1/users',
'api/v1/students',
'api/v1/classes',
'api/v1/attendance',
'api/v1/finance/invoices',
'api/v1/finance/payments',
'api/v1/inventory/items',
'api/v1/messages',
'api/v1/support',
'api/v1/report-cards',
];
}
/** @return list<array<string,mixed>> */
private function hostileQueries(): array
{
return [
['page' => -1, 'per_page' => 0],
['page' => 'abc', 'per_page' => '999999999999'],
['search' => str_repeat('x', 5000)],
['status' => ['nested' => ['bad' => true]]],
['include' => '../../../.env'],
];
}
/** @return list<string> */
private function dateRangePaths(): array
{
return [
'api/v1/attendance',
'api/v1/finance/reports',
'api/v1/finance/payments',
'api/v1/report-cards',
'api/v1/messages',
];
}
/** @return list<array<string,string>> */
private function badDateRanges(): array
{
return [
['start_date' => '2026-12-31', 'end_date' => '2025-01-01'],
['from' => 'not-a-date', 'to' => 'also-not-a-date'],
['date' => '0000-00-00'],
['school_year' => '../../2025', 'semester' => '<script>alert(1)</script>'],
];
}
}