Catalogue en lecture seule (labs, banque de quiz officielle, cours) - choix d'Adam pour la première version, zéro donnée d'élève. Laravel Sanctum, un jeton Bearer par développeur créé depuis /jetons-api (un prof), portée catalogue:lire seulement. Quota par jeton (pas par compte ni par IP) dans Redis via un limiteur nommé, 60 requêtes/minute. Documentation : spécification OpenAPI 3.0 et page publique /api/docs. Désactive /sanctum/csrf-cookie (config/sanctum.php) : l'API est en jeton Bearer seulement, jamais en cookie de SPA. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
113 lines
4.6 KiB
PHP
113 lines
4.6 KiB
PHP
<?php
|
|
|
|
use App\Models\Classe;
|
|
use App\Models\Cours;
|
|
use App\Models\Lab;
|
|
use App\Models\Quiz;
|
|
use App\Models\User;
|
|
|
|
function jetonApi(User $prof, array $abilities = ['catalogue:lire']): string
|
|
{
|
|
return $prof->createToken('test', $abilities)->plainTextToken;
|
|
}
|
|
|
|
test('aucune requête sans jeton', function () {
|
|
$this->getJson('/api/v1/labs')->assertUnauthorized();
|
|
});
|
|
|
|
test("un jeton sans l'ability catalogue:lire est refusé", function () {
|
|
$jeton = jetonApi(User::factory()->create(), []);
|
|
|
|
$this->withToken($jeton)->getJson('/api/v1/labs')->assertForbidden();
|
|
});
|
|
|
|
test('un jeton révoqué ne fonctionne plus', function () {
|
|
$prof = User::factory()->create();
|
|
$jeton = jetonApi($prof);
|
|
$prof->tokens()->delete();
|
|
|
|
$this->withToken($jeton)->getJson('/api/v1/labs')->assertUnauthorized();
|
|
});
|
|
|
|
test('liste et détail des labs', function () {
|
|
$jeton = jetonApi(User::factory()->create());
|
|
Lab::factory()->create(['identifiant' => '08-deux-lan-routeur', 'titre' => 'Deux LAN et un routeur', 'competences' => ['routage-statique']]);
|
|
|
|
$this->withToken($jeton)->getJson('/api/v1/labs')
|
|
->assertOk()
|
|
->assertJsonFragment(['identifiant' => '08-deux-lan-routeur']);
|
|
|
|
$this->withToken($jeton)->getJson('/api/v1/labs/08-deux-lan-routeur')
|
|
->assertOk()
|
|
->assertJson(['data' => ['identifiant' => '08-deux-lan-routeur', 'titre' => 'Deux LAN et un routeur', 'competences' => ['routage-statique']]]);
|
|
|
|
$this->withToken($jeton)->getJson('/api/v1/labs/inconnu')->assertNotFound();
|
|
});
|
|
|
|
test('la liste des quiz ne montre que la banque officielle (partagés, avec thème), jamais les questions', function () {
|
|
$jeton = jetonApi(User::factory()->create());
|
|
$classe = Classe::factory()->create();
|
|
|
|
$officiel = Quiz::create(['classe_id' => $classe->id, 'titre' => 'Officiel', 'theme' => 'NET', 'partage' => true]);
|
|
$officiel->questions()->create(['enonce' => 'Une question', 'type' => 'vrai_faux', 'ordre' => 0, 'reponse_vrai_faux' => true]);
|
|
Quiz::create(['classe_id' => $classe->id, 'titre' => 'Partagé sans thème', 'theme' => null, 'partage' => true]);
|
|
Quiz::create(['classe_id' => $classe->id, 'titre' => 'Privé', 'theme' => 'NET', 'partage' => false]);
|
|
|
|
$reponse = $this->withToken($jeton)->getJson('/api/v1/quiz')->assertOk();
|
|
$titres = collect($reponse->json('data'))->pluck('titre');
|
|
|
|
expect($titres->all())->toBe(['Officiel'])
|
|
->and($reponse->json('data.0'))->not->toHaveKey('questions')
|
|
->and($reponse->json('data.0.questions_count'))->toBe(1);
|
|
|
|
$this->withToken($jeton)->getJson("/api/v1/quiz/{$officiel->id}")->assertOk();
|
|
|
|
$prive = Quiz::where('titre', 'Privé')->first();
|
|
$this->withToken($jeton)->getJson("/api/v1/quiz/{$prive->id}")->assertNotFound();
|
|
});
|
|
|
|
test('liste et détail des cours : le contenu complet seulement sur le détail', function () {
|
|
$jeton = jetonApi(User::factory()->create());
|
|
Cours::create(['identifiant' => '08-deux-lan-routeur', 'titre' => 'Deux LAN et un routeur', 'contenu_markdown' => '# Cours']);
|
|
|
|
$this->withToken($jeton)->getJson('/api/v1/cours')
|
|
->assertOk()
|
|
->assertJsonMissing(['contenu_markdown' => '# Cours']);
|
|
|
|
$this->withToken($jeton)->getJson('/api/v1/cours/08-deux-lan-routeur')
|
|
->assertOk()
|
|
->assertJson(['data' => ['contenu_markdown' => '# Cours']]);
|
|
});
|
|
|
|
test('chaque réponse porte les en-têtes de limite de débit', function () {
|
|
$jeton = jetonApi(User::factory()->create());
|
|
|
|
$this->withToken($jeton)->getJson('/api/v1/labs')
|
|
->assertHeader('X-RateLimit-Limit', 60);
|
|
});
|
|
|
|
test('un prof crée un jeton, le voit en clair une seule fois, le révoque', function () {
|
|
$prof = User::factory()->create();
|
|
|
|
$page = prof($this, $prof)->post(route('jetons-api.store'), ['nom' => 'Mon script'])->assertRedirect();
|
|
$page = prof($this, $prof)->get(route('jetons-api.index'));
|
|
$page->assertOk()->assertSee('Mon script');
|
|
|
|
expect($prof->tokens()->count())->toBe(1);
|
|
$jeton = $prof->tokens()->first();
|
|
|
|
prof($this, $prof)->get(route('jetons-api.index'))->assertDontSee($jeton->token);
|
|
|
|
prof($this, $prof)->post(route('jetons-api.destroy', $jeton))->assertRedirect();
|
|
expect($prof->tokens()->count())->toBe(0);
|
|
});
|
|
|
|
test("un prof ne peut pas révoquer le jeton d'un autre prof", function () {
|
|
$prof = User::factory()->create();
|
|
$autreProf = User::factory()->create();
|
|
jetonApi($autreProf);
|
|
$jeton = $autreProf->tokens()->first();
|
|
|
|
prof($this, $prof)->post(route('jetons-api.destroy', $jeton))->assertForbidden();
|
|
expect($autreProf->tokens()->count())->toBe(1);
|
|
});
|