@trackunit/iris-app-playwright 0.2.116-alpha-5b28b432cab.0 → 0.2.116
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/fixtures/hostReadiness.d.ts +0 -5
- package/src/fixtures/hostReadiness.js +1 -11
- package/src/fixtures/login.fixture.d.ts +7 -75
- package/src/fixtures/login.fixture.js +45 -301
- package/migrations/entry.js.map +0 -1
- package/src/credentials/guards.js.map +0 -1
- package/src/credentials/keys.js.map +0 -1
- package/src/credentials/resolveE2EPassword.js.map +0 -1
- package/src/fixtures/acceptTerms.d.ts +0 -59
- package/src/fixtures/acceptTerms.js +0 -136
- package/src/fixtures/acceptTerms.js.map +0 -1
- package/src/fixtures/baseTest.js.map +0 -1
- package/src/fixtures/describe.js.map +0 -1
- package/src/fixtures/featureFlags.fixture.js.map +0 -1
- package/src/fixtures/harRecording.fixture.js.map +0 -1
- package/src/fixtures/hostReadiness.js.map +0 -1
- package/src/fixtures/hydrateStorageState.js.map +0 -1
- package/src/fixtures/irisApp.fixture.js.map +0 -1
- package/src/fixtures/localDevMode.fixture.js.map +0 -1
- package/src/fixtures/login.fixture.js.map +0 -1
- package/src/fixtures/loginCacheKey.js.map +0 -1
- package/src/fixtures/resolveCredentials.js.map +0 -1
- package/src/fixtures/withTimeout.js.map +0 -1
- package/src/generators/playwright-configuration/generator.js.map +0 -1
- package/src/index.js.map +0 -1
- package/src/plugins/createLogFile.js.map +0 -1
- package/src/plugins/defaultPlaywrightConfig.js.map +0 -1
- package/src/plugins/harRecordingPath.js.map +0 -1
- package/src/plugins/logsReporter.js.map +0 -1
- package/src/plugins/nxPreset.js.map +0 -1
- package/src/plugins/redactHar.js.map +0 -1
- package/src/plugins/redactPatterns.js.map +0 -1
- package/src/plugins/redactSensitive.js.map +0 -1
- package/src/plugins/redactTrace.js.map +0 -1
- package/src/plugins/scanArtifactsForSecrets.js.map +0 -1
- package/src/plugins/setupPlugins.js.map +0 -1
- package/src/plugins/writeFileWithPrettier.js.map +0 -1
- package/src/support.js.map +0 -1
- package/src/utils/Codeowner.js.map +0 -1
- package/src/utils/fileNameBuilder.js.map +0 -1
- package/src/utils/fileUpdater.js.map +0 -1
package/package.json
CHANGED
|
@@ -11,11 +11,6 @@ export declare class HostNeverBecameReadyError extends Error {
|
|
|
11
11
|
/** Creates an error whose message is exactly "host never became ready". The wait that ended is kept as `cause`. */
|
|
12
12
|
constructor(cause?: unknown);
|
|
13
13
|
}
|
|
14
|
-
/** Thrown when the terms gate remains visibly loading after its shorter diagnostic budget. */
|
|
15
|
-
export declare class TermsGateDidNotSettleError extends HostNeverBecameReadyError {
|
|
16
|
-
/** Creates a readiness error that names the shorter terms-gate timeout. */
|
|
17
|
-
constructor(timeoutMs: number);
|
|
18
|
-
}
|
|
19
14
|
/**
|
|
20
15
|
* True only when `NX_FEATURE_BRANCH_BASE_URL` is a non-empty string.
|
|
21
16
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isFeatureBranchHost = exports.
|
|
3
|
+
exports.isFeatureBranchHost = exports.HostNeverBecameReadyError = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Thrown when a feature-branch host never becomes ready.
|
|
6
6
|
*
|
|
@@ -18,16 +18,6 @@ class HostNeverBecameReadyError extends Error {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
exports.HostNeverBecameReadyError = HostNeverBecameReadyError;
|
|
21
|
-
/** Thrown when the terms gate remains visibly loading after its shorter diagnostic budget. */
|
|
22
|
-
class TermsGateDidNotSettleError extends HostNeverBecameReadyError {
|
|
23
|
-
/** Creates a readiness error that names the shorter terms-gate timeout. */
|
|
24
|
-
constructor(timeoutMs) {
|
|
25
|
-
super();
|
|
26
|
-
this.name = "TermsGateDidNotSettleError";
|
|
27
|
-
this.message = `terms gate did not settle within ${String(timeoutMs)}ms`;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
exports.TermsGateDidNotSettleError = TermsGateDidNotSettleError;
|
|
31
21
|
/**
|
|
32
22
|
* True only when `NX_FEATURE_BRANCH_BASE_URL` is a non-empty string.
|
|
33
23
|
*
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { type CachedStorageState, type StorageStateHydrator } from "./hydrateStorageState";
|
|
1
|
+
import { type CachedStorageState } from "./hydrateStorageState";
|
|
3
2
|
import { type AuthCredentials, type ResolveCredentialsOptions } from "./resolveCredentials";
|
|
4
3
|
export type { AuthCredentials };
|
|
5
4
|
export type LoginOptions = ResolveCredentialsOptions;
|
|
@@ -27,14 +26,6 @@ export declare const MAIN_MENU_READY_SELECTOR = "[data-testid=\"mainMenu\"]:not(
|
|
|
27
26
|
type MainMenuReadinessInput = {
|
|
28
27
|
readonly selector: string;
|
|
29
28
|
readonly fragments: ReadonlyArray<string>;
|
|
30
|
-
/**
|
|
31
|
-
* False on a feature-branch host, so `/terms` is not a finished login.
|
|
32
|
-
* {@link waitForMainMenuReady} always sets this in Node. The predicate runs
|
|
33
|
-
* in the browser, so the default must not call {@link isFeatureBranchHost}:
|
|
34
|
-
* that closure is not serialized, and a dropped argument would throw there.
|
|
35
|
-
* Omitted means ready, which is the staging hide-menu behaviour.
|
|
36
|
-
*/
|
|
37
|
-
readonly termsCountsAsReady?: boolean;
|
|
38
29
|
};
|
|
39
30
|
type MainMenuWaitPage = {
|
|
40
31
|
waitForFunction: (pageFunction: (input: MainMenuReadinessInput) => boolean, input: MainMenuReadinessInput, options?: {
|
|
@@ -62,24 +53,12 @@ export declare const extendTestTimeout: (testInfo: TestTimeoutInfo | undefined,
|
|
|
62
53
|
* branch is unchanged.
|
|
63
54
|
*/
|
|
64
55
|
export declare const reserveFeatureBranchLoginBudget: (testInfo?: TestTimeoutInfo) => void;
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
-
*
|
|
68
|
-
* Root cause (GLU-2632): `/terms` is in {@link HIDE_MENU_URL_FRAGMENTS}, so every
|
|
69
|
-
* host treated the unfinished terms prompt as a finished login. Callers on a
|
|
70
|
-
* feature-branch host pass `termsCountsAsReady: false`, so a URL containing
|
|
71
|
-
* `/terms` is not ready even if a menu node is present. An omitted flag stays
|
|
72
|
-
* ready, which keeps today's behaviour for every other host.
|
|
73
|
-
*/
|
|
74
|
-
export declare const isMainMenuReady: ({ selector, fragments, termsCountsAsReady, }: MainMenuReadinessInput) => boolean;
|
|
56
|
+
/** Returns whether the main menu is ready or intentionally hidden for the current route. */
|
|
57
|
+
export declare const isMainMenuReady: ({ selector, fragments }: MainMenuReadinessInput) => boolean;
|
|
75
58
|
/**
|
|
76
59
|
* Waits until Manager's main menu is rendered without a loading skeleton, or
|
|
77
60
|
* until the browser is on a HostLayout hide-menu route (marketplace / auth /
|
|
78
61
|
* terms) where the menu is intentionally absent.
|
|
79
|
-
*
|
|
80
|
-
* On a feature-branch host, `/terms` is excluded: the prompt is not a finished
|
|
81
|
-
* login (GLU-2632). The flag is computed here, in Node, and passed in because
|
|
82
|
-
* {@link isMainMenuReady} is evaluated in the page.
|
|
83
62
|
*/
|
|
84
63
|
export declare const waitForMainMenuReady: (page: MainMenuWaitPage, testInfo?: TestTimeoutInfo) => Promise<void>;
|
|
85
64
|
type EnvReadyResponse = {
|
|
@@ -159,10 +138,6 @@ type LoginRequestContext = {
|
|
|
159
138
|
type SessionDocument = {
|
|
160
139
|
ok: () => boolean;
|
|
161
140
|
};
|
|
162
|
-
type TermsButton = {
|
|
163
|
-
isVisible: () => Promise<boolean>;
|
|
164
|
-
click: () => Promise<void>;
|
|
165
|
-
};
|
|
166
141
|
type OktaLoginPage = {
|
|
167
142
|
goto: (url: string, options?: {
|
|
168
143
|
timeout?: number;
|
|
@@ -172,38 +147,19 @@ type OktaLoginPage = {
|
|
|
172
147
|
}) => Promise<unknown>;
|
|
173
148
|
locator: (selector: string) => {
|
|
174
149
|
waitFor: (options: {
|
|
175
|
-
state: "visible"
|
|
150
|
+
state: "visible";
|
|
176
151
|
timeout: number;
|
|
177
152
|
}) => Promise<unknown>;
|
|
178
|
-
isVisible?: () => Promise<boolean>;
|
|
179
153
|
};
|
|
180
|
-
/** Present on a real Playwright page. Session-handoff fakes omit it. */
|
|
181
|
-
url?: () => string;
|
|
182
|
-
getByRole?: (role: "button", options: {
|
|
183
|
-
name: string;
|
|
184
|
-
exact?: boolean;
|
|
185
|
-
}) => TermsButton;
|
|
186
|
-
on?: TermsStatusPage["on"];
|
|
187
|
-
off?: TermsStatusPage["off"];
|
|
188
|
-
waitForFunction?: MainMenuWaitPage["waitForFunction"];
|
|
189
154
|
};
|
|
190
|
-
/**
|
|
191
|
-
* How long a ready shell must stay off `/terms`, with the gate no longer loading, before
|
|
192
|
-
* login treats the prompt as not coming.
|
|
193
|
-
*/
|
|
194
|
-
export declare const TERMS_GATE_QUIET_MS = 1000;
|
|
195
|
-
/** How long a visibly loading terms gate may remain unchanged before login fails specifically. */
|
|
196
|
-
export declare const TERMS_GATE_STALL_TIMEOUT_MS = 20000;
|
|
197
155
|
/**
|
|
198
156
|
* Performs the slow path: wait for the branch host to serve `/env`, hit Okta's
|
|
199
157
|
* `/api/v1/authn`, redeem the `sessionToken` via the manager-classic redirect,
|
|
200
|
-
* and wait for the host layout to render.
|
|
201
|
-
*
|
|
202
|
-
* budget. Factored out so the cache hit/miss branches read clearly.
|
|
158
|
+
* and wait for the host layout to render. Factored out so the cache hit/miss
|
|
159
|
+
* branches read clearly.
|
|
203
160
|
*
|
|
204
161
|
* `createRequestContext`, `now`, and `sleep` are injectable so a unit test can
|
|
205
|
-
* drive the session handoff
|
|
206
|
-
* three-minute wait.
|
|
162
|
+
* drive the session handoff without a browser or a real three-minute wait.
|
|
207
163
|
*/
|
|
208
164
|
export declare const performOktaLogin: ({ page, baseURL, credentials, testInfo, createRequestContext, now, sleep: pause, }: {
|
|
209
165
|
page: OktaLoginPage;
|
|
@@ -216,28 +172,4 @@ export declare const performOktaLogin: ({ page, baseURL, credentials, testInfo,
|
|
|
216
172
|
/** Injectable pause between session document reloads. Production waits on the event loop. */
|
|
217
173
|
sleep?: (ms: number) => Promise<void>;
|
|
218
174
|
}) => Promise<void>;
|
|
219
|
-
type SignInPage = Omit<OktaLoginPage, "waitForFunction"> & {
|
|
220
|
-
context: () => StorageStateHydrator & {
|
|
221
|
-
storageState: () => Promise<CachedStorageState>;
|
|
222
|
-
};
|
|
223
|
-
/** Required so the shell wait after a restored session can run in the page. */
|
|
224
|
-
waitForFunction: MainMenuWaitPage["waitForFunction"];
|
|
225
|
-
};
|
|
226
|
-
/**
|
|
227
|
-
* Signs in, reusing this worker's cached session when one exists.
|
|
228
|
-
*
|
|
229
|
-
* `now` and `sleep` are injectable so a unit test can drive the restored
|
|
230
|
-
* feature-branch terms wait without a browser or a real three-minute clock.
|
|
231
|
-
* Production uses `Date.now` and the event loop.
|
|
232
|
-
*/
|
|
233
|
-
export declare const signIn: ({ page, baseURL, cache, testInfo, options, now, sleep: pause, createRequestContext, }: {
|
|
234
|
-
page: SignInPage;
|
|
235
|
-
baseURL: string | undefined;
|
|
236
|
-
cache: Map<string, CachedStorageState>;
|
|
237
|
-
testInfo?: TestTimeoutInfo;
|
|
238
|
-
options?: LoginOptions;
|
|
239
|
-
now?: () => number;
|
|
240
|
-
sleep?: (ms: number) => Promise<void>;
|
|
241
|
-
createRequestContext?: (baseURL: string | undefined) => Promise<LoginRequestContext>;
|
|
242
|
-
}) => Promise<void>;
|
|
243
175
|
export { expect } from "@playwright/test";
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.expect = exports.
|
|
3
|
+
exports.expect = exports.performOktaLogin = exports.test = exports.waitForEnvReady = exports.EnvReadyTimeoutError = exports.waitForMainMenuReady = exports.isMainMenuReady = exports.reserveFeatureBranchLoginBudget = exports.extendTestTimeout = exports.MAIN_MENU_READY_SELECTOR = exports.SESSION_DOCUMENT_RETRY_DELAY_MS = exports.ENV_READY_POLL_INTERVAL_MS = exports.ENV_READY_TIMEOUT_ENV_VAR = exports.ENV_READY_TIMEOUT_MS = exports.MAIN_MENU_READY_TIMEOUT_MS = exports.HIDE_MENU_URL_FRAGMENTS = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
|
-
const acceptTerms_1 = require("./acceptTerms");
|
|
6
5
|
const hostReadiness_1 = require("./hostReadiness");
|
|
7
6
|
const hydrateStorageState_1 = require("./hydrateStorageState");
|
|
8
7
|
const loginCacheKey_1 = require("./loginCacheKey");
|
|
@@ -70,19 +69,8 @@ const reserveFeatureBranchLoginBudget = (testInfo) => {
|
|
|
70
69
|
(0, exports.extendTestTimeout)(testInfo, exports.MAIN_MENU_READY_TIMEOUT_MS);
|
|
71
70
|
};
|
|
72
71
|
exports.reserveFeatureBranchLoginBudget = reserveFeatureBranchLoginBudget;
|
|
73
|
-
/**
|
|
74
|
-
|
|
75
|
-
*
|
|
76
|
-
* Root cause (GLU-2632): `/terms` is in {@link HIDE_MENU_URL_FRAGMENTS}, so every
|
|
77
|
-
* host treated the unfinished terms prompt as a finished login. Callers on a
|
|
78
|
-
* feature-branch host pass `termsCountsAsReady: false`, so a URL containing
|
|
79
|
-
* `/terms` is not ready even if a menu node is present. An omitted flag stays
|
|
80
|
-
* ready, which keeps today's behaviour for every other host.
|
|
81
|
-
*/
|
|
82
|
-
const isMainMenuReady = ({ selector, fragments, termsCountsAsReady = true, }) => {
|
|
83
|
-
if (!termsCountsAsReady && window.location.pathname.includes("/terms")) {
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
72
|
+
/** Returns whether the main menu is ready or intentionally hidden for the current route. */
|
|
73
|
+
const isMainMenuReady = ({ selector, fragments }) => {
|
|
86
74
|
if (document.querySelector(selector) !== null) {
|
|
87
75
|
return true;
|
|
88
76
|
}
|
|
@@ -93,29 +81,10 @@ exports.isMainMenuReady = isMainMenuReady;
|
|
|
93
81
|
* Waits until Manager's main menu is rendered without a loading skeleton, or
|
|
94
82
|
* until the browser is on a HostLayout hide-menu route (marketplace / auth /
|
|
95
83
|
* terms) where the menu is intentionally absent.
|
|
96
|
-
*
|
|
97
|
-
* On a feature-branch host, `/terms` is excluded: the prompt is not a finished
|
|
98
|
-
* login (GLU-2632). The flag is computed here, in Node, and passed in because
|
|
99
|
-
* {@link isMainMenuReady} is evaluated in the page.
|
|
100
84
|
*/
|
|
101
85
|
const waitForMainMenuReady = async (page, testInfo) => {
|
|
102
86
|
(0, exports.extendTestTimeout)(testInfo, exports.MAIN_MENU_READY_TIMEOUT_MS);
|
|
103
|
-
|
|
104
|
-
await page.waitForFunction(exports.isMainMenuReady, {
|
|
105
|
-
selector: exports.MAIN_MENU_READY_SELECTOR,
|
|
106
|
-
fragments: exports.HIDE_MENU_URL_FRAGMENTS,
|
|
107
|
-
termsCountsAsReady: !(0, hostReadiness_1.isFeatureBranchHost)(),
|
|
108
|
-
}, { timeout: exports.MAIN_MENU_READY_TIMEOUT_MS });
|
|
109
|
-
}
|
|
110
|
-
catch (error) {
|
|
111
|
-
// The terms prompt can redirect after the inner shell wait has returned.
|
|
112
|
-
// On a feature-branch host that timeout is the host never becoming ready,
|
|
113
|
-
// before any page assertion. Other hosts keep Playwright's timeout.
|
|
114
|
-
if ((0, hostReadiness_1.isFeatureBranchHost)() && error instanceof test_1.errors.TimeoutError) {
|
|
115
|
-
throw new hostReadiness_1.HostNeverBecameReadyError(error);
|
|
116
|
-
}
|
|
117
|
-
throw error;
|
|
118
|
-
}
|
|
87
|
+
await page.waitForFunction(exports.isMainMenuReady, { selector: exports.MAIN_MENU_READY_SELECTOR, fragments: exports.HIDE_MENU_URL_FRAGMENTS }, { timeout: exports.MAIN_MENU_READY_TIMEOUT_MS });
|
|
119
88
|
};
|
|
120
89
|
exports.waitForMainMenuReady = waitForMainMenuReady;
|
|
121
90
|
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
@@ -195,187 +164,44 @@ exports.test = test_1.test.extend({
|
|
|
195
164
|
{ scope: "worker" },
|
|
196
165
|
],
|
|
197
166
|
login: async ({ page, baseURL, _loginCache }, provide, testInfo) => {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
* Starts the terms-status watcher only on a feature-branch host, and only when the
|
|
218
|
-
* page can listen for requests. An unset or empty `NX_FEATURE_BRANCH_BASE_URL` does
|
|
219
|
-
* not subscribe and does not click Accept (GLU-2632).
|
|
220
|
-
*/
|
|
221
|
-
const startFeatureBranchTermsWatch = (page) => {
|
|
222
|
-
if (!(0, hostReadiness_1.isFeatureBranchHost)() || page.on === undefined || page.off === undefined) {
|
|
223
|
-
return noopTermsWatcher();
|
|
224
|
-
}
|
|
225
|
-
// Call through the page. A detached `page.on` runs without `this`, and Playwright
|
|
226
|
-
// then throws `this.listenerCount is not a function` before any request is seen.
|
|
227
|
-
return (0, acceptTerms_1.watchTermsStatus)({
|
|
228
|
-
on: (event, listener) => {
|
|
229
|
-
page.on?.(event, listener);
|
|
230
|
-
},
|
|
231
|
-
off: (event, listener) => {
|
|
232
|
-
page.off?.(event, listener);
|
|
233
|
-
},
|
|
234
|
-
});
|
|
235
|
-
};
|
|
236
|
-
/**
|
|
237
|
-
* True while `TermsGate` has not decided yet: either the lazy chunk has not replaced its
|
|
238
|
-
* Suspense fallback, or the chunk is mounted and shows its own spinner because the status
|
|
239
|
-
* lookup is still loading. A page that cannot see those nodes is treated as decided.
|
|
240
|
-
*/
|
|
241
|
-
const isTermsGateStillLoading = async (page) => {
|
|
242
|
-
for (const selector of [TERMS_GATE_FALLBACK_SELECTOR, TERMS_GATE_LOADING_SELECTOR]) {
|
|
243
|
-
const node = page.locator(selector);
|
|
244
|
-
if (node.isVisible !== undefined && (await node.isVisible())) {
|
|
245
|
-
return true;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return false;
|
|
249
|
-
};
|
|
250
|
-
/** True only for the spinner owned by `TermsGate`, not the shared application Suspense fallback. */
|
|
251
|
-
const isTermsStatusStillLoading = async (page) => {
|
|
252
|
-
const node = page.locator(TERMS_GATE_LOADING_SELECTOR);
|
|
253
|
-
return node.isVisible !== undefined && (await node.isVisible());
|
|
254
|
-
};
|
|
255
|
-
/**
|
|
256
|
-
* Finishes the terms prompt, then waits for the main shell, inside the login budget.
|
|
257
|
-
*
|
|
258
|
-
* Root cause (GLU-2632): feature-branch `/terms` is not a finished login, and waiting
|
|
259
|
-
* for the menu never clicks the prompt. `#host-layout-content` is visible while
|
|
260
|
-
* `TermsGate` is still a Suspense fallback, so returning as soon as the layout appears
|
|
261
|
-
* can miss a later redirect. URL and rendered gate state are authoritative: a `/terms`
|
|
262
|
-
* URL starts the accept flow, while a ready shell that stays off `/terms` with no gate
|
|
263
|
-
* loader for {@link TERMS_GATE_QUIET_MS} is finished. Starting a new status request
|
|
264
|
-
* resets that quiet window, but its response is never awaited because browser requests
|
|
265
|
-
* can remain pending across navigation. A visibly loading gate gets the shorter
|
|
266
|
-
* {@link TERMS_GATE_STALL_TIMEOUT_MS} diagnostic budget.
|
|
267
|
-
*/
|
|
268
|
-
const finishFeatureBranchTerms = async ({ page, watcher, now, sleep: pause, deadline, }) => {
|
|
269
|
-
if (!(0, hostReadiness_1.isFeatureBranchHost)()) {
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
let acceptedPrompt = false;
|
|
273
|
-
let quietSince;
|
|
274
|
-
let quietRevision = 0;
|
|
275
|
-
let gateLoadingSince;
|
|
276
|
-
const acceptIfPrompted = async () => {
|
|
277
|
-
// Call through the page. Detached Playwright methods run without `this` and throw
|
|
278
|
-
// reading `_mainFrame` before terms can finish.
|
|
279
|
-
const pageUrl = page.url;
|
|
280
|
-
if (pageUrl === undefined || !(0, acceptTerms_1.isTermsRoute)(pageUrl.call(page))) {
|
|
281
|
-
return false;
|
|
282
|
-
}
|
|
283
|
-
if (page.getByRole === undefined) {
|
|
284
|
-
throw new hostReadiness_1.HostNeverBecameReadyError();
|
|
285
|
-
}
|
|
286
|
-
const readUrl = pageUrl.bind(page);
|
|
287
|
-
const findButton = page.getByRole.bind(page);
|
|
288
|
-
await (0, acceptTerms_1.acceptTermsWithinBudget)({
|
|
289
|
-
page: {
|
|
290
|
-
url: readUrl,
|
|
291
|
-
getByRole: findButton,
|
|
292
|
-
waitForURL: (predicate, options) => page.waitForURL(predicate, options),
|
|
293
|
-
},
|
|
294
|
-
now,
|
|
295
|
-
sleep: pause,
|
|
296
|
-
deadline,
|
|
297
|
-
});
|
|
298
|
-
return true;
|
|
299
|
-
};
|
|
300
|
-
if (page.waitForFunction === undefined) {
|
|
301
|
-
await acceptIfPrompted();
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
const waitForFunction = page.waitForFunction.bind(page);
|
|
305
|
-
const shellInput = {
|
|
306
|
-
selector: exports.MAIN_MENU_READY_SELECTOR,
|
|
307
|
-
fragments: exports.HIDE_MENU_URL_FRAGMENTS,
|
|
308
|
-
termsCountsAsReady: false,
|
|
309
|
-
};
|
|
310
|
-
while (now() < deadline) {
|
|
311
|
-
const started = now();
|
|
312
|
-
acceptedPrompt = (await acceptIfPrompted()) || acceptedPrompt;
|
|
313
|
-
const remaining = deadline - now();
|
|
314
|
-
if (remaining <= 0) {
|
|
315
|
-
break;
|
|
316
|
-
}
|
|
317
|
-
const slice = Math.min(exports.TERMS_GATE_QUIET_MS, remaining);
|
|
318
|
-
let shellReady = false;
|
|
319
|
-
try {
|
|
320
|
-
await waitForFunction(exports.isMainMenuReady, shellInput, { timeout: slice });
|
|
321
|
-
shellReady = true;
|
|
322
|
-
}
|
|
323
|
-
catch (error) {
|
|
324
|
-
if (!isPlaywrightTimeout(error) && now() < deadline) {
|
|
325
|
-
throw error;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
if (shellReady) {
|
|
329
|
-
acceptedPrompt = (await acceptIfPrompted()) || acceptedPrompt;
|
|
330
|
-
}
|
|
331
|
-
const onTerms = page.url !== undefined && (0, acceptTerms_1.isTermsRoute)(page.url());
|
|
332
|
-
const gateLoading = await isTermsGateStillLoading(page);
|
|
333
|
-
const termsStatusLoading = gateLoading && (await isTermsStatusStillLoading(page));
|
|
334
|
-
if (termsStatusLoading) {
|
|
335
|
-
gateLoadingSince ?? (gateLoadingSince = now());
|
|
336
|
-
if (now() - gateLoadingSince >= exports.TERMS_GATE_STALL_TIMEOUT_MS) {
|
|
337
|
-
throw new hostReadiness_1.TermsGateDidNotSettleError(exports.TERMS_GATE_STALL_TIMEOUT_MS);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
else {
|
|
341
|
-
gateLoadingSince = undefined;
|
|
342
|
-
}
|
|
343
|
-
if (!shellReady || onTerms || gateLoading) {
|
|
344
|
-
quietSince = undefined;
|
|
345
|
-
}
|
|
346
|
-
else if (acceptedPrompt) {
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
else {
|
|
350
|
-
// A request that first appears mid-window restarts it: the app may still be
|
|
351
|
-
// committing a redirect after the current DOM state was observed.
|
|
352
|
-
const revision = watcher.revision();
|
|
353
|
-
if (quietSince === undefined || revision !== quietRevision) {
|
|
354
|
-
quietSince = now();
|
|
355
|
-
quietRevision = revision;
|
|
356
|
-
}
|
|
357
|
-
if (now() - quietSince >= exports.TERMS_GATE_QUIET_MS) {
|
|
358
|
-
return;
|
|
167
|
+
const loginFn = async (options) => {
|
|
168
|
+
const credentials = await (0, resolveCredentials_1.resolveCredentials)(options);
|
|
169
|
+
const cacheKey = (0, loginCacheKey_1.buildLoginCacheKey)(baseURL, credentials);
|
|
170
|
+
const cached = _loginCache.get(cacheKey);
|
|
171
|
+
if (cached !== undefined) {
|
|
172
|
+
let cachedSessionRestored = false;
|
|
173
|
+
try {
|
|
174
|
+
await (0, hydrateStorageState_1.hydrateStorageState)(page.context(), cached);
|
|
175
|
+
await page.goto("/");
|
|
176
|
+
await page.locator("#host-layout-content").waitFor({ state: "visible", timeout: 15000 });
|
|
177
|
+
cachedSessionRestored = true;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
_loginCache.delete(cacheKey);
|
|
181
|
+
}
|
|
182
|
+
if (cachedSessionRestored) {
|
|
183
|
+
await (0, exports.waitForMainMenuReady)(page, testInfo);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
359
186
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
187
|
+
(0, exports.reserveFeatureBranchLoginBudget)(testInfo);
|
|
188
|
+
await (0, exports.performOktaLogin)({ page, baseURL, credentials, testInfo });
|
|
189
|
+
// Capture the fully-authenticated state for reuse by later tests in
|
|
190
|
+
// this worker. Errors here would mask the actual login success, so we
|
|
191
|
+
// never let snapshot capture throw — at worst we'll re-hit Okta next
|
|
192
|
+
// time, which is the pre-cache behaviour.
|
|
193
|
+
try {
|
|
194
|
+
const snapshot = await page.context().storageState();
|
|
195
|
+
_loginCache.set(cacheKey, snapshot);
|
|
369
196
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (now() === started && after === mark) {
|
|
373
|
-
throw new hostReadiness_1.HostNeverBecameReadyError();
|
|
197
|
+
catch {
|
|
198
|
+
/* Snapshot capture is best-effort; failing here is non-fatal. */
|
|
374
199
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
200
|
+
await (0, exports.waitForMainMenuReady)(page, testInfo);
|
|
201
|
+
};
|
|
202
|
+
await provide(loginFn);
|
|
203
|
+
},
|
|
204
|
+
});
|
|
379
205
|
const openDefaultRequestContext = async (baseURL) => {
|
|
380
206
|
const context = await test_1.request.newContext({ baseURL });
|
|
381
207
|
return {
|
|
@@ -492,18 +318,14 @@ const gotoSessionDocument = async (page, url, now, deadline, pause) => {
|
|
|
492
318
|
/**
|
|
493
319
|
* Performs the slow path: wait for the branch host to serve `/env`, hit Okta's
|
|
494
320
|
* `/api/v1/authn`, redeem the `sessionToken` via the manager-classic redirect,
|
|
495
|
-
* and wait for the host layout to render.
|
|
496
|
-
*
|
|
497
|
-
* budget. Factored out so the cache hit/miss branches read clearly.
|
|
321
|
+
* and wait for the host layout to render. Factored out so the cache hit/miss
|
|
322
|
+
* branches read clearly.
|
|
498
323
|
*
|
|
499
324
|
* `createRequestContext`, `now`, and `sleep` are injectable so a unit test can
|
|
500
|
-
* drive the session handoff
|
|
501
|
-
* three-minute wait.
|
|
325
|
+
* drive the session handoff without a browser or a real three-minute wait.
|
|
502
326
|
*/
|
|
503
327
|
const performOktaLogin = async ({ page, baseURL, credentials, testInfo, createRequestContext = openDefaultRequestContext, now = Date.now, sleep: pause = sleep, }) => {
|
|
504
328
|
const context = await createRequestContext(baseURL);
|
|
505
|
-
// Before the session navigation: the menu can render before TermsGate redirects.
|
|
506
|
-
const termsWatcher = startFeatureBranchTermsWatch(page);
|
|
507
329
|
try {
|
|
508
330
|
const env = await readHostEnv(context, baseURL, testInfo);
|
|
509
331
|
const domainValue = env.auth?.url;
|
|
@@ -511,10 +333,9 @@ const performOktaLogin = async ({ page, baseURL, credentials, testInfo, createRe
|
|
|
511
333
|
throw new Error(`No domain found from servers /env found env: ${JSON.stringify(env)}`);
|
|
512
334
|
}
|
|
513
335
|
const domain = domainValue;
|
|
514
|
-
// Started once `/env` is ready. The Okta post, the session document
|
|
515
|
-
// handoff
|
|
516
|
-
//
|
|
517
|
-
// its own fixed timeout (GLU-2631, GLU-2632).
|
|
336
|
+
// Started once `/env` is ready. The Okta post, the session document and the
|
|
337
|
+
// handoff all run inside this window; only the last two are retried against
|
|
338
|
+
// it, the post keeps its own fixed timeout (GLU-2631).
|
|
518
339
|
const readyDeadline = (0, hostReadiness_1.isFeatureBranchHost)() ? now() + exports.MAIN_MENU_READY_TIMEOUT_MS : undefined;
|
|
519
340
|
const authResponse = await context.post(`${domain}/api/v1/authn`, {
|
|
520
341
|
data: {
|
|
@@ -536,89 +357,12 @@ const performOktaLogin = async ({ page, baseURL, credentials, testInfo, createRe
|
|
|
536
357
|
await gotoSessionDocument(page, `/auth/manager-classic#session_token=${sessionToken}&userProfile=true`, now, readyDeadline, pause);
|
|
537
358
|
await waitForSessionHandoff(page, now, readyDeadline);
|
|
538
359
|
await page.locator("#host-layout-content").waitFor({ state: "visible", timeout: 15000 });
|
|
539
|
-
if (readyDeadline !== undefined) {
|
|
540
|
-
await finishFeatureBranchTerms({
|
|
541
|
-
page,
|
|
542
|
-
watcher: termsWatcher,
|
|
543
|
-
now,
|
|
544
|
-
sleep: pause,
|
|
545
|
-
deadline: readyDeadline,
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
360
|
}
|
|
549
361
|
finally {
|
|
550
|
-
termsWatcher.stop();
|
|
551
362
|
await context.dispose();
|
|
552
363
|
}
|
|
553
364
|
};
|
|
554
365
|
exports.performOktaLogin = performOktaLogin;
|
|
555
|
-
/**
|
|
556
|
-
* Signs in, reusing this worker's cached session when one exists.
|
|
557
|
-
*
|
|
558
|
-
* `now` and `sleep` are injectable so a unit test can drive the restored
|
|
559
|
-
* feature-branch terms wait without a browser or a real three-minute clock.
|
|
560
|
-
* Production uses `Date.now` and the event loop.
|
|
561
|
-
*/
|
|
562
|
-
const signIn = async ({ page, baseURL, cache, testInfo, options, now = Date.now, sleep: pause = sleep, createRequestContext, }) => {
|
|
563
|
-
const credentials = await (0, resolveCredentials_1.resolveCredentials)(options);
|
|
564
|
-
const cacheKey = (0, loginCacheKey_1.buildLoginCacheKey)(baseURL, credentials);
|
|
565
|
-
const cached = cache.get(cacheKey);
|
|
566
|
-
if (cached !== undefined) {
|
|
567
|
-
let cachedSessionRestored = false;
|
|
568
|
-
const termsWatcher = startFeatureBranchTermsWatch(page);
|
|
569
|
-
try {
|
|
570
|
-
try {
|
|
571
|
-
await (0, hydrateStorageState_1.hydrateStorageState)(page.context(), cached);
|
|
572
|
-
await page.goto("/");
|
|
573
|
-
await page.locator("#host-layout-content").waitFor({ state: "visible", timeout: 15000 });
|
|
574
|
-
if ((0, hostReadiness_1.isFeatureBranchHost)()) {
|
|
575
|
-
// The terms wait can use the whole three-minute budget. Reserve it
|
|
576
|
-
// before that wait: the miss-path call below is skipped when restore
|
|
577
|
-
// succeeds, and the menu wait extends the timeout only after this returns.
|
|
578
|
-
(0, exports.reserveFeatureBranchLoginBudget)(testInfo);
|
|
579
|
-
const deadline = now() + exports.MAIN_MENU_READY_TIMEOUT_MS;
|
|
580
|
-
await finishFeatureBranchTerms({
|
|
581
|
-
page,
|
|
582
|
-
watcher: termsWatcher,
|
|
583
|
-
now,
|
|
584
|
-
sleep: pause,
|
|
585
|
-
deadline,
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
cachedSessionRestored = true;
|
|
589
|
-
}
|
|
590
|
-
catch (error) {
|
|
591
|
-
if (error instanceof hostReadiness_1.HostNeverBecameReadyError) {
|
|
592
|
-
throw error;
|
|
593
|
-
}
|
|
594
|
-
cache.delete(cacheKey);
|
|
595
|
-
}
|
|
596
|
-
if (cachedSessionRestored) {
|
|
597
|
-
await (0, exports.waitForMainMenuReady)(page, testInfo);
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
finally {
|
|
602
|
-
termsWatcher.stop();
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
(0, exports.reserveFeatureBranchLoginBudget)(testInfo);
|
|
606
|
-
await (0, exports.performOktaLogin)({ page, baseURL, credentials, testInfo, now, sleep: pause, createRequestContext });
|
|
607
|
-
await (0, exports.waitForMainMenuReady)(page, testInfo);
|
|
608
|
-
// Capture only after the shell is ready. A feature-branch host can sit
|
|
609
|
-
// on `/terms` with the layout mounted; storing that snapshot would reuse
|
|
610
|
-
// a session whose prompt is not finished (GLU-2632). Errors here would
|
|
611
|
-
// mask the actual login success, so snapshot capture never throws — at
|
|
612
|
-
// worst we'll re-hit Okta next time, which is the pre-cache behaviour.
|
|
613
|
-
try {
|
|
614
|
-
const snapshot = await page.context().storageState();
|
|
615
|
-
cache.set(cacheKey, snapshot);
|
|
616
|
-
}
|
|
617
|
-
catch {
|
|
618
|
-
/* Snapshot capture is best-effort; failing here is non-fatal. */
|
|
619
|
-
}
|
|
620
|
-
};
|
|
621
|
-
exports.signIn = signIn;
|
|
622
366
|
var test_2 = require("@playwright/test");
|
|
623
367
|
Object.defineProperty(exports, "expect", { enumerable: true, get: function () { return test_2.expect; } });
|
|
624
368
|
//# sourceMappingURL=login.fixture.js.map
|
package/migrations/entry.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"entry.js","sourceRoot":"","sources":["../../../../../libs/iris-app-sdk/iris-app-playwright/migrations/entry.ts"],"names":[],"mappings":"","sourcesContent":["export {};\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../../../../../../libs/iris-app-sdk/iris-app-playwright/src/credentials/guards.ts"],"names":[],"mappings":";;;AAAA,oEAAoE;AAC7D,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAoC,EAAE,CAC3E,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC;AADjC,QAAA,QAAQ,YACyB","sourcesContent":["/** Narrows `unknown` to a plain object without a type assertion. */\nexport const isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null;\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../../../../../../libs/iris-app-sdk/iris-app-playwright/src/credentials/keys.ts"],"names":[],"mappings":";AAAA;;;;;;;;;GASG;;;AAEH;;;;;;GAMG;AACI,MAAM,eAAe,GAAG,CAAC,QAAgB,EAAU,EAAE,CAC1D,QAAQ;KACL,WAAW,EAAE;KACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;KAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;AAJhB,QAAA,eAAe,mBAIC;AAE7B,+EAA+E;AACxE,MAAM,YAAY,GAAG,CAAC,QAAgB,EAAU,EAAE,CAAC,UAAU,IAAA,uBAAe,EAAC,QAAQ,CAAC,EAAE,CAAC;AAAnF,QAAA,YAAY,gBAAuE;AAEhG;;;;;;GAMG;AACI,MAAM,gBAAgB,GAAG,CAAC,QAAgB,EAAU,EAAE,CAAC,IAAA,oBAAY,EAAC,QAAQ,CAAC,CAAC;AAAxE,QAAA,gBAAgB,oBAAwD","sourcesContent":["/**\n * Credential-key derivation for the manager E2E password lookup (GLU-1451 / GLU-1452).\n *\n * An `auth.json` fixture identifies its account by `username` only — the password\n * is resolved at runtime by a stable key derived from the name: from a CircleCI\n * env variable in CI and from Vault locally.\n *\n * The `E2E_PW_<SLUG>` convention MUST stay in sync with the publish side in\n * `devtools/e2e-okta-rotation/slug.ts` (which writes the CircleCI/Vault values).\n */\n\n/**\n * Uppercase slug of an account name: non-alphanumeric runs collapse to a single\n * `_`, with leading/trailing separators trimmed. Keeps account names with spaces\n * and dashes valid as env-var/Vault-field keys.\n *\n * `Team Helios - E2E Test` -> `TEAM_HELIOS_E2E_TEST`\n */\nexport const slugifyUsername = (username: string): string =>\n username\n .toUpperCase()\n .replace(/[^A-Z0-9]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\");\n\n/** The environment-variable key for an account's password: `E2E_PW_<SLUG>`. */\nexport const deriveEnvKey = (username: string): string => `E2E_PW_${slugifyUsername(username)}`;\n\n/**\n * The Vault field key for an account's password. This is the **same** key as the\n * env variable (`E2E_PW_<SLUG>`): the publish side writes each account's password\n * to a Vault field named by `devtools/e2e-okta-rotation/slug.ts`'s `envKey()`\n * (`E2E_PW_<SLUG>`), so the resolver must read it back under that exact key. A\n * lowercased-slug field would never be found once Vault is seeded.\n */\nexport const deriveVaultField = (username: string): string => deriveEnvKey(username);\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"resolveE2EPassword.js","sourceRoot":"","sources":["../../../../../../libs/iris-app-sdk/iris-app-playwright/src/credentials/resolveE2EPassword.ts"],"names":[],"mappings":";;;;AAAA,2BAAkC;AAClC,+CAAyB;AACzB,mDAA6B;AAC7B,gEAA6D;AAC7D,qCAAoC;AACpC,iCAAwD;AAwBxD;;;GAGG;AACH,MAAM,cAAc,GAAG,CAAC,MAAe,EAAE,KAAa,EAAsB,EAAE;IAC5E,IAAI,CAAC,IAAA,iBAAQ,EAAC,MAAM,CAAC,EAAE,CAAC;QACtB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC;IAC1B,IAAI,CAAC,IAAA,iBAAQ,EAAC,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,MAAM,GAAG,IAAA,iBAAQ,EAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;IACzD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC5B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACvE,CAAC,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,kBAAkB,GAAG,KAAK,EAAE,GAAsB,EAAoC,EAAE;IAC5F,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC;IACtC,IAAI,KAAa,CAAC;IAClB,IAAI,CAAC;QACH,KAAK,GAAG,IAAA,iBAAY,EAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;IACxE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC,CAAC,6BAA6B;IACjD,CAAC;IACD,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;QACjB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,QAAQ,GAAG,GAAG,CAAC,kBAAkB,IAAI,GAAG,CAAC,cAAc,CAAC;IAC9D,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,EAAE,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CACb,8HAA8H,CAC/H,CAAC;IACJ,CAAC;IACD,0EAA0E;IAC1E,+EAA+E;IAC/E,8EAA8E;IAC9E,wEAAwE;IACxE,MAAM,SAAS,GAAG,MAAM,0DAAO,YAAY,IAAE,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IACpE,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,OAAO,IAAI,CACT,6FAA6F;YAC3F,wCAAwC,CAC3C,CAAC;IACJ,CAAC;IACD,6EAA6E;IAC7E,gFAAgF;IAChF,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC;QAC/B,QAAQ;QACR,KAAK;QACL,cAAc,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,KAAM,EAAE;KACrD,CAAC,CAAC;IACH,OAAO,EAAE,IAAI,EAAE,UAAU,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;AACzD,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,uBAAuB,GAAwB,IAAI,GAAG,CAAC;IAC3D,WAAW;IACX,WAAW;IACX,cAAc;IACd,YAAY;IACZ,WAAW;IACX,iBAAiB;IACjB,cAAc;IACd,aAAa;CACd,CAAC,CAAC;AAEH,2GAA2G;AAC3G,MAAM,uBAAuB,GAAG,CAAC,KAAc,EAAW,EAAE;IAC1D,IAAI,CAAC,IAAA,iBAAQ,EAAC,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;IACxB,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,uBAAuB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACvE,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,4BAA4B,GAAwB,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAE9E,uFAAuF;AACvF,MAAM,mBAAmB,GAAG,CAAC,KAAc,EAAsB,EAAE;IACjE,IAAI,CAAC,IAAA,iBAAQ,EAAC,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC;IAChC,IAAI,CAAC,IAAA,iBAAQ,EAAC,QAAQ,CAAC,EAAE,CAAC;QACxB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,UAAU,GAAG,QAAQ,CAAC,UAAU,CAAC;IACvC,OAAO,OAAO,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC;AACjE,CAAC,CAAC;AAEF,4FAA4F;AAC5F,MAAM,IAAI,GAAG,CAAC,OAAe,EAAS,EAAE;IACtC,MAAM,IAAI,KAAK,CAAC,IAAA,iCAAe,EAAC,OAAO,CAAC,CAAC,CAAC;AAC5C,CAAC,CAAC;AAEF,MAAM,aAAa,GAAG,IAAI,GAAG,EAA8B,CAAC;AAE5D;;;;;GAKG;AACI,MAAM,qBAAqB,GAAG,GAAS,EAAE;IAC9C,aAAa,CAAC,KAAK,EAAE,CAAC;AACxB,CAAC,CAAC;AAFW,QAAA,qBAAqB,yBAEhC;AAEF;;;;;;;;;;;;GAYG;AACI,MAAM,kBAAkB,GAAG,KAAK,EACrC,QAAgB,EAChB,UAAqC,EAAE,EACtB,EAAE;IACnB,IAAI,QAAQ,KAAK,EAAE,IAAI,QAAQ,KAAK,YAAY,EAAE,CAAC;QACjD,OAAO,IAAI,CAAC,yDAAyD,QAAQ,KAAK,CAAC,CAAC;IACtF,CAAC;IACD,MAAM,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC3C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACvC,MAAM,GAAG,GAAG,IAAA,mBAAY,EAAC,QAAQ,CAAC,CAAC;IAEnC,uEAAuE;IACvE,MAAM,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;IACzB,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,EAAE,EAAE,CAAC;QAC5C,aAAa,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACrC,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,4DAA4D;IAC5D,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,kBAAkB,CAAC;IAC9D,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,GAAG,CAAC,CAAC;IACrC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,IAAI,CACT,wBAAwB,QAAQ,uBAAuB,GAAG,6BAA6B;YACrF,gEAAgE,GAAG,CAAC,kBAAkB,6DAA6D,CACtJ,CAAC;IACJ,CAAC;IAED,MAAM,WAAW,GAAG,GAAG,CAAC,cAAc,CAAC;IACvC,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO,IAAI,CAAC,2BAA2B,CAAC,CAAC;IAC3C,CAAC;IACD,MAAM,QAAQ,GAAG,WAAW,CAAC;IAE7B,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACtC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,uBAAuB,CAAC,KAAK,CAAC,EAAE,CAAC;YACnC,OAAO,IAAI,CACT,wBAAwB,QAAQ,mCAAmC;gBACjE,+DAA+D;gBAC/D,4CAA4C,GAAG,0DAA0D,GAAG,CAAC,kBAAkB,GAAG,CACrI,CAAC;QACJ,CAAC;QACD,MAAM,MAAM,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;QAC1C,IAAI,MAAM,KAAK,SAAS,IAAI,4BAA4B,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YACrE,OAAO,IAAI,CACT,wBAAwB,QAAQ,uCAAuC,MAAM,MAAM;gBACjF,0CAA0C;gBAC1C,6CAA6C,GAAG,CAAC,kBAAkB,iDAAiD,GAAG,+EAA+E,GAAG,CAAC,kBAAkB,GAAG,CAClO,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CACT,wBAAwB,QAAQ,qBAAqB,QAAQ,oBAAoB,GAAG,CAAC,kBAAkB,GAAG,CAC3G,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GAAG,cAAc,CAAC,MAAM,EAAE,IAAA,uBAAgB,EAAC,QAAQ,CAAC,CAAC,CAAC;IACrE,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QAC5B,OAAO,IAAI,CACT,wBAAwB,QAAQ,gBAAgB,IAAA,uBAAgB,EAAC,QAAQ,CAAC,oBAAoB,QAAQ,IAAI,CAC3G,CAAC;IACJ,CAAC;IACD,aAAa,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IACvC,OAAO,SAAS,CAAC;AACnB,CAAC,CAAC;AAtEW,QAAA,kBAAkB,sBAsE7B","sourcesContent":["import { readFileSync } from \"fs\";\nimport * as os from \"os\";\nimport * as path from \"path\";\nimport { redactSensitive } from \"../plugins/redactSensitive\";\nimport { isRecord } from \"./guards\";\nimport { deriveEnvKey, deriveVaultField } from \"./keys\";\n\n/**\n * Minimal structural view of the `node-vault` client's KV read: it returns\n * `{ data: ... }` (KV v1) or `{ data: { data: ... } }` (KV v2). Kept as an\n * interface so tests inject a fake and neither `node-vault` nor a real\n * Vault/VPN is needed. `read` returns `unknown` so callers narrow structurally\n * (no `as` cast at the boundary).\n */\nexport interface VaultReader {\n read(secretPath: string): Promise<unknown>;\n}\n\nexport interface ResolveE2EPasswordOptions {\n /** Process env to read (env-first precedence). Defaults to `process.env`. */\n env?: NodeJS.ProcessEnv;\n /**\n * Vault client factory (local path). Returns `undefined` when Vault can't be\n * reached/authenticated (e.g. no `~/.vault-token`). Defaults to a `node-vault`\n * client using the local token, matching `libs/server/vault`.\n */\n createVault?: (env: NodeJS.ProcessEnv) => Promise<VaultReader | undefined>;\n}\n\n/**\n * Reads a field from a node-vault KV read result. KV v2 nests the fields under\n * `data.data`; KV v1 puts them directly on `data`. Narrows structurally.\n */\nconst readVaultField = (result: unknown, field: string): string | undefined => {\n if (!isRecord(result)) {\n return undefined;\n }\n const outer = result.data;\n if (!isRecord(outer)) {\n return undefined;\n }\n const fields = isRecord(outer.data) ? outer.data : outer;\n const value = fields[field];\n return typeof value === \"string\" && value !== \"\" ? value : undefined;\n};\n\n/**\n * Default Vault client: reads the local `~/.vault-token` and points `node-vault`\n * at the endpoint from `E2E_VAULT_ENDPOINT` (or `VAULT_ENDPOINT`).\n *\n * The Vault address is **never hardcoded** — it is supplied by the environment,\n * so any consumer of this package can opt into their own Vault. `node-vault` is\n * an optional peer dependency, imported lazily and only on the local path, so\n * consumers that only use the env-var (CI) path never load it.\n */\nconst defaultCreateVault = async (env: NodeJS.ProcessEnv): Promise<VaultReader | undefined> => {\n const home = env.HOME ?? os.homedir();\n let token: string;\n try {\n token = readFileSync(path.join(home, \".vault-token\"), \"utf-8\").trim();\n } catch {\n return undefined; // not logged in / not on VPN\n }\n if (token === \"\") {\n return undefined;\n }\n const endpoint = env.E2E_VAULT_ENDPOINT ?? env.VAULT_ENDPOINT;\n if (endpoint === undefined || endpoint === \"\") {\n throw new Error(\n \"E2E credentials: a Vault token was found but no endpoint — set E2E_VAULT_ENDPOINT (or VAULT_ENDPOINT) to your Vault address.\"\n );\n }\n // Imported lazily so consumers on the env-var path (and unit tests, which\n // inject `createVault`) never load the optional `node-vault` peer. Its absence\n // is an expected configuration state, so fail with the resolver's actionable,\n // redacted style instead of leaking a raw `ERR_MODULE_NOT_FOUND` stack.\n const nodeVault = await import(\"node-vault\").catch(() => undefined);\n if (nodeVault === undefined) {\n return fail(\n \"E2E credentials: install the optional `node-vault` peer dependency for local Vault lookup, \" +\n \"or set the E2E_PW_<SLUG> env variable.\"\n );\n }\n // `timeout` bounds the whole request so an off-VPN endpoint that black-holes\n // the connection fails fast (~10s) instead of hanging until the test times out.\n const client = nodeVault.default({\n endpoint,\n token,\n requestOptions: { strictSSL: true, timeout: 10_000 },\n });\n return { read: secretPath => client.read(secretPath) };\n};\n\n/**\n * Node/socket error codes that mean the Vault host could not be reached at all —\n * as opposed to a reachable Vault answering with a 403/404. Off-VPN, the internal\n * Vault hostname either fails DNS (`ENOTFOUND`/`EAI_AGAIN`) or the connection is\n * refused/timed out, so these map to an actionable \"connect to the VPN\" hint.\n */\nconst VAULT_UNREACHABLE_CODES: ReadonlySet<string> = new Set([\n \"ENOTFOUND\",\n \"EAI_AGAIN\",\n \"ECONNREFUSED\",\n \"ECONNRESET\",\n \"ETIMEDOUT\",\n \"ESOCKETTIMEDOUT\",\n \"EHOSTUNREACH\",\n \"ENETUNREACH\",\n]);\n\n/** True when a thrown Vault-read error is a transport failure (host unreachable), not an HTTP response. */\nconst isVaultUnreachableError = (error: unknown): boolean => {\n if (!isRecord(error)) {\n return false;\n }\n const code = error.code;\n return typeof code === \"string\" && VAULT_UNREACHABLE_CODES.has(code);\n};\n\n/**\n * HTTP statuses a *reachable* Vault returns when the caller's token is missing,\n * invalid, or expired — i.e. \"not logged in / not authenticated\" (as opposed to a\n * 404 for a real missing path). `node-vault` attaches the HTTP response to the\n * thrown error as `error.response.statusCode`, so we narrow to it structurally\n * rather than making an extra `tokenLookupSelf` round-trip.\n */\nconst VAULT_UNAUTHENTICATED_STATUS: ReadonlySet<number> = new Set([401, 403]);\n\n/** Extracts the HTTP status code from a thrown `node-vault` read error, if present. */\nconst getVaultErrorStatus = (error: unknown): number | undefined => {\n if (!isRecord(error)) {\n return undefined;\n }\n const response = error.response;\n if (!isRecord(response)) {\n return undefined;\n }\n const statusCode = response.statusCode;\n return typeof statusCode === \"number\" ? statusCode : undefined;\n};\n\n/** Throws an error whose message is routed through `redactSensitive` (defence in depth). */\nconst fail = (message: string): never => {\n throw new Error(redactSensitive(message));\n};\n\nconst passwordCache = new Map<string, string | undefined>();\n\n/**\n * Clears the in-memory resolved-password cache. The cache is a module-level\n * singleton that lives for the whole process, so a resolved value would\n * otherwise leak across calls (and across test cases). Call this between tests\n * to keep each `resolveE2EPassword` call independent.\n */\nexport const resetE2EPasswordCache = (): void => {\n passwordCache.clear();\n};\n\n/**\n * Resolves an account's E2E password by name.\n *\n * Order: env variable `E2E_PW_<SLUG>` (CI path, never touches Vault) -> Vault\n * field `<slug>` (local path). Throws a clear, account-named, redacted error on\n * miss. The password is never included in any error/diagnostic (on failure there\n * is no resolved value to leak).\n *\n * @param username - Account identifier from the fixture (`auth.json` `username`).\n * @param options - Process env and Vault-client seams.\n * @returns {Promise<string>} The resolved password.\n * @throws when the fixture is a placeholder, or neither env nor Vault yields a value.\n */\nexport const resolveE2EPassword = async (\n username: string,\n options: ResolveE2EPasswordOptions = {}\n): Promise<string> => {\n if (username === \"\" || username === \"REPLACE_ME\") {\n return fail(`E2E credentials: fixture account name is unresolved (\"${username}\").`);\n }\n const cached = passwordCache.get(username);\n if (cached !== undefined) {\n return cached;\n }\n\n const env = options.env ?? process.env;\n const key = deriveEnvKey(username);\n\n // CI path: an injected env variable wins and Vault is never contacted.\n const fromEnv = env[key];\n if (fromEnv !== undefined && fromEnv !== \"\") {\n passwordCache.set(username, fromEnv);\n return fromEnv;\n }\n\n // Local path: read the password from Vault by the same key.\n const createVault = options.createVault ?? defaultCreateVault;\n const vault = await createVault(env);\n if (vault === undefined) {\n return fail(\n `E2E credentials for \"${username}\": no env variable (${key}) and Vault is unavailable ` +\n `(not authenticated / not on VPN). Run \\`vault login -address=${env.E2E_VAULT_ENDPOINT} -method=oidc -path=okta\\` on VPN, or set the env variable.`\n );\n }\n\n const logicalPath = env.E2E_VAULT_PATH;\n if (!logicalPath) {\n return fail(\"E2E_VAULT_PATH is not set\");\n }\n const readPath = logicalPath;\n\n let result: unknown;\n try {\n result = await vault.read(readPath);\n } catch (error) {\n if (isVaultUnreachableError(error)) {\n return fail(\n `E2E credentials for \"${username}\": Vault endpoint is unreachable ` +\n `(likely not on VPN, or E2E_VAULT_ENDPOINT is misconfigured). ` +\n `Connect to the VPN and retry, or set the ${key} env variable, right now vault is trying to connect to ${env.E2E_VAULT_ENDPOINT}.`\n );\n }\n const status = getVaultErrorStatus(error);\n if (status !== undefined && VAULT_UNAUTHENTICATED_STATUS.has(status)) {\n return fail(\n `E2E credentials for \"${username}\": Vault rejected the request (HTTP ${status}) — ` +\n `your Vault token is missing or expired. ` +\n `Run something like \\`vault login -address=${env.E2E_VAULT_ENDPOINT} -method=oidc -path=okta\\` on VPN, or set the ${key} env variable, right now vault is trying to connect to a logged in vault at ${env.E2E_VAULT_ENDPOINT}.`\n );\n }\n return fail(\n `E2E credentials for \"${username}\": Vault read of \"${readPath}\" failed on host ${env.E2E_VAULT_ENDPOINT}.`\n );\n }\n\n const fromVault = readVaultField(result, deriveVaultField(username));\n if (fromVault === undefined) {\n return fail(\n `E2E credentials for \"${username}\": no field \"${deriveVaultField(username)}\" at Vault path \"${readPath}\".`\n );\n }\n passwordCache.set(username, fromVault);\n return fromVault;\n};\n"]}
|