-
Notifications
You must be signed in to change notification settings - Fork 243
E2E: one-time global auth before test start #7272
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
phyllis-sy-wu
wants to merge
5
commits into
main
Choose a base branch
from
psyw-0413-E2E-one-time-auth
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
064dac3
E2E: one-time global auth before test start
phyllis-sy-wu 5f5c9e9
fix: use URL hostname check instead of substring match for accounts.s…
phyllis-sy-wu ac5f972
replace custom copyDirSync with fs.cpSync
phyllis-sy-wu 474f407
remove redundant blank space
phyllis-sy-wu 1429fe0
use the same directories.root to keep temp files in one place
phyllis-sy-wu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| /** | ||
| * Playwright globalSetup — authenticates once before any workers start. | ||
| * | ||
| * Performs CLI `auth login` with a dedicated temp dir, then stores the | ||
| * path in E2E_AUTH_CONFIG_DIR so each worker can copy the session files | ||
| * into its own isolated XDG dirs. | ||
| */ | ||
|
|
||
| /* eslint-disable no-restricted-imports */ | ||
| import {createIsolatedEnv, directories, executables, globalLog} from './env.js' | ||
| import {CLI_TIMEOUT, BROWSER_TIMEOUT} from './constants.js' | ||
| import {stripAnsi} from '../helpers/strip-ansi.js' | ||
| import {waitForText} from '../helpers/wait-for-text.js' | ||
| import {completeLogin} from '../helpers/browser-login.js' | ||
| import {execa} from 'execa' | ||
| import {chromium} from '@playwright/test' | ||
| import * as path from 'path' | ||
| import * as fs from 'fs' | ||
|
|
||
| function isAccountsShopifyUrl(rawUrl: string): boolean { | ||
| try { | ||
| return new URL(rawUrl).hostname === 'accounts.shopify.com' | ||
| // eslint-disable-next-line no-catch-all/no-catch-all | ||
| } catch { | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| export default async function globalSetup() { | ||
| const email = process.env.E2E_ACCOUNT_EMAIL | ||
| const password = process.env.E2E_ACCOUNT_PASSWORD | ||
|
|
||
| if (!email || !password) return | ||
|
|
||
| const debug = process.env.DEBUG === '1' | ||
| globalLog('auth', 'global setup starting') | ||
|
|
||
| // Create a temp dir for the auth session | ||
| const tmpBase = process.env.E2E_TEMP_DIR ?? path.join(directories.root, '.e2e-tmp') | ||
| fs.mkdirSync(tmpBase, {recursive: true}) | ||
| const {xdgEnv} = createIsolatedEnv(tmpBase) | ||
|
|
||
phyllis-sy-wu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const processEnv: NodeJS.ProcessEnv = { | ||
| ...process.env, | ||
| ...xdgEnv, | ||
| SHOPIFY_RUN_AS_USER: '0', | ||
| NODE_OPTIONS: '', | ||
| CI: '1', | ||
| SHOPIFY_CLI_1P_DEV: undefined, | ||
| SHOPIFY_FLAG_CLIENT_ID: undefined, | ||
| } | ||
|
|
||
| // Clear any existing session | ||
| await execa('node', [executables.cli, 'auth', 'logout'], { | ||
| env: processEnv, | ||
| reject: false, | ||
| }) | ||
|
|
||
| // Spawn auth login via PTY | ||
| const nodePty = await import('node-pty') | ||
| const spawnEnv: {[key: string]: string} = {} | ||
| for (const [key, value] of Object.entries(processEnv)) { | ||
| if (value !== undefined) spawnEnv[key] = value | ||
| } | ||
| spawnEnv.CI = '' | ||
| spawnEnv.CODESPACES = 'true' | ||
|
|
||
| const ptyProcess = nodePty.spawn('node', [executables.cli, 'auth', 'login'], { | ||
| name: 'xterm-color', | ||
| cols: 120, | ||
| rows: 30, | ||
| env: spawnEnv, | ||
| }) | ||
|
|
||
| let output = '' | ||
| ptyProcess.onData((data: string) => { | ||
| output += data | ||
| if (debug) process.stdout.write(data) | ||
| }) | ||
|
|
||
| await waitForText(() => output, 'Open this link to start the auth process', CLI_TIMEOUT.short) | ||
|
|
||
| const stripped = stripAnsi(output) | ||
| const urlMatch = stripped.match(/https:\/\/accounts\.shopify\.com\S+/) | ||
| if (!urlMatch) { | ||
| throw new Error(`[e2e] global-auth: could not find login URL in output:\n${stripped}`) | ||
| } | ||
|
|
||
| // Complete login in a headless browser | ||
| const browser = await chromium.launch({headless: !process.env.E2E_HEADED}) | ||
| const context = await browser.newContext({ | ||
| extraHTTPHeaders: { | ||
| 'X-Shopify-Loadtest-Bf8d22e7-120e-4b5b-906c-39ca9d5499a9': 'true', | ||
| }, | ||
| }) | ||
| const page = await context.newPage() | ||
|
|
||
| await completeLogin(page, urlMatch[0], email, password) | ||
|
|
||
| await waitForText(() => output, 'Logged in', BROWSER_TIMEOUT.max) | ||
| try { | ||
| ptyProcess.kill() | ||
| // eslint-disable-next-line no-catch-all/no-catch-all | ||
| } catch (_error) { | ||
| // Process may already be dead | ||
| } | ||
|
|
||
| // Visit admin.shopify.com and dev.shopify.com to establish session cookies | ||
| // (completeLogin only authenticates on accounts.shopify.com) | ||
| const orgId = (process.env.E2E_ORG_ID ?? '').trim() | ||
| if (orgId) { | ||
| // Establish admin.shopify.com cookies | ||
| await page.goto('https://admin.shopify.com/', {waitUntil: 'domcontentloaded'}) | ||
| await page.waitForTimeout(BROWSER_TIMEOUT.medium) | ||
|
|
||
| // Handle account picker if shown | ||
| if (isAccountsShopifyUrl(page.url())) { | ||
| const accountButton = page.locator(`text=${email}`).first() | ||
| if (await accountButton.isVisible({timeout: BROWSER_TIMEOUT.long}).catch(() => false)) { | ||
| await accountButton.click() | ||
| await page.waitForTimeout(BROWSER_TIMEOUT.medium) | ||
| } | ||
| } | ||
|
|
||
| // Establish dev.shopify.com cookies | ||
| await page.goto(`https://dev.shopify.com/dashboard/${orgId}/apps`, {waitUntil: 'domcontentloaded'}) | ||
| await page.waitForTimeout(BROWSER_TIMEOUT.medium) | ||
|
|
||
| if (isAccountsShopifyUrl(page.url())) { | ||
| const accountButton = page.locator(`text=${email}`).first() | ||
| if (await accountButton.isVisible({timeout: BROWSER_TIMEOUT.long}).catch(() => false)) { | ||
| await accountButton.click() | ||
| await page.waitForTimeout(BROWSER_TIMEOUT.medium) | ||
| } | ||
| } | ||
|
|
||
| globalLog('auth', 'browser sessions established for admin + dev dashboard') | ||
| } | ||
|
|
||
| // Save browser cookies/storage so workers can reuse the session | ||
| // Now includes cookies for both accounts.shopify.com AND admin.shopify.com | ||
| const storageStatePath = path.join(tmpBase, 'browser-storage-state.json') | ||
| await context.storageState({path: storageStatePath}) | ||
| await browser.close() | ||
phyllis-sy-wu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Store paths so workers can copy CLI auth + load browser state | ||
| /* eslint-disable require-atomic-updates */ | ||
| process.env.E2E_AUTH_CONFIG_DIR = xdgEnv.XDG_CONFIG_HOME | ||
| process.env.E2E_AUTH_DATA_DIR = xdgEnv.XDG_DATA_HOME | ||
| process.env.E2E_AUTH_STATE_DIR = xdgEnv.XDG_STATE_HOME | ||
| process.env.E2E_AUTH_CACHE_DIR = xdgEnv.XDG_CACHE_HOME | ||
| process.env.E2E_BROWSER_STATE_PATH = storageStatePath | ||
| /* eslint-enable require-atomic-updates */ | ||
|
|
||
| globalLog('auth', `global setup done, config at ${xdgEnv.XDG_CONFIG_HOME}`) | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.