127 lines
5.3 KiB
PHP
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',
|
|
];
|
|
}
|
|
}
|