CommunityRédaction et éditiongithub.com

Laravel Guardrails

đŸ›Ąïž CompĂ©tences de garde-fous pour Laravel assistĂ© par IA — empĂȘchez les fuites inter-locataires, les N+1, les migrations destructrices avant que votre agent IA ne les envoie. Pour Claude Code, Cursor et 40+ agents.

Qu'est-ce que Laravel Guardrails ?

Cette compétence définit une discipline de test rigoureuse avec Pest 3/4 pour les projets Laravel, conçue pour les flux de travail assistés par IA dans des agents comme Claude Code, Cursor et plus de 40 autres. L'objectif principal est d'éviter deux modes de défaillance courants : soit l'absence de tests ou des tests uniquement positifs qui entraßnent des pannes silencieuses en production, soit des tests fragiles et trop mockés qui donnent une fausse confiance et échouent lors des refactorisations. La compétence impose l'écriture de tests comportementaux avec des usines, des jeux de données et des fausses frontiÚres, en utilisant des fonctionnalités Laravel intégrées comme RefreshDatabase, Http::fake, Queue::fake, Mail::fake, Notification::fake et bus::fake. Elle exige l'assertion systématique des chemins heureux, des échecs et des chemins d'autorisation (401/403/404) pour chaque point de terminaison, y compris les tests d'inviolabilité. La compétence applique également des tests d'architecture avec les préréglages security(), php() et laravel() de Pest comme garde-fous globaux à faible coût, et fournit des commandes de vérification pour auditer la couverture, les odeurs de sur-mockage et les insertions manuelles. Elle inclut des directives flexibles pour les cas limites légitimes, comme le mocking d'adaptateurs SDK tiers ou les tests unitaires de logique pure sans base de données.

Compatible avec✓Claude Code~Codex CLI✓Cursor
npx skills add shaxzodbek-uzb/laravel-guardrails

Installed? Explore more RĂ©daction et Ă©dition skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Discipline de test Pest

Les fonctionnalitĂ©s livrĂ©es non testĂ©es — ou « testĂ©es » avec des suites trop mockĂ©es qui vĂ©rifient les dĂ©tails d'implĂ©mentation — plantent silencieusement en production et pourrissent Ă  chaque refactorisation. Cette compĂ©tence vous fait Ă©crire des tests Pest 3/4 qui prouvent le comportement, couvrent les chemins d'Ă©chec et d'autorisation, et survivent aux refactorisations.

Le piĂšge

Deux modes de défaillance, tous deux coûteux :

  1. Pas de tests / tests uniquement positifs. La fonctionnalité fonctionne dans la démo, puis en production un invité accÚde au point de terminaison, un locataire lit les données d'un autre locataire, une lacune de validation laisse entrer des déchets, ou une 404 devient une 500. Les failles d'autorisation (chemins 403/404 jamais vérifiés) sont ainsi que les fuites de données arrivent en production.
  2. Tests fragiles et trop mockĂ©s. Vous mockez votre propre UserRepository/service avec shouldReceive('save')->once(), donc le test passe mĂȘme si la vraie requĂȘte est erronĂ©e, et il explose dĂšs que quelqu'un renomme une mĂ©thode. La suite est verte, donne une fausse confiance, et est supprimĂ©e par frustration lors de la prochaine refactorisation — vous laissant revenir au mode 1.

La solution est la mĂȘme discipline dans les deux cas : fausser les frontiĂšres que vous ne possĂ©dez pas, exĂ©cuter le code que vous possĂ©dez, et vĂ©rifier les rĂ©sultats observables (lignes DB, statut HTTP, jobs/mails dispatchĂ©s) — y compris les rĂ©sultats d'Ă©chec et d'autorisation.

RĂšgles

  1. Écrire le test avec la fonctionnalitĂ©, pas « plus tard ». Un changement de contrĂŽleur/job/commande/politique est incomplet tant qu'il n'a pas un test Pest. Ne signalez jamais une fonctionnalitĂ© comme terminĂ©e sans un test correspondant vĂ©rifiant son comportement.
  2. Utiliser les idiomes Pest. Définissez les cas avec it('...') ou test('...') ; vérifiez avec le fluide expect(...) ; partagez la configuration avec beforeEach(). N'écrivez pas class FooTest extends TestCase à la PHPUnit sauf si le fichier utilise déjà ce style.
  3. Choisissez le bon type. Comportement HTTP/end-to-end → Test de fonctionnalitĂ© (dĂ©marre le framework, accĂšde aux routes via $this->get/post/...). Logique pure sans framework/DB → Test unitaire. Par dĂ©faut, choisissez un test de fonctionnalitĂ© en cas de doute — il attrape plus de problĂšmes.
  4. Réinitialisez la base de données avec un trait, via uses(). Appliquez RefreshDatabase (enveloppé dans une transaction) ou LazilyRefreshDatabase (migre uniquement lorsqu'un test touche la DB) dans tests/Pest.php pour tout le répertoire Feature, ou uses(RefreshDatabase::class) en haut d'un fichier. NE laissez JAMAIS les tests DB dépendre d'un état résiduel.
  5. Construisez les donnĂ©es avec des usines, jamais des insertions Ă  la main. Utilisez User::factory()->create() / ->make() et les usines de relations. Les DB::table()->insert([...]) ou new Model([...])->save() faits Ă  la main dans les tests sont fragiles et ignorent les casts/valeurs par dĂ©faut — bannissez-les.
  6. DĂ©doublez avec des jeux de donnĂ©es, pas du copier-coller. Lorsque la mĂȘme vĂ©rification s'exĂ©cute sur plusieurs entrĂ©es (charges utiles valides/invalides, matrices de rĂŽles), utilisez ->with([...]) (inline ou un jeu de donnĂ©es nommĂ© dans tests/Datasets/) au lieu de N tests quasi identiques.
  7. Faussez les frontiĂšres que vous ne possĂ©dez pas. Pour tout ce qui est externe ou asynchrone, utilisez les fausses façade avant l'action : Http::fake(), Queue::fake(), Bus::fake(), Mail::fake(), Notification::fake(), Event::fake(), Storage::fake(). Pour le temps, utilisez $this->travel(...), $this->travelTo(...), ou $this->freezeTime() — ne vĂ©rifiez jamais contre un now() non figĂ©.
  8. NE mockez PAS vos propres modĂšles/services jusqu'Ă  les rendre insignifiants. Évitez Mockery::mock(MyService::class)->shouldReceive(...) et Model::shouldReceive(...) pour le code testĂ©. Laissez le vrai code s'exĂ©cuter contre la DB de test et les frontiĂšres fausses. Mockez votre propre classe uniquement Ă  une vĂ©ritable couture que vous n'exercez pas (par exemple, un wrapper SDK tiers lent).
  9. Vérifiez les résultats, pas les internes. Préférez assertDatabaseHas / assertDatabaseMissing / assertModelExists, assertStatus / assertOk / assertCreated, assertJson / assertJsonPath, assertRedirect, Mail::assertSent, Notification::assertSentTo, Queue::assertPushed, Bus::assertDispatched, Storage::disk()->assertExists. Ne vérifiez pas « la méthode X a été appelée une fois » comme proxy du comportement.
  10. Testez toujours le chemin d'Ă©chec ET l'autorisation. Chaque point de terminaison/action nĂ©cessite, au minimum : le chemin heureux, un chemin de validation/Ă©chec, et les chemins d'auth — invitĂ© → 401/redirection, mauvais utilisateur/rĂŽle → 403, ressource manquante/Ă©trangĂšre → 404. Pour toute fonctionnalitĂ© sensible Ă  la sĂ©curitĂ©, ajoutez un test explicite « un autre utilisateur ne peut pas accĂ©der/modifier cela ». C'est non-nĂ©gociable pour tout ce qui touche Ă  la propriĂ©tĂ© ou Ă  la location.
  11. Ajoutez des tests d'architecture comme garde-fous globaux Ă  faible coĂ»t. Gardez un fichier tests/Arch.php (ou des cas d'arch dans Pest.php) avec les prĂ©rĂ©glages qui conviennent : arch()->preset()->php() (bannit la sortie de dĂ©bogage die/var_dump/dd et les fonctions PHP obsolĂštes), arch()->preset()->security() (bannit eval, extract, unserialize, le hachage faible/md5/sha1, l'alĂ©atoire non sĂ©curisĂ©, etc.), et arch()->preset()->laravel() (impose les conventions Laravel — y compris pas de env() en dehors de config). Ajoutez des invariants de projet comme arch('controllers')->expect('App\Http\Controllers')->toExtend('App\Http\Controllers\Controller').
  12. Un comportement par test, nom descriptif. it('bloque les invités du tableau de bord'), pas it('fonctionne'). Si un nom de test a besoin d'un « et », divisez-le.
  13. Couverture de porte dans CI. Exécutez ./vendor/bin/pest --coverage --min=NN pour imposer un minimum, et ./vendor/bin/pest --type-coverage (nécessite le plugin type-coverage) pour imposer des signatures typées. Traitez les nouvelles lignes non testées comme un défaut.
  14. Browser/E2E avec parcimonie (Pest 4). Pest 4 propose des tests navigateur via visit('/')->...->assertSee(...) (basé sur Playwright). Réservez-le pour les flux full-stack/JS véritables ; ne réimplémentez pas les vérifications de couche HTTP comme des tests navigateur lents.

Bon vs mauvais

// tests/Feature/UpdatePostTest.php

use App\Models\Post;
use App\Models\User;

// ❌ trop mockĂ© : teste qu'une mĂ©thode a Ă©tĂ© appelĂ©e, pas ce qui s'est passĂ©.
//    Passe mĂȘme si le mauvais article est mis Ă  jour ou si l'auth est absente.
it('met Ă  jour un article', function () {
    $service = Mockery::mock(App\Services\PostService::class);
    $service->shouldReceive('update')->once()->andReturnTrue();
    $this->app->instance(App\Services\PostService::class, $service);

    $this->put('/posts/1', ['title' => 'Nouveau'])->assertOk();
});
// ✅ exĂ©cute le vrai code contre la DB de test ; vĂ©rifie le rĂ©sultat ET l'authz.
use App\Models\Post;
use App\Models\User;

it('laisse le propriétaire mettre à jour son article', function () {
    $owner = User::factory()->create();
    $post  = Post::factory()->for($owner)->create(['title' => 'Ancien']);

    $this->actingAs($owner)
        ->put("/posts/{$post->id}", ['title' => 'Nouveau'])
        ->assertRedirect();

    $this->assertDatabaseHas('posts', ['id' => $post->id, 'title' => 'Nouveau']);
});

it('interdit à un non-propriétaire de mettre à jour l\'article', function () {
    $post     = Post::factory()->create(['title' => 'Ancien']);
    $attacker = User::factory()->create();

    $this->actingAs($attacker)
        ->put("/posts/{$post->id}", ['title' => 'Piraté'])
        ->assertForbidden(); // 403

    $this->assertDatabaseHas('posts', ['id' => $post->id, 'title' => 'Ancien']);
});

it('bloque les invités', function () {
    $post = Post::factory()->create();

    $this->put("/posts/{$post->id}", ['title' => 'Nouveau'])
        ->assertRedirect('/login'); // ou assertUnauthorized() pour une API
});
// ❌ touche le vrai rĂ©seau (instable, lent, peut fuiter des clĂ©s) ; ne vĂ©rifie rien d'utile.
it('notifie slack', function () {
    $resp = (new App\Services\Slack)->ping('déploiement ok');
    expect($resp)->toBeArray();
});
// ✅ fausse la frontiĂšre que vous ne possĂ©dez pas ; vĂ©rifie la requĂȘte envoyĂ©e.
use Illuminate\Support\Facades\Http;

it('envoie le message de déploiement à slack', function () {
    Http::fake(['hooks.slack.com/*' => Http::response(['ok' => true])]);

    (new App\Services\Slack)->ping('déploiement ok');

    Http::assertSent(fn ($request) =>
        $request->url() === 'https://hooks.slack.com/services/T/B/x'
        && $request['text'] === 'déploiement ok'
    );
});
// ❌ tests de validation quasi identiques copiĂ©s-collĂ©s.
it('rejette un email vide', function () {
    $this->post('/register', ['email' => ''])->assertSessionHasErrors('email');
});
it('rejette un mauvais email', function () {
    $this->post('/register', ['email' => 'nope'])->assertSessionHasErrors('email');
});
// ✅ un seul test, pilotĂ© par un jeu de donnĂ©es.
it('rejette les emails invalides', function (string $email) {
    $this->post('/register', ['email' => $email])
        ->assertSessionHasErrors('email');
})->with([
    'vide'        => '',
    'pas d@'      => 'nope',
    'pas de domaine' => 'a@',
]);
// ✅ frontiĂšres asynchrones + temporelles fausses ; vĂ©rifie le dispatch et l'effet secondaire.
use App\Jobs\SendReminder;
use App\Mail\WelcomeMail;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;

it('met en file d\'attente un rappel et accueille l\'utilisateur lors de l\'inscription', function () {
    Queue::fake();
    Mail::fake();
    $this->freezeTime();

    $this->post('/register', [
        'name' => 'Ada', 'email' => '[email protected]', 'password' => 'mot-de-passe-secret',
    ])->assertRedirect();

    Queue::assertPushed(SendReminder::class);
    Mail::assertSent(WelcomeMail::class, fn ($m) => $m->hasTo('[email protected]'));
    $this->assertDatabaseHas('users', ['email' => '[email protected]']);
});
// tests/Arch.php — ✅ garde-fous globaux Ă  faible coĂ»t
arch()->preset()->php();        // bannit la sortie de débogage die/var_dump/dd + fonctions PHP obsolÚtes
arch()->preset()->security();   // bannit eval, extract, unserialize, md5/sha1, aléatoire faible, etc.
arch()->preset()->laravel();    // impose les conventions Laravel, incl. pas de env() en dehors de config

arch('les contrÎleurs étendent le contrÎleur de base')
    ->expect('App\Http\Controllers')
    ->toExtend('App\Http\Controllers\Controller');

arch('les modĂšles vivent dans le bon espace de noms')
    ->expect('App\Models')
    ->toExtend('Illuminate\Database\Eloquent\Model');

Comment vérifier

Exécutez ces commandes avant de déclarer une fonctionnalité + ses tests comme terminés :

# 1. Toute la suite doit ĂȘtre verte.
./vendor/bin/pest

# 2. Exécutez uniquement le(s) nouveau(x)/modifié(s) fichier(s) pendant l'itération.
./vendor/bin/pest tests/Feature/UpdatePostTest.php

# 3. Imposez un seuil de couverture (nécessite Xdebug ou PCOV).
./vendor/bin/pest --coverage --min=80

# 4. Couverture des types (nécessite pestphp/pest-plugin-type-coverage).
./vendor/bin/pest --type-coverage --min=100

# 5. Le style reste propre.
./vendor/bin/pint --test

Ensuite, auditez que vous N'AVEZ PAS réintroduit le piÚge :

# Couverture d'authz : chaque test de fonctionnalité protégé doit vérifier un chemin 401/403/404.
grep -rEn "assertForbidden|assertUnauthorized|assertNotFound|->assertStatus\((401|403|404)\)" tests/

# Odeur de sur-mockage : mocker vos propres classes d'application est un drapeau rouge — examinez chaque occurrence.
grep -rEn "Mockery::|->shouldReceive\(|::partialMock\(|::spy\(" tests/

# Insertions à la main au lieu d'usines — remplacez par ->factory().
grep -rEn "DB::table\(.*\)->insert|new App\\\\Models" tests/

# Les frontiĂšres externes doivent ĂȘtre fausses — confirmez que les fausses existent lĂ  oĂč le code appelle de l'extĂ©rieur.
grep -rEn "Http::fake|Queue::fake|Bus::fake|Mail::fake|Notification::fake|Storage::fake|Event::fake" tests/

# Les prĂ©rĂ©glages d'arch sĂ©curitĂ© + php (les garde-fous globaux Ă  faible coĂ»t) doivent ĂȘtre prĂ©sents quelque part.
grep -rEn "preset\(\\)->(security|php|laravel)\(\\)" tests/

Vérifiez dans les tests, pas seulement dans le code : pour chaque nouveau point de terminaison, confirmez qu'il y a (a) une vérification de résultat du chemin heureux (assertDatabaseHas/assertOk/assertJsonPath), (b) au moins une vérification d'échec/validation, et (c) les vérifications d'autorisation invité + mauvais utilisateur. Si l'un manque, la fonctionnalité n'est pas terminée.

Quand il est acceptable de déroger à la rÚgle

  • Mocker votre propre code Ă  une vĂ©ritable couture externe. Un wrapper mince autour d'un SDK tiers payant (SMS, passerelle de paiement) que vous ne pouvez pas fausser au niveau HTTP est une cible lĂ©gitime pour Mockery — vous isolez leur dĂ©pendance, vous ne faussez pas le comportement testĂ©.
  • Tester unitairement une logique pure sans base de donnĂ©es. Les objets de valeur, formatteurs et calculateurs n'ont besoin ni de RefreshDatabase ni d'usines — instanciez-les directement et utilisez expect() pour le rĂ©sultat ; c'est plus rapide et correct.
  • make() plutĂŽt que create() lorsqu'un test ne persiste jamais — User::factory()->make() Ă©vite une Ă©criture DB pour les vĂ©rifications de logique pure.
  • Seuils de couverture plus bas sur le code hĂ©ritĂ©. Fixez un --min rĂ©aliste et augmentez-le progressivement plutĂŽt que de bloquer tout travail ; ne laissez jamais « 100% ou rien » devenir une excuse pour livrer zĂ©ro test.
  • Tests navigateur pour les flux vraiment pilotĂ©s par JS (interactions Livewire/Inertia/Vue, sĂ©lecteurs de fichiers) — lĂ , visit() est le bon outil, pas une exagĂ©ration.

Références

Skills associés