Skip to content

test(payments): the mint chain runs end to end against the real Stripe test API - #238

Merged
mahmutkaya merged 1 commit into
developfrom
test/e2e-connect-mint
Sep 5, 2026
Merged

mahmutkaya merged 1 commit into
developfrom
test/e2e-connect-mint

Conversation

@mahmutkaya

Copy link
Copy Markdown
Contributor

Summary

  • The mint chain now runs end to end. mintForProposal → createExpressAccount → recordConnectAccount → the fields a registry entry carries was unit-tested, mutation-tested and hand-probed call by call, but had never executed in one piece in any environment — staging cannot rehearse it (provisioningConfigured() is Boolean(process.env.PROVISION_GITHUB_TOKEN), absent there), so the first ever run of the hand-offs would have been a real restaurant's account.
  • tests/e2e/connect-mint.spec.ts exercises everything below openProvisioningPr against the real Stripe API on an sk_test_ key and the suite's throwaway Postgres. The GitHub half stays out: it writes to the deploy repo, which is not a throwaway.
  • One production-adjacent change comes with it: prisma/schema.prisma gains moduleFormat = "cjs", because the generator was emitting an ESM-only client into a package that declares no type — so only a bundler could load lib/db.ts, which is precisely why this chain had no way to be exercised outside a browser request.

Issue / plan link

  • Plan: workspace docs/plans/BACKLOG.md"The mint path has NEVER run end to end anywhere — staging cannot rehearse it"; ADR-011 amendment slices E1–E4.

Type

  • test (+ chore for the generator format)

What it asserts (all through the real functions, no mocks)

  1. A CHF tenant that bought online-payments gets an acct_-shaped account and a paymentsLinkUrl that is absolute and carries no locale prefix — and the claim is then measured, not just asserted about a string: the same URL is fetched with accept-language: fr and de and must produce two different locale redirects. A stored URL has to serve a French-reading room and a German-reading owner.
  2. The StripeConnectAccount row exists for that slug with the returned account id, a non-null onboardingToken that the URL actually addresses, country: CH, and the <slug>-connect-express-v1 key.
  3. What Stripe itself holds: type: express, country: CH, metadata[sofra_tenant], the tenant's own business_profile[url], mcc 5812, and all three capabilities requested at creation (they cannot be added by update).
  4. Idempotency, in both forms. A second proposal reads our own row and re-uses the same account and the same link. Then the row is deleted — the crash between the Stripe POST and the write, which is the window this table exists for — and the next call must recover the same acct_ from Stripe's idempotency key, not mint a twin. Stripe's own account list, filtered on metadata[sofra_tenant], is the only oracle that can see a second live account; the unique slug column cannot.
  5. A tenant without online-payments mints nothing ({}, no note, no row, no account).
  6. EUR refuses BEFORE any network call. Proven with a deliberately broken key held for two calls: the CHF control comes back with Stripe's 401 (so the instrument is awake), and EUR comes back with the country refusal instead — i.e. it never got as far as the key.

Every "nothing was created" claim is paired with a positive control (a slug that must be found in the same listing), because an empty result and a blind instrument read identically.

Cleanup

Asserted, not best-effort — unlike the Mollie teardown, where a missed cancel leaves a test charge into a dead sink. A leaked connected account is a live-shaped KYC object with a bearer onboarding link. So afterAll builds its inventory from the database AND Stripe (not from what a test remembered to report — the account that most needs deleting is the one created by a test that then failed), deletes each, and asserts deleted: true and a follow-up GET → 403. Failures are collected before any is reported, so a throw on the first account cannot leak the second.

The file header states why this is test-mode only: a test-mode account is deletable, a live one belongs to a real business, and Stripe refuses to delete live accounts with the standard dashboard where it carries the negative-balance risk.

NFR triage (DEV-PHASES-PLAN P1)

  • D1 security: no new surface. The spec calls DELETE /v1/accounts in the test process only — deliberately not added to lib/stripe.ts, because nothing in this app may destroy a restaurant's payment account. scripts/e2e-suite.sh and CI both hard-refuse anything that is not sk_test_; the CI secret is scoped to one step and fork PRs never receive it.
  • D9 testing: this PR is the D9 item — the largest remaining verification gap in the Express work.
  • D10 conventions: mirrors billing-mollie.spec.ts exactly — real API, refuses a live key, and skips with a stated reason rather than passing quietly.
  • Rest: n/a because no runtime code path, no UI, no schema data change, no user-visible string.

Database / migrations

No schema change. moduleFormat is a generator output format; lib/generated/ is gitignored and rebuilt by prisma generate on every build, so nothing here is a reviewable artifact and prisma_drift is untouched.

Standard checklist

  • npm run typecheck · npm run lint · npm run test:coverage · node scripts/check-single-file.mjs --all green locally
  • No secrets/keys in the diff (.env.example gains a documented placeholder only)
  • Branch off develop, PR to develop

Test plan — what actually ran

  • bash scripts/e2e-suite.sh (full, unmocked, with the real Mollie test_ key): 83/83 passed, so the generator change is exercised by every DB-touching spec and by a real Mollie first payment, not only by the new file.
  • bash scripts/e2e-suite.sh connect-mint: 3/3, twice. Each run created one real Stripe test account and deleted it — logged as e2e cleanup: 1 connected account(s) — [{"id":"acct_…","deleted":true,"afterGet":403}].
  • Audited afterwards: the Stripe test platform holds no account tagged with an e2e- slug; only the two pre-existing untagged ones remain.
  • Skip path: with no key the file reports "STRIPE_API_KEY is not set — export STRIPE_API_KEY_TEST" and never passes silently.

Deploy notes

  • Migration one-off required: no
  • New env vars on the box: none. STRIPE_API_KEY_TEST is read by scripts/e2e-suite.sh only (with a fallback to STRIPE_SECRET_KEY when that is itself an sk_test_ key, so the spec does not skip on a machine that already has a good test key).
  • Owner action, optional: CI is wired for secrets.STRIPE_API_KEY_TEST but the secret does not exist yet, so the spec skips on GitHub exactly as the Mollie one does. Until it is added, the chain is proven by a local scripts/e2e-suite.sh run and nothing more.
  • Rollout order / risk: test-only, plus a Prisma output format proven by the full suite.

…e test API

`mintForProposal -> createExpressAccount -> recordConnectAccount -> the fields a
registry entry carries` was unit- and mutation-tested and each Stripe call had
been probed by hand, but the CHAIN had never executed in one piece anywhere:
staging cannot rehearse it (`provisioningConfigured()` is
`Boolean(PROVISION_GITHUB_TOKEN)` and that variable is absent there), so the
first run of the hand-offs would have been a real restaurant's account.

tests/e2e/connect-mint.spec.ts runs everything below `openProvisioningPr`
against the REAL Stripe API on an `sk_test_` key and the suite's throwaway
Postgres. Nothing is mocked (CLAUDE.md §7). It asserts the minted `acct_`, the
locale-free `payments_link_url:` (and drives the middleware in French and in
German to prove one stored URL serves both), the StripeConnectAccount row, what
Stripe itself holds, that a cash-only tenant mints nothing, and that EUR is
refused BEFORE the call — proven with a deliberately broken key whose 401 the
CHF control does surface.

The assertion the file exists for is idempotency, in both forms: a re-run reads
our own row, and a run whose row is DELETED (the crash between the Stripe POST
and the write) recovers the SAME account from the idempotency key rather than
minting a twin. Stripe's own account list, filtered on `metadata[sofra_tenant]`,
is the only oracle that can see that failure — the unique slug column cannot.

Cleanup is asserted, not best-effort: every account is deleted (`deleted: true`,
then GET -> 403) from an inventory taken from the database AND from Stripe, so
an account created by a test that then failed is still removed.

Guards mirror billing-mollie.spec.ts: the suite and CI refuse anything but
`sk_test_`, and with no key the spec SKIPS with a stated reason.

prisma/schema.prisma: `moduleFormat = "cjs"`. The generator was emitting an
ESM-only client (`import.meta.url`) into a package that declares no `type`, so
only a bundler could load `lib/db.ts` — which is why this chain had no way to be
exercised outside a browser request. Output format only; lib/generated/ is
gitignored and rebuilt on every build.

Measured: the full unmocked suite is 83/83 green on this change (including the
real Mollie first payment), and the Stripe test platform holds no `e2e-` account
afterwards.
@sonarqubecloud

sonarqubecloud Bot commented Sep 5, 2026

Copy link
Copy Markdown

Quality Gate Passed Quality Gate passed

Issues
0 New issues
3 Accepted issues

Measures
0 Security Hotspots
No data about Coverage
0.0% Duplication on New Code

See analysis details on SonarQube Cloud

@mahmutkaya

Copy link
Copy Markdown
Contributor Author

SonarCloud triage — quality gate OK (6 conditions populated; verified with a negative control: a bogus project key answers 404 {"errors":[…]} and a bogus token 401, so the instrument discriminates rather than reporting a comfortable 200 for a scope it never analysed).

Three issues were raised, all typescript:S1607 "Remove this unit test or explain why it is ignored", one per test.skip(...) in tests/e2e/connect-mint.spec.ts (193, 303, 320). All three transitioned to ACCEPTED with a rationale, which is the same triage the identical issue already carries on tests/e2e/billing-mollie.spec.ts:97:

This is not a disabled test. test.skip(cond, reason) is called inside the test body as a runtime decision — the file mints REAL Stripe connected accounts, so it must not run without an sk_test_ key, and Playwright then reports the run as SKIPPED with a stated reason rather than passed. Removing it would either fail the suite on any machine without a Stripe test key, or invite the one thing this file exists to prevent: a green run that verified nothing.

Open Sonar issues on this PR: 0.

@mahmutkaya
mahmutkaya merged commit ac05b58 into develop Sep 5, 2026
18 checks passed
@mahmutkaya
mahmutkaya deleted the test/e2e-connect-mint branch September 5, 2026 22:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant