Paillasse/portail/tests/Feature/LancementLabTest.php

146 lines
6.1 KiB
PHP
Raw Normal View History

<?php
use App\Jobs\CreerStackLab;
use App\Models\Classe;
use App\Models\Eleve;
use App\Models\Lab;
use App\Models\LabSession;
use App\Models\Seance;
use App\Services\Labs\ClientCorrecteur;
use App\Services\Labs\CorrigerSession;
use App\Services\Labs\StockChaud;
use App\Services\OpenStack\FabriqueStack;
use Illuminate\Support\Facades\Queue;
function sessionEnAttente(string $etat = LabSession::EN_ATTENTE): LabSession
{
$classe = Classe::factory()->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()]);
$eleve = Eleve::factory()->create(['classe_id' => $classe->id]);
return LabSession::create(['seance_id' => $seance->id, 'eleve_id' => $eleve->id, 'lab_id' => $lab->id, 'etat' => $etat]);
}
function admettre($test, LabSession $session)
{
$prof = $session->seance->classe->prof;
$jeton = $prof->demarreNouvelleSession();
return $test->withSession(['jeton_session_web' => $jeton])
->actingAs($prof, 'web')
->post(route('seances.admettre', [$session->seance, $session]));
}
$stackPrete = [
'nom' => 'stock-08-deux-lan-routeur-abc123-e1',
'id' => 'id-heat',
'instances' => ['pc-a' => 'uuid-a', 'routeur' => 'uuid-r', 'pc-b' => 'uuid-b'],
'brut' => '{}',
];
test('une stack du stock chaud est attribuée tout de suite, sans job', function () use ($stackPrete) {
Queue::fake();
$this->mock(StockChaud::class, function ($stock) use ($stackPrete) {
$stock->shouldReceive('prendre')->once()->with('08-deux-lan-routeur')->andReturn($stackPrete);
$stock->shouldReceive('confirmerAttribution')->once();
});
$session = sessionEnAttente();
admettre($this, $session)->assertRedirect();
$session->refresh();
expect($session->etat)->toBe(LabSession::ACTIF)
->and($session->heat_stack_name)->toBe($stackPrete['nom'])
->and($session->machines()->pluck('instance_id', 'nom')->all())->toBe($stackPrete['instances']);
Queue::assertNothingPushed();
});
test('stock vide : la stack est construite par CreerStackLab, une seule fois malgré un double clic', function () {
Queue::fake();
$this->mock(StockChaud::class, fn ($stock) => $stock->shouldReceive('prendre')->once()->andReturn(null));
$session = sessionEnAttente();
admettre($this, $session);
admettre($this, $session);
expect($session->refresh()->etat)->toBe(LabSession::ADMIS);
Queue::assertPushedOn('sessions', CreerStackLab::class);
Queue::assertPushed(CreerStackLab::class, 1);
});
test('après le dernier essai raté, la session passe en échec avec sa raison', function () {
$this->mock(FabriqueStack::class, fn ($f) => $f->shouldReceive('nettoyerEssais')->once()->with(Mockery::pattern('/^lab-session-\d+$/')));
$session = sessionEnAttente(LabSession::ADMIS);
(new CreerStackLab($session))->failed(new RuntimeException('CREATE_FAILED (quota)'));
$session->refresh();
expect($session->etat)->toBe(LabSession::ECHEC)
->and($session->raison_echec)->toBe('CREATE_FAILED (quota)')
->and($session->heat_stack_name)->toBeNull();
});
test('le prof peut relancer une session en échec', function () {
Queue::fake();
$this->mock(StockChaud::class, fn ($stock) => $stock->shouldReceive('prendre')->andReturn(null));
$session = sessionEnAttente(LabSession::ECHEC);
$session->update(['raison_echec' => 'ancienne raison']);
admettre($this, $session);
expect($session->refresh()->etat)->toBe(LabSession::ADMIS)
->and($session->raison_echec)->toBeNull();
Queue::assertPushed(CreerStackLab::class);
});
test('la correction est réussie quand toutes les vérifications hors bonus passent', function () {
$session = sessionEnAttente(LabSession::ACTIF);
foreach (['pc-a', 'routeur', 'pc-b'] as $nom) {
$session->machines()->create(['nom' => $nom, 'instance_id' => "uuid-{$nom}"]);
}
$this->mock(ClientCorrecteur::class, fn ($c) => $c->shouldReceive('verifier')
->andReturnUsing(fn ($instance, $lab, $verification) => ['reussi' => $verification !== 'bonus-permanent', 'erreur' => null]));
$correction = app(CorrigerSession::class)->corriger($session);
expect($correction->reussi)->toBeTrue()
->and($correction->note)->toBe(10)
->and($correction->note_max)->toBe(10);
});
test("une vérification ratée donne « échoué » avec le message du lab, une panne d'agent un message technique", function () {
$session = sessionEnAttente(LabSession::ACTIF);
foreach (['pc-a', 'routeur', 'pc-b'] as $nom) {
$session->machines()->create(['nom' => $nom, 'instance_id' => "uuid-{$nom}"]);
}
$this->mock(ClientCorrecteur::class, fn ($c) => $c->shouldReceive('verifier')
->andReturnUsing(fn ($instance, $lab, $verification) => match ($verification) {
'forwarding' => ['reussi' => false, 'erreur' => null],
'ip-pc-b' => ['reussi' => false, 'erreur' => 'agent_injoignable'],
default => ['reussi' => true, 'erreur' => null],
}));
$correction = app(CorrigerSession::class)->corriger($session);
$details = collect($correction->details)->keyBy('id');
expect($correction->reussi)->toBeFalse()
->and($correction->note)->toBe(8)
->and($details['forwarding']['message'])->toContain('net.ipv4.ip_forward')
->and($details['ip-pc-b']['message'])->toContain('ne répond pas');
});
test("l'élève voit la séance ouverte de sa classe et peut la rejoindre ; la déconnexion porte un jeton CSRF", function () {
$session = sessionEnAttente();
$eleve = $session->eleve;
$session->delete();
$jeton = $eleve->demarreNouvelleSession();
$page = $this->withSession(['jeton_session_eleve' => $jeton])->actingAs($eleve, 'eleve')->get(route('eleve'));
$page->assertOk()
->assertSee('Rejoindre la séance')
->assertSee(route('seances.rejoindre', $session->seance_id), false);
expect(preg_match('#action="'.preg_quote(route('deconnexion'), '#').'" method="post"[^>]*><input type="hidden" name="_token"#', $page->getContent()))->toBe(1);
});