add test batches

This commit is contained in:
root
2026-06-08 23:45:55 -04:00
parent c792b8be05
commit 8d4d610b82
1480 changed files with 22587 additions and 10762 deletions
@@ -13,6 +13,7 @@ class ApiAuthenticationAndAuthorizationTest extends TestCase
use CreatesApiTestUsers;
use RefreshDatabase;
public function test_api_login_returns_token_and_user_object(): void
{
$user = $this->createApiUserWithRole('teacher', [
@@ -102,7 +103,7 @@ class ApiAuthenticationAndAuthorizationTest extends TestCase
foreach ($this->sampleProtectedRoutes() as $route) {
$method = $this->primaryMethod($route);
$uri = '/'.$this->uriWithPlaceholders($route->uri());
$uri = '/' . $this->uriWithPlaceholders($route->uri());
$response = $this->json($method, $uri);
$status = $response->getStatusCode();
@@ -112,7 +113,7 @@ class ApiAuthenticationAndAuthorizationTest extends TestCase
}
}
$this->assertSame([], $failures, "Anonymous requests reached unexpected responses:\n".implode("\n", $failures));
$this->assertSame([], $failures, "Anonymous requests reached unexpected responses:\n" . implode("\n", $failures));
}
/**
@@ -47,6 +47,7 @@ class ApiPreferencesFeatureTest extends TestCase
->assertJsonPath('status', true);
}
public function test_non_admin_cannot_list_or_manage_another_users_preferences(): void
{
$owner = $this->createApiUserWithRole('teacher');
+1 -1
View File
@@ -36,7 +36,7 @@ class ApiPublicEndpointsTest extends TestCase
public function test_public_frontend_content_endpoints_do_not_server_error(): void
{
foreach (['/', 'facility', 'team', 'call-to-action', 'testimonial', 'not-found'] as $path) {
$uri = '/api/v1/frontend'.($path === '/' ? '' : '/'.$path);
$uri = '/api/v1/frontend' . ($path === '/' ? '' : '/' . $path);
$response = $this->getJson($uri);
$this->assertLessThan(500, $response->getStatusCode(), "$uri returned a server error");
@@ -2,6 +2,8 @@
namespace Tests\Feature\Api;
use App\Models\Permission;
use App\Models\Role;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\Concerns\CreatesApiTestUsers;
+5 -5
View File
@@ -24,11 +24,11 @@ class ApiRouteContractTest extends TestCase
[$class, $method] = explode('@', $action, 2);
if (! class_exists($class) || ! method_exists($class, $method)) {
$missing[] = implode('|', $route->methods()).' '.$route->uri().' -> '.$action;
$missing[] = implode('|', $route->methods()) . ' ' . $route->uri() . ' -> ' . $action;
}
}
$this->assertSame([], $missing, "These API routes reference missing controllers or methods:\n".implode("\n", $missing));
$this->assertSame([], $missing, "These API routes reference missing controllers or methods:\n" . implode("\n", $missing));
}
public function test_api_routes_do_not_duplicate_method_and_uri_pairs(): void
@@ -42,7 +42,7 @@ class ApiRouteContractTest extends TestCase
continue;
}
$key = $method.' '.$route->uri();
$key = $method . ' ' . $route->uri();
if (isset($seen[$key])) {
$duplicates[] = $key;
}
@@ -90,11 +90,11 @@ class ApiRouteContractTest extends TestCase
});
if (! $hasProtection) {
$unprotected[] = implode('|', $methods).' '.$route->uri();
$unprotected[] = implode('|', $methods) . ' ' . $route->uri();
}
}
$this->assertSame([], $unprotected, "Mutating API routes without an obvious auth/throttle middleware:\n".implode("\n", $unprotected));
$this->assertSame([], $unprotected, "Mutating API routes without an obvious auth/throttle middleware:\n" . implode("\n", $unprotected));
}
/**
@@ -0,0 +1,169 @@
<?php
namespace Tests\Feature\Api;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Routing\Route as LaravelRoute;
use Illuminate\Support\Facades\Route;
use Tests\TestCase;
/**
* Route-to-use-case coverage guard.
*
* This does not replace workflow tests. It prevents the slower rot where a new
* endpoint is added with no explicit business workflow owner, because apparently
* endpoints reproduce when nobody is watching them.
*/
class ApiUseCaseCoverageMatrixTest extends TestCase
{
use RefreshDatabase;
public function test_every_api_route_is_mapped_to_an_explicit_use_case_domain(): void
{
$unmapped = [];
foreach ($this->apiRoutes() as $route) {
$uri = $route->uri();
if ($this->domainFor($uri) === null) {
$unmapped[] = implode('|', array_diff($route->methods(), ['HEAD'])) . ' ' . $uri;
}
}
$this->assertSame(
[],
$unmapped,
"API routes without an explicit use-case domain:\n" . implode("\n", $unmapped) .
"\n\nAdd the route to the matrix and then add/extend an E2E workflow test for that domain."
);
}
public function test_required_use_case_domains_have_route_coverage(): void
{
$domains = collect($this->apiRoutes())
->map(fn (LaravelRoute $route): ?string => $this->domainFor($route->uri()))
->filter()
->unique()
->sort()
->values()
->all();
$missing = array_values(array_diff(array_keys($this->useCaseDomains()), $domains));
$this->assertSame(
[],
$missing,
"Required use-case domains with no matching API route coverage:\n" . implode("\n", $missing)
);
}
public function test_every_protected_api_route_rejects_anonymous_requests(): void
{
$failures = [];
foreach ($this->protectedRoutes() as $route) {
$method = $this->primaryMethod($route);
$uri = '/' . $this->uriWithSafePlaceholders($route->uri());
$response = $this->json($method, $uri);
$status = $response->getStatusCode();
if (! in_array($status, [401, 403, 404, 405, 419, 422], true)) {
$failures[] = "$method $uri returned $status";
}
}
$this->assertSame(
[],
$failures,
"Anonymous requests reached protected API routes unexpectedly:\n" . implode("\n", $failures)
);
}
/**
* @return array<string, string>
*/
private function useCaseDomains(): array
{
return [
'public_content_and_support' => '#^api/(?:v1/)?(?:login|register|auth|documentation|docs|docs/public|policies|pages|contact|frontend|health|system/db-check|access_denied|certificates/verify|timeoff/notify|winners/competitions|confirm_authorized_user|set_authorized_user_password|badge_scan|scanner)(?:/|$)#',
'legacy_compatibility' => '#^api/(?:proofread|emails|compare|attendance-templates|attendance-comment-templates|administrator/attendance-templates)(?:/|$)#',
'preferences_navigation_and_ui' => '#^api/v1/(?:preferences|nav-builder|landing|info-icon|dashboard|utilities|ui|settings|configuration|system|stats|role-switcher)(?:/|$)#',
'administration_enrollment_and_school_years' => '#^api/v1/(?:administrator|school-years|class-sections|users|role-permissions|ip-bans|notifications)(?:/|$)#',
'students_parents_and_families' => '#^api/v1/(?:students|parents|families|family-admin|emergency-contacts)(?:/|$)#',
'teachers_staff_and_class_operations' => '#^api/v1/(?:teacher|teachers|staff|assignments|class-prep|class-progress)(?:/|$)#',
'attendance_and_safety' => '#^api/v1/(?:attendance|attendance-tracking|attendance-comment-templates|attendance-templates|incidents)(?:/|$)#',
'grading_scores_and_reporting' => '#^api/v1/(?:scores|grading|reports|badges|certificates|print-requests)(?:/|$)#',
'finance_billing_and_inventory' => '#^api/v1/(?:finance|discounts|expenses|extra-charges|inventory|files)(?:/|$)#',
'communications_and_messaging' => '#^api/v1/(?:messages|broadcast-email|email|email-extractor|communications|support|whatsapp)(?:/|$)#',
'competition_and_public_results' => '#^api/v1/(?:competition-scores)(?:/|$)#',
];
}
private function domainFor(string $uri): ?string
{
foreach ($this->useCaseDomains() as $domain => $pattern) {
if (preg_match($pattern, $uri) === 1) {
return $domain;
}
}
return null;
}
/**
* @return list<LaravelRoute>
*/
private function apiRoutes(): array
{
return array_values(array_filter(Route::getRoutes()->getRoutes(), function (LaravelRoute $route): bool {
return str_starts_with($route->uri(), 'api/');
}));
}
/**
* @return list<LaravelRoute>
*/
private function protectedRoutes(): array
{
return array_values(array_filter($this->apiRoutes(), function (LaravelRoute $route): bool {
if ($this->isStreamOrGeneratedFileRoute($route->uri())) {
return false;
}
return collect($route->gatherMiddleware())->contains(function (string $middleware): bool {
return str_contains($middleware, 'auth:api')
|| str_contains($middleware, 'jwt.auth')
|| str_contains($middleware, 'admin.access')
|| str_contains($middleware, 'parent.access')
|| str_contains($middleware, 'teacher.portal.auth');
});
}));
}
private function isStreamOrGeneratedFileRoute(string $uri): bool
{
return str_contains($uri, 'files/')
|| str_contains($uri, 'receipts/')
|| str_contains($uri, 'reimbursements/')
|| str_contains($uri, 'attachments/')
|| str_contains($uri, '/pdf')
|| str_contains($uri, '/csv');
}
private function primaryMethod(LaravelRoute $route): string
{
foreach ($route->methods() as $method) {
if (! in_array($method, ['HEAD', 'OPTIONS'], true)) {
return $method;
}
}
return 'GET';
}
private function uriWithSafePlaceholders(string $uri): string
{
return preg_replace('/\{[^}]+\}/', '1', $uri) ?? $uri;
}
}
@@ -2,6 +2,7 @@
namespace Tests\Feature\Api;
use App\Models\Role;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
@@ -121,4 +121,4 @@ class AdministratorAbsenceControllerTest extends TestCase
'dates' => ['2026-03-08'],
]);
}
}
}
@@ -128,4 +128,4 @@ class AdministratorEnrollmentControllerTest extends TestCase
'message' => 'Enrollment statuses updated and notifications sent.',
]);
}
}
}
@@ -263,7 +263,6 @@ class AdministratorEnrollmentUpdateStatusesApiTest extends TestCase
private function postUpdateStatuses(array $enrollmentStatus)
{
$query = http_build_query(['enrollment_status' => $enrollmentStatus]);
return $this->post('/api/v1/administrator/enrollment-withdrawal/update-statuses?'.$query, []);
return $this->post('/api/v1/administrator/enrollment-withdrawal/update-statuses?' . $query, []);
}
}
@@ -157,4 +157,4 @@ class AdministratorNotificationControllerTest extends TestCase
'message' => 'Print notification recipients updated.',
]);
}
}
}
@@ -2,10 +2,10 @@
namespace Tests\Feature\Api\Administrator;
use App\Http\Middleware\EnsurePrintRequestsAdminAccess;
use App\Models\User;
use App\Services\Administrator\TeacherSubmissionNotificationService;
use App\Services\Administrator\TeacherSubmissionReportService;
use App\Http\Middleware\EnsurePrintRequestsAdminAccess;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use Mockery;
@@ -281,7 +281,7 @@ class AdministratorTeacherSubmissionReportApiTest extends TestCase
'class_section_id' => 12,
'admin_id' => 901,
'notification_category' => 'teacher_submissions',
'message' => 'Reminder '.$i,
'message' => 'Reminder ' . $i,
'status' => 'sent',
'school_year' => '2025-2026',
'semester' => 'Fall',
@@ -80,7 +80,7 @@ class EmergencyContactControllerTest extends TestCase
]);
Sanctum::actingAs($admin, [], 'api');
$response = $this->patch('/api/v1/administrator/emergency-contacts/'.$contact->id, [
$response = $this->patch('/api/v1/administrator/emergency-contacts/' . $contact->id, [
'parent_id' => $parent->id,
'name' => 'New Name',
'cellphone' => '5555555555',
@@ -116,7 +116,7 @@ class EmergencyContactControllerTest extends TestCase
]);
Sanctum::actingAs($admin, [], 'api');
$response = $this->delete('/api/v1/administrator/emergency-contacts/'.$contact->id, [
$response = $this->delete('/api/v1/administrator/emergency-contacts/' . $contact->id, [
'parent_id' => $parent->id,
], [
'Accept' => 'application/json',
@@ -145,7 +145,7 @@ class EmergencyContactControllerTest extends TestCase
]);
Sanctum::actingAs($admin, [], 'api');
$response = $this->get('/api/v1/administrator/emergency-contacts/'.$contact->id.'?parent_id='.$parent->id, [
$response = $this->get('/api/v1/administrator/emergency-contacts/' . $contact->id . '?parent_id=' . $parent->id, [
'Accept' => 'application/json',
]);
@@ -171,7 +171,7 @@ class EmergencyContactControllerTest extends TestCase
]);
Sanctum::actingAs($admin, [], 'api');
$response = $this->patch('/api/v1/administrator/emergency-contacts/'.$contact->id, [
$response = $this->patch('/api/v1/administrator/emergency-contacts/' . $contact->id, [
'parent_id' => $otherParent->id,
'name' => 'New Name',
'cellphone' => '9999999999',
@@ -205,7 +205,7 @@ class EmergencyContactControllerTest extends TestCase
]);
Sanctum::actingAs($admin, [], 'api');
$response = $this->delete('/api/v1/administrator/emergency-contacts/'.$contact->id, [
$response = $this->delete('/api/v1/administrator/emergency-contacts/' . $contact->id, [
'parent_id' => $otherParent->id,
], [
'Accept' => 'application/json',
@@ -16,17 +16,11 @@ class AssignmentApiControllerTest extends TestCase
use RefreshDatabase;
protected User $user;
protected User $teacherMain;
protected User $teacherAssistant;
protected ClassSection $sectionA;
protected ClassSection $sectionB;
protected Student $studentOne;
protected Student $studentTwo;
protected function setUp(): void
@@ -424,4 +418,4 @@ class AssignmentApiControllerTest extends TestCase
$this->assertSame(['Main Teacher'], $mainTeachers);
}
}
}
+49 -49
View File
@@ -2,12 +2,12 @@
namespace Tests\Feature\Api;
use App\Models\ClassSection;
use App\Models\Configuration;
use App\Models\User;
use App\Models\Student;
use App\Models\StudentClass;
use App\Models\TeacherClass;
use App\Models\User;
use App\Models\ClassSection;
use App\Models\Configuration;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use PHPUnit\Framework\Attributes\Test;
@@ -48,36 +48,36 @@ class AssignmentApiTest extends TestCase
$teacher = User::factory()->create([
'firstname' => 'Ahmad',
'lastname' => 'Ali',
'lastname' => 'Ali',
]);
TeacherClass::query()->create([
'teacher_id' => $teacher->id,
'class_section_id' => $section->class_section_id,
'position' => 'main',
'semester' => 'Fall',
'school_year' => '2025-2026',
'description' => 'Main teacher assignment',
'teacher_id' => $teacher->id,
'class_section_id' => $section->class_section_id,
'position' => 'main',
'semester' => 'Fall',
'school_year' => '2025-2026',
'description' => 'Main teacher assignment',
]);
$student = Student::factory()->create([
'firstname' => 'Sara',
'lastname' => 'Yousef',
'is_active' => 1,
'gender' => 'F',
'firstname' => 'Sara',
'lastname' => 'Yousef',
'is_active' => 1,
'gender' => 'F',
'registration_grade' => '5',
'photo_consent' => 1,
'tuition_paid' => 1,
'school_id' => 'SCH-1001',
'photo_consent' => 1,
'tuition_paid' => 1,
'school_id' => 'SCH-1001',
]);
StudentClass::query()->create([
'student_id' => $student->id,
'class_section_id' => $section->class_section_id,
'semester' => 'Fall',
'school_year' => '2025-2026',
'description' => 'Student assigned',
'is_active' => 1, // remove if your table doesnt have this column
'student_id' => $student->id,
'class_section_id' => $section->class_section_id,
'semester' => 'Fall',
'school_year' => '2025-2026',
'description' => 'Student assigned',
'is_active' => 1, // remove if your table doesnt have this column
]);
$response = $this->getJson('/api/assignments');
@@ -96,7 +96,7 @@ class AssignmentApiTest extends TestCase
'semester',
'school_year',
'description',
],
]
],
'schoolYears',
'selectedYear',
@@ -122,19 +122,19 @@ class AssignmentApiTest extends TestCase
$teacher = User::factory()->create(['firstname' => 'Test', 'lastname' => 'Teacher']);
TeacherClass::query()->create([
'teacher_id' => $teacher->id,
'teacher_id' => $teacher->id,
'class_section_id' => $section1->class_section_id,
'position' => 'main',
'semester' => 'Fall',
'school_year' => '2024-2025',
'position' => 'main',
'semester' => 'Fall',
'school_year' => '2024-2025',
]);
TeacherClass::query()->create([
'teacher_id' => $teacher->id,
'teacher_id' => $teacher->id,
'class_section_id' => $section2->class_section_id,
'position' => 'main',
'semester' => 'Fall',
'school_year' => '2025-2026',
'position' => 'main',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
$response = $this->getJson('/api/assignments?school_year=2025-2026');
@@ -158,11 +158,11 @@ class AssignmentApiTest extends TestCase
$section = ClassSection::factory()->create();
$payload = [
'student_id' => $student->id,
'student_id' => $student->id,
'class_section_id' => $section->class_section_id,
'semester' => 'Spring',
'school_year' => '2025-2026',
'description' => 'Moved after placement review',
'semester' => 'Spring',
'school_year' => '2025-2026',
'description' => 'Moved after placement review',
];
$response = $this->postJson('/api/assignments', $payload);
@@ -171,10 +171,10 @@ class AssignmentApiTest extends TestCase
->assertJsonPath('message', 'Assignment saved successfully.');
$this->assertDatabaseHas('student_class', [
'student_id' => $student->id,
'student_id' => $student->id,
'class_section_id' => $section->class_section_id,
'semester' => 'Spring',
'school_year' => '2025-2026',
'semester' => 'Spring',
'school_year' => '2025-2026',
]);
}
@@ -203,19 +203,19 @@ class AssignmentApiTest extends TestCase
// First create
$this->postJson('/api/assignments', [
'student_id' => $student->id,
'student_id' => $student->id,
'class_section_id' => $sectionA->class_section_id,
'semester' => 'Fall',
'school_year' => '2025-2026',
'semester' => 'Fall',
'school_year' => '2025-2026',
])->assertCreated();
// Second call same unique key should update (per service updateOrCreate)
$this->postJson('/api/assignments', [
'student_id' => $student->id,
'student_id' => $student->id,
'class_section_id' => $sectionA->class_section_id,
'semester' => 'Fall',
'school_year' => '2025-2026',
'description' => 'Reassigned',
'semester' => 'Fall',
'school_year' => '2025-2026',
'description' => 'Reassigned',
])->assertCreated();
$this->assertEquals(
@@ -228,10 +228,10 @@ class AssignmentApiTest extends TestCase
);
$this->assertDatabaseHas('student_class', [
'student_id' => $student->id,
'student_id' => $student->id,
'class_section_id' => $sectionA->class_section_id,
'semester' => 'Fall',
'school_year' => '2025-2026',
'semester' => 'Fall',
'school_year' => '2025-2026',
]);
}
@@ -113,7 +113,7 @@ class LateSlipLogsControllerTest extends TestCase
$user = User::query()->create([
'firstname' => 'Test',
'lastname' => 'User',
'email' => $roleName.'@example.com',
'email' => $roleName . '@example.com',
'cellphone' => '5555555555',
'address_street' => '123 Main',
'city' => 'City',
@@ -85,10 +85,10 @@ class AttendanceCommentTemplateApiTest extends TestCase
$mock->shouldReceive('create')
->once()
->with(Mockery::on(function ($data) use ($payload) {
return (int) $data['min_score'] === 80
&& (int) $data['max_score'] === 100
return (int)$data['min_score'] === 80
&& (int)$data['max_score'] === 100
&& $data['template_text'] === $payload['template_text']
&& (bool) $data['is_active'] === true;
&& (bool)$data['is_active'] === true;
}))
->andReturn([
'id' => 10,
@@ -159,7 +159,7 @@ class AttendanceCommentTemplateApiTest extends TestCase
->once()
->with(7, Mockery::on(function ($data) {
return $data['template_text'] === 'Updated comment text'
&& (bool) $data['is_active'] === false;
&& (bool)$data['is_active'] === false;
}))
->andReturn([
'id' => 7,
@@ -221,7 +221,7 @@ class AttendanceCommentTemplateApiTest extends TestCase
'max_score' => 49,
'template_text' => 'Low attendance',
'is_active' => true,
],
]
]);
$this->app->instance(AttendanceCommentTemplateService::class, $mock);
@@ -232,4 +232,4 @@ class AttendanceCommentTemplateApiTest extends TestCase
->assertJsonCount(1, 'templates')
->assertJsonPath('templates.0.id', 1);
}
}
}
@@ -4,7 +4,6 @@ namespace Tests\Feature\Api\V1\AttendanceTracking;
use App\Services\AttendanceTracking\AttendanceTrackingService;
use Mockery;
use Mockery\MockInterface;
use Tests\TestCase;
class AttendanceTrackingControllerTest extends TestCase
@@ -15,7 +14,7 @@ class AttendanceTrackingControllerTest extends TestCase
parent::tearDown();
}
protected function mockService(): MockInterface
protected function mockService(): \Mockery\MockInterface
{
$service = Mockery::mock(AttendanceTrackingService::class);
$this->app->instance(AttendanceTrackingService::class, $service);
@@ -159,7 +159,7 @@ class IpBanControllerTest extends TestCase
$user = User::query()->create([
'firstname' => 'Test',
'lastname' => 'User',
'email' => $roleName.'@example.com',
'email' => $roleName . '@example.com',
'cellphone' => '5555555555',
'address_street' => '123 Main',
'city' => 'City',
+1 -1
View File
@@ -234,4 +234,4 @@ class BadgeControllerTest extends TestCase
$this->postJson('/api/badges/log-print', ['user_ids' => [1]])->assertStatus(401);
$this->postJson('/api/badges/pdf', ['user_ids' => [1]])->assertStatus(401);
}
}
}
@@ -31,8 +31,7 @@ class BroadcastEmailControllerTest extends TestCase
{
$this->seedParentData();
$fake = new class extends EmailService
{
$fake = new class extends EmailService {
public array $sent = [];
public function send(
@@ -42,9 +41,9 @@ class BroadcastEmailControllerTest extends TestCase
string $fromKey = 'general',
array $cc = [],
array $bcc = []
): bool {
): bool
{
$this->sent[] = compact('to', 'subject', 'html', 'fromKey', 'cc', 'bcc');
return true;
}
};
@@ -92,7 +92,7 @@ class ClassProgressControllerTest extends TestCase
Sanctum::actingAs($user);
$response = $this->getJson('/api/v1/class-progress/'.$report->id);
$response = $this->getJson('/api/v1/class-progress/' . $report->id);
$response->assertOk();
$response->assertJsonPath('status', true);
@@ -110,7 +110,7 @@ class ClassProgressControllerTest extends TestCase
Sanctum::actingAs($user);
$response = $this->patchJson('/api/v1/class-progress/'.$report->id, [
$response = $this->patchJson('/api/v1/class-progress/' . $report->id, [
'status' => 'behind',
]);
@@ -132,7 +132,7 @@ class ClassProgressControllerTest extends TestCase
Sanctum::actingAs($user);
$response = $this->deleteJson('/api/v1/class-progress/'.$report->id);
$response = $this->deleteJson('/api/v1/class-progress/' . $report->id);
$response->assertOk();
$this->assertDatabaseMissing('class_progress_reports', [
@@ -152,7 +152,7 @@ class ClassProgressControllerTest extends TestCase
Sanctum::actingAs($other);
$response = $this->getJson('/api/v1/class-progress/'.$report->id);
$response = $this->getJson('/api/v1/class-progress/' . $report->id);
$response->assertStatus(403);
}
@@ -187,7 +187,7 @@ class ClassProgressControllerTest extends TestCase
Sanctum::actingAs($user);
$response = $this->get('/api/v1/class-progress/attachments/'.$attachment->id);
$response = $this->get('/api/v1/class-progress/attachments/' . $attachment->id);
$response->assertOk();
}
@@ -257,7 +257,7 @@ class ClassSectionControllerTest extends TestCase
'firstname' => 'Test',
'lastname' => 'User',
'cellphone' => '5555555555',
'email' => $roleName.'@example.com',
'email' => $roleName . '@example.com',
'address_street' => '123 Main',
'city' => 'City',
'state' => 'ST',
@@ -50,8 +50,7 @@ class CommunicationControllerTest extends TestCase
{
$this->seedCommunicationData();
$fake = new class extends EmailService
{
$fake = new class extends EmailService {
public array $sent = [];
public function send(
@@ -63,7 +62,6 @@ class CommunicationControllerTest extends TestCase
array $bcc = []
): bool {
$this->sent[] = compact('to', 'subject', 'html', 'fromKey', 'cc', 'bcc');
return true;
}
};
@@ -23,7 +23,7 @@ class FamilyAdminControllerTest extends TestCase
$this->seedFamilyGuardian($familyId, $user->id);
Sanctum::actingAs($user);
$response = $this->getJson('/api/v1/family-admin?student_id='.$studentId);
$response = $this->getJson('/api/v1/family-admin?student_id=' . $studentId);
$response->assertOk();
$response->assertJsonPath('status', true);
@@ -58,7 +58,7 @@ class FamilyAdminControllerTest extends TestCase
$this->seedPayment($user->id);
Sanctum::actingAs($user);
$response = $this->getJson('/api/v1/family-admin/card?family_id='.$familyId);
$response = $this->getJson('/api/v1/family-admin/card?family_id=' . $familyId);
$response->assertOk();
$response->assertJsonPath('status', true);
@@ -123,7 +123,7 @@ class FamilyAdminControllerTest extends TestCase
'firstname' => 'Parent',
'lastname' => 'User',
'cellphone' => '9999999999',
'email' => 'parent'.$id.'@example.com',
'email' => 'parent' . $id . '@example.com',
'address_street' => '123 Street',
'city' => 'City',
'state' => 'ST',
@@ -163,7 +163,7 @@ class FamilyAdminControllerTest extends TestCase
{
return DB::table('families')->insertGetId([
'family_code' => $code,
'household_name' => 'Family '.$code,
'household_name' => 'Family ' . $code,
'is_active' => 1,
'created_at' => now(),
'updated_at' => now(),
@@ -20,7 +20,7 @@ class FamilyControllerTest extends TestCase
$this->seedFamilyStudent($familyId, $studentId);
Sanctum::actingAs($user);
$response = $this->getJson('/api/v1/families/by-student/'.$studentId);
$response = $this->getJson('/api/v1/families/by-student/' . $studentId);
$response->assertOk();
$response->assertJsonPath('status', true);
@@ -34,7 +34,7 @@ class FamilyControllerTest extends TestCase
$this->seedFamilyGuardian($familyId, $user->id);
Sanctum::actingAs($user);
$response = $this->getJson('/api/v1/families/'.$familyId.'/guardians');
$response = $this->getJson('/api/v1/families/' . $familyId . '/guardians');
$response->assertOk();
$response->assertJsonPath('status', true);
@@ -292,7 +292,7 @@ class FamilyControllerTest extends TestCase
{
return DB::table('families')->insertGetId([
'family_code' => $code,
'household_name' => 'Family '.$code,
'household_name' => 'Family ' . $code,
'is_active' => 1,
'created_at' => now(),
'updated_at' => now(),
@@ -28,7 +28,7 @@ class EventChargeControllerTest extends TestCase
{
$charge = $this->makeCharge();
$this->getJson('/api/v1/finance/event-charges/'.$charge->id)
$this->getJson('/api/v1/finance/event-charges/' . $charge->id)
->assertStatus(401);
}
@@ -37,7 +37,7 @@ class EventChargeControllerTest extends TestCase
$this->actingAsApiAdministrator();
$charge = $this->makeCharge();
$this->getJson('/api/v1/finance/event-charges/'.$charge->id)
$this->getJson('/api/v1/finance/event-charges/' . $charge->id)
->assertOk()
->assertJsonPath('ok', true)
->assertJsonPath('event_charge.id', $charge->id)
@@ -57,7 +57,7 @@ class EventChargeControllerTest extends TestCase
$this->actingAsApiAdministrator();
$charge = $this->makeCharge();
$this->putJson('/api/v1/finance/event-charges/'.$charge->id, [
$this->putJson('/api/v1/finance/event-charges/' . $charge->id, [
'event_name' => 'Museum Visit',
'amount' => 40.50,
])
@@ -76,7 +76,7 @@ class EventChargeControllerTest extends TestCase
$this->actingAsApiAdministrator();
$charge = $this->makeCharge(['charged' => 0]);
$this->postJson('/api/v1/finance/event-charges/'.$charge->id.'/approve')
$this->postJson('/api/v1/finance/event-charges/' . $charge->id . '/approve')
->assertOk()
->assertJsonPath('ok', true);
@@ -91,7 +91,7 @@ class EventChargeControllerTest extends TestCase
$this->actingAsApiAdministrator();
$charge = $this->makeCharge(['charged' => 1]);
$this->postJson('/api/v1/finance/event-charges/'.$charge->id.'/void')
$this->postJson('/api/v1/finance/event-charges/' . $charge->id . '/void')
->assertOk()
->assertJsonPath('ok', true);
@@ -106,7 +106,7 @@ class EventChargeControllerTest extends TestCase
$this->actingAsApiAdministrator();
$charge = $this->makeCharge(['charged' => 1]);
$this->deleteJson('/api/v1/finance/event-charges/'.$charge->id)
$this->deleteJson('/api/v1/finance/event-charges/' . $charge->id)
->assertOk()
->assertJsonPath('ok', true);
@@ -49,7 +49,7 @@ class FrontendControllerTest extends TestCase
$user = User::query()->create([
'firstname' => 'Test',
'lastname' => 'User',
'email' => $roleName.'@example.com',
'email' => $roleName . '@example.com',
'cellphone' => '5555555555',
'address_street' => '123 Main',
'city' => 'City',
@@ -4,6 +4,7 @@ namespace Tests\Feature\Api\V1\Frontend;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
@@ -57,7 +57,7 @@ class LandingPageControllerTest extends TestCase
$user = User::query()->create([
'firstname' => 'Test',
'lastname' => 'User',
'email' => $roleName.'@example.com',
'email' => $roleName . '@example.com',
'cellphone' => '5555555555',
'address_street' => '123 Main',
'city' => 'City',
@@ -15,7 +15,7 @@ class PageControllerTest extends TestCase
{
$dir = public_path('html');
File::ensureDirectoryExists($dir);
File::put($dir.'/privacy_policy.html', '<h1>Privacy</h1>');
File::put($dir . '/privacy_policy.html', '<h1>Privacy</h1>');
$response = $this->getJson('/api/v1/pages/privacy');
@@ -28,7 +28,7 @@ class PageControllerTest extends TestCase
{
$dir = public_path('html');
File::ensureDirectoryExists($dir);
File::delete($dir.'/help_center.html');
File::delete($dir . '/help_center.html');
$response = $this->getJson('/api/v1/pages/help');
@@ -0,0 +1,173 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\Concerns\AssertsE2EApiResponses;
use Tests\Concerns\SeedsE2ETestFixtures;
use Tests\TestCase;
/**
* Classroom, attendance, grading, and report-card full-surface probes.
*/
class ApiAcademicAttendanceFullSurfaceContractTest extends TestCase
{
use RefreshDatabase;
use SeedsE2ETestFixtures;
use AssertsE2EApiResponses;
public function test_academic_attendance_and_grading_surfaces_have_controlled_contracts(): void
{
$this->seedE2EConfiguration();
$world = $this->seedTeacherClassWithStudent(9301);
$this->actingAs($world['teacher'], 'api');
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);
}
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->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);
}
}
/** @param array<string, mixed> $world @return list<string> */
private function teacherGetEndpoints(array $world): array
{
$classSectionId = (int) $world['class_section_id'];
$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/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/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/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/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/staff/month?month=2025-10',
'/api/v1/attendance/staff/admins',
'/api/v1/attendance/late-slip-logs',
'/api/v1/attendance/late-slip-logs/1',
'/api/v1/attendance/early-dismissals',
'/api/v1/attendance/early-dismissals/student-options',
'/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/compose',
'/api/v1/attendance-tracking/parents-info',
'/api/v1/attendance-comment-templates',
'/api/v1/attendance-comment-templates/list-data',
'/api/v1/attendance-comment-templates/1',
'/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,
];
}
/** @param array<string, mixed> $world @return list<array{0: string, 1: string, 2: array<string, mixed>}> */
private function teacherMutationEndpoints(array $world): array
{
$studentId = (int) $world['student_id'];
$classSectionId = (int) $world['class_section_id'];
$scorePayload = ['class_section_id' => $classSectionId, 'student_id' => $studentId, 'score' => 95, 'points' => 10, 'column' => 'E2E Probe'];
return [
['POST', '/api/v1/attendance/teacher/submit', ['class_section_id' => $classSectionId, 'attendance' => [$studentId => 'present'], 'date' => '2025-10-05']],
['POST', '/api/v1/attendance/admin/update', ['student_id' => $studentId, 'status' => 'present', 'date' => '2025-10-05']],
['POST', '/api/v1/attendance/admin/add-entry', ['student_id' => $studentId, 'status' => 'present', 'date' => '2025-10-05']],
['POST', '/api/v1/attendance/staff/admins/save', ['attendance' => []]],
['POST', '/api/v1/attendance/staff/cell', ['user_id' => $world['teacher']->id, 'date' => '2025-10-05', 'status' => 'present']],
['DELETE', '/api/v1/attendance/late-slip-logs/1', []],
['POST', '/api/v1/attendance/early-dismissals', ['student_id' => $studentId, 'reason' => 'Contract probe', 'dismissal_time' => '12:00']],
['POST', '/api/v1/attendance/early-dismissals/signature', ['student_id' => $studentId, 'signature' => 'data:image/png;base64,ZmFrZQ==']],
['POST', '/api/v1/attendance/management/scan', ['badge_code' => 'INVALID-BADGE']],
['POST', '/api/v1/attendance/management/manual-entry', ['student_id' => $studentId, 'status' => 'present']],
['POST', '/api/v1/attendance/management/exit-entry', ['student_id' => $studentId]],
['POST', '/api/v1/attendance/management/absent', ['student_id' => $studentId]],
['POST', '/api/v1/attendance/management/1/follow-up', ['notes' => 'Contract probe']],
['POST', '/api/v1/attendance/management/1/late-slip-reprint', ['reason' => 'Contract probe']],
['POST', '/api/v1/attendance-tracking/record', ['student_id' => $studentId, 'violation_type' => 'late', 'date' => '2025-10-05']],
['POST', '/api/v1/attendance-tracking/send-auto-emails', ['student_ids' => [$studentId]]],
['POST', '/api/v1/attendance-tracking/send-manual-email', ['student_id' => $studentId, 'message' => 'Contract probe']],
['POST', '/api/v1/attendance-tracking/save-notification-note', ['student_id' => $studentId, 'note' => 'Contract probe']],
['POST', '/api/v1/attendance-comment-templates', ['name' => 'Probe', 'comment' => 'Contract probe']],
['PUT', '/api/v1/attendance-comment-templates/1', ['name' => 'Probe Updated', 'comment' => 'Contract probe updated']],
['DELETE', '/api/v1/attendance-comment-templates/1', []],
['POST', '/api/v1/attendance-templates/save', ['name' => 'Probe', 'comment' => 'Contract probe']],
['POST', '/api/v1/attendance-templates/delete', ['id' => 1]],
['POST', '/api/v1/assignments', ['class_section_id' => $classSectionId, 'title' => 'Contract Assignment']],
['POST', '/api/v1/teacher/add-homework', $scorePayload],
['POST', '/api/v1/teacher/add-quiz', $scorePayload],
['POST', '/api/v1/teacher/add-quiz-column-form', ['class_section_id' => $classSectionId, 'name' => 'Probe Quiz']],
['POST', '/api/v1/teacher/add-midterm-exam', $scorePayload],
['POST', '/api/v1/teacher/add-final-exam', $scorePayload],
['POST', '/api/v1/teacher/add-participation', $scorePayload],
['POST', '/api/v1/teacher/add-project', $scorePayload],
['POST', '/api/v1/teacher/exam-drafts', ['class_section_id' => $classSectionId, 'title' => 'Contract Draft', 'content' => 'Draft body']],
['POST', '/api/v1/teacher/drafts', ['class_section_id' => $classSectionId, 'title' => 'Contract Draft', 'content' => 'Draft body']],
['POST', '/api/v1/teacher/inventory/books/distribute', ['student_id' => $studentId, 'item_id' => 1, 'quantity' => 1]],
['POST', '/api/v1/teacher/absence-vacation', ['date' => '2025-10-05', 'reason' => 'Contract probe']],
['POST', '/api/v1/teacher/class-progress-submit', ['class_section_id' => $classSectionId, 'student_id' => $studentId, 'progress' => 'Good']],
];
}
/** @param array<string, mixed> $world @return list<array{0: string, 1: string, 2: array<string, mixed>}> */
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/badges/form-options', []],
['POST', '/api/v1/print-requests', ['title' => 'Contract Probe', 'copies' => 1, 'needed_by' => '2025-10-12']],
['GET', '/api/v1/print-requests/admin', []],
['PATCH', '/api/v1/print-requests/1/status', ['status' => 'completed']],
['GET', '/api/v1/print-requests/teacher', []],
['PATCH', '/api/v1/print-requests/1', ['title' => 'Contract Probe Updated']],
['DELETE', '/api/v1/print-requests/1', []],
];
}
}
@@ -0,0 +1,31 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiAccountStatusLifecycleContractTest extends FullSurfaceE2EContractCase
{
public function test_suspended_disabled_and_pending_status_values_do_not_gain_access_through_profile_or_dashboard_routes(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['auth/me', 'frontend', 'dashboard', 'profile', 'preferences', 'notifications']), 0, 80);
$statuses = ['suspended', 'disabled', 'inactive', 'pending', 'blocked'];
foreach ($statuses as $status) {
$actor = $this->createApiUserWithRole('parent', ['status' => $status]);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if ($method !== 'GET') {
continue;
}
$uri = $route->uri();
$response = $this->requestAs($actor, 'GET', $uri);
$this->assertControlled($response, 'GET', $uri);
$this->assertNoServerError($response, 'account status ' . $status . ' at GET ' . $uri);
}
}
}
}
@@ -0,0 +1,163 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\Concerns\AssertsE2EApiResponses;
use Tests\Concerns\SeedsE2ETestFixtures;
use Tests\TestCase;
/**
* Administrator full-surface operational contract checks.
*/
class ApiAdminOperationsFullSurfaceContractTest extends TestCase
{
use RefreshDatabase;
use SeedsE2ETestFixtures;
use AssertsE2EApiResponses;
public function test_administrator_operational_surface_responds_with_controlled_contracts(): void
{
$this->seedE2EConfiguration();
$world = $this->seedTeacherClassWithStudent(9201);
$admin = $this->actingAsApiAdministrator();
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);
}
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);
}
}
/**
* @param array<string, mixed> $world
* @return list<string>
*/
private function adminGetEndpoints(array $world): array
{
$studentId = (int) $world['student_id'];
$classSectionId = (int) $world['class_section_id'];
return [
'/api/v1/administrator/attendance-templates',
'/api/v1/administrator/absence',
'/api/v1/administrator/dashboard/metrics',
'/api/v1/administrator/dashboard/user-search?query=Fixture',
'/api/v1/administrator/teacher-submissions',
'/api/v1/administrator/trophy',
'/api/v1/administrator/trophy/winners',
'/api/v1/administrator/trophy/final',
'/api/v1/administrator/teacher-class/assignments',
'/api/v1/administrator/notifications/alerts',
'/api/v1/administrator/notifications/print-recipients',
'/api/v1/administrator/enrollment-withdrawal',
'/api/v1/administrator/enrollment-withdrawal/new-students',
'/api/v1/administrator/enroll-withdrawal',
'/api/v1/administrator/enroll-withdrawal/new-students',
'/api/v1/administrator/promotions',
'/api/v1/administrator/promotions/summary',
'/api/v1/administrator/promotions/levels',
'/api/v1/administrator/promotions/1',
'/api/v1/administrator/promotions/1/reminders',
'/api/v1/administrator/promotions/1/audit',
'/api/v1/administrator/emergency-contacts',
'/api/v1/administrator/emergency-contacts/data',
'/api/v1/administrator/emergency-contacts/1',
'/api/v1/administrator/staff',
'/api/v1/administrator/staff/1',
'/api/v1/administrator/flags/pending',
'/api/v1/administrator/flags/form-meta',
'/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/expenses',
'/api/v1/administrator/expenses/options',
'/api/v1/administrator/expenses/1',
'/api/v1/administrator/reports/financial-detailed',
'/api/v1/administrator/reports/stakeholder-analysis',
'/api/v1/administrator/reports/parent-payment-followups',
'/api/v1/administrator/invoices/management',
'/api/v1/administrator/paypal-transactions',
'/api/v1/administrator/reimbursements/under-processing',
'/api/v1/administrator/reimbursements/export',
'/api/v1/administrator/reimbursements/reimbursed-expenses',
'/api/v1/administrator/reimbursements',
'/api/v1/administrator/printables/badges/form-options',
'/api/v1/administrator/printables/report-card/meta',
'/api/v1/administrator/certificates/dashboard',
'/api/v1/administrator/certificates/form-options',
'/api/v1/administrator/certificates/audit-log',
'/api/v1/administrator/certificates/reprint/CERT-E2E-001',
'/api/v1/administrator/role-permission/roles',
'/api/v1/administrator/role-permission/roles/1',
'/api/v1/administrator/role-permission/users',
'/api/v1/administrator/role-permission/permissions',
'/api/v1/administrator/role-permission/permissions/1',
'/api/v1/administrator/role-permission/roles/1/permissions',
'/api/v1/school-years',
'/api/v1/school-years/current',
'/api/v1/school-years/options',
'/api/v1/school-years/1/summary',
'/api/v1/school-years/1/parent-balances',
'/api/v1/school-years/1/promotion-preview',
'/api/v1/class-sections',
'/api/v1/classes',
'/api/v1/users',
'/api/v1/users/' . $world['parent_id'],
'/api/v1/students/' . $studentId,
];
}
/**
* @param array<string, mixed> $world
* @return list<array{0: string, 1: string, 2: array<string, mixed>}>
*/
private function adminMutationEndpoints(array $world, int $adminId): array
{
$studentId = (int) $world['student_id'];
$teacherId = (int) $world['teacher']->id;
$classSectionId = (int) $world['class_section_id'];
return [
['POST', '/api/v1/administrator/absence', ['user_id' => $adminId, 'date' => '2025-10-05', 'reason' => 'Contract probe']],
['POST', '/api/v1/administrator/teacher-submissions/notify', ['teacher_id' => $teacherId, 'message' => 'Contract probe']],
['POST', '/api/v1/administrator/teacher-class/assign', ['teacher_id' => $teacherId, 'class_section_id' => $classSectionId, 'position' => 'main']],
['POST', '/api/v1/administrator/teacher-class/delete', ['teacher_id' => $teacherId, 'class_section_id' => $classSectionId]],
['POST', '/api/v1/administrator/notifications/alerts', ['alerts' => ['contract_probe' => true]]],
['POST', '/api/v1/administrator/notifications/print-recipients', ['recipients' => [$adminId]]],
['POST', '/api/v1/administrator/enrollment-withdrawal/update-statuses', ['enrollment_status' => [$studentId => 'enrolled']]],
['POST', '/api/v1/administrator/promotions/evaluate', ['student_ids' => [$studentId]]],
['POST', '/api/v1/administrator/promotions/reminders/dispatch', ['dry_run' => true]],
['POST', '/api/v1/administrator/promotions/deadlines/expire', ['dry_run' => true]],
['POST', '/api/v1/administrator/promotions/deadlines', ['deadline' => '2025-12-01']],
['POST', '/api/v1/administrator/promotions/placement/preview', ['student_ids' => [$studentId]]],
['POST', '/api/v1/administrator/promotions/placement/batches/1/finalize', ['confirm' => true]],
['POST', '/api/v1/administrator/promotions/levels', ['from_level' => '1', 'to_level' => '2']],
['POST', '/api/v1/administrator/promotions/levels/seed', ['confirm' => true]],
['PATCH', '/api/v1/administrator/promotions/1/status', ['status' => 'approved']],
['PATCH', '/api/v1/administrator/promotions/1/enrollment-steps', ['steps' => ['confirmed' => true]]],
['PATCH', '/api/v1/administrator/emergency-contacts/1', ['name' => 'Contract Contact', 'phone' => '5551234567']],
['DELETE', '/api/v1/administrator/emergency-contacts/1', []],
['POST', '/api/v1/administrator/staff', ['firstname' => 'Staff', 'lastname' => 'Probe', 'email' => 'staff.probe@example.test']],
['PATCH', '/api/v1/administrator/staff/1', ['firstname' => 'Staff Updated']],
['DELETE', '/api/v1/administrator/staff/1', []],
['POST', '/api/v1/administrator/flags', ['student_id' => $studentId, 'reason' => 'Contract probe']],
['POST', '/api/v1/administrator/flags/1/state', ['state' => 'reviewing']],
['POST', '/api/v1/administrator/flags/1/close', ['resolution' => 'closed']],
['POST', '/api/v1/administrator/flags/1/cancel', ['reason' => 'cancelled']],
['POST', '/api/v1/school-years', ['name' => '2026-2027', 'start_date' => '2026-09-01', 'end_date' => '2027-06-30']],
['PATCH', '/api/v1/school-years/1', ['status' => 'active']],
['POST', '/api/v1/school-years/1/validate-close', ['confirm' => true]],
['POST', '/api/v1/school-years/1/preview-close', ['confirm' => true]],
['POST', '/api/v1/school-years/1/close', ['confirm' => true]],
['POST', '/api/v1/school-years/1/reopen', ['confirm' => true]],
];
}
}
@@ -0,0 +1,55 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Illuminate\Http\UploadedFile;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiAttachmentUploadContentSafetyContractTest extends FullSurfaceE2EContractCase
{
public function test_upload_endpoints_reject_executable_or_misleading_files_cleanly(): void
{
$routes = $this->apiRoutesContainingAny(['upload', 'import', 'attachment', 'document', 'avatar', 'photo', 'file']);
$this->assertNotEmpty($routes, 'Upload-like routes should be covered by content safety tests.');
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
foreach (['file', 'attachment', 'document', 'import_file', 'avatar'] as $field) {
$payload = $this->payloadFor($method, $route->uri()) + [
$field => UploadedFile::fake()->create('malware.php', 4, 'application/x-php'),
];
$response = $this->actingAs($this->actorFor($route->uri()), 'api')
->json($method, $this->materializePath($route->uri()), $payload);
$this->assertStatusIn($response, [400, 403, 404, 409, 413, 415, 422], "{$method} {$route->uri()} should reject executable {$field}.");
$this->assertStringNotContainsString('<?php', $response->getContent());
}
}
}
public function test_download_and_file_response_routes_do_not_allow_path_traversal_parameters(): void
{
foreach ($this->apiRoutesContainingAny(['download', 'export', 'file', 'document', 'receipt', 'certificate']) as $route) {
$method = $this->primaryMethod($route);
if ($method !== 'GET') {
continue;
}
$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");
$body = $response->getContent();
$this->assertStringNotContainsString('APP_KEY=', $body);
$this->assertStringNotContainsString('DB_PASSWORD=', $body);
}
}
}
@@ -0,0 +1,46 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiAuditLogAndAdministrativeTraceabilityContractTest extends FullSurfaceE2EContractCase
{
public function test_admin_mutations_return_traceable_status_without_exposing_internal_log_records(): void
{
$routes = $this->apiRoutesContainingAny(['users', 'students', 'school-years', 'finance', 'inventory', 'permissions', 'roles', 'settings']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
continue;
}
$uri = $route->uri();
$response = $this->requestAs($this->admin, $method, $uri, $this->payloadFor($method, $uri));
$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.');
}
}
public function test_audit_and_log_routes_are_not_mutable_by_public_or_low_privilege_users(): void
{
$routes = $this->apiRoutesContainingAny(['audit', 'logs', 'activity', 'history']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
$uri = $route->uri();
$public = $this->json($method, $this->materializePath($uri), $this->payloadFor($method, $uri));
$parent = $this->requestAs($this->parent, $method, $uri, $this->payloadFor($method, $uri));
$this->assertNoServerError($public, "$method $uri public audit route");
$this->assertNoServerError($parent, "$method $uri parent audit route");
$this->assertStatusIn($public, [302, 400, 401, 403, 404, 405, 419, 422], "$method $uri public audit route");
$this->assertStatusIn($parent, [302, 400, 401, 403, 404, 405, 419, 422], "$method $uri parent audit route");
}
}
}
@@ -0,0 +1,90 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiAuthenticationLifecycleDepthContractTest extends FullSurfaceE2EContractCase
{
public function test_authentication_lifecycle_handles_login_me_refresh_logout_and_invalid_credentials_consistently(): void
{
$loginRoutes = [
'/api/v1/auth/login',
'/api/login',
'/user/login',
];
foreach ($loginRoutes as $path) {
$response = $this->postJson($path, [
'email' => $this->admin->email,
'password' => 'password',
]);
$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.'
);
}
}
foreach ($loginRoutes as $path) {
$response = $this->postJson($path, [
'email' => $this->admin->email,
'password' => 'definitely-wrong-password',
]);
$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);
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.'
);
}
}
foreach (['/api/v1/auth/logout', '/api/logout', '/user/logout'] as $path) {
$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);
}
}
public function test_password_reset_and_registration_like_routes_do_not_leak_account_existence(): void
{
$routes = $this->apiRoutesContainingAny(['forgot', 'password', 'reset', 'register', 'verify']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
$uri = $route->uri();
$response = $this->json($method, $this->materializePath($uri), [
'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.');
}
}
}
@@ -0,0 +1,29 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiAuthorizationCacheInvalidationContractTest extends FullSurfaceE2EContractCase
{
public function test_role_and_permission_mutations_do_not_make_low_privilege_actor_admin_by_cache_accident(): void
{
$privilegedRoutes = array_slice($this->apiRoutesContainingAny(['roles', 'permissions', 'users', 'settings', 'school-years', 'finance', 'inventory']), 0, 100);
foreach ($privilegedRoutes as $route) {
$method = $this->primaryMethod($route);
$uri = $route->uri();
$payload = $this->payloadFor($method, $uri) + [
'role' => 'administrator',
'roles' => ['administrator'],
'permissions' => ['*', 'admin.*', 'users.delete'],
'is_admin' => true,
];
$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);
}
}
}
@@ -0,0 +1,59 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch11RouteRegressionNetTest extends FullSurfaceE2EContractCase
{
public function test_every_api_family_has_at_least_one_negative_contract_probe_in_batch_11(): void
{
$families = [
'auth' => ['auth', 'login', 'logout', 'frontend'],
'school_year' => ['school-year', 'school_year', 'semester'],
'academic' => ['scores', 'grading', 'homework', 'quiz', 'report-card'],
'attendance' => ['attendance', 'absence', 'late', 'dismissal'],
'finance' => ['finance', 'invoice', 'payment', 'refund'],
'inventory' => ['inventory', 'supplier', 'supply'],
'communications' => ['messages', 'support', 'email', 'whatsapp', 'notification'],
'family' => ['parent', 'family', 'guardian', 'authorized'],
'admin' => ['admin', 'users', 'roles', 'permissions', 'settings'],
'files' => ['import', 'export', 'download', 'print', 'certificate', 'badge'],
];
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.'
);
}
}
public function test_batch_11_high_risk_routes_return_controlled_responses_under_combined_hostile_input(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['auth', 'attendance', 'finance', 'students', 'users', 'settings', 'export', 'import', 'webhook', 'messages']), 0, 160);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
$uri = $route->uri();
$payload = $this->payloadFor($method, $uri) + [
'id' => '../../../etc/passwd',
'student_id' => ['bad' => true],
'amount' => 'Infinity',
'notes' => '<script>alert(1)</script>',
'url' => 'http://169.254.169.254/latest/meta-data/',
'return_url' => 'https://evil.example',
];
$this->actingAs($this->actorFor($uri), 'api');
$response = $this->withHeaders([
'Accept' => 'application/json',
'X-Forwarded-For' => '127.0.0.1',
'Idempotency-Key' => 'batch-11-regression-net',
])->json($method, $this->materializePath($uri), $payload);
$this->assertControlled($response, $method, $uri);
$this->assertNoServerError($response, 'batch 11 combined hostile probe at ' . $method . ' ' . $uri);
}
}
}
@@ -0,0 +1,49 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch12AcademicGradeBoundaryContractTest extends FullSurfaceE2EContractCase
{
public function test_grade_and_score_routes_reject_out_of_range_and_wrong_student_contexts(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['score', 'grade', 'homework', 'quiz', 'project', 'midterm', 'final', 'participation', 'report-card']), 0, 160);
$scores = [-1, 101, 'A+++', 'NaN', 'Infinity', 999999];
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
foreach ($scores as $score) {
$response = $this->requestAs($this->teacher, $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [
'score' => $score,
'points' => $score,
'student_id' => $this->ids['invoiceId'],
'class_section_id' => $this->ids['invoiceId'],
]);
$this->assertControlled($response, $method, $route->uri());
$this->assertNoServerError($response, 'academic grade boundary '.$method.' '.$route->uri());
}
}
}
public function test_locked_or_finalized_grade_routes_do_not_accept_low_privilege_overrides(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['lock', 'unlock', 'finalize', 'publish', 'report-card']), 0, 100);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
continue;
}
$response = $this->requestAs($this->parent, $method, $route->uri(), $this->payloadFor($method, $route->uri()) + ['force' => true, 'locked' => false]);
$this->assertStatusIn($response, [401, 403, 404, 405, 409, 419, 422], 'parent grade finalization override '.$method.' '.$route->uri());
$this->assertNoServerError($response, 'parent grade finalization override '.$method.' '.$route->uri());
}
}
}
@@ -0,0 +1,56 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
use Illuminate\Http\UploadedFile;
class ApiBatch12AttachmentLifecycleContractTest extends FullSurfaceE2EContractCase
{
public function test_attachment_upload_routes_handle_multiple_file_shapes_cleanly(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['upload', 'attachment', 'document', 'import', 'media', 'file']), 0, 120);
$files = [
UploadedFile::fake()->create('normal.pdf', 12, 'application/pdf'),
UploadedFile::fake()->create('image-as-pdf.pdf', 12, 'image/png'),
UploadedFile::fake()->create('script.php', 1, 'application/x-php'),
];
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
foreach ($files as $file) {
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [
'file' => $file,
'attachment' => $file,
'document' => $file,
]);
$this->assertControlled($response, $method, $route->uri());
$this->assertNoServerError($response, 'attachment lifecycle '.$method.' '.$route->uri());
}
}
}
public function test_file_download_routes_reject_path_and_storage_driver_probing(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['download', 'file', 'media', 'receipt', 'certificate', 'badge']), 0, 100);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), [
'path' => '../../.env',
'disk' => 's3',
'filename' => '../storage/logs/laravel.log',
]);
$this->assertControlled($response, $method, $route->uri());
$this->assertNoServerError($response, 'file path probe '.$method.' '.$route->uri());
$this->assertStringNotContainsString('APP_KEY', $response->getContent(), 'download route exposed environment content');
}
}
}
@@ -0,0 +1,51 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch12AttendanceCorrectionAuditContractTest extends FullSurfaceE2EContractCase
{
public function test_attendance_corrections_require_valid_actor_date_status_and_reason(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['attendance', 'absence', 'late', 'dismissal', 'slip']), 0, 150);
$payloads = [
['status' => 'present', 'reason' => ''],
['status' => 'teleported', 'reason' => 'nonsense'],
['date' => '1900-01-01', 'reason' => 'too old'],
['date' => '2099-12-31', 'reason' => 'future'],
['student_id' => $this->ids['invoiceId'], 'reason' => 'wrong id type'],
];
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
foreach ($payloads as $payload) {
$response = $this->requestAs($this->teacher, $method, $route->uri(), $this->payloadFor($method, $route->uri()) + $payload);
$this->assertControlled($response, $method, $route->uri());
$this->assertNoServerError($response, 'attendance correction audit '.$method.' '.$route->uri());
}
}
}
public function test_parent_and_student_cannot_correct_attendance_records(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['attendance', 'absence', 'late', 'dismissal']), 0, 100);
foreach ([$this->parent, $this->studentUser] as $actor) {
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
continue;
}
$response = $this->requestAs($actor, $method, $route->uri(), $this->payloadFor($method, $route->uri()));
$this->assertStatusIn($response, [401, 403, 404, 405, 409, 419, 422], 'low privilege attendance correction '.$method.' '.$route->uri());
$this->assertNoServerError($response, 'low privilege attendance correction '.$method.' '.$route->uri());
}
}
}
}
@@ -0,0 +1,47 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch12BulkMutationBoundaryContractTest extends FullSurfaceE2EContractCase
{
public function test_bulk_mutation_routes_reject_empty_and_cross_domain_targets_cleanly(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['bulk', 'batch', 'assign', 'remove', 'delete', 'archive', 'restore', 'import']), 0, 140);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
continue;
}
foreach ([
['ids' => []],
['ids' => [$this->ids['studentId'], $this->ids['invoiceId'], $this->ids['classSectionId']]],
['student_ids' => [$this->ids['invoiceId']], 'invoice_ids' => [$this->ids['studentId']]],
] as $payload) {
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + $payload);
$this->assertControlled($response, $method, $route->uri());
$this->assertNoServerError($response, 'bulk boundary '.$method.' '.$route->uri());
}
}
}
public function test_bulk_routes_do_not_accept_unbounded_target_lists(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['bulk', 'batch', 'assign', 'import']), 0, 100);
$ids = range(1, 750);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + ['ids' => $ids, 'student_ids' => $ids]);
$this->assertControlled($response, $method, $route->uri());
$this->assertNoServerError($response, 'unbounded bulk list '.$method.' '.$route->uri());
}
}
}
@@ -0,0 +1,51 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch12CombinedRegressionSweepTest extends FullSurfaceE2EContractCase
{
public function test_batch_12_combined_security_business_rule_and_contract_probe(): void
{
$routes = array_slice($this->apiRoutesContainingAny([
'auth', 'users', 'roles', 'permissions', 'students', 'parents', 'family',
'attendance', 'scores', 'report-card', 'finance', 'invoice', 'payment',
'inventory', 'import', 'export', 'webhook', 'settings', 'messages',
]), 0, 220);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
$uri = $route->uri();
$payload = $this->payloadFor($method, $uri) + [
'scope' => 'all',
'force' => true,
'is_admin' => true,
'role' => 'administrator',
'student_id' => $this->ids['invoiceId'],
'invoice_id' => $this->ids['studentId'],
'amount' => '1e309',
'status' => '<script>alert(1)</script>',
'return_url' => 'https://evil.example/after-login',
'callback_url' => 'http://169.254.169.254/latest/meta-data/',
'fields' => ['*', 'password', 'remember_token'],
];
$this->actingAs($this->actorFor($uri), 'api');
$response = $this->withHeaders([
'Accept' => 'application/json',
'X-Admin' => '1',
'X-Original-Method' => 'DELETE',
'Idempotency-Key' => 'batch-12-combined-regression',
])->json($method, $this->materializePath($uri), $payload);
$this->assertControlled($response, $method, $uri);
$this->assertNoServerError($response, 'batch 12 combined regression '.$method.' '.$uri);
$body = $response->getContent();
$this->assertStringNotContainsString('SQLSTATE', $body, 'batch 12 response leaked SQL at '.$uri);
$this->assertStringNotContainsString('APP_KEY', $body, 'batch 12 response leaked env at '.$uri);
$this->assertStringNotContainsString('remember_token', strtolower($body), 'batch 12 response leaked auth internals at '.$uri);
}
}
}
@@ -0,0 +1,46 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch12ConsistencyUnderPartialPayloadContractTest extends FullSurfaceE2EContractCase
{
public function test_patch_routes_do_not_null_identity_or_ownership_when_optional_fields_are_missing(): void
{
$routes = array_slice($this->apiRoutes(), 0, 250);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['PUT', 'PATCH'], true)) {
continue;
}
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), ['notes' => 'partial batch 12 update']);
$this->assertControlled($response, $method, $route->uri());
$this->assertNoServerError($response, 'partial payload update '.$method.' '.$route->uri());
if ($response->getStatusCode() >= 200 && $response->getStatusCode() < 300) {
$body = strtolower($response->getContent());
$this->assertStringNotContainsString('"id":null', $body, 'successful partial update nulled id');
$this->assertStringNotContainsString('"user_id":null', $body, 'successful partial update nulled owner');
}
}
}
public function test_post_routes_handle_sparse_payloads_as_validation_not_crashes(): void
{
$routes = array_slice($this->apiRoutes(), 0, 250);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if ($method !== 'POST') {
continue;
}
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), ['name' => null]);
$this->assertControlled($response, $method, $route->uri());
$this->assertNoServerError($response, 'sparse post payload '.$method.' '.$route->uri());
}
}
}
@@ -0,0 +1,53 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch12CrossActorDataLeakageContractTest extends FullSurfaceE2EContractCase
{
public function test_parent_teacher_and_student_scoped_reads_do_not_leak_admin_only_fields(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['students', 'parents', 'teacher', 'attendance', 'invoice', 'report-card', 'messages', 'profile']), 0, 150);
$sensitiveKeys = ['salary', 'permissions', 'roles_raw', 'password', 'remember_token', 'deleted_at', 'created_by', 'updated_by', 'internal_notes'];
foreach ([$this->parent, $this->teacher, $this->studentUser] as $actor) {
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if ($method !== 'GET') {
continue;
}
$response = $this->requestAs($actor, $method, $route->uri());
$this->assertControlled($response, $method, $route->uri());
$this->assertNoServerError($response, 'cross actor leak '.$method.' '.$route->uri());
$body = strtolower($response->getContent());
foreach ($sensitiveKeys as $key) {
$this->assertStringNotContainsString($key, $body, $route->uri().' leaked sensitive key '.$key);
}
}
}
}
public function test_actor_cannot_select_a_different_owner_scope_with_query_parameters(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['students', 'attendance', 'invoice', 'family', 'messages', 'report-card']), 0, 120);
foreach ($routes as $route) {
if ($this->primaryMethod($route) !== 'GET') {
continue;
}
$response = $this->actingAs($this->parent, 'api')->json('GET', $this->materializePath($route->uri()), [
'parent_id' => $this->ids['parentId'] + 999,
'student_id' => $this->ids['studentId'] + 999,
'family_id' => $this->ids['familyId'] + 999,
'scope' => 'all',
]);
$this->assertControlled($response, 'GET', $route->uri());
$this->assertNoServerError($response, 'owner query override '.$route->uri());
}
}
}
@@ -0,0 +1,51 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch12FinancialLedgerIntegrityContractTest extends FullSurfaceE2EContractCase
{
public function test_financial_mutations_reject_ledger_breaking_amounts_and_cross_invoice_ids(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['finance', 'invoice', 'payment', 'refund', 'charge', 'installment', 'fee']), 0, 150);
$amounts = ['-0.01', '-999999', '999999999999999999999', '1e309', 'NaN', 'Infinity', '0.00000001'];
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
foreach ($amounts as $amount) {
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [
'amount' => $amount,
'paid_amount' => $amount,
'balance' => $amount,
'invoice_id' => $this->ids['studentId'],
]);
$this->assertControlled($response, $method, $route->uri());
$this->assertNoServerError($response, 'financial ledger boundary '.$method.' '.$route->uri());
$this->assertStringNotContainsString('SQLSTATE', $response->getContent(), 'financial route leaked SQL');
}
}
}
public function test_parent_cannot_force_finance_scope_to_all_families(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['invoice', 'payment', 'finance', 'receipt', 'balance']), 0, 100);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
$response = $this->requestAs($this->parent, $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [
'scope' => 'all',
'parent_id' => $this->ids['parentId'] + 777,
'include_voided' => true,
]);
$this->assertControlled($response, $method, $route->uri());
$this->assertNoServerError($response, 'parent finance scope override '.$method.' '.$route->uri());
}
}
}
@@ -0,0 +1,48 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch12GuardianDelegationExpirationContractTest extends FullSurfaceE2EContractCase
{
public function test_guardian_and_authorized_user_routes_validate_expiration_relationship_and_scope(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['guardian', 'authorized', 'family', 'pickup', 'whatsapp']), 0, 130);
$payloads = [
['expires_at' => '1900-01-01', 'relationship' => ''],
['expires_at' => 'not-a-date', 'relationship' => '<script>alert(1)</script>'],
['student_id' => $this->ids['invoiceId'], 'parent_id' => $this->ids['teacherId']],
['can_pickup' => 'yes please', 'can_receive_messages' => ['bad' => true]],
];
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
foreach ($payloads as $payload) {
$response = $this->requestAs($this->parent, $method, $route->uri(), $this->payloadFor($method, $route->uri()) + $payload);
$this->assertControlled($response, $method, $route->uri());
$this->assertNoServerError($response, 'guardian delegation '.$method.' '.$route->uri());
}
}
}
public function test_student_cannot_manage_guardians_or_authorized_users(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['guardian', 'authorized', 'family']), 0, 100);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
continue;
}
$response = $this->requestAs($this->studentUser, $method, $route->uri(), $this->payloadFor($method, $route->uri()));
$this->assertStatusIn($response, [401, 403, 404, 405, 419, 422], 'student guardian mutation '.$method.' '.$route->uri());
$this->assertNoServerError($response, 'student guardian mutation '.$method.' '.$route->uri());
}
}
}
@@ -0,0 +1,41 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch12LongRunningExportJobContractTest extends FullSurfaceE2EContractCase
{
public function test_export_report_and_generation_routes_handle_async_parameters_without_crashing(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['export', 'download', 'print', 'generate', 'report', 'certificate', 'badge', 'receipt']), 0, 160);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
$payload = $this->payloadFor($method, $route->uri()) + [
'async' => true,
'notify_when_ready' => true,
'format' => 'xlsx',
'fields' => ['*', 'password', 'remember_token'],
'filters' => ['scope' => 'all', 'school_year' => self::E2E_SCHOOL_YEAR],
];
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $payload);
$this->assertControlled($response, $method, $route->uri());
$this->assertNoServerError($response, 'long running export job '.$method.' '.$route->uri());
$this->assertStringNotContainsString('remember_token', $response->getContent(), 'export route exposed sensitive fields');
}
}
public function test_low_privilege_users_cannot_request_full_school_exports(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['export', 'download', 'report']), 0, 120);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
$response = $this->requestAs($this->parent, $method, $route->uri(), ['scope' => 'all', 'include' => ['students', 'finance', 'users']]);
$this->assertControlled($response, $method, $route->uri());
$this->assertNoServerError($response, 'low privilege full export '.$method.' '.$route->uri());
}
}
}
@@ -0,0 +1,47 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch12MaintenanceModeAndOperationalControlContractTest extends FullSurfaceE2EContractCase
{
public function test_operational_control_routes_are_admin_only_and_controlled(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['maintenance', 'cache', 'queue', 'job', 'horizon', 'telescope', 'metrics', 'logs', 'debug']), 0, 120);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
$uri = $route->uri();
$parentResponse = $this->requestAs($this->parent, $method, $uri, $this->payloadFor($method, $uri));
$this->assertStatusIn($parentResponse, [401, 403, 404, 405, 419, 422], 'parent must not control operational endpoint '.$method.' '.$uri);
$this->assertNoServerError($parentResponse, 'parent operational probe '.$method.' '.$uri);
$adminResponse = $this->requestAs($this->admin, $method, $uri, $this->payloadFor($method, $uri));
$this->assertControlled($adminResponse, $method, $uri);
$this->assertNoServerError($adminResponse, 'admin operational probe '.$method.' '.$uri);
}
}
public function test_public_requests_cannot_toggle_system_state(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['maintenance', 'cache', 'queue', 'config', 'settings']), 0, 80);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
continue;
}
$response = $this->json($method, $this->materializePath($route->uri()), [
'enabled' => true,
'force' => true,
'confirm' => true,
]);
$this->assertStatusIn($response, [401, 403, 404, 405, 419, 422], 'public system toggle must be blocked '.$method.' '.$route->uri());
$this->assertNoServerError($response, 'public system toggle '.$method.' '.$route->uri());
}
}
}
@@ -0,0 +1,39 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch12RouteMetadataRiskRegisterTest extends FullSurfaceE2EContractCase
{
public function test_high_risk_route_families_exist_in_the_contract_risk_register(): void
{
$riskRegister = [
'identity' => ['auth', 'login', 'logout', 'profile', 'frontend'],
'authorization' => ['roles', 'permissions', 'switch-role', 'impersonate'],
'student_privacy' => ['students', 'parents', 'family', 'guardian'],
'attendance_safety' => ['attendance', 'late', 'dismissal', 'absence'],
'academic_records' => ['scores', 'grades', 'report-card', 'certificate'],
'financial_records' => ['finance', 'invoice', 'payment', 'refund', 'receipt'],
'files' => ['upload', 'download', 'import', 'export', 'print'],
'external_integrations' => ['webhook', 'callback', 'paypal', 'email', 'whatsapp'],
'operations' => ['settings', 'configuration', 'health', 'debug', 'logs'],
];
foreach ($riskRegister as $risk => $needles) {
$this->assertNotEmpty($this->apiRoutesContainingAny($needles), 'Batch 12 risk register missing route family: '.$risk);
}
}
public function test_mutating_high_risk_routes_have_specific_route_names_when_available(): void
{
foreach (array_slice($this->apiRoutesContainingAny(['finance', 'attendance', 'users', 'roles', 'permissions', 'settings', 'import', 'webhook']), 0, 180) as $route) {
if (! in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
continue;
}
$name = $route->getName();
$this->assertTrue($name === null || ! preg_match('/(^|\.)(store|update|destroy)$/', $name) || substr_count((string) $name, '.') >= 2, 'High-risk route name is too generic: '.($name ?? '[unnamed]').' '.$route->uri());
}
}
}
@@ -0,0 +1,27 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch12SearchEnumerationResistanceContractTest extends FullSurfaceE2EContractCase
{
public function test_search_autocomplete_and_lookup_routes_resist_identifier_enumeration(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['search', 'lookup', 'autocomplete', 'find', 'students', 'users', 'parents']), 0, 150);
$queries = ['a', '%', '_', '*', '000000', $this->admin->email, ' OR 1=1 --', '<img src=x onerror=alert(1)>'];
foreach ($routes as $route) {
if ($this->primaryMethod($route) !== 'GET') {
continue;
}
foreach ($queries as $query) {
$response = $this->requestAs($this->parent, 'GET', $route->uri(), ['search' => $query, 'q' => $query, 'email' => $query]);
$this->assertControlled($response, 'GET', $route->uri());
$this->assertNoServerError($response, 'enumeration resistant search '.$route->uri());
$this->assertStringNotContainsString('SQLSTATE', $response->getContent(), 'search leaked SQL');
}
}
}
}
@@ -0,0 +1,43 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch12SensitiveConfigurationExposureContractTest extends FullSurfaceE2EContractCase
{
public function test_configuration_and_bootstrap_responses_do_not_expose_secrets(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['settings', 'configuration', 'config', 'frontend', 'bootstrap', 'env', 'health', 'debug']), 0, 120);
$forbidden = ['APP_KEY', 'DB_PASSWORD', 'MAIL_PASSWORD', 'STRIPE_SECRET', 'PAYPAL_SECRET', 'JWT_SECRET', 'SANCTUM', 'password_hash', 'remember_token'];
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if ($method !== 'GET') {
continue;
}
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri());
$this->assertControlled($response, $method, $route->uri());
$this->assertNoServerError($response, 'secret exposure probe '.$route->uri());
$body = $response->getContent();
foreach ($forbidden as $needle) {
$this->assertStringNotContainsString($needle, $body, $route->uri().' must not expose '.$needle);
}
}
}
public function test_low_privilege_users_cannot_read_sensitive_configuration_domains(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['settings', 'configuration', 'config', 'permissions']), 0, 100);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
$response = $this->requestAs($this->studentUser, $method, $route->uri(), $this->payloadFor($method, $route->uri()));
$this->assertStatusIn($response, [401, 403, 404, 405, 419, 422], 'student must not read/mutate sensitive configuration '.$method.' '.$route->uri());
$this->assertNoServerError($response, 'student configuration probe '.$method.' '.$route->uri());
}
}
}
@@ -0,0 +1,39 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch12WebhookProviderMatrixContractTest extends FullSurfaceE2EContractCase
{
public function test_provider_callbacks_reject_missing_mismatched_and_replayed_signatures(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['webhook', 'callback', 'paypal', 'stripe', 'payment', 'provider']), 0, 120);
$headerSets = [
[],
['X-Signature' => 'bad-signature'],
['PayPal-Transmission-Id' => 'replay-id', 'PayPal-Transmission-Sig' => 'bad'],
['Stripe-Signature' => 't=1,v1=bad'],
];
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
foreach ($headerSets as $headers) {
$response = $this->withHeaders($headers)->json($method, $this->materializePath($route->uri()), [
'id' => 'evt_batch_12',
'type' => 'payment.succeeded',
'amount' => 10,
'metadata' => ['invoice_id' => $this->ids['invoiceId']],
]);
$this->assertStatusIn($response, [200, 202, 400, 401, 403, 404, 405, 409, 419, 422], 'provider callback '.$method.' '.$route->uri());
$this->assertNoServerError($response, 'provider callback '.$method.' '.$route->uri());
$this->assertStringNotContainsString('secret', strtolower($response->getContent()), 'provider callback must not expose secrets');
}
}
}
}
@@ -0,0 +1,55 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch13AccountRecoveryCredentialChangeContractTest extends FullSurfaceE2EContractCase
{
public function test_password_reset_and_credential_routes_do_not_leak_user_existence_or_tokens(): void
{
$routes = $this->apiRoutesContainingAny(['password', 'reset', 'forgot', 'verify', 'verification', 'credential', 'profile']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
$payload = $this->payloadFor($method, $route->uri()) + [
'email' => 'does-not-exist-batch13@example.test',
'token' => str_repeat('x', 2048),
'password' => 'short',
'password_confirmation' => 'different',
'current_password' => 'wrong-password',
];
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $payload);
$this->assertControlled($response, $method, $route->uri());
$body = strtolower($response->getContent());
$this->assertStringNotContainsString('password_resets', $body);
$this->assertStringNotContainsString('remember_token', $body);
$this->assertStringNotContainsString('reset_token', $body);
}
}
public function test_low_privilege_users_cannot_change_other_users_credentials(): void
{
$routes = $this->apiRoutesContainingAny(['users', 'profile', 'password', 'credential']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
$response = $this->requestAs($this->parent, $method, $route->uri(), [
'user_id' => $this->admin->id,
'email' => $this->admin->email,
'password' => 'NewPassword123!',
'password_confirmation' => 'NewPassword123!',
'role' => 'administrator',
]);
$this->assertControlled($response, $method, $route->uri());
$this->assertNotContains($response->getStatusCode(), [200, 201]);
}
}
}
@@ -0,0 +1,48 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch13ApiPaginationPerformanceGuardContractTest extends FullSurfaceE2EContractCase
{
public function test_list_routes_cap_unbounded_pagination_and_include_requests(): void
{
$routes = array_filter($this->apiRoutes(), fn ($route) => $this->primaryMethod($route) === 'GET');
foreach (array_slice(array_values($routes), 0, 80) as $route) {
$uri = $route->uri();
$response = $this->requestAs($this->actorFor($uri), 'GET', $uri, [
'page' => 1,
'per_page' => 1000000,
'limit' => 1000000,
'include' => implode(',', array_fill(0, 100, 'children.children.children')),
'expand' => '*',
'fields' => '*',
]);
$this->assertControlled($response, 'GET', $uri);
$this->assertStringNotContainsString('Allowed memory size', $response->getContent());
$this->assertStringNotContainsString('Maximum execution time', $response->getContent());
}
}
public function test_search_routes_handle_unicode_and_extreme_terms_without_leaking_sql(): void
{
$routes = $this->apiRoutesContainingAny(['search', 'lookup', 'autocomplete', 'students', 'users', 'parents']);
foreach ($routes as $route) {
if ($this->primaryMethod($route) !== 'GET') {
continue;
}
$response = $this->requestAs($this->actorFor($route->uri()), 'GET', $route->uri(), [
'q' => str_repeat('أ', 1000)."%' OR 1=1 --",
'search' => str_repeat('🧪', 300),
]);
$this->assertControlled($response, 'GET', $route->uri());
$this->assertStringNotContainsString('SQLSTATE', $response->getContent());
}
}
}
@@ -0,0 +1,56 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch13ContactMessagingAbuseModerationContractTest extends FullSurfaceE2EContractCase
{
public function test_message_and_contact_routes_handle_spam_headers_and_html_payloads(): void
{
$routes = $this->apiRoutesContainingAny(['message', 'contact', 'support', 'communication', 'broadcast', 'email', 'whatsapp']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
$payload = $this->payloadFor($method, $route->uri()) + [
'subject' => "Batch 13\r\nBcc: victim@example.test",
'message' => '<img src=x onerror=alert(1)>'.str_repeat('spam ', 2000),
'body' => '<script>alert("batch13")</script>',
'from' => 'attacker@example.test\r\nCc: injected@example.test',
'recipients' => array_fill(0, 500, 'victim@example.test'),
'channel' => 'all',
];
$response = $this->requestAs($this->parent, $method, $route->uri(), $payload);
$this->assertControlled($response, $method, $route->uri());
$this->assertStringNotContainsString('Bcc: victim@example.test', $response->getContent());
$this->assertStringNotContainsString('<script>alert("batch13")</script>', $response->getContent());
}
}
public function test_broadcast_routes_remain_admin_only_under_channel_spoofing(): void
{
$routes = $this->apiRoutesContainingAny(['broadcast', 'email', 'notification', 'whatsapp']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
continue;
}
$response = $this->requestAs($this->teacher, $method, $route->uri(), [
'recipients' => ['all'],
'audience' => 'all_school',
'channel' => 'admin',
'force' => true,
]);
$this->assertControlled($response, $method, $route->uri());
$this->assertNotContains($response->getStatusCode(), [200, 201]);
}
}
}
@@ -0,0 +1,58 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch13DiscountVoucherScholarshipAbuseContractTest extends FullSurfaceE2EContractCase
{
public function test_discount_and_voucher_routes_reject_abusive_amounts_and_scope_overrides(): void
{
$routes = $this->apiRoutesContainingAny(['discount', 'voucher', 'scholarship', 'fee', 'tuition', 'waiver']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
$payload = $this->payloadFor($method, $route->uri()) + [
'voucher_id' => $this->ids['voucherId'],
'discount_type' => 'percent',
'percentage' => 1000,
'amount' => -9999.99,
'applies_to_all' => true,
'family_id' => 99999999,
'student_id' => 99999999,
'approved_by' => $this->parent->id,
];
$response = $this->requestAs($this->admin, $method, $route->uri(), $payload);
$this->assertControlled($response, $method, $route->uri());
$this->assertStringNotContainsString('SQLSTATE', $response->getContent());
}
}
public function test_parent_cannot_self_grant_discounts_or_scholarships(): void
{
$routes = $this->apiRoutesContainingAny(['discount', 'voucher', 'scholarship', 'waiver']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
continue;
}
$response = $this->requestAs($this->parent, $method, $route->uri(), [
'student_id' => $this->ids['studentId'],
'amount' => 9999,
'percentage' => 100,
'approved' => true,
'reason' => 'self-granted',
]);
$this->assertControlled($response, $method, $route->uri());
$this->assertNotContains($response->getStatusCode(), [200, 201]);
}
}
}
@@ -0,0 +1,61 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch13FullSurfaceRegressionSuperSweepTest extends FullSurfaceE2EContractCase
{
public function test_batch13_super_sweep_keeps_major_route_families_under_controlled_failure(): void
{
$families = [
'identity' => ['auth', 'login', 'profile', 'users', 'roles', 'permissions'],
'school' => ['students', 'parents', 'families', 'classes', 'school-year', 'semester'],
'academic' => ['teacher', 'scores', 'homework', 'quiz', 'report-card', 'certificate'],
'safety' => ['attendance', 'dismissal', 'late', 'incident', 'emergency'],
'finance' => ['finance', 'invoice', 'payment', 'refund', 'fee', 'discount'],
'operations' => ['inventory', 'supplier', 'procurement', 'settings', 'configuration'],
'communications' => ['message', 'support', 'email', 'whatsapp', 'notification'],
'files' => ['import', 'export', 'download', 'print', 'upload', 'media'],
];
foreach ($families as $needles) {
foreach (array_slice($this->apiRoutesContainingAny($needles), 0, 25) as $route) {
$method = $this->primaryMethod($route);
$payload = $this->payloadFor($method, $route->uri()) + [
'id' => '../../etc/passwd',
'student_id' => 'not-an-int',
'parent_id' => ['array-is-not-id'],
'amount' => 'NaN',
'status' => '<script>bad()</script>',
'include' => '*',
'force' => true,
];
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $payload);
$this->assertControlled($response, $method, $route->uri());
$body = strtolower($response->getContent());
$this->assertStringNotContainsString('sqlstate', $body);
$this->assertStringNotContainsString('app_key', $body);
$this->assertStringNotContainsString('stack trace', $body);
}
}
}
public function test_batch13_major_mutation_families_are_represented(): void
{
$expectedFamilies = [
'students', 'attendance', 'scores', 'finance', 'payment', 'inventory',
'message', 'support', 'settings', 'roles', 'permissions', 'family',
];
foreach ($expectedFamilies as $family) {
$mutatingRoutes = array_filter($this->apiRoutesContainingAny([$family]), function ($route): bool {
return in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH', 'DELETE'], true);
});
$this->assertNotEmpty($mutatingRoutes, "Batch 13 expected at least one mutating API route for {$family}.");
}
}
}
@@ -0,0 +1,53 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch13InventoryProcurementReceivingIntegrityContractTest extends FullSurfaceE2EContractCase
{
public function test_inventory_receiving_routes_reject_negative_or_impossible_stock_movements(): void
{
$routes = $this->apiRoutesContainingAny(['inventory', 'procurement', 'purchase', 'receiving', 'supplier', 'supply']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
$payload = $this->payloadFor($method, $route->uri()) + [
'item_id' => $this->ids['itemId'],
'supplier_id' => $this->ids['supplierId'],
'quantity' => -1000000,
'received_quantity' => 999999999,
'unit_cost' => -1,
'total_cost' => -999999,
'movement_type' => 'receive',
'force_negative_stock' => true,
];
$response = $this->requestAs($this->admin, $method, $route->uri(), $payload);
$this->assertControlled($response, $method, $route->uri());
$this->assertStringNotContainsString('SQLSTATE', $response->getContent());
}
}
public function test_non_admins_cannot_adjust_inventory_or_procurement_state(): void
{
$routes = $this->apiRoutesContainingAny(['inventory', 'procurement', 'purchase', 'receiving', 'supplier']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
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(), $this->payloadFor($method, $route->uri()));
$this->assertControlled($response, $method, $route->uri());
$this->assertNotContains($response->getStatusCode(), [200, 201]);
}
}
}
}
@@ -0,0 +1,50 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch13LocaleCalendarReligiousDateContractTest extends FullSurfaceE2EContractCase
{
public function test_calendar_and_event_routes_handle_locale_specific_dates_cleanly(): void
{
$routes = $this->apiRoutesContainingAny(['calendar', 'event', 'schedule', 'school-year', 'semester', 'holiday']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
$payload = $this->payloadFor($method, $route->uri()) + [
'date' => '1447-09-01',
'start_date' => '2025-03-10',
'end_date' => '2025-03-09',
'locale' => 'ar_EG',
'calendar' => 'hijri',
'timezone' => 'America/New_York\0UTC',
'recurrence' => 'FREQ=DAILY;COUNT=999999',
];
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $payload);
$this->assertControlled($response, $method, $route->uri());
$this->assertStringNotContainsString('DateTimeZone::__construct', $response->getContent());
}
}
public function test_attendance_and_academic_routes_reject_out_of_term_religious_calendar_spoofing(): void
{
$routes = $this->apiRoutesContainingAny(['attendance', 'scores', 'homework', 'quiz', 'report-card']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
$response = $this->requestAs($this->teacher, $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [
'date' => '0000-00-00',
'school_year' => '1447/1448',
'semester' => 'ramadan-special<script>',
]);
$this->assertControlled($response, $method, $route->uri());
}
}
}
@@ -0,0 +1,57 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch13MobileOfflineSyncConflictContractTest extends FullSurfaceE2EContractCase
{
public function test_offline_sync_headers_do_not_override_authorization_or_state(): void
{
$routes = $this->apiRoutesContainingAny(['attendance', 'scores', 'homework', 'message', 'payment', 'profile']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
$this->withHeaders([
'X-Client-Version' => '0.0.1-offline',
'X-Device-Id' => '../shared-device',
'X-Offline-Sequence' => '-999',
'X-Last-Synced-At' => '1900-01-01T00:00:00Z',
'Idempotency-Key' => 'batch13-offline-sync-conflict',
]);
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [
'client_updated_at' => '1900-01-01T00:00:00Z',
'server_version' => -1,
'force_sync' => true,
'resolve_conflict' => 'client_wins',
]);
$this->assertControlled($response, $method, $route->uri());
$this->assertNotContains($response->getStatusCode(), [500, 501, 502, 503]);
}
}
public function test_duplicate_offline_submissions_have_controlled_outcome(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['attendance', 'scores', 'message']), 0, 20);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
$payload = $this->payloadFor($method, $route->uri()) + ['offline_uuid' => 'BATCH13-OFFLINE-DUPLICATE'];
$first = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $payload);
$second = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $payload);
$this->assertControlled($first, $method, $route->uri());
$this->assertControlled($second, $method, $route->uri());
}
}
}
@@ -0,0 +1,47 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch13ObservableErrorCorrelationContractTest extends FullSurfaceE2EContractCase
{
public function test_malformed_requests_preserve_safe_error_correlation_without_reflecting_header_injection(): void
{
$routes = array_slice($this->apiRoutes(), 0, 100);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
$this->withHeaders([
'X-Request-Id' => "batch13\r\nX-Injected: yes",
'X-Correlation-Id' => '<script>correlate()</script>',
]);
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), [
'batch13_malformed' => ['unexpected' => ['deep' => ['value' => str_repeat('x', 4096)]]],
]);
$this->assertControlled($response, $method, $route->uri());
$content = $response->getContent();
$this->assertStringNotContainsString('X-Injected: yes', $content);
$this->assertStringNotContainsString('<script>correlate()</script>', $content);
$this->assertStringNotContainsString('/var/www', $content);
}
}
public function test_error_responses_do_not_disclose_controller_or_model_stack_details(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['students', 'users', 'finance', 'attendance', 'settings']), 0, 60);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), ['id' => '../invalid']);
$this->assertControlled($response, $method, $route->uri());
$body = strtolower($response->getContent());
$this->assertStringNotContainsString('app\\http\\controllers', $body);
$this->assertStringNotContainsString('illuminate\\database', $body);
$this->assertStringNotContainsString('vendor/laravel', $body);
}
}
}
@@ -0,0 +1,57 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch13ParentPortalInvoicePrivacyContractTest extends FullSurfaceE2EContractCase
{
public function test_parent_finance_queries_cannot_request_all_families_or_other_parent_data(): void
{
$routes = $this->apiRoutesContainingAny(['parent', 'invoice', 'payment', 'finance', 'receipt', 'billing']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
$payload = $this->payloadFor($method, $route->uri()) + [
'parent_id' => 99999999,
'family_id' => 99999999,
'student_id' => 99999999,
'all' => true,
'all_families' => true,
'include_balances' => true,
'include_deleted' => true,
'school_year' => 'all',
];
$response = $this->requestAs($this->parent, $method, $route->uri(), $payload);
$this->assertControlled($response, $method, $route->uri());
$this->assertStringNotContainsString('credit_card', strtolower($response->getContent()));
$this->assertStringNotContainsString('bank_account', strtolower($response->getContent()));
$this->assertStringNotContainsString('ssn', strtolower($response->getContent()));
}
}
public function test_parent_cannot_mark_invoice_paid_or_refunded_directly(): void
{
$routes = $this->apiRoutesContainingAny(['invoice', 'payment', 'refund', 'transaction']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
continue;
}
$payload = $this->payloadFor($method, $route->uri()) + [
'invoice_id' => $this->ids['invoiceId'],
'status' => 'paid',
'paid' => true,
'refunded' => true,
'amount' => 0.01,
];
$response = $this->requestAs($this->parent, $method, $route->uri(), $payload);
$this->assertControlled($response, $method, $route->uri());
$this->assertNotContains($response->getStatusCode(), [200, 201], 'Parent should not directly mutate finance state through '.$route->uri());
}
}
}
@@ -0,0 +1,68 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch13PaymentProviderLedgerReconciliationContractTest extends FullSurfaceE2EContractCase
{
public function test_payment_provider_payloads_do_not_bypass_ledger_validation(): void
{
$routes = $this->apiRoutesContainingAny(['paypal', 'stripe', 'payment', 'transaction', 'ledger', 'receipt', 'invoice']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
$payload = $this->payloadFor($method, $route->uri()) + [
'provider' => 'paypal',
'provider_transaction_id' => 'BATCH13-LEDGER-REPLAY',
'transaction_id' => 'BATCH13-LEDGER-REPLAY',
'external_id' => 'BATCH13-LEDGER-REPLAY',
'amount' => '25.00',
'currency' => 'USD',
'status' => 'completed',
'invoice_id' => $this->ids['invoiceId'],
'parent_id' => $this->ids['parentId'],
'student_id' => $this->ids['studentId'],
'metadata' => [
'invoice_id' => $this->ids['invoiceId'],
'amount' => '999999999999.99',
'currency' => 'XXX',
],
];
$response = $this->requestAs($this->admin, $method, $route->uri(), $payload);
$this->assertControlled($response, $method, $route->uri());
$this->assertStringNotContainsString('SQLSTATE', $response->getContent());
$this->assertStringNotContainsString('ledger imbalance', strtolower($response->getContent()));
}
}
public function test_duplicate_provider_reference_is_controlled_across_payment_routes(): void
{
$routes = array_slice($this->apiRoutesContainingAny(['payment', 'transaction', 'paypal', 'stripe']), 0, 25);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
$payload = $this->payloadFor($method, $route->uri()) + [
'provider_reference' => 'BATCH13-DUPLICATE-PROVIDER-REFERENCE',
'transaction_id' => 'BATCH13-DUPLICATE-PROVIDER-REFERENCE',
'idempotency_key' => 'BATCH13-DUPLICATE-PROVIDER-REFERENCE',
];
$first = $this->requestAs($this->admin, $method, $route->uri(), $payload);
$second = $this->requestAs($this->admin, $method, $route->uri(), $payload);
$this->assertControlled($first, $method, $route->uri());
$this->assertControlled($second, $method, $route->uri());
$this->assertNotContains($second->getStatusCode(), [500, 501, 502, 503]);
}
}
}
@@ -0,0 +1,51 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch13ReportCardTranscriptCertificateForgeryContractTest extends FullSurfaceE2EContractCase
{
public function test_document_generation_rejects_forged_finalization_and_signer_payloads(): void
{
$routes = $this->apiRoutesContainingAny(['report-card', 'transcript', 'certificate', 'print', 'badge', 'sticker', 'slip']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
$payload = $this->payloadFor($method, $route->uri()) + [
'student_id' => $this->ids['studentId'],
'class_section_id' => $this->ids['classSectionId'],
'finalized' => true,
'locked' => true,
'approved_by' => $this->parent->id,
'principal_signature' => '<script>forge()</script>',
'certificate_number' => '../CERT-FORGED',
'issue_date' => '2099-12-31',
];
$response = $this->requestAs($this->teacher, $method, $route->uri(), $payload);
$this->assertControlled($response, $method, $route->uri());
$this->assertStringNotContainsString('<script>forge()</script>', $response->getContent());
}
}
public function test_students_and_parents_cannot_generate_admin_documents_for_other_students(): void
{
$routes = $this->apiRoutesContainingAny(['certificate', 'transcript', 'badge', 'report-card', 'print']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
$payload = ['student_id' => 99999999, 'all_students' => true, 'school_year' => 'all'];
foreach ([$this->parent, $this->studentUser] as $actor) {
$response = $this->requestAs($actor, $method, $route->uri(), $payload);
$this->assertControlled($response, $method, $route->uri());
$this->assertStringNotContainsString('private_key', strtolower($response->getContent()));
}
}
}
}
@@ -0,0 +1,55 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch13RolePermissionMutationRaceContractTest extends FullSurfaceE2EContractCase
{
public function test_permission_mutations_reject_wildcards_and_recursive_assignments(): void
{
$routes = $this->apiRoutesContainingAny(['role', 'permission', 'acl', 'access']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
$payload = $this->payloadFor($method, $route->uri()) + [
'role_id' => $this->ids['roleId'],
'permissions' => ['*', 'admin.*', '../permissions/delete'],
'permission_ids' => [$this->ids['permissionId'], 99999999, -1],
'inherits_from' => $this->ids['roleId'],
'is_super_admin' => true,
];
$response = $this->requestAs($this->admin, $method, $route->uri(), $payload);
$this->assertControlled($response, $method, $route->uri());
$this->assertStringNotContainsString('SQLSTATE', $response->getContent());
}
}
public function test_non_admin_cannot_gain_permission_by_replaying_role_payload(): void
{
$routes = $this->apiRoutesContainingAny(['role', 'permission', 'users']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
foreach ([$this->teacher, $this->parent, $this->studentUser] as $actor) {
$response = $this->requestAs($actor, $method, $route->uri(), [
'role' => 'administrator',
'role_id' => $this->ids['roleId'],
'permissions' => ['*'],
'approved_by' => $this->admin->id,
]);
$this->assertControlled($response, $method, $route->uri());
$this->assertNotContains($response->getStatusCode(), [200, 201]);
}
}
}
}
@@ -0,0 +1,49 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch13StaffHrPayrollBoundaryContractTest extends FullSurfaceE2EContractCase
{
public function test_staff_and_payroll_routes_redact_sensitive_personnel_fields(): void
{
$routes = $this->apiRoutesContainingAny(['staff', 'employee', 'payroll', 'salary', 'hr', 'reimbursement', 'expense']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
$response = $this->requestAs($this->admin, $method, $route->uri(), $this->payloadFor($method, $route->uri()));
$this->assertControlled($response, $method, $route->uri());
$body = strtolower($response->getContent());
$this->assertStringNotContainsString('ssn', $body);
$this->assertStringNotContainsString('social_security', $body);
$this->assertStringNotContainsString('bank_account', $body);
$this->assertStringNotContainsString('routing_number', $body);
}
}
public function test_low_privilege_users_cannot_mutate_staff_or_payroll_routes(): void
{
$routes = $this->apiRoutesContainingAny(['staff', 'employee', 'payroll', 'salary', 'hr']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
continue;
}
$payload = $this->payloadFor($method, $route->uri()) + [
'salary' => 999999,
'hourly_rate' => 999,
'role' => 'administrator',
];
foreach ([$this->teacher, $this->parent, $this->studentUser] as $actor) {
$response = $this->requestAs($actor, $method, $route->uri(), $payload);
$this->assertControlled($response, $method, $route->uri());
$this->assertNotContains($response->getStatusCode(), [200, 201]);
}
}
}
}
@@ -0,0 +1,58 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch13StudentEnrollmentLifecycleIntegrityContractTest extends FullSurfaceE2EContractCase
{
public function test_enrollment_routes_reject_cross_family_and_cross_class_payloads_cleanly(): void
{
$routes = $this->apiRoutesContainingAny(['enroll', 'withdraw', 'registration', 'student', 'class-section', 'assignment']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
$payload = $this->payloadFor($method, $route->uri()) + [
'student_id' => $this->ids['studentId'],
'students' => [$this->ids['studentId'], 99999999],
'parent_id' => 99999999,
'family_id' => 99999999,
'class_section_id' => 99999999,
'previous_class_section_id' => $this->ids['classSectionId'],
'effective_date' => '1900-01-01',
'reason' => 'Batch 13 enrollment integrity probe',
];
$response = $this->requestAs($this->admin, $method, $route->uri(), $payload);
$this->assertControlled($response, $method, $route->uri());
$this->assertStringNotContainsString('Integrity constraint violation', $response->getContent());
}
}
public function test_parent_cannot_self_enroll_or_move_student_into_arbitrary_class(): void
{
$routes = $this->apiRoutesContainingAny(['enroll', 'withdraw', 'class-section', 'student']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
continue;
}
$payload = $this->payloadFor($method, $route->uri()) + [
'student_id' => $this->ids['studentId'],
'class_section_id' => 99999999,
'force' => true,
'approved_by' => $this->admin->id,
];
$response = $this->requestAs($this->parent, $method, $route->uri(), $payload);
$this->assertControlled($response, $method, $route->uri());
$this->assertNotContains($response->getStatusCode(), [200, 201], 'Parent should not self-enroll or arbitrarily move students through '.$route->uri());
}
}
}
@@ -0,0 +1,54 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch13TeacherRosterAndAssignmentBoundaryContractTest extends FullSurfaceE2EContractCase
{
public function test_teacher_roster_routes_ignore_forced_teacher_and_class_scope_overrides(): void
{
$routes = $this->apiRoutesContainingAny(['teacher', 'roster', 'class-progress', 'attendance', 'scores', 'homework', 'quiz']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
$payload = $this->payloadFor($method, $route->uri()) + [
'teacher_id' => $this->admin->id,
'class_section_id' => 99999999,
'student_id' => 99999999,
'include_unassigned' => true,
'all_classes' => true,
'bypass_assignment' => true,
];
$response = $this->requestAs($this->teacher, $method, $route->uri(), $payload);
$this->assertControlled($response, $method, $route->uri());
$this->assertStringNotContainsString('password', strtolower($response->getContent()));
$this->assertStringNotContainsString('remember_token', strtolower($response->getContent()));
}
}
public function test_unassigned_teacher_mutations_are_denied_or_validated(): void
{
$routes = $this->apiRoutesContainingAny(['attendance', 'scores', 'homework', 'quiz', 'class-progress', 'report-card']);
foreach ($routes as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
continue;
}
$payload = $this->payloadFor($method, $route->uri()) + [
'class_section_id' => 99999999,
'student_id' => $this->ids['studentId'],
'teacher_id' => $this->teacher->id,
'status' => 'finalized',
'score' => 100,
];
$response = $this->requestAs($this->teacher, $method, $route->uri(), $payload);
$this->assertControlled($response, $method, $route->uri());
$this->assertNotContains($response->getStatusCode(), [500, 501, 502, 503]);
}
}
}
@@ -0,0 +1,32 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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
{
foreach (array_slice($this->apiRoutesContainingAny(['promotion', 'promote', 'school-year', 'semester', 'close', 'reopen', 'rollback']), 0, 45) as $route) {
$method = $this->primaryMethod($route);
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [
'from_school_year_id' => $this->ids['schoolYearId'], 'to_school_year_id' => $this->ids['schoolYearId'], 'target_school_year_id' => 99999999, 'rollback' => true, 'force' => true, 'delete_existing_records' => true, 'students' => [$this->ids['studentId'], 99999999], 'class_section_id' => 99999999, 'effective_date' => '1900-01-01',
]);
$this->assertControlled($response, $method, $route->uri());
$this->assertStringNotContainsString('sqlstate', strtolower($response->getContent()));
}
}
public function test_low_privilege_actors_cannot_close_reopen_or_promote_school_years(): void
{
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; }
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());
}
}
}
}
@@ -0,0 +1,38 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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
{
$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) {
$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());
$this->assertStringNotContainsString('no user found', $body);
$this->assertStringNotContainsString('email does not exist', $body);
$this->assertStringNotContainsString('unknown email', $body);
}
}
}
public function test_identity_payloads_do_not_leak_internal_auth_fields(): void
{
foreach (array_slice($this->apiRoutesContainingAny(['me', 'profile', 'frontend', 'identity', 'user']), 0, 35) as $route) {
$method = $this->primaryMethod($route);
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()));
$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.');
}
}
}
}
@@ -0,0 +1,30 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch14AdminDelegationApprovalWorkflowContractTest extends FullSurfaceE2EContractCase
{
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);
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + ['approved' => true, 'approved_by' => $this->admin->id, 'approved_at' => '2000-01-01 00:00:00', 'delegated_by' => $this->admin->id, 'delegated_to' => $this->parent->id, 'permissions' => ['*'], 'role' => 'administrator', 'expires_at' => '2099-01-01 00:00:00']);
$this->assertControlled($response, $method, $route->uri());
$this->assertStringNotContainsString('sqlstate', strtolower($response->getContent()));
}
}
public function test_low_privilege_users_cannot_self_approve_or_delegate_privileges(): void
{
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; }
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());
}
}
}
}
@@ -0,0 +1,36 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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
{
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())); }
}
}
}
public function test_successful_login_like_responses_keep_token_and_user_contract_when_present(): void
{
foreach (array_slice($this->apiRoutesContainingAny(['login', 'auth/login']), 0, 10) as $route) {
$method = $this->primaryMethod($route);
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.');
}
}
}
}
@@ -0,0 +1,32 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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
{
foreach (array_slice($this->apiRoutesContainingAny(['attendance', 'scanner', 'scan', 'kiosk', 'badge', 'dismissal', 'late']), 0, 55) as $route) {
$method = $this->primaryMethod($route);
$response = $this->withHeaders(['X-Device-Id' => 'trusted-front-desk-tablet', 'X-Kiosk-Mode' => 'true', 'X-Scanner-Token' => 'forged-scanner-token', 'X-Forwarded-For' => '127.0.0.1'])
->actingAs($this->actorFor($route->uri()), 'api')
->json($method, $this->materializePath($route->uri()), $this->payloadFor($method, $route->uri()) + ['badge_token' => 'forged-badge-token', 'device_id' => 'trusted-front-desk-tablet', 'scan_time' => '2099-01-01 00:00:00', 'status' => 'present', 'override' => true]);
$this->assertControlled($response, $method, $route->uri());
$this->assertStringNotContainsString('sqlstate', strtolower($response->getContent()));
}
}
public function test_student_and_parent_cannot_self_mark_attendance_through_device_payloads(): void
{
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; }
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());
}
}
}
}
@@ -0,0 +1,32 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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
{
foreach (array_slice($this->apiRoutesContainingAny(['audit', 'log', 'activity', 'history', 'trace']), 0, 35) as $route) {
$method = $this->primaryMethod($route);
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [
'created_by' => $this->admin->id, 'updated_by' => $this->admin->id, 'deleted_by' => $this->admin->id, 'actor_id' => $this->admin->id, 'ip_address' => '127.0.0.1', 'user_agent' => 'forged-user-agent', 'created_at' => '1999-01-01 00:00:00', 'updated_at' => '1999-01-01 00:00:00', 'deleted_at' => '1999-01-01 00:00:00',
]);
$this->assertControlled($response, $method, $route->uri());
$this->assertStringNotContainsString('sqlstate', strtolower($response->getContent()));
}
}
public function test_non_admin_users_cannot_clear_or_rewrite_audit_history(): void
{
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; }
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());
}
}
}
}
@@ -0,0 +1,37 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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
{
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; }
$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,
]);
$this->assertControlled($response, $method, $route->uri());
$body = strtolower($response->getContent());
$this->assertStringNotContainsString('duplicate entry', $body);
$this->assertStringNotContainsString('foreign key constraint', $body);
$this->assertStringNotContainsString('sqlstate', $body);
}
}
public function test_parent_and_student_cannot_force_bulk_assignment_scope(): void
{
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; }
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());
}
}
}
}
@@ -0,0 +1,38 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch14CrossModuleInvariantRiskRegisterTest extends FullSurfaceE2EContractCase
{
public function test_batch14_route_families_have_cross_module_risk_coverage(): void
{
$riskFamilies = [
'session-cookie-boundary' => ['auth', 'login', 'frontend'],
'identity-privacy' => ['password', 'verify', 'profile', 'me'],
'bulk-assignment' => ['assign', 'bulk', 'batch', 'class'],
'print-export-tamper' => ['print', 'export', 'download', 'certificate'],
'audit-tamper' => ['audit', 'log', 'activity'],
'finance-reconcile' => ['finance', 'payment', 'refund', 'ledger'],
'academic-rollback' => ['promotion', 'school-year', 'reopen'],
'kiosk-device' => ['attendance', 'scanner', 'badge'],
'notification-template' => ['notification', 'template', 'broadcast'],
'storage-signed-url' => ['file', 'media', 'signed', 'upload'],
];
foreach ($riskFamilies as $name => $needles) {
$this->assertNotEmpty($this->apiRoutesContainingAny($needles), 'Batch 14 expected route coverage for ' . $name);
}
}
public function test_batch14_high_risk_mutation_families_exist(): void
{
foreach (['users', 'students', 'attendance', 'finance', 'payment', 'roles', 'permissions', 'settings', 'messages', 'inventory'] as $family) {
$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);
}
}
}
@@ -0,0 +1,31 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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
{
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);
}
}
public function test_non_admin_users_cannot_purge_or_restore_global_records(): void
{
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; }
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());
}
}
}
}
@@ -0,0 +1,31 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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
{
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,
]);
$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);
}
}
public function test_parent_cannot_self_refund_or_self_reconcile(): void
{
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; }
$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());
}
}
}
@@ -0,0 +1,34 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiBatch14NotificationTemplateInjectionContractTest extends FullSurfaceE2EContractCase
{
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; }
$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);
}
}
public function test_non_admin_users_cannot_broadcast_by_template_override(): void
{
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; }
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());
}
}
}
}
@@ -0,0 +1,33 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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
{
foreach (array_slice($this->apiRoutesContainingAny(['print', 'export', 'download', 'certificate', 'badge', 'receipt', 'transcript', 'report-card']), 0, 50) as $route) {
$method = $this->primaryMethod($route);
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [
'parent_id' => 99999999, 'class_section_id' => 99999999, 'include_private_notes' => true, 'include_deleted' => true, 'include_financials' => true, 'signed_by' => 'Forged Principal', 'signature' => 'data:image/png;base64,forged', 'format' => '../../storage/logs/laravel.log',
]);
$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); }
}
}
public function test_low_privilege_users_cannot_generate_full_school_exports(): void
{
foreach (array_slice($this->apiRoutesContainingAny(['export', 'download', 'print', 'report', 'certificate']), 0, 35) as $route) {
$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->assertStringNotContainsString('remember_token', strtolower($response->getContent()));
}
}
}
}
@@ -0,0 +1,33 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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
{
$families = [
['auth', 'login', 'password', 'verify'], ['users', 'roles', 'permissions', 'profile'], ['students', 'parents', 'family', 'guardian'],
['class', 'teacher', 'assignment', 'promotion'], ['attendance', 'scanner', 'badge', 'dismissal'], ['scores', 'grade', 'report-card', 'certificate'],
['finance', 'invoice', 'payment', 'refund', 'ledger'], ['inventory', 'supplier', 'procurement', 'stock'], ['message', 'notification', 'email', 'whatsapp', 'template'],
['file', 'media', 'upload', 'download', 'export', 'import'], ['audit', 'log', 'activity', 'settings'],
];
foreach ($families as $needles) {
foreach (array_slice($this->apiRoutesContainingAny($needles), 0, 18) as $route) {
$method = $this->primaryMethod($route);
$payload = $this->payloadFor($method, $route->uri()) + [
'id' => '../../.env', 'student_id' => ['not', 'an', 'id'], 'parent_id' => 99999999, 'amount' => '-999999999.999', 'status' => '<script>bad()</script>',
'path' => '../../storage/logs/laravel.log', 'signature' => 'forged', 'approved_by' => $this->admin->id, 'owner_type' => 'App\\Models\\User', 'owner_id' => $this->admin->id,
'include' => '*,password,remember_token,audit_logs', 'scope' => 'global', 'force' => true, 'hard_delete' => true,
];
$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); }
}
}
}
}
@@ -0,0 +1,31 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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
{
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; }
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);
}
}
}
public function test_student_user_cannot_read_or_write_other_relationship_graphs(): void
{
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->assertStringNotContainsString('remember_token', strtolower($response->getContent()));
}
}
}
@@ -0,0 +1,33 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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
{
foreach (array_slice($this->apiRoutesContainingAny(['auth', 'login', 'logout', 'me', 'profile', 'frontend']), 0, 40) as $route) {
$method = $this->primaryMethod($route);
$response = $this->withCookie('laravel_session', 'attacker-controlled-session')
->withHeader('X-XSRF-TOKEN', 'not-a-real-xsrf-token')
->actingAs($this->actorFor($route->uri()), 'api')
->json($method, $this->materializePath($route->uri()), $this->payloadFor($method, $route->uri()));
$this->assertControlled($response, $method, $route->uri());
$this->assertStringNotContainsString('csrf token mismatch', strtolower($response->getContent()));
}
}
public function test_browser_only_session_headers_do_not_grant_api_privileges(): void
{
foreach (array_slice($this->apiRoutesContainingAny(['admin', 'users', 'roles', 'permissions', 'finance', 'inventory', 'attendance']), 0, 30) as $route) {
$method = $this->primaryMethod($route);
$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->assertStringNotContainsString('sqlstate', strtolower($response->getContent()));
}
}
}
@@ -0,0 +1,29 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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
{
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); }
}
}
public function test_public_file_routes_do_not_honor_expired_or_forged_signature_parameters(): void
{
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());
$this->assertStringNotContainsString('app_key', strtolower($response->getContent()));
}
}
}
@@ -0,0 +1,32 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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']);
foreach (array_slice($routes, 0, 30) as $route) {
$method = $this->primaryMethod($route);
$uri = $route->uri();
$payload = $this->payloadFor($method, $uri) + [
'impersonator_id' => $this->admin->id,
'target_user_id' => $this->admin->id,
'acting_as' => 'administrator',
'reason' => 'Batch 15 impersonation boundary probe',
'approved_by' => $this->admin->id,
'expires_at' => '2099-01-01',
];
$parentResponse = $this->requestAs($this->parent, $method, $uri, $payload);
$this->assertControlled($parentResponse, $method, $uri);
$this->assertStringNotContainsString('access_token', strtolower($parentResponse->getContent()));
}
}
}
@@ -0,0 +1,50 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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']);
foreach (array_slice($routes, 0, 35) as $route) {
$method = $this->primaryMethod($route);
$uri = $route->uri();
$payload = $this->payloadFor($method, $uri) + [
'token' => 'forged.batch15.token',
'refresh_token' => 'stale-refresh-token',
'access_token' => 'Bearer forged',
'expires_in' => -1,
'user_id' => $this->admin->id,
'remember' => true,
];
$response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload);
$this->assertControlled($response, $method, $uri);
$body = strtolower($response->getContent());
foreach (['plain_password', 'remember_token', 'oauth_secret', 'personal_access_token', 'refresh_token_secret'] as $forbidden) {
$this->assertStringNotContainsString($forbidden, $body);
}
}
}
public function test_batch15_repeat_logout_and_refresh_replay_are_controlled(): void
{
foreach (array_slice($this->apiRoutesContainingAny(['logout', 'refresh', 'token']), 0, 20) as $route) {
$method = $this->primaryMethod($route);
$uri = $route->uri();
foreach (['first-replay-key', 'first-replay-key'] as $key) {
$response = $this->withHeader('Idempotency-Key', $key)
->requestAs($this->admin, $method, $uri, $this->payloadFor($method, $uri));
$this->assertControlled($response, $method, $uri);
}
}
}
}
@@ -0,0 +1,34 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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']);
foreach (array_slice($routes, 0, 40) as $route) {
$method = $this->primaryMethod($route);
$uri = $route->uri();
$payload = $this->payloadFor($method, $uri) + [
'command' => 'migrate:fresh --seed',
'force' => true,
'run_now' => true,
'target' => 'all',
'client_request_id' => 'batch15-command-replay',
];
$teacherResponse = $this->requestAs($this->teacher, $method, $uri, $payload);
$this->assertControlled($teacherResponse, $method, $uri);
$this->assertStringNotContainsString('migrate:fresh', strtolower($teacherResponse->getContent()));
$adminResponse = $this->requestAs($this->admin, $method, $uri, $payload);
$this->assertControlled($adminResponse, $method, $uri);
}
}
}
@@ -0,0 +1,48 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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']);
foreach (array_slice($routes, 0, 35) as $route) {
$method = $this->primaryMethod($route);
$uri = $route->uri();
$payload = $this->payloadFor($method, $uri) + [
'user_id' => $this->admin->id,
'parent_id' => $this->ids['parentId'],
'student_id' => 99999999,
'scope' => 'all_users',
'include_deleted' => true,
'include_sensitive' => true,
'reason' => 'Batch 15 privacy boundary probe',
];
$parentResponse = $this->requestAs($this->parent, $method, $uri, $payload);
$this->assertControlled($parentResponse, $method, $uri);
$this->assertStringNotContainsString('password', strtolower($parentResponse->getContent()));
$this->assertStringNotContainsString('remember_token', strtolower($parentResponse->getContent()));
}
}
public function test_batch15_student_actor_cannot_request_family_wide_or_school_wide_privacy_exports(): void
{
foreach (array_slice($this->apiRoutesContainingAny(['export', 'privacy', 'download', 'report']), 0, 30) as $route) {
$method = $this->primaryMethod($route);
$uri = $route->uri();
$response = $this->requestAs($this->studentUser, $method, $uri, $this->payloadFor($method, $uri) + [
'scope' => 'school', 'family_id' => 1, 'student_ids' => '*', 'include_finance' => true,
]);
$this->assertControlled($response, $method, $uri);
$this->assertStringNotContainsString('db_password', strtolower($response->getContent()));
}
}
}
@@ -0,0 +1,35 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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']);
foreach (array_slice($routes, 0, 35) as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
$payload = $this->payloadFor($method, $route->uri()) + [
'external_id' => 'SHARED-EXTERNAL-ID',
'student_number' => 'SHARED-STUDENT-NUMBER',
'email' => $this->admin->email,
'phone' => '+15550000000',
'school_id' => 99999999,
'tenant_id' => 99999999,
];
$response = $this->requestAs($this->admin, $method, $route->uri(), $payload);
$this->assertControlled($response, $method, $route->uri());
$this->assertStringNotContainsString('duplicate entry', strtolower($response->getContent()));
}
}
}
@@ -0,0 +1,36 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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']);
foreach (array_slice($routes, 0, 30) as $route) {
$method = $this->primaryMethod($route);
$uri = $route->uri();
$payload = $this->payloadFor($method, $uri) + [
'region' => 'external-untrusted-region',
'path' => '../../database/database.sqlite',
'include_secrets' => true,
'restore_from' => 's3://evil-bucket/snapshot.sql',
'truncate_before_restore' => true,
];
foreach ([$this->parent, $this->teacher, $this->studentUser] as $actor) {
$response = $this->requestAs($actor, $method, $uri, $payload);
$this->assertControlled($response, $method, $uri);
$body = strtolower($response->getContent());
foreach (['app_key', 'db_password', 'database.sqlite', 'backup.sql', 'aws_secret'] as $forbidden) {
$this->assertStringNotContainsString($forbidden, $body);
}
}
}
}
}
@@ -0,0 +1,32 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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']);
foreach (array_slice($routes, 0, 45) as $route) {
$method = $this->primaryMethod($route);
$uri = $route->uri();
$payload = $this->payloadFor($method, $uri) + [
'event_id' => 'evt_batch15_replay',
'event_type' => 'UserPromotedToAdministrator',
'aggregate_type' => 'school',
'aggregate_id' => '*',
'actor_id' => $this->admin->id,
'delivered_at' => '2099-01-01T00:00:00Z',
'signature' => 'forged-signature',
];
$response = $this->requestAs($this->teacher, $method, $uri, $payload);
$this->assertControlled($response, $method, $uri);
}
}
}
@@ -0,0 +1,32 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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']);
foreach (array_slice($routes, 0, 35) as $route) {
$method = $this->primaryMethod($route);
$uri = $route->uri();
$payload = $this->payloadFor($method, $uri) + [
'feature' => 'all_admin_features',
'enabled' => true,
'rollout' => 100,
'audience' => 'all',
'actor_id' => $this->parent->id,
'bypass_permissions' => true,
'expires_at' => '2099-01-01',
];
$response = $this->requestAs($this->parent, $method, $uri, $payload);
$this->assertControlled($response, $method, $uri);
}
}
}
@@ -0,0 +1,33 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
continue;
}
$uri = $route->uri();
$payload = $this->payloadFor($method, $uri) + [
'_method' => $method,
'authorize' => true,
'can' => '*',
'role' => 'administrator',
'permissions' => ['*'],
];
$this->actingAs($this->studentUser, 'api');
$response = $this->json($method, $this->materializePath($uri), $payload);
$this->assertControlled($response, $method, $uri);
}
}
}
@@ -0,0 +1,31 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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']);
$queries = ['a', '*', '%', '@', 'admin@example.com', $this->admin->email, ' OR 1=1 --', str_repeat('x', 2048)];
foreach (array_slice($routes, 0, 45) as $route) {
$method = $this->primaryMethod($route);
foreach (array_slice($queries, 0, 4) as $query) {
$response = $this->requestAs($this->studentUser, $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [
'q' => $query,
'search' => $query,
'email' => $query,
'scope' => 'all',
]);
$this->assertControlled($response, $method, $route->uri());
$this->assertStringNotContainsString('sqlstate', strtolower($response->getContent()));
}
}
}
}
@@ -0,0 +1,35 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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']);
$text = "اختبار עברית café e\u{0301} \u{202E}evil.pdf 😬";
foreach (array_slice($routes, 0, 45) as $route) {
$method = $this->primaryMethod($route);
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
continue;
}
$response = $this->withHeader('Accept-Language', 'ar,en;q=0.7')
->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [
'name' => $text,
'title' => $text,
'message' => $text,
'notes' => $text,
'description' => $text,
]);
$this->assertControlled($response, $method, $route->uri());
$this->assertStringNotContainsString('malformed utf-8', strtolower($response->getContent()));
}
}
}
@@ -0,0 +1,35 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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']);
foreach (array_slice($routes, 0, 55) as $route) {
$method = $this->primaryMethod($route);
if ($method !== 'GET') {
continue;
}
$response = $this->requestAs($this->parent, $method, $route->uri(), [
'fields' => '*,password,remember_token,permissions,roles,audit_logs',
'include' => 'users,roles,permissions,finance.raw_provider_payload,deleted,archived',
'meta' => 'debug,sql,bindings,policies',
'debug' => true,
]);
$this->assertControlled($response, $method, $route->uri());
$body = strtolower($response->getContent());
foreach (['remember_token', 'raw_provider_payload', 'sql', 'bindings', 'password'] as $forbidden) {
$this->assertStringNotContainsString($forbidden, $body);
}
}
}
}
@@ -0,0 +1,35 @@
<?php
namespace Tests\Feature\Api\V1\FullSurface;
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']);
foreach (array_slice($routes, 0, 25) as $route) {
$method = $this->primaryMethod($route);
$uri = $route->uri();
for ($attempt = 1; $attempt <= 3; $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',
'message' => str_repeat('abuse ', 200),
]);
$this->assertControlled($response, $method, $uri);
$body = strtolower($response->getContent());
foreach (['too many connections', 'sqlstate', 'stack trace', 'vendor/laravel'] as $forbidden) {
$this->assertStringNotContainsString($forbidden, $body);
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More