create(); $lab = Lab::factory()->create(['identifiant' => '08-deux-lan-routeur']); $seance = Seance::create([ 'classe_id' => $classe->id, 'lab_id' => $lab->id, 'statut' => Seance::SALLE_ATTENTE, 'demarree_a' => now(), 'mode_examen' => true, 'duree_examen_min' => $dureeExamenMin, ]); $eleve = Eleve::factory()->create(['classe_id' => $classe->id]); return LabSession::create(['seance_id' => $seance->id, 'eleve_id' => $eleve->id, 'lab_id' => $lab->id, 'etat' => LabSession::EN_ATTENTE]); } test("admettre un élève en mode examen utilise la durée de l'examen, pas celle du lab", function () { Queue::fake(); $this->mock(StockChaud::class, function ($stock) { $stock->shouldReceive('prendre')->once()->andReturn(null); }); $session = labSessionEnAttenteExamen(dureeExamenMin: 90); admettre($this, $session)->assertRedirect(); $session->refresh(); expect(now()->diffInMinutes($session->expire_a))->toBeGreaterThan(85) ->and(now()->diffInMinutes($session->expire_a))->toBeLessThanOrEqual(90); }); test("un élève signale un événement d'examen sur sa propre session", function () { $session = labSessionEnAttenteExamen(); $session->update(['etat' => LabSession::ACTIF]); eleve($this, $session->eleve) ->postJson(route('lab-sessions.evenements-examen', $session), ['type' => 'changement_onglet']) ->assertCreated(); expect($session->evenementsExamen()->count())->toBe(1) ->and($session->evenementsExamen()->first()->type)->toBe(EvenementExamen::CHANGEMENT_ONGLET); }); test("un élève ne peut pas signaler un événement sur la session d'un autre élève", function () { $session = labSessionEnAttenteExamen(); $intrus = Eleve::factory()->create(); eleve($this, $intrus) ->postJson(route('lab-sessions.evenements-examen', $session), ['type' => 'changement_onglet']) ->assertForbidden(); expect($session->evenementsExamen()->count())->toBe(0); }); test("pas d'événement d'examen possible sur une séance normale (mode examen désactivé)", function () { $session = sessionEnAttente(); $session->update(['etat' => LabSession::ACTIF]); eleve($this, $session->eleve) ->postJson(route('lab-sessions.evenements-examen', $session), ['type' => 'changement_onglet']) ->assertForbidden(); }); test('type invalide refusé', function () { $session = labSessionEnAttenteExamen(); $session->update(['etat' => LabSession::ACTIF]); eleve($this, $session->eleve) ->postJson(route('lab-sessions.evenements-examen', $session), ['type' => 'webcam_allumee']) ->assertUnprocessable(); }); test('purger une classe archivée supprime aussi les événements d’examen', function () { $session = labSessionEnAttenteExamen(); $session->update(['etat' => LabSession::TERMINE]); $session->evenementsExamen()->create(['type' => EvenementExamen::SORTIE_PLEIN_ECRAN]); $classe = $session->seance->classe; $classe->forceFill(['archivee_a' => now()->subDays(91)])->save(); $this->artisan('classes:purger-archivees'); expect(EvenementExamen::count())->toBe(0); }); test('les scripts en ligne du mode examen (barre prof et page élève) sont du JavaScript valide', function () { if (Process::run('node --version')->failed()) { $this->markTestSkipped('Node absent'); } $session = labSessionEnAttenteExamen(); $session->update(['etat' => LabSession::ACTIF]); $session->machines()->create(['nom' => 'pc-a', 'instance_id' => 'uuid-a']); $pages = [ fn () => prof($this, $session->seance->classe->prof)->get(route('mes-classes.show', $session->seance->classe)), fn () => eleve($this, $session->eleve)->get(route('seances.eleve.attente', $session->seance)), ]; $erreurs = []; foreach ($pages as $i => $visite) { preg_match_all('#]*\bsrc=)[^>]*>(.*?)#s', $visite()->getContent(), $scripts); foreach ($scripts[1] as $j => $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[] = "page {$i} script {$j} : ".strtok($resultat->errorOutput(), "\n"); } } } expect($erreurs)->toBe([]); });