ajoute l'API publique /api/v1 (bloc B, premier chantier)
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>
This commit is contained in:
parent
3b961209f5
commit
ecf3dfb21d
22 changed files with 874 additions and 2 deletions
113
portail/tests/Feature/ApiV1Test.php
Normal file
113
portail/tests/Feature/ApiV1Test.php
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<?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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue