ajoute les pages légales, la purge RGPD et sert polices et scripts depuis le portail
Pages /legal et /confidentialite réécrites d'après le système réel, purge des classes archivées depuis 90 jours (prouvée par Pest), archivage côté prof. Polices, xterm.js et CodeMirror ne sont plus chargés depuis Google ni jsDelivr. Pages d'aperçu signalées, sans vrais établissements ni faux chiffres. Journaux Laravel gardés 14 jours. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0a5df89201
commit
e4a6aaebb1
62 changed files with 822 additions and 238 deletions
36
portail/tests/Feature/PagesLegalesTest.php
Normal file
36
portail/tests/Feature/PagesLegalesTest.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
test('les pages légales sont en ligne et la page de statut redirige vers le site de statut', function () {
|
||||
$this->get(route('legal'))->assertOk()->assertSee('Esquisse Formation')->assertSee('RCS Paris 433 115 904');
|
||||
$this->get(route('confidentialite'))->assertOk()
|
||||
->assertSee('90 jours après l’archivage', false)
|
||||
->assertSee('aucune adresse e-mail', false)
|
||||
->assertSee('cdn.tailwindcss.com');
|
||||
$this->get(route('statut'))->assertRedirect('https://status.syllabuslab.fr');
|
||||
});
|
||||
|
||||
test('« Conforme au RGPD » renvoie à la politique de confidentialité, et rien n’affiche « Hébergé en France · Paris »', function () {
|
||||
$accueil = $this->get(route('accueil'))->getContent();
|
||||
|
||||
expect($accueil)->toContain('href="'.route('confidentialite').'" style="color:#0b1220">Conforme au RGPD</a>')
|
||||
->and($accueil)->toContain(route('statut'))
|
||||
->and($accueil)->not->toContain('· Paris</span>')
|
||||
->and($accueil)->not->toContain('20 cursus');
|
||||
});
|
||||
|
||||
test('les pages d’aperçu sont signalées et ne citent aucun vrai établissement comme utilisateur', function () {
|
||||
// Noms cherchés un par un : dans les tableaux, nom et ville sont dans
|
||||
// deux cellules différentes (un premier test qui les cherchait collés
|
||||
// laissait passer « Lycée Vauban | Brest »).
|
||||
$vraisNoms = '/Vauban|Condorcet|Saint-Exupéry|Louis-Armand|Jean-Jaurès|Villetaneuse|Gustave-Eiffel|Martinière|Académie de (Créteil|Paris)/';
|
||||
$apercus = ['/classes/suivi', '/examen', '/banque', '/quiz/projection', '/quiz', '/bibliotheque', '/generateur', '/labs/web',
|
||||
'/maison', '/devoirs', '/td/imprimer', '/cockpit', '/mon-espace', '/vie-scolaire', '/eleves/fiche', '/correction',
|
||||
'/sujets/creer', '/messagerie', '/exports', '/academie', '/paiement', '/paiement/confirme', '/compte/facturation',
|
||||
'/etablissement', '/cohortes', '/visio', '/ressources/outil', '/ide', '/labs/essai', '/cours/adressage-ipv4',
|
||||
'/cours/adressage-ipv4/cm', '/cours/fiche'];
|
||||
foreach ($apercus as $route) {
|
||||
$page = $this->get($route)->assertOk()->getContent();
|
||||
expect($page)->toContain('Aperçu d’une fonction en préparation')
|
||||
->and($page)->not->toMatch($vraisNoms, $route);
|
||||
}
|
||||
});
|
||||
|
|
@ -11,7 +11,9 @@ test('chaque page publique s’affiche sans erreur', function () {
|
|||
&& $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(), '_') || $r->uri() === 'up');
|
||||
->reject(fn ($r) => str_starts_with($r->uri(), 'horizon') || str_starts_with($r->uri(), '_') || $r->uri() === 'up')
|
||||
// Redirections voulues (ex. /statut vers le site de statut).
|
||||
->reject(fn ($r) => $r->getActionName() === '\\Illuminate\\Routing\\RedirectController');
|
||||
|
||||
expect($pages)->not->toBeEmpty();
|
||||
|
||||
|
|
@ -20,3 +22,16 @@ test('chaque page publique s’affiche sans erreur', function () {
|
|||
|
||||
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')));
|
||||
|
||||
foreach ($pages as $route) {
|
||||
$html = $this->get('/'.ltrim($route->uri(), '/'))->getContent();
|
||||
expect($html)->not->toMatch('#fonts\.(googleapis|gstatic|bunny)\.(com|net)#', "page /{$route->uri()}");
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -46,3 +46,14 @@ test('le rapport de violation est accepté sans jeton CSRF', function () {
|
|||
json_encode(['csp-report' => ['document-uri' => 'https://syllabuslab.fr/eleve?x=1', 'violated-directive' => 'script-src', 'blocked-uri' => 'inline']]))
|
||||
->assertNoContent();
|
||||
});
|
||||
|
||||
test("les pages de l'application ne chargent aucune ressource externe", function () {
|
||||
$eleve = Eleve::factory()->create();
|
||||
$jeton = $eleve->demarreNouvelleSession();
|
||||
|
||||
foreach ([route('login'), route('register')] as $url) {
|
||||
expect($this->get($url)->getContent())->not->toMatch('#(src|href)="https?://(?!localhost)#', $url);
|
||||
}
|
||||
$page = $this->withSession(['jeton_session_eleve' => $jeton])->actingAs($eleve, 'eleve')->get(route('eleve'))->getContent();
|
||||
expect($page)->not->toMatch('#(src|href)="https?://(?!localhost)[^"]*\.(js|css|woff2?)#');
|
||||
});
|
||||
|
|
|
|||
99
portail/tests/Feature/PurgeRgpdTest.php
Normal file
99
portail/tests/Feature/PurgeRgpdTest.php
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Classe;
|
||||
use App\Models\Correction;
|
||||
use App\Models\Eleve;
|
||||
use App\Models\Lab;
|
||||
use App\Models\LabSession;
|
||||
use App\Models\LabSessionMachine;
|
||||
use App\Models\Seance;
|
||||
use App\Models\User;
|
||||
|
||||
/**
|
||||
* Une classe complète : 2 élèves, une séance, une session de lab terminée
|
||||
* avec une machine et une correction.
|
||||
*/
|
||||
function classeComplete(?int $archiveeIlYaJours): Classe
|
||||
{
|
||||
$classe = Classe::factory()->create();
|
||||
if ($archiveeIlYaJours !== null) {
|
||||
$classe->forceFill(['archivee_a' => now()->subDays($archiveeIlYaJours)])->save();
|
||||
}
|
||||
$eleves = Eleve::factory()->count(2)->create(['classe_id' => $classe->id]);
|
||||
$lab = Lab::factory()->create();
|
||||
$seance = Seance::create(['classe_id' => $classe->id, 'lab_id' => $lab->id, 'statut' => Seance::TERMINEE]);
|
||||
$session = LabSession::create(['seance_id' => $seance->id, 'eleve_id' => $eleves[0]->id, 'lab_id' => $lab->id, 'etat' => LabSession::EXPIRE]);
|
||||
$session->machines()->create(['nom' => 'pc-a', 'instance_id' => 'uuid-a']);
|
||||
$session->corrections()->create(['reussi' => true, 'note' => 10, 'note_max' => 10, 'details' => []]);
|
||||
|
||||
return $classe;
|
||||
}
|
||||
|
||||
test('la purge supprime une classe archivée depuis plus de 90 jours et toutes les données de ses élèves', function () {
|
||||
$classe = classeComplete(91);
|
||||
$prof = $classe->prof;
|
||||
|
||||
$this->artisan('classes:purger-archivees')->expectsOutput('1 classe(s) purgée(s).')->assertSuccessful();
|
||||
|
||||
expect(Classe::find($classe->id))->toBeNull()
|
||||
->and(Eleve::where('classe_id', $classe->id)->count())->toBe(0)
|
||||
->and(Seance::where('classe_id', $classe->id)->count())->toBe(0)
|
||||
->and(LabSession::count())->toBe(0)
|
||||
->and(LabSessionMachine::count())->toBe(0)
|
||||
->and(Correction::count())->toBe(0)
|
||||
// Le compte du prof n'est pas une donnée d'élève : il reste.
|
||||
->and(User::find($prof->id))->not->toBeNull();
|
||||
});
|
||||
|
||||
test('la purge épargne les classes archivées depuis moins de 90 jours et les classes en cours', function () {
|
||||
$recente = classeComplete(89);
|
||||
$enCours = classeComplete(null);
|
||||
|
||||
$this->artisan('classes:purger-archivees')->expectsOutput('0 classe(s) purgée(s).');
|
||||
|
||||
expect(Classe::find($recente->id))->not->toBeNull()
|
||||
->and(Classe::find($enCours->id))->not->toBeNull()
|
||||
->and(Eleve::count())->toBe(4)
|
||||
->and(Correction::count())->toBe(2);
|
||||
});
|
||||
|
||||
test('la purge ne touche qu’aux classes concernées', function () {
|
||||
$aPurger = classeComplete(120);
|
||||
$aGarder = classeComplete(null);
|
||||
|
||||
$this->artisan('classes:purger-archivees');
|
||||
|
||||
expect(Classe::pluck('id')->all())->toBe([$aGarder->id])
|
||||
->and(Eleve::pluck('classe_id')->unique()->all())->toBe([$aGarder->id]);
|
||||
});
|
||||
|
||||
test('un prof archive sa classe : plus de séance possible, date de suppression annoncée', function () {
|
||||
$classe = classeComplete(null);
|
||||
$prof = $classe->prof;
|
||||
$jeton = $prof->demarreNouvelleSession();
|
||||
$connecte = fn () => $this->withSession(['jeton_session_web' => $jeton])->actingAs($prof, 'web');
|
||||
|
||||
$connecte()->post(route('mes-classes.archiver', $classe))->assertRedirect(route('mes-classes.show', $classe));
|
||||
|
||||
$classe->refresh();
|
||||
expect($classe->estArchivee())->toBeTrue()
|
||||
->and($classe->supprimeeLe()->toDateString())->toBe(now()->addDays(90)->toDateString());
|
||||
|
||||
$connecte()->get(route('mes-classes.show', $classe))
|
||||
->assertSee('Classe archivée')
|
||||
->assertSee($classe->supprimeeLe()->timezone(config('app.fuseau_utilisateurs'))->format('d/m/Y'));
|
||||
|
||||
$connecte()->post(route('mes-classes.seances.store', $classe), ['lab_id' => Lab::first()->id])->assertSessionHasErrors('prevue_a');
|
||||
expect(Seance::where('classe_id', $classe->id)->where('statut', '!=', Seance::TERMINEE)->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('un autre prof ne peut pas archiver la classe', function () {
|
||||
$classe = classeComplete(null);
|
||||
$intrus = User::factory()->create();
|
||||
$jeton = $intrus->demarreNouvelleSession();
|
||||
|
||||
$this->withSession(['jeton_session_web' => $jeton])->actingAs($intrus, 'web')
|
||||
->post(route('mes-classes.archiver', $classe))->assertForbidden();
|
||||
|
||||
expect($classe->refresh()->estArchivee())->toBeFalse();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue