Skip to content

Commit 429a036

Browse files
authored
Review & fix users e2e tests flakiness (#3378)
* keep artifacts to check where tests are failing * run only totp test * try waiting for endpoints after actions * add profile wait * try arbitrary waits * add 2 more waits * remove two last explicit waits * reduce waits and add one more after typing totp * increase again to 1000 * make switch validation dependant on type totp code * add screenshots in test reports * add screenshots artifacts in e2e flaky test analysis * make sure to wait for newly issued code * custom waits * wait for totp enrollment * modify validations order to make test more consistent * fix rest of totp tests * modify selector for disable totp feature * disable python setup and execute all tests * remove python setup and fix check item menu test * restore delete artifacts after analysis * pr changes request
1 parent 36963b9 commit 429a036

File tree

5 files changed

+82
-59
lines changed

5 files changed

+82
-59
lines changed

.github/workflows/e2e-flaky-tests-detection.yaml

-4
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,6 @@ jobs:
164164
uses: actions/setup-node@v4
165165
with:
166166
node-version: ${{ env.NODE_VERSION }}
167-
- name: Set up Python
168-
uses: actions/setup-python@v5
169-
with:
170-
python-version: ${{ env.PYTHON_VERSION }}
171167
- name: Retrieve Cached Dependencies
172168
uses: actions/cache@v4
173169
id: mix-cache

test/e2e/cypress/e2e/users.cy.js

+26-24
Original file line numberDiff line numberDiff line change
@@ -226,15 +226,15 @@ describe('Users', () => {
226226

227227
it('should complete TOTP enrollment properly', () => {
228228
usersPage.clickAuthenticatorAppSwitch();
229-
usersPage.typeUserTotpCode();
229+
usersPage.getSecretAndTypeTotpCode();
230230
usersPage.clickVerifyTotpButton();
231231
usersPage.authenticatorAppSwitchIsEnabled();
232232
usersPage.totpEnabledToasterIsDisplayed();
233233
});
234234

235235
it('should fail to login if TOTP code is not given', () => {
236236
usersPage.clickAuthenticatorAppSwitch();
237-
usersPage.typeUserTotpCode();
237+
usersPage.getSecretAndTypeTotpCode();
238238
usersPage.clickVerifyTotpButton();
239239
loginPage.loginFailsIfOtpNotProvided(
240240
usersPage.USER.username,
@@ -244,40 +244,42 @@ describe('Users', () => {
244244

245245
it('should disable TOTP authentication and check login works without TOTP', () => {
246246
usersPage.clickAuthenticatorAppSwitch();
247-
usersPage.typeUserTotpCode();
247+
usersPage.getSecretAndTypeTotpCode();
248248
usersPage.clickVerifyTotpButton();
249249
usersPage.clickAuthenticatorAppSwitch();
250250
usersPage.clickDisableTotpButton();
251251
loginPage.loginShouldSucceed(usersPage.USER.username, usersPage.PASSWORD);
252252
});
253253

254-
it('should reconfigure TOTP and validate login cases', () => {
254+
it('should configure TOTP and validate login cases, then disable it', () => {
255255
usersPage.clickAuthenticatorAppSwitch();
256-
usersPage.typeUserTotpCode();
257-
usersPage.clickVerifyTotpButton();
258-
usersPage.clickAuthenticatorAppSwitch();
259-
usersPage.clickDisableTotpButton();
260-
usersPage.clickAuthenticatorAppSwitch();
261-
usersPage.typeUserTotpCode().then((totpSecret) => {
262-
usersPage.clickVerifyTotpButton();
263-
usersPage.authenticatorAppSwitchIsEnabled();
264-
usersPage.clickSignOutButton();
265-
loginPage.login(usersPage.USER.username, usersPage.PASSWORD);
266-
loginPage.typeInvalidLoginTotpCode();
267-
loginPage.clickSubmitLoginButton();
268-
loginPage.invalidCredentialsErrorIsDisplayed();
269-
loginPage.typeAlreadyUsedTotpCode(totpSecret);
270-
loginPage.clickSubmitLoginButton();
271-
loginPage.invalidCredentialsErrorIsDisplayed();
272-
loginPage.waitForNewTotpCodeAndTypeIt(totpSecret);
273-
loginPage.clickSubmitLoginButton();
274-
dashboardPage.dashboardPageIsDisplayed();
256+
usersPage.getTotpSecret().then((totpSecret) => {
257+
usersPage.typeUserTotpCode(totpSecret).then((code) => {
258+
usersPage.clickVerifyTotpButton();
259+
usersPage.authenticatorAppSwitchIsEnabled();
260+
usersPage.clickSignOutButton();
261+
loginPage.login(usersPage.USER.username, usersPage.PASSWORD);
262+
loginPage.typeInvalidLoginTotpCode();
263+
loginPage.clickSubmitLoginButton();
264+
loginPage.invalidCredentialsErrorIsDisplayed();
265+
loginPage.typeAlreadyUsedTotpCode(code);
266+
loginPage.clickSubmitLoginButton();
267+
loginPage.invalidCredentialsErrorIsDisplayed();
268+
loginPage.typeLoginTotpCode(totpSecret);
269+
loginPage.clickSubmitLoginButton();
270+
dashboardPage.dashboardPageIsDisplayed();
271+
usersPage.visit('/profile');
272+
usersPage.clickAuthenticatorAppSwitch();
273+
usersPage.clickDisableTotpButton();
274+
usersPage.clickAuthenticatorAppSwitch();
275+
usersPage.newIssuedTotpSecretIsDifferent();
276+
});
275277
});
276278
});
277279

278280
it('should be disabled by admin user', () => {
279281
usersPage.clickAuthenticatorAppSwitch();
280-
usersPage.typeUserTotpCode();
282+
usersPage.getSecretAndTypeTotpCode();
281283
usersPage.clickVerifyTotpButton();
282284
usersPage.clickSignOutButton();
283285
usersPage.apiLoginAndCreateSession();

test/e2e/cypress/pageObject/base_po.js

+32-18
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,36 @@ export const clickUserDropdownProfileButton = () => {
7171
return cy.get(userDropdownProfileButton).click();
7272
};
7373

74-
export const typeTotpCode = (totpSecret, inputField) => {
75-
const { otp } = TOTP.generate(totpSecret);
76-
return cy
77-
.get(inputField)
78-
.clear()
79-
.type(otp)
80-
.then(() => totpSecret);
74+
const _getTotpWaitTime = (forceNext) => {
75+
const currentTime = Date.now();
76+
const totpDuration = 30000;
77+
const minimumRemainingTimeToReuse = 10000;
78+
const expirationTime = Math.ceil(currentTime / totpDuration) * totpDuration;
79+
const remainingTime = Math.floor(expirationTime - currentTime);
80+
81+
const totpWaitingTime =
82+
forceNext === false && remainingTime > minimumRemainingTimeToReuse
83+
? 0
84+
: remainingTime;
85+
86+
return totpWaitingTime;
87+
};
88+
89+
export const typeNextGeneratedTotpCode = (
90+
totpSecret,
91+
inputField,
92+
forceNext = false
93+
) => {
94+
const timeToWait = _getTotpWaitTime(forceNext);
95+
96+
return cy.wait(timeToWait).then(() => {
97+
const { otp } = TOTP.generate(totpSecret);
98+
return cy
99+
.get(inputField)
100+
.clear()
101+
.type(otp)
102+
.then(() => otp);
103+
});
81104
};
82105

83106
// UI Validations
@@ -100,17 +123,8 @@ export const validateItemNotPresentInNavigationMenu = (itemName) => {
100123
});
101124
};
102125

103-
export const validateItemPresentInNavigationMenu = (navigationMenuItem) => {
104-
return cy.get(navigation.navigationItems).then(($elements) => {
105-
const itemFound = Array.from($elements).some((element) =>
106-
element.innerText.includes(navigationMenuItem)
107-
);
108-
expect(
109-
itemFound,
110-
`"${navigationMenuItem}" navigation item should be present`
111-
).to.be.true;
112-
});
113-
};
126+
export const validateItemPresentInNavigationMenu = (navigationMenuItem) =>
127+
cy.get(`a:contains("${navigationMenuItem}")`).should('be.visible');
114128

115129
export const addTagButtonsAreDisabled = () =>
116130
cy.get(addTagButtons).should('have.class', 'opacity-50');

test/e2e/cypress/pageObject/login_po.js

+5-7
Original file line numberDiff line numberDiff line change
@@ -61,19 +61,17 @@ export const clickSubmitLoginButton = () => {
6161
return cy.get(submitLoginButton).click();
6262
};
6363

64-
export const typeLoginTotpCode = (totpSecret) => {
65-
basePage.typeTotpCode(totpSecret, totpCodeInput);
66-
};
64+
export const typeLoginTotpCode = (totpSecret) =>
65+
basePage.typeNextGeneratedTotpCode(totpSecret, totpCodeInput, true);
6766

68-
export const typeAlreadyUsedTotpCode = (totpSecret) => {
69-
return typeLoginTotpCode(totpSecret);
67+
export const typeAlreadyUsedTotpCode = (code) => {
68+
cy.get(totpCodeInput).clear().type(code);
7069
};
7170

7271
export const typeInvalidLoginTotpCode = () => {
7372
return cy.get(totpCodeInput).clear().type('invalid');
7473
};
7574

7675
export const waitForNewTotpCodeAndTypeIt = (totpSecret) => {
77-
// eslint-disable-next-line cypress/no-unnecessary-waiting
78-
return cy.wait(30000).then(() => typeLoginTotpCode(totpSecret));
76+
typeLoginTotpCode(totpSecret);
7977
};

test/e2e/cypress/pageObject/users_po.js

+19-6
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ const newTotpCodeIssuedMessage = 'div:contains("Your new TOTP secret is:")';
3535
const totpSecret = `${newTotpCodeIssuedMessage} + div[class*="bold"]`;
3636
const newTotpCodeInputField = 'input[placeholder="TOTP code"]';
3737
const verifyTotpButton = 'button:contains("Verify")';
38-
const confirmDisableTotpButton = 'button:contains("Disable")';
38+
const confirmDisableTotpButton =
39+
'div[id*="headlessui-dialog-panel"] button:contains("Disable")';
3940
const editUserTotpDropdown = 'button.totp-selection-dropdown';
4041
const enableUserTotpOption = `${editUserTotpDropdown} + div div:contains("Enabled")`;
4142

@@ -153,14 +154,17 @@ export const selectFromTotpDropdown = (choice) => {
153154
return basePage.selectFromDropdown(editUserTotpDropdown, choice);
154155
};
155156

156-
const getTotpSecret = () => {
157+
export const getTotpSecret = () => {
157158
return cy.get(totpSecret).then((element) => element.text());
158159
};
159160

160-
export const typeUserTotpCode = () => {
161-
return getTotpSecret().then((totpSecret) =>
162-
basePage.typeTotpCode(totpSecret, newTotpCodeInputField)
163-
);
161+
export const typeUserTotpCode = (totpSecret) =>
162+
basePage.typeNextGeneratedTotpCode(totpSecret, newTotpCodeInputField);
163+
164+
export const getSecretAndTypeTotpCode = () => {
165+
getTotpSecret().then((totpSecret) => {
166+
typeUserTotpCode(totpSecret);
167+
});
164168
};
165169

166170
export const typeInvalidUserTotpCode = () => {
@@ -359,3 +363,12 @@ export const enableTotpOptionIsDisabled = () => {
359363
.invoke('attr', 'aria-disabled')
360364
.should('eq', 'true');
361365
};
366+
367+
export const newIssuedTotpSecretIsDifferent = (originalTotpSecret) => {
368+
getTotpSecret().then((newTotpSecret) => {
369+
expect(
370+
newTotpSecret === originalTotpSecret,
371+
'New issued TOTP secret is different'
372+
).to.be.false;
373+
});
374+
};

0 commit comments

Comments
 (0)