dánwòdánwò

Payments

Stripe

Customers, products and prices, payment intents, refunds and Checkout with its hosted page, plus Stripe's signed webhooks.

danwo's Stripe emulator answers Stripe's API and its hosted Checkout page. The official stripe package works against it unchanged, and your app keeps its own sk_test_ key.

This page covers payments and Checkout. Subscriptions and invoices have their own page, Stripe billing, and so do the browser's Stripe.js and test cards.

Set up

Add stripeService to your environment:

danwo.config.ts
import { appEnvironment, defineConfig, stripeService } from "@danwo/sdk"

export default defineConfig({
  testDir: "tests",
  readiness: { mode: "page+backend", settle: true },
  environment: () =>
    appEnvironment({
      app: { command: "npm run dev", url: "http://localhost:3000" },
      services: [stripeService()],
    }),
})

Your app needs nothing new. Any sk_test_ key is accepted, so keep the one in your .env. settle: true makes each step wait until Stripe's webhooks have reached your app.

Seed it

The seed is a function: it runs at the start and before every test.

danwo.config.ts · the seed
stripeService({
  seed: () => ({
    customers: [{ id: "cus_ada", email: "[email protected]", name: "Ada" }],
    products: [{ name: "Credits" }],
    prices: [{ product_name: "Credits", unit_amount: 500, currency: "usd", lookup_key: "credits" }],
    webhookEndpoints: [
      {
        url: "http://localhost:3000/api/webhooks/stripe",
        events: ["checkout.session.completed"],
        secret: "whsec_test",
      },
    ],
  }),
})
FieldWhat it seeds
customersCustomers, with your own ids if you give them.
productsProducts, named so prices can refer to them.
pricesOne-off or recurring prices, by product_name, with an optional lookup_key.
webhookEndpointsWhere webhooks go, which events, and the signing secret.
paymentMethodConfigurationsWhich payment methods Checkout offers.

Use it in a test

The stripe fixture is the emulator. Ask it what happened with Stripe's own API, through stripe.fetch:

tests/buy.danwo.ts
import { type StripeFixtures, test as base } from "@danwo/sdk"

const test = base.extend<StripeFixtures>()

test("pays for credits once", async ({ app, screen, expect, stripe }) => {
  await app.open("/")
  await screen.getByRole("button", { name: "Buy 500 credits" }).click()
  await screen.getByTestId("checkout-pay").click()
  await expect(screen.getByText("Credits: 500")).toBeVisible()

  const answer = await stripe.fetch(
    new Request("https://api.stripe.com/v1/checkout/sessions?payment_status=paid", {
      headers: { authorization: "Bearer sk_test_any" },
    }),
  )
  const { data } = await answer.json()

  expect(data).toHaveLength(1)
})

On the emulator's Checkout page, checkout-pay pays and checkout-cancel goes back to your cancel_url.

The card number field holds Visa's test card. Type Stripe's declining card instead and the payment fails the way it does on Stripe:

Card numberOn the Checkout page
4242 4242 4242 4242pays, and checkout.session.completed is sent
4000 0000 0000 0002declined: the page shows an alert, the session stays open, no event is sent

The alert has the test id checkout-error, and the payer can try another card.

What's covered

AreaCovered
CustomersCreate, read, update, delete and list them, and customer sessions.
Products and pricesOne-off and recurring prices, found by lookup key.
Payment intentsThe whole life of a payment: create, confirm, cancel.
Payment methodsSaved cards, attached to and detached from customers.
Charges and refundsCharges, and full or partial refunds.
Invoice itemsOne-off charges for usage you count, by price or amount.
CheckoutSessions in payment or subscription mode, the hosted page, expiry.
Webhook endpointsEndpoints, and signed deliveries with retries.
RequestsIdempotency keys, expansion and Stripe's error shapes.

Faults

CallFaults the matrix tries
A write, such as POST /v1/checkout/sessions500, 429 rate limit, connection reset, answer lost after Stripe acted
A read, such as GET /v1/customers/:id500, 429 rate limit, connection reset
Paying on the hosted Checkout pagethe same as a write, and a declined card
Every webhook deliveryarrives twice, arrives 10 minutes late, forged signature, 1 hour stale

The error bodies are Stripe's, so the SDK raises StripeAPIError or StripeRateLimitError just as it would for real.

Webhooks

Deliveries carry a Stripe-Signature header made with the endpoint's secret, so stripe.webhooks.constructEvent accepts them. A failed delivery is retried after 5 minutes, 30 minutes and 2 hours, on the emulator's clock.

EventSent when
checkout.session.completedA customer pays on the Checkout page.
checkout.session.expiredA session expires unpaid.
payment_intent.succeededA payment goes through.
payment_intent.payment_failedA payment is declined.
charge.refundedA charge is refunded, in full or in part.
customer.createdA customer is created.

The billing events are on Stripe billing.

Fidelity

Live. danwo fidelity stripe runs 11 scenarios against a Stripe test-mode account and compares each answer's shape. All 11 match. Checkout differs where the account has Stripe Tax turned on, which is recorded as a known difference.

Not covered yet

  • Connect, Issuing, Terminal and Treasury
  • Payment Links, Quotes and coupons
  • Capturing a payment intent later, disputes and Radar rules
  • Deleting products and prices, and customer search

Next

On this page

Run it on every pull request

A GitHub check and one comment per pull request, with the flight records attached.

Set up CI