146 lines
6.1 KiB
PHP
146 lines
6.1 KiB
PHP
|
|
<?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);
|
||
|
|
});
|