Digital card automation¶
How to test the membership digital card (PR #13988) programmatically — the configurable per-slot fields (name, member no, plan, expiration, status, custom org attribute…) and the Apple/Google wallet passes that render from them.
Why not just drive the admin screen¶
The field config lives on Membership → Settings → Digital Card in the
AngularJS manager SPA. Driving that SPA headless is unreliable: it holds its API
token in memory, acquired across a cross-app login redirect, and the headless
daemon repeatedly lands on about:blank after the redirect — the screen never
stabilises. Chasing it wastes time and produces flaky tests.
The card is server-rendered, so we test it the way the server actually produces it, with no SPA scripting:
- Set a known field config via the admin REST API
(
POST /v2/organisations/{orgId}/membership/cardSettings), authenticated with a Bearer token from/account/gettokenbycookie+ the owner's session cookie. - Render the member's real digital card by fetching the server-rendered
/Membership/Cardpage (reached from/wallet) over HTTP with the member's session cookie. - Assert the configured label/value appears in the rendered markup
(
PassCard-primaryFields/PassCard-secondaryFieldsetc.).
The one browser-dependent step — getting the member subscribed so a card
exists — reuses subscribed-member.ps1
(Stripe test card, Hosted/Embedded auto-detect). Everything after that is plain
HTTP and deterministic.
The cookie → HTTP bridge¶
The auth cookie (Raklet.Login, httpOnly on .raklet.org) can't be read from
document.cookie, but the gstack browse daemon exposes the full jar via
browse cookies. helpers/digital-card.ps1
turns that jar into an HTTP Cookie header (Get-BrowseCookieHeader), so any
authenticated server page or API is reachable with Invoke-WebRequest /
Invoke-RestMethod — bypassing the flaky in-browser page entirely. This bridge
is reusable for any server-rendered authed page that resists SPA driving.
Auth quirk. The V2 membership endpoints (
cardSettings,subscriptions) authorise with the forms-auth cookie, not the SPA's Bearer token (Bearer returns 401 there). The organisation-list endpoints want the Bearer token. The helpers take the cookie everywhere and derive the Bearer internally where needed, so callers only ever pass the cookie fromGet-BrowseCookieHeader.
Helpers (browse-tests/helpers/digital-card.ps1)¶
| Function | Purpose |
|---|---|
Get-BrowseCookieHeader |
Export the daemon's cookie jar for a host as a Cookie header |
Get-OrganisationIdForPermalink |
Resolve an org Id from its permalink (cookie in, Bearer derived internally) |
Get-DigitalCardSettings / Set-DigitalCardField / Save-DigitalCardSettings |
Read / set a slot / restore the card settings via the admin API (cookie-auth) |
Get-SubscriptionIdForMember |
Resolve a member's active subscription Id (the CustomMembership.Id the card is keyed on) |
Get-MemberDigitalCardHtml |
Fetch a member's server-rendered card HTML (pass -SubscriptionId to hit /Membership/Card directly) |
Invoke-MembershipSubscribeWizard |
Drive gercek's multi-step subscribe (Start Membership → /checkout/pay → Hosted Checkout, Stripe test card) |
Assert-CardContains |
Assert a label/value renders on the card |
Subscribe state-setup¶
gercek's membership subscribe is a multi-step wizard, not single-page checkout:
/subscription/create?planId=… → Start Membership (creates the membership +
invoice, lands on /checkout/pay) → Proceed to Payment → Hosted Checkout
(checkout.stripe.com) → complete with the test card. Invoke-MembershipSubscribeWizard
drives all of it.
After the card is submitted, Stripe activates the membership via a webhook — so
on a public env (.raklet.net) the subscription becomes active on its own and the
test polls Get-SubscriptionIdForMember until it appears. On local the webhook
can't reach the box; the activation otherwise happens on the success redirect to
/checkout/paysuccess?id={session}&… (which PaySuccess processes synchronously),
but the headless daemon doesn't follow that redirect — so a local run needs the
member already subscribed, or webhook forwarding. The verification half (config →
render → assert) is fully deterministic regardless.
The test¶
browse-tests/payment-regression/12-membership-digital-card-fields.ps1:
ensures the member is subscribed (subscribe wizard if needed), sets a unique marker
label on the Secondary slot as the owner, asserts the marker renders on the
member's card (fetched by subscriptionId), then restores the previous settings
(non-destructive). Verified live on gercek.raklet.org 2026-06-14. Run it through
the suite runner:
. scripts/ci/load-browse-creds.ps1 # populates BROWSE_TEST_* from local.settings.json
browse-tests/run.ps1 -Suite payment-regression # or run the single .ps1 directly
Wallet passes¶
The Apple/Google pass field layout (which configured fields land in which
pass region) is covered by unit tests — AppleWalletPassFieldLayoutTests and
GoogleWalletMembershipCardServiceTests — because a generated .pkpass / Google
Wallet object isn't meaningfully inspectable from a headless browser. The HTTP
harness above covers the on-card rendering; the unit tests cover the pass
projection. Together they exercise the resolver end to end without the SPA.