setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); if ($pdo instanceof \Pdo\Sqlite) $pdo->createFunction('NOW', static fn() => gmdate('Y-m-d H:i:s'), 0); else $pdo->sqliteCreateFunction('NOW', static fn() => gmdate('Y-m-d H:i:s'), 0); (new ReflectionProperty(Database::class, 'instance'))->setValue(null, $pdo); $pdo->exec('CREATE TABLE directorates (id INTEGER PRIMARY KEY, type TEXT)'); $pdo->exec('CREATE TABLE schools (id INTEGER PRIMARY KEY, name TEXT, type TEXT, directorate_id INTEGER)'); $pdo->exec('CREATE TABLE students (id INTEGER PRIMARY KEY, national_id TEXT, grade_level TEXT, school_id INTEGER, is_school_sponsored INTEGER)'); $pdo->exec('CREATE TABLE teachers (id INTEGER PRIMARY KEY, is_school_exclusive INTEGER, is_marketplace_public INTEGER)'); $pdo->exec('CREATE TABLE courses (id INTEGER PRIMARY KEY, teacher_id INTEGER, school_id INTEGER, is_school_exclusive INTEGER, is_published INTEGER, price_jod NUMERIC, grade_level TEXT)'); $pdo->exec('CREATE TABLE lessons (id INTEGER PRIMARY KEY, course_id INTEGER, is_free_preview INTEGER)'); $pdo->exec('CREATE TABLE school_rosters (school_id INTEGER, claimed_student_id INTEGER, is_claimed INTEGER, grade_level TEXT)'); $pdo->exec('CREATE TABLE course_access_passes (id INTEGER PRIMARY KEY, student_id INTEGER, course_id INTEGER, pass_type TEXT, expires_at TEXT, is_active INTEGER)'); $pdo->exec("INSERT INTO schools VALUES (1,'School A','private',NULL),(2,'School B','private',NULL)"); $pdo->exec("INSERT INTO students VALUES (1,'a','grade_10',NULL,0),(2,'b','grade_10',1,1),(3,'c','grade_10',2,1),(4,'d','grade_9',NULL,0),(5,'e','grade_10',1,1)"); $pdo->exec('INSERT INTO teachers VALUES (1,0,1),(2,1,0)'); $pdo->exec("INSERT INTO courses VALUES (10,1,NULL,0,1,10,'grade_10'),(11,2,1,1,1,30,'grade_10'),(12,2,2,1,1,30,'grade_10'),(13,1,NULL,0,1,0,'grade_10'),(14,1,NULL,0,0,0,'grade_10')"); $pdo->exec('INSERT INTO lessons VALUES (100,10,0),(110,11,0),(120,12,0),(130,13,0),(140,14,0)'); $pdo->exec("INSERT INTO school_rosters VALUES (1,2,1,'grade_10'),(2,3,1,'grade_10')"); $pdo->exec("INSERT INTO course_access_passes VALUES (1,1,10,'full_marketplace',NULL,1),(2,2,10,'school_included',NULL,1)"); $checks = 0; function expectAccess(int $studentId, int $courseId, int $lessonId, bool $allowed, string $reason, bool $legacySideEffects = false): void { global $checks; $result = StudentAccessControlService::validateLessonAccess($studentId, null, 'grade_10', $courseId, $lessonId, $legacySideEffects); if ((bool)$result['allowed'] !== $allowed || (string)$result['reason'] !== $reason) { throw new RuntimeException("Unexpected access for student {$studentId}, course {$courseId}: " . json_encode($result)); } $checks++; } expectAccess(1, 10, 100, true, 'paid_pass_active'); expectAccess(1, 11, 110, false, 'school_scope_mismatch'); expectAccess(2, 11, 110, true, 'school_course_included'); expectAccess(2, 12, 120, false, 'school_scope_mismatch'); expectAccess(2, 10, 100, false, 'payment_required'); expectAccess(2, 13, 130, true, 'free_course'); expectAccess(5, 11, 110, false, 'school_roster_unverified'); expectAccess(4, 13, 130, false, 'grade_mismatch', true); expectAccess(1, 14, 140, false, 'course_unavailable'); expectAccess(1, 10, 110, false, 'lesson_course_mismatch'); expectAccess(99, 13, 130, false, 'unauthenticated_or_not_found'); $grade = $pdo->query('SELECT grade_level FROM students WHERE id=4')->fetchColumn(); $passes = (int)$pdo->query('SELECT COUNT(*) FROM course_access_passes')->fetchColumn(); if ($grade !== 'grade_9' || $passes !== 2) throw new RuntimeException('Access reads changed grade or issued passes'); $checks++; echo "PASS {$checks} local student-access scope checks (not an HTTP/MySQL test)\n";