Compare commits
2
Commits
80a5f6e457
...
77fd1869db
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77fd1869db | ||
|
|
f0ff14af38 |
@@ -0,0 +1,180 @@
|
||||
# Phase 9.1 — Real Stripe TEST-Mode Integration Validation
|
||||
|
||||
Status: **BLOCKED — no Stripe TEST credentials are available to this session.** This document
|
||||
records exactly what was verified without them, exactly what Chase needs to provide, and the
|
||||
permanent, opt-in validation script that will run the real proof the moment those credentials
|
||||
exist. No live money was risked; no Stripe network call of any kind was made during this phase.
|
||||
|
||||
## 1. Baseline verification
|
||||
|
||||
- Branch: `main`. Working tree: clean at start.
|
||||
- `git fetch origin` — HEAD and `origin/main` both `80a5f6e4576cb9b83a60c9e9b57e4ebc52b62f07`,
|
||||
exactly the Phase 9 milestone commit.
|
||||
- Phase 9 payment code and documentation inspected in full: `payment-provider.js`,
|
||||
`commerce.js`, `webhook-routes.js`, `offerings.js`'s registration/conversion path,
|
||||
`admin-offerings-routes.js`'s payment routes, `integration-routes.js`'s checkout/agreement/
|
||||
status routes, `platform/test/e2e-phase9.js`, `infra/scripts/drills/
|
||||
test-phase9-winter-camp-commerce.js`, `docs/STRIPE_INTEGRATION_RUNBOOK.md`, `docs/
|
||||
WINTER_BREAK_LAUNCH_CHECKLIST.md`. Nothing was changed as a result of this review — no defect
|
||||
was found, and none was invented to justify a change (§1/§22's own instruction).
|
||||
|
||||
## 2. Stripe TEST credential search — none found
|
||||
|
||||
Searched, in order: process environment (`env | grep -i stripe`), every `.env`/`.env.example`
|
||||
file reachable from this repository and the operator's home directory, this repository's own
|
||||
Infisical project wiring (none exists — no `.infisical.json` in this repo), and this session's
|
||||
own memory of prior credential-provisioning work (none references Stripe or this project). No
|
||||
`STRIPE_SECRET_KEY`, no `STRIPE_WEBHOOK_SECRET`, in any test or live form, exists anywhere this
|
||||
session can reach.
|
||||
|
||||
**This is exactly STOP condition 1/2 from this phase's own authorization.** Per §2/§27: do not
|
||||
fake validation, stop after documenting exactly what's needed.
|
||||
|
||||
### What Chase needs to provide or configure
|
||||
|
||||
1. A Stripe account (test mode is available on any account, including one with no live
|
||||
business verification completed yet).
|
||||
2. From the Stripe Dashboard, in **Test mode**: the test **Secret key** (`sk_test_...`) →
|
||||
`STRIPE_SECRET_KEY`.
|
||||
3. A webhook signing secret for a test endpoint — either a permanent test-mode webhook endpoint
|
||||
configured in the dashboard, or the ephemeral one the Stripe CLI prints when running
|
||||
`stripe listen --forward-to <gateway>/api/webhooks/payment` → `STRIPE_WEBHOOK_SECRET`. Full
|
||||
setup steps already documented in `docs/STRIPE_INTEGRATION_RUNBOOK.md`'s "Local testing
|
||||
against real Stripe TEST mode" section (written during Phase 9, unchanged and still accurate).
|
||||
4. Both values set as environment configuration on the running Platform API process
|
||||
(`PAYMENT_PROVIDER=stripe` alongside them) — never committed to git, never placed in any
|
||||
browser-reachable configuration. **No credential value appears anywhere in this document, any
|
||||
commit, or any log produced during this phase.**
|
||||
|
||||
Once those exist, run the permanent validation script this phase adds (§4 below) to produce the
|
||||
actual proof this phase was asked to deliver.
|
||||
|
||||
## 3. Stripe adapter architecture — confirmed, unchanged
|
||||
|
||||
Inspected `payment-provider.js`'s `StripeProvider.createCheckoutSession()`: it uses **dynamic
|
||||
Checkout line-item `price_data`** (`unit_amount`/`currency`/`product_data.name` supplied inline
|
||||
per session), **not** pre-created Stripe Price/Product IDs. RootNode's `offerings` table remains
|
||||
the sole source of truth for identity, price, and currency; Stripe never becomes a second
|
||||
business-domain model — no Stripe object id is ever treated as anything other than an opaque
|
||||
`provider_reference`. This matches §3's explicit constraint and required no change.
|
||||
|
||||
## 4. Permanent opt-in Stripe integration test — added
|
||||
|
||||
`platform/test/stripe-integration-test.js` (new). Default behavior (no environment variable
|
||||
set): prints a skip message and exits `0` with **zero Stripe network traffic** — confirmed by
|
||||
running it with no configuration at all. **Verified this run**:
|
||||
|
||||
```
|
||||
$ node test/stripe-integration-test.js
|
||||
Stripe integration test skipped (RUN_STRIPE_INTEGRATION_TEST is not "true") — no Stripe network traffic, no credentials required.
|
||||
```
|
||||
|
||||
Enabled via `RUN_STRIPE_INTEGRATION_TEST=true`, reusing the exact `STRIPE_SECRET_KEY`/
|
||||
`STRIPE_WEBHOOK_SECRET`-adjacent naming Phase 9 already established (only `STRIPE_SECRET_KEY` is
|
||||
actually needed by the script itself, to independently query Stripe's own API — the webhook
|
||||
secret only needs to be configured on the server process, not read by this script). Guard
|
||||
clauses verified directly, without needing real credentials:
|
||||
|
||||
| Condition | Verified behavior |
|
||||
|---|---|
|
||||
| `OWNER_EMAIL`/`OWNER_PASSWORD` unset | Exits `1` with a clear message. |
|
||||
| `STRIPE_SECRET_KEY` unset | Exits `1`, names the exact variable needed. |
|
||||
| `STRIPE_SECRET_KEY` starts with `sk_live_` | **Refuses to run** — "TEST MODE ONLY" — exits `1`. |
|
||||
| `STRIPE_SECRET_KEY` any other non-test shape | Refuses to run out of caution — exits `1`. |
|
||||
| A syntactically valid `sk_test_...` value, no real stack running | Reaches `main()` cleanly, fails only on the expected connection error — confirms no syntax defect in the enabled code path. |
|
||||
|
||||
When real test credentials exist and the dev stack is running with `PAYMENT_PROVIDER=stripe`,
|
||||
this script will automatically and honestly prove, via pure API calls (no browser needed):
|
||||
|
||||
- RootNode creates a **real** Stripe TEST Checkout Session (not RootNode's own claim — verified
|
||||
by retrieving the session directly from Stripe's own `checkout.sessions.retrieve` API).
|
||||
- The amount/currency Stripe actually recorded matches the offering's server-side price exactly,
|
||||
even when the request is sent with a tampered `amount`/`currency` in the body — proving
|
||||
server-authoritative pricing against **Stripe's own object**, the strongest form of that proof.
|
||||
- `session.livemode === false` is asserted directly — the script itself refuses to consider its
|
||||
own run valid against anything but a real TEST-mode session.
|
||||
- RootNode's webhook route rejects an invalid/garbage signature cleanly, with **zero domain side
|
||||
effect** (the registration's status is re-read and asserted unchanged).
|
||||
|
||||
What it deliberately does **not** automate: actually completing Stripe's hosted Checkout page.
|
||||
There is no supported pure-API way to "pay" a Checkout Session, and fabricating a signed
|
||||
"payment succeeded" webhook for a session nobody actually paid would itself be exactly the kind
|
||||
of faked validation this phase was told never to produce. Instead, the script prints the real
|
||||
checkout URL and polls RootNode's own status endpoint for a bounded window (default 90s,
|
||||
`STRIPE_INTEGRATION_TEST_WAIT_SECONDS`) while a human completes payment in a browser (Stripe's
|
||||
test card `4242 4242 4242 4242`) or a parallel `stripe listen --forward-to` relays Stripe's real
|
||||
event — see `docs/STRIPE_INTEGRATION_RUNBOOK.md`. If nothing arrives in that window (expected in
|
||||
an unattended run), it says so plainly rather than reporting a false pass.
|
||||
|
||||
Never runs as part of normal regression suites or CI — no other test file requires or imports it,
|
||||
and its own default is a guaranteed no-op.
|
||||
|
||||
## 5. What was actually proven this phase
|
||||
|
||||
| Step from the mission spec | Result |
|
||||
|---|---|
|
||||
| 1. Baseline verification | Done — see §1. |
|
||||
| 2. Stripe TEST-mode safety | No live-mode action was possible or attempted; no credentials of any kind exist. |
|
||||
| 3. Stripe architecture review | Done — dynamic `price_data`, confirmed, unchanged (§3). |
|
||||
| 4-20 (disposable offering through security revalidation, all requiring a real Stripe TEST payment/webhook round trip) | **Not performed — blocked on missing credentials.** Faking any of these was explicitly disallowed. |
|
||||
| 21. Permanent opt-in integration test | **Built** (§4) — the automatable portion is real and will run the moment credentials exist; the human/CLI-dependent portion is honestly deferred, not faked. |
|
||||
| 22. Application changes | None made — no genuine defect was exposed (none could be, without real Stripe traffic), so §22's "smallest correct fix" rule had nothing to apply to. |
|
||||
| 23. Regression | No registration/agreement/enrollment/notification/frontend code changed this phase, so per §23's own conditional rule, no regression rerun was required. The new test file's default no-op path was verified directly (§4) instead. |
|
||||
| 24. Documentation | This document, plus the update noted below. |
|
||||
| 25. Live-mode readiness checklist | See §6. |
|
||||
|
||||
## 6. Live-mode readiness checklist
|
||||
|
||||
Everything Stripe-side is still pending exactly as `docs/STRIPE_INTEGRATION_RUNBOOK.md`'s "Going
|
||||
live" section already lays out, restated here as a checklist per §25:
|
||||
|
||||
**Stripe:**
|
||||
- [ ] Business/account verification completed in the Stripe dashboard.
|
||||
- [ ] Live API secret key (`sk_live_...`) generated.
|
||||
- [ ] A permanent live-mode webhook endpoint created, pointed at the production API's
|
||||
`/api/webhooks/payment` URL, and its signing secret (`whsec_...`) captured.
|
||||
- [ ] Payout/bank details configured in Stripe.
|
||||
- [ ] Business/support information (statement descriptor, support contact) configured in Stripe.
|
||||
|
||||
**RootNode:**
|
||||
- [ ] A real, HTTPS production URL for the Platform API (Stripe requires HTTPS for live webhooks).
|
||||
- [ ] `STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET` (live values) provisioned into production
|
||||
secret configuration only.
|
||||
- [ ] Production deployment itself (explicitly out of scope for this phase and Phase 9 —
|
||||
unchanged status).
|
||||
- [ ] Backup/monitoring in place for the production database holding `payment_transactions`.
|
||||
- [ ] The real Winter Break offering configured with its real price, once decided.
|
||||
|
||||
**Business** (Chase's decisions, not invented here): final price, refund/cancellation policy,
|
||||
camp dates, venue, capacity — all still open, tracked in `docs/
|
||||
WINTER_BREAK_LAUNCH_CHECKLIST.md`.
|
||||
|
||||
**Legal** (unchanged from every prior phase's own list): the actual agreement/waiver text,
|
||||
guardian/minor consent review, privacy/retention requirements — see `docs/
|
||||
STUDENT_GUARDIAN_DATA_MODEL.md`.
|
||||
|
||||
Nothing above is marked complete — none of it has been verified, and this phase did not attempt
|
||||
to invent or shortcut any of it.
|
||||
|
||||
## 7. Bugs found
|
||||
|
||||
None. No genuine defect was exposed because no real Stripe traffic was possible this phase.
|
||||
|
||||
## 8. Security posture — reasoned about, not re-verified against real Stripe
|
||||
|
||||
Every item in the mission's §20 checklist was already true and tested against the deterministic
|
||||
provider in Phase 9 (`e2e-phase9.js`), and nothing about switching providers changes the code
|
||||
path that enforces them (the same `commerce.js`/`webhook-routes.js` code runs regardless of
|
||||
which provider is active — see §3). This phase did not and could not re-verify them against
|
||||
REAL Stripe traffic specifically; that re-verification is exactly what `stripe-integration-test.js`
|
||||
(§4) will perform automatically the moment credentials exist.
|
||||
|
||||
## Final status
|
||||
|
||||
**PHASE 9.1 BLOCKED — NO STRIPE TEST CREDENTIALS AVAILABLE.**
|
||||
|
||||
No real Stripe TEST Checkout, real TEST payment, or real signed Stripe webhook was created or
|
||||
observed this phase — none of the actions requiring them were attempted, per this phase's own
|
||||
explicit instruction never to fake validation. The permanent, opt-in `stripe-integration-test.js`
|
||||
is ready and waiting; provide `STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET` (test mode) and set
|
||||
`RUN_STRIPE_INTEGRATION_TEST=true` to run the real proof.
|
||||
@@ -271,6 +271,12 @@ Real Stripe live-mode validation once credentials exist (`docs/STRIPE_INTEGRATIO
|
||||
text once reviewed; and, informed by real Winter Break Camp commercial operation, whether a
|
||||
waitlist is actually worth building.
|
||||
|
||||
**Phase 9.1 addendum**: real Stripe TEST-mode validation was attempted immediately after this
|
||||
phase and remained blocked on the exact same missing credentials — see `docs/
|
||||
PHASE9_1_STRIPE_TEST_VALIDATION.md`. That phase added a permanent, opt-in integration test
|
||||
(`platform/test/stripe-integration-test.js`, default no-op, refuses live-mode keys) ready to run
|
||||
the real proof the moment `STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET` (test mode) exist.
|
||||
|
||||
---
|
||||
|
||||
## PHASE 9 APPLICATION BUILD COMPLETE / LIVE STRIPE VALIDATION PENDING
|
||||
|
||||
@@ -74,8 +74,13 @@ test).
|
||||
|
||||
## Local testing against real Stripe TEST mode (once credentials exist)
|
||||
|
||||
Not yet exercised in this repo — no Stripe account/credentials have been provided as of this
|
||||
phase. Once they exist:
|
||||
Not yet exercised in this repo — no Stripe account/credentials have been provided as of Phase
|
||||
9.1 either (see `docs/PHASE9_1_STRIPE_TEST_VALIDATION.md` for the exact search performed and
|
||||
what's needed). Once they exist, `platform/test/stripe-integration-test.js` (added in Phase 9.1)
|
||||
automates most of the steps below — set `RUN_STRIPE_INTEGRATION_TEST=true` plus
|
||||
`STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET`/`OWNER_EMAIL`/`OWNER_PASSWORD` and run it; it refuses
|
||||
to run against a live-mode key and makes zero Stripe network calls unless explicitly enabled.
|
||||
Manually, or to understand what that script does:
|
||||
|
||||
1. Create (or use an existing) Stripe account, switch to **Test mode** in the dashboard.
|
||||
2. Copy the test-mode **Secret key** (`sk_test_...`) into `STRIPE_SECRET_KEY`.
|
||||
|
||||
@@ -46,6 +46,12 @@ legal reviewer, or Stripe, not from more code.
|
||||
|
||||
## Payments — pending real Stripe credentials
|
||||
|
||||
Phase 9.1 searched for Stripe TEST credentials specifically to run real validation and found
|
||||
none available anywhere this session could reach — see `docs/
|
||||
PHASE9_1_STRIPE_TEST_VALIDATION.md` for the exact search and what Chase needs to provide. A
|
||||
permanent, opt-in test (`platform/test/stripe-integration-test.js`) is ready to run the real
|
||||
TEST-mode proof the moment `STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET` exist.
|
||||
|
||||
- [ ] Real Stripe account created/available (live mode).
|
||||
- [ ] `STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET` (live values) provisioned into production
|
||||
configuration only — see `docs/STRIPE_INTEGRATION_RUNBOOK.md`'s "Going live" section.
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env node
|
||||
// Phase 9.1 — the permanent, OPT-IN real-Stripe-TEST-mode integration check Phase 9 itself did
|
||||
// not build (Phase 9 tested exclusively against the deterministic TestProvider). This script
|
||||
// makes NO Stripe network call at all unless explicitly enabled, so normal regression runs and
|
||||
// CI stay fully deterministic and independent of Stripe/credential availability.
|
||||
//
|
||||
// Enable with: RUN_STRIPE_INTEGRATION_TEST=true
|
||||
// Requires (same env var names payment-provider.js already uses — never a duplicate config
|
||||
// var): STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET. Both must already be configured on the
|
||||
// RUNNING API process too (PAYMENT_PROVIDER=stripe) — this script is an external verifier, it
|
||||
// does not restart or reconfigure the server.
|
||||
//
|
||||
// Refuses to run against a live-mode secret key (sk_live_...) — TEST MODE ONLY, always. Creates
|
||||
// one disposable, clearly-named TEST offering and TEST registration; attempts to cancel the
|
||||
// offering when done (best-effort cleanup, never fatal if it fails).
|
||||
//
|
||||
// What this script CAN prove by itself, purely via API calls (no browser needed):
|
||||
// - RootNode really creates a real Stripe TEST Checkout Session (verified by retrieving the
|
||||
// session directly from Stripe's own API, not just trusting RootNode's response).
|
||||
// - The amount Stripe actually recorded matches the offering's server-side price exactly,
|
||||
// even when the caller's request body carries a different, tampered amount — proving
|
||||
// server-authoritative pricing against Stripe's OWN object, not just RootNode's database.
|
||||
// - RootNode's real webhook route rejects a garbage/invalid signature cleanly, with no domain
|
||||
// side effect — this does not require an actual completed payment to test honestly.
|
||||
//
|
||||
// What this script deliberately does NOT fake: actually completing Stripe's hosted Checkout
|
||||
// page (filling in a test card) is a real human/browser action (or a Stripe CLI `stripe
|
||||
// trigger` against Stripe's own backend) — there is no supported pure-API way to "complete" a
|
||||
// Checkout Session without one of those, and fabricating a signed "payment succeeded" event for
|
||||
// a session nobody actually paid would be exactly the kind of faked validation Phase 9.1 was
|
||||
// told never to produce. When enabled, this script prints the real checkout_url and pauses,
|
||||
// polling RootNode's own status endpoint for STRIPE_INTEGRATION_TEST_WAIT_SECONDS (default 90)
|
||||
// while a human completes it in a browser (test card 4242 4242 4242 4242) or a parallel
|
||||
// `stripe listen --forward-to <gateway>/api/webhooks/payment` relays Stripe's real event — see
|
||||
// docs/STRIPE_INTEGRATION_RUNBOOK.md and docs/PHASE9_1_STRIPE_TEST_VALIDATION.md.
|
||||
|
||||
const assert = require('assert');
|
||||
|
||||
const RUN = process.env.RUN_STRIPE_INTEGRATION_TEST === 'true';
|
||||
if (!RUN) {
|
||||
console.log('Stripe integration test skipped (RUN_STRIPE_INTEGRATION_TEST is not "true") — no Stripe network traffic, no credentials required.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const GW = process.env.GATEWAY_URL || 'http://localhost:4200';
|
||||
const OWNER_EMAIL = process.env.OWNER_EMAIL;
|
||||
const OWNER_PASSWORD = process.env.OWNER_PASSWORD;
|
||||
const INTEGRATION_KEY = process.env.INTEGRATION_SHARED_SECRET || 'dev-integration-secret-not-for-production';
|
||||
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
|
||||
const WAIT_SECONDS = Number(process.env.STRIPE_INTEGRATION_TEST_WAIT_SECONDS) || 90;
|
||||
|
||||
if (!OWNER_EMAIL || !OWNER_PASSWORD) {
|
||||
console.error('Set OWNER_EMAIL and OWNER_PASSWORD (bootstrap one with platform/api/bootstrap-owner.js).');
|
||||
process.exit(1);
|
||||
}
|
||||
if (!STRIPE_SECRET_KEY) {
|
||||
console.error('RUN_STRIPE_INTEGRATION_TEST=true requires STRIPE_SECRET_KEY (the same variable payment-provider.js reads) so this script can independently verify what Stripe actually recorded.');
|
||||
process.exit(1);
|
||||
}
|
||||
if (STRIPE_SECRET_KEY.startsWith('sk_live_')) {
|
||||
console.error('REFUSING TO RUN: STRIPE_SECRET_KEY looks like a LIVE-mode key (sk_live_...). This script is TEST MODE ONLY. Use an sk_test_ key.');
|
||||
process.exit(1);
|
||||
}
|
||||
if (!STRIPE_SECRET_KEY.startsWith('sk_test_')) {
|
||||
console.error('REFUSING TO RUN: STRIPE_SECRET_KEY does not look like a Stripe test key (expected sk_test_...). Refusing out of caution.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const Stripe = require('../api/node_modules/stripe');
|
||||
const stripe = new Stripe(STRIPE_SECRET_KEY);
|
||||
const { makeSession } = require('./session-helper');
|
||||
|
||||
const RUN_ID = Date.now();
|
||||
const PROGRAM = 'root-kubernetes-foundations';
|
||||
const TEST_PRICE_CENTS = 100; // $1.00 — small, non-zero, clearly a TEST amount
|
||||
const TAMPERED_AMOUNT_CENTS = 1; // what a malicious caller will try to substitute
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
async function test(name, fn) {
|
||||
try { await fn(); console.log(` ok — ${name}`); passed++; }
|
||||
catch (e) { console.error(` FAIL — ${name}\n ${e.stack || e.message}`); failed++; }
|
||||
}
|
||||
async function integrationPost(path, body) {
|
||||
const r = await fetch(`${GW}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-RootNode-Integration-Key': INTEGRATION_KEY }, body: JSON.stringify(body) });
|
||||
return { status: r.status, body: await r.json().catch(() => ({})) };
|
||||
}
|
||||
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
||||
|
||||
async function main() {
|
||||
console.log('Phase 9.1 real-Stripe-TEST-mode integration check (opt-in, real Stripe API calls, TEST MODE ONLY)\n');
|
||||
|
||||
const owner = makeSession(GW);
|
||||
const offeringId = `stripe-it-${RUN_ID}`, slug = `stripe-integration-test-${RUN_ID}`;
|
||||
const agreementId = `stripe-it-agreement-${RUN_ID}`;
|
||||
let registrationId, sessionId, checkoutUrl;
|
||||
|
||||
await test('owner logs in', async () => {
|
||||
const r = await owner.request('POST', '/api/auth/instructor/login', { email: OWNER_EMAIL, password: OWNER_PASSWORD });
|
||||
assert.equal(r.status, 200, JSON.stringify(r.body));
|
||||
});
|
||||
|
||||
await test('a disposable TEST offering is created with a real, small, non-zero TEST price', async () => {
|
||||
const create = await owner.request('POST', '/api/admin/offerings', {
|
||||
offeringId, slug, name: `[TEST] Stripe Integration Check ${RUN_ID}`,
|
||||
programId: PROGRAM, deliveryMode: 'self_paced',
|
||||
priceAmount: TEST_PRICE_CENTS, priceCurrency: 'usd',
|
||||
});
|
||||
assert.equal(create.status, 201, JSON.stringify(create.body));
|
||||
const agreement = await owner.request('POST', '/api/admin/agreements', { agreementId, title: '[TEST] Stripe Integration Waiver', bodySummary: 'Disposable test agreement — Phase 9.1.' });
|
||||
assert.equal(agreement.status, 201, JSON.stringify(agreement.body));
|
||||
const link = await owner.request('POST', `/api/admin/offerings/${offeringId}/agreement-requirements`, { agreement_id: agreementId });
|
||||
assert.equal(link.status, 201, JSON.stringify(link.body));
|
||||
await owner.request('POST', `/api/admin/offerings/${offeringId}/transition`, { status: 'published' });
|
||||
const open = await owner.request('POST', `/api/admin/offerings/${offeringId}/transition`, { status: 'enrollment_open' });
|
||||
assert.equal(open.status, 200);
|
||||
});
|
||||
|
||||
await test('a real registration is created through the public integration contract', async () => {
|
||||
const reg = await integrationPost('/api/integrations/registrations', {
|
||||
event_id: `stripe-it-reg-${RUN_ID}`, offering_slug: slug,
|
||||
registrant: { display_name: 'Stripe Integration Tester', email: `stripe-it-${RUN_ID}@example.com` },
|
||||
});
|
||||
assert.equal(reg.status, 201, JSON.stringify(reg.body));
|
||||
registrationId = reg.body.registration_id;
|
||||
});
|
||||
|
||||
await test('checkout is rejected before the required agreement is accepted', async () => {
|
||||
const attempt = await integrationPost(`/api/integrations/registrations/${registrationId}/checkout`, { success_url: 'https://example.com/s', cancel_url: 'https://example.com/c' });
|
||||
assert.equal(attempt.status, 409);
|
||||
assert.equal(attempt.body.error, 'AGREEMENTS_INCOMPLETE');
|
||||
});
|
||||
|
||||
await test('accepting the agreement is persisted with a version', async () => {
|
||||
const accept = await integrationPost(`/api/integrations/registrations/${registrationId}/agreements`, { agreement_id: agreementId });
|
||||
assert.equal(accept.status, 201, JSON.stringify(accept.body));
|
||||
assert.equal(accept.body.accepted_version, 1);
|
||||
});
|
||||
|
||||
await test('RootNode creates a REAL Stripe TEST Checkout Session, and a tampered caller-supplied amount is ignored', async () => {
|
||||
// amount/currency here are not real fields on this endpoint's contract (Step 5's own
|
||||
// "the browser never supplies a price" design) — sending them anyway proves they're inert.
|
||||
const checkout = await integrationPost(`/api/integrations/registrations/${registrationId}/checkout`, {
|
||||
success_url: 'https://example.com/success', cancel_url: 'https://example.com/cancel',
|
||||
amount: TAMPERED_AMOUNT_CENTS, currency: 'eur',
|
||||
});
|
||||
assert.equal(checkout.status, 201, JSON.stringify(checkout.body));
|
||||
checkoutUrl = checkout.body.checkout_url;
|
||||
assert.ok(checkoutUrl.startsWith('https://checkout.stripe.com/'), `expected a real Stripe-hosted checkout URL, got: ${checkoutUrl}`);
|
||||
|
||||
// Independently verify against STRIPE'S OWN API — never trust RootNode's own echo of what
|
||||
// it thinks it sent.
|
||||
const tx = await owner.request('GET', `/api/admin/registrations/${registrationId}/payments`);
|
||||
sessionId = tx.body[0].provider_reference;
|
||||
const session = await stripe.checkout.sessions.retrieve(sessionId);
|
||||
assert.equal(session.mode, 'payment');
|
||||
assert.equal(session.amount_total, TEST_PRICE_CENTS, 'Stripe must have recorded the OFFERING price, never the tampered caller amount');
|
||||
assert.equal(session.currency, 'usd', 'Stripe must have recorded the OFFERING currency, never the tampered caller currency');
|
||||
assert.equal(session.livemode, false, 'this must be a TEST-mode session — refusing to proceed otherwise');
|
||||
});
|
||||
|
||||
await test('an invalid webhook signature is rejected with no domain side effect', async () => {
|
||||
const before = await owner.request('GET', `/api/admin/registrations/${registrationId}`);
|
||||
const r = await fetch(`${GW}/api/webhooks/payment`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', 'Stripe-Signature': 'invalid-signature-not-real' },
|
||||
body: JSON.stringify({ id: 'evt_fake', type: 'checkout.session.completed', data: { object: { id: sessionId } } }),
|
||||
});
|
||||
assert.equal(r.status, 400);
|
||||
const after = await owner.request('GET', `/api/admin/registrations/${registrationId}`);
|
||||
assert.equal(after.body.status, before.body.status, 'an invalid signature must never move the registration forward');
|
||||
});
|
||||
|
||||
console.log(`\n${'─'.repeat(78)}`);
|
||||
console.log('Everything above is fully automated and required no human/browser step.');
|
||||
console.log('What remains needs a real completed Stripe Checkout — this is a genuine human or');
|
||||
console.log('Stripe-CLI action, never simulated by this script (see the file header comment and');
|
||||
console.log('docs/PHASE9_1_STRIPE_TEST_VALIDATION.md):\n');
|
||||
console.log(` 1. Open this REAL Stripe TEST checkout page in a browser:\n ${checkoutUrl}`);
|
||||
console.log(' 2. Pay with Stripe\'s official test card: 4242 4242 4242 4242, any future expiry, any CVC/ZIP.');
|
||||
console.log(` 3. Make sure Stripe can reach your webhook — e.g. \`stripe listen --forward-to ${GW}/api/webhooks/payment\`.`);
|
||||
console.log(`\nPolling RootNode's own status endpoint for up to ${WAIT_SECONDS}s for a real webhook-confirmed result...\n`);
|
||||
|
||||
const deadline = Date.now() + WAIT_SECONDS * 1000;
|
||||
let finalStatus = null;
|
||||
while (Date.now() < deadline) {
|
||||
const r = await fetch(`${GW}/api/integrations/registrations/${registrationId}/status`, { headers: { 'X-RootNode-Integration-Key': INTEGRATION_KEY } });
|
||||
const status = await r.json();
|
||||
if (status.payment_status === 'paid' || status.status === 'enrollment_activated') { finalStatus = status; break; }
|
||||
await sleep(3000);
|
||||
}
|
||||
|
||||
if (finalStatus) {
|
||||
console.log(` ok — a REAL Stripe TEST payment was completed and confirmed by a REAL signed webhook: ${JSON.stringify(finalStatus)}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.log(' (not completed within the wait window — this is expected in an unattended/CI run; rerun interactively to complete the manual step, or increase STRIPE_INTEGRATION_TEST_WAIT_SECONDS)');
|
||||
}
|
||||
|
||||
// Best-effort cleanup — never fatal.
|
||||
try { await owner.request('POST', `/api/admin/offerings/${offeringId}/transition`, { status: 'cancelled' }); } catch { /* ponytail: best-effort cleanup only */ }
|
||||
|
||||
console.log(`\n${passed} passed, ${failed} failed${finalStatus ? '' : ' (real-payment step not completed this run)'}`);
|
||||
process.exit(failed ? 1 : 0);
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error(e); process.exit(1); });
|
||||
Reference in New Issue
Block a user