Skip to content

Commit 77f1be5

Browse files
committed
fix(tests): gate pair specs via ConfigPage so WAF-fronted envs route correctly
Because: Stage sits behind Fastly Next-Gen WAF, which serves a JavaScript "Client Challenge" interstitial to plain fetch() calls. The pair spec gate read the content server with fetch(), failed to match the fxa-config meta tag, and returned false. Backbone pair specs then ran against stage (which serves React) and failed, while the React specs skipped. This commit: - Rewrites isPairRoutesReact to take (browser, target) and reuse the existing ConfigPage helper, which opens a real Playwright page so the WAF challenge JS executes before the meta tag is read. - Applies the WAF bypass header (CI_WAF_TOKEN) to the context it creates so the config read works in CI the same way fixture-created contexts do. - Moves the gate from beforeEach to beforeAll in all four pair specs since the rollout is env-stable for the lifetime of a worker, avoiding a WAF challenge on every test. - Captures the result in a worker-scoped variable in pairingFlowiOS so both inline call sites reuse the single lookup.
1 parent ec1df80 commit 77f1be5

5 files changed

Lines changed: 60 additions & 47 deletions

File tree

packages/functional-tests/lib/pairing-helpers.ts

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
*/
1515

1616
import crypto from 'crypto';
17-
import { expect, Page } from '@playwright/test';
17+
import { Browser, expect, Page } from '@playwright/test';
18+
import { ConfigPage } from '../pages/config';
19+
import { BaseTarget } from './targets/base';
1820
import { MarionetteClient } from './marionette';
1921
import {
2022
PAIRING_CLIENT_ID,
@@ -26,22 +28,39 @@ import {
2628
import { getTotpCode } from './totp';
2729

2830
/**
29-
* Fetch the content server's fxa-config and check whether React pairing
30-
* routes are enabled (showReactApp.pairRoutes). When enabled, the Backbone
31-
* /pair/* routes are deregistered and only React (fxa-settings) serves them.
31+
* Check whether React pairing routes are enabled (showReactApp.pairRoutes).
32+
* When enabled, the Backbone /pair/* routes are deregistered and only React
33+
* (fxa-settings) serves them.
34+
*
35+
* Reuses the shared ConfigPage helper, which opens a real Playwright page and
36+
* reads the fxa-config meta tag. This matters for stage and production,
37+
* which are behind Fastly's Next-Gen WAF: a plain fetch() receives the
38+
* JavaScript "Client Challenge" interstitial instead of the real HTML. A
39+
* browser page executes the challenge and then renders the real page with
40+
* the meta tag.
41+
*
42+
* Callers should invoke this from `test.beforeAll` so it runs once per
43+
* worker; pair-route rollout is stable for the lifetime of a test run.
3244
*/
3345
export async function isPairRoutesReact(
34-
contentServerUrl: string
46+
browser: Browser,
47+
target: BaseTarget
3548
): Promise<boolean> {
49+
// Mirror playwright.config.ts's `use.extraHTTPHeaders` so requests made from
50+
// this helper also carry the WAF bypass token in CI. Without this, the WAF
51+
// serves its JS interstitial and the fxa-config meta tag never renders.
52+
const extraHTTPHeaders: Record<string, string> = {};
53+
target.ciHeader?.forEach((value, key) => {
54+
extraHTTPHeaders[key] = value;
55+
});
56+
const context = await browser.newContext({ extraHTTPHeaders });
57+
const page = await context.newPage();
3658
try {
37-
const resp = await fetch(contentServerUrl);
38-
const html = await resp.text();
39-
const match = html.match(/name="fxa-config" content="([^"]+)"/);
40-
if (!match) return false;
41-
const config = JSON.parse(decodeURIComponent(match[1]));
42-
return config.showReactApp?.pairRoutes === true;
43-
} catch {
44-
return false;
59+
const configPage = new ConfigPage(page, target);
60+
const config = await configPage.getConfig();
61+
return config?.showReactApp?.pairRoutes === true;
62+
} finally {
63+
await context.close();
4564
}
4665
}
4766

packages/functional-tests/tests/pairing/pairingFlow.spec.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,13 @@ test.describe('severity-2 #smoke', () => {
8989
// because content-server reads showReactApp.pairRoutes at startup (see
9090
// add-routes.js) and pair routes have fullProdRollout: true, so a single
9191
// server instance only serves one stack.
92-
test.beforeEach(async ({ target }, testInfo) => {
93-
// Marionette launches a local Firefox binary configured to talk to
94-
// the target environment via about:config prefs.
95-
const isReact = await isPairRoutesReact(target.contentServerUrl);
96-
if (!isReact) {
97-
testInfo.skip(
98-
true,
99-
'React pair specs require showReactApp.pairRoutes=true'
100-
);
101-
}
92+
// Runs once per worker. Pair-route rollout is env-stable.
93+
test.beforeAll(async ({ browser, target }) => {
94+
const isReact = await isPairRoutesReact(browser, target);
95+
test.skip(
96+
!isReact,
97+
'React pair specs require showReactApp.pairRoutes=true'
98+
);
10299
});
103100

104101
test('authority generates QR URL and supplicant connects via channel', async ({

packages/functional-tests/tests/pairing/pairingFlowBackbone.spec.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -122,14 +122,13 @@ test.describe('severity-2 #smoke', () => {
122122
test.describe.serial('Backbone pairing flow', () => {
123123
// Mutually exclusive with pairingFlow.spec.ts — only one of these suites
124124
// runs at a time, gated by the server's showReactApp.pairRoutes config.
125-
test.beforeEach(async ({ target }, testInfo) => {
126-
const isReact = await isPairRoutesReact(target.contentServerUrl);
127-
if (isReact) {
128-
testInfo.skip(
129-
true,
130-
'Backbone pair specs require showReactApp.pairRoutes=false'
131-
);
132-
}
125+
// Runs once per worker. Pair-route rollout is env-stable.
126+
test.beforeAll(async ({ browser, target }) => {
127+
const isReact = await isPairRoutesReact(browser, target);
128+
test.skip(
129+
isReact,
130+
'Backbone pair specs require showReactApp.pairRoutes=false'
131+
);
133132
});
134133

135134
test('authority generates QR URL and supplicant connects via Backbone templates', async ({

packages/functional-tests/tests/pairing/pairingFlowBackboneNegative.spec.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,13 @@ test.setTimeout(120_000);
3838

3939
test.describe('severity-2 #smoke', () => {
4040
test.describe.serial('Backbone pairing flow — negative paths', () => {
41-
test.beforeEach(async ({ target }, testInfo) => {
42-
const isReact = await isPairRoutesReact(target.contentServerUrl);
43-
if (isReact) {
44-
testInfo.skip(
45-
true,
46-
'Backbone pair specs require showReactApp.pairRoutes=false'
47-
);
48-
}
41+
// Runs once per worker. Pair-route rollout is env-stable.
42+
test.beforeAll(async ({ browser, target }) => {
43+
const isReact = await isPairRoutesReact(browser, target);
44+
test.skip(
45+
isReact,
46+
'Backbone pair specs require showReactApp.pairRoutes=false'
47+
);
4948
});
5049

5150
test('supplicant cancels on /pair/supp/allow and lands on /pair/failure', async ({

packages/functional-tests/tests/pairing/pairingFlowNegative.spec.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,13 @@ test.setTimeout(120_000);
3636

3737
test.describe('severity-2 #smoke', () => {
3838
test.describe.serial('Firefox pairing flow — negative paths', () => {
39-
test.beforeEach(async ({ target }, testInfo) => {
40-
const isReact = await isPairRoutesReact(target.contentServerUrl);
41-
if (!isReact) {
42-
testInfo.skip(
43-
true,
44-
'React pair specs require showReactApp.pairRoutes=true'
45-
);
46-
}
39+
// Runs once per worker. Pair-route rollout is env-stable.
40+
test.beforeAll(async ({ browser, target }) => {
41+
const isReact = await isPairRoutesReact(browser, target);
42+
test.skip(
43+
!isReact,
44+
'React pair specs require showReactApp.pairRoutes=true'
45+
);
4746
});
4847

4948
test('supplicant cancels on /pair/supp/allow and lands on /pair/failure', async ({

0 commit comments

Comments
 (0)