Fix Laravel Pint formatting
This commit is contained in:
@@ -11,19 +11,19 @@ trait AssertsE2EApiResponses
|
||||
$this->assertLessThan(
|
||||
500,
|
||||
$response->getStatusCode(),
|
||||
$context . ' should return a controlled response, not a 5xx crash. Body: ' . $response->getContent()
|
||||
$context.' should return a controlled response, not a 5xx crash. Body: '.$response->getContent()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, int> $statuses
|
||||
* @param array<int, int> $statuses
|
||||
*/
|
||||
protected function assertStatusIn(TestResponse $response, array $statuses, string $context): void
|
||||
{
|
||||
$this->assertContains(
|
||||
$response->getStatusCode(),
|
||||
$statuses,
|
||||
$context . ' returned unexpected status ' . $response->getStatusCode() . '. Body: ' . $response->getContent()
|
||||
$context.' returned unexpected status '.$response->getStatusCode().'. Body: '.$response->getContent()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ trait CreatesApiTestUsers
|
||||
'status' => 'Active',
|
||||
'is_verified' => 1,
|
||||
'is_suspended' => 0,
|
||||
'email' => $roleName . '-api-test-' . str_replace('.', '', uniqid('', true)) . '@example.test',
|
||||
'email' => $roleName.'-api-test-'.str_replace('.', '', uniqid('', true)).'@example.test',
|
||||
], $overrides));
|
||||
|
||||
$role = $roles[$roleName] ?? Role::query()->where('name', $roleName)->firstOrFail();
|
||||
|
||||
@@ -22,4 +22,4 @@ trait SeedsAdministratorApiConfig
|
||||
['config_value' => $semester]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ trait SeedsE2ETestFixtures
|
||||
use CreatesApiTestUsers;
|
||||
|
||||
protected const E2E_SCHOOL_YEAR = '2025-2026';
|
||||
|
||||
protected const E2E_PREV_SCHOOL_YEAR = '2024-2025';
|
||||
|
||||
protected const E2E_SEMESTER = 'Fall';
|
||||
|
||||
protected function seedE2EConfiguration(): void
|
||||
@@ -108,18 +110,18 @@ trait SeedsE2ETestFixtures
|
||||
$teacher = $this->createApiUserWithRole('teacher', [
|
||||
'firstname' => 'Tara',
|
||||
'lastname' => 'Teacher',
|
||||
'email' => 'e2e.teacher.' . uniqid('', true) . '@example.test',
|
||||
'email' => 'e2e.teacher.'.uniqid('', true).'@example.test',
|
||||
]);
|
||||
$this->seedTeacherClassAssignment($teacher->id, $classSectionId);
|
||||
|
||||
$parent = $this->createApiUserWithRole('parent', [
|
||||
'firstname' => 'Parent',
|
||||
'lastname' => 'Fixture',
|
||||
'email' => 'e2e.parent.' . uniqid('', true) . '@example.test',
|
||||
'email' => 'e2e.parent.'.uniqid('', true).'@example.test',
|
||||
]);
|
||||
|
||||
$studentId = DB::table('students')->insertGetId([
|
||||
'school_id' => 'S-E2E-' . random_int(1000, 9999),
|
||||
'school_id' => 'S-E2E-'.random_int(1000, 9999),
|
||||
'firstname' => 'Kai',
|
||||
'lastname' => 'Student',
|
||||
'dob' => '2014-09-01',
|
||||
@@ -221,7 +223,7 @@ trait SeedsE2ETestFixtures
|
||||
protected function postEnrollmentStatuses(array $studentStatusMap): TestResponse
|
||||
{
|
||||
return $this->post(
|
||||
'/api/v1/administrator/enrollment-withdrawal/update-statuses?' .
|
||||
'/api/v1/administrator/enrollment-withdrawal/update-statuses?'.
|
||||
http_build_query(['enrollment_status' => $studentStatusMap]),
|
||||
[]
|
||||
);
|
||||
@@ -231,7 +233,7 @@ trait SeedsE2ETestFixtures
|
||||
{
|
||||
return DB::table('invoices')->insertGetId(array_merge([
|
||||
'parent_id' => $parentId,
|
||||
'invoice_number' => 'INV-E2E-' . random_int(1000, 9999),
|
||||
'invoice_number' => 'INV-E2E-'.random_int(1000, 9999),
|
||||
'total_amount' => 500.00,
|
||||
'balance' => 500.00,
|
||||
'paid_amount' => 0,
|
||||
@@ -250,7 +252,7 @@ trait SeedsE2ETestFixtures
|
||||
{
|
||||
return DB::table('invoices')->insertGetId([
|
||||
'parent_id' => $parentId,
|
||||
'invoice_number' => 'INV-PRIOR-' . random_int(1000, 9999),
|
||||
'invoice_number' => 'INV-PRIOR-'.random_int(1000, 9999),
|
||||
'total_amount' => 200.00,
|
||||
'balance' => 150.00,
|
||||
'paid_amount' => 50.00,
|
||||
@@ -268,7 +270,7 @@ trait SeedsE2ETestFixtures
|
||||
protected function seedDiscountVoucher(): int
|
||||
{
|
||||
return DB::table('discount_vouchers')->insertGetId([
|
||||
'code' => 'E2E10-' . random_int(100, 999),
|
||||
'code' => 'E2E10-'.random_int(100, 999),
|
||||
'discount_type' => 'fixed',
|
||||
'discount_value' => 50,
|
||||
'max_uses' => 100,
|
||||
|
||||
@@ -13,7 +13,6 @@ class ApiAuthenticationAndAuthorizationTest extends TestCase
|
||||
use CreatesApiTestUsers;
|
||||
use RefreshDatabase;
|
||||
|
||||
|
||||
public function test_api_login_returns_token_and_user_object(): void
|
||||
{
|
||||
$user = $this->createApiUserWithRole('teacher', [
|
||||
@@ -103,7 +102,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();
|
||||
@@ -113,7 +112,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,7 +47,6 @@ class ApiPreferencesFeatureTest extends TestCase
|
||||
->assertJsonPath('status', true);
|
||||
}
|
||||
|
||||
|
||||
public function test_non_admin_cannot_list_or_manage_another_users_preferences(): void
|
||||
{
|
||||
$owner = $this->createApiUserWithRole('teacher');
|
||||
|
||||
@@ -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,8 +2,6 @@
|
||||
|
||||
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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,14 +26,14 @@ class ApiUseCaseCoverageMatrixTest extends TestCase
|
||||
$uri = $route->uri();
|
||||
|
||||
if ($this->domainFor($uri) === null) {
|
||||
$unmapped[] = implode('|', array_diff($route->methods(), ['HEAD'])) . ' ' . $uri;
|
||||
$unmapped[] = implode('|', array_diff($route->methods(), ['HEAD'])).' '.$uri;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame(
|
||||
[],
|
||||
$unmapped,
|
||||
"API routes without an explicit use-case domain:\n" . implode("\n", $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."
|
||||
);
|
||||
}
|
||||
@@ -53,7 +53,7 @@ class ApiUseCaseCoverageMatrixTest extends TestCase
|
||||
$this->assertSame(
|
||||
[],
|
||||
$missing,
|
||||
"Required use-case domains with no matching API route coverage:\n" . implode("\n", $missing)
|
||||
"Required use-case domains with no matching API route coverage:\n".implode("\n", $missing)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ class ApiUseCaseCoverageMatrixTest extends TestCase
|
||||
|
||||
foreach ($this->protectedRoutes() as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
$uri = '/' . $this->uriWithSafePlaceholders($route->uri());
|
||||
$uri = '/'.$this->uriWithSafePlaceholders($route->uri());
|
||||
|
||||
$response = $this->json($method, $uri);
|
||||
$status = $response->getStatusCode();
|
||||
@@ -76,7 +76,7 @@ class ApiUseCaseCoverageMatrixTest extends TestCase
|
||||
$this->assertSame(
|
||||
[],
|
||||
$failures,
|
||||
"Anonymous requests reached protected API routes unexpectedly:\n" . implode("\n", $failures)
|
||||
"Anonymous requests reached protected API routes unexpectedly:\n".implode("\n", $failures)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
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,6 +263,7 @@ 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,11 +16,17 @@ 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
|
||||
@@ -418,4 +424,4 @@ class AssignmentApiControllerTest extends TestCase
|
||||
|
||||
$this->assertSame(['Main Teacher'], $mainTeachers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
namespace Tests\Feature\Api;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\Configuration;
|
||||
use App\Models\Student;
|
||||
use App\Models\StudentClass;
|
||||
use App\Models\TeacherClass;
|
||||
use App\Models\ClassSection;
|
||||
use App\Models\Configuration;
|
||||
use App\Models\User;
|
||||
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 doesn’t 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 doesn’t 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,6 +4,7 @@ namespace Tests\Feature\Api\V1\AttendanceTracking;
|
||||
|
||||
use App\Services\AttendanceTracking\AttendanceTrackingService;
|
||||
use Mockery;
|
||||
use Mockery\MockInterface;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AttendanceTrackingControllerTest extends TestCase
|
||||
@@ -14,7 +15,7 @@ class AttendanceTrackingControllerTest extends TestCase
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
protected function mockService(): \Mockery\MockInterface
|
||||
protected function mockService(): 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',
|
||||
|
||||
@@ -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,7 +31,8 @@ class BroadcastEmailControllerTest extends TestCase
|
||||
{
|
||||
$this->seedParentData();
|
||||
|
||||
$fake = new class extends EmailService {
|
||||
$fake = new class extends EmailService
|
||||
{
|
||||
public array $sent = [];
|
||||
|
||||
public function send(
|
||||
@@ -41,9 +42,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,7 +50,8 @@ class CommunicationControllerTest extends TestCase
|
||||
{
|
||||
$this->seedCommunicationData();
|
||||
|
||||
$fake = new class extends EmailService {
|
||||
$fake = new class extends EmailService
|
||||
{
|
||||
public array $sent = [];
|
||||
|
||||
public function send(
|
||||
@@ -62,6 +63,7 @@ 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,7 +4,6 @@ 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');
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ use Tests\TestCase;
|
||||
*/
|
||||
class ApiAcademicAttendanceFullSurfaceContractTest extends TestCase
|
||||
{
|
||||
use AssertsE2EApiResponses;
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
use AssertsE2EApiResponses;
|
||||
|
||||
public function test_academic_attendance_and_grading_surfaces_have_controlled_contracts(): void
|
||||
{
|
||||
@@ -24,21 +24,21 @@ class ApiAcademicAttendanceFullSurfaceContractTest extends TestCase
|
||||
|
||||
foreach ($this->teacherGetEndpoints($world) as $uri) {
|
||||
$response = $this->getJson($uri);
|
||||
$this->assertNoServerError($response, 'Teacher GET ' . $uri);
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Teacher GET ' . $uri);
|
||||
$this->assertNoServerError($response, 'Teacher GET '.$uri);
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Teacher GET '.$uri);
|
||||
}
|
||||
|
||||
foreach ($this->teacherMutationEndpoints($world) as [$method, $uri, $payload]) {
|
||||
$response = $this->json($method, $uri, $payload);
|
||||
$this->assertNoServerError($response, 'Teacher ' . $method . ' ' . $uri);
|
||||
$this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Teacher ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'Teacher '.$method.' '.$uri);
|
||||
$this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Teacher '.$method.' '.$uri);
|
||||
}
|
||||
|
||||
$this->actingAsApiAdministrator();
|
||||
foreach ($this->adminAcademicEndpoints($world) as [$method, $uri, $payload]) {
|
||||
$response = $this->json($method, $uri, $payload);
|
||||
$this->assertNoServerError($response, 'Admin academic ' . $method . ' ' . $uri);
|
||||
$this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Admin academic ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'Admin academic '.$method.' '.$uri);
|
||||
$this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Admin academic '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,35 +49,35 @@ class ApiAcademicAttendanceFullSurfaceContractTest extends TestCase
|
||||
$studentId = (int) $world['student_id'];
|
||||
|
||||
return [
|
||||
'/api/v1/teachers/classes?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/class-view?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teachers/classes?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/class-view?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/no-classes',
|
||||
'/api/v1/teacher/select-semester',
|
||||
'/api/v1/teacher/homework-list?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/add-homework?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/add-quiz?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/add-quiz-column-form?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/add-midterm-exam?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/add-final-exam?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/add-participation?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/add-project?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/homework-list?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/add-homework?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/add-quiz?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/add-quiz-column-form?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/add-midterm-exam?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/add-final-exam?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/add-participation?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/add-project?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/teacher-assignment',
|
||||
'/api/v1/teacher/exam-drafts',
|
||||
'/api/v1/teacher/drafts',
|
||||
'/api/v1/teacher/inventory/books/distribute',
|
||||
'/api/v1/teacher/print-requests/context',
|
||||
'/api/v1/teacher/absence-vacation',
|
||||
'/api/v1/teacher/showupdate-attendance?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/showupdate_attendance?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/scores?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/showupdate-attendance?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/showupdate_attendance?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/scores?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/calendar',
|
||||
'/api/v1/teacher/class-progress-submit?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/class-progress-history?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/class-progress-view?class_section_id=' . $classSectionId,
|
||||
'/api/v1/teacher/class-progress-submit?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/class-progress-history?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/class-progress-view?class_section_id='.$classSectionId,
|
||||
'/api/v1/teacher/competition-scores',
|
||||
'/api/v1/attendance/teacher/grid?class_section_id=' . $classSectionId,
|
||||
'/api/v1/attendance/teacher/form?class_section_id=' . $classSectionId,
|
||||
'/api/v1/attendance/admin/daily?date=2025-10-05&class_section_id=' . $classSectionId,
|
||||
'/api/v1/attendance/teacher/grid?class_section_id='.$classSectionId,
|
||||
'/api/v1/attendance/teacher/form?class_section_id='.$classSectionId,
|
||||
'/api/v1/attendance/admin/daily?date=2025-10-05&class_section_id='.$classSectionId,
|
||||
'/api/v1/attendance/staff/month?month=2025-10',
|
||||
'/api/v1/attendance/staff/admins',
|
||||
'/api/v1/attendance/late-slip-logs',
|
||||
@@ -87,7 +87,7 @@ class ApiAcademicAttendanceFullSurfaceContractTest extends TestCase
|
||||
'/api/v1/attendance/management/dashboard',
|
||||
'/api/v1/attendance-tracking/pending-violations',
|
||||
'/api/v1/attendance-tracking/notified-violations',
|
||||
'/api/v1/attendance-tracking/student-case/' . $studentId,
|
||||
'/api/v1/attendance-tracking/student-case/'.$studentId,
|
||||
'/api/v1/attendance-tracking/compose',
|
||||
'/api/v1/attendance-tracking/parents-info',
|
||||
'/api/v1/attendance-comment-templates',
|
||||
@@ -96,15 +96,15 @@ class ApiAcademicAttendanceFullSurfaceContractTest extends TestCase
|
||||
'/api/v1/attendance-templates',
|
||||
'/api/v1/assignments',
|
||||
'/api/v1/assignments/class-assignment-data',
|
||||
'/api/v1/scores/overview?class_section_id=' . $classSectionId,
|
||||
'/api/v1/scores/homework?class_section_id=' . $classSectionId,
|
||||
'/api/v1/scores/quiz?class_section_id=' . $classSectionId,
|
||||
'/api/v1/scores/project?class_section_id=' . $classSectionId,
|
||||
'/api/v1/scores/midterm?class_section_id=' . $classSectionId,
|
||||
'/api/v1/scores/final?class_section_id=' . $classSectionId,
|
||||
'/api/v1/scores/participation?class_section_id=' . $classSectionId,
|
||||
'/api/v1/grading/homework-tracking?class_section_id=' . $classSectionId,
|
||||
'/api/v1/reports/report-cards/meta?class_section_id=' . $classSectionId,
|
||||
'/api/v1/scores/overview?class_section_id='.$classSectionId,
|
||||
'/api/v1/scores/homework?class_section_id='.$classSectionId,
|
||||
'/api/v1/scores/quiz?class_section_id='.$classSectionId,
|
||||
'/api/v1/scores/project?class_section_id='.$classSectionId,
|
||||
'/api/v1/scores/midterm?class_section_id='.$classSectionId,
|
||||
'/api/v1/scores/final?class_section_id='.$classSectionId,
|
||||
'/api/v1/scores/participation?class_section_id='.$classSectionId,
|
||||
'/api/v1/grading/homework-tracking?class_section_id='.$classSectionId,
|
||||
'/api/v1/reports/report-cards/meta?class_section_id='.$classSectionId,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -159,8 +159,8 @@ class ApiAcademicAttendanceFullSurfaceContractTest extends TestCase
|
||||
private function adminAcademicEndpoints(array $world): array
|
||||
{
|
||||
return [
|
||||
['GET', '/api/v1/reports/report-cards?student_id=' . $world['student_id'], []],
|
||||
['GET', '/api/v1/reports/stickers?student_id=' . $world['student_id'], []],
|
||||
['GET', '/api/v1/reports/report-cards?student_id='.$world['student_id'], []],
|
||||
['GET', '/api/v1/reports/stickers?student_id='.$world['student_id'], []],
|
||||
['GET', '/api/v1/badges/form-options', []],
|
||||
['POST', '/api/v1/print-requests', ['title' => 'Contract Probe', 'copies' => 1, 'needed_by' => '2025-10-12']],
|
||||
['GET', '/api/v1/print-requests/admin', []],
|
||||
|
||||
@@ -24,7 +24,7 @@ class ApiAccountStatusLifecycleContractTest extends FullSurfaceE2EContractCase
|
||||
$response = $this->requestAs($actor, 'GET', $uri);
|
||||
|
||||
$this->assertControlled($response, 'GET', $uri);
|
||||
$this->assertNoServerError($response, 'account status ' . $status . ' at GET ' . $uri);
|
||||
$this->assertNoServerError($response, 'account status '.$status.' at GET '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ use Tests\TestCase;
|
||||
*/
|
||||
class ApiAdminOperationsFullSurfaceContractTest extends TestCase
|
||||
{
|
||||
use AssertsE2EApiResponses;
|
||||
use RefreshDatabase;
|
||||
use SeedsE2ETestFixtures;
|
||||
use AssertsE2EApiResponses;
|
||||
|
||||
public function test_administrator_operational_surface_responds_with_controlled_contracts(): void
|
||||
{
|
||||
@@ -24,19 +24,19 @@ class ApiAdminOperationsFullSurfaceContractTest extends TestCase
|
||||
|
||||
foreach ($this->adminGetEndpoints($world) as $uri) {
|
||||
$response = $this->getJson($uri);
|
||||
$this->assertNoServerError($response, 'Admin GET ' . $uri);
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Admin GET ' . $uri);
|
||||
$this->assertNoServerError($response, 'Admin GET '.$uri);
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Admin GET '.$uri);
|
||||
}
|
||||
|
||||
foreach ($this->adminMutationEndpoints($world, $admin->id) as [$method, $uri, $payload]) {
|
||||
$response = $this->json($method, $uri, $payload);
|
||||
$this->assertNoServerError($response, 'Admin ' . $method . ' ' . $uri);
|
||||
$this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Admin ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'Admin '.$method.' '.$uri);
|
||||
$this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Admin '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $world
|
||||
* @param array<string, mixed> $world
|
||||
* @return list<string>
|
||||
*/
|
||||
private function adminGetEndpoints(array $world): array
|
||||
@@ -76,7 +76,7 @@ class ApiAdminOperationsFullSurfaceContractTest extends TestCase
|
||||
'/api/v1/administrator/flags/students/1',
|
||||
'/api/v1/administrator/flags/processed',
|
||||
'/api/v1/administrator/flags/analysis',
|
||||
'/api/v1/administrator/grading/homework-tracking?class_section_id=' . $classSectionId,
|
||||
'/api/v1/administrator/grading/homework-tracking?class_section_id='.$classSectionId,
|
||||
'/api/v1/administrator/expenses',
|
||||
'/api/v1/administrator/expenses/options',
|
||||
'/api/v1/administrator/expenses/1',
|
||||
@@ -110,13 +110,13 @@ class ApiAdminOperationsFullSurfaceContractTest extends TestCase
|
||||
'/api/v1/class-sections',
|
||||
'/api/v1/classes',
|
||||
'/api/v1/users',
|
||||
'/api/v1/users/' . $world['parent_id'],
|
||||
'/api/v1/students/' . $studentId,
|
||||
'/api/v1/users/'.$world['parent_id'],
|
||||
'/api/v1/students/'.$studentId,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $world
|
||||
* @param array<string, mixed> $world
|
||||
* @return list<array{0: string, 1: string, 2: array<string, mixed>}>
|
||||
*/
|
||||
private function adminMutationEndpoints(array $world, int $adminId): array
|
||||
|
||||
@@ -43,7 +43,7 @@ class ApiAttachmentUploadContentSafetyContractTest extends FullSurfaceE2EContrac
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $this->materializePath($route->uri()) . '?filename=../../.env&path=../../storage/logs/laravel.log';
|
||||
$path = $this->materializePath($route->uri()).'?filename=../../.env&path=../../storage/logs/laravel.log';
|
||||
$response = $this->actingAs($this->actorFor($route->uri()), 'api')->getJson($path);
|
||||
|
||||
$this->assertStatusIn($response, [200, 204, 400, 401, 403, 404, 409, 422], "GET {$route->uri()} path traversal probe");
|
||||
|
||||
+2
-2
@@ -21,8 +21,8 @@ class ApiAuditLogAndAdministrativeTraceabilityContractTest extends FullSurfaceE2
|
||||
|
||||
$this->assertNoServerError($response, "$method $uri admin mutation traceability");
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertStringNotContainsStringIgnoringCase('audit_log_id', $response->getContent(), $uri . ' should not leak raw internal audit identifiers by accident.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('stack_trace', $response->getContent(), $uri . ' should not leak stack traces in mutation responses.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('audit_log_id', $response->getContent(), $uri.' should not leak raw internal audit identifiers by accident.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('stack_trace', $response->getContent(), $uri.' should not leak stack traces in mutation responses.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,12 +20,12 @@ class ApiAuthenticationLifecycleDepthContractTest extends FullSurfaceE2EContract
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$this->assertNoServerError($response, 'valid login at ' . $path);
|
||||
$this->assertNoServerError($response, 'valid login at '.$path);
|
||||
|
||||
if ($response->getStatusCode() >= 200 && $response->getStatusCode() < 300) {
|
||||
$this->assertTrue(
|
||||
$response->json('token') !== null || $response->json('access_token') !== null || $response->json('user') !== null,
|
||||
$path . ' must return a usable login contract, not a mystery envelope.'
|
||||
$path.' must return a usable login contract, not a mystery envelope.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -36,18 +36,18 @@ class ApiAuthenticationLifecycleDepthContractTest extends FullSurfaceE2EContract
|
||||
'password' => 'definitely-wrong-password',
|
||||
]);
|
||||
|
||||
$this->assertStatusIn($response, [400, 401, 403, 419, 422], 'invalid login at ' . $path);
|
||||
$this->assertNoServerError($response, 'invalid login at ' . $path);
|
||||
$this->assertStatusIn($response, [400, 401, 403, 419, 422], 'invalid login at '.$path);
|
||||
$this->assertNoServerError($response, 'invalid login at '.$path);
|
||||
}
|
||||
|
||||
foreach (['/api/v1/auth/me', '/api/v1/me', '/api/user'] as $path) {
|
||||
$response = $this->actingAs($this->admin, 'api')->getJson($path);
|
||||
$this->assertNoServerError($response, 'authenticated identity lookup at ' . $path);
|
||||
$this->assertNoServerError($response, 'authenticated identity lookup at '.$path);
|
||||
|
||||
if ($response->getStatusCode() === 200) {
|
||||
$this->assertTrue(
|
||||
$response->json('user') !== null || $response->json('id') !== null || $response->json('data') !== null,
|
||||
$path . ' must expose a stable identity payload.'
|
||||
$path.' must expose a stable identity payload.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -56,10 +56,10 @@ class ApiAuthenticationLifecycleDepthContractTest extends FullSurfaceE2EContract
|
||||
$first = $this->actingAs($this->admin, 'api')->postJson($path);
|
||||
$second = $this->actingAs($this->admin, 'api')->postJson($path);
|
||||
|
||||
$this->assertNoServerError($first, 'first logout at ' . $path);
|
||||
$this->assertNoServerError($second, 'repeat logout at ' . $path);
|
||||
$this->assertStatusIn($first, [200, 202, 204, 302, 401, 404, 419, 422], 'first logout at ' . $path);
|
||||
$this->assertStatusIn($second, [200, 202, 204, 302, 401, 404, 419, 422], 'repeat logout at ' . $path);
|
||||
$this->assertNoServerError($first, 'first logout at '.$path);
|
||||
$this->assertNoServerError($second, 'repeat logout at '.$path);
|
||||
$this->assertStatusIn($first, [200, 202, 204, 302, 401, 404, 419, 422], 'first logout at '.$path);
|
||||
$this->assertStatusIn($second, [200, 202, 204, 302, 401, 404, 419, 422], 'repeat logout at '.$path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,16 +75,16 @@ class ApiAuthenticationLifecycleDepthContractTest extends FullSurfaceE2EContract
|
||||
|
||||
$uri = $route->uri();
|
||||
$response = $this->json($method, $this->materializePath($uri), [
|
||||
'email' => 'nobody.' . uniqid() . '@example.test',
|
||||
'email' => 'nobody.'.uniqid().'@example.test',
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'not-the-same',
|
||||
'token' => 'not-a-real-token',
|
||||
]);
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, $method . ' ' . $uri);
|
||||
$this->assertStringNotContainsStringIgnoringCase('select * from', $response->getContent(), $uri . ' must not leak account lookup SQL.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('No query results for model', $response->getContent(), $uri . ' must not leak framework model lookups.');
|
||||
$this->assertNoServerError($response, $method.' '.$uri);
|
||||
$this->assertStringNotContainsStringIgnoringCase('select * from', $response->getContent(), $uri.' must not leak account lookup SQL.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('No query results for model', $response->getContent(), $uri.' must not leak framework model lookups.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ class ApiAuthorizationCacheInvalidationContractTest extends FullSurfaceE2EContra
|
||||
|
||||
$response = $this->requestAs($this->parent, $method, $uri, $payload);
|
||||
|
||||
$this->assertStatusIn($response, [401, 403, 404, 405, 419, 422], 'parent privilege escalation should be blocked at ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'permission cache escalation probe at ' . $method . ' ' . $uri);
|
||||
$this->assertStatusIn($response, [401, 403, 404, 405, 419, 422], 'parent privilege escalation should be blocked at '.$method.' '.$uri);
|
||||
$this->assertNoServerError($response, 'permission cache escalation probe at '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class ApiBatch11RouteRegressionNetTest extends FullSurfaceE2EContractCase
|
||||
foreach ($families as $family => $needles) {
|
||||
$this->assertNotEmpty(
|
||||
$this->apiRoutesContainingAny($needles),
|
||||
'Batch 11 expects at least one route for the ' . $family . ' family. If this fails, update the route catalog instead of pretending the family vanished.'
|
||||
'Batch 11 expects at least one route for the '.$family.' family. If this fails, update the route catalog instead of pretending the family vanished.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ class ApiBatch11RouteRegressionNetTest extends FullSurfaceE2EContractCase
|
||||
])->json($method, $this->materializePath($uri), $payload);
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, 'batch 11 combined hostile probe at ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'batch 11 combined hostile probe at '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
namespace Tests\Feature\Api\V1\FullSurface;
|
||||
|
||||
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch12AttachmentLifecycleContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
+5
-3
@@ -6,7 +6,7 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14AcademicPromotionRollbackIntegrityContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_promotion_and_school_year_routes_reject_unsafe_rollback_payloads(): void
|
||||
public function test_promotion_and_school_year_routes_reject_unsafe_rollback_payloads(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['promotion', 'promote', 'school-year', 'semester', 'close', 'reopen', 'rollback']), 0, 45) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
@@ -22,10 +22,12 @@ public function test_promotion_and_school_year_routes_reject_unsafe_rollback_pay
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['promotion', 'school-year', 'close', 'reopen']), 0, 30) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { continue; }
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
|
||||
continue;
|
||||
}
|
||||
foreach ([$this->teacher, $this->parent, $this->studentUser] as $actor) {
|
||||
$response = $this->requestAs($actor, $method, $route->uri(), ['force' => true, 'confirmed' => true, 'school_year_id' => $this->ids['schoolYearId']]);
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri());
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method.' '.$route->uri());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -6,13 +6,15 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14AccountEnumerationAndIdentityPrivacyContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_identity_recovery_routes_do_not_reveal_whether_user_exists(): void
|
||||
public function test_identity_recovery_routes_do_not_reveal_whether_user_exists(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['forgot', 'password', 'reset', 'verify', 'verification', 'recovery']);
|
||||
foreach (array_slice($routes, 0, 30) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { continue; }
|
||||
foreach ([$this->admin->email, 'missing-user-' . uniqid() . '@example.test'] as $email) {
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
|
||||
continue;
|
||||
}
|
||||
foreach ([$this->admin->email, 'missing-user-'.uniqid().'@example.test'] as $email) {
|
||||
$response = $this->json($method, $this->materializePath($route->uri()), ['email' => $email, 'token' => 'invalid-token', 'password' => 'NewPassword123!', 'password_confirmation' => 'NewPassword123!']);
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
$body = strtolower($response->getContent());
|
||||
@@ -31,7 +33,7 @@ public function test_identity_recovery_routes_do_not_reveal_whether_user_exists(
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
$body = strtolower($response->getContent());
|
||||
foreach (['password', 'remember_token', 'two_factor', 'api_token', 'provider_token', 'secret'] as $forbidden) {
|
||||
$this->assertStringNotContainsString($forbidden, $body, $route->uri() . ' leaked identity internals.');
|
||||
$this->assertStringNotContainsString($forbidden, $body, $route->uri().' leaked identity internals.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -6,7 +6,7 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14AdminDelegationApprovalWorkflowContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_approval_and_delegation_routes_reject_forged_approval_metadata(): void
|
||||
public function test_approval_and_delegation_routes_reject_forged_approval_metadata(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['approve', 'approval', 'delegate', 'delegation', 'authorize', 'permission', 'role', 'impersonate']), 0, 50) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
@@ -20,10 +20,12 @@ public function test_approval_and_delegation_routes_reject_forged_approval_metad
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['approve', 'approval', 'delegate', 'permission', 'role']), 0, 35) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { continue; }
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
|
||||
continue;
|
||||
}
|
||||
foreach ([$this->teacher, $this->parent, $this->studentUser] as $actor) {
|
||||
$response = $this->requestAs($actor, $method, $route->uri(), ['approved' => true, 'permissions' => ['*'], 'role' => 'administrator', 'force' => true]);
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri());
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method.' '.$route->uri());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-6
@@ -6,15 +6,17 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14ApiConsumerContractCompatibilityTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_core_identity_contracts_remain_compatible_for_web_mobile_and_api_clients(): void
|
||||
public function test_core_identity_contracts_remain_compatible_for_web_mobile_and_api_clients(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['login', 'auth/me', 'me', 'frontend', 'profile']), 0, 25) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
foreach ([['Accept' => 'application/json', 'X-Client' => 'web'], ['Accept' => 'application/json', 'X-Client' => 'mobile', 'X-App-Version' => '1.0.0'], ['Accept' => 'application/vnd.alrahma.v1+json', 'X-Client' => 'api-consumer']] as $headers) {
|
||||
$response = $this->withHeaders($headers)->actingAs($this->actorFor($route->uri()), 'api')->json($method, $this->materializePath($route->uri()), $this->payloadFor($method, $route->uri()));
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
$this->assertJsonResponseWhenSuccessful($response, $method . ' ' . $route->uri());
|
||||
if ($response->isSuccessful()) { $this->assertStringNotContainsString('undefined index', strtolower($response->getContent())); }
|
||||
$this->assertJsonResponseWhenSuccessful($response, $method.' '.$route->uri());
|
||||
if ($response->isSuccessful()) {
|
||||
$this->assertStringNotContainsString('undefined index', strtolower($response->getContent()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,13 +25,15 @@ public function test_core_identity_contracts_remain_compatible_for_web_mobile_an
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['login', 'auth/login']), 0, 10) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if ($method !== 'POST') { continue; }
|
||||
if ($method !== 'POST') {
|
||||
continue;
|
||||
}
|
||||
$response = $this->json($method, $this->materializePath($route->uri()), ['email' => $this->admin->email, 'password' => 'password']);
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
if ($response->isSuccessful() && $this->isJsonString($response->getContent())) {
|
||||
$data = $response->json();
|
||||
$this->assertTrue(isset($data['token']) || isset($data['access_token']), $route->uri() . ' should expose token/access_token when login succeeds.');
|
||||
$this->assertArrayHasKey('user', $data, $route->uri() . ' should expose user when login succeeds.');
|
||||
$this->assertTrue(isset($data['token']) || isset($data['access_token']), $route->uri().' should expose token/access_token when login succeeds.');
|
||||
$this->assertArrayHasKey('user', $data, $route->uri().' should expose user when login succeeds.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -6,7 +6,7 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14AttendanceDeviceAndKioskAbuseContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_kiosk_scanner_and_attendance_routes_reject_forged_device_identity(): void
|
||||
public function test_kiosk_scanner_and_attendance_routes_reject_forged_device_identity(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['attendance', 'scanner', 'scan', 'kiosk', 'badge', 'dismissal', 'late']), 0, 55) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
@@ -22,10 +22,12 @@ public function test_kiosk_scanner_and_attendance_routes_reject_forged_device_id
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['attendance', 'scan', 'dismissal', 'late']), 0, 30) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { continue; }
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
|
||||
continue;
|
||||
}
|
||||
foreach ([$this->studentUser, $this->parent] as $actor) {
|
||||
$response = $this->requestAs($actor, $method, $route->uri(), ['student_id' => $this->ids['studentId'], 'status' => 'present', 'verified_by_device' => true, 'override_reason' => 'parent approved']);
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri());
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method.' '.$route->uri());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -6,7 +6,7 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14AuditTrailTamperResistanceContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_audit_and_log_routes_reject_client_supplied_actor_and_timestamp_fields(): void
|
||||
public function test_audit_and_log_routes_reject_client_supplied_actor_and_timestamp_fields(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['audit', 'log', 'activity', 'history', 'trace']), 0, 35) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
@@ -22,10 +22,12 @@ public function test_audit_and_log_routes_reject_client_supplied_actor_and_times
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['audit', 'log', 'history', 'activity']), 0, 25) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { continue; }
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
|
||||
continue;
|
||||
}
|
||||
foreach ([$this->parent, $this->teacher, $this->studentUser] as $actor) {
|
||||
$response = $this->requestAs($actor, $method, $route->uri(), ['action' => 'clear', 'purge' => true, 'rewrite' => true]);
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri());
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method.' '.$route->uri());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,13 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14BulkAssignmentCollisionContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_bulk_assignment_routes_handle_duplicate_and_conflicting_ids_cleanly(): void
|
||||
public function test_bulk_assignment_routes_handle_duplicate_and_conflicting_ids_cleanly(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['assign', 'assignment', 'enroll', 'bulk', 'batch', 'class', 'teacher', 'student']), 0, 50) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { continue; }
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
|
||||
continue;
|
||||
}
|
||||
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [
|
||||
'student_ids' => [$this->ids['studentId'], $this->ids['studentId'], 99999999], 'teacher_ids' => [$this->teacher->id, $this->teacher->id, $this->parent->id],
|
||||
'class_section_ids' => [$this->ids['classSectionId'], $this->ids['classSectionId'], 99999999], 'parent_ids' => [$this->ids['parentId'], $this->ids['parentId'], 99999999], 'force' => true, 'overwrite' => true,
|
||||
@@ -27,10 +29,12 @@ public function test_bulk_assignment_routes_handle_duplicate_and_conflicting_ids
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['assign', 'bulk', 'batch', 'class-sections', 'teacher-class', 'enroll']), 0, 30) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { continue; }
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
|
||||
continue;
|
||||
}
|
||||
foreach ([$this->parent, $this->studentUser] as $actor) {
|
||||
$response = $this->requestAs($actor, $method, $route->uri(), ['student_ids' => [$this->ids['studentId']], 'teacher_ids' => [$this->teacher->id], 'class_section_ids' => [$this->ids['classSectionId']], 'all' => true, 'force' => true]);
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri());
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method.' '.$route->uri());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14CrossModuleInvariantRiskRegisterTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_batch14_route_families_have_cross_module_risk_coverage(): void
|
||||
public function test_batch14_route_families_have_cross_module_risk_coverage(): void
|
||||
{
|
||||
$riskFamilies = [
|
||||
'session-cookie-boundary' => ['auth', 'login', 'frontend'],
|
||||
@@ -22,7 +22,7 @@ public function test_batch14_route_families_have_cross_module_risk_coverage(): v
|
||||
];
|
||||
|
||||
foreach ($riskFamilies as $name => $needles) {
|
||||
$this->assertNotEmpty($this->apiRoutesContainingAny($needles), 'Batch 14 expected route coverage for ' . $name);
|
||||
$this->assertNotEmpty($this->apiRoutesContainingAny($needles), 'Batch 14 expected route coverage for '.$name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public function test_batch14_route_families_have_cross_module_risk_coverage(): v
|
||||
$mutations = array_filter($this->apiRoutesContainingAny([$family]), function ($route): bool {
|
||||
return in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH', 'DELETE'], true);
|
||||
});
|
||||
$this->assertNotEmpty($mutations, 'Batch 14 expected mutating route family: ' . $family);
|
||||
$this->assertNotEmpty($mutations, 'Batch 14 expected mutating route family: '.$family);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-4
@@ -6,14 +6,15 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14DataLifecycleArchivePurgeContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_archive_purge_and_retention_routes_require_safe_controlled_requests(): void
|
||||
public function test_archive_purge_and_retention_routes_require_safe_controlled_requests(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['archive', 'restore', 'purge', 'erase', 'retention', 'delete', 'destroy']), 0, 50) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + ['ids' => [$this->ids['studentId'], 99999999, '../../.env'], 'hard_delete' => true, 'purge' => true, 'cascade' => true, 'include_audit_logs' => true, 'reason' => '', 'confirmed' => false]);
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
$body = strtolower($response->getContent());
|
||||
$this->assertStringNotContainsString('sqlstate', $body); $this->assertStringNotContainsString('foreign key constraint', $body);
|
||||
$this->assertStringNotContainsString('sqlstate', $body);
|
||||
$this->assertStringNotContainsString('foreign key constraint', $body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +22,12 @@ public function test_archive_purge_and_retention_routes_require_safe_controlled_
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['purge', 'erase', 'restore', 'archive', 'delete']), 0, 35) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { continue; }
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
|
||||
continue;
|
||||
}
|
||||
foreach ([$this->teacher, $this->parent, $this->studentUser] as $actor) {
|
||||
$response = $this->requestAs($actor, $method, $route->uri(), ['ids' => [$this->ids['studentId']], 'hard_delete' => true, 'purge' => true, 'global' => true]);
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri());
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method.' '.$route->uri());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-5
@@ -6,16 +6,18 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14FinanceReconciliationAndRefundAbuseContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_finance_reconciliation_routes_reject_mismatched_invoice_parent_and_payment_fields(): void
|
||||
public function test_finance_reconciliation_routes_reject_mismatched_invoice_parent_and_payment_fields(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['finance', 'invoice', 'payment', 'refund', 'transaction', 'ledger', 'reconcile']), 0, 55) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [
|
||||
'invoice_id' => $this->ids['invoiceId'], 'parent_id' => 99999999, 'student_id' => 99999999, 'payment_id' => 99999999, 'refund_id' => 99999999, 'transaction_id' => 'provider-' . uniqid(), 'provider_transaction_id' => 'provider-' . uniqid(), 'amount' => -99999.99, 'net_amount' => -99999.99, 'fee_amount' => -99999.99, 'currency' => 'XXX', 'reconciled' => true, 'force_posted' => true,
|
||||
'invoice_id' => $this->ids['invoiceId'], 'parent_id' => 99999999, 'student_id' => 99999999, 'payment_id' => 99999999, 'refund_id' => 99999999, 'transaction_id' => 'provider-'.uniqid(), 'provider_transaction_id' => 'provider-'.uniqid(), 'amount' => -99999.99, 'net_amount' => -99999.99, 'fee_amount' => -99999.99, 'currency' => 'XXX', 'reconciled' => true, 'force_posted' => true,
|
||||
]);
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
$body = strtolower($response->getContent());
|
||||
$this->assertStringNotContainsString('sqlstate', $body); $this->assertStringNotContainsString('duplicate entry', $body); $this->assertStringNotContainsString('foreign key constraint', $body);
|
||||
$this->assertStringNotContainsString('sqlstate', $body);
|
||||
$this->assertStringNotContainsString('duplicate entry', $body);
|
||||
$this->assertStringNotContainsString('foreign key constraint', $body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +25,11 @@ public function test_finance_reconciliation_routes_reject_mismatched_invoice_par
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['refund', 'reconcile', 'ledger', 'payment']), 0, 35) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { continue; }
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
|
||||
continue;
|
||||
}
|
||||
$response = $this->requestAs($this->parent, $method, $route->uri(), ['invoice_id' => $this->ids['invoiceId'], 'amount' => 100.00, 'approved' => true, 'reconciled' => true, 'refund_now' => true]);
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri());
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method.' '.$route->uri());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-5
@@ -6,17 +6,21 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14NotificationTemplateInjectionContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_notification_and_template_routes_reject_template_injection_payloads(): void
|
||||
public function test_notification_and_template_routes_reject_template_injection_payloads(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['notification', 'notify', 'message', 'email', 'whatsapp', 'template', 'broadcast']), 0, 55) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { continue; }
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
|
||||
continue;
|
||||
}
|
||||
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [
|
||||
'subject' => '{{ config("app.key") }}', 'message' => '<script>alert(document.cookie)</script>{{ $user->password }}', 'body' => '${jndi:ldap://127.0.0.1/a}', 'template' => '@php echo env("APP_KEY"); @endphp', 'channel' => 'all', 'recipients' => ['*'], 'phone' => "+15551234567\r\nBcc: attacker@example.test", 'email' => "victim@example.test\r\nBcc: attacker@example.test",
|
||||
]);
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
$body = strtolower($response->getContent());
|
||||
$this->assertStringNotContainsString('app_key', $body); $this->assertStringNotContainsString('document.cookie', $body); $this->assertStringNotContainsString('jndi:ldap', $body);
|
||||
$this->assertStringNotContainsString('app_key', $body);
|
||||
$this->assertStringNotContainsString('document.cookie', $body);
|
||||
$this->assertStringNotContainsString('jndi:ldap', $body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +28,12 @@ public function test_notification_and_template_routes_reject_template_injection_
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['broadcast', 'notification', 'email', 'whatsapp']), 0, 30) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { continue; }
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
|
||||
continue;
|
||||
}
|
||||
foreach ([$this->parent, $this->teacher, $this->studentUser] as $actor) {
|
||||
$response = $this->requestAs($actor, $method, $route->uri(), ['channel' => 'all', 'recipients' => ['all_school'], 'template_id' => 1, 'force_send' => true]);
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri());
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method.' '.$route->uri());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14PrintExportTamperingContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_print_export_and_document_generation_routes_reject_tampered_scope_fields(): void
|
||||
public function test_print_export_and_document_generation_routes_reject_tampered_scope_fields(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['print', 'export', 'download', 'certificate', 'badge', 'receipt', 'transcript', 'report-card']), 0, 50) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
@@ -15,7 +15,9 @@ public function test_print_export_and_document_generation_routes_reject_tampered
|
||||
]);
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
$body = strtolower($response->getContent());
|
||||
foreach (['app_key', 'laravel.log', 'password', 'remember_token', 'sqlstate'] as $forbidden) { $this->assertStringNotContainsString($forbidden, $body); }
|
||||
foreach (['app_key', 'laravel.log', 'password', 'remember_token', 'sqlstate'] as $forbidden) {
|
||||
$this->assertStringNotContainsString($forbidden, $body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +27,7 @@ public function test_print_export_and_document_generation_routes_reject_tampered
|
||||
$method = $this->primaryMethod($route);
|
||||
foreach ([$this->parent, $this->teacher, $this->studentUser] as $actor) {
|
||||
$response = $this->requestAs($actor, $method, $route->uri(), ['scope' => 'all_school', 'all_students' => true, 'include_finance' => true, 'include_guardians' => true, 'format' => 'csv']);
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri());
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], $method.' '.$route->uri());
|
||||
$this->assertStringNotContainsString('remember_token', strtolower($response->getContent()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14RegressionMegaSweepTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_batch14_mega_sweep_keeps_new_risk_payloads_under_controlled_failure(): void
|
||||
public function test_batch14_mega_sweep_keeps_new_risk_payloads_under_controlled_failure(): void
|
||||
{
|
||||
$families = [
|
||||
['auth', 'login', 'password', 'verify'], ['users', 'roles', 'permissions', 'profile'], ['students', 'parents', 'family', 'guardian'],
|
||||
@@ -26,7 +26,9 @@ public function test_batch14_mega_sweep_keeps_new_risk_payloads_under_controlled
|
||||
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $payload);
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
$body = strtolower($response->getContent());
|
||||
foreach (['sqlstate', 'app_key', 'db_password', 'remember_token', 'stack trace', 'laravel.log'] as $forbidden) { $this->assertStringNotContainsString($forbidden, $body); }
|
||||
foreach (['sqlstate', 'app_key', 'db_password', 'remember_token', 'stack trace', 'laravel.log'] as $forbidden) {
|
||||
$this->assertStringNotContainsString($forbidden, $body);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-4
@@ -6,15 +6,19 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14RelationshipOwnershipMatrixExpansionTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_relationship_owner_override_fields_are_not_trusted_across_route_families(): void
|
||||
public function test_relationship_owner_override_fields_are_not_trusted_across_route_families(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['student', 'parent', 'family', 'invoice', 'attendance', 'score', 'guardian', 'authorized']), 0, 60) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { continue; }
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
|
||||
continue;
|
||||
}
|
||||
foreach ([$this->parent, $this->teacher] as $actor) {
|
||||
$response = $this->requestAs($actor, $method, $route->uri(), $this->payloadFor($method, $route->uri()) + ['student_ids' => [$this->ids['studentId'], 99999999], 'parent_id' => 99999999, 'family_id' => 99999999, 'guardian_id' => 99999999, 'owner_id' => $this->admin->id, 'owner_type' => 'App\\Models\\User', 'created_by' => $this->admin->id, 'scope' => 'global']);
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
$body = strtolower($response->getContent()); $this->assertStringNotContainsString('sqlstate', $body); $this->assertStringNotContainsString('remember_token', $body);
|
||||
$body = strtolower($response->getContent());
|
||||
$this->assertStringNotContainsString('sqlstate', $body);
|
||||
$this->assertStringNotContainsString('remember_token', $body);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +28,7 @@ public function test_relationship_owner_override_fields_are_not_trusted_across_r
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['parent', 'family', 'guardian', 'invoice', 'attendance', 'report-card']), 0, 40) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
$response = $this->requestAs($this->studentUser, $method, $route->uri(), ['student_id' => 99999999, 'parent_id' => 99999999, 'family_id' => 99999999, 'include' => 'guardians,invoices,attendance,notes']);
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri());
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], $method.' '.$route->uri());
|
||||
$this->assertStringNotContainsString('remember_token', strtolower($response->getContent()));
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14SessionCookieCsrfBoundaryContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_api_auth_routes_do_not_depend_on_browser_session_cookies(): void
|
||||
public function test_api_auth_routes_do_not_depend_on_browser_session_cookies(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['auth', 'login', 'logout', 'me', 'profile', 'frontend']), 0, 40) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
@@ -26,7 +26,7 @@ public function test_api_auth_routes_do_not_depend_on_browser_session_cookies():
|
||||
$response = $this->withCookie('laravel_session', 'fake-admin-browser-session')
|
||||
->withHeaders(['X-Requested-With' => 'XMLHttpRequest', 'X-CSRF-TOKEN' => 'fake-csrf', 'X-User-Id' => (string) $this->admin->id])
|
||||
->json($method, $this->materializePath($route->uri()), $this->payloadFor($method, $route->uri()));
|
||||
$this->assertStatusIn($response, [401, 403, 404, 405, 419, 422], $method . ' ' . $route->uri());
|
||||
$this->assertStatusIn($response, [401, 403, 404, 405, 419, 422], $method.' '.$route->uri());
|
||||
$this->assertStringNotContainsString('sqlstate', strtolower($response->getContent()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,16 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch14StoragePathAndSignedUrlContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
public function test_file_and_signed_url_routes_reject_path_and_disk_overrides(): void
|
||||
public function test_file_and_signed_url_routes_reject_path_and_disk_overrides(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['file', 'media', 'upload', 'download', 'export', 'import', 'attachment', 'document', 'signed']), 0, 55) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
$response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + ['path' => '../../.env', 'filename' => '../../storage/logs/laravel.log', 'disk' => 'local', 'storage_disk' => 's3://private-bucket', 'url' => 'file:///etc/passwd', 'expires' => '2099-01-01', 'signature' => 'forged-signature', 'mime_type' => 'text/x-php']);
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
$body = strtolower($response->getContent());
|
||||
foreach (['app_key', 'db_password', 'laravel.log', '/etc/passwd', 'begin rsa private key'] as $forbidden) { $this->assertStringNotContainsString($forbidden, $body); }
|
||||
foreach (['app_key', 'db_password', 'laravel.log', '/etc/passwd', 'begin rsa private key'] as $forbidden) {
|
||||
$this->assertStringNotContainsString($forbidden, $body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +23,8 @@ public function test_file_and_signed_url_routes_reject_path_and_disk_overrides()
|
||||
{
|
||||
foreach (array_slice($this->apiRoutesContainingAny(['download', 'signed', 'public', 'media', 'file']), 0, 35) as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
$response = $this->json($method, $this->materializePath($route->uri()) . '?signature=forged&expires=1&path=../../.env');
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 410, 419, 422], $method . ' ' . $route->uri());
|
||||
$response = $this->json($method, $this->materializePath($route->uri()).'?signature=forged&expires=1&path=../../.env');
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 410, 419, 422], $method.' '.$route->uri());
|
||||
$this->assertStringNotContainsString('app_key', strtolower($response->getContent()));
|
||||
}
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15AdminImpersonationSessionBoundaryContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_impersonation_and_acting_as_routes_require_real_admin_context_not_payload_claims(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['impersonate', 'acting-as', 'act-as', 'switch-user', 'delegate', 'behalf']);
|
||||
@@ -28,5 +27,4 @@ class ApiBatch15AdminImpersonationSessionBoundaryContractTest extends FullSurfac
|
||||
$this->assertStringNotContainsString('access_token', strtolower($parentResponse->getContent()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15AuthTokenRotationAndRevocationContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_token_rotation_revocation_and_identity_routes_do_not_accept_stale_or_forged_tokens(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['auth', 'login', 'logout', 'refresh', 'token', 'me', 'profile', 'session']);
|
||||
@@ -46,5 +45,4 @@ class ApiBatch15AuthTokenRotationAndRevocationContractTest extends FullSurfaceE2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15BackgroundCommandAndSchedulerContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_command_scheduler_and_job_trigger_routes_are_admin_only_and_idempotent(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['command', 'schedule', 'scheduler', 'cron', 'job', 'queue', 'sync', 'recalculate', 'cleanup', 'send']);
|
||||
@@ -30,5 +29,4 @@ class ApiBatch15BackgroundCommandAndSchedulerContractTest extends FullSurfaceE2E
|
||||
$this->assertControlled($adminResponse, $method, $uri);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15ConsentPrivacyAndDataSubjectRightsContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_privacy_consent_erasure_and_export_routes_are_admin_or_owner_scoped(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['privacy', 'consent', 'erase', 'erasure', 'delete-account', 'export-data', 'data-subject', 'retention']);
|
||||
@@ -44,5 +43,4 @@ class ApiBatch15ConsentPrivacyAndDataSubjectRightsContractTest extends FullSurfa
|
||||
$this->assertStringNotContainsString('db_password', strtolower($response->getContent()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15CrossSchoolIdentityCollisionContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_duplicate_external_identifiers_and_cross_school_aliases_fail_cleanly(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['students', 'users', 'parents', 'family', 'guardian', 'enrollment', 'registration']);
|
||||
@@ -31,5 +30,4 @@ class ApiBatch15CrossSchoolIdentityCollisionContractTest extends FullSurfaceE2EC
|
||||
$this->assertStringNotContainsString('duplicate entry', strtolower($response->getContent()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15DataResidencyBackupAndRestoreContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_backup_restore_residency_and_snapshot_routes_do_not_expose_or_mutate_for_low_privilege_users(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['backup', 'restore', 'snapshot', 'residency', 'tenant', 'database', 'dump', 'archive']);
|
||||
@@ -32,5 +31,4 @@ class ApiBatch15DataResidencyBackupAndRestoreContractTest extends FullSurfaceE2E
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15DomainEventOutboxAndNotificationConsistencyContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_event_outbox_and_notification_trigger_routes_reject_forged_event_metadata(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['event', 'outbox', 'notification', 'broadcast', 'message', 'email', 'whatsapp', 'webhook']);
|
||||
@@ -28,5 +27,4 @@ class ApiBatch15DomainEventOutboxAndNotificationConsistencyContractTest extends
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15FeatureFlagExperimentAndRolloutContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_feature_flag_and_rollout_routes_reject_low_privilege_override_payloads(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['feature', 'flag', 'experiment', 'rollout', 'settings', 'configuration']);
|
||||
@@ -28,5 +27,4 @@ class ApiBatch15FeatureFlagExperimentAndRolloutContractTest extends FullSurfaceE
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15FormRequestAuthorizationCoverageContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_mutating_routes_do_not_allow_public_or_low_privilege_form_request_bypass(): void
|
||||
{
|
||||
foreach (array_slice($this->apiRoutes(), 0, 120) as $route) {
|
||||
@@ -29,5 +28,4 @@ class ApiBatch15FormRequestAuthorizationCoverageContractTest extends FullSurface
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15GlobalSearchAndDirectoryEnumerationContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_global_search_directory_and_lookup_routes_resist_enumeration_inputs(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['search', 'lookup', 'directory', 'autocomplete', 'users', 'students', 'parents']);
|
||||
@@ -27,5 +26,4 @@ class ApiBatch15GlobalSearchAndDirectoryEnumerationContractTest extends FullSurf
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15LocalizationContentDirectionAndEncodingContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_rtl_unicode_and_encoding_payloads_remain_controlled_across_text_routes(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['message', 'contact', 'support', 'notification', 'student', 'parent', 'settings', 'certificate']);
|
||||
@@ -31,5 +30,4 @@ class ApiBatch15LocalizationContentDirectionAndEncodingContractTest extends Full
|
||||
$this->assertStringNotContainsString('malformed utf-8', strtolower($response->getContent()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15MetadataFilteringAndSparseFieldsetSecurityContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_sparse_fieldsets_metadata_and_embeds_do_not_reveal_sensitive_associations(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['users', 'students', 'parents', 'family', 'finance', 'attendance', 'reports', 'dashboard']);
|
||||
@@ -31,5 +30,4 @@ class ApiBatch15MetadataFilteringAndSparseFieldsetSecurityContractTest extends F
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15RateLimitAndAbusePatternContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_repeated_sensitive_requests_return_controlled_responses_without_leaking_internals(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['login', 'password', 'verify', 'contact', 'support', 'message', 'scanner', 'badge']);
|
||||
@@ -16,7 +15,7 @@ class ApiBatch15RateLimitAndAbusePatternContractTest extends FullSurfaceE2EContr
|
||||
$uri = $route->uri();
|
||||
|
||||
for ($attempt = 1; $attempt <= 3; $attempt++) {
|
||||
$response = $this->withHeader('X-Forwarded-For', '203.0.113.' . $attempt)
|
||||
$response = $this->withHeader('X-Forwarded-For', '203.0.113.'.$attempt)
|
||||
->requestAs($this->actorFor($uri), $method, $uri, $this->payloadFor($method, $uri) + [
|
||||
'email' => 'rate-limit-'.$attempt.'@example.test',
|
||||
'password' => 'wrong-password',
|
||||
@@ -31,5 +30,4 @@ class ApiBatch15RateLimitAndAbusePatternContractTest extends FullSurfaceE2EContr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15RegressionUltraSweepTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_ultra_sweep_keeps_cross_domain_hostile_payloads_under_controlled_responses(): void
|
||||
{
|
||||
$families = [
|
||||
@@ -43,5 +42,4 @@ class ApiBatch15RegressionUltraSweepTest extends FullSurfaceE2EContractCase
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15ReportDrilldownAndAggregatePrivacyContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_report_drilldown_filters_do_not_allow_scope_escape_or_sensitive_field_inclusion(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['report', 'analytics', 'dashboard', 'summary', 'drilldown', 'export']);
|
||||
@@ -34,5 +33,4 @@ class ApiBatch15ReportDrilldownAndAggregatePrivacyContractTest extends FullSurfa
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15RouteParameterPoisoningExpansionTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_route_parameters_reject_encoded_path_script_and_type_confusion_payloads_cleanly(): void
|
||||
{
|
||||
$poisoned = ['..%2F..%2F.env', '%00', '<svg/onload=alert(1)>', '1 or 1=1', str_repeat('9', 80), '٠١٢٣', 'null', '[]'];
|
||||
@@ -18,7 +17,7 @@ class ApiBatch15RouteParameterPoisoningExpansionTest extends FullSurfaceE2EContr
|
||||
|
||||
$method = $this->primaryMethod($route);
|
||||
foreach (array_slice($poisoned, 0, 4) as $value) {
|
||||
$path = '/' . preg_replace('/\{[^}]+\}/', $value, $route->uri());
|
||||
$path = '/'.preg_replace('/\{[^}]+\}/', $value, $route->uri());
|
||||
$this->actingAs($this->actorFor($route->uri()), 'api');
|
||||
$response = $this->json($method, $path, $this->payloadFor($method, $route->uri()));
|
||||
$this->assertControlled($response, $method, $route->uri());
|
||||
@@ -26,5 +25,4 @@ class ApiBatch15RouteParameterPoisoningExpansionTest extends FullSurfaceE2EContr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiBatch15StudentSafetyIncidentEscalationContractTest extends FullSurfaceE2EContractCase
|
||||
{
|
||||
|
||||
public function test_batch15_safety_incident_escalation_routes_are_not_mutable_by_parents_or_students(): void
|
||||
{
|
||||
$routes = $this->apiRoutesContainingAny(['incident', 'violation', 'safety', 'alert', 'flag', 'dismissal', 'late', 'absence', 'attendance']);
|
||||
@@ -29,5 +28,4 @@ class ApiBatch15StudentSafetyIncidentEscalationContractTest extends FullSurfaceE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class ApiBusinessRuleEdgeCaseContractTest extends FullSurfaceE2EContractCase
|
||||
$response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload);
|
||||
$this->assertNoServerError($response, "$method $uri mismatched academic context");
|
||||
$this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 409, 419, 422], "$method $uri mismatched academic context");
|
||||
$this->assertStringNotContainsStringIgnoringCase('foreign key constraint', $response->getContent(), $uri . ' should validate academic context before DB failure leaks.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('foreign key constraint', $response->getContent(), $uri.' should validate academic context before DB failure leaks.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +51,8 @@ class ApiBusinessRuleEdgeCaseContractTest extends FullSurfaceE2EContractCase
|
||||
|
||||
$response = $this->requestAs($this->admin, $method, $uri, $payload);
|
||||
$this->assertNoServerError($response, "$method $uri amount $amount");
|
||||
$this->assertStringNotContainsStringIgnoringCase('decimal', $response->getContent(), $uri . ' should not leak decimal cast internals.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('SQLSTATE', $response->getContent(), $uri . ' should not leak SQLSTATE for financial edge cases.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('decimal', $response->getContent(), $uri.' should not leak decimal cast internals.');
|
||||
$this->assertStringNotContainsStringIgnoringCase('SQLSTATE', $response->getContent(), $uri.' should not leak SQLSTATE for financial edge cases.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ class ApiCalendarScheduleConflictContractTest extends FullSurfaceE2EContractCase
|
||||
$query = '?from=1900-01-01&to=2100-12-31&scope=all&include_private=true';
|
||||
|
||||
foreach (['parent' => $this->parent, 'teacher' => $this->teacher, 'student' => $this->studentUser] as $label => $actor) {
|
||||
$response = $this->requestAs($actor, 'GET', $uri . $query);
|
||||
$response = $this->requestAs($actor, 'GET', $uri.$query);
|
||||
$this->assertNoServerError($response, "$label GET $uri calendar scope");
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 409, 419, 422], "$label GET $uri calendar scope");
|
||||
$this->assertStringNotContainsStringIgnoringCase('private_note', $response->getContent(), "$uri should not expose private event notes to $label.");
|
||||
|
||||
@@ -18,6 +18,7 @@ class ApiClientCompatibilityVersioningContractTest extends FullSurfaceE2EContrac
|
||||
$uri = $route->uri();
|
||||
if (str_starts_with($uri, 'api/v1/')) {
|
||||
$this->assertTrue(true);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -31,7 +32,7 @@ class ApiClientCompatibilityVersioningContractTest extends FullSurfaceE2EContrac
|
||||
|
||||
$this->assertTrue(
|
||||
$isAllowed,
|
||||
$uri . ' is an unversioned API route. If this is deliberate legacy support, add it to the explicit allow-list.'
|
||||
$uri.' is an unversioned API route. If this is deliberate legacy support, add it to the explicit allow-list.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -45,8 +46,8 @@ class ApiClientCompatibilityVersioningContractTest extends FullSurfaceE2EContrac
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = $name . '|' . $this->primaryMethod($route);
|
||||
$this->assertArrayNotHasKey($key, $seen, 'Duplicate API route name/method pair: ' . $key . ' for ' . $route->uri() . ' and ' . ($seen[$key] ?? 'unknown'));
|
||||
$key = $name.'|'.$this->primaryMethod($route);
|
||||
$this->assertArrayNotHasKey($key, $seen, 'Duplicate API route name/method pair: '.$key.' for '.$route->uri().' and '.($seen[$key] ?? 'unknown'));
|
||||
$seen[$key] = $route->uri();
|
||||
}
|
||||
}
|
||||
@@ -62,10 +63,10 @@ class ApiClientCompatibilityVersioningContractTest extends FullSurfaceE2EContrac
|
||||
$legacyResponse = $this->postJson($legacy, ['email' => 'bad@example.test', 'password' => 'wrong']);
|
||||
$currentResponse = $this->postJson($current, ['email' => 'bad@example.test', 'password' => 'wrong']);
|
||||
|
||||
$this->assertNoServerError($legacyResponse, $legacy . ' legacy auth failure');
|
||||
$this->assertNoServerError($currentResponse, $current . ' current auth failure');
|
||||
$this->assertStatusIn($legacyResponse, [400, 401, 403, 419, 422], $legacy . ' legacy auth failure');
|
||||
$this->assertStatusIn($currentResponse, [400, 401, 403, 419, 422], $current . ' current auth failure');
|
||||
$this->assertNoServerError($legacyResponse, $legacy.' legacy auth failure');
|
||||
$this->assertNoServerError($currentResponse, $current.' current auth failure');
|
||||
$this->assertStatusIn($legacyResponse, [400, 401, 403, 419, 422], $legacy.' legacy auth failure');
|
||||
$this->assertStatusIn($currentResponse, [400, 401, 403, 419, 422], $current.' current auth failure');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class ApiConcurrencyAndStaleWriteContractTest extends FullSurfaceE2EContractCase
|
||||
}
|
||||
|
||||
$payload = $this->payloadFor($method, $route->uri()) + [
|
||||
'client_request_id' => 'e2e-concurrency-' . md5($route->uri()),
|
||||
'client_request_id' => 'e2e-concurrency-'.md5($route->uri()),
|
||||
'updated_at' => '2000-01-01T00:00:00Z',
|
||||
];
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class ApiContractCoverageHeatmapExpansionTest extends FullSurfaceE2EContractCase
|
||||
foreach ($families as $family => $needles) {
|
||||
$matches = array_filter($routes, fn (LaravelRoute $route): bool => $this->uriContainsAny($route->uri(), $needles));
|
||||
|
||||
$this->assertNotEmpty($matches, $family . ' must have route-level E2E contract coverage candidates.');
|
||||
$this->assertNotEmpty($matches, $family.' must have route-level E2E contract coverage candidates.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,11 +53,11 @@ class ApiContractCoverageHeatmapExpansionTest extends FullSurfaceE2EContractCase
|
||||
}
|
||||
|
||||
if (! $this->uriContainsAny($uri, $ownedNeedles)) {
|
||||
$unowned[] = $method . ' ' . $uri;
|
||||
$unowned[] = $method.' '.$uri;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $unowned, 'Mutating API routes must be assigned to a full-surface use-case contract bucket: ' . implode(', ', $unowned));
|
||||
$this->assertSame([], $unowned, 'Mutating API routes must be assigned to a full-surface use-case contract bucket: '.implode(', ', $unowned));
|
||||
}
|
||||
|
||||
/** @param list<string> $needles */
|
||||
|
||||
@@ -20,11 +20,12 @@ class ApiContractSnapshotSeedTest extends FullSurfaceE2EContractCase
|
||||
foreach ($sample['expected_any'] as $key) {
|
||||
if (str_contains($flat, $key)) {
|
||||
$this->assertTrue(true);
|
||||
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
$this->fail($label . ' did not expose any expected contract key: ' . implode(', ', $sample['expected_any']));
|
||||
$this->fail($label.' did not expose any expected contract key: '.implode(', ', $sample['expected_any']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ class ApiCrossVersionRouteCompatibilityMatrixTest extends FullSurfaceE2EContract
|
||||
? $this->postJson($legacy, ['email' => 'bad@example.test', 'password' => 'wrong'])
|
||||
: $this->actingAs($this->admin, 'api')->getJson($legacy);
|
||||
|
||||
$this->assertNoServerError($versionedResponse, 'versioned compatibility ' . $versioned);
|
||||
$this->assertNoServerError($legacyResponse, 'legacy compatibility ' . $legacy);
|
||||
$this->assertNoServerError($versionedResponse, 'versioned compatibility '.$versioned);
|
||||
$this->assertNoServerError($legacyResponse, 'legacy compatibility '.$legacy);
|
||||
$this->assertLessThan(500, $versionedResponse->getStatusCode());
|
||||
$this->assertLessThan(500, $legacyResponse->getStatusCode());
|
||||
}
|
||||
@@ -37,10 +37,10 @@ class ApiCrossVersionRouteCompatibilityMatrixTest extends FullSurfaceE2EContract
|
||||
foreach ($this->apiRoutes() as $route) {
|
||||
$uri = $route->uri();
|
||||
if (! str_starts_with($uri, 'api/v1/') && ! str_contains($uri, 'docs') && ! str_contains($uri, 'health')) {
|
||||
$legacyLike[] = $this->primaryMethod($route) . ' ' . $uri;
|
||||
$legacyLike[] = $this->primaryMethod($route).' '.$uri;
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertLessThan(80, count($legacyLike), 'Unversioned API compatibility routes should remain bounded, not become a second API by accident: ' . implode(', ', $legacyLike));
|
||||
$this->assertLessThan(80, count($legacyLike), 'Unversioned API compatibility routes should remain bounded, not become a second API by accident: '.implode(', ', $legacyLike));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ class ApiCsvExcelFormulaInjectionContractTest extends FullSurfaceE2EContractCase
|
||||
$response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload);
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, 'CSV formula payload at ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'CSV formula payload at '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class ApiDataExportLeastPrivilegeContractTest extends FullSurfaceE2EContractCase
|
||||
}
|
||||
|
||||
$uri = $route->uri();
|
||||
$response = $this->requestAs($actor, $method, $uri . '?all=true&include_sensitive=true&format=csv');
|
||||
$response = $this->requestAs($actor, $method, $uri.'?all=true&include_sensitive=true&format=csv');
|
||||
|
||||
$this->assertNoServerError($response, "$label GET $uri export");
|
||||
$this->assertStatusIn($response, [200, 202, 204, 302, 400, 401, 403, 404, 409, 419, 422], "$label GET $uri export");
|
||||
@@ -52,7 +52,7 @@ class ApiDataExportLeastPrivilegeContractTest extends FullSurfaceE2EContractCase
|
||||
$query = '?parent_id=999999&student_id=999999&class_section_id=999999&scope=all&include=financial,medical,contacts';
|
||||
|
||||
foreach (['parent' => $this->parent, 'teacher' => $this->teacher] as $label => $actor) {
|
||||
$response = $this->requestAs($actor, 'GET', $uri . $query);
|
||||
$response = $this->requestAs($actor, 'GET', $uri.$query);
|
||||
$this->assertNoServerError($response, "$label GET $uri scoped export");
|
||||
$this->assertStatusIn($response, [200, 202, 204, 302, 400, 401, 403, 404, 409, 419, 422], "$label GET $uri scoped export");
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@ class ApiDataMinimizationByRoleContractTest extends FullSurfaceE2EContractCase
|
||||
foreach ([$this->parent, $this->studentUser] as $actor) {
|
||||
$response = $this->requestAs($actor, 'GET', $uri);
|
||||
$this->assertControlled($response, 'GET', $uri);
|
||||
$this->assertNoServerError($response, 'data minimization at GET ' . $uri);
|
||||
$this->assertNoServerError($response, 'data minimization at GET '.$uri);
|
||||
|
||||
if ($response->getStatusCode() >= 200 && $response->getStatusCode() < 300) {
|
||||
foreach ($forbidden as $field) {
|
||||
$this->assertStringNotContainsString('"' . $field . '"', $response->getContent(), 'Low-privilege response leaked ' . $field . ' at GET ' . $uri);
|
||||
$this->assertStringNotContainsString('"'.$field.'"', $response->getContent(), 'Low-privilege response leaked '.$field.' at GET '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class ApiDataShapeBackwardCompatibilityContractTest extends FullSurfaceE2EContra
|
||||
$response = $this->actingAs($this->actorFor($uri), 'api')->getJson($this->materializePath($uri));
|
||||
|
||||
$this->assertControlled($response, 'GET', $uri);
|
||||
$this->assertNoServerError($response, 'collection envelope ' . $uri);
|
||||
$this->assertNoServerError($response, 'collection envelope '.$uri);
|
||||
|
||||
if ($response->getStatusCode() !== 200 || ! $this->isJsonString($response->getContent())) {
|
||||
continue;
|
||||
@@ -31,7 +31,7 @@ class ApiDataShapeBackwardCompatibilityContractTest extends FullSurfaceE2EContra
|
||||
|| array_key_exists('message', $payload)
|
||||
|| array_key_exists('status', $payload)
|
||||
),
|
||||
$uri . ' should keep a predictable collection/read envelope.'
|
||||
$uri.' should keep a predictable collection/read envelope.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ class ApiDataShapeBackwardCompatibilityContractTest extends FullSurfaceE2EContra
|
||||
$response = $this->actingAs($this->actorFor($uri), 'api')->getJson($this->materializePath($uri));
|
||||
|
||||
$this->assertControlled($response, 'GET', $uri);
|
||||
$this->assertNoServerError($response, 'detail envelope ' . $uri);
|
||||
$this->assertNoServerError($response, 'detail envelope '.$uri);
|
||||
|
||||
if ($response->getStatusCode() !== 200 || ! $this->isJsonString($response->getContent())) {
|
||||
continue;
|
||||
@@ -54,7 +54,7 @@ class ApiDataShapeBackwardCompatibilityContractTest extends FullSurfaceE2EContra
|
||||
$payload = $response->json();
|
||||
$this->assertTrue(
|
||||
data_get($payload, 'id') !== null || data_get($payload, 'data.id') !== null || data_get($payload, 'message') !== null || data_get($payload, 'status') !== null,
|
||||
$uri . ' should keep an identifier, data wrapper, or explicit status/message contract.'
|
||||
$uri.' should keep an identifier, data wrapper, or explicit status/message contract.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class ApiDatabaseTransactionRollbackContractTest extends FullSurfaceE2EContractC
|
||||
$beforeStudents = DB::table('students')->count();
|
||||
|
||||
$payload = $this->payloadFor($method, $uri) + [
|
||||
'email' => 'rollback.' . uniqid() . '@example.test',
|
||||
'email' => 'rollback.'.uniqid().'@example.test',
|
||||
'firstname' => 'Rollback',
|
||||
'lastname' => 'Probe',
|
||||
'parent_id' => 999999,
|
||||
|
||||
@@ -16,11 +16,11 @@ class ApiDeepAuthorizationIsolationMatrixTest extends FullSurfaceE2EContractCase
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if (! in_array($status, [401, 403, 404, 405, 419, 422], true)) {
|
||||
$failures[] = "$label allowed cross-portal access with $status: " . $response->getContent();
|
||||
$failures[] = "$label allowed cross-portal access with $status: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Cross-role authorization isolation failed:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Cross-role authorization isolation failed:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
public function test_owner_scoped_parent_routes_do_not_expose_another_family_student(): void
|
||||
@@ -42,11 +42,11 @@ class ApiDeepAuthorizationIsolationMatrixTest extends FullSurfaceE2EContractCase
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if (! in_array($status, [401, 403, 404, 405, 419, 422], true)) {
|
||||
$failures[] = "$method $uri leaked cross-family access with $status: " . $response->getContent();
|
||||
$failures[] = "$method $uri leaked cross-family access with $status: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Parent owner-scope boundaries failed:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Parent owner-scope boundaries failed:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
/** @return array<string, array{0: User, 1: string, 2: string}> */
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Tests\Feature\Api\V1\FullSurface;
|
||||
|
||||
use Illuminate\Routing\Route as LaravelRoute;
|
||||
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
|
||||
|
||||
class ApiDeepDocumentationAndRouteParityContractTest extends FullSurfaceE2EContractCase
|
||||
@@ -22,10 +21,10 @@ class ApiDeepDocumentationAndRouteParityContractTest extends FullSurfaceE2EContr
|
||||
continue;
|
||||
}
|
||||
|
||||
$unnamed[] = $this->primaryMethod($route) . ' ' . $uri;
|
||||
$unnamed[] = $this->primaryMethod($route).' '.$uri;
|
||||
}
|
||||
|
||||
$this->assertSame([], $unnamed, "API routes should be nameable for docs, client generation, and redirects. Unnamed routes:\n" . implode("\n", $unnamed));
|
||||
$this->assertSame([], $unnamed, "API routes should be nameable for docs, client generation, and redirects. Unnamed routes:\n".implode("\n", $unnamed));
|
||||
}
|
||||
|
||||
public function test_openapi_or_docs_catalog_endpoint_is_reachable_and_route_table_remains_documentable(): void
|
||||
@@ -42,11 +41,11 @@ class ApiDeepDocumentationAndRouteParityContractTest extends FullSurfaceE2EContr
|
||||
$response = $this->requestAs($this->admin, $method, $uri);
|
||||
|
||||
if ($response->getStatusCode() >= 500) {
|
||||
$failures[] = "$method $uri docs endpoint crashed: " . $response->getContent();
|
||||
$failures[] = "$method $uri docs endpoint crashed: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Documentation endpoints must not be decorative rubble:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Documentation endpoints must not be decorative rubble:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
public function test_route_use_case_catalog_covers_new_deep_contract_domains(): void
|
||||
|
||||
+6
-5
@@ -20,16 +20,17 @@ class ApiDeepFileImportExportAndPrintableContractTest extends FullSurfaceE2ECont
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if ($status >= 500) {
|
||||
$failures[] = "$method $uri crashed when generating downloadable/printable output: " . $response->getContent();
|
||||
$failures[] = "$method $uri crashed when generating downloadable/printable output: ".$response->getContent();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! in_array($status, [200, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], true)) {
|
||||
$failures[] = "$method $uri returned unexpected $status for downloadable/printable output: " . $response->getContent();
|
||||
$failures[] = "$method $uri returned unexpected $status for downloadable/printable output: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Download/export/print routes must fail cleanly when fixtures are thin:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Download/export/print routes must fail cleanly when fixtures are thin:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
public function test_import_upload_routes_accept_test_files_or_return_validation_without_server_errors(): void
|
||||
@@ -49,11 +50,11 @@ class ApiDeepFileImportExportAndPrintableContractTest extends FullSurfaceE2ECont
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if ($status >= 500) {
|
||||
$failures[] = "$method $uri crashed on upload/import probe: " . $response->getContent();
|
||||
$failures[] = "$method $uri crashed on upload/import probe: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Upload/import routes must validate file input, not panic:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Upload/import routes must validate file input, not panic:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
/** @return list<LaravelRoute> */
|
||||
|
||||
+7
-7
@@ -15,8 +15,8 @@ class ApiDeepIdempotencyAndDuplicateSubmissionContractTest extends FullSurfaceE2
|
||||
$method = $this->primaryMethod($route);
|
||||
$uri = $route->uri();
|
||||
$payload = $this->payloadFor($method, $uri) + [
|
||||
'external_reference' => 'E2E-DUP-' . md5($uri),
|
||||
'idempotency_key' => 'E2E-DUP-' . md5($uri),
|
||||
'external_reference' => 'E2E-DUP-'.md5($uri),
|
||||
'idempotency_key' => 'E2E-DUP-'.md5($uri),
|
||||
];
|
||||
|
||||
$first = $this->requestAs($this->actorFor($uri), $method, $uri, $payload);
|
||||
@@ -26,16 +26,16 @@ class ApiDeepIdempotencyAndDuplicateSubmissionContractTest extends FullSurfaceE2
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if ($status >= 500) {
|
||||
$failures[] = "$method $uri $label duplicate request crashed with $status: " . $response->getContent();
|
||||
$failures[] = "$method $uri $label duplicate request crashed with $status: ".$response->getContent();
|
||||
}
|
||||
|
||||
if (! in_array($status, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], true)) {
|
||||
$failures[] = "$method $uri $label duplicate request returned unexpected $status: " . $response->getContent();
|
||||
$failures[] = "$method $uri $label duplicate request returned unexpected $status: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Duplicate submissions need controlled behavior, because users double-click like it's a sport:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Duplicate submissions need controlled behavior, because users double-click like it's a sport:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
public function test_repeat_delete_or_archive_requests_are_safe_to_retry_or_fail_cleanly(): void
|
||||
@@ -49,12 +49,12 @@ class ApiDeepIdempotencyAndDuplicateSubmissionContractTest extends FullSurfaceE2
|
||||
|
||||
foreach (['first' => $first, 'second' => $second] as $label => $response) {
|
||||
if ($response->getStatusCode() >= 500) {
|
||||
$failures[] = "DELETE $uri $label retry crashed: " . $response->getContent();
|
||||
$failures[] = "DELETE $uri $label retry crashed: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Retrying destructive operations must not crash:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Retrying destructive operations must not crash:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
/** @return list<LaravelRoute> */
|
||||
|
||||
@@ -14,7 +14,7 @@ class ApiDeepPaginationSearchAndFilterContractTest extends FullSurfaceE2EContrac
|
||||
foreach ($this->listRoutes() as $route) {
|
||||
$method = $this->primaryMethod($route);
|
||||
$uri = $route->uri();
|
||||
$path = $this->materializePath($uri) . '?' . http_build_query([
|
||||
$path = $this->materializePath($uri).'?'.http_build_query([
|
||||
'page' => 1,
|
||||
'per_page' => 10,
|
||||
'search' => 'e2e',
|
||||
@@ -30,16 +30,17 @@ class ApiDeepPaginationSearchAndFilterContractTest extends FullSurfaceE2EContrac
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if ($status >= 500) {
|
||||
$failures[] = "$method $uri crashed with list filters: " . $response->getContent();
|
||||
$failures[] = "$method $uri crashed with list filters: ".$response->getContent();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! in_array($status, [200, 204, 302, 400, 401, 403, 404, 405, 409, 422], true)) {
|
||||
$failures[] = "$method $uri returned unexpected $status with list filters: " . $response->getContent();
|
||||
$failures[] = "$method $uri returned unexpected $status with list filters: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "List endpoints must not crash when clients send boring grid params:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "List endpoints must not crash when clients send boring grid params:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
public function test_date_range_filters_fail_cleanly_when_dates_are_invalid(): void
|
||||
@@ -48,7 +49,7 @@ class ApiDeepPaginationSearchAndFilterContractTest extends FullSurfaceE2EContrac
|
||||
|
||||
foreach ($this->dateFilterRoutes() as $route) {
|
||||
$uri = $route->uri();
|
||||
$path = $this->materializePath($uri) . '?' . http_build_query([
|
||||
$path = $this->materializePath($uri).'?'.http_build_query([
|
||||
'from' => 'not-a-date',
|
||||
'to' => 'still-not-a-date',
|
||||
'start_date' => '2025-99-99',
|
||||
@@ -59,11 +60,11 @@ class ApiDeepPaginationSearchAndFilterContractTest extends FullSurfaceE2EContrac
|
||||
$response = $this->json('GET', $path);
|
||||
|
||||
if ($response->getStatusCode() >= 500) {
|
||||
$failures[] = "GET $uri crashed on invalid date filters: " . $response->getContent();
|
||||
$failures[] = "GET $uri crashed on invalid date filters: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Invalid date filters should be validation, not detonation:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Invalid date filters should be validation, not detonation:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
/** @return list<LaravelRoute> */
|
||||
|
||||
@@ -18,16 +18,17 @@ class ApiDeepResponseShapeAndEnvelopeContractTest extends FullSurfaceE2EContract
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if ($status >= 500) {
|
||||
$failures[] = "$method $uri crashed with $status: " . $response->getContent();
|
||||
$failures[] = "$method $uri crashed with $status: ".$response->getContent();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($status >= 200 && $status < 300 && $response->getContent() !== '' && ! $this->isJsonString($response->getContent())) {
|
||||
$failures[] = "$method $uri returned non-json successful API response: " . $response->getContent();
|
||||
$failures[] = "$method $uri returned non-json successful API response: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Successful API responses should be machine-readable JSON:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Successful API responses should be machine-readable JSON:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
public function test_collection_endpoints_expose_list_like_payloads_or_explicit_empty_state(): void
|
||||
@@ -46,7 +47,8 @@ class ApiDeepResponseShapeAndEnvelopeContractTest extends FullSurfaceE2EContract
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if (! in_array($status, [200, 204, 302, 401, 403, 404, 422], true)) {
|
||||
$failures[] = "$method $uri returned unexpected $status for collection contract: " . $response->getContent();
|
||||
$failures[] = "$method $uri returned unexpected $status for collection contract: ".$response->getContent();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -62,12 +64,12 @@ class ApiDeepResponseShapeAndEnvelopeContractTest extends FullSurfaceE2EContract
|
||||
|| array_key_exists('status', $payload));
|
||||
|
||||
if (! $hasCollectionShape) {
|
||||
$failures[] = "$method $uri returned JSON but no recognizable collection/envelope keys: " . $response->getContent();
|
||||
$failures[] = "$method $uri returned JSON but no recognizable collection/envelope keys: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Collection routes need a stable list/envelope shape:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Collection routes need a stable list/envelope shape:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
/** @return list<LaravelRoute> */
|
||||
|
||||
@@ -63,15 +63,16 @@ class ApiDeepStateTransitionInvariantContractTest extends FullSurfaceE2EContract
|
||||
$response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload);
|
||||
|
||||
if ($response->getStatusCode() >= 500) {
|
||||
$failures[] = "$method $uri crashed during $domain transition probe: " . $response->getContent();
|
||||
$failures[] = "$method $uri crashed during $domain transition probe: ".$response->getContent();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! in_array($response->getStatusCode(), $this->controlledStatuses($method), true)) {
|
||||
$failures[] = "$method $uri returned unexpected {$response->getStatusCode()} during $domain transition probe: " . $response->getContent();
|
||||
$failures[] = "$method $uri returned unexpected {$response->getStatusCode()} during $domain transition probe: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, ucfirst($domain) . " state transitions must be controlled:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, ucfirst($domain)." state transitions must be controlled:\n".implode("\n", $failures));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,17 +20,18 @@ class ApiDeepValidationFailureContractTest extends FullSurfaceE2EContractCase
|
||||
$status = $response->getStatusCode();
|
||||
|
||||
if ($status >= 500) {
|
||||
$failures[] = "$method $uri crashed on $label hostile payload with $status: " . $response->getContent();
|
||||
$failures[] = "$method $uri crashed on $label hostile payload with $status: ".$response->getContent();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! in_array($status, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], true)) {
|
||||
$failures[] = "$method $uri returned unexpected $status on $label hostile payload: " . $response->getContent();
|
||||
$failures[] = "$method $uri returned unexpected $status on $label hostile payload: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Hostile payloads must fail cleanly, not leak framework explosions:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Hostile payloads must fail cleanly, not leak framework explosions:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
public function test_required_field_validation_uses_machine_readable_error_shape_when_validation_fails(): void
|
||||
@@ -47,17 +48,18 @@ class ApiDeepValidationFailureContractTest extends FullSurfaceE2EContractCase
|
||||
}
|
||||
|
||||
if (! $this->isJsonString($response->getContent())) {
|
||||
$failures[] = "$method $uri returned 422 but not JSON: " . $response->getContent();
|
||||
$failures[] = "$method $uri returned 422 but not JSON: ".$response->getContent();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$json = $response->json();
|
||||
if (! is_array($json) || (! array_key_exists('errors', $json) && ! array_key_exists('message', $json))) {
|
||||
$failures[] = "$method $uri returned 422 without message/errors: " . $response->getContent();
|
||||
$failures[] = "$method $uri returned 422 without message/errors: ".$response->getContent();
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertSame([], $failures, "Validation errors must be usable by clients, not poetic prose:\n" . implode("\n", $failures));
|
||||
$this->assertSame([], $failures, "Validation errors must be usable by clients, not poetic prose:\n".implode("\n", $failures));
|
||||
}
|
||||
|
||||
/** @return list<LaravelRoute> */
|
||||
|
||||
@@ -10,7 +10,7 @@ class ApiEmptyDatasetAndNullStateContractTest extends FullSurfaceE2EContractCase
|
||||
public function test_list_endpoints_return_stable_empty_shapes_when_filters_match_nothing(): void
|
||||
{
|
||||
foreach ($this->listPaths() as $path) {
|
||||
$response = $this->requestAs($this->actorFor($path), 'GET', $path . '?search=__no_match_contract_probe__&page=1&per_page=5');
|
||||
$response = $this->requestAs($this->actorFor($path), 'GET', $path.'?search=__no_match_contract_probe__&page=1&per_page=5');
|
||||
|
||||
$this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 419, 422], "empty list $path");
|
||||
$this->assertNoServerError($response, "empty list $path");
|
||||
|
||||
@@ -22,7 +22,7 @@ class ApiEnumAndStateMachineContractTest extends FullSurfaceE2EContractCase
|
||||
$response = $this->requestAs($this->actorFor($uri), $method, $uri, $this->payloadFor($method, $uri) + ['status' => $state, 'state' => $state]);
|
||||
|
||||
$this->assertControlled($response, $method, $uri);
|
||||
$this->assertNoServerError($response, 'unknown status ' . $state . ' at ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'unknown status '.$state.' at '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,8 +37,8 @@ class ApiEnumAndStateMachineContractTest extends FullSurfaceE2EContractCase
|
||||
|
||||
$response = $this->requestAs($this->parent, $method, $uri, $this->payloadFor($method, $uri));
|
||||
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], 'low privilege terminal state transition ' . $method . ' ' . $uri);
|
||||
$this->assertNoServerError($response, 'low privilege terminal state transition ' . $method . ' ' . $uri);
|
||||
$this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], 'low privilege terminal state transition '.$method.' '.$uri);
|
||||
$this->assertNoServerError($response, 'low privilege terminal state transition '.$method.' '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user