Fix Laravel Pint formatting
This commit is contained in:
@@ -12,9 +12,9 @@ use Tests\TestCase;
|
||||
*/
|
||||
class ApiAcademicAttendanceFullSurfaceContractTest extends TestCase
|
||||
{
|
||||
use AssertsE2EApiResponses;
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
use AssertsE2EApiResponses;
|
||||
|
||||
public function test_academic_attendance_and_grading_surfaces_have_controlled_contracts(): void
|
||||
{
|
||||
@@ -24,21 +24,21 @@ class ApiAcademicAttendanceFullSurfaceContractTest extends TestCase
|
||||
|
||||
foreach ($this->teacherGetEndpoints($world) as $uri) {
|
||||
$response = $this->getJson($uri);
|
||||
$this->assertNoServerError($response, 'Teacher GET ' . $uri);
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Teacher GET ' . $uri);
|
||||
$this->assertNoServerError($response, 'Teacher GET '.$uri);
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Teacher GET '.$uri);
|
||||
}
|
||||
|
||||
foreach ($this->teacherMutationEndpoints($world) as [$method, $uri, $payload]) {
|
||||
$response = $this->json($method, $uri, $payload);
|
||||
$this->assertNoServerError($response, 'Teacher ' . $method . ' ' . $uri);
|
||||
$this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Teacher ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'Teacher '.$method.' '.$uri);
|
||||
$this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Teacher '.$method.' '.$uri);
|
||||
}
|
||||
|
||||
$this->actingAsApiAdministrator();
|
||||
foreach ($this->adminAcademicEndpoints($world) as [$method, $uri, $payload]) {
|
||||
$response = $this->json($method, $uri, $payload);
|
||||
$this->assertNoServerError($response, 'Admin academic ' . $method . ' ' . $uri);
|
||||
$this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Admin academic ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'Admin academic '.$method.' '.$uri);
|
||||
$this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Admin academic '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,35 +49,35 @@ class ApiAcademicAttendanceFullSurfaceContractTest extends TestCase
|
||||
$studentId = (int) $world['student_id'];
|
||||
|
||||
return [
|
||||
'/api/v1/teachers/classes?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/class-view?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teachers/classes?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/class-view?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/no-classes',
|
||||
'/api/v1/teacher/select-semester',
|
||||
'/api/v1/teacher/homework-list?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/add-homework?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/add-quiz?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/add-quiz-column-form?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/add-midterm-exam?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/add-final-exam?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/add-participation?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/add-project?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/homework-list?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/add-homework?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/add-quiz?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/add-quiz-column-form?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/add-midterm-exam?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/add-final-exam?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/add-participation?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/add-project?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/teacher-assignment',
|
||||
'/api/v1/teacher/exam-drafts',
|
||||
'/api/v1/teacher/drafts',
|
||||
'/api/v1/teacher/inventory/books/distribute',
|
||||
'/api/v1/teacher/print-requests/context',
|
||||
'/api/v1/teacher/absence-vacation',
|
||||
'/api/v1/teacher/showupdate-attendance?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/showupdate_attendance?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/scores?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/showupdate-attendance?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/showupdate_attendance?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/scores?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/calendar',
|
||||
'/api/v1/teacher/class-progress-submit?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/class-progress-history?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/class-progress-view?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/class-progress-submit?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/class-progress-history?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/class-progress-view?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/competition-scores',
|
||||
'/api/v1/attendance/teacher/grid?class_section_id=' . $classSectionId,
|
||||
'/api/v1/attendance/teacher/form?class_section_id=' . $classSectionId,
|
||||
'/api/v1/attendance/admin/daily?date=2025-10-05&class_section_id=' . $classSectionId,
|
||||
'/api/v1/attendance/teacher/grid?class_section_id='.$classSectionId,
|
||||
'/api/v1/attendance/teacher/form?class_section_id='.$classSectionId,
|
||||
'/api/v1/attendance/admin/daily?date=2025-10-05&class_section_id='.$classSectionId,
|
||||
'/api/v1/attendance/staff/month?month=2025-10',
|
||||
'/api/v1/attendance/staff/admins',
|
||||
'/api/v1/attendance/late-slip-logs',
|
||||
@@ -87,7 +87,7 @@ class ApiAcademicAttendanceFullSurfaceContractTest extends TestCase
|
||||
'/api/v1/attendance/management/dashboard',
|
||||
'/api/v1/attendance-tracking/pending-violations',
|
||||
'/api/v1/attendance-tracking/notified-violations',
|
||||
'/api/v1/attendance-tracking/student-case/' . $studentId,
|
||||
'/api/v1/attendance-tracking/student-case/'.$studentId,
|
||||
'/api/v1/attendance-tracking/compose',
|
||||
'/api/v1/attendance-tracking/parents-info',
|
||||
'/api/v1/attendance-comment-templates',
|
||||
@@ -96,15 +96,15 @@ class ApiAcademicAttendanceFullSurfaceContractTest extends TestCase
|
||||
'/api/v1/attendance-templates',
|
||||
'/api/v1/assignments',
|
||||
'/api/v1/assignments/class-assignment-data',
|
||||
'/api/v1/scores/overview?class_section_id=' . $classSectionId,
|
||||
'/api/v1/scores/homework?class_section_id=' . $classSectionId,
|
||||
'/api/v1/scores/quiz?class_section_id=' . $classSectionId,
|
||||
'/api/v1/scores/project?class_section_id=' . $classSectionId,
|
||||
'/api/v1/scores/midterm?class_section_id=' . $classSectionId,
|
||||
'/api/v1/scores/final?class_section_id=' . $classSectionId,
|
||||
'/api/v1/scores/participation?class_section_id=' . $classSectionId,
|
||||
'/api/v1/grading/homework-tracking?class_section_id=' . $classSectionId,
|
||||
'/api/v1/reports/report-cards/meta?class_section_id=' . $classSectionId,
|
||||
'/api/v1/scores/overview?class_section_id='.$classSectionId,
|
||||
'/api/v1/scores/homework?class_section_id='.$classSectionId,
|
||||
'/api/v1/scores/quiz?class_section_id='.$classSectionId,
|
||||
'/api/v1/scores/project?class_section_id='.$classSectionId,
|
||||
'/api/v1/scores/midterm?class_section_id='.$classSectionId,
|
||||
'/api/v1/scores/final?class_section_id='.$classSectionId,
|
||||
'/api/v1/scores/participation?class_section_id='.$classSectionId,
|
||||
'/api/v1/grading/homework-tracking?class_section_id='.$classSectionId,
|
||||
'/api/v1/reports/report-cards/meta?class_section_id='.$classSectionId,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -159,8 +159,8 @@ class ApiAcademicAttendanceFullSurfaceContractTest extends TestCase
|
||||
private function adminAcademicEndpoints(array $world): array
|
||||
{
|
||||
return [
|
||||
['GET', '/api/v1/reports/report-cards?student_id=' . $world['student_id'], []],
|
||||
['GET', '/api/v1/reports/stickers?student_id=' . $world['student_id'], []],
|
||||
['GET', '/api/v1/reports/report-cards?student_id='.$world['student_id'], []],
|
||||
['GET', '/api/v1/reports/stickers?student_id='.$world['student_id'], []],
|
||||
['GET', '/api/v1/badges/form-options', []],
|
||||
['POST', '/api/v1/print-requests', ['title' => 'Contract Probe', 'copies' => 1, 'needed_by' => '2025-10-12']],
|
||||
['GET', '/api/v1/print-requests/admin', []],
|
||||
|
||||
@@ -24,7 +24,7 @@ class ApiAccountStatusLifecycleContractTest extends FullSurfaceE2EContractCase
|
||||
$response = $this->requestAs($actor, 'GET', $uri);
|
||||
|
||||
$this->assertControlled($response, 'GET', $uri);
|
||||
$this->assertNoServerError($response, 'account status ' . $status . ' at GET ' . $uri);
|
||||
$this->assertNoServerError($response, 'account status '.$status.' at GET '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ use Tests\TestCase;
|
||||
*/
|
||||
class ApiAdminOperationsFullSurfaceContractTest extends TestCase
|
||||
{
|
||||
use AssertsE2EApiResponses;
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
use AssertsE2EApiResponses;
|
||||
|
||||
public function test_administrator_operational_surface_responds_with_controlled_contracts(): void
|
||||
{
|
||||
@@ -24,19 +24,19 @@ class ApiAdminOperationsFullSurfaceContractTest extends TestCase
|
||||
|
||||
foreach ($this->adminGetEndpoints($world) as $uri) {
|
||||
$response = $this->getJson($uri);
|
||||
$this->assertNoServerError($response, 'Admin GET ' . $uri);
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Admin GET ' . $uri);
|
||||
$this->assertNoServerError($response, 'Admin GET '.$uri);
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Admin GET '.$uri);
|
||||
}
|
||||
|
||||
foreach ($this->adminMutationEndpoints($world, $admin->id) as [$method, $uri, $payload]) {
|
||||
$response = $this->json($method, $uri, $payload);
|
||||
$this->assertNoServerError($response, 'Admin ' . $method . ' ' . $uri);
|
||||
$this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Admin ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'Admin '.$method.' '.$uri);
|
||||
$this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Admin '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $world
|
||||
* @param array<string, mixed> $world
|
||||
* @return list<string>
|
||||
*/
|
||||
private function adminGetEndpoints(array $world): array
|
||||
@@ -76,7 +76,7 @@ class ApiAdminOperationsFullSurfaceContractTest extends TestCase
|
||||
'/api/v1/administrator/flags/students/1',
|
||||
'/api/v1/administrator/flags/processed',
|
||||
'/api/v1/administrator/flags/analysis',
|
||||
'/api/v1/administrator/grading/homework-tracking?class_section_id=' . $classSectionId,
|
||||
'/api/v1/administrator/grading/homework-tracking?class_section_id='.$classSectionId,
|
||||
'/api/v1/administrator/expenses',
|
||||
'/api/v1/administrator/expenses/options',
|
||||
'/api/v1/administrator/expenses/1',
|
||||
@@ -110,13 +110,13 @@ class ApiAdminOperationsFullSurfaceContractTest extends TestCase
|
||||
'/api/v1/class-sections',
|
||||
'/api/v1/classes',
|
||||
'/api/v1/users',
|
||||
'/api/v1/users/' . $world['parent_id'],
|
||||
'/api/v1/students/' . $studentId,
|
||||
'/api/v1/users/'.$world['parent_id'],
|
||||
'/api/v1/students/'.$studentId,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $world
|
||||
* @param array<string, mixed> $world
|
||||
* @return list<array{0: string, 1: string, 2: array<string, mixed>}>
|
||||
*/
|
||||
private function adminMutationEndpoints(array $world, int $adminId): array
|
||||
|
||||
@@ -43,7 +43,7 @@ class ApiAttachmentUploadContentSafetyContractTest extends FullSurfaceE2EContrac
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $this->materializePath($route->uri()) . '?filename=../../.env&path=../../storage/logs/laravel.log';
|
||||
$path = $this->materializePath($route->uri()).'?filename=../../.env&path=../../storage/logs/laravel.log';
|
||||
$response = $this->actingAs($this->actorFor($route->uri()), 'api')->getJson($path);
|
||||
|
||||
$this->assertStatusIn($response, [200, 204, 400, 401, 403, 404, 409, 422], "GET {$route->uri()} path traversal probe");
|
||||
|
||||
+2
-2
@@ -21,8 +21,8 @@ class ApiAuditLogAndAdministrativeTraceabilityContractTest extends FullSurfaceE2
|
||||
|
||||
$this->assertNoServerError($response, "$method $uri admin mutation traceability");
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertStringNotContainsStringIgnoringCase('audit_log_id', $response->getContent(), $uri . ' should not leak raw internal audit identifiers by accident.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('stack_trace', $response->getContent(), $uri . ' should not leak stack traces in mutation responses.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('audit_log_id', $response->getContent(), $uri.' should not leak raw internal audit identifiers by accident.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('stack_trace', $response->getContent(), $uri.' should not leak stack traces in mutation responses.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,12 +20,12 @@ class ApiAuthenticationLifecycleDepthContractTest extends FullSurfaceE2EContract
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$this->assertNoServerError($response, 'valid login at ' . $path);
|
||||
$this->assertNoServerError($response, 'valid login at '.$path);
|
||||
|
||||
if ($response->getStatusCode() >= 200 && $response->getStatusCode() < 300) {
|
||||
$this->assertTrue(
|
||||
$response->json('token') !== null || $response->json('access_token') !== null || $response->json('user') !== null,
|
||||
$path . ' must return a usable login contract, not a mystery envelope.'
|
||||
$path.' must return a usable login contract, not a mystery envelope.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -36,18 +36,18 @@ class ApiAuthenticationLifecycleDepthContractTest extends FullSurfaceE2EContract
|
||||
'password' => 'definitely-wrong-password',
|
||||
]);
|
||||
|
||||
$this->assertStatusIn($response, [400, 401, 403, 419, 422], 'invalid login at ' . $path);
|
||||
$this->assertNoServerError($response, 'invalid login at ' . $path);
|
||||
$this->assertStatusIn($response, [400, 401, 403, 419, 422], 'invalid login at '.$path);
|
||||
$this->assertNoServerError($response, 'invalid login at '.$path);
|
||||
}
|
||||
|
||||
foreach (['/api/v1/auth/me', '/api/v1/me', '/api/user'] as $path) {
|
||||
$response = $this->actingAs($this->admin, 'api')->getJson($path);
|
||||
$this->assertNoServerError($response, 'authenticated identity lookup at ' . $path);
|
||||
$this->assertNoServerError($response, 'authenticated identity lookup at '.$path);
|
||||
|
||||
if ($response->getStatusCode() === 200) {
|
||||
$this->assertTrue(
|
||||
$response->json('user') !== null || $response->json('id') !== null || $response->json('data') !== null,
|
||||
$path . ' must expose a stable identity payload.'
|
||||
$path.' must expose a stable identity payload.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -56,10 +56,10 @@ class ApiAuthenticationLifecycleDepthContractTest extends FullSurfaceE2EContract
|
||||
$first = $this->actingAs($this->admin, 'api')->postJson($path);
|
||||
$second = $this->actingAs($this->admin, 'api')->postJson($path);
|
||||
|
||||
$this->assertNoServerError($first, 'first logout at ' . $path);
|
||||
$this->assertNoServerError($second, 'repeat logout at ' . $path);
|
||||
$this->assertStatusIn($first, [200, 202, 204, 302, 401, 404, 419, 422], 'first logout at ' . $path);
|
||||
$this->assertStatusIn($second, [200, 202, 204, 302, 401, 404, 419, 422], 'repeat logout at ' . $path);
|
||||
$this->assertNoServerError($first, 'first logout at '.$path);
|
||||
$this->assertNoServerError($second, 'repeat logout at '.$path);
|
||||
$this->assertStatusIn($first, [200, 202, 204, 302, 401, 404, 419, 422], 'first logout at '.$path);
|
||||
$this->assertStatusIn($second, [200, 202, 204, 302, 401, 404, 419, 422], 'repeat logout at '.$path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,16 +75,16 @@ class ApiAuthenticationLifecycleDepthContractTest extends FullSurfaceE2EContract
|
||||
|
||||
$uri = $route->uri();
|
||||
$response = $this->json($method, $this->materializePath($uri), [
|
||||
'email' => 'nobody.' . uniqid() . '@example.test',
|
||||
'email' => 'nobody.'.uniqid().'@example.test',
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'not-the-same',
|
||||
'token' => 'not-a-real-token',
|
||||
]);
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, $method . ' ' . $uri);
|
||||
$this->assertStringNotContainsStringIgnoringCase('select * from', $response->getContent(), $uri . ' must not leak account lookup SQL.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('No query results for model', $response->getContent(), $uri . ' must not leak framework model lookups.');
|
||||
$this->assertNoServerError($response, $method.' '.$uri);
|
||||
$this->assertStringNotContainsStringIgnoringCase('select * from', $response->getContent(), $uri.' must not leak account lookup SQL.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('No query results for model', $response->getContent(), $uri.' must not leak framework model lookups.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ class ApiAuthorizationCacheInvalidationContractTest extends FullSurfaceE2EContra
|
||||
|
||||
$response = $this->requestAs($this->parent, $method, $uri, $payload);
|
||||
|
||||
$this->assertStatusIn($response, [401, 403, 404, 405, 419, 422], 'parent privilege escalation should be blocked at ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'permission cache escalation probe at ' . $method . ' ' . $uri);
|
||||
$this->assertStatusIn($response, [401, 403, 404, 405, 419, 422], 'parent privilege escalation should be blocked at '.$method.' '.$uri);
|
||||
$this->assertNoServerError($response, 'permission cache escalation probe at '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class ApiBatch11RouteRegressionNetTest extends FullSurfaceE2EContractCase
|
||||
foreach ($families as $family => $needles) {
|
||||
$this->assertNotEmpty(
|
||||
$this->apiRoutesContainingAny($needles),
|
||||
'Batch 11 expects at least one route for the ' . $family . ' family. If this fails, update the route catalog instead of pretending the family vanished.'
|
||||
'Batch 11 expects at least one route for the '.$family.' family. If this fails, update the route catalog instead of pretending the family vanished.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ class ApiBatch11RouteRegressionNetTest extends FullSurfaceE2EContractCase
|
||||
])->json($method, $this->materializePath($uri), $payload);
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, 'batch 11 combined hostile probe at ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'batch 11 combined hostile probe at '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
namespace Tests\Feature\Api\V1\FullSurface;
|
||||
|
||||
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch12AttachmentLifecycleContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
+5
-3
@@ -6,7 +6,7 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14AcademicPromotionRollbackIntegrityContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_promotion_and_school_year_routes_reject_unsafe_rollback_payloads(): void
|
||||
public function test_promotion_and_school_year_routes_reject_unsafe_rollback_payloads(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['promotion', 'promote', 'school-year', 'semester', 'close', 'reopen', 'rollback']), 0, 45) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
@@ -22,10 +22,12 @@ public function test_promotion_and_school_year_routes_reject_unsafe_rollback_pay
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['promotion', 'school-year', 'close', 'reopen']), 0, 30) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { continue; }
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
|
||||
continue;
|
||||
}
|
||||
foreach ([$this->teacher, $this->parent, $this->studentUser] as $actor) {
|
||||
$response = $this->requestAs($actor, $method, $route->uri(), ['force' => true, 'confirmed' => true, 'school_year_id' => $this->ids['schoolYearId']]);
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri());
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method.' '.$route->uri());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -6,13 +6,15 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14AccountEnumerationAndIdentityPrivacyContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_identity_recovery_routes_do_not_reveal_whether_user_exists(): void
|
||||
public function test_identity_recovery_routes_do_not_reveal_whether_user_exists(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['forgot', 'password', 'reset', 'verify', 'verification', 'recovery']);
|
||||
foreach (array_slice($routes, 0, 30) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { continue; }
|
||||
foreach ([$this->admin->email, 'missing-user-' . uniqid() . '@example.test'] as $email) {
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
|
||||
continue;
|
||||
}
|
||||
foreach ([$this->admin->email, 'missing-user-'.uniqid().'@example.test'] as $email) {
|
||||
$response = $this->json($method, $this->materializePath($route->uri()), ['email' => $email, 'token' => 'invalid-token', 'password' => 'NewPassword123!', 'password_confirmation' => 'NewPassword123!']);
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
$body = strtolower($response->getContent());
|
||||
@@ -31,7 +33,7 @@ public function test_identity_recovery_routes_do_not_reveal_whether_user_exists(
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
$body = strtolower($response->getContent());
|
||||
foreach (['password', 'remember_token', 'two_factor', 'api_token', 'provider_token', 'secret'] as $forbidden) {
|
||||
$this->assertStringNotContainsString($forbidden, $body, $route->uri() . ' leaked identity internals.');
|
||||
$this->assertStringNotContainsString($forbidden, $body, $route->uri().' leaked identity internals.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -6,7 +6,7 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14AdminDelegationApprovalWorkflowContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_approval_and_delegation_routes_reject_forged_approval_metadata(): void
|
||||
public function test_approval_and_delegation_routes_reject_forged_approval_metadata(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['approve', 'approval', 'delegate', 'delegation', 'authorize', 'permission', 'role', 'impersonate']), 0, 50) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
@@ -20,10 +20,12 @@ public function test_approval_and_delegation_routes_reject_forged_approval_metad
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['approve', 'approval', 'delegate', 'permission', 'role']), 0, 35) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { continue; }
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
|
||||
continue;
|
||||
}
|
||||
foreach ([$this->teacher, $this->parent, $this->studentUser] as $actor) {
|
||||
$response = $this->requestAs($actor, $method, $route->uri(), ['approved' => true, 'permissions' => ['*'], 'role' => 'administrator', 'force' => true]);
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri());
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method.' '.$route->uri());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-6
@@ -6,15 +6,17 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14ApiConsumerContractCompatibilityTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_core_identity_contracts_remain_compatible_for_web_mobile_and_api_clients(): void
|
||||
public function test_core_identity_contracts_remain_compatible_for_web_mobile_and_api_clients(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['login', 'auth/me', 'me', 'frontend', 'profile']), 0, 25) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
foreach ([['Accept' => 'application/json', 'X-Client' => 'web'], ['Accept' => 'application/json', 'X-Client' => 'mobile', 'X-App-Version' => '1.0.0'], ['Accept' => 'application/vnd.alrahma.v1+json', 'X-Client' => 'api-consumer']] as $headers) {
|
||||
$response = $this->withHeaders($headers)->actingAs($this->actorFor($route->uri()), 'api')->json($method, $this->materializePath($route->uri()), $this->payloadFor($method, $route->uri()));
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
$this->assertJsonResponseWhenSuccessful($response, $method . ' ' . $route->uri());
|
||||
if ($response->isSuccessful()) { $this->assertStringNotContainsString('undefined index', strtolower($response->getContent())); }
|
||||
$this->assertJsonResponseWhenSuccessful($response, $method.' '.$route->uri());
|
||||
if ($response->isSuccessful()) {
|
||||
$this->assertStringNotContainsString('undefined index', strtolower($response->getContent()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,13 +25,15 @@ public function test_core_identity_contracts_remain_compatible_for_web_mobile_an
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['login', 'auth/login']), 0, 10) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if ($method !== 'POST') { continue; }
|
||||
if ($method !== 'POST') {
|
||||
continue;
|
||||
}
|
||||
$response = $this->json($method, $this->materializePath($route->uri()), ['email' => $this->admin->email, 'password' => 'password']);
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
if ($response->isSuccessful() && $this->isJsonString($response->getContent())) {
|
||||
$data = $response->json();
|
||||
$this->assertTrue(isset($data['token']) || isset($data['access_token']), $route->uri() . ' should expose token/access_token when login succeeds.');
|
||||
$this->assertArrayHasKey('user', $data, $route->uri() . ' should expose user when login succeeds.');
|
||||
$this->assertTrue(isset($data['token']) || isset($data['access_token']), $route->uri().' should expose token/access_token when login succeeds.');
|
||||
$this->assertArrayHasKey('user', $data, $route->uri().' should expose user when login succeeds.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -6,7 +6,7 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14AttendanceDeviceAndKioskAbuseContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_kiosk_scanner_and_attendance_routes_reject_forged_device_identity(): void
|
||||
public function test_kiosk_scanner_and_attendance_routes_reject_forged_device_identity(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['attendance', 'scanner', 'scan', 'kiosk', 'badge', 'dismissal', 'late']), 0, 55) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
@@ -22,10 +22,12 @@ public function test_kiosk_scanner_and_attendance_routes_reject_forged_device_id
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['attendance', 'scan', 'dismissal', 'late']), 0, 30) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { continue; }
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
|
||||
continue;
|
||||
}
|
||||
foreach ([$this->studentUser, $this->parent] as $actor) {
|
||||
$response = $this->requestAs($actor, $method, $route->uri(), ['student_id' => $this->ids['studentId'], 'status' => 'present', 'verified_by_device' => true, 'override_reason' => 'parent approved']);
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri());
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method.' '.$route->uri());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -6,7 +6,7 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14AuditTrailTamperResistanceContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_audit_and_log_routes_reject_client_supplied_actor_and_timestamp_fields(): void
|
||||
public function test_audit_and_log_routes_reject_client_supplied_actor_and_timestamp_fields(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['audit', 'log', 'activity', 'history', 'trace']), 0, 35) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
@@ -22,10 +22,12 @@ public function test_audit_and_log_routes_reject_client_supplied_actor_and_times
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['audit', 'log', 'history', 'activity']), 0, 25) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { continue; }
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
|
||||
continue;
|
||||
}
|
||||
foreach ([$this->parent, $this->teacher, $this->studentUser] as $actor) {
|
||||
$response = $this->requestAs($actor, $method, $route->uri(), ['action' => 'clear', 'purge' => true, 'rewrite' => true]);
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri());
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method.' '.$route->uri());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,13 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14BulkAssignmentCollisionContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_bulk_assignment_routes_handle_duplicate_and_conflicting_ids_cleanly(): void
|
||||
public function test_bulk_assignment_routes_handle_duplicate_and_conflicting_ids_cleanly(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['assign', 'assignment', 'enroll', 'bulk', 'batch', 'class', 'teacher', 'student']), 0, 50) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { continue; }
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
|
||||
continue;
|
||||
}
|
||||
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [
|
||||
'student_ids' => [$this->ids['studentId'], $this->ids['studentId'], 99999999], 'teacher_ids' => [$this->teacher->id, $this->teacher->id, $this->parent->id],
|
||||
'class_section_ids' => [$this->ids['classSectionId'], $this->ids['classSectionId'], 99999999], 'parent_ids' => [$this->ids['parentId'], $this->ids['parentId'], 99999999], 'force' => true, 'overwrite' => true,
|
||||
@@ -27,10 +29,12 @@ public function test_bulk_assignment_routes_handle_duplicate_and_conflicting_ids
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['assign', 'bulk', 'batch', 'class-sections', 'teacher-class', 'enroll']), 0, 30) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { continue; }
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
|
||||
continue;
|
||||
}
|
||||
foreach ([$this->parent, $this->studentUser] as $actor) {
|
||||
$response = $this->requestAs($actor, $method, $route->uri(), ['student_ids' => [$this->ids['studentId']], 'teacher_ids' => [$this->teacher->id], 'class_section_ids' => [$this->ids['classSectionId']], 'all' => true, 'force' => true]);
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri());
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method.' '.$route->uri());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14CrossModuleInvariantRiskRegisterTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_batch14_route_families_have_cross_module_risk_coverage(): void
|
||||
public function test_batch14_route_families_have_cross_module_risk_coverage(): void
|
||||
{
|
||||
$riskFamilies = [
|
||||
'session-cookie-boundary' => ['auth', 'login', 'frontend'],
|
||||
@@ -22,7 +22,7 @@ public function test_batch14_route_families_have_cross_module_risk_coverage(): v
|
||||
];
|
||||
|
||||
foreach ($riskFamilies as $name => $needles) {
|
||||
$this->assertNotEmpty($this->apiRoutesContainingAny($needles), 'Batch 14 expected route coverage for ' . $name);
|
||||
$this->assertNotEmpty($this->apiRoutesContainingAny($needles), 'Batch 14 expected route coverage for '.$name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public function test_batch14_route_families_have_cross_module_risk_coverage(): v
|
||||
$mutations = array_filter($this->apiRoutesContainingAny([$family]), function ($route): bool {
|
||||
return in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH', 'DELETE'], true);
|
||||
});
|
||||
$this->assertNotEmpty($mutations, 'Batch 14 expected mutating route family: ' . $family);
|
||||
$this->assertNotEmpty($mutations, 'Batch 14 expected mutating route family: '.$family);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-4
@@ -6,14 +6,15 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14DataLifecycleArchivePurgeContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_archive_purge_and_retention_routes_require_safe_controlled_requests(): void
|
||||
public function test_archive_purge_and_retention_routes_require_safe_controlled_requests(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['archive', 'restore', 'purge', 'erase', 'retention', 'delete', 'destroy']), 0, 50) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + ['ids' => [$this->ids['studentId'], 99999999, '../../.env'], 'hard_delete' => true, 'purge' => true, 'cascade' => true, 'include_audit_logs' => true, 'reason' => '', 'confirmed' => false]);
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
$body = strtolower($response->getContent());
|
||||
$this->assertStringNotContainsString('sqlstate', $body); $this->assertStringNotContainsString('foreign key constraint', $body);
|
||||
$this->assertStringNotContainsString('sqlstate', $body);
|
||||
$this->assertStringNotContainsString('foreign key constraint', $body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +22,12 @@ public function test_archive_purge_and_retention_routes_require_safe_controlled_
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['purge', 'erase', 'restore', 'archive', 'delete']), 0, 35) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { continue; }
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
|
||||
continue;
|
||||
}
|
||||
foreach ([$this->teacher, $this->parent, $this->studentUser] as $actor) {
|
||||
$response = $this->requestAs($actor, $method, $route->uri(), ['ids' => [$this->ids['studentId']], 'hard_delete' => true, 'purge' => true, 'global' => true]);
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri());
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method.' '.$route->uri());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-5
@@ -6,16 +6,18 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14FinanceReconciliationAndRefundAbuseContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_finance_reconciliation_routes_reject_mismatched_invoice_parent_and_payment_fields(): void
|
||||
public function test_finance_reconciliation_routes_reject_mismatched_invoice_parent_and_payment_fields(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['finance', 'invoice', 'payment', 'refund', 'transaction', 'ledger', 'reconcile']), 0, 55) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [
|
||||
'invoice_id' => $this->ids['invoiceId'], 'parent_id' => 99999999, 'student_id' => 99999999, 'payment_id' => 99999999, 'refund_id' => 99999999, 'transaction_id' => 'provider-' . uniqid(), 'provider_transaction_id' => 'provider-' . uniqid(), 'amount' => -99999.99, 'net_amount' => -99999.99, 'fee_amount' => -99999.99, 'currency' => 'XXX', 'reconciled' => true, 'force_posted' => true,
|
||||
'invoice_id' => $this->ids['invoiceId'], 'parent_id' => 99999999, 'student_id' => 99999999, 'payment_id' => 99999999, 'refund_id' => 99999999, 'transaction_id' => 'provider-'.uniqid(), 'provider_transaction_id' => 'provider-'.uniqid(), 'amount' => -99999.99, 'net_amount' => -99999.99, 'fee_amount' => -99999.99, 'currency' => 'XXX', 'reconciled' => true, 'force_posted' => true,
|
||||
]);
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
$body = strtolower($response->getContent());
|
||||
$this->assertStringNotContainsString('sqlstate', $body); $this->assertStringNotContainsString('duplicate entry', $body); $this->assertStringNotContainsString('foreign key constraint', $body);
|
||||
$this->assertStringNotContainsString('sqlstate', $body);
|
||||
$this->assertStringNotContainsString('duplicate entry', $body);
|
||||
$this->assertStringNotContainsString('foreign key constraint', $body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +25,11 @@ public function test_finance_reconciliation_routes_reject_mismatched_invoice_par
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['refund', 'reconcile', 'ledger', 'payment']), 0, 35) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { continue; }
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
|
||||
continue;
|
||||
}
|
||||
$response = $this->requestAs($this->parent, $method, $route->uri(), ['invoice_id' => $this->ids['invoiceId'], 'amount' => 100.00, 'approved' => true, 'reconciled' => true, 'refund_now' => true]);
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri());
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method.' '.$route->uri());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-5
@@ -6,17 +6,21 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14NotificationTemplateInjectionContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_notification_and_template_routes_reject_template_injection_payloads(): void
|
||||
public function test_notification_and_template_routes_reject_template_injection_payloads(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['notification', 'notify', 'message', 'email', 'whatsapp', 'template', 'broadcast']), 0, 55) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { continue; }
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
|
||||
continue;
|
||||
}
|
||||
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [
|
||||
'subject' => '{{ config("app.key") }}', 'message' => '<script>alert(document.cookie)</script>{{ $user->password }}', 'body' => '${jndi:ldap://127.0.0.1/a}', 'template' => '@php echo env("APP_KEY"); @endphp', 'channel' => 'all', 'recipients' => ['*'], 'phone' => "+15551234567\r\nBcc: attacker@example.test", 'email' => "victim@example.test\r\nBcc: attacker@example.test",
|
||||
]);
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
$body = strtolower($response->getContent());
|
||||
$this->assertStringNotContainsString('app_key', $body); $this->assertStringNotContainsString('document.cookie', $body); $this->assertStringNotContainsString('jndi:ldap', $body);
|
||||
$this->assertStringNotContainsString('app_key', $body);
|
||||
$this->assertStringNotContainsString('document.cookie', $body);
|
||||
$this->assertStringNotContainsString('jndi:ldap', $body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +28,12 @@ public function test_notification_and_template_routes_reject_template_injection_
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['broadcast', 'notification', 'email', 'whatsapp']), 0, 30) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { continue; }
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
|
||||
continue;
|
||||
}
|
||||
foreach ([$this->parent, $this->teacher, $this->studentUser] as $actor) {
|
||||
$response = $this->requestAs($actor, $method, $route->uri(), ['channel' => 'all', 'recipients' => ['all_school'], 'template_id' => 1, 'force_send' => true]);
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri());
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method.' '.$route->uri());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14PrintExportTamperingContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_print_export_and_document_generation_routes_reject_tampered_scope_fields(): void
|
||||
public function test_print_export_and_document_generation_routes_reject_tampered_scope_fields(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['print', 'export', 'download', 'certificate', 'badge', 'receipt', 'transcript', 'report-card']), 0, 50) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
@@ -15,7 +15,9 @@ public function test_print_export_and_document_generation_routes_reject_tampered
|
||||
]);
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
$body = strtolower($response->getContent());
|
||||
foreach (['app_key', 'laravel.log', 'password', 'remember_token', 'sqlstate'] as $forbidden) { $this->assertStringNotContainsString($forbidden, $body); }
|
||||
foreach (['app_key', 'laravel.log', 'password', 'remember_token', 'sqlstate'] as $forbidden) {
|
||||
$this->assertStringNotContainsString($forbidden, $body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +27,7 @@ public function test_print_export_and_document_generation_routes_reject_tampered
|
||||
$method = $this->primaryMethod($route);
|
||||
foreach ([$this->parent, $this->teacher, $this->studentUser] as $actor) {
|
||||
$response = $this->requestAs($actor, $method, $route->uri(), ['scope' => 'all_school', 'all_students' => true, 'include_finance' => true, 'include_guardians' => true, 'format' => 'csv']);
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri());
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], $method.' '.$route->uri());
|
||||
$this->assertStringNotContainsString('remember_token', strtolower($response->getContent()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14RegressionMegaSweepTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_batch14_mega_sweep_keeps_new_risk_payloads_under_controlled_failure(): void
|
||||
public function test_batch14_mega_sweep_keeps_new_risk_payloads_under_controlled_failure(): void
|
||||
{
|
||||
$families = [
|
||||
['auth', 'login', 'password', 'verify'], ['users', 'roles', 'permissions', 'profile'], ['students', 'parents', 'family', 'guardian'],
|
||||
@@ -26,7 +26,9 @@ public function test_batch14_mega_sweep_keeps_new_risk_payloads_under_controlled
|
||||
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $payload);
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
$body = strtolower($response->getContent());
|
||||
foreach (['sqlstate', 'app_key', 'db_password', 'remember_token', 'stack trace', 'laravel.log'] as $forbidden) { $this->assertStringNotContainsString($forbidden, $body); }
|
||||
foreach (['sqlstate', 'app_key', 'db_password', 'remember_token', 'stack trace', 'laravel.log'] as $forbidden) {
|
||||
$this->assertStringNotContainsString($forbidden, $body);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-4
@@ -6,15 +6,19 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14RelationshipOwnershipMatrixExpansionTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_relationship_owner_override_fields_are_not_trusted_across_route_families(): void
|
||||
public function test_relationship_owner_override_fields_are_not_trusted_across_route_families(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['student', 'parent', 'family', 'invoice', 'attendance', 'score', 'guardian', 'authorized']), 0, 60) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { continue; }
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
|
||||
continue;
|
||||
}
|
||||
foreach ([$this->parent, $this->teacher] as $actor) {
|
||||
$response = $this->requestAs($actor, $method, $route->uri(), $this->payloadFor($method, $route->uri()) + ['student_ids' => [$this->ids['studentId'], 99999999], 'parent_id' => 99999999, 'family_id' => 99999999, 'guardian_id' => 99999999, 'owner_id' => $this->admin->id, 'owner_type' => 'App\\Models\\User', 'created_by' => $this->admin->id, 'scope' => 'global']);
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
$body = strtolower($response->getContent()); $this->assertStringNotContainsString('sqlstate', $body); $this->assertStringNotContainsString('remember_token', $body);
|
||||
$body = strtolower($response->getContent());
|
||||
$this->assertStringNotContainsString('sqlstate', $body);
|
||||
$this->assertStringNotContainsString('remember_token', $body);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +28,7 @@ public function test_relationship_owner_override_fields_are_not_trusted_across_r
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['parent', 'family', 'guardian', 'invoice', 'attendance', 'report-card']), 0, 40) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
$response = $this->requestAs($this->studentUser, $method, $route->uri(), ['student_id' => 99999999, 'parent_id' => 99999999, 'family_id' => 99999999, 'include' => 'guardians,invoices,attendance,notes']);
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri());
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], $method.' '.$route->uri());
|
||||
$this->assertStringNotContainsString('remember_token', strtolower($response->getContent()));
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14SessionCookieCsrfBoundaryContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_api_auth_routes_do_not_depend_on_browser_session_cookies(): void
|
||||
public function test_api_auth_routes_do_not_depend_on_browser_session_cookies(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['auth', 'login', 'logout', 'me', 'profile', 'frontend']), 0, 40) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
@@ -26,7 +26,7 @@ public function test_api_auth_routes_do_not_depend_on_browser_session_cookies():
|
||||
$response = $this->withCookie('laravel_session', 'fake-admin-browser-session')
|
||||
->withHeaders(['X-Requested-With' => 'XMLHttpRequest', 'X-CSRF-TOKEN' => 'fake-csrf', 'X-User-Id' => (string) $this->admin->id])
|
||||
->json($method, $this->materializePath($route->uri()), $this->payloadFor($method, $route->uri()));
|
||||
$this->assertStatusIn($response, [401, 403, 404, 405, 419, 422], $method . ' ' . $route->uri());
|
||||
$this->assertStatusIn($response, [401, 403, 404, 405, 419, 422], $method.' '.$route->uri());
|
||||
$this->assertStringNotContainsString('sqlstate', strtolower($response->getContent()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,16 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14StoragePathAndSignedUrlContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_file_and_signed_url_routes_reject_path_and_disk_overrides(): void
|
||||
public function test_file_and_signed_url_routes_reject_path_and_disk_overrides(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['file', 'media', 'upload', 'download', 'export', 'import', 'attachment', 'document', 'signed']), 0, 55) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + ['path' => '../../.env', 'filename' => '../../storage/logs/laravel.log', 'disk' => 'local', 'storage_disk' => 's3://private-bucket', 'url' => 'file:///etc/passwd', 'expires' => '2099-01-01', 'signature' => 'forged-signature', 'mime_type' => 'text/x-php']);
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
$body = strtolower($response->getContent());
|
||||
foreach (['app_key', 'db_password', 'laravel.log', '/etc/passwd', 'begin rsa private key'] as $forbidden) { $this->assertStringNotContainsString($forbidden, $body); }
|
||||
foreach (['app_key', 'db_password', 'laravel.log', '/etc/passwd', 'begin rsa private key'] as $forbidden) {
|
||||
$this->assertStringNotContainsString($forbidden, $body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +23,8 @@ public function test_file_and_signed_url_routes_reject_path_and_disk_overrides()
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['download', 'signed', 'public', 'media', 'file']), 0, 35) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
$response = $this->json($method, $this->materializePath($route->uri()) . '?signature=forged&expires=1&path=../../.env');
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 410, 419, 422], $method . ' ' . $route->uri());
|
||||
$response = $this->json($method, $this->materializePath($route->uri()).'?signature=forged&expires=1&path=../../.env');
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 410, 419, 422], $method.' '.$route->uri());
|
||||
$this->assertStringNotContainsString('app_key', strtolower($response->getContent()));
|
||||
}
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15AdminImpersonationSessionBoundaryContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_impersonation_and_acting_as_routes_require_real_admin_context_not_payload_claims(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['impersonate', 'acting-as', 'act-as', 'switch-user', 'delegate', 'behalf']);
|
||||
@@ -28,5 +27,4 @@ class ApiBatch15AdminImpersonationSessionBoundaryContractTest extends FullSurfac
|
||||
$this->assertStringNotContainsString('access_token', strtolower($parentResponse->getContent()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15AuthTokenRotationAndRevocationContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_token_rotation_revocation_and_identity_routes_do_not_accept_stale_or_forged_tokens(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['auth', 'login', 'logout', 'refresh', 'token', 'me', 'profile', 'session']);
|
||||
@@ -46,5 +45,4 @@ class ApiBatch15AuthTokenRotationAndRevocationContractTest extends FullSurfaceE2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15BackgroundCommandAndSchedulerContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_command_scheduler_and_job_trigger_routes_are_admin_only_and_idempotent(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['command', 'schedule', 'scheduler', 'cron', 'job', 'queue', 'sync', 'recalculate', 'cleanup', 'send']);
|
||||
@@ -30,5 +29,4 @@ class ApiBatch15BackgroundCommandAndSchedulerContractTest extends FullSurfaceE2E
|
||||
$this->assertControlled($adminResponse, $method, $uri);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15ConsentPrivacyAndDataSubjectRightsContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_privacy_consent_erasure_and_export_routes_are_admin_or_owner_scoped(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['privacy', 'consent', 'erase', 'erasure', 'delete-account', 'export-data', 'data-subject', 'retention']);
|
||||
@@ -44,5 +43,4 @@ class ApiBatch15ConsentPrivacyAndDataSubjectRightsContractTest extends FullSurfa
|
||||
$this->assertStringNotContainsString('db_password', strtolower($response->getContent()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15CrossSchoolIdentityCollisionContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_duplicate_external_identifiers_and_cross_school_aliases_fail_cleanly(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['students', 'users', 'parents', 'family', 'guardian', 'enrollment', 'registration']);
|
||||
@@ -31,5 +30,4 @@ class ApiBatch15CrossSchoolIdentityCollisionContractTest extends FullSurfaceE2EC
|
||||
$this->assertStringNotContainsString('duplicate entry', strtolower($response->getContent()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15DataResidencyBackupAndRestoreContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_backup_restore_residency_and_snapshot_routes_do_not_expose_or_mutate_for_low_privilege_users(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['backup', 'restore', 'snapshot', 'residency', 'tenant', 'database', 'dump', 'archive']);
|
||||
@@ -32,5 +31,4 @@ class ApiBatch15DataResidencyBackupAndRestoreContractTest extends FullSurfaceE2E
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15DomainEventOutboxAndNotificationConsistencyContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_event_outbox_and_notification_trigger_routes_reject_forged_event_metadata(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['event', 'outbox', 'notification', 'broadcast', 'message', 'email', 'whatsapp', 'webhook']);
|
||||
@@ -28,5 +27,4 @@ class ApiBatch15DomainEventOutboxAndNotificationConsistencyContractTest extends
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15FeatureFlagExperimentAndRolloutContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_feature_flag_and_rollout_routes_reject_low_privilege_override_payloads(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['feature', 'flag', 'experiment', 'rollout', 'settings', 'configuration']);
|
||||
@@ -28,5 +27,4 @@ class ApiBatch15FeatureFlagExperimentAndRolloutContractTest extends FullSurfaceE
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15FormRequestAuthorizationCoverageContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_mutating_routes_do_not_allow_public_or_low_privilege_form_request_bypass(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutes(), 0, 120) as $route) {
|
||||
@@ -29,5 +28,4 @@ class ApiBatch15FormRequestAuthorizationCoverageContractTest extends FullSurface
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15GlobalSearchAndDirectoryEnumerationContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_global_search_directory_and_lookup_routes_resist_enumeration_inputs(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['search', 'lookup', 'directory', 'autocomplete', 'users', 'students', 'parents']);
|
||||
@@ -27,5 +26,4 @@ class ApiBatch15GlobalSearchAndDirectoryEnumerationContractTest extends FullSurf
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15LocalizationContentDirectionAndEncodingContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_rtl_unicode_and_encoding_payloads_remain_controlled_across_text_routes(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['message', 'contact', 'support', 'notification', 'student', 'parent', 'settings', 'certificate']);
|
||||
@@ -31,5 +30,4 @@ class ApiBatch15LocalizationContentDirectionAndEncodingContractTest extends Full
|
||||
$this->assertStringNotContainsString('malformed utf-8', strtolower($response->getContent()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15MetadataFilteringAndSparseFieldsetSecurityContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_sparse_fieldsets_metadata_and_embeds_do_not_reveal_sensitive_associations(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['users', 'students', 'parents', 'family', 'finance', 'attendance', 'reports', 'dashboard']);
|
||||
@@ -31,5 +30,4 @@ class ApiBatch15MetadataFilteringAndSparseFieldsetSecurityContractTest extends F
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15RateLimitAndAbusePatternContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_repeated_sensitive_requests_return_controlled_responses_without_leaking_internals(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['login', 'password', 'verify', 'contact', 'support', 'message', 'scanner', 'badge']);
|
||||
@@ -16,7 +15,7 @@ class ApiBatch15RateLimitAndAbusePatternContractTest extends FullSurfaceE2EContr
|
||||
$uri = $route->uri();
|
||||
|
||||
for ($attempt = 1; $attempt <= 3; $attempt++) {
|
||||
$response = $this->withHeader('X-Forwarded-For', '203.0.113.' . $attempt)
|
||||
$response = $this->withHeader('X-Forwarded-For', '203.0.113.'.$attempt)
|
||||
->requestAs($this->actorFor($uri), $method, $uri, $this->payloadFor($method, $uri) + [
|
||||
'email' => 'rate-limit-'.$attempt.'@example.test',
|
||||
'password' => 'wrong-password',
|
||||
@@ -31,5 +30,4 @@ class ApiBatch15RateLimitAndAbusePatternContractTest extends FullSurfaceE2EContr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15RegressionUltraSweepTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_ultra_sweep_keeps_cross_domain_hostile_payloads_under_controlled_responses(): void
|
||||
{
|
||||
$families = [
|
||||
@@ -43,5 +42,4 @@ class ApiBatch15RegressionUltraSweepTest extends FullSurfaceE2EContractCase
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15ReportDrilldownAndAggregatePrivacyContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_report_drilldown_filters_do_not_allow_scope_escape_or_sensitive_field_inclusion(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['report', 'analytics', 'dashboard', 'summary', 'drilldown', 'export']);
|
||||
@@ -34,5 +33,4 @@ class ApiBatch15ReportDrilldownAndAggregatePrivacyContractTest extends FullSurfa
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15RouteParameterPoisoningExpansionTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_route_parameters_reject_encoded_path_script_and_type_confusion_payloads_cleanly(): void
|
||||
{
|
||||
$poisoned = ['..%2F..%2F.env', '%00', '<svg/onload=alert(1)>', '1 or 1=1', str_repeat('9', 80), '٠١٢٣', 'null', '[]'];
|
||||
@@ -18,7 +17,7 @@ class ApiBatch15RouteParameterPoisoningExpansionTest extends FullSurfaceE2EContr
|
||||
|
||||
$method = $this->primaryMethod($route);
|
||||
foreach (array_slice($poisoned, 0, 4) as $value) {
|
||||
$path = '/' . preg_replace('/\{[^}]+\}/', $value, $route->uri());
|
||||
$path = '/'.preg_replace('/\{[^}]+\}/', $value, $route->uri());
|
||||
$this->actingAs($this->actorFor($route->uri()), 'api');
|
||||
$response = $this->json($method, $path, $this->payloadFor($method, $route->uri()));
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
@@ -26,5 +25,4 @@ class ApiBatch15RouteParameterPoisoningExpansionTest extends FullSurfaceE2EContr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15StudentSafetyIncidentEscalationContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_safety_incident_escalation_routes_are_not_mutable_by_parents_or_students(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['incident', 'violation', 'safety', 'alert', 'flag', 'dismissal', 'late', 'absence', 'attendance']);
|
||||
@@ -29,5 +28,4 @@ class ApiBatch15StudentSafetyIncidentEscalationContractTest extends FullSurfaceE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class ApiBusinessRuleEdgeCaseContractTest extends FullSurfaceE2EContractCase
|
||||
$response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload);
|
||||
$this->assertNoServerError($response, "$method $uri mismatched academic context");
|
||||
$this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 409, 419, 422], "$method $uri mismatched academic context");
|
||||
$this->assertStringNotContainsStringIgnoringCase('foreign key constraint', $response->getContent(), $uri . ' should validate academic context before DB failure leaks.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('foreign key constraint', $response->getContent(), $uri.' should validate academic context before DB failure leaks.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +51,8 @@ class ApiBusinessRuleEdgeCaseContractTest extends FullSurfaceE2EContractCase
|
||||
|
||||
$response = $this->requestAs($this->admin, $method, $uri, $payload);
|
||||
$this->assertNoServerError($response, "$method $uri amount $amount");
|
||||
$this->assertStringNotContainsStringIgnoringCase('decimal', $response->getContent(), $uri . ' should not leak decimal cast internals.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('SQLSTATE', $response->getContent(), $uri . ' should not leak SQLSTATE for financial edge cases.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('decimal', $response->getContent(), $uri.' should not leak decimal cast internals.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('SQLSTATE', $response->getContent(), $uri.' should not leak SQLSTATE for financial edge cases.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ class ApiCalendarScheduleConflictContractTest extends FullSurfaceE2EContractCase
|
||||
$query = '?from=1900-01-01&to=2100-12-31&scope=all&include_private=true';
|
||||
|
||||
foreach (['parent' => $this->parent, 'teacher' => $this->teacher, 'student' => $this->studentUser] as $label => $actor) {
|
||||
$response = $this->requestAs($actor, 'GET', $uri . $query);
|
||||
$response = $this->requestAs($actor, 'GET', $uri.$query);
|
||||
$this->assertNoServerError($response, "$label GET $uri calendar scope");
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 409, 419, 422], "$label GET $uri calendar scope");
|
||||
$this->assertStringNotContainsStringIgnoringCase('private_note', $response->getContent(), "$uri should not expose private event notes to $label.");
|
||||
|
||||
@@ -18,6 +18,7 @@ class ApiClientCompatibilityVersioningContractTest extends FullSurfaceE2EContrac
|
||||
$uri = $route->uri();
|
||||
if (str_starts_with($uri, 'api/v1/')) {
|
||||
$this->assertTrue(true);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -31,7 +32,7 @@ class ApiClientCompatibilityVersioningContractTest extends FullSurfaceE2EContrac
|
||||
|
||||
$this->assertTrue(
|
||||
$isAllowed,
|
||||
$uri . ' is an unversioned API route. If this is deliberate legacy support, add it to the explicit allow-list.'
|
||||
$uri.' is an unversioned API route. If this is deliberate legacy support, add it to the explicit allow-list.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -45,8 +46,8 @@ class ApiClientCompatibilityVersioningContractTest extends FullSurfaceE2EContrac
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = $name . '|' . $this->primaryMethod($route);
|
||||
$this->assertArrayNotHasKey($key, $seen, 'Duplicate API route name/method pair: ' . $key . ' for ' . $route->uri() . ' and ' . ($seen[$key] ?? 'unknown'));
|
||||
$key = $name.'|'.$this->primaryMethod($route);
|
||||
$this->assertArrayNotHasKey($key, $seen, 'Duplicate API route name/method pair: '.$key.' for '.$route->uri().' and '.($seen[$key] ?? 'unknown'));
|
||||
$seen[$key] = $route->uri();
|
||||
}
|
||||
}
|
||||
@@ -62,10 +63,10 @@ class ApiClientCompatibilityVersioningContractTest extends FullSurfaceE2EContrac
|
||||
$legacyResponse = $this->postJson($legacy, ['email' => 'bad@example.test', 'password' => 'wrong']);
|
||||
$currentResponse = $this->postJson($current, ['email' => 'bad@example.test', 'password' => 'wrong']);
|
||||
|
||||
$this->assertNoServerError($legacyResponse, $legacy . ' legacy auth failure');
|
||||
$this->assertNoServerError($currentResponse, $current . ' current auth failure');
|
||||
$this->assertStatusIn($legacyResponse, [400, 401, 403, 419, 422], $legacy . ' legacy auth failure');
|
||||
$this->assertStatusIn($currentResponse, [400, 401, 403, 419, 422], $current . ' current auth failure');
|
||||
$this->assertNoServerError($legacyResponse, $legacy.' legacy auth failure');
|
||||
$this->assertNoServerError($currentResponse, $current.' current auth failure');
|
||||
$this->assertStatusIn($legacyResponse, [400, 401, 403, 419, 422], $legacy.' legacy auth failure');
|
||||
$this->assertStatusIn($currentResponse, [400, 401, 403, 419, 422], $current.' current auth failure');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class ApiConcurrencyAndStaleWriteContractTest extends FullSurfaceE2EContractCase
|
||||
}
|
||||
|
||||
$payload = $this->payloadFor($method, $route->uri()) + [
|
||||
'client_request_id' => 'e2e-concurrency-' . md5($route->uri()),
|
||||
'client_request_id' => 'e2e-concurrency-'.md5($route->uri()),
|
||||
'updated_at' => '2000-01-01T00:00:00Z',
|
||||
];
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class ApiContractCoverageHeatmapExpansionTest extends FullSurfaceE2EContractCase
|
||||
foreach ($families as $family => $needles) {
|
||||
$matches = array_filter($routes, fn (LaravelRoute $route): bool => $this->uriContainsAny($route->uri(), $needles));
|
||||
|
||||
$this->assertNotEmpty($matches, $family . ' must have route-level E2E contract coverage candidates.');
|
||||
$this->assertNotEmpty($matches, $family.' must have route-level E2E contract coverage candidates.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,11 +53,11 @@ class ApiContractCoverageHeatmapExpansionTest extends FullSurfaceE2EContractCase
|
||||
}
|
||||
|
||||
if (! $this->uriContainsAny($uri, $ownedNeedles)) {
|
||||
$unowned[] = $method . ' ' . $uri;
|
||||
$unowned[] = $method.' '.$uri;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $unowned, 'Mutating API routes must be assigned to a full-surface use-case contract bucket: ' . implode(', ', $unowned));
|
||||
$this->assertSame([], $unowned, 'Mutating API routes must be assigned to a full-surface use-case contract bucket: '.implode(', ', $unowned));
|
||||
}
|
||||
|
||||
/** @param list<string> $needles */
|
||||
|
||||
@@ -20,11 +20,12 @@ class ApiContractSnapshotSeedTest extends FullSurfaceE2EContractCase
|
||||
foreach ($sample['expected_any'] as $key) {
|
||||
if (str_contains($flat, $key)) {
|
||||
$this->assertTrue(true);
|
||||
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
$this->fail($label . ' did not expose any expected contract key: ' . implode(', ', $sample['expected_any']));
|
||||
$this->fail($label.' did not expose any expected contract key: '.implode(', ', $sample['expected_any']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ class ApiCrossVersionRouteCompatibilityMatrixTest extends FullSurfaceE2EContract
|
||||
? $this->postJson($legacy, ['email' => 'bad@example.test', 'password' => 'wrong'])
|
||||
: $this->actingAs($this->admin, 'api')->getJson($legacy);
|
||||
|
||||
$this->assertNoServerError($versionedResponse, 'versioned compatibility ' . $versioned);
|
||||
$this->assertNoServerError($legacyResponse, 'legacy compatibility ' . $legacy);
|
||||
$this->assertNoServerError($versionedResponse, 'versioned compatibility '.$versioned);
|
||||
$this->assertNoServerError($legacyResponse, 'legacy compatibility '.$legacy);
|
||||
$this->assertLessThan(500, $versionedResponse->getStatusCode());
|
||||
$this->assertLessThan(500, $legacyResponse->getStatusCode());
|
||||
}
|
||||
@@ -37,10 +37,10 @@ class ApiCrossVersionRouteCompatibilityMatrixTest extends FullSurfaceE2EContract
|
||||
foreach ($this->apiRoutes() as $route) {
|
||||
$uri = $route->uri();
|
||||
if (! str_starts_with($uri, 'api/v1/') && ! str_contains($uri, 'docs') && ! str_contains($uri, 'health')) {
|
||||
$legacyLike[] = $this->primaryMethod($route) . ' ' . $uri;
|
||||
$legacyLike[] = $this->primaryMethod($route).' '.$uri;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertLessThan(80, count($legacyLike), 'Unversioned API compatibility routes should remain bounded, not become a second API by accident: ' . implode(', ', $legacyLike));
|
||||
$this->assertLessThan(80, count($legacyLike), 'Unversioned API compatibility routes should remain bounded, not become a second API by accident: '.implode(', ', $legacyLike));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class ApiCsvExcelFormulaInjectionContractTest extends FullSurfaceE2EContractCase
|
||||
$response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload);
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, 'CSV formula payload at ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'CSV formula payload at '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class ApiDataExportLeastPrivilegeContractTest extends FullSurfaceE2EContractCase
|
||||
}
|
||||
|
||||
$uri = $route->uri();
|
||||
$response = $this->requestAs($actor, $method, $uri . '?all=true&include_sensitive=true&format=csv');
|
||||
$response = $this->requestAs($actor, $method, $uri.'?all=true&include_sensitive=true&format=csv');
|
||||
|
||||
$this->assertNoServerError($response, "$label GET $uri export");
|
||||
$this->assertStatusIn($response, [200, 202, 204, 302, 400, 401, 403, 404, 409, 419, 422], "$label GET $uri export");
|
||||
@@ -52,7 +52,7 @@ class ApiDataExportLeastPrivilegeContractTest extends FullSurfaceE2EContractCase
|
||||
$query = '?parent_id=999999&student_id=999999&class_section_id=999999&scope=all&include=financial,medical,contacts';
|
||||
|
||||
foreach (['parent' => $this->parent, 'teacher' => $this->teacher] as $label => $actor) {
|
||||
$response = $this->requestAs($actor, 'GET', $uri . $query);
|
||||
$response = $this->requestAs($actor, 'GET', $uri.$query);
|
||||
$this->assertNoServerError($response, "$label GET $uri scoped export");
|
||||
$this->assertStatusIn($response, [200, 202, 204, 302, 400, 401, 403, 404, 409, 419, 422], "$label GET $uri scoped export");
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@ class ApiDataMinimizationByRoleContractTest extends FullSurfaceE2EContractCase
|
||||
foreach ([$this->parent, $this->studentUser] as $actor) {
|
||||
$response = $this->requestAs($actor, 'GET', $uri);
|
||||
$this->assertControlled($response, 'GET', $uri);
|
||||
$this->assertNoServerError($response, 'data minimization at GET ' . $uri);
|
||||
$this->assertNoServerError($response, 'data minimization at GET '.$uri);
|
||||
|
||||
if ($response->getStatusCode() >= 200 && $response->getStatusCode() < 300) {
|
||||
foreach ($forbidden as $field) {
|
||||
$this->assertStringNotContainsString('"' . $field . '"', $response->getContent(), 'Low-privilege response leaked ' . $field . ' at GET ' . $uri);
|
||||
$this->assertStringNotContainsString('"'.$field.'"', $response->getContent(), 'Low-privilege response leaked '.$field.' at GET '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class ApiDataShapeBackwardCompatibilityContractTest extends FullSurfaceE2EContra
|
||||
$response = $this->actingAs($this->actorFor($uri), 'api')->getJson($this->materializePath($uri));
|
||||
|
||||
$this->assertControlled($response, 'GET', $uri);
|
||||
$this->assertNoServerError($response, 'collection envelope ' . $uri);
|
||||
$this->assertNoServerError($response, 'collection envelope '.$uri);
|
||||
|
||||
if ($response->getStatusCode() !== 200 || ! $this->isJsonString($response->getContent())) {
|
||||
continue;
|
||||
@@ -31,7 +31,7 @@ class ApiDataShapeBackwardCompatibilityContractTest extends FullSurfaceE2EContra
|
||||
|| array_key_exists('message', $payload)
|
||||
|| array_key_exists('status', $payload)
|
||||
),
|
||||
$uri . ' should keep a predictable collection/read envelope.'
|
||||
$uri.' should keep a predictable collection/read envelope.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ class ApiDataShapeBackwardCompatibilityContractTest extends FullSurfaceE2EContra
|
||||
$response = $this->actingAs($this->actorFor($uri), 'api')->getJson($this->materializePath($uri));
|
||||
|
||||
$this->assertControlled($response, 'GET', $uri);
|
||||
$this->assertNoServerError($response, 'detail envelope ' . $uri);
|
||||
$this->assertNoServerError($response, 'detail envelope '.$uri);
|
||||
|
||||
if ($response->getStatusCode() !== 200 || ! $this->isJsonString($response->getContent())) {
|
||||
continue;
|
||||
@@ -54,7 +54,7 @@ class ApiDataShapeBackwardCompatibilityContractTest extends FullSurfaceE2EContra
|
||||
$payload = $response->json();
|
||||
$this->assertTrue(
|
||||
data_get($payload, 'id') !== null || data_get($payload, 'data.id') !== null || data_get($payload, 'message') !== null || data_get($payload, 'status') !== null,
|
||||
$uri . ' should keep an identifier, data wrapper, or explicit status/message contract.'
|
||||
$uri.' should keep an identifier, data wrapper, or explicit status/message contract.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class ApiDatabaseTransactionRollbackContractTest extends FullSurfaceE2EContractC
|
||||
$beforeStudents = DB::table('students')->count();
|
||||
|
||||
$payload = $this->payloadFor($method, $uri) + [
|
||||
'email' => 'rollback.' . uniqid() . '@example.test',
|
||||
'email' => 'rollback.'.uniqid().'@example.test',
|
||||
'firstname' => 'Rollback',
|
||||
'lastname' => 'Probe',
|
||||
'parent_id' => 999999,
|
||||
|
||||
@@ -16,11 +16,11 @@ class ApiDeepAuthorizationIsolationMatrixTest extends FullSurfaceE2EContractCase
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if (! in_array($status, [401, 403, 404, 405, 419, 422], true)) {
|
||||
$failures[] = "$label allowed cross-portal access with $status: " . $response->getContent();
|
||||
$failures[] = "$label allowed cross-portal access with $status: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Cross-role authorization isolation failed:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Cross-role authorization isolation failed:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
public function test_owner_scoped_parent_routes_do_not_expose_another_family_student(): void
|
||||
@@ -42,11 +42,11 @@ class ApiDeepAuthorizationIsolationMatrixTest extends FullSurfaceE2EContractCase
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if (! in_array($status, [401, 403, 404, 405, 419, 422], true)) {
|
||||
$failures[] = "$method $uri leaked cross-family access with $status: " . $response->getContent();
|
||||
$failures[] = "$method $uri leaked cross-family access with $status: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Parent owner-scope boundaries failed:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Parent owner-scope boundaries failed:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
/** @return array<string, array{0: User, 1: string, 2: string}> */
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Tests\Feature\Api\V1\FullSurface;
|
||||
|
||||
use Illuminate\Routing\Route as LaravelRoute;
|
||||
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiDeepDocumentationAndRouteParityContractTest extends FullSurfaceE2EContractCase
|
||||
@@ -22,10 +21,10 @@ class ApiDeepDocumentationAndRouteParityContractTest extends FullSurfaceE2EContr
|
||||
continue;
|
||||
}
|
||||
|
||||
$unnamed[] = $this->primaryMethod($route) . ' ' . $uri;
|
||||
$unnamed[] = $this->primaryMethod($route).' '.$uri;
|
||||
}
|
||||
|
||||
$this->assertSame([], $unnamed, "API routes should be nameable for docs, client generation, and redirects. Unnamed routes:\n" . implode("\n", $unnamed));
|
||||
$this->assertSame([], $unnamed, "API routes should be nameable for docs, client generation, and redirects. Unnamed routes:\n".implode("\n", $unnamed));
|
||||
}
|
||||
|
||||
public function test_openapi_or_docs_catalog_endpoint_is_reachable_and_route_table_remains_documentable(): void
|
||||
@@ -42,11 +41,11 @@ class ApiDeepDocumentationAndRouteParityContractTest extends FullSurfaceE2EContr
|
||||
$response = $this->requestAs($this->admin, $method, $uri);
|
||||
|
||||
if ($response->getStatusCode() >= 500) {
|
||||
$failures[] = "$method $uri docs endpoint crashed: " . $response->getContent();
|
||||
$failures[] = "$method $uri docs endpoint crashed: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Documentation endpoints must not be decorative rubble:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Documentation endpoints must not be decorative rubble:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
public function test_route_use_case_catalog_covers_new_deep_contract_domains(): void
|
||||
|
||||
+6
-5
@@ -20,16 +20,17 @@ class ApiDeepFileImportExportAndPrintableContractTest extends FullSurfaceE2ECont
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if ($status >= 500) {
|
||||
$failures[] = "$method $uri crashed when generating downloadable/printable output: " . $response->getContent();
|
||||
$failures[] = "$method $uri crashed when generating downloadable/printable output: ".$response->getContent();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! in_array($status, [200, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], true)) {
|
||||
$failures[] = "$method $uri returned unexpected $status for downloadable/printable output: " . $response->getContent();
|
||||
$failures[] = "$method $uri returned unexpected $status for downloadable/printable output: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Download/export/print routes must fail cleanly when fixtures are thin:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Download/export/print routes must fail cleanly when fixtures are thin:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
public function test_import_upload_routes_accept_test_files_or_return_validation_without_server_errors(): void
|
||||
@@ -49,11 +50,11 @@ class ApiDeepFileImportExportAndPrintableContractTest extends FullSurfaceE2ECont
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if ($status >= 500) {
|
||||
$failures[] = "$method $uri crashed on upload/import probe: " . $response->getContent();
|
||||
$failures[] = "$method $uri crashed on upload/import probe: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Upload/import routes must validate file input, not panic:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Upload/import routes must validate file input, not panic:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
/** @return list<LaravelRoute> */
|
||||
|
||||
+7
-7
@@ -15,8 +15,8 @@ class ApiDeepIdempotencyAndDuplicateSubmissionContractTest extends FullSurfaceE2
|
||||
$method = $this->primaryMethod($route);
|
||||
$uri = $route->uri();
|
||||
$payload = $this->payloadFor($method, $uri) + [
|
||||
'external_reference' => 'E2E-DUP-' . md5($uri),
|
||||
'idempotency_key' => 'E2E-DUP-' . md5($uri),
|
||||
'external_reference' => 'E2E-DUP-'.md5($uri),
|
||||
'idempotency_key' => 'E2E-DUP-'.md5($uri),
|
||||
];
|
||||
|
||||
$first = $this->requestAs($this->actorFor($uri), $method, $uri, $payload);
|
||||
@@ -26,16 +26,16 @@ class ApiDeepIdempotencyAndDuplicateSubmissionContractTest extends FullSurfaceE2
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if ($status >= 500) {
|
||||
$failures[] = "$method $uri $label duplicate request crashed with $status: " . $response->getContent();
|
||||
$failures[] = "$method $uri $label duplicate request crashed with $status: ".$response->getContent();
|
||||
}
|
||||
|
||||
if (! in_array($status, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], true)) {
|
||||
$failures[] = "$method $uri $label duplicate request returned unexpected $status: " . $response->getContent();
|
||||
$failures[] = "$method $uri $label duplicate request returned unexpected $status: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Duplicate submissions need controlled behavior, because users double-click like it's a sport:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Duplicate submissions need controlled behavior, because users double-click like it's a sport:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
public function test_repeat_delete_or_archive_requests_are_safe_to_retry_or_fail_cleanly(): void
|
||||
@@ -49,12 +49,12 @@ class ApiDeepIdempotencyAndDuplicateSubmissionContractTest extends FullSurfaceE2
|
||||
|
||||
foreach (['first' => $first, 'second' => $second] as $label => $response) {
|
||||
if ($response->getStatusCode() >= 500) {
|
||||
$failures[] = "DELETE $uri $label retry crashed: " . $response->getContent();
|
||||
$failures[] = "DELETE $uri $label retry crashed: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Retrying destructive operations must not crash:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Retrying destructive operations must not crash:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
/** @return list<LaravelRoute> */
|
||||
|
||||
@@ -14,7 +14,7 @@ class ApiDeepPaginationSearchAndFilterContractTest extends FullSurfaceE2EContrac
|
||||
foreach ($this->listRoutes() as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
$uri = $route->uri();
|
||||
$path = $this->materializePath($uri) . '?' . http_build_query([
|
||||
$path = $this->materializePath($uri).'?'.http_build_query([
|
||||
'page' => 1,
|
||||
'per_page' => 10,
|
||||
'search' => 'e2e',
|
||||
@@ -30,16 +30,17 @@ class ApiDeepPaginationSearchAndFilterContractTest extends FullSurfaceE2EContrac
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if ($status >= 500) {
|
||||
$failures[] = "$method $uri crashed with list filters: " . $response->getContent();
|
||||
$failures[] = "$method $uri crashed with list filters: ".$response->getContent();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! in_array($status, [200, 204, 302, 400, 401, 403, 404, 405, 409, 422], true)) {
|
||||
$failures[] = "$method $uri returned unexpected $status with list filters: " . $response->getContent();
|
||||
$failures[] = "$method $uri returned unexpected $status with list filters: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "List endpoints must not crash when clients send boring grid params:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "List endpoints must not crash when clients send boring grid params:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
public function test_date_range_filters_fail_cleanly_when_dates_are_invalid(): void
|
||||
@@ -48,7 +49,7 @@ class ApiDeepPaginationSearchAndFilterContractTest extends FullSurfaceE2EContrac
|
||||
|
||||
foreach ($this->dateFilterRoutes() as $route) {
|
||||
$uri = $route->uri();
|
||||
$path = $this->materializePath($uri) . '?' . http_build_query([
|
||||
$path = $this->materializePath($uri).'?'.http_build_query([
|
||||
'from' => 'not-a-date',
|
||||
'to' => 'still-not-a-date',
|
||||
'start_date' => '2025-99-99',
|
||||
@@ -59,11 +60,11 @@ class ApiDeepPaginationSearchAndFilterContractTest extends FullSurfaceE2EContrac
|
||||
$response = $this->json('GET', $path);
|
||||
|
||||
if ($response->getStatusCode() >= 500) {
|
||||
$failures[] = "GET $uri crashed on invalid date filters: " . $response->getContent();
|
||||
$failures[] = "GET $uri crashed on invalid date filters: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Invalid date filters should be validation, not detonation:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Invalid date filters should be validation, not detonation:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
/** @return list<LaravelRoute> */
|
||||
|
||||
@@ -18,16 +18,17 @@ class ApiDeepResponseShapeAndEnvelopeContractTest extends FullSurfaceE2EContract
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if ($status >= 500) {
|
||||
$failures[] = "$method $uri crashed with $status: " . $response->getContent();
|
||||
$failures[] = "$method $uri crashed with $status: ".$response->getContent();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($status >= 200 && $status < 300 && $response->getContent() !== '' && ! $this->isJsonString($response->getContent())) {
|
||||
$failures[] = "$method $uri returned non-json successful API response: " . $response->getContent();
|
||||
$failures[] = "$method $uri returned non-json successful API response: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Successful API responses should be machine-readable JSON:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Successful API responses should be machine-readable JSON:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
public function test_collection_endpoints_expose_list_like_payloads_or_explicit_empty_state(): void
|
||||
@@ -46,7 +47,8 @@ class ApiDeepResponseShapeAndEnvelopeContractTest extends FullSurfaceE2EContract
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if (! in_array($status, [200, 204, 302, 401, 403, 404, 422], true)) {
|
||||
$failures[] = "$method $uri returned unexpected $status for collection contract: " . $response->getContent();
|
||||
$failures[] = "$method $uri returned unexpected $status for collection contract: ".$response->getContent();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -62,12 +64,12 @@ class ApiDeepResponseShapeAndEnvelopeContractTest extends FullSurfaceE2EContract
|
||||
|| array_key_exists('status', $payload));
|
||||
|
||||
if (! $hasCollectionShape) {
|
||||
$failures[] = "$method $uri returned JSON but no recognizable collection/envelope keys: " . $response->getContent();
|
||||
$failures[] = "$method $uri returned JSON but no recognizable collection/envelope keys: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Collection routes need a stable list/envelope shape:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Collection routes need a stable list/envelope shape:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
/** @return list<LaravelRoute> */
|
||||
|
||||
@@ -63,15 +63,16 @@ class ApiDeepStateTransitionInvariantContractTest extends FullSurfaceE2EContract
|
||||
$response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload);
|
||||
|
||||
if ($response->getStatusCode() >= 500) {
|
||||
$failures[] = "$method $uri crashed during $domain transition probe: " . $response->getContent();
|
||||
$failures[] = "$method $uri crashed during $domain transition probe: ".$response->getContent();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! in_array($response->getStatusCode(), $this->controlledStatuses($method), true)) {
|
||||
$failures[] = "$method $uri returned unexpected {$response->getStatusCode()} during $domain transition probe: " . $response->getContent();
|
||||
$failures[] = "$method $uri returned unexpected {$response->getStatusCode()} during $domain transition probe: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, ucfirst($domain) . " state transitions must be controlled:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, ucfirst($domain)." state transitions must be controlled:\n".implode("\n", $failures));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,17 +20,18 @@ class ApiDeepValidationFailureContractTest extends FullSurfaceE2EContractCase
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if ($status >= 500) {
|
||||
$failures[] = "$method $uri crashed on $label hostile payload with $status: " . $response->getContent();
|
||||
$failures[] = "$method $uri crashed on $label hostile payload with $status: ".$response->getContent();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! in_array($status, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], true)) {
|
||||
$failures[] = "$method $uri returned unexpected $status on $label hostile payload: " . $response->getContent();
|
||||
$failures[] = "$method $uri returned unexpected $status on $label hostile payload: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Hostile payloads must fail cleanly, not leak framework explosions:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Hostile payloads must fail cleanly, not leak framework explosions:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
public function test_required_field_validation_uses_machine_readable_error_shape_when_validation_fails(): void
|
||||
@@ -47,17 +48,18 @@ class ApiDeepValidationFailureContractTest extends FullSurfaceE2EContractCase
|
||||
}
|
||||
|
||||
if (! $this->isJsonString($response->getContent())) {
|
||||
$failures[] = "$method $uri returned 422 but not JSON: " . $response->getContent();
|
||||
$failures[] = "$method $uri returned 422 but not JSON: ".$response->getContent();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$json = $response->json();
|
||||
if (! is_array($json) || (! array_key_exists('errors', $json) && ! array_key_exists('message', $json))) {
|
||||
$failures[] = "$method $uri returned 422 without message/errors: " . $response->getContent();
|
||||
$failures[] = "$method $uri returned 422 without message/errors: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Validation errors must be usable by clients, not poetic prose:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Validation errors must be usable by clients, not poetic prose:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
/** @return list<LaravelRoute> */
|
||||
|
||||
@@ -10,7 +10,7 @@ class ApiEmptyDatasetAndNullStateContractTest extends FullSurfaceE2EContractCase
|
||||
public function test_list_endpoints_return_stable_empty_shapes_when_filters_match_nothing(): void
|
||||
{
|
||||
foreach ($this->listPaths() as $path) {
|
||||
$response = $this->requestAs($this->actorFor($path), 'GET', $path . '?search=__no_match_contract_probe__&page=1&per_page=5');
|
||||
$response = $this->requestAs($this->actorFor($path), 'GET', $path.'?search=__no_match_contract_probe__&page=1&per_page=5');
|
||||
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 419, 422], "empty list $path");
|
||||
$this->assertNoServerError($response, "empty list $path");
|
||||
|
||||
@@ -22,7 +22,7 @@ class ApiEnumAndStateMachineContractTest extends FullSurfaceE2EContractCase
|
||||
$response = $this->requestAs($this->actorFor($uri), $method, $uri, $this->payloadFor($method, $uri) + ['status' => $state, 'state' => $state]);
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, 'unknown status ' . $state . ' at ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'unknown status '.$state.' at '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,8 +37,8 @@ class ApiEnumAndStateMachineContractTest extends FullSurfaceE2EContractCase
|
||||
|
||||
$response = $this->requestAs($this->parent, $method, $uri, $this->payloadFor($method, $uri));
|
||||
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], 'low privilege terminal state transition ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'low privilege terminal state transition ' . $method . ' ' . $uri);
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], 'low privilege terminal state transition '.$method.' '.$uri);
|
||||
$this->assertNoServerError($response, 'low privilege terminal state transition '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class ApiEtagConditionalMutationContractTest extends FullSurfaceE2EContractCase
|
||||
])->json($method, $this->materializePath($uri), $this->payloadFor($method, $uri));
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, 'stale conditional mutation at ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'stale conditional mutation at '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class ApiFailureModeResilienceExpansionTest extends FullSurfaceE2EContractCase
|
||||
$response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload);
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, 'null relationship probe for ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'null relationship probe for '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,16 +36,16 @@ class ApiFailureModeResilienceExpansionTest extends FullSurfaceE2EContractCase
|
||||
$method = $this->primaryMethod($route);
|
||||
$uri = $route->uri();
|
||||
$payload = $this->payloadFor($method, $uri) + [
|
||||
'student_id' => 'student-' . $this->ids['studentId'],
|
||||
'parent_id' => 'parent-' . $this->ids['parentId'],
|
||||
'class_section_id' => 'class-' . $this->ids['classSectionId'],
|
||||
'invoice_id' => 'invoice-' . $this->ids['invoiceId'],
|
||||
'student_id' => 'student-'.$this->ids['studentId'],
|
||||
'parent_id' => 'parent-'.$this->ids['parentId'],
|
||||
'class_section_id' => 'class-'.$this->ids['classSectionId'],
|
||||
'invoice_id' => 'invoice-'.$this->ids['invoiceId'],
|
||||
];
|
||||
|
||||
$response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload);
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, 'string relationship probe for ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'string relationship probe for '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ use Tests\TestCase;
|
||||
*/
|
||||
class ApiFamilyPortalFullSurfaceContractTest extends TestCase
|
||||
{
|
||||
use AssertsE2EApiResponses;
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
use AssertsE2EApiResponses;
|
||||
|
||||
public function test_family_portal_surface_has_controlled_contracts_and_privacy_boundaries(): void
|
||||
{
|
||||
@@ -25,15 +25,15 @@ class ApiFamilyPortalFullSurfaceContractTest extends TestCase
|
||||
|
||||
foreach ($this->parentEndpoints($world) as [$method, $uri, $payload]) {
|
||||
$response = $this->json($method, $uri, $payload);
|
||||
$this->assertNoServerError($response, 'Parent ' . $method . ' ' . $uri);
|
||||
$this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Parent ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'Parent '.$method.' '.$uri);
|
||||
$this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Parent '.$method.' '.$uri);
|
||||
}
|
||||
|
||||
$this->actingAs($otherParent, 'api');
|
||||
foreach ($this->wrongFamilyBoundaryEndpoints($world) as [$method, $uri, $payload]) {
|
||||
$response = $this->json($method, $uri, $payload);
|
||||
$this->assertNoServerError($response, 'Wrong family ' . $method . ' ' . $uri);
|
||||
$this->assertStatusIn($response, [401, 403, 404, 405, 409, 419, 422], 'Wrong family ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'Wrong family '.$method.' '.$uri);
|
||||
$this->assertStatusIn($response, [401, 403, 404, 405, 409, 419, 422], 'Wrong family '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,10 +47,10 @@ class ApiFamilyPortalFullSurfaceContractTest extends TestCase
|
||||
['GET', '/api/v1/parents/profile', []],
|
||||
['PATCH', '/api/v1/parents/profile', ['firstname' => 'Parent', 'lastname' => 'Updated']],
|
||||
['GET', '/api/v1/parents/students', []],
|
||||
['GET', '/api/v1/parents/students/' . $studentId, []],
|
||||
['PATCH', '/api/v1/parents/students/' . $studentId, ['firstname' => 'Kai', 'lastname' => 'Updated', 'dob' => '2014-09-01', 'gender' => 'Male']],
|
||||
['GET', '/api/v1/parents/students/'.$studentId, []],
|
||||
['PATCH', '/api/v1/parents/students/'.$studentId, ['firstname' => 'Kai', 'lastname' => 'Updated', 'dob' => '2014-09-01', 'gender' => 'Male']],
|
||||
['GET', '/api/v1/parents/attendance', []],
|
||||
['GET', '/api/v1/parents/attendance/student/' . $studentId, []],
|
||||
['GET', '/api/v1/parents/attendance/student/'.$studentId, []],
|
||||
['GET', '/api/v1/parents/invoices', []],
|
||||
['GET', '/api/v1/parents/enrollments', []],
|
||||
['GET', '/api/v1/parents/promotions', []],
|
||||
@@ -62,9 +62,9 @@ class ApiFamilyPortalFullSurfaceContractTest extends TestCase
|
||||
['GET', '/api/v1/emergency-contacts/1', []],
|
||||
['PATCH', '/api/v1/emergency-contacts/1', ['name' => 'Emergency Updated', 'phone' => '5557654321']],
|
||||
['DELETE', '/api/v1/emergency-contacts/1', []],
|
||||
['GET', '/api/v1/family-admin/card?student_id=' . $studentId, []],
|
||||
['GET', '/api/v1/family-admin/card?student_id='.$studentId, []],
|
||||
['POST', '/api/v1/family-admin/compose-email', ['parent_id' => $parentId, 'subject' => 'Contract probe', 'message' => 'Body']],
|
||||
['GET', '/api/v1/families/by-student/' . $studentId, []],
|
||||
['GET', '/api/v1/families/by-student/'.$studentId, []],
|
||||
['GET', '/api/v1/families/1/guardians', []],
|
||||
['POST', '/api/v1/families/bootstrap', ['student_id' => $studentId, 'parent_id' => $parentId]],
|
||||
['POST', '/api/v1/families/attach-second/by-user', ['student_id' => $studentId, 'user_id' => $parentId]],
|
||||
@@ -88,9 +88,9 @@ class ApiFamilyPortalFullSurfaceContractTest extends TestCase
|
||||
$studentId = (int) $world['student_id'];
|
||||
|
||||
return [
|
||||
['GET', '/api/v1/parents/students/' . $studentId, []],
|
||||
['PATCH', '/api/v1/parents/students/' . $studentId, ['firstname' => 'Hijacked', 'lastname' => 'Student']],
|
||||
['GET', '/api/v1/parents/attendance/student/' . $studentId, []],
|
||||
['GET', '/api/v1/parents/students/'.$studentId, []],
|
||||
['PATCH', '/api/v1/parents/students/'.$studentId, ['firstname' => 'Hijacked', 'lastname' => 'Student']],
|
||||
['GET', '/api/v1/parents/attendance/student/'.$studentId, []],
|
||||
['POST', '/api/v1/emergency-contacts', ['student_id' => $studentId, 'name' => 'Wrong Family', 'phone' => '5550000000']],
|
||||
['POST', '/api/v1/authorized-users', ['student_id' => $studentId, 'firstname' => 'Wrong', 'lastname' => 'Family', 'email' => 'wrong@example.test']],
|
||||
];
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ class ApiFeatureFlagAndConfigurationDriftContractTest extends FullSurfaceE2ECont
|
||||
|
||||
$this->assertNoServerError($response, "$method $uri missing config");
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertStringNotContainsStringIgnoringCase('Trying to access array offset', $response->getContent(), $uri . ' should not assume configuration rows exist.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('Trying to access array offset', $response->getContent(), $uri.' should not assume configuration rows exist.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-13
@@ -12,9 +12,9 @@ use Tests\TestCase;
|
||||
*/
|
||||
class ApiFinanceInventoryCommunicationsFullSurfaceContractTest extends TestCase
|
||||
{
|
||||
use AssertsE2EApiResponses;
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
use AssertsE2EApiResponses;
|
||||
|
||||
public function test_back_office_and_communications_surfaces_have_controlled_contracts(): void
|
||||
{
|
||||
@@ -25,13 +25,13 @@ class ApiFinanceInventoryCommunicationsFullSurfaceContractTest extends TestCase
|
||||
|
||||
foreach ($this->endpoints($world, $invoiceId) as [$method, $uri, $payload]) {
|
||||
$response = $this->json($method, $uri, $payload);
|
||||
$this->assertNoServerError($response, $method . ' ' . $uri);
|
||||
$this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, $method.' '.$uri);
|
||||
$this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], $method.' '.$uri);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $world
|
||||
* @param array<string, mixed> $world
|
||||
* @return list<array{0: string, 1: string, 2: array<string, mixed>}>
|
||||
*/
|
||||
private function endpoints(array $world, int $invoiceId): array
|
||||
@@ -45,10 +45,10 @@ class ApiFinanceInventoryCommunicationsFullSurfaceContractTest extends TestCase
|
||||
['GET', '/api/v1/finance/financial-summary', []],
|
||||
['GET', '/api/v1/finance/stakeholder-analysis', []],
|
||||
['GET', '/api/v1/finance/parent-payment-followups', []],
|
||||
['POST', '/api/v1/finance/parent-payment-followups/' . $parentId . '/note', ['note' => 'Contract probe']],
|
||||
['POST', '/api/v1/finance/parent-payment-followups/' . $parentId . '/mark-contacted', ['contacted_at' => '2025-10-05']],
|
||||
['POST', '/api/v1/finance/parent-payment-followups/' . $parentId . '/promise-to-pay', ['amount' => 25, 'promise_date' => '2025-10-15']],
|
||||
['POST', '/api/v1/finance/parent-payment-followups/' . $parentId . '/resolve', ['resolution' => 'resolved']],
|
||||
['POST', '/api/v1/finance/parent-payment-followups/'.$parentId.'/note', ['note' => 'Contract probe']],
|
||||
['POST', '/api/v1/finance/parent-payment-followups/'.$parentId.'/mark-contacted', ['contacted_at' => '2025-10-05']],
|
||||
['POST', '/api/v1/finance/parent-payment-followups/'.$parentId.'/promise-to-pay', ['amount' => 25, 'promise_date' => '2025-10-15']],
|
||||
['POST', '/api/v1/finance/parent-payment-followups/'.$parentId.'/resolve', ['resolution' => 'resolved']],
|
||||
['GET', '/api/v1/finance/carryforwards/preview', []],
|
||||
['POST', '/api/v1/finance/carryforwards/draft', ['parent_id' => $parentId, 'amount' => 25]],
|
||||
['POST', '/api/v1/finance/carryforwards/1/approve', ['approved' => true]],
|
||||
@@ -66,7 +66,7 @@ class ApiFinanceInventoryCommunicationsFullSurfaceContractTest extends TestCase
|
||||
['POST', '/api/v1/finance/event-charges/1/attach-to-invoice', ['invoice_id' => $invoiceId]],
|
||||
['GET', '/api/v1/finance/installment-plans', []],
|
||||
['GET', '/api/v1/finance/installment-plans/1', []],
|
||||
['POST', '/api/v1/finance/invoices/' . $invoiceId . '/installment-plans', ['amount' => 100, 'installments' => 2]],
|
||||
['POST', '/api/v1/finance/invoices/'.$invoiceId.'/installment-plans', ['amount' => 100, 'installments' => 2]],
|
||||
['POST', '/api/v1/finance/installment-plans/1/activate', ['activate' => true]],
|
||||
['POST', '/api/v1/finance/installment-plans/1/cancel', ['reason' => 'Contract probe']],
|
||||
['POST', '/api/v1/finance/installment-plans/1/restructure', ['installments' => 3]],
|
||||
@@ -75,8 +75,8 @@ class ApiFinanceInventoryCommunicationsFullSurfaceContractTest extends TestCase
|
||||
['GET', '/api/v1/finance/installments/overdue', []],
|
||||
['POST', '/api/v1/finance/payments/1/send-receipt', ['email' => 'receipt@example.test']],
|
||||
['POST', '/api/v1/finance/refunds/1/send-receipt', ['email' => 'refund@example.test']],
|
||||
['POST', '/api/v1/finance/invoices/' . $invoiceId . '/send-statement', ['email' => 'statement@example.test']],
|
||||
['POST', '/api/v1/finance/parents/' . $parentId . '/send-overdue-reminder', ['email' => 'overdue@example.test']],
|
||||
['POST', '/api/v1/finance/invoices/'.$invoiceId.'/send-statement', ['email' => 'statement@example.test']],
|
||||
['POST', '/api/v1/finance/parents/'.$parentId.'/send-overdue-reminder', ['email' => 'overdue@example.test']],
|
||||
['POST', '/api/v1/finance/installments/1/send-reminder', ['email' => 'installment@example.test']],
|
||||
['GET', '/api/v1/finance/notification-logs', []],
|
||||
['GET', '/api/v1/finance/unpaid-parents', []],
|
||||
@@ -93,7 +93,7 @@ class ApiFinanceInventoryCommunicationsFullSurfaceContractTest extends TestCase
|
||||
['POST', '/api/v1/finance/fees/charges/event/1/mark-paid', ['amount' => 25]],
|
||||
['GET', '/api/v1/finance/refunds', []],
|
||||
['POST', '/api/v1/finance/refunds/recalculate-overpayments', ['parent_id' => $parentId]],
|
||||
['GET', '/api/v1/finance/refunds/parent-balances/' . $parentId, []],
|
||||
['GET', '/api/v1/finance/refunds/parent-balances/'.$parentId, []],
|
||||
['POST', '/api/v1/finance/refunds', ['parent_id' => $parentId, 'amount' => 25, 'reason' => 'Contract probe']],
|
||||
['GET', '/api/v1/finance/refunds/1', []],
|
||||
['PATCH', '/api/v1/finance/refunds/1', ['amount' => 20]],
|
||||
@@ -104,7 +104,7 @@ class ApiFinanceInventoryCommunicationsFullSurfaceContractTest extends TestCase
|
||||
['GET', '/api/v1/inventory/categories', []],
|
||||
['POST', '/api/v1/inventory/categories', ['name' => 'Contract Category']],
|
||||
['GET', '/api/v1/inventory/items', []],
|
||||
['POST', '/api/v1/inventory/items', ['name' => 'Contract Item', 'sku' => 'E2E-' . uniqid(), 'quantity' => 2]],
|
||||
['POST', '/api/v1/inventory/items', ['name' => 'Contract Item', 'sku' => 'E2E-'.uniqid(), 'quantity' => 2]],
|
||||
['GET', '/api/v1/inventory/items/1', []],
|
||||
['PATCH', '/api/v1/inventory/items/1', ['name' => 'Contract Item Updated']],
|
||||
['DELETE', '/api/v1/inventory/items/1', []],
|
||||
|
||||
@@ -13,14 +13,14 @@ class ApiFrontendBackendContractParityTest extends FullSurfaceE2EContractCase
|
||||
foreach ($paths as $path) {
|
||||
$response = $this->actingAs($this->admin, 'api')->getJson($path);
|
||||
|
||||
$this->assertNoServerError($response, 'identity contract at ' . $path);
|
||||
$this->assertNoServerError($response, 'identity contract at '.$path);
|
||||
|
||||
if ($response->getStatusCode() === 200) {
|
||||
$body = $response->json();
|
||||
$this->assertIsArray($body, $path . ' should return an inspectable JSON identity payload.');
|
||||
$this->assertIsArray($body, $path.' should return an inspectable JSON identity payload.');
|
||||
$this->assertTrue(
|
||||
data_get($body, 'user.email') !== null || data_get($body, 'email') !== null || data_get($body, 'data.email') !== null,
|
||||
$path . ' should expose a stable user email for frontend identity hydration.'
|
||||
$path.' should expose a stable user email for frontend identity hydration.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -36,11 +36,11 @@ class ApiFrontendBackendContractParityTest extends FullSurfaceE2EContractCase
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$this->assertNoServerError($response, 'login alias at ' . $path);
|
||||
$this->assertNoServerError($response, 'login alias at '.$path);
|
||||
|
||||
if ($response->getStatusCode() >= 200 && $response->getStatusCode() < 300) {
|
||||
$this->assertTrue($response->json('token') !== null || $response->json('access_token') !== null, $path . ' must expose a token field.');
|
||||
$this->assertTrue($response->json('user') !== null || $response->json('data.user') !== null, $path . ' must expose a user object.');
|
||||
$this->assertTrue($response->json('token') !== null || $response->json('access_token') !== null, $path.' must expose a token field.');
|
||||
$this->assertTrue($response->json('user') !== null || $response->json('data.user') !== null, $path.' must expose a user object.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ class ApiFrontendBackendContractParityTest extends FullSurfaceE2EContractCase
|
||||
$response = $this->actingAs($this->parent, 'api')->getJson($this->materializePath($uri));
|
||||
|
||||
$this->assertControlled($response, 'GET', $uri);
|
||||
$this->assertNoServerError($response, 'frontend bootstrap route at ' . $uri);
|
||||
$this->assertNoServerError($response, 'frontend bootstrap route at '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ use Tests\TestCase;
|
||||
*/
|
||||
class ApiFullSurfaceEndToEndContractTest extends TestCase
|
||||
{
|
||||
use AssertsE2EApiResponses;
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
use AssertsE2EApiResponses;
|
||||
|
||||
/**
|
||||
* @var array<string, int|string>
|
||||
@@ -32,8 +32,11 @@ class ApiFullSurfaceEndToEndContractTest extends TestCase
|
||||
private array $ids = [];
|
||||
|
||||
private User $admin;
|
||||
|
||||
private User $teacher;
|
||||
|
||||
private User $parent;
|
||||
|
||||
private User $studentUser;
|
||||
|
||||
protected function setUp(): void
|
||||
@@ -95,7 +98,7 @@ class ApiFullSurfaceEndToEndContractTest extends TestCase
|
||||
|
||||
foreach ($this->probeableApiRoutes() as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
$uri = '/' . $this->materializeUri($route->uri());
|
||||
$uri = '/'.$this->materializeUri($route->uri());
|
||||
$actor = $this->actorFor($route->uri());
|
||||
$payload = $this->payloadFor($method, $route->uri());
|
||||
|
||||
@@ -104,16 +107,17 @@ class ApiFullSurfaceEndToEndContractTest extends TestCase
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if ($status >= 500) {
|
||||
$failures[] = "$method $uri crashed with $status: " . $response->getContent();
|
||||
$failures[] = "$method $uri crashed with $status: ".$response->getContent();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! in_array($status, $this->allowedContractStatuses($method), true)) {
|
||||
$failures[] = "$method $uri returned unexpected $status: " . $response->getContent();
|
||||
$failures[] = "$method $uri returned unexpected $status: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Full-surface API contract failures:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Full-surface API contract failures:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
public function test_all_mutating_api_routes_reject_empty_or_malformed_payloads_without_server_errors(): void
|
||||
@@ -126,17 +130,17 @@ class ApiFullSurfaceEndToEndContractTest extends TestCase
|
||||
continue;
|
||||
}
|
||||
|
||||
$uri = '/' . $this->materializeUri($route->uri());
|
||||
$uri = '/'.$this->materializeUri($route->uri());
|
||||
$this->actingAs($this->actorFor($route->uri()), 'api');
|
||||
$response = $this->json($method, $uri, []);
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if ($status >= 500) {
|
||||
$failures[] = "$method $uri malformed payload crashed with $status: " . $response->getContent();
|
||||
$failures[] = "$method $uri malformed payload crashed with $status: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Malformed-payload probes must fail cleanly, not explode:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Malformed-payload probes must fail cleanly, not explode:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
public function test_portal_routes_enforce_wrong_actor_boundaries_across_the_full_surface(): void
|
||||
@@ -149,11 +153,11 @@ class ApiFullSurfaceEndToEndContractTest extends TestCase
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if (! in_array($status, [401, 403, 404, 405, 419, 422], true)) {
|
||||
$failures[] = "$method $uri allowed wrong actor with $status: " . $response->getContent();
|
||||
$failures[] = "$method $uri allowed wrong actor with $status: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Wrong-role portal boundary failures:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Wrong-role portal boundary failures:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,13 +233,14 @@ class ApiFullSurfaceEndToEndContractTest extends TestCase
|
||||
{
|
||||
return preg_replace_callback('/\{([^}:]+)(?:\?)?[^}]*\}/', function (array $matches): string {
|
||||
$key = $matches[1];
|
||||
|
||||
return (string) ($this->ids[$key] ?? $this->ids['id']);
|
||||
}, $uri) ?? $uri;
|
||||
}
|
||||
|
||||
private function materializePath(string $path): string
|
||||
{
|
||||
return '/' . ltrim($this->materializeUri(ltrim($path, '/')), '/');
|
||||
return '/'.ltrim($this->materializeUri(ltrim($path, '/')), '/');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -309,7 +314,7 @@ class ApiFullSurfaceEndToEndContractTest extends TestCase
|
||||
if (str_contains($uri, 'inventory') || str_contains($uri, 'suppliers') || str_contains($uri, 'supply')) {
|
||||
return $base + [
|
||||
'name' => 'Contract Probe Item',
|
||||
'sku' => 'E2E-' . uniqid(),
|
||||
'sku' => 'E2E-'.uniqid(),
|
||||
'quantity' => 3,
|
||||
'unit_cost' => 5.25,
|
||||
];
|
||||
|
||||
@@ -26,7 +26,7 @@ class ApiHtmlJavaScriptEscapingContractTest extends FullSurfaceE2EContractCase
|
||||
$response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload);
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, 'HTML/script payload at ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'HTML/script payload at '.$method.' '.$uri);
|
||||
$this->assertStringNotContainsString('<script>alert("e2e")</script>', $response->getContent());
|
||||
$this->assertStringNotContainsString('onerror=alert(1)', $response->getContent());
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class ApiHttpSemanticsAndMethodSafetyContractTest extends FullSurfaceE2EContract
|
||||
]);
|
||||
|
||||
$this->assertControlled($response, 'GET', $uri);
|
||||
$this->assertNoServerError($response, 'GET with mutating-looking query at ' . $uri);
|
||||
$this->assertNoServerError($response, 'GET with mutating-looking query at '.$uri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ class ApiHttpSemanticsAndMethodSafetyContractTest extends FullSurfaceE2EContract
|
||||
|
||||
$response = $this->actingAs($this->actorFor($uri), 'api')->json($wrong, $this->materializePath($uri), $this->payloadFor($wrong, $uri));
|
||||
|
||||
$this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], $wrong . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'wrong method probe ' . $wrong . ' ' . $uri);
|
||||
$this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], $wrong.' '.$uri);
|
||||
$this->assertNoServerError($response, 'wrong method probe '.$wrong.' '.$uri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class ApiHttpSemanticsAndMethodSafetyContractTest extends FullSurfaceE2EContract
|
||||
$response = $this->actingAs($this->actorFor($uri), 'api')->deleteJson($this->materializePath($uri));
|
||||
|
||||
$this->assertControlled($response, 'DELETE', $uri);
|
||||
$this->assertNoServerError($response, 'bodyless delete at ' . $uri);
|
||||
$this->assertNoServerError($response, 'bodyless delete at '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class ApiIdempotencyScopeAndReplayContractTest extends FullSurfaceE2EContractCas
|
||||
|
||||
$this->assertControlled($first, $method, $uri);
|
||||
$this->assertControlled($second, $method, $uri);
|
||||
$this->assertNoServerError($second, 'idempotency replay at ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($second, 'idempotency replay at '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class ApiIdempotencyScopeAndReplayContractTest extends FullSurfaceE2EContractCas
|
||||
$response = $this->withHeaders(['Idempotency-Key' => "bad\r\nX-Injected: yes"])->json('POST', $this->materializePath($uri), $this->payloadFor('POST', $uri));
|
||||
|
||||
$this->assertControlled($response, 'POST', $uri);
|
||||
$this->assertNoServerError($response, 'malformed idempotency key at POST ' . $uri);
|
||||
$this->assertNoServerError($response, 'malformed idempotency key at POST '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class ApiImportTemplateAndCsvContractTest extends FullSurfaceE2EContractCase
|
||||
$response = $this->actingAs($this->actorFor($uri), 'api')->json($method, $this->materializePath($uri), $this->payloadFor($method, $uri) + ['file' => $file]);
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, 'malformed csv import at ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'malformed csv import at '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,9 +39,9 @@ class ApiImportTemplateAndCsvContractTest extends FullSurfaceE2EContractCase
|
||||
$public = $this->getJson($this->materializePath($uri));
|
||||
$authenticated = $this->actingAs($this->actorFor($uri), 'api')->getJson($this->materializePath($uri));
|
||||
|
||||
$this->assertStatusIn($public, [200, 204, 302, 400, 401, 403, 404, 405, 419, 422], 'public template download ' . $uri);
|
||||
$this->assertStatusIn($public, [200, 204, 302, 400, 401, 403, 404, 405, 419, 422], 'public template download '.$uri);
|
||||
$this->assertControlled($authenticated, 'GET', $uri);
|
||||
$this->assertNoServerError($authenticated, 'authenticated template download ' . $uri);
|
||||
$this->assertNoServerError($authenticated, 'authenticated template download '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ class ApiMigrationSafeDefaultContractTest extends FullSurfaceE2EContractCase
|
||||
$response = $this->actingAs($this->actorFor($uri), 'api')->getJson($this->materializePath($uri));
|
||||
|
||||
$this->assertControlled($response, 'GET', $uri);
|
||||
$this->assertNoServerError($response, 'missing optional configuration rows at ' . $uri);
|
||||
$this->assertNoServerError($response, 'missing optional configuration rows at '.$uri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class ApiMigrationSafeDefaultContractTest extends FullSurfaceE2EContractCase
|
||||
$response = $this->requestAs($this->actorFor($uri), $method, $uri, $this->payloadFor($method, $uri));
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, 'school year context resilience at ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'school year context resilience at '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class ApiMobileClientToleranceContractTest extends FullSurfaceE2EContractCase
|
||||
])->json($method, $this->materializePath($uri), $this->payloadFor($method, $uri));
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, 'mobile headers at ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'mobile headers at '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ class ApiMobileClientToleranceContractTest extends FullSurfaceE2EContractCase
|
||||
|
||||
$this->assertControlled($first, $method, $uri);
|
||||
$this->assertControlled($second, $method, $uri);
|
||||
$this->assertNoServerError($second, 'mobile retry at ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($second, 'mobile retry at '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ class ApiNotificationMessagingDeliveryContractTest extends FullSurfaceE2EContrac
|
||||
$response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload);
|
||||
$this->assertNoServerError($response, "$method $uri bad delivery inputs");
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertStringNotContainsStringIgnoringCase('Swift', $response->getContent(), $uri . ' should not leak mail transport internals.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('SMTP', $response->getContent(), $uri . ' should not leak SMTP internals.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('Swift', $response->getContent(), $uri.' should not leak mail transport internals.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('SMTP', $response->getContent(), $uri.' should not leak SMTP internals.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class ApiNullBooleanAndTypeCoercionContractTest extends FullSurfaceE2EContractCa
|
||||
$response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload);
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, 'type coercion probe at ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'type coercion probe at '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class ApiOpenRedirectAndReturnUrlContractTest extends FullSurfaceE2EContractCase
|
||||
$response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload);
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, 'open redirect probe at ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'open redirect probe at '.$method.' '.$uri);
|
||||
$this->assertStringNotContainsString('evil.example', (string) $response->headers->get('Location'));
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ class ApiOpenRedirectAndReturnUrlContractTest extends FullSurfaceE2EContractCase
|
||||
]);
|
||||
|
||||
$this->assertControlled($response, 'GET', $uri);
|
||||
$this->assertNoServerError($response, 'redirect query probe at GET ' . $uri);
|
||||
$this->assertNoServerError($response, 'redirect query probe at GET '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class ApiOperationalEndpointExposureContractTest extends FullSurfaceE2EContractC
|
||||
$response = $this->json($method, $this->materializePath($uri), $this->payloadFor($method, $uri));
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, 'operational endpoint exposure at ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'operational endpoint exposure at '.$method.' '.$uri);
|
||||
$this->assertStringNotContainsStringIgnoringCase('APP_KEY', $response->getContent());
|
||||
$this->assertStringNotContainsStringIgnoringCase('DB_PASSWORD', $response->getContent());
|
||||
$this->assertStringNotContainsStringIgnoringCase('AWS_SECRET', $response->getContent());
|
||||
|
||||
+5
-5
@@ -27,7 +27,7 @@ class ApiOperationalResiliencePayloadSizeContractTest extends FullSurfaceE2ECont
|
||||
|
||||
$this->assertNoServerError($response, "$method $uri huge payload");
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertStringNotContainsStringIgnoringCase('Allowed memory size', $response->getContent(), $uri . ' should not leak memory exhaustion.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('Allowed memory size', $response->getContent(), $uri.' should not leak memory exhaustion.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,11 +39,11 @@ class ApiOperationalResiliencePayloadSizeContractTest extends FullSurfaceE2ECont
|
||||
}
|
||||
|
||||
$uri = $route->uri();
|
||||
$query = '?include=' . urlencode(str_repeat('parents.students.invoices.payments.', 100))
|
||||
. '&fields=' . urlencode(str_repeat('id,email,password,remember_token,', 200))
|
||||
. '&expand=' . urlencode(str_repeat('children.', 200));
|
||||
$query = '?include='.urlencode(str_repeat('parents.students.invoices.payments.', 100))
|
||||
.'&fields='.urlencode(str_repeat('id,email,password,remember_token,', 200))
|
||||
.'&expand='.urlencode(str_repeat('children.', 200));
|
||||
|
||||
$response = $this->requestAs($this->actorFor($uri), 'GET', $uri . $query);
|
||||
$response = $this->requestAs($this->actorFor($uri), 'GET', $uri.$query);
|
||||
$this->assertNoServerError($response, "GET $uri abusive expansion query");
|
||||
$this->assertControlled($response, 'GET', $uri);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Tests\Feature\Api\V1\FullSurface;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
@@ -66,13 +65,13 @@ class ApiOwnerBoundaryAndPrivacyExpansionTest extends FullSurfaceE2EContractCase
|
||||
private function seedOtherFamily(): array
|
||||
{
|
||||
$otherParent = $this->createApiUserWithRole('parent', [
|
||||
'email' => 'other.family.' . uniqid() . '@example.test',
|
||||
'email' => 'other.family.'.uniqid().'@example.test',
|
||||
'firstname' => 'Other',
|
||||
'lastname' => 'Family',
|
||||
]);
|
||||
|
||||
$studentId = DB::table('students')->insertGetId([
|
||||
'school_id' => 'S-OTHER-' . random_int(1000, 9999),
|
||||
'school_id' => 'S-OTHER-'.random_int(1000, 9999),
|
||||
'firstname' => 'Other',
|
||||
'lastname' => 'Student',
|
||||
'dob' => '2016-01-01',
|
||||
|
||||
@@ -24,7 +24,7 @@ class ApiPaginationCursorAndOffsetAbuseContractTest extends FullSurfaceE2EContra
|
||||
$response = $this->requestAs($this->actorFor($uri), 'GET', $uri, $query);
|
||||
|
||||
$this->assertControlled($response, 'GET', $uri);
|
||||
$this->assertNoServerError($response, 'pagination cursor abuse at GET ' . $uri);
|
||||
$this->assertNoServerError($response, 'pagination cursor abuse at GET '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,9 +28,9 @@ class ApiPolymorphicOwnershipBoundaryContractTest extends FullSurfaceE2EContract
|
||||
|
||||
$response = $this->requestAs($this->parent, $method, $uri, $payload);
|
||||
|
||||
$this->assertStatusIn($response, [200, 201, 202, 204, 400, 401, 403, 404, 405, 409, 419, 422], 'polymorphic overpost ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'polymorphic overpost ' . $method . ' ' . $uri);
|
||||
$this->assertStringNotContainsString('App\\Models\\User', $response->getContent(), $uri . ' must not leak model class names.');
|
||||
$this->assertStatusIn($response, [200, 201, 202, 204, 400, 401, 403, 404, 405, 409, 419, 422], 'polymorphic overpost '.$method.' '.$uri);
|
||||
$this->assertNoServerError($response, 'polymorphic overpost '.$method.' '.$uri);
|
||||
$this->assertStringNotContainsString('App\\Models\\User', $response->getContent(), $uri.' must not leak model class names.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,11 +44,11 @@ class ApiPolymorphicOwnershipBoundaryContractTest extends FullSurfaceE2EContract
|
||||
}
|
||||
|
||||
$uri = $route->uri();
|
||||
$response = $this->actingAs($this->parent, 'api')->getJson($this->materializePath($uri) . '?owner_type=user&owner_id=' . $this->admin->id);
|
||||
$response = $this->actingAs($this->parent, 'api')->getJson($this->materializePath($uri).'?owner_type=user&owner_id='.$this->admin->id);
|
||||
|
||||
$this->assertControlled($response, 'GET', $uri);
|
||||
$this->assertNoServerError($response, 'cross actor owner query ' . $uri);
|
||||
$this->assertStringNotContainsStringIgnoringCase($this->admin->email, $response->getContent(), $uri . ' must not leak admin-owned data to parent.');
|
||||
$this->assertNoServerError($response, 'cross actor owner query '.$uri);
|
||||
$this->assertStringNotContainsStringIgnoringCase($this->admin->email, $response->getContent(), $uri.' must not leak admin-owned data to parent.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ class ApiPrivacyRetentionAndErasureContractTest extends FullSurfaceE2EContractCa
|
||||
|
||||
$response = $this->json($method, $this->materializePath($uri), $this->payloadFor($method, $uri));
|
||||
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 419, 422], 'public privacy mutation ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'public privacy mutation ' . $method . ' ' . $uri);
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 419, 422], 'public privacy mutation '.$method.' '.$uri);
|
||||
$this->assertNoServerError($response, 'public privacy mutation '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,10 +39,10 @@ class ApiPrivacyRetentionAndErasureContractTest extends FullSurfaceE2EContractCa
|
||||
$response = $this->actingAs($this->actorFor($uri), 'api')->getJson($this->materializePath($uri));
|
||||
|
||||
$this->assertControlled($response, 'GET', $uri);
|
||||
$this->assertNoServerError($response, 'privacy export/read at ' . $uri);
|
||||
$this->assertNoServerError($response, 'privacy export/read at '.$uri);
|
||||
|
||||
foreach ($forbidden as $needle) {
|
||||
$this->assertStringNotContainsStringIgnoringCase($needle, $response->getContent(), $uri . ' must not leak ' . $needle);
|
||||
$this->assertStringNotContainsStringIgnoringCase($needle, $response->getContent(), $uri.' must not leak '.$needle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ use Tests\TestCase;
|
||||
*/
|
||||
class ApiPublicAndAuthFullSurfaceContractTest extends TestCase
|
||||
{
|
||||
use AssertsE2EApiResponses;
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
use AssertsE2EApiResponses;
|
||||
|
||||
public function test_public_and_auth_surface_has_stable_contracts(): void
|
||||
{
|
||||
@@ -23,14 +23,14 @@ class ApiPublicAndAuthFullSurfaceContractTest extends TestCase
|
||||
|
||||
foreach ($this->publicGetEndpoints() as $uri) {
|
||||
$response = $this->getJson($uri);
|
||||
$this->assertNoServerError($response, 'Public GET ' . $uri);
|
||||
$this->assertStatusIn($response, [200, 302, 400, 401, 403, 404, 419, 422], 'Public GET ' . $uri);
|
||||
$this->assertNoServerError($response, 'Public GET '.$uri);
|
||||
$this->assertStatusIn($response, [200, 302, 400, 401, 403, 404, 419, 422], 'Public GET '.$uri);
|
||||
}
|
||||
|
||||
foreach ($this->publicPostEndpoints() as [$uri, $payload]) {
|
||||
$response = $this->postJson($uri, $payload);
|
||||
$this->assertNoServerError($response, 'Public POST ' . $uri);
|
||||
$this->assertStatusIn($response, [200, 201, 202, 302, 400, 401, 403, 404, 419, 422], 'Public POST ' . $uri);
|
||||
$this->assertNoServerError($response, 'Public POST '.$uri);
|
||||
$this->assertStatusIn($response, [200, 201, 202, 302, 400, 401, 403, 404, 419, 422], 'Public POST '.$uri);
|
||||
}
|
||||
|
||||
$login = $this->postJson('/api/v1/auth/login', ['email' => $admin->email, 'password' => 'password']);
|
||||
@@ -40,8 +40,8 @@ class ApiPublicAndAuthFullSurfaceContractTest extends TestCase
|
||||
$this->actingAs($admin, 'api');
|
||||
foreach (['/api/v1/auth/me', '/api/v1/frontend/me', '/api/v1/auth/logout'] as $uri) {
|
||||
$response = str_ends_with($uri, 'logout') ? $this->postJson($uri) : $this->getJson($uri);
|
||||
$this->assertNoServerError($response, 'Authenticated auth/frontend endpoint ' . $uri);
|
||||
$this->assertStatusIn($response, [200, 204, 401, 403, 419, 422], 'Authenticated auth/frontend endpoint ' . $uri);
|
||||
$this->assertNoServerError($response, 'Authenticated auth/frontend endpoint '.$uri);
|
||||
$this->assertStatusIn($response, [200, 204, 401, 403, 419, 422], 'Authenticated auth/frontend endpoint '.$uri);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ class ApiQueryParameterFuzzingContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
foreach ($this->collectionPaths() as $path) {
|
||||
foreach ($this->hostileQueries() as $query) {
|
||||
$response = $this->requestAs($this->actorFor($path), 'GET', $path . '?' . http_build_query($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");
|
||||
@@ -22,7 +22,7 @@ class ApiQueryParameterFuzzingContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
foreach ($this->dateRangePaths() as $path) {
|
||||
foreach ($this->badDateRanges() as $query) {
|
||||
$response = $this->requestAs($this->actorFor($path), 'GET', $path . '?' . http_build_query($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");
|
||||
@@ -33,7 +33,7 @@ class ApiQueryParameterFuzzingContractTest extends FullSurfaceE2EContractCase
|
||||
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([
|
||||
$response = $this->requestAs($this->actorFor($path), 'GET', $path.'?'.http_build_query([
|
||||
'sort' => 'id desc; drop table users; --',
|
||||
'direction' => 'sideways',
|
||||
]));
|
||||
|
||||
@@ -19,12 +19,12 @@ class ApiQueueJobAndAsyncTriggerContractTest extends FullSurfaceE2EContractCase
|
||||
}
|
||||
|
||||
$studentAttempt = $this->requestAs($this->studentUser, $method, $uri, $this->payloadFor($method, $uri));
|
||||
$this->assertStatusIn($studentAttempt, [400, 401, 403, 404, 405, 409, 419, 422], 'student async trigger ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($studentAttempt, 'student async trigger ' . $method . ' ' . $uri);
|
||||
$this->assertStatusIn($studentAttempt, [400, 401, 403, 404, 405, 409, 419, 422], 'student async trigger '.$method.' '.$uri);
|
||||
$this->assertNoServerError($studentAttempt, 'student async trigger '.$method.' '.$uri);
|
||||
|
||||
$adminAttempt = $this->requestAs($this->admin, $method, $uri, $this->payloadFor($method, $uri));
|
||||
$this->assertControlled($adminAttempt, $method, $uri);
|
||||
$this->assertNoServerError($adminAttempt, 'admin async trigger ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($adminAttempt, 'admin async trigger '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ class ApiQueueJobAndAsyncTriggerContractTest extends FullSurfaceE2EContractCase
|
||||
|
||||
$this->assertControlled($first, $method, $uri);
|
||||
$this->assertControlled($second, $method, $uri);
|
||||
$this->assertNoServerError($second, 'duplicate async trigger ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($second, 'duplicate async trigger '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@ class ApiRateLimitTokenAndSessionHardeningContractTest extends FullSurfaceE2ECon
|
||||
for ($attempt = 1; $attempt <= 8; $attempt++) {
|
||||
$response = $this->postJson($path, [
|
||||
'email' => $this->admin->email,
|
||||
'password' => 'definitely-not-the-password-' . $attempt,
|
||||
'password' => 'definitely-not-the-password-'.$attempt,
|
||||
]);
|
||||
|
||||
$this->assertStatusIn($response, [400, 401, 403, 419, 422, 429], "Bad login attempt {$attempt} for {$path}");
|
||||
@@ -76,7 +76,7 @@ class ApiRateLimitTokenAndSessionHardeningContractTest extends FullSurfaceE2ECon
|
||||
private function routeExistsFor(string $method, string $path): bool
|
||||
{
|
||||
foreach ($this->apiRoutes() as $route) {
|
||||
if ($this->primaryMethod($route) === $method && '/' . ltrim($route->uri(), '/') === $path) {
|
||||
if ($this->primaryMethod($route) === $method && '/'.ltrim($route->uri(), '/') === $path) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class ApiRelationshipCardinalityContractTest extends FullSurfaceE2EContractCase
|
||||
$response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload);
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, 'relationship cardinality probe at ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'relationship cardinality probe at '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class ApiReportMathAndFinancialInvariantContractTest extends FullSurfaceE2EContr
|
||||
$response = $this->requestAs($this->admin, $method, $uri, $this->payloadFor($method, $uri) + ['amount' => $amount, 'total' => $amount, 'balance' => $amount]);
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, 'bad financial amount ' . $amount . ' for ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'bad financial amount '.$amount.' for '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ class ApiReportMathAndFinancialInvariantContractTest extends FullSurfaceE2EContr
|
||||
}
|
||||
|
||||
$uri = $route->uri();
|
||||
$response = $this->actingAs($this->actorFor($uri), 'api')->getJson($this->materializePath($uri) . '?' . http_build_query([
|
||||
$response = $this->actingAs($this->actorFor($uri), 'api')->getJson($this->materializePath($uri).'?'.http_build_query([
|
||||
'from' => '2026-12-31',
|
||||
'to' => '2025-01-01',
|
||||
'group_by' => 'parent.password',
|
||||
@@ -46,8 +46,8 @@ class ApiReportMathAndFinancialInvariantContractTest extends FullSurfaceE2EContr
|
||||
]));
|
||||
|
||||
$this->assertControlled($response, 'GET', $uri);
|
||||
$this->assertNoServerError($response, 'bad report aggregation query at ' . $uri);
|
||||
$this->assertStringNotContainsStringIgnoringCase('password', $response->getContent(), $uri . ' must not echo sensitive aggregate fields.');
|
||||
$this->assertNoServerError($response, 'bad report aggregation query at '.$uri);
|
||||
$this->assertStringNotContainsStringIgnoringCase('password', $response->getContent(), $uri.' must not echo sensitive aggregate fields.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ class ApiRequestHeaderSpoofingContractTest extends FullSurfaceE2EContractCase
|
||||
'X-Impersonate-User' => (string) $this->admin->id,
|
||||
])->json($method, $this->materializePath($uri), $this->payloadFor($method, $uri));
|
||||
|
||||
$this->assertStatusIn($response, [401, 403, 404, 405, 419, 422], 'spoofed headers should not authorize parent at ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'header spoofing at ' . $method . ' ' . $uri);
|
||||
$this->assertStatusIn($response, [401, 403, 404, 405, 419, 422], 'spoofed headers should not authorize parent at '.$method.' '.$uri);
|
||||
$this->assertNoServerError($response, 'header spoofing at '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ class ApiRoleWorkflowNegativeMatrixContractTest extends FullSurfaceE2EContractCa
|
||||
return $this->createApiUserWithRole('parent', array_merge([
|
||||
'firstname' => 'Blocked',
|
||||
'lastname' => 'User',
|
||||
'email' => 'blocked.' . uniqid() . '@example.test',
|
||||
'email' => 'blocked.'.uniqid().'@example.test',
|
||||
], $overrides));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Tests\Feature\Api\V1\FullSurface;
|
||||
|
||||
use Illuminate\Routing\Route as LaravelRoute;
|
||||
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiRouteNamingVersioningAndHygieneTest extends FullSurfaceE2EContractCase
|
||||
@@ -35,7 +34,7 @@ class ApiRouteNamingVersioningAndHygieneTest extends FullSurfaceE2EContractCase
|
||||
$seen = [];
|
||||
|
||||
foreach ($this->apiRoutes() as $route) {
|
||||
$key = $this->primaryMethod($route) . ' ' . $route->uri();
|
||||
$key = $this->primaryMethod($route).' '.$route->uri();
|
||||
$this->assertArrayNotHasKey($key, $seen, "Duplicate API method/URI pair: $key");
|
||||
$seen[$key] = true;
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ class ApiRouteParameterAbuseContractTest extends FullSurfaceE2EContractCase
|
||||
$response = $this->requestAs($this->actorFor($template), $method, $uri, $this->payloadFor($method, $template));
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, 'bad route parameter ' . $method . ' ' . $uri);
|
||||
$this->assertStringNotContainsString('.env', $response->getContent(), $uri . ' must not leak filesystem targets.');
|
||||
$this->assertNoServerError($response, 'bad route parameter '.$method.' '.$uri);
|
||||
$this->assertStringNotContainsString('.env', $response->getContent(), $uri.' must not leak filesystem targets.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ class ApiRouteParameterAbuseContractTest extends FullSurfaceE2EContractCase
|
||||
$response = $this->requestAs($this->actorFor($template), $method, $uri, $this->payloadFor($method, $template));
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, 'overflowing route id ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'overflowing route id '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class ApiRouteUseCaseCatalogCompletenessTest extends TestCase
|
||||
$this->assertSame(
|
||||
[],
|
||||
$unowned,
|
||||
"API routes without an explicit E2E user journey owner:\n" . implode("\n", $unowned) .
|
||||
"API routes without an explicit E2E user journey owner:\n".implode("\n", $unowned).
|
||||
"\n\nAdd the route to the journey catalog and add or extend the scenario that proves it."
|
||||
);
|
||||
}
|
||||
@@ -57,7 +57,7 @@ class ApiRouteUseCaseCatalogCompletenessTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $missing, "Declared E2E journeys with no registered API routes:\n" . implode("\n", $missing));
|
||||
$this->assertSame([], $missing, "Declared E2E journeys with no registered API routes:\n".implode("\n", $missing));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,7 +127,7 @@ class ApiRouteUseCaseCatalogCompletenessTest extends TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $patterns
|
||||
* @param list<string> $patterns
|
||||
*/
|
||||
private function matchesAny(string $signature, array $patterns): bool
|
||||
{
|
||||
@@ -155,6 +155,6 @@ class ApiRouteUseCaseCatalogCompletenessTest extends TestCase
|
||||
$methods = array_values(array_diff($route->methods(), ['HEAD', 'OPTIONS']));
|
||||
sort($methods);
|
||||
|
||||
return implode(',', $methods) . ' ' . $route->uri();
|
||||
return implode(',', $methods).' '.$route->uri();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class ApiSchemaKeyStabilityExpansionContractTest extends FullSurfaceE2EContractC
|
||||
|
||||
foreach ($contracts as $uri => $expectedAnyKeys) {
|
||||
$response = $this->requestAs($this->actorFor($uri), 'GET', $uri);
|
||||
$this->assertNoServerError($response, 'GET ' . $uri);
|
||||
$this->assertNoServerError($response, 'GET '.$uri);
|
||||
|
||||
if ($response->getStatusCode() !== 200 || ! $this->isJsonString($response->getContent())) {
|
||||
continue;
|
||||
@@ -32,7 +32,7 @@ class ApiSchemaKeyStabilityExpansionContractTest extends FullSurfaceE2EContractC
|
||||
$payload = $this->flattenKeys($response->json());
|
||||
$this->assertTrue(
|
||||
count(array_intersect($expectedAnyKeys, $payload)) > 0,
|
||||
$uri . ' should expose at least one stable identity/display key. Keys: ' . implode(', ', $payload)
|
||||
$uri.' should expose at least one stable identity/display key. Keys: '.implode(', ', $payload)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ class ApiSchemaKeyStabilityExpansionContractTest extends FullSurfaceE2EContractC
|
||||
$keys = $this->flattenKeys($response->json());
|
||||
$this->assertTrue(
|
||||
count(array_intersect(['message', 'errors', 'error', 'ok', 'status'], $keys)) > 0,
|
||||
"$method $uri must return an actionable error envelope. Keys: " . implode(', ', $keys)
|
||||
"$method $uri must return an actionable error envelope. Keys: ".implode(', ', $keys)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ class ApiSchemaKeyStabilityExpansionContractTest extends FullSurfaceE2EContractC
|
||||
if (is_string($key)) {
|
||||
$keys[] = $key;
|
||||
if (is_array($value)) {
|
||||
$keys = array_merge($keys, $this->flattenKeys($value, $prefix . $key . '.'));
|
||||
$keys = array_merge($keys, $this->flattenKeys($value, $prefix.$key.'.'));
|
||||
}
|
||||
} elseif (is_array($value)) {
|
||||
$keys = array_merge($keys, $this->flattenKeys($value, $prefix));
|
||||
|
||||
@@ -21,11 +21,11 @@ class ApiSchoolCalendarAcademicBoundaryExpansionTest extends FullSurfaceE2EContr
|
||||
$uri = $route->uri();
|
||||
$payload = $this->payloadFor($method, $uri) + ['date' => $date, 'due_date' => $date, 'exam_date' => $date];
|
||||
$response = $method === 'GET'
|
||||
? $this->actingAs($this->actorFor($uri), 'api')->getJson($this->materializePath($uri) . '?date=' . urlencode($date))
|
||||
? $this->actingAs($this->actorFor($uri), 'api')->getJson($this->materializePath($uri).'?date='.urlencode($date))
|
||||
: $this->requestAs($this->actorFor($uri), $method, $uri, $payload);
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, 'academic boundary date ' . $date . ' at ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'academic boundary date '.$date.' at '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,11 +39,11 @@ class ApiSchoolCalendarAcademicBoundaryExpansionTest extends FullSurfaceE2EContr
|
||||
$uri = $route->uri();
|
||||
$payload = $this->payloadFor($method, $uri) + ['semester' => 'admin', 'term' => '../../secret', 'quarter' => '<script>1</script>'];
|
||||
$response = $method === 'GET'
|
||||
? $this->actingAs($this->actorFor($uri), 'api')->getJson($this->materializePath($uri) . '?semester=admin&term=../../secret')
|
||||
? $this->actingAs($this->actorFor($uri), 'api')->getJson($this->materializePath($uri).'?semester=admin&term=../../secret')
|
||||
: $this->requestAs($this->actorFor($uri), $method, $uri, $payload);
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, 'bad semester/term values at ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'bad semester/term values at '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@ class ApiSearchIndexAndAutocompleteContractTest extends FullSurfaceE2EContractCa
|
||||
|
||||
$uri = $route->uri();
|
||||
foreach ($terms as $term) {
|
||||
$response = $this->requestAs($this->actorFor($uri), 'GET', $uri . '?q=' . urlencode($term) . '&term=' . urlencode($term) . '&search=' . urlencode($term));
|
||||
$response = $this->requestAs($this->actorFor($uri), 'GET', $uri.'?q='.urlencode($term).'&term='.urlencode($term).'&search='.urlencode($term));
|
||||
$this->assertNoServerError($response, "GET $uri search term $term");
|
||||
$this->assertControlled($response, 'GET', $uri);
|
||||
$this->assertStringNotContainsStringIgnoringCase('SQLSTATE', $response->getContent(), $uri . ' should not leak SQLSTATE from search.');
|
||||
$this->assertStringNotContainsString('<script>alert(1)</script>', $response->getContent(), $uri . ' should not reflect raw script search terms.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('SQLSTATE', $response->getContent(), $uri.' should not leak SQLSTATE from search.');
|
||||
$this->assertStringNotContainsString('<script>alert(1)</script>', $response->getContent(), $uri.' should not reflect raw script search terms.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ class ApiSearchIndexAndAutocompleteContractTest extends FullSurfaceE2EContractCa
|
||||
$query = '?q=*&scope=all&include=users,parents,students,finance,medical';
|
||||
|
||||
foreach (['parent' => $this->parent, 'teacher' => $this->teacher, 'student' => $this->studentUser] as $label => $actor) {
|
||||
$response = $this->requestAs($actor, 'GET', $uri . $query);
|
||||
$response = $this->requestAs($actor, 'GET', $uri.$query);
|
||||
$this->assertNoServerError($response, "$label GET $uri broad search");
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 409, 419, 422], "$label GET $uri broad search");
|
||||
$this->assertStringNotContainsStringIgnoringCase('remember_token', $response->getContent(), "$uri broad search must not leak auth internals.");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user