Skip to content

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:

  1. 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.
  2. Render the member's real digital card by fetching the server-rendered /Membership/Card page (reached from /wallet) over HTTP with the member's session cookie.
  3. Assert the configured label/value appears in the rendered markup (PassCard-primaryFields / PassCard-secondaryFields etc.).

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 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 from Get-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 PaymentHosted 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.