Files
saqel/backend/scripts/test_student_access_scope_local.php
T

62 lines
4.1 KiB
PHP

<?php
declare(strict_types=1);
// SQLite memory fixture: no production DB, no network, no money or student writes.
// MySQL/HTTP concurrency and real-school roster verification remain pending.
require_once dirname(__DIR__) . '/app/Core/Database.php';
require_once dirname(__DIR__) . '/app/Services/StudentAccessControlService.php';
use App\Core\Database;
use App\Services\StudentAccessControlService;
$pdo = class_exists('Pdo\\Sqlite') ? new \Pdo\Sqlite('sqlite::memory:') : new PDO('sqlite::memory:');
$pdo->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";