@camunda/e2e-test-suite 0.0.1189 → 0.0.1191
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/dist/pages/8.10/LoginPage.js +12 -9
- package/dist/pages/8.10/OCIdentityHomePage.js +5 -1
- package/dist/pages/8.10/TaskPanelPage.d.ts +2 -3
- package/dist/pages/8.10/TaskPanelPage.js +50 -20
- package/dist/pages/8.10/UtilitiesPage.js +3 -7
- package/dist/pages/8.8/LoginPage.js +3 -2
- package/dist/pages/8.8/UtilitiesPage.js +3 -7
- package/dist/pages/8.9/LoginPage.js +8 -3
- package/dist/pages/8.9/UtilitiesPage.js +3 -7
- package/dist/utils/authErrorClassification.d.ts +8 -0
- package/dist/utils/authErrorClassification.js +95 -0
- package/dist/utils/utils.d.ts +2 -1
- package/dist/utils/utils.js +5 -13
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.LoginPage = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const UtilitiesPage_1 = require("./UtilitiesPage");
|
|
6
|
+
const authErrorClassification_1 = require("../../utils/authErrorClassification");
|
|
6
7
|
const userNameInputValidInputSchema = {
|
|
7
8
|
_schema: {
|
|
8
9
|
maxlength: 254,
|
|
@@ -47,9 +48,14 @@ class LoginPage {
|
|
|
47
48
|
// missing this detection entirely, so a rate-limited login here fell
|
|
48
49
|
// through to the generic 10-20s retry backoff instead of the 60-90s
|
|
49
50
|
// needed to outlast a typical rate-limit window.
|
|
51
|
+
//
|
|
52
|
+
// Kept as one locator because every caller wants to stop waiting on
|
|
53
|
+
// either page; which of the two rendered is decided at throw time by
|
|
54
|
+
// classifyAuthErrorPage, since only that distinction says whether the
|
|
55
|
+
// remedy is a higher limit or an allow-listed runner IP.
|
|
50
56
|
this.rateLimitError = page
|
|
51
|
-
.getByText(
|
|
52
|
-
.or(page.getByText(
|
|
57
|
+
.getByText(authErrorClassification_1.AUTH0_QUOTA_TEXT)
|
|
58
|
+
.or(page.getByText(authErrorClassification_1.AUTH0_GENERIC_ERROR_TEXT));
|
|
53
59
|
this.loginButton = page.getByRole('button', {
|
|
54
60
|
name: 'Continue',
|
|
55
61
|
exact: true,
|
|
@@ -106,13 +112,10 @@ class LoginPage {
|
|
|
106
112
|
}
|
|
107
113
|
// Surface a rate-limit page with the marker loginWithRetry keys its
|
|
108
114
|
// 60-90s backoff on, rather than burning the remaining attempts on a
|
|
109
|
-
// form the provider is not going to serve.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
.catch(() => false)) {
|
|
114
|
-
throw new Error('AUTH0_RATE_LIMIT: identity provider returned an error page');
|
|
115
|
-
}
|
|
115
|
+
// form the provider is not going to serve. The marker also records
|
|
116
|
+
// which of the two Auth0 failure modes this was, and the error copy
|
|
117
|
+
// behind it, so a recurrence can be diagnosed from the run log alone.
|
|
118
|
+
await (0, authErrorClassification_1.throwIfAuthErrorPage)(this.page);
|
|
116
119
|
// Neither the password step nor the email step is on screen: some
|
|
117
120
|
// interstitial we do not model. Hand back to fillPassword's own wait
|
|
118
121
|
// instead of re-typing blindly into a page we cannot identify.
|
|
@@ -16,8 +16,12 @@ class OCIdentityHomePage {
|
|
|
16
16
|
noPermissionsMessage;
|
|
17
17
|
constructor(page) {
|
|
18
18
|
this.page = page;
|
|
19
|
+
// The Admin redesign (new design system, enabled by default on SaaS) moved
|
|
20
|
+
// the header off the old Carbon shell: its logo link now carries the static
|
|
21
|
+
// aria-label "Camunda Admin" (AppHeaderV2 in camunda/camunda's
|
|
22
|
+
// identity/client), replacing the old "Camunda logo Admin" accessible name.
|
|
19
23
|
this.adminBanner = page.getByRole('link', {
|
|
20
|
-
name: 'Camunda
|
|
24
|
+
name: 'Camunda Admin',
|
|
21
25
|
});
|
|
22
26
|
this.groupsTab = page
|
|
23
27
|
.getByRole('banner')
|
|
@@ -2,17 +2,16 @@ import { Page, Locator } from '@playwright/test';
|
|
|
2
2
|
declare class TaskPanelPage {
|
|
3
3
|
private page;
|
|
4
4
|
readonly availableTasks: Locator;
|
|
5
|
-
readonly
|
|
5
|
+
readonly filterSelectButton: Locator;
|
|
6
6
|
readonly taskListPageBanner: Locator;
|
|
7
7
|
readonly processesPageTab: Locator;
|
|
8
8
|
readonly tasksTab: Locator;
|
|
9
9
|
readonly noPermissionsMessage: Locator;
|
|
10
|
-
readonly expandButton: Locator;
|
|
11
10
|
readonly processesInfoText: Locator;
|
|
12
11
|
constructor(page: Page);
|
|
13
12
|
taskInList(name: string): Locator;
|
|
14
13
|
openTask(name: string, totalTimeout?: number): Promise<void>;
|
|
15
|
-
filterBy(option: 'All open' | 'Unassigned' | 'Assigned to me' | 'Completed'): Promise<void>;
|
|
14
|
+
filterBy(option: 'All open tasks' | 'Unassigned' | 'Assigned to me' | 'Completed'): Promise<void>;
|
|
16
15
|
taskCount(name: string): Promise<number>;
|
|
17
16
|
clickTasksTab(): Promise<void>;
|
|
18
17
|
scrollToLastTask(name: string): Promise<void>;
|
|
@@ -8,17 +8,25 @@ const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLoc
|
|
|
8
8
|
class TaskPanelPage {
|
|
9
9
|
page;
|
|
10
10
|
availableTasks;
|
|
11
|
-
|
|
11
|
+
filterSelectButton;
|
|
12
12
|
taskListPageBanner;
|
|
13
13
|
processesPageTab;
|
|
14
14
|
tasksTab;
|
|
15
15
|
noPermissionsMessage;
|
|
16
|
-
expandButton;
|
|
17
16
|
processesInfoText;
|
|
18
17
|
constructor(page) {
|
|
19
18
|
this.page = page;
|
|
20
19
|
this.availableTasks = page.getByTitle('Available tasks');
|
|
21
|
-
|
|
20
|
+
// The old Carbon expandable filter sidebar (aria-controls="task-nav-bar",
|
|
21
|
+
// "Expand to show filters") is gone after Tasklist's redesign. Filtering is
|
|
22
|
+
// now a single dropdown-trigger button (id="filter-select") whose
|
|
23
|
+
// accessible name is the static "Filters" aria-label; clicking it opens a
|
|
24
|
+
// Radix DropdownMenu of filter options (matches the current locator in
|
|
25
|
+
// camunda/camunda's own qa/c8-orchestration-cluster-e2e-test-suite).
|
|
26
|
+
this.filterSelectButton = page.getByRole('button', {
|
|
27
|
+
name: 'Filters',
|
|
28
|
+
exact: true,
|
|
29
|
+
});
|
|
22
30
|
// The header logo's accessible name is now just "Camunda logo" -- the
|
|
23
31
|
// per-app suffix ("Camunda logo Tasklist") was dropped when Tasklist's
|
|
24
32
|
// header moved off the old Carbon shell (matches the already-corrected
|
|
@@ -29,9 +37,6 @@ class TaskPanelPage {
|
|
|
29
37
|
this.processesPageTab = page.getByRole('link', { name: 'Processes' });
|
|
30
38
|
this.tasksTab = page.getByRole('link', { name: 'Tasks' });
|
|
31
39
|
this.noPermissionsMessage = page.getByText('You don’t have access to this component');
|
|
32
|
-
this.expandButton = page.getByRole('button', {
|
|
33
|
-
name: 'Expand to show filters',
|
|
34
|
-
});
|
|
35
40
|
this.processesInfoText = page.getByText('Browse and run processes published by your organization');
|
|
36
41
|
}
|
|
37
42
|
// Tasklist's importer can lag well behind Zeebe under heavy concurrent load
|
|
@@ -82,21 +87,46 @@ class TaskPanelPage {
|
|
|
82
87
|
`${totalTimeout}ms: ${lastError}`);
|
|
83
88
|
}
|
|
84
89
|
async filterBy(option) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
// Open the always-visible filter dropdown, then pick the option from the
|
|
91
|
+
// Radix menu that appears (role="menuitem"). There is no separate
|
|
92
|
+
// expand/collapse step like the old Carbon sidebar had.
|
|
93
|
+
//
|
|
94
|
+
// The trigger also carries the active filter's label, so it doubles as
|
|
95
|
+
// the "selection applied" signal, replacing the heading the old Carbon
|
|
96
|
+
// panel rendered for the active filter.
|
|
97
|
+
//
|
|
98
|
+
// TasksLayoutPage renders <Filters disabled={isPending} />, so a task
|
|
99
|
+
// list refetch landing while the menu is open disables the trigger and
|
|
100
|
+
// tears the menu down before Radix runs the item's onSelect: the menu
|
|
101
|
+
// closes but the filter never changes. Re-open and pick again rather
|
|
102
|
+
// than returning with the list still on the previous filter (same
|
|
103
|
+
// bounded retry as pages/c8Run-8.10/TaskPanelPage.ts).
|
|
104
|
+
const menuItem = this.page.getByRole('menuitem', {
|
|
105
|
+
name: option,
|
|
106
|
+
exact: true,
|
|
107
|
+
});
|
|
108
|
+
const triggerShowingOption = this.filterSelectButton.filter({
|
|
109
|
+
hasText: new RegExp(`^${option}$`),
|
|
110
|
+
});
|
|
111
|
+
const maxAttempts = 3;
|
|
112
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
113
|
+
await (0, test_1.expect)(this.filterSelectButton).toBeEnabled({ timeout: 60000 });
|
|
114
|
+
await this.filterSelectButton.click();
|
|
115
|
+
await (0, test_1.expect)(menuItem).toBeVisible({ timeout: 10000 });
|
|
116
|
+
await menuItem.click();
|
|
117
|
+
const applied = await triggerShowingOption
|
|
118
|
+
.waitFor({ state: 'visible', timeout: 15000 })
|
|
119
|
+
.then(() => true)
|
|
120
|
+
.catch(() => false);
|
|
121
|
+
if (applied) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
console.warn(`Attempt ${attempt} to apply the "${option}" task filter did not ` +
|
|
125
|
+
'take effect. Retrying...');
|
|
126
|
+
await this.page.keyboard.press('Escape');
|
|
127
|
+
await (0, test_1.expect)(this.filterSelectButton).toHaveAttribute('aria-expanded', 'false', { timeout: 10000 });
|
|
99
128
|
}
|
|
129
|
+
throw new Error(`Task filter "${option}" was not applied after ${maxAttempts} attempts`);
|
|
100
130
|
}
|
|
101
131
|
async taskCount(name) {
|
|
102
132
|
await (0, sleep_1.sleep)(20000);
|
|
@@ -6,6 +6,7 @@ const ModelerHomePage_1 = require("./ModelerHomePage");
|
|
|
6
6
|
const HomePage_1 = require("./HomePage");
|
|
7
7
|
const sleep_1 = require("../../utils/sleep");
|
|
8
8
|
const randomSleep_1 = require("../../utils/randomSleep");
|
|
9
|
+
const authErrorClassification_1 = require("../../utils/authErrorClassification");
|
|
9
10
|
const LoginPage_1 = require("./LoginPage");
|
|
10
11
|
const fileUpload_1 = require("../../utils/fileUpload");
|
|
11
12
|
const mailSlurpClient_1 = require("../../utils/mailSlurpClient");
|
|
@@ -53,12 +54,7 @@ async function waitForLoginForm(page, loginPage, totalTimeout = 90000, roundTime
|
|
|
53
54
|
// whole budget waiting for a login form that will never appear. Bailing
|
|
54
55
|
// out immediately with a typed marker lets loginWithRetry's caller back
|
|
55
56
|
// off long enough to actually outlast the rate-limit window instead.
|
|
56
|
-
|
|
57
|
-
.first()
|
|
58
|
-
.isVisible()
|
|
59
|
-
.catch(() => false)) {
|
|
60
|
-
throw new Error('AUTH0_RATE_LIMIT: identity provider returned an error page');
|
|
61
|
-
}
|
|
57
|
+
await (0, authErrorClassification_1.throwIfAuthErrorPage)(page);
|
|
62
58
|
try {
|
|
63
59
|
// Confirm we reached the login page before filling credentials.
|
|
64
60
|
// Without this check the locator for 'Email address' can match a hidden
|
|
@@ -172,7 +168,7 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
|
|
|
172
168
|
catch (error) {
|
|
173
169
|
lastError = error;
|
|
174
170
|
if (attempt < maxRetries - 1) {
|
|
175
|
-
const rateLimited = String(error).includes(
|
|
171
|
+
const rateLimited = String(error).includes(authErrorClassification_1.AUTH0_RATE_LIMIT);
|
|
176
172
|
console.warn(`Attempt ${attempt + 1} failed for logging in. Retrying with clean session...`, error);
|
|
177
173
|
if (rateLimited) {
|
|
178
174
|
// Auth0 rate-limit windows commonly last 30-90s. Back off long
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.LoginPage = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
|
+
const authErrorClassification_1 = require("../../utils/authErrorClassification");
|
|
5
6
|
const UtilitiesPage_1 = require("./UtilitiesPage");
|
|
6
7
|
const sessionRecovery_1 = require("../../utils/sessionRecovery");
|
|
7
8
|
class LoginPage {
|
|
@@ -37,8 +38,8 @@ class LoginPage {
|
|
|
37
38
|
});
|
|
38
39
|
this.errorMessage = page.getByText('Wrong email or password');
|
|
39
40
|
this.rateLimitError = page
|
|
40
|
-
.getByText(
|
|
41
|
-
.or(page.getByText(
|
|
41
|
+
.getByText(authErrorClassification_1.AUTH0_QUOTA_TEXT)
|
|
42
|
+
.or(page.getByText(authErrorClassification_1.AUTH0_GENERIC_ERROR_TEXT));
|
|
42
43
|
}
|
|
43
44
|
async fillUsername(username) {
|
|
44
45
|
await this.usernameInput.waitFor({ state: 'visible', timeout: 60000 });
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.waitForLoadingToFinish = exports.assertTestUsesCorrectOrganizationFromModeler = exports.expectCountToBeOneOf = exports.createAPIClient = exports.modelAndRunConnectorsTimerEventDiagram = exports.modelAndRunConnectorsDocHandlingDiagram = exports.deleteCluster = exports.deleteAllUserGroups = exports.assertTestUsesCorrectOrganization = exports.clickInvitationLinkInEmail = exports.assertLatestAlertEmail = exports.enableAuthorizations = exports.disableRBA = exports.runMultipleProcesses = exports.modelDiagramFromFile = exports.runProcess = exports.completeTaskWithRetry = exports.assertPageTextWithRetry = exports.assertLocatorVisibleWithRetry = exports.modelRestConnector = exports.loginWithRetry = exports.forceLogoutIfNeeded = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
|
+
const authErrorClassification_1 = require("../../utils/authErrorClassification");
|
|
5
6
|
const ModelerHomePage_1 = require("./ModelerHomePage");
|
|
6
7
|
const HomePage_1 = require("./HomePage");
|
|
7
8
|
const sleep_1 = require("../../utils/sleep");
|
|
@@ -80,12 +81,7 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
|
|
|
80
81
|
// the usual auth controls. Bail out early on it so the outer retry backs
|
|
81
82
|
// off long enough to outlast the window instead of burning attempts
|
|
82
83
|
// against a 60s wait for a login form that will never render.
|
|
83
|
-
|
|
84
|
-
.first()
|
|
85
|
-
.isVisible()
|
|
86
|
-
.catch(() => false)) {
|
|
87
|
-
throw new Error('AUTH0_RATE_LIMIT: identity provider returned an error page');
|
|
88
|
-
}
|
|
84
|
+
await (0, authErrorClassification_1.throwIfAuthErrorPage)(page);
|
|
89
85
|
await (0, test_1.expect)(loginPage.loginMessage.or(loginPage.passwordHeading)).toBeVisible({ timeout: 60000 });
|
|
90
86
|
await loginPage.loginWithTestUser(testUser);
|
|
91
87
|
return;
|
|
@@ -93,7 +89,7 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
|
|
|
93
89
|
catch (error) {
|
|
94
90
|
lastError = error;
|
|
95
91
|
if (attempt < maxRetries - 1) {
|
|
96
|
-
const rateLimited = String(error).includes(
|
|
92
|
+
const rateLimited = String(error).includes(authErrorClassification_1.AUTH0_RATE_LIMIT);
|
|
97
93
|
console.warn(`Attempt ${attempt + 1} failed for logging in. Retrying with clean session...`, error);
|
|
98
94
|
if (rateLimited) {
|
|
99
95
|
// Auth0 rate-limit windows commonly last 30-90s. Back off long
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.LoginPage = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
|
+
const authErrorClassification_1 = require("../../utils/authErrorClassification");
|
|
5
6
|
const UtilitiesPage_1 = require("./UtilitiesPage");
|
|
6
7
|
const userNameInputValidInputSchema = {
|
|
7
8
|
_schema: {
|
|
@@ -46,8 +47,8 @@ class LoginPage {
|
|
|
46
47
|
});
|
|
47
48
|
this.errorMessage = page.getByText('Wrong email or password');
|
|
48
49
|
this.rateLimitError = page
|
|
49
|
-
.getByText(
|
|
50
|
-
.or(page.getByText(
|
|
50
|
+
.getByText(authErrorClassification_1.AUTH0_QUOTA_TEXT)
|
|
51
|
+
.or(page.getByText(authErrorClassification_1.AUTH0_GENERIC_ERROR_TEXT));
|
|
51
52
|
}
|
|
52
53
|
async fillUsername(username) {
|
|
53
54
|
await (0, test_1.expect)(this.usernameInput.locator).toBeVisible({ timeout: 120000 });
|
|
@@ -127,7 +128,11 @@ class LoginPage {
|
|
|
127
128
|
.catch(() => false),
|
|
128
129
|
]);
|
|
129
130
|
if (rateLimitVisible) {
|
|
130
|
-
|
|
131
|
+
// Classify which of the two Auth0 failure modes this was, and capture
|
|
132
|
+
// the error copy, so a recurrence is diagnosable from the run log.
|
|
133
|
+
// Falls back to the bare marker if the page moved on in between.
|
|
134
|
+
throw new Error((await (0, authErrorClassification_1.classifyAuthErrorPage)(this.page)) ??
|
|
135
|
+
`${authErrorClassification_1.AUTH0_RATE_LIMIT}: identity provider returned an error page`);
|
|
131
136
|
}
|
|
132
137
|
if (passwordHeadingVisible || passwordInputVisible) {
|
|
133
138
|
authStep = 'password';
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.waitForLoadingToFinish = exports.assertTestUsesCorrectOrganizationFromModeler = exports.expectCountToBeOneOf = exports.createAPIClient = exports.modelAndRunConnectorsTimerEventDiagram = exports.modelAndRunConnectorsDocHandlingDiagram = exports.deleteCluster = exports.deleteAllUserGroups = exports.assertTestUsesCorrectOrganization = exports.assertLatestAlertEmail = exports.clickInvitationLinkInEmail = exports.enableAuthorizations = exports.disableRBA = exports.runMultipleProcesses = exports.runProcess = exports.completeTaskWithRetry = exports.assertPageTextWithRetry = exports.assertLocatorVisibleWithRetry = exports.modelRestConnector = exports.modelDiagramFromFile = exports.loginWithRetry = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
|
+
const authErrorClassification_1 = require("../../utils/authErrorClassification");
|
|
5
6
|
const ModelerHomePage_1 = require("./ModelerHomePage");
|
|
6
7
|
const HomePage_1 = require("./HomePage");
|
|
7
8
|
const sleep_1 = require("../../utils/sleep");
|
|
@@ -51,12 +52,7 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
|
|
|
51
52
|
// The identity provider occasionally returns a generic error / rate
|
|
52
53
|
// limit page that has none of the usual auth controls. Bail out early
|
|
53
54
|
// when that page is visible so the outer retry can back off.
|
|
54
|
-
|
|
55
|
-
.first()
|
|
56
|
-
.isVisible()
|
|
57
|
-
.catch(() => false)) {
|
|
58
|
-
throw new Error('AUTH0_RATE_LIMIT: identity provider returned an error page');
|
|
59
|
-
}
|
|
55
|
+
await (0, authErrorClassification_1.throwIfAuthErrorPage)(page);
|
|
60
56
|
// Auth0 may restore directly on password entry depending on session
|
|
61
57
|
// history, so allow either identifier screen or password screen.
|
|
62
58
|
await (0, test_1.expect)(loginPage.loginMessage.or(loginPage.passwordHeading)).toBeVisible({ timeout: 60000 });
|
|
@@ -66,7 +62,7 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
|
|
|
66
62
|
catch (error) {
|
|
67
63
|
lastError = error;
|
|
68
64
|
if (attempt < maxRetries - 1) {
|
|
69
|
-
const rateLimited = String(error).includes(
|
|
65
|
+
const rateLimited = String(error).includes(authErrorClassification_1.AUTH0_RATE_LIMIT);
|
|
70
66
|
console.warn(`Attempt ${attempt + 1} failed for logging in. Retrying with clean session...`, error);
|
|
71
67
|
if (rateLimited) {
|
|
72
68
|
// Auth0 rate-limit windows commonly last 30-90s. Back off long
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Page } from '@playwright/test';
|
|
2
|
+
export declare const AUTH0_RATE_LIMIT = "AUTH0_RATE_LIMIT";
|
|
3
|
+
export declare const AUTH0_RATE_LIMIT_QUOTA = "AUTH0_RATE_LIMIT_QUOTA";
|
|
4
|
+
export declare const AUTH0_RATE_LIMIT_GENERIC = "AUTH0_RATE_LIMIT_GENERIC";
|
|
5
|
+
export declare const AUTH0_QUOTA_TEXT: RegExp;
|
|
6
|
+
export declare const AUTH0_GENERIC_ERROR_TEXT: RegExp;
|
|
7
|
+
export declare function classifyAuthErrorPage(page: Page): Promise<string | null>;
|
|
8
|
+
export declare function throwIfAuthErrorPage(page: Page): Promise<void>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.throwIfAuthErrorPage = exports.classifyAuthErrorPage = exports.AUTH0_GENERIC_ERROR_TEXT = exports.AUTH0_QUOTA_TEXT = exports.AUTH0_RATE_LIMIT_GENERIC = exports.AUTH0_RATE_LIMIT_QUOTA = exports.AUTH0_RATE_LIMIT = void 0;
|
|
4
|
+
// Auth0 renders an error page for two structurally different failures, and
|
|
5
|
+
// until now both were counted as one signal. They need different responses:
|
|
6
|
+
//
|
|
7
|
+
// QUOTA - the tenant's request rate limit ("...rate limit has been
|
|
8
|
+
// reached"). This is what moving the tenant to a Production
|
|
9
|
+
// environment raises.
|
|
10
|
+
// GENERIC - Auth0's catch-all error page. Attack Protection blocks
|
|
11
|
+
// (suspicious IP throttling, brute-force protection) surface here,
|
|
12
|
+
// and those are governed by their own settings rather than the
|
|
13
|
+
// tenant's rate limits, so a Production environment does nothing
|
|
14
|
+
// for them. A CI runner signing in as ~19 freshly created accounts
|
|
15
|
+
// from one egress IP inside a few minutes is precisely the pattern
|
|
16
|
+
// they exist to detect.
|
|
17
|
+
//
|
|
18
|
+
// Telling them apart matters because the remedies do not overlap: one is a
|
|
19
|
+
// limit to raise, the other is a source IP to allow-list. Counting them as one
|
|
20
|
+
// number is why we could not say which we were hitting.
|
|
21
|
+
//
|
|
22
|
+
// Both markers keep the AUTH0_RATE_LIMIT prefix, so the existing
|
|
23
|
+
// String(error).includes(AUTH0_RATE_LIMIT) backoff checks match unchanged.
|
|
24
|
+
exports.AUTH0_RATE_LIMIT = 'AUTH0_RATE_LIMIT';
|
|
25
|
+
exports.AUTH0_RATE_LIMIT_QUOTA = `${exports.AUTH0_RATE_LIMIT}_QUOTA`;
|
|
26
|
+
exports.AUTH0_RATE_LIMIT_GENERIC = `${exports.AUTH0_RATE_LIMIT}_GENERIC`;
|
|
27
|
+
exports.AUTH0_QUOTA_TEXT = /rate limit has been reached/i;
|
|
28
|
+
exports.AUTH0_GENERIC_ERROR_TEXT = /We are sorry, an error occurred/i;
|
|
29
|
+
const MAX_DETAIL_CHARS = 300;
|
|
30
|
+
// The error page's own copy and error code are what separate the two
|
|
31
|
+
// mechanisms, so capture them rather than only the fact that some error
|
|
32
|
+
// rendered. Deliberately narrow: host plus path, and Auth0's two error query
|
|
33
|
+
// params, never the whole URL - on an OAuth redirect that can carry `code` and
|
|
34
|
+
// `state`, which do not belong in a CI log.
|
|
35
|
+
async function readErrorContext(page) {
|
|
36
|
+
const parts = [];
|
|
37
|
+
try {
|
|
38
|
+
const url = new URL(page.url());
|
|
39
|
+
parts.push(`${url.host}${url.pathname}`);
|
|
40
|
+
for (const key of ['error', 'error_description']) {
|
|
41
|
+
const value = url.searchParams.get(key);
|
|
42
|
+
if (value) {
|
|
43
|
+
parts.push(`${key}=${value}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// page.url() is not always parsable (about:blank on a torn-down context).
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const text = await page.locator('body').innerText({ timeout: 5000 });
|
|
52
|
+
const collapsed = text.replace(/\s+/g, ' ').trim();
|
|
53
|
+
if (collapsed) {
|
|
54
|
+
parts.push(`text="${collapsed.slice(0, MAX_DETAIL_CHARS)}"`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Best-effort only: this runs on a failure path and must never replace
|
|
59
|
+
// the caller's diagnosis with its own.
|
|
60
|
+
}
|
|
61
|
+
return parts.join(' | ');
|
|
62
|
+
}
|
|
63
|
+
// Returns the message to throw when an Auth0 error page is on screen, or null
|
|
64
|
+
// when it is not. Never throws, for the reason above.
|
|
65
|
+
async function classifyAuthErrorPage(page) {
|
|
66
|
+
const [quota, generic] = await Promise.all([
|
|
67
|
+
page
|
|
68
|
+
.getByText(exports.AUTH0_QUOTA_TEXT)
|
|
69
|
+
.first()
|
|
70
|
+
.isVisible()
|
|
71
|
+
.catch(() => false),
|
|
72
|
+
page
|
|
73
|
+
.getByText(exports.AUTH0_GENERIC_ERROR_TEXT)
|
|
74
|
+
.first()
|
|
75
|
+
.isVisible()
|
|
76
|
+
.catch(() => false),
|
|
77
|
+
]);
|
|
78
|
+
if (!quota && !generic) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
// Quota wins when both match: its copy is the specific diagnosis, and the
|
|
82
|
+
// generic banner can wrap it.
|
|
83
|
+
const marker = quota ? exports.AUTH0_RATE_LIMIT_QUOTA : exports.AUTH0_RATE_LIMIT_GENERIC;
|
|
84
|
+
const context = await readErrorContext(page);
|
|
85
|
+
const summary = `${marker}: identity provider returned an error page`;
|
|
86
|
+
return context ? `${summary} | ${context}` : summary;
|
|
87
|
+
}
|
|
88
|
+
exports.classifyAuthErrorPage = classifyAuthErrorPage;
|
|
89
|
+
async function throwIfAuthErrorPage(page) {
|
|
90
|
+
const message = await classifyAuthErrorPage(page);
|
|
91
|
+
if (message) {
|
|
92
|
+
throw new Error(message);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.throwIfAuthErrorPage = throwIfAuthErrorPage;
|
package/dist/utils/utils.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { Page } from '@playwright/test';
|
|
2
|
-
|
|
2
|
+
import { AUTH0_RATE_LIMIT } from './authErrorClassification';
|
|
3
|
+
export { AUTH0_RATE_LIMIT };
|
|
3
4
|
export declare function loginToApp(appName: string, page: Page): Promise<void>;
|
package/dist/utils/utils.js
CHANGED
|
@@ -3,16 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.loginToApp = exports.AUTH0_RATE_LIMIT = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const LoginPage_1 = require("../pages/8.8/LoginPage");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const rateLimited = await loginPage.rateLimitError
|
|
9
|
-
.first()
|
|
10
|
-
.isVisible()
|
|
11
|
-
.catch(() => false);
|
|
12
|
-
if (rateLimited) {
|
|
13
|
-
throw new Error(`${exports.AUTH0_RATE_LIMIT}: identity provider returned an error page`);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
6
|
+
const authErrorClassification_1 = require("./authErrorClassification");
|
|
7
|
+
Object.defineProperty(exports, "AUTH0_RATE_LIMIT", { enumerable: true, get: function () { return authErrorClassification_1.AUTH0_RATE_LIMIT; } });
|
|
16
8
|
async function loginToApp(appName, page) {
|
|
17
9
|
await page.goto(appName, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
18
10
|
// 8.8 SaaS occasionally experiences 429s on app.js during navigation rendering,
|
|
@@ -25,13 +17,13 @@ async function loginToApp(appName, page) {
|
|
|
25
17
|
// caller can tell a rate limit apart from a slow login form, and the caller
|
|
26
18
|
// needs that distinction to pick a backoff long enough to outlast the window.
|
|
27
19
|
await (0, test_1.expect)(loginPage.usernameInput.or(loginPage.rateLimitError.first())).toBeVisible({ timeout: 60000 });
|
|
28
|
-
await
|
|
20
|
+
await (0, authErrorClassification_1.throwIfAuthErrorPage)(page);
|
|
29
21
|
await loginPage.fillUsername(process.env.C8_USERNAME);
|
|
30
22
|
await (0, test_1.expect)(loginPage.usernameInput).toHaveValue(process.env.C8_USERNAME);
|
|
31
23
|
await (0, test_1.expect)(loginPage.loginMessage).toBeVisible();
|
|
32
24
|
await loginPage.clickContinueButton();
|
|
33
25
|
await (0, test_1.expect)(loginPage.passwordInput.or(loginPage.rateLimitError.first())).toBeVisible({ timeout: 90000 });
|
|
34
|
-
await
|
|
26
|
+
await (0, authErrorClassification_1.throwIfAuthErrorPage)(page);
|
|
35
27
|
await loginPage.fillPassword(process.env.C8_PASSWORD);
|
|
36
28
|
await (0, test_1.expect)(loginPage.passwordHeading).toBeVisible({ timeout: 90000 });
|
|
37
29
|
await loginPage.clickLoginButton();
|
|
@@ -44,7 +36,7 @@ async function loginToApp(appName, page) {
|
|
|
44
36
|
catch (error) {
|
|
45
37
|
// Auth0 can also serve the rate-limit page as the response to the password
|
|
46
38
|
// submit, in which case the redirect back to appName never happens.
|
|
47
|
-
await
|
|
39
|
+
await (0, authErrorClassification_1.throwIfAuthErrorPage)(page);
|
|
48
40
|
throw error;
|
|
49
41
|
}
|
|
50
42
|
await page
|