Skip to content

Commit 85c69bc

Browse files
Separate authentication from test setup
1 parent 6fb1287 commit 85c69bc

9 files changed

Lines changed: 139 additions & 68 deletions

File tree

.gitlab-ci.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,6 @@ e2e:
254254
- yarn build:apps
255255
# Browsers are pre-installed in the CI image. If playwright (current or pinned 1.40.1)
256256
# is upgraded without rebuilding the image, this job will crash — rebuild the image to fix it.
257-
- if [ "$BROWSER" = "chromium" ]; then node scripts/salesforce-lwc-app.ts auth; fi
258257
- FORCE_COLOR=1 PW_BROWSER=$BROWSER yarn test:e2e --project=$BROWSER
259258
after_script:
260259
- node ./scripts/test/export-test-result.ts e2e

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
"build": "yarn workspaces foreach --all --parallel --topological-dev run build",
1616
"build:bundle": "yarn workspaces foreach --all --parallel run build:bundle",
1717
"build:apps": "node scripts/build/build-test-apps.ts",
18-
"salesforce:auth": "node scripts/salesforce-lwc-app.ts auth",
1918
"salesforce:deploy-app": "yarn workspace @datadog/browser-rum-slim build:bundle && node scripts/salesforce-lwc-app.ts deploy-app",
2019
"salesforce:get-url": "node scripts/salesforce-lwc-app.ts get-url",
2120
"build:docs:json": "typedoc --logLevel Verbose --json ./generated-docs.json",

scripts/lib/buildSalesforceLwcUrl.ts

Lines changed: 0 additions & 42 deletions
This file was deleted.

scripts/salesforce-lwc-app.ts

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import { parseArgs } from 'node:util'
77
import { printLog, runMain } from './lib/executionUtils.ts'
88
import { getSfLwcClientId, getSfLwcInstanceUrl, getSfLwcJwtPrivateKey, getSfLwcUsername } from './lib/secrets.ts'
99
import { command } from './lib/command.ts'
10-
import { buildSalesforceLwcUrl } from './lib/buildSalesforceLwcUrl.ts'
1110

1211
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
1312
const salesforceAppDir = resolve(repositoryRoot, 'test/apps/sf-lwc-app')
13+
const salesforceHomePath = '/lightning/app/c__SF_LWC_App/page/home'
1414
const defaultTargetOrg = 'sf-lwc-ci'
1515

1616
runMain(() => {
@@ -21,9 +21,6 @@ runMain(() => {
2121
type: 'boolean',
2222
short: 'h',
2323
},
24-
proxy: {
25-
type: 'string',
26-
},
2724
},
2825
})
2926

@@ -32,31 +29,27 @@ runMain(() => {
3229
}
3330

3431
if (positionals.length !== 1) {
35-
throw new Error('Usage: node scripts/salesforce-lwc-app.ts <auth|deploy-app|get-url>')
32+
throw new Error('Usage: node scripts/salesforce-lwc-app.ts <deploy-app|get-url>')
3633
}
3734

3835
const commandName = positionals[0]
3936

4037
switch (commandName) {
41-
// Authenticate in the Salesforce CLI.
42-
case 'auth':
43-
authenticate()
44-
break
4538
// Deploy the app to the Salesforce org. To be done only when the app is updated.
4639
case 'deploy-app':
4740
deployApp()
4841
break
49-
// Get the authenticated URL of the app with the RUM configuration.
42+
// Get the authenticated URL of the app.
5043
case 'get-url':
51-
process.stdout.write(`with the following URL: ${buildSalesforceLwcUrl(values.proxy)}\n`)
44+
process.stdout.write(`${buildSalesforceLwcUrl()}\n`)
5245
break
5346
default:
54-
throw new Error(`Unknown command "${commandName ?? ''}". Expected: auth|deploy-app|get-url`)
47+
throw new Error(`Unknown command "${commandName ?? ''}". Expected: deploy-app|get-url`)
5548
}
5649
})
5750

5851
function showUsageAndExit() {
59-
console.log('Usage: node scripts/salesforce-lwc-app.ts <auth|deploy-app|get-url> [--proxy <url>]')
52+
console.log('Usage: node scripts/salesforce-lwc-app.ts <deploy-app|get-url>')
6053
process.exit(0)
6154
}
6255

@@ -85,10 +78,41 @@ function authenticate() {
8578
function deployApp() {
8679
const targetOrg = process.env.SF_TARGET_ORG ?? defaultTargetOrg
8780

81+
if (!isOrgAuthenticated(targetOrg)) {
82+
authenticate()
83+
}
84+
8885
printLog(`Deploying Salesforce LWC app to ${targetOrg}...`)
8986
command`sf project deploy start --target-org ${targetOrg} --source-dir force-app --ignore-conflicts --concise`
9087
.withCurrentWorkingDirectory(salesforceAppDir)
9188
.withLogs()
9289
.run()
9390
printLog('Salesforce LWC app deployed.')
9491
}
92+
93+
function isOrgAuthenticated(targetOrg: string): boolean {
94+
try {
95+
command`sf org display --target-org ${targetOrg}`.withCurrentWorkingDirectory(salesforceAppDir).run()
96+
return true
97+
} catch {
98+
return false
99+
}
100+
}
101+
102+
function buildSalesforceLwcUrl(): string {
103+
const targetOrg = process.env.SF_TARGET_ORG ?? defaultTargetOrg
104+
const path = new URL(salesforceHomePath, 'https://salesforce.local')
105+
106+
const output = command`sf org open --target-org ${targetOrg} --path ${path.pathname}${path.search} --url-only`
107+
.withCurrentWorkingDirectory(salesforceAppDir)
108+
.run()
109+
110+
// The sf CLI appends ANSI reset codes (\x1b[39m etc.) directly to the URL on stdout.
111+
// Excluding control characters (0x00–0x1f, which includes ESC/0x1b) strips them cleanly.
112+
// eslint-disable-next-line no-control-regex
113+
const url = output.match(/https:\/\/[^\s\x00-\x1f]+/g)?.at(-1)
114+
if (!url) {
115+
throw new Error(`Unable to find Salesforce URL in command output:\n${output}`)
116+
}
117+
return url
118+
}

test/apps/sf-lwc-app/force-app/main/default/lwc/datadogInit/datadogInit.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,12 @@ export default class DatadogInit extends NavigationMixin(LightningElement) {
6363

6464
getInitConfiguration(searchParams) {
6565
return {
66-
applicationId: searchParams.get('c__applicationId'),
67-
clientToken: searchParams.get('c__clientToken'),
68-
env: searchParams.get('c__env'),
69-
service: searchParams.get('c__service'),
70-
site: searchParams.get('c__site'),
7166
trackViewsManually: true,
7267
trackEarlyRequests: true,
7368
trackLongTasks: true,
7469
trackResources: true,
7570
trackUserInteractions: true,
71+
...window.dd_RUM_CONFIGURATION,
7672
...this.getQueryInitConfiguration(searchParams),
7773
}
7874
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { createSign } from 'node:crypto'
2+
3+
import {
4+
getSfLwcClientId,
5+
getSfLwcInstanceUrl,
6+
getSfLwcJwtPrivateKey,
7+
getSfLwcUsername,
8+
} from '../../../../scripts/lib/secrets.ts'
9+
10+
const salesforceHomePath = '/lightning/app/c__SF_LWC_App/page/home'
11+
12+
// The frontdoor.jsp OTP expires in ~1 minute, so the URL must be generated at test time —
13+
// not at suite startup — to guarantee a valid token when the test actually navigates.
14+
export async function buildSalesforceLwcUrl(): Promise<string> {
15+
const instanceUrl = getSfLwcInstanceUrl()
16+
const privateKey = Buffer.from(getSfLwcJwtPrivateKey(), 'base64').toString('utf8')
17+
const accessToken = await getAccessToken(getSfLwcClientId(), getSfLwcUsername(), instanceUrl, privateKey)
18+
19+
const path = new URL(salesforceHomePath, 'https://salesforce.local')
20+
21+
const response = await fetch(`${instanceUrl}/services/oauth2/singleaccess`, {
22+
method: 'POST',
23+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
24+
body: new URLSearchParams({
25+
access_token: accessToken,
26+
redirect_uri: `${path.pathname}${path.search}`,
27+
}),
28+
})
29+
30+
if (!response.ok) {
31+
throw new Error(`UI Bridge API failed (${response.status}): ${await response.text()}`)
32+
}
33+
34+
const json = (await response.json()) as Record<string, string>
35+
const frontdoorUri = json['frontdoor_uri']
36+
if (!frontdoorUri) {
37+
throw new Error('UI Bridge API response missing frontdoor_uri')
38+
}
39+
return frontdoorUri
40+
}
41+
42+
async function getAccessToken(
43+
clientId: string,
44+
username: string,
45+
instanceUrl: string,
46+
privateKey: string
47+
): Promise<string> {
48+
const header = Buffer.from(JSON.stringify({ alg: 'RS256' })).toString('base64url')
49+
const payload = Buffer.from(
50+
JSON.stringify({
51+
iss: clientId,
52+
sub: username,
53+
aud: instanceUrl,
54+
// JWT expiry is set to 3 minutes — enough to complete the token exchange.
55+
exp: Math.floor(Date.now() / 1000) + 180,
56+
})
57+
).toString('base64url')
58+
59+
const sign = createSign('RSA-SHA256')
60+
sign.update(`${header}.${payload}`)
61+
const jwt = `${header}.${payload}.${sign.sign(privateKey, 'base64url')}`
62+
63+
const response = await fetch(`${instanceUrl}/services/oauth2/token`, {
64+
method: 'POST',
65+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
66+
body: new URLSearchParams({
67+
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
68+
assertion: jwt,
69+
}),
70+
})
71+
72+
if (!response.ok) {
73+
throw new Error(`Salesforce token request failed (${response.status}): ${await response.text()}`)
74+
}
75+
76+
const json = (await response.json()) as Record<string, string>
77+
const accessToken = json['access_token']
78+
if (!accessToken) {
79+
throw new Error('Salesforce token response missing access_token')
80+
}
81+
return accessToken
82+
}

test/e2e/lib/framework/createTest.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ import {
1616
} from '../helpers/configuration'
1717
import { validateRumFormat } from '../helpers/validation'
1818
import type { BrowserConfiguration } from '../../../browsers.conf'
19-
import { buildSalesforceLwcUrl } from '../../../../scripts/lib/buildSalesforceLwcUrl'
2019
import {
2120
NEXTJS_APP_ROUTER_PORT,
2221
NUXT_APP_PORT,
2322
NUXT_VUE_ROUTER_V4_APP_PORT,
2423
VUE_ROUTER_APP_PORT,
2524
VUE_ROUTER_V4_APP_PORT,
2625
} from '../helpers/playwright'
26+
import { buildSalesforceLwcUrl } from './buildSalesforceLwcUrl'
2727
import { IntakeRegistry } from './intakeRegistry'
2828
import { flushEvents } from './flushEvents'
2929
import type { Servers } from './httpServers'
@@ -280,8 +280,8 @@ class TestBuilder {
280280
withSalesforceApp() {
281281
this.salesforceApp = true
282282
this.setups = [{ factory: () => '' }]
283-
this.baseUrlHooks.push((baseUrl, servers) => {
284-
baseUrl.href = buildSalesforceLwcUrl(servers.datadogHttpApi.origin)
283+
this.baseUrlHooks.push(async (baseUrl) => {
284+
baseUrl.href = await buildSalesforceLwcUrl()
285285
})
286286
return this
287287
}
@@ -422,7 +422,9 @@ function declareTest(title: string, setupOptions: SetupOptions, factory: SetupFa
422422

423423
const servers = await getTestServers()
424424
const baseUrl = new URL(servers.base.origin)
425-
setupOptions.baseUrlHooks.forEach((hook) => hook(baseUrl, servers, setupOptions))
425+
for (const hook of setupOptions.baseUrlHooks) {
426+
await hook(baseUrl, servers, setupOptions)
427+
}
426428

427429
test.skip(
428430
baseUrl.hostname.endsWith('.localhost') && isBrowserStack,
@@ -469,6 +471,13 @@ function declareTest(title: string, setupOptions: SetupOptions, factory: SetupFa
469471
contentType: 'application/javascript',
470472
})
471473
})
474+
475+
await page.addInitScript(
476+
`window.dd_RUM_CONFIGURATION = ${JSON.stringify({
477+
...DEFAULT_RUM_CONFIGURATION,
478+
proxy: servers.datadogHttpApi.origin,
479+
})}`
480+
)
472481
}
473482

474483
await setUpTest(browserLogs, setupOptions, testContext)

test/e2e/lib/framework/pageSetups.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export interface EventBridgeOptions {
5151
}
5252

5353
export type SetupFactory = (options: SetupOptions, servers: Servers) => string
54-
export type UrlHook = (baseUrl: URL, servers: Servers, options: SetupOptions) => void
54+
export type UrlHook = (baseUrl: URL, servers: Servers, options: SetupOptions) => void | Promise<void>
5555

5656
// By default, run tests only with the 'bundle' setup outside of the CI (to run faster on the
5757
// developer laptop) or with Browser Stack (to limit flakiness).

test/e2e/scenario/salesforce/salesforceLwc.scenario.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ test.use({
1212

1313
createTest('salesforce')
1414
.withSalesforceApp()
15-
.run(async ({ page, intakeRegistry, flushEvents }) => {
15+
.run(async ({ page, intakeRegistry, flushEvents, browserName }) => {
16+
if (browserName !== 'chromium') {
17+
return
18+
}
19+
1620
await expect(page.getByTestId('home-custom-actions')).toBeVisible({ timeout: 30000 })
1721

1822
await page.getByTestId('custom-action-1').click()

0 commit comments

Comments
 (0)