e2e-payments/README.md
Rama main · réplica de lectura del repositorio original.
# Payment sandbox e2e
Browser-driven, **real-money-shaped** end-to-end payment tests against the live
provider **sandboxes** (Stripe, Square, SumUp), orchestrated by Cucumber.
The main test suite (`deno task test`) exercises payments against `stripe-mock`
and stubbed Square/SumUp responses — fast, deterministic, and run on every PR.
This harness is the complement: it boots the **real** app server
(`src/index.ts`), exposes it through a public tunnel, and drives a **real
Chromium** through a complete paid booking, entering a sandbox test card on the
provider's own hosted checkout page and confirming the booking is recorded as
paid. It catches the one class of bug mocks cannot: our API calls, checkout
redirect, return URL, refund, and webhook drifting from what the providers
actually do.
It is intentionally **not** a PR gate (see
`.github/workflows/payment-sandbox-e2e.yml` — nightly + manual). It needs
third-party network access and is slower and flakier than mocked tests.
## Running
```bash
# From the repo root (builds static assets, boots the app):
devenv shell deno task e2e free
# A real provider sandbox (example: Stripe):
STRIPE_SECRET_KEY=sk_test_... devenv shell -- deno task e2e stripe
```
Watch it happen in a real window with `HEADLESS=false`.
A missing paid secret **fails** instead of skipping — the nightly contract
requires every provider.
## What it does
For a target (`free` | `stripe` | `square` | `sumup`), `main.ts` parses the
target, validates its secrets (throwing before any browser call if missing),
maps it to its exhaustive case selection, cleans the artifact root, builds
static assets once, then hands everything to the repository's shared Cucumber
runner (`runSpecs`). Each scenario in the Feature file gets:
1. a fresh file-backed app database;
2. a new real `src/index.ts` child process;
3. a new tunnel for paid providers (or the local URL for free);
4. one browser process with separate authenticated owner and cookie-free visitor
browser contexts (a second owner context for the stale-form race);
5. a unique run identity (booker email, listing names, owner credentials — never
the defaults); and
6. a typed provider runtime holding only this scenario's exact ids.
Hooks own infrastructure acquisition and disposal. Setup, sign-in, provider
configuration, listing creation, booking, and refund actions belong in visible
Given/When steps rather than disappearing into hooks.
### The Feature
`e2e-payments/specs/live-payment-providers.feature` is the human contract: each
Rule names one safe-durable result, and every step states a visible outcome.
The nine scenarios:
- **free-booking-once** — the no-provider journey proves setup, the public form
and the admin assertions work before a third-party provider is involved.
- **stripe-refund-recovers** — Stripe returns money while the local Money write
fails; the booking stays protected, a stale form cannot double-refund, and
Refresh completes the local record when Money recovers.
- **square-refund-safe** — a Square payment is replayed and refunded; the refund
is either recorded or safely observing, with no second Refund.
- **sumup-refund-safe** — a SumUp payment is replayed (genuine self-delivered
callback); forged ids receive the one fixed refusal without a read; the
keyless refund reaches a safe result.
- **sumup-return-pending** — the visitor opens the payment return before paying;
the waiting page says the payment is not confirmed yet and keeps checking on
its own, no error is logged, the timed window stops after ten checks with a
click on Check again opening a fresh one, and the same return books once the
payment is confirmed.
- **stripe-invalidated-checkout-refunded** — the owner changes the price while a
visitor is paying; the webhook processes the later charge, retains the booking
at quantity 0, and automatically refunds.
- **stripe-plan-months** — a site plan is priced and bought in months through
Stripe Checkout. The sandbox stocks no assignable site in the pool, so the
purchase can hand the buyer nothing: the booking and its money stand, and the
owner's log records the empty pool as an incident.
- **stripe-plan-pooled** — the same purchase with an assignable site the owner
registered ahead of time, on an app server whose `SITE_BUILD_DRY_RUN` env
answers the assignment's Bunny API calls (the renewal pushes) from canned
bodies. The purchase takes the pooled site, credits it its months, and the
owner's log records no assignment incident.
- **complex-order-<provider>** — a package, member, and plain listing booked in
one order, with per-listing income verified.
Square's sandbox has no hosted buyer card page, so its driver completes the
payment through the Payments API before the browser follows the real app return
URL. SumUp's callback is self-delivered by the harness with the genuine checkout
id — the harness does not claim to prove SumUp's own webhook delivery.
### Browser interaction
The app is driven the way a person drives it: fill fields by their accessible
name, click buttons and links by their visible text, and submit through the
visible submit control (`requestSubmit`) — never force, never `form.submit()`.
Some headless Chromium builds stop scheduling compositor frames after a form
POST, which stalls Playwright's stability wait; the harness detects that and
falls back to `requestSubmit` / a scripted click, which still runs browser
validation and the app's own event handlers. A failed ordinary action is only
replayed through that fallback when a page-side witness proves the click never
dispatched — a post-dispatch failure rethrows, so a live refund form can never
be submitted twice by the fallback.
### Cleanup
Teardown runs every cleanup action (fault removal, browser, provider resources,
tunnel, server), collecting failures. A cleanup failure on a passing scenario
fails it, and on a failed scenario is reported alongside the original error
without obscuring it.
### Notes per provider
| Provider | Country | Webhook | Refund |
| -------- | -------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------- |
| Stripe | US (USD) | Signed webhook registration, rotation, and cleanup of only this scenario's exact URL | Real refund; Money fault scenario with persistent trigger |
| Square | GB (GBP) | None (return-URL only; Square has no browser card page) | Real sandbox refund API |
| SumUp | GB (GBP) | Unsigned, self-delivered genuine callback; forged ids refused unread | Real keyless refund; delayed history may report pending |
Secrets / env: see `src/config.ts` for all knobs (all optional except
`DB_URL`/`DB_ENCRYPTION_KEY` which the runner sets). Set provider secrets as
environment variables before running the corresponding target.
## Layout
```
src/
main.ts thin target boundary → runSpecs
entry.ts the failure boundary both sandbox harnesses share
targets.ts exhaustive target→case selection + catalog handshake
config.ts env-driven config, required secrets, identity generation
server.ts boot/teardown the real Deno app server on a file DB
tunnel.ts cloudflared quick tunnel (+ no-tunnel passthrough)
browser.ts Chromium lifecycle + honest form/navigation helpers
cleanup.ts attemptEveryCleanup + cleanup error precedence
db-fault.ts scoped Money-transfer refusal (persistent trigger)
refund-outcome.ts classify submitted refund: recorded or observing
flow.ts shared journey helpers (setup, login, listing, booking)
order-flow.ts the complex-order journey (catalog build + verification)
providers/
types.ts PaidSandboxCheckout / SandboxRefundObservation / driver
shared.ts providerFetch, requiredField, observeViaRead, factories
stripe.ts Stripe: pay, refund observation, exact-URL endpoint cleanup
square.ts Square: sandbox API completion, payment read
sumup.ts SumUp: hosted card fill, checkout/transaction ids
sumup-callback.ts callback contract (genuine twice + refusal probes)
card.ts resilient hosted-checkout field filling
cucumber/
support/world.ts LiveWorld: scenario identity + phases + boundary methods
support/hooks.ts Before (infra) + AfterStep (flag) + After (teardown)
support/journal.ts per-scenario non-secret phase journal
steps/setup.ts Given steps: provider config, listing publication
steps/booking.ts When/Then: visitor pays, return replay, admin assertions
steps/refund.ts When/Then: refund flows, Money fault, recovery
steps/pages.ts shared page-navigation and page-fact gathering helpers
```