Dossier personnel de l'élève monté comme lecteur réseau : rclone sert le dossier local, l'authentification est déléguée au portail via auth_request Nginx (mêmes identifiants que la connexion), sans nouveau système de comptes. Nettoyé par la purge RGPD comme les autres données d'élève. Corrige au passage un vrai bug d'isolation trouvé en testant avec deux comptes jetables : rewrite injectait la variable posée par auth_request_set, mais rewrite s'exécute dans une phase Nginx antérieure à cette variable, donc toujours vide - un élève pouvait lire le fichier d'un autre en devinant son nom. Corrigé par une location à regex nommée + proxy_pass. Détails : ADR 0008. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
78 lines
3.9 KiB
PHP
78 lines
3.9 KiB
PHP
<?php
|
||
|
||
use Illuminate\Support\Facades\Process;
|
||
use Illuminate\Support\Facades\Route;
|
||
|
||
// Toutes les pages publiques sans paramètre doivent s'afficher : la page
|
||
// /cours a été en erreur 500 de l'étape 8 à l'étape 12 sans que personne
|
||
// ne le voie.
|
||
test('chaque page publique s’affiche sans erreur', function () {
|
||
$pages = collect(Route::getRoutes())
|
||
->filter(fn ($r) => in_array('GET', $r->methods(), true)
|
||
&& $r->getName() !== null
|
||
&& ! str_contains($r->uri(), '{')
|
||
&& ! collect($r->gatherMiddleware())->contains(fn ($m) => is_string($m) && str_starts_with($m, 'auth')))
|
||
->reject(fn ($r) => str_starts_with($r->uri(), 'horizon') || str_starts_with($r->uri(), '_') || str_starts_with($r->uri(), 'interne/') || $r->uri() === 'up')
|
||
// Redirections voulues (ex. /statut vers le site de statut ; pages de
|
||
// suivi d'une demande pour rejoindre une classe, sans demande en cours).
|
||
->reject(fn ($r) => $r->getActionName() === '\\Illuminate\\Routing\\RedirectController'
|
||
|| in_array($r->getName(), ['rejoindre.attente', 'rejoindre.bienvenue'], true));
|
||
|
||
expect($pages)->not->toBeEmpty();
|
||
|
||
$echecs = $pages->mapWithKeys(fn ($r) => [$r->uri() => $this->get('/'.ltrim($r->uri(), '/'))->getStatusCode()])
|
||
->reject(fn ($code) => $code === 200);
|
||
|
||
expect($echecs->all())->toBe([]);
|
||
});
|
||
|
||
// Étape 14 : la politique de confidentialité affirme qu'aucune page ne fait
|
||
// partir l'adresse IP du visiteur chez un fournisseur de polices.
|
||
test('aucune page publique ne charge de police depuis un service externe', function () {
|
||
$pages = collect(Route::getRoutes())
|
||
->filter(fn ($r) => in_array('GET', $r->methods(), true) && $r->getName() !== null && ! str_contains($r->uri(), '{'))
|
||
->reject(fn ($r) => collect($r->gatherMiddleware())->contains(fn ($m) => is_string($m) && str_starts_with($m, 'auth')))
|
||
->reject(fn ($r) => str_starts_with($r->uri(), 'interne/'));
|
||
|
||
foreach ($pages as $route) {
|
||
$html = $this->get('/'.ltrim($route->uri(), '/'))->getContent();
|
||
expect($html)->not->toMatch('#fonts\.(googleapis|gstatic|bunny)\.(com|net)#', "page /{$route->uri()}");
|
||
}
|
||
});
|
||
|
||
test('aucune feuille de style publique n’importe de ressource externe', function () {
|
||
foreach (glob(public_path('css/*.css')) as $fichier) {
|
||
expect(file_get_contents($fichier))->not->toMatch('#@import\s+url\(\s*["\']?https?://#', basename($fichier));
|
||
}
|
||
});
|
||
|
||
// /ide a eu une erreur de syntaxe JavaScript de l'étape 8 à l'étape 14 (des
|
||
// « !! » devenus « . » dans la maquette) : chaque script en ligne de chaque
|
||
// page publique est maintenant analysé par Node.
|
||
test('les scripts en ligne des pages publiques sont du JavaScript valide', function () {
|
||
if (Process::run('node --version')->failed()) {
|
||
$this->markTestSkipped('Node absent');
|
||
}
|
||
|
||
$pages = collect(Route::getRoutes())
|
||
->filter(fn ($r) => in_array('GET', $r->methods(), true) && $r->getName() !== null && ! str_contains($r->uri(), '{'))
|
||
->reject(fn ($r) => collect($r->gatherMiddleware())->contains(fn ($m) => is_string($m) && str_starts_with($m, 'auth')))
|
||
->reject(fn ($r) => $r->getActionName() === '\Illuminate\Routing\RedirectController')
|
||
->reject(fn ($r) => str_starts_with($r->uri(), 'interne/'));
|
||
|
||
$erreurs = [];
|
||
foreach ($pages as $route) {
|
||
preg_match_all('#<script(?![^>]*\bsrc=)[^>]*>(.*?)</script>#s', $this->get('/'.ltrim($route->uri(), '/'))->getContent(), $scripts);
|
||
foreach ($scripts[1] as $i => $code) {
|
||
$fichier = tempnam(sys_get_temp_dir(), 'js').'.js';
|
||
file_put_contents($fichier, $code);
|
||
$resultat = Process::run(['node', '--check', $fichier]);
|
||
unlink($fichier);
|
||
if ($resultat->failed()) {
|
||
$erreurs[] = "/{$route->uri()} script {$i} : ".strtok($resultat->errorOutput(), "\n");
|
||
}
|
||
}
|
||
}
|
||
|
||
expect($erreurs)->toBe([]);
|
||
});
|