CommunityRedacción y edicióngithub.com

shaxzodbek-uzb/laravel-guardrails

🛡️ Production guardrail skills for AI-assisted Laravel — stop cross-tenant leaks, N+1, destructive migrations before your AI agent ships them. For Claude Code, Cursor & 40+ agents.

¿Qué es laravel-guardrails?

laravel-guardrails is a Claude Code agent skill that 🛡️ Production guardrail skills for AI-assisted Laravel — stop cross-tenant leaks, N+1, destructive migrations before your AI agent ships them. For Claude Code, Cursor & 40+ agents.

Compatible conClaude Code~Codex CLICursor
npx skills add shaxzodbek-uzb/laravel-guardrails

Installed? Explore more Redacción y edición skills: steipete/notion, affaan-m/seo, affaan-m/brand-voice · View all 6 →

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Pest Testing Discipline

Features shipped untested — or "tested" with over-mocked suites that assert implementation details — break silently in production and rot on every refactor. This skill makes you write Pest 3/4 tests that prove behavior, cover the unhappy path and authorization, and survive refactoring.

The footgun

Two failure modes, both expensive:

  1. No tests / happy-path-only tests. The feature works in the demo, then in production a guest hits the endpoint, a tenant reads another tenant's data, a validation gap lets garbage in, or a 404 becomes a 500. Authorization holes (403/404 paths that were never asserted) are how data leaks ship to prod.
  2. Brittle, over-mocked tests. You mock your own UserRepository/service with shouldReceive('save')->once(), so the test passes even when the real query is wrong, and it explodes the moment anyone renames a method. The suite is green, gives false confidence, and is deleted in frustration during the next refactor — leaving you back at mode 1.

The fix is the same discipline either way: fake the boundaries you don't own, exercise the code you do own, and assert observable outcomes (DB rows, HTTP status, dispatched jobs/mail) — including the failure and authorization outcomes.

Rules

  1. Write the test with the feature, not "later". A controller/job/command/policy change is incomplete until it has a Pest test. Never report a feature done without a corresponding test asserting its behavior.
  2. Use Pest idioms. Define cases with it('...') or test('...'); assert with the fluent expect(...); share setup with beforeEach(). Do not write PHPUnit class FooTest extends TestCase unless the file already uses that style.
  3. Pick the right type. HTTP/end-to-end behavior → Feature test (boots the framework, hits routes via $this->get/post/...). Pure logic with no framework/DB → Unit test. Default to a Feature test when in doubt — it catches more.
  4. Reset the database with a trait, via uses(). Apply RefreshDatabase (transaction-wrapped) or LazilyRefreshDatabase (only migrates when a test touches the DB) in tests/Pest.php for the whole Feature directory, or uses(RefreshDatabase::class) at the top of a file. NEVER leave DB tests dependent on leftover state.
  5. Build data with factories, never hand-rolled inserts. Use User::factory()->create() / ->make() and relationship factories. Hand-built DB::table()->insert([...]) or new Model([...])->save() in tests is brittle and skips casts/defaults — ban it.
  6. Deduplicate with datasets, not copy-paste. When the same assertion runs over many inputs (valid/invalid payloads, role matrices), use ->with([...]) (inline or a named dataset in tests/Datasets/) instead of N near-identical tests.
  7. Fake the boundaries you don't own. For anything external or async, use the facade fakes before the action: Http::fake(), Queue::fake(), Bus::fake(), Mail::fake(), Notification::fake(), Event::fake(), Storage::fake(). For time, use $this->travel(...), $this->travelTo(...), or $this->freezeTime() — never assert against an un-frozen now().
  8. Do NOT mock your own models/services into meaninglessness. Avoid Mockery::mock(MyService::class)->shouldReceive(...) and Model::shouldReceive(...) for code under test. Let the real code run against the test DB and the faked boundaries. Mock your own class only at a genuine seam you are not exercising (e.g. a slow third-party SDK wrapper).
  9. Assert outcomes, not internals. Prefer assertDatabaseHas / assertDatabaseMissing / assertModelExists, assertStatus / assertOk / assertCreated, assertJson / assertJsonPath, assertRedirect, Mail::assertSent, Notification::assertSentTo, Queue::assertPushed, Bus::assertDispatched, Storage::disk()->assertExists. Do not assert "method X was called once" as a proxy for behavior.
  10. Always test the unhappy path AND authorization. Every endpoint/action needs, at minimum: the happy path, a validation/failure path, and the auth paths — guest → 401/redirect, wrong user/role → 403, missing/foreign resource → 404. For any security-sensitive feature add an explicit "another user cannot access/modify this" test. This is non-negotiable for anything touching ownership or tenancy.
  11. Add architecture tests as cheap, global guardrails. Keep a tests/Arch.php (or arch cases in Pest.php) with the presets that fit: arch()->preset()->php() (bans die/var_dump/dd-style debug output and deprecated PHP functions), arch()->preset()->security() (bans eval, extract, unserialize, weak hashing/md5/sha1, insecure randomness, etc.), and arch()->preset()->laravel() (enforces Laravel conventions — including no env() outside config). Add project invariants like arch('controllers')->expect('App\Http\Controllers')->toExtend('App\Http\Controllers\Controller').
  12. One behavior per test, descriptive name. it('blocks guests from the dashboard'), not it('works'). If a test name needs "and", split it.
  13. Gate coverage in CI. Run ./vendor/bin/pest --coverage --min=NN to enforce a minimum, and ./vendor/bin/pest --type-coverage (requires the type-coverage plugin) to enforce typed signatures. Treat new untested lines as a defect.
  14. Browser/E2E sparingly (Pest 4). Pest 4 ships browser testing via visit('/')->...->assertSee(...) (Playwright-backed). Reserve it for true full-stack/JS flows; do not reimplement HTTP-layer assertions as slow browser tests.

Good vs bad

// tests/Feature/UpdatePostTest.php

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

// ❌ over-mocked: tests that a method was called, not what happened.
//    Passes even if the wrong post is updated or auth is missing.
it('updates a post', 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' => 'New'])->assertOk();
});
// ✅ exercises real code against the test DB; asserts the outcome AND authz.
use App\Models\Post;
use App\Models\User;

it('lets the owner update their post', function () {
    $owner = User::factory()->create();
    $post  = Post::factory()->for($owner)->create(['title' => 'Old']);

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

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

it('forbids a non-owner from updating the post', function () {
    $post     = Post::factory()->create(['title' => 'Old']);
    $attacker = User::factory()->create();

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

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

it('blocks guests', function () {
    $post = Post::factory()->create();

    $this->put("/posts/{$post->id}", ['title' => 'New'])
        ->assertRedirect('/login'); // or assertUnauthorized() for an API
});
// ❌ hits the real network (flaky, slow, may leak keys); asserts nothing useful.
it('notifies slack', function () {
    $resp = (new App\Services\Slack)->ping('deploy ok');
    expect($resp)->toBeArray();
});
// ✅ fake the boundary you don't own; assert the request you sent.
use Illuminate\Support\Facades\Http;

it('posts the deploy message to slack', function () {
    Http::fake(['hooks.slack.com/*' => Http::response(['ok' => true])]);

    (new App\Services\Slack)->ping('deploy ok');

    Http::assertSent(fn ($request) =>
        $request->url() === 'https://hooks.slack.com/services/T/B/x'
        && $request['text'] === 'deploy ok'
    );
});
// ❌ copy-pasted near-identical validation tests.
it('rejects empty email', function () {
    $this->post('/register', ['email' => ''])->assertSessionHasErrors('email');
});
it('rejects bad email', function () {
    $this->post('/register', ['email' => 'nope'])->assertSessionHasErrors('email');
});
// ✅ one test, table-driven with a dataset.
it('rejects invalid emails', function (string $email) {
    $this->post('/register', ['email' => $email])
        ->assertSessionHasErrors('email');
})->with([
    'empty'       => '',
    'no at-sign'  => 'nope',
    'no domain'   => 'a@',
]);
// ✅ async + time boundaries faked; assert the dispatch and the side effect.
use App\Jobs\SendReminder;
use App\Mail\WelcomeMail;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;

it('queues a reminder and welcomes the user on signup', function () {
    Queue::fake();
    Mail::fake();
    $this->freezeTime();

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

    Queue::assertPushed(SendReminder::class);
    Mail::assertSent(WelcomeMail::class, fn ($m) => $m->hasTo('[email protected]'));
    $this->assertDatabaseHas('users', ['email' => '[email protected]']);
});
// tests/Arch.php — ✅ cheap global guardrails
arch()->preset()->php();        // bans die/var_dump/dd-style debug output + deprecated PHP fns
arch()->preset()->security();   // bans eval, extract, unserialize, md5/sha1, weak randomness, etc.
arch()->preset()->laravel();    // enforces Laravel conventions, incl. no env() outside config

arch('controllers extend the base controller')
    ->expect('App\Http\Controllers')
    ->toExtend('App\Http\Controllers\Controller');

arch('models live in the right namespace')
    ->expect('App\Models')
    ->toExtend('Illuminate\Database\Eloquent\Model');

How to verify

Run these before claiming a feature + its tests are done:

# 1. The whole suite must be green.
./vendor/bin/pest

# 2. Run just the new/changed file(s) while iterating.
./vendor/bin/pest tests/Feature/UpdatePostTest.php

# 3. Enforce a coverage floor (requires Xdebug or PCOV).
./vendor/bin/pest --coverage --min=80

# 4. Type coverage (requires pestphp/pest-plugin-type-coverage).
./vendor/bin/pest --type-coverage --min=100

# 5. Style stays clean.
./vendor/bin/pint --test

Then audit that you did NOT reintroduce the footgun:

# Authz coverage: each protected feature test should assert a 401/403/404 path.
grep -rEn "assertForbidden|assertUnauthorized|assertNotFound|->assertStatus\((401|403|404)\)" tests/

# Over-mocking smell: mocking your OWN app classes is a red flag — review each hit.
grep -rEn "Mockery::|->shouldReceive\(|::partialMock\(|::spy\(" tests/

# Hand-rolled inserts instead of factories — replace with ->factory().
grep -rEn "DB::table\(.*\)->insert|new App\\\\Models" tests/

# External boundaries must be faked — confirm fakes exist where code calls out.
grep -rEn "Http::fake|Queue::fake|Bus::fake|Mail::fake|Notification::fake|Storage::fake|Event::fake" tests/

# The security + php arch presets (the cheap global guardrails) must be present somewhere.
grep -rEn "preset\(\)->(security|php|laravel)\(\)" tests/

Assert in tests, not just code: for every new endpoint confirm there is (a) a happy-path outcome assertion (assertDatabaseHas/assertOk/assertJsonPath), (b) at least one failure/validation assertion, and (c) the guest + wrong-user authorization assertions. If any is missing, the feature is not done.

When it's OK to bend the rule

  • Mocking your own code at a true external seam. A thin wrapper around a paid third-party SDK (SMS, payment gateway) that you cannot fake at the HTTP layer is a legitimate Mockery target — you are isolating their dependency, not faking the behavior under test.
  • Unit-testing pure logic without the DB. Value objects, formatters, and calculators need no RefreshDatabase and no factories — instantiate them directly and expect() the result; that is faster and correct.
  • make() over create() when a test never persists — User::factory()->make() avoids a DB write for pure-logic assertions.
  • Lower coverage thresholds on legacy code. Set a realistic --min and ratchet it up over time rather than blocking all work; never let "100% or nothing" become an excuse to ship zero tests.
  • Browser tests for genuinely JS-driven flows (Livewire/Inertia/Vue interactions, file pickers) — there, visit() is the right tool, not an over-reach.

References

Skills relacionados

steipete/notion

Notion CLI/API for pages, Markdown content, data sources, files, comments, search, Workers, and raw API calls.

community

affaan-m/seo

Audit, plan, and implement SEO improvements across technical SEO, on-page optimization, structured data, Core Web Vitals, and content strategy. Use when the user wants better search visibility, SEO remediation, schema markup, sitemap/robots work, or keyword mapping.

community

affaan-m/brand-voice

Build a source-derived writing style profile from real posts, essays, launch notes, docs, or site copy, then reuse that profile across content, outreach, and social workflows. Use when the user wants voice consistency without generic AI writing tropes.

community

affaan-m/crosspost

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

community

affaan-m/x-api

X/Twitter API integration for posting tweets, threads, reading timelines, search, and analytics. Covers OAuth auth patterns, rate limits, and platform-native content posting. Use when the user wants to interact with X programmatically.

community

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

community