Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1227,11 +1227,11 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

# Asserts the fake Stripe server against responses captured from Stripe, and
# that it refuses the requests Stripe refuses. Needs no Ghost, no Docker and
# no browser, so it does not belong in the e2e matrix that waits on the image.
# Needs no Ghost, no Docker and no browser, so it does not belong in the e2e
# matrix that waits on the image. Run through nx so the target pulls in the
# build it declares.
- name: Check Stripe fixtures
run: pnpm --filter @tryghost/e2e test:fixtures
run: pnpm nx run @tryghost/e2e:test:fixtures

- uses: tryghost/actions/actions/slack-build@e7a401946f91165a6426290705f501a377ec1533 # main
if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main'
Expand Down
249 changes: 249 additions & 0 deletions e2e/helpers/services/stripe/allowed-countries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
/**
* The countries Stripe Checkout accepts in `shipping_address_collection`, measured against
* the live API by `scripts/probe-stripe-constraints.ts`.
*
* Kept here rather than imported from Ghost, because this package models Stripe and Ghost
* is what it is modelling: a fake that shared the product's list could never catch the
* product offering a country Stripe refuses. The two are held together by a test in
* `ghost/core` that reads this file, so they cannot drift quietly.
*/
export const STRIPE_ALLOWED_COUNTRIES = [
'AC',
'AD',
'AE',
'AF',
'AG',
'AI',
'AL',
'AM',
'AO',
'AQ',
'AR',
'AT',
'AU',
'AW',
'AX',
'AZ',
'BA',
'BB',
'BD',
'BE',
'BF',
'BG',
'BH',
'BI',
'BJ',
'BL',
'BM',
'BN',
'BO',
'BQ',
'BR',
'BS',
'BT',
'BV',
'BW',
'BY',
'BZ',
'CA',
'CD',
'CF',
'CG',
'CH',
'CI',
'CK',
'CL',
'CM',
'CN',
'CO',
'CR',
'CV',
'CW',
'CY',
'CZ',
'DE',
'DJ',
'DK',
'DM',
'DO',
'DZ',
'EC',
'EE',
'EG',
'EH',
'ER',
'ES',
'ET',
'FI',
'FJ',
'FK',
'FO',
'FR',
'GA',
'GB',
'GD',
'GE',
'GF',
'GG',
'GH',
'GI',
'GL',
'GM',
'GN',
'GP',
'GQ',
'GR',
'GS',
'GT',
'GU',
'GW',
'GY',
'HK',
'HN',
'HR',
'HT',
'HU',
'ID',
'IE',
'IL',
'IM',
'IN',
'IO',
'IQ',
'IS',
'IT',
'JE',
'JM',
'JO',
'JP',
'KE',
'KG',
'KH',
'KI',
'KM',
'KN',
'KR',
'KW',
'KY',
'KZ',
'LA',
'LB',
'LC',
'LI',
'LK',
'LR',
'LS',
'LT',
'LU',
'LV',
'LY',
'MA',
'MC',
'MD',
'ME',
'MF',
'MG',
'MK',
'ML',
'MM',
'MN',
'MO',
'MQ',
'MR',
'MS',
'MT',
'MU',
'MV',
'MW',
'MX',
'MY',
'MZ',
'NA',
'NC',
'NE',
'NG',
'NI',
'NL',
'NO',
'NP',
'NR',
'NU',
'NZ',
'OM',
'PA',
'PE',
'PF',
'PG',
'PH',
'PK',
'PL',
'PM',
'PN',
'PR',
'PS',
'PT',
'PY',
'QA',
'RE',
'RO',
'RS',
'RU',
'RW',
'SA',
'SB',
'SC',
'SD',
'SE',
'SG',
'SH',
'SI',
'SJ',
'SK',
'SL',
'SM',
'SN',
'SO',
'SR',
'SS',
'ST',
'SV',
'SX',
'SZ',
'TA',
'TC',
'TD',
'TF',
'TG',
'TH',
'TJ',
'TK',
'TL',
'TM',
'TN',
'TO',
'TR',
'TT',
'TV',
'TW',
'TZ',
'UA',
'UG',
'US',
'UY',
'UZ',
'VA',
'VC',
'VE',
'VG',
'VN',
'VU',
'WF',
'WS',
'XK',
'YE',
'YT',
'ZA',
'ZM',
'ZW',
'ZZ',
] as const;
28 changes: 28 additions & 0 deletions e2e/helpers/services/stripe/fake-stripe-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
buildPrice,
buildProduct,
} from './builders';
import { STRIPE_ALLOWED_COUNTRIES } from './allowed-countries';
import {
renderFakeCheckoutPage,
renderFakeDonationCheckoutPage,
Expand All @@ -31,6 +32,19 @@ import {
const MAX_CUSTOM_FIELDS = 3;
const MAX_CUSTOM_FIELD_LABEL_LENGTH = 50;

// What this catches is a request reaching Stripe with a country nothing validated — a
// configuration saved before the rule existed, say — which would fail the session create
// and take the sale with it.
const ALLOWED_COUNTRIES = new Set<string>(STRIPE_ALLOWED_COUNTRIES);

// Stripe names the offending element and then lists every code it will take.
function allowedCountriesMessage(index: number): string {
const codes = [...STRIPE_ALLOWED_COUNTRIES];
const last = codes[codes.length - 1];
const listed = `${codes.slice(0, -1).join(', ')}, or ${last}`;
return `Invalid shipping_address_collection[allowed_countries][${index}]: must be one of ${listed}`;
}

export class FakeStripeServer extends FakeServer {
private readonly products: Map<string, StripeProduct> = new Map();
private readonly prices: Map<string, StripePrice> = new Map();
Expand Down Expand Up @@ -738,6 +752,20 @@ export class FakeStripeServer extends FakeServer {
// Read against `true` rather than for truthiness: form decoding delivers the flag as
// the string `"false"`, which is truthy, and refusing on that would refuse a checkout
// that had switched tax collection off.
const rawCountries = (body.shipping_address_collection as { allowed_countries?: unknown })
?.allowed_countries;
const countries = Array.isArray(rawCountries)
? rawCountries
: rawCountries && typeof rawCountries === 'object'
? Object.values(rawCountries)
: [];
const refused = countries.findIndex(
(code) => typeof code !== 'string' || !ALLOWED_COUNTRIES.has(code),
);
if (refused !== -1) {
return allowedCountriesMessage(refused);
}

const taxIdFlag = (body.tax_id_collection as { enabled?: unknown })?.enabled;
const collectsTaxId = taxIdFlag === true || taxIdFlag === 'true';
const mayRename = (body.customer_update as { name?: unknown })?.name === 'auto';
Expand Down
Loading
Loading