CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/stripe-test-cards-and-webhooks

Wraps Stripe API testing patterns: test-mode initialization, the canonical test cards (4242 success; 4000 0000 0000 0002 declined; 4000 0027 6000 3184 3DS challenge per 3ds-test-flow-reference), the Stripe CLI webhook flow (`stripe listen --forward-to`), the Stripe CLI fixture commands (`stripe trigger payment_intent.succeeded`), and the webhook signature verification (Stripe-Signature header + HMAC-SHA256). Use when testing Stripe-integrated code.

73

Quality

92%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Medium

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

SKILL.md

name:
stripe-test-cards-and-webhooks
description:
Wraps Stripe API testing patterns: test-mode initialization, the canonical test cards (4242 success; 4000 0000 0000 0002 declined; 4000 0027 6000 3184 3DS challenge per 3ds-test-flow-reference), the Stripe CLI webhook flow (`stripe listen --forward-to`), the Stripe CLI fixture commands (`stripe trigger payment_intent.succeeded`), and the webhook signature verification (Stripe-Signature header + HMAC-SHA256). Use when testing Stripe-integrated code.

stripe-test-cards-and-webhooks

Overview

Stripe's test-mode is feature-complete: every API call works against test data, no real money moves. Per docs.stripe.com/testing, test keys (sk_test_* / pk_test_*) accept canonical test cards that deterministically produce success / decline / 3DS challenge.

When to use

  • Tests for code that integrates Stripe.
  • Verifying webhook handling.
  • 3DS challenge flow tests per 3ds-test-flow-reference.

Authoring

Install

npm install stripe
pip install stripe

Test keys come from the Stripe Dashboard (Developers → API keys → toggle to "View test data").

Initialize

import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_TEST_KEY!);

Canonical test cards

Per docs.stripe.com/testing:

CardBehaviour
4242 4242 4242 4242Success (Visa)
5555 5555 5555 4444Success (Mastercard)
4000 0000 0000 0002Decline (generic_decline)
4000 0000 0000 9995Decline (insufficient_funds)
4000 0000 0000 9987Decline (lost_card)
4000 0000 0000 0069Expired card
4000 0027 6000 31843DS authentication required (challenge)
4000 0000 0000 30553DS supported but frictionless
4100 0000 0000 0019Fraud-prevention block

Test PAN any future expiry; any 3-digit CVC.

PaymentIntent end-to-end

test('successful payment via 4242', async () => {
  const intent = await stripe.paymentIntents.create({
    amount: 1000,
    currency: 'usd',
    payment_method: 'pm_card_visa',
    confirm: true,
    return_url: 'https://example.com/return',
  });
  expect(intent.status).toBe('succeeded');
});

test('declined payment', async () => {
  await expect(
    stripe.paymentIntents.create({
      amount: 1000,
      currency: 'usd',
      payment_method: 'pm_card_chargeDeclined',
      confirm: true,
      return_url: 'https://example.com/return',
    })
  ).rejects.toThrow(/card was declined/);
});

Webhook handler test

import { buffer } from 'micro';
import handler from './stripe-webhook';

test('webhook handler validates signature', async () => {
  const payload = JSON.stringify({ type: 'payment_intent.succeeded', data: {...} });
  const signature = stripe.webhooks.generateTestHeaderString({
    payload,
    secret: process.env.STRIPE_WEBHOOK_SECRET!,
  });

  const req = { headers: { 'stripe-signature': signature }, body: Buffer.from(payload) };
  const res = await handler(req as any);
  expect(res.statusCode).toBe(200);
});

Per docs.stripe.com/webhooks/signatures: the Stripe CLI's generateTestHeaderString produces a valid HMAC-SHA256 signature for testing.

Stripe CLI local forwarding

Per docs.stripe.com/stripe-cli:

stripe listen --forward-to http://localhost:3000/webhooks/stripe
# Forwards real Stripe webhook events to your local endpoint
stripe trigger payment_intent.succeeded
# Sends a synthetic event to test your handler

The CLI also exposes a signing secret (different from your prod webhook secret) for local testing.

Idempotency

test('idempotency key prevents duplicate charges', async () => {
  const key = 'order-12345';
  const r1 = await stripe.paymentIntents.create({...}, { idempotencyKey: key });
  const r2 = await stripe.paymentIntents.create({...}, { idempotencyKey: key });
  expect(r1.id).toBe(r2.id);  // Same PaymentIntent
});

Running

npm test

For webhook-integration tests with stripe listen running:

stripe listen --forward-to http://localhost:3000/webhooks/stripe &
npm test

CI integration

jobs:
  stripe-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v4
      - run: npm ci && npm test
        env:
          STRIPE_TEST_KEY: ${{ secrets.STRIPE_TEST_KEY }}
          STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}

Anti-patterns

Anti-patternWhy it failsFix
Mocking Stripe SDK directlyLoses signature verification, error mappingTest against real test-mode API
Hardcoded test cards in many testsUpdate breakage when Stripe changesPer 3ds-test-flow-reference, use named constants
Skip webhook signature verificationWebhook replay attackAlways verify
Tests without idempotency keyRetried tests duplicate-createAlways set idempotency
Mix prod + test keysReal money riskStrict per-env key separation
Test webhook handler without raw bodybody-parser strips bytes; signature mismatchUse raw body middleware
One-off webhook event testsPer-event-type integration coverage missedTest the full event matrix relevant to your flow
Long-running stripe listen in CIHangs buildsUse stripe listen --print-secret then background

Limitations

  • Test mode is real API surface. Rate limits apply; long CI runs can hit them.
  • Dispute timelines compress in test mode. Use Stripe CLI trigger to fast-forward dispute states.
  • Webhook delivery has ~30s SLA. Tests waiting for webhooks need timeouts.
  • Doesn't validate platform-side bookkeeping. Stripe reports are separate.

References

SKILL.md

tile.json