Files
alrahma_sunday_school_api/tests/Feature/Api/V1/FullSurface/ApiOwnerBoundaryAndPrivacyExpansionTest.php
2026-06-11 11:46:12 -04:00

127 lines
5.3 KiB
PHP

<?php
namespace Tests\Feature\Api\V1\FullSurface;
use Illuminate\Support\Facades\DB;
use Tests\Feature\Api\V1\FullSurface\Support\FullSurfaceE2EContractCase;
class ApiOwnerBoundaryAndPrivacyExpansionTest extends FullSurfaceE2EContractCase
{
public function test_parent_family_endpoints_do_not_expose_other_parent_records(): void
{
$other = $this->seedOtherFamily();
foreach ($this->otherFamilyPaths($other['student_id'], $other['parent_id']) as $path) {
$response = $this->requestAs($this->parent, 'GET', $path);
$this->assertStatusIn($response, [200, 302, 401, 403, 404, 405, 419, 422], "parent privacy $path");
$this->assertNoServerError($response, "parent privacy $path");
if ($response->getStatusCode() === 200 && $this->isJsonString($response->getContent())) {
$body = json_encode($response->json(), JSON_THROW_ON_ERROR);
$this->assertStringNotContainsString((string) $other['student_id'], $body, "$path exposed another family student id.");
$this->assertStringNotContainsString($other['email'], $body, "$path exposed another family email.");
}
}
}
public function test_teacher_class_endpoints_do_not_expose_unassigned_class_records(): void
{
$other = $this->seedTeacherClassWithStudent(9802);
$unassignedTeacher = $this->createApiUserWithRole('teacher');
foreach ($this->teacherClassPaths((int) $other['class_section_id'], (int) $other['student_id']) as $path) {
$response = $this->requestAs($unassignedTeacher, 'GET', $path);
$this->assertStatusIn($response, [200, 302, 401, 403, 404, 405, 419, 422], "teacher class isolation $path");
$this->assertNoServerError($response, "teacher class isolation $path");
if ($response->getStatusCode() === 200 && $this->isJsonString($response->getContent())) {
$body = json_encode($response->json(), JSON_THROW_ON_ERROR);
$this->assertStringNotContainsString((string) $other['student_id'], $body, "$path exposed unassigned student id.");
}
}
}
public function test_serialized_users_do_not_leak_sensitive_auth_material(): void
{
foreach ($this->identityAndDirectoryPaths() as $path) {
$response = $this->requestAs($this->admin, 'GET', $path);
$this->assertStatusIn($response, [200, 204, 302, 401, 403, 404, 405, 419, 422], "sensitive serialization $path");
$this->assertNoServerError($response, "sensitive serialization $path");
if ($response->getStatusCode() === 200) {
$content = strtolower($response->getContent());
$this->assertStringNotContainsString('password_hash', $content, "$path leaked password_hash.");
$this->assertStringNotContainsString('remember_token', $content, "$path leaked remember_token.");
$this->assertStringNotContainsString('api_token', $content, "$path leaked api_token.");
$this->assertStringNotContainsString('two_factor_secret', $content, "$path leaked two_factor_secret.");
}
}
}
/** @return array{parent_id:int, student_id:int, email:string} */
private function seedOtherFamily(): array
{
$otherParent = $this->createApiUserWithRole('parent', [
'email' => 'other.family.'.uniqid().'@example.test',
'firstname' => 'Other',
'lastname' => 'Family',
]);
$studentId = DB::table('students')->insertGetId([
'school_id' => 'S-OTHER-'.random_int(1000, 9999),
'firstname' => 'Other',
'lastname' => 'Student',
'dob' => '2016-01-01',
'age' => 9,
'gender' => 'Female',
'parent_id' => $otherParent->id,
'year_of_registration' => '2025',
'school_year' => self::E2E_SCHOOL_YEAR,
'semester' => self::E2E_SEMESTER,
'is_active' => 1,
]);
return ['parent_id' => $otherParent->id, 'student_id' => $studentId, 'email' => $otherParent->email];
}
/** @return list<string> */
private function otherFamilyPaths(int $studentId, int $parentId): array
{
return [
"api/v1/parents/students/$studentId",
"api/v1/parents/students/$studentId/attendance",
"api/v1/parents/students/$studentId/invoices",
"api/v1/parents/$parentId/profile",
"api/v1/families/$parentId",
"api/v1/emergency-contacts/student/$studentId",
];
}
/** @return list<string> */
private function teacherClassPaths(int $classSectionId, int $studentId): array
{
return [
"api/v1/teacher/classes/$classSectionId/roster",
"api/v1/teacher/classes/$classSectionId/progress",
"api/v1/teacher/students/$studentId/scores",
"api/v1/teacher/classes/$classSectionId/attendance",
"api/v1/class-progress/$classSectionId",
];
}
/** @return list<string> */
private function identityAndDirectoryPaths(): array
{
return [
'api/v1/auth/me',
'api/v1/users',
'api/v1/parents/profile',
'api/v1/teachers/classes',
'api/v1/students',
];
}
}