stripe-webhook-testing — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited stripe-webhook-testing (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Testing Stripe webhook handlers requires a layered approach:
.inject(), database transactions, idempotencyUse fastify.inject() to test the full HTTP pipeline without network I/O:
import { build } from './app'; // Your Fastify app builder
describe('Webhook Handler', () => {
let app: FastifyInstance;
beforeAll(async () => {
app = await build({ testing: true });
});
afterAll(async () => {
await app.close();
});
it('accepts a valid webhook with correct signature', async () => {
const payload = JSON.stringify(mockCheckoutEvent);
const signature = generateTestSignature(payload, TEST_WEBHOOK_SECRET);
const response = await app.inject({
method: 'POST',
url: '/webhooks/stripe',
headers: {
'content-type': 'application/json',
'stripe-signature': signature,
},
payload,
});
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({ received: true });
});
});Create an HMAC-SHA256 signature matching Stripe's format (t={timestamp},v1={hash}). Compute the hash over {timestamp}.{rawPayload} using the webhook secret. See references/essential-test-cases.md for the complete generateTestSignature() helper.
Six test cases must exist for any Stripe webhook handler. Full implementations with code are in `references/essential-test-cases.md`.
| # | Test | Asserts |
|---|---|---|
| 1 | Happy path — valid webhook accepted | checkout.session.completed with valid signature → 200, subscription created in DB |
| 2 | Invalid signature | Tampered stripe-signature header → 400 |
| 3 | Missing signature | No stripe-signature header → 400 |
| 4 | Idempotency — duplicate event | Same event sent twice → processed once, processed_events count = 1 |
| 5 | Schema validation — invalid API input | Malformed billing API request → 400 before handler runs |
| 6 | Out-of-order events | subscription.deleted before subscription.updated → remains canceled |
stripe.subscriptions.retrieve() for thin eventsfastify.inject(), not HTTP clientsprocessed_events tableUse the Stripe CLI to generate realistic event payloads with valid structures:
# Generate a specific event type
stripe trigger checkout.session.completed
# Listen and forward events to local endpoint
stripe listen --forward-to http://localhost:3000/webhooks/stripe
# Generate event JSON for fixtures
stripe events resend evt_... --webhook-endpoint we_...Save captured events as JSON fixtures in tests/fixtures/:
tests/fixtures/
├── checkout.session.completed.json
├── customer.subscription.updated.json
├── invoice.paid.json
└── invoice.payment_failed.jsonUse an in-memory SQLite database for test isolation:
import Database from 'better-sqlite3';
function createTestDb(): Database.Database {
const db = new Database(':memory:');
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
// Run migrations
db.exec(readFileSync('./migrations/001_init.sql', 'utf8'));
return db;
}Each test suite gets a fresh database. No cleanup needed — the in-memory database is destroyed when the connection closes.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.