Merge pull request #16621 from mozilla/fxa-9178

fix(functional-tests): signInTotp tests in stage and prod
This commit is contained in:
Katrina Anderson 2024-03-25 18:35:18 -04:00 коммит произвёл GitHub
Родитель 9487944698 46b357ac89
Коммит 08b405470c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
14 изменённых файлов: 247 добавлений и 112 удалений

Просмотреть файл

@ -23,6 +23,7 @@ import { LegalPage } from './legal';
import { CookiesDisabledPage } from './cookiesDisabled';
import { PostVerifyPage } from './postVerify';
import { ResetPasswordReactPage } from './resetPasswordReact';
import { SigninReactPage } from './signinReact';
import { SignupReactPage } from './signupReact';
import { ConfigPage } from './config';
import { PrivacyPage } from './privacy';
@ -53,6 +54,7 @@ export function create(page: Page, target: BaseTarget) {
legal: new LegalPage(page, target),
cookiesDisabled: new CookiesDisabledPage(page, target),
postVerify: new PostVerifyPage(page, target),
signinReact: new SigninReactPage(page, target),
signupReact: new SignupReactPage(page, target),
configPage: new ConfigPage(page, target),
privacy: new PrivacyPage(page, target),

Просмотреть файл

@ -67,6 +67,18 @@ export class SettingsPage extends SettingsLayout {
return this.lazyRow('data-collection', DataCollectionRow);
}
get settingsHeading() {
return this.page.getByRole('heading', { name: 'Settings' });
}
get twoStepAuthenticationStatus() {
return this.page.getByTestId('two-step-unit-row-header-value');
}
get addTwoStepAuthenticationButton() {
return this.page.getByTestId('two-step-unit-row-route');
}
clickDeleteAccount() {
return Promise.all([
this.page.locator('[data-testid=settings-delete-account]').click(),

Просмотреть файл

@ -6,6 +6,10 @@ export abstract class SettingsLayout extends BaseLayout {
return this.page.locator('[data-testid="drop-down-bento-menu"]');
}
get alertBar() {
return this.page.getByTestId('alert-bar-content');
}
get avatarDropDownMenu() {
return this.page.getByTestId('drop-down-avatar-menu');
}
@ -26,11 +30,6 @@ export abstract class SettingsLayout extends BaseLayout {
return super.goto('load', query);
}
async alertBarText() {
const alert = this.page.locator('[data-testid=alert-bar-content]');
return alert.textContent();
}
async waitForAlertBar() {
return this.page.waitForSelector('[data-testid=alert-bar-content]');
}

Просмотреть файл

@ -1,9 +1,14 @@
import jsQR from 'jsqr';
import UPNG from 'upng-js';
import { expect } from '../../lib/fixtures/standard';
import { SettingsLayout } from './layout';
import { getCode } from 'fxa-settings/src/lib/totp';
import { DataTrioComponent } from './components/dataTrio';
import { Credentials } from '../../lib/targets';
export type TotpCredentials = {
secret: string;
recoveryCodes: string[];
};
export class TotpPage extends SettingsLayout {
readonly path = 'settings/two_step_authentication';
@ -12,6 +17,54 @@ export class TotpPage extends SettingsLayout {
return new DataTrioComponent(this.page);
}
get twoStepAuthenticationHeading() {
return this.page.getByRole('heading', { name: 'Two-step authentication' });
}
get step1Heading() {
return this.page.getByRole('heading', { name: 'Step 1 of 3' });
}
get step1CantScanCodeLink() {
return this.page.getByTestId('cant-scan-code');
}
get step1ManualCode() {
return this.page.getByTestId('manual-code');
}
get step1AuthenticationCodeTextbox() {
return this.page.getByTestId('totp-input-label');
}
get step1SubmitButton() {
return this.page.getByTestId('submit-totp');
}
get step2Heading() {
return this.page.getByRole('heading', { name: 'Step 2 of 3' });
}
get step2recoveryCodes() {
return this.page.getByTestId('datablock');
}
get step2ContinueButton() {
return this.page.getByTestId('ack-recovery-code');
}
get step3Heading() {
return this.page.getByRole('heading', { name: 'Step 3 of 3' });
}
get step3FinishButton() {
return this.page.getByTestId('submit-recovery-code');
}
get step3RecoveryCodeTextbox() {
return this.page.getByTestId('recovery-code-input-field');
}
async useQRCode() {
const qr = await this.page.waitForSelector('[data-testid="2fa-qr-code"]');
const png = await qr.screenshot();
@ -39,66 +92,39 @@ export class TotpPage extends SettingsLayout {
return await getCode(secret);
}
async useManualCode() {
await this.page.click('[data-testid=cant-scan-code]');
const secret = (
await this.page.innerText('[data-testid=manual-code]')
).replace(/\s/g, '');
async useManualCode(): Promise<string> {
await this.step1CantScanCodeLink.click();
const secret = (await this.step1ManualCode.innerText())?.replace(/\s/g, '');
const code = await getCode(secret);
await this.page.fill('input[type=text]', code);
await this.step1AuthenticationCodeTextbox.fill(code);
return secret;
}
submit() {
return this.page.click('button[type=submit]');
}
clickClose() {
return Promise.all([
this.page.locator('[data-testid=close-button]').click(),
this.page.waitForEvent('framenavigated'),
]);
}
async getRecoveryCodes(): Promise<string[]> {
await this.page.waitForSelector('[data-testid=datablock]');
return this.page.$$eval('[data-testid=datablock] span', (elements) =>
elements.map((el) => (el as HTMLElement).innerText)
);
const codesRaw = await this.step2recoveryCodes.textContent();
return codesRaw ? codesRaw.trim().split(/\s+/) : [];
}
setRecoveryCode(code: string) {
return this.page.fill('[data-testid=recovery-code-input-field]', code);
}
async fillTwoStepAuthenticationForm(
method: 'qr' | 'manual' = 'manual'
): Promise<TotpCredentials> {
await expect(this.twoStepAuthenticationHeading).toBeVisible();
await expect(this.step1Heading).toBeVisible();
async enable(credentials: Credentials, method: 'qr' | 'manual' = 'manual') {
await this.waitForStep(1, 3);
const secret =
method === 'qr' ? await this.useQRCode() : await this.useManualCode();
await this.submit();
await this.waitForStep(2, 3);
await this.step1SubmitButton.click();
await expect(this.step2Heading).toBeVisible();
const recoveryCodes = await this.getRecoveryCodes();
await this.submit();
await this.waitForStep(3, 3);
await this.setRecoveryCode(recoveryCodes[0]);
await this.submit();
await this.waitForEnabled();
if (credentials) {
credentials.secret = secret === null ? undefined : secret;
}
return {
secret,
recoveryCodes,
};
}
await this.step2ContinueButton.click();
async waitForStep(step: number, of: number) {
await this.page.getByText(`STEP ${step} of ${of}`).waitFor();
}
await expect(this.step3Heading).toBeVisible();
async waitForEnabled() {
await this.page
.getByTestId(`two-step-disable-button-unit-row-modal`)
.waitFor();
await this.step3RecoveryCodeTextbox.fill(recoveryCodes[0]);
await this.step3FinishButton.click();
return { secret, recoveryCodes };
}
}

Просмотреть файл

@ -0,0 +1,67 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { expect } from '@playwright/test';
import { BaseLayout } from './layout';
import { getReactFeatureFlagUrl } from '../lib/react-flag';
export class SigninReactPage extends BaseLayout {
readonly path = 'signin';
get authenticationFormHeading() {
return this.page.getByRole('heading', {
name: /^Enter (?:authentication|security) code/,
});
}
get authenticationCodeTextbox() {
return this.page
.getByRole('textbox', { name: 'code' })
.or(this.page.getByPlaceholder('Enter 6-digit code'));
}
get authenticationCodeTextboxTooltip() {
return this.page.getByText('Invalid two-step authentication code', {
exact: true,
});
}
get confirmButton() {
return this.page.getByRole('button', { name: 'Confirm' });
}
get passwordFormHeading() {
return this.page.getByRole('heading', { name: /^Enter your password/ });
}
get passwordTextbox() {
return this.page.getByRole('textbox', { name: 'password' });
}
get signInButton() {
return this.page.getByRole('button', { name: 'Sign in' });
}
goto(route = '/', params = new URLSearchParams()) {
params.set('forceExperiment', 'generalizedReactApp');
params.set('forceExperimentGroup', 'react');
return this.page.goto(
getReactFeatureFlagUrl(this.target, route, params.toString())
);
}
async fillOutAuthenticationForm(code: string): Promise<void> {
await expect(this.authenticationFormHeading).toBeVisible();
await this.authenticationCodeTextbox.fill(code);
await this.confirmButton.click();
}
async fillOutPasswordForm(password: string): Promise<void> {
await expect(this.passwordFormHeading).toBeVisible();
await this.passwordTextbox.fill(password);
await this.signInButton.click();
}
}

Просмотреть файл

@ -65,22 +65,14 @@ export class SignupReactPage extends BaseLayout {
}
async fillOutEmailForm(email: string) {
await expect(
this.emailFormHeading,
'The Email form heading is missing or not as expected. ' +
'Are you filling out the right form?'
).toBeVisible();
await expect(this.emailFormHeading).toBeVisible();
await this.emailTextbox.fill(email);
await this.submitButton.click();
}
async fillOutSignupForm(password: string, age: string) {
await expect(
this.signupFormHeading,
'The Signup form heading is missing or not as expected. ' +
'Are you filling out the right form?'
).toBeVisible();
await expect(this.signupFormHeading).toBeVisible();
await this.passwordTextbox.fill(password);
await this.verifyPasswordTextbox.fill(password);
@ -95,11 +87,7 @@ export class SignupReactPage extends BaseLayout {
EmailHeader.shortCode
);
await expect(
this.codeFormHeading,
'The Confirmation Code form heading is missing or not as expected. ' +
'Are you filling out the right form?'
).toBeVisible();
await expect(this.codeFormHeading).toBeVisible();
await this.codeTextbox.fill(code);
await this.confirmButton.click();

Просмотреть файл

@ -16,7 +16,8 @@ test.describe('severity-1 #smoke', () => {
}) => {
await settings.goto();
await settings.totp.clickAdd();
await totp.enable(credentials);
const { secret } = await totp.fillTwoStepAuthenticationForm();
credentials.secret = secret;
await settings.signOut();
await relier.goto();
@ -33,7 +34,8 @@ test.describe('severity-1 #smoke', () => {
}) => {
await settings.goto();
await settings.totp.clickAdd();
await totp.enable(credentials);
const { secret } = await totp.fillTwoStepAuthenticationForm();
credentials.secret = secret;
await settings.totp.clickDisable();
await settings.clickModalConfirm();

Просмотреть файл

@ -189,7 +189,8 @@ test.describe('severity-1 #smoke', () => {
async function addTotpFlow({ credentials, pages: { totp, settings } }) {
await settings.goto();
await settings.totp.clickAdd();
await totp.enable(credentials);
const { secret } = await totp.fillTwoStepAuthenticationForm();
credentials.secret = secret;
}
/**

Просмотреть файл

@ -6,47 +6,76 @@ import { test, expect } from '../../lib/fixtures/standard';
test.describe('severity-1 #smoke', () => {
test.describe('two step auth', () => {
test.beforeEach(async () => {
test.beforeEach(async ({}) => {
test.slow();
});
// https://testrail.stage.mozaws.net/index.php?/cases/view/1293446
// https://testrail.stage.mozaws.net/index.php?/cases/view/1293452
// FXA-9178
// eslint-disable-next-line playwright/no-skipped-test
test.skip('add and remove totp', async ({
test('add totp', async ({
credentials,
target,
pages: { settings, totp, page, signupReact },
pages: { settings, totp, page, signinReact, signupReact },
}) => {
await settings.goto();
let status = await settings.totp.statusText();
expect(status).toEqual('Not Set');
await settings.totp.clickAdd();
const { secret } = await totp.enable(credentials);
await settings.waitForAlertBar();
status = await settings.totp.statusText();
expect(status).toEqual('Enabled');
await expect(settings.settingsHeading).toBeVisible();
await expect(settings.twoStepAuthenticationStatus).toHaveText('Not Set');
await settings.addTwoStepAuthenticationButton.click();
const { secret } = await totp.fillTwoStepAuthenticationForm();
credentials.secret = secret;
await expect(settings.settingsHeading).toBeVisible();
await expect(settings.alertBar).toHaveText(
'Two-step authentication enabled'
);
await expect(settings.twoStepAuthenticationStatus).toHaveText('Enabled');
await settings.signOut();
await page.goto(
`${target.contentServerUrl}/?showReactApp=true&forceExperiment=generalizedReactApp&forceExperimentGroup=react`
);
await signupReact.fillOutEmailForm(credentials.email);
await signinReact.fillOutPasswordForm(credentials.password);
const code = await totp.getNextCode(credentials.secret);
await signinReact.fillOutAuthenticationForm(code);
await signupReact.fillOutEmailFirst(credentials.email);
await page.fill('[name="password"]', credentials.password);
await page.click('[type="submit"]');
await expect(page).toHaveURL(/settings/);
await expect(settings.settingsHeading).toBeVisible();
await expect(settings.twoStepAuthenticationStatus).toHaveText('Enabled');
});
test('error message when totp code is invalid', async ({
credentials,
target,
pages: { settings, totp, page, signinReact, signupReact },
}) => {
await settings.goto();
await expect(settings.settingsHeading).toBeVisible();
await expect(settings.twoStepAuthenticationStatus).toHaveText('Not Set');
await settings.addTwoStepAuthenticationButton.click();
const { secret } = await totp.fillTwoStepAuthenticationForm();
credentials.secret = secret;
await expect(settings.settingsHeading).toBeVisible();
await expect(settings.alertBar).toHaveText(
'Two-step authentication enabled'
);
await expect(settings.twoStepAuthenticationStatus).toHaveText('Enabled');
await settings.signOut();
await page.goto(
`${target.contentServerUrl}/?showReactApp=true&forceExperiment=generalizedReactApp&forceExperimentGroup=react`
);
await signupReact.fillOutEmailForm(credentials.email);
await signinReact.fillOutPasswordForm(credentials.password);
await page.waitForURL(/signin_totp_code/);
await page.waitForSelector('#root');
await page.fill('[name="code"]', '111111');
await page.click('[type="submit"]');
page.getByText('Invalid two-step authentication code');
await signinReact.fillOutAuthenticationForm('111111');
const code = await totp.getNextCode(secret);
await page.fill('[name="code"]', code);
await page.click('[type="submit"]');
await page.waitForURL(/settings/);
await expect(signinReact.authenticationCodeTextboxTooltip).toHaveText(
'Invalid two-step authentication code'
);
});
});
});

Просмотреть файл

@ -145,7 +145,7 @@ test.describe('severity-1 #smoke', () => {
await signupReact.fillOutCodeForm(newEmail);
}
expect(await settings.alertBarText()).toContain(
await expect(settings.alertBar).toHaveText(
'Account confirmed successfully'
);
const primaryEmail = await settings.primaryEmail.statusText();

Просмотреть файл

@ -19,7 +19,7 @@ test.describe('severity-1 #smoke', () => {
await settings.closeAlertBar();
await settings.secondaryEmail.clickDelete();
await settings.waitForAlertBar();
expect(await settings.alertBarText()).toContain('successfully deleted');
await expect(settings.alertBar).toHaveText(/successfully deleted/);
await settings.secondaryEmail.clickAdd();
await secondaryEmail.setEmail(newEmail);
await secondaryEmail.submit();
@ -28,7 +28,7 @@ test.describe('severity-1 #smoke', () => {
expect(await settings.secondaryEmail.statusText()).toContain('UNCONFIRMED');
await settings.secondaryEmail.clickDelete();
await settings.waitForAlertBar();
expect(await settings.alertBarText()).toContain('successfully deleted');
await expect(settings.alertBar).toHaveText(/successfully deleted/);
});
// https://testrail.stage.mozaws.net/index.php?/cases/view/1293504

Просмотреть файл

@ -22,7 +22,8 @@ test.describe('severity-1 #smoke', () => {
let status = await settings.totp.statusText();
expect(status).toEqual('Not Set');
await settings.totp.clickAdd();
await totp.enable(credentials);
const { secret } = await totp.fillTwoStepAuthenticationForm();
credentials.secret = secret;
await settings.waitForAlertBar();
status = await settings.totp.statusText();
expect(status).toEqual('Enabled');
@ -49,7 +50,8 @@ test.describe('severity-1 #smoke', () => {
let status = await settings.totp.statusText();
expect(status).toEqual('Not Set');
await settings.totp.clickAdd();
await totp.enable(credentials, 'qr');
const { secret } = await totp.fillTwoStepAuthenticationForm('qr');
credentials.secret = secret;
await settings.waitForAlertBar();
status = await settings.totp.statusText();
expect(status).toEqual('Enabled');
@ -62,7 +64,8 @@ test.describe('severity-1 #smoke', () => {
}) => {
await settings.goto();
await settings.totp.clickAdd();
await totp.enable(credentials);
const { secret } = await totp.fillTwoStepAuthenticationForm();
credentials.secret = secret;
await settings.signOut();
await login.login(credentials.email, credentials.password);
await login.setTotp(credentials.secret);
@ -78,7 +81,9 @@ test.describe('severity-1 #smoke', () => {
}) => {
await settings.goto();
await settings.totp.clickAdd();
const { recoveryCodes } = await totp.enable(credentials);
const { secret, recoveryCodes } =
await totp.fillTwoStepAuthenticationForm();
credentials.secret = secret;
await settings.totp.clickChange();
await settings.clickModalConfirm();
const newCodes = await totp.getRecoveryCodes();
@ -86,8 +91,8 @@ test.describe('severity-1 #smoke', () => {
expect(newCodes).not.toContain(code);
}
await settings.clickRecoveryCodeAck();
await totp.setRecoveryCode(newCodes[0]);
await totp.submit();
await totp.step3RecoveryCodeTextbox.fill(newCodes[0]);
await totp.step3FinishButton.click();
await settings.waitForAlertBar();
await settings.signOut();
await login.login(credentials.email, credentials.password);
@ -113,7 +118,9 @@ test.describe('severity-1 #smoke', () => {
}, { project }) => {
await settings.goto();
await settings.totp.clickAdd();
const { recoveryCodes } = await totp.enable(credentials);
const { secret, recoveryCodes } =
await totp.fillTwoStepAuthenticationForm();
credentials.secret = secret;
await settings.signOut();
for (let i = 0; i < recoveryCodes.length - 3; i++) {
await login.login(
@ -148,10 +155,11 @@ test.describe('severity-1 #smoke', () => {
}) => {
await settings.goto();
await settings.totp.clickAdd();
const { secret } = await totp.enable(credentials);
const { secret } = await totp.fillTwoStepAuthenticationForm();
credentials.secret = secret;
await settings.signOut();
await login.login(credentials.email, credentials.password);
await login.setTotp(secret);
await login.setTotp(credentials.secret);
await settings.clickDeleteAccount();
await deleteAccount.checkAllBoxes();
await deleteAccount.clickContinue();

Просмотреть файл

@ -110,7 +110,7 @@ test.describe('severity-2 #smoke', () => {
// Toggle switch, verify the alert bar appears and its status
await settings.dataCollection.toggleShareData('off');
expect(await settings.alertBarText()).toContain('Opt out successful.');
await expect(settings.alertBar).toHaveText(/Opt out successful/);
expect(await settings.dataCollection.getToggleStatus()).toBe('false');
// Subscribe to plan and verify URL does not include flow parameter

Просмотреть файл

@ -156,7 +156,8 @@ test.describe('severity-2 #smoke', () => {
await settings.goto();
await settings.totp.clickAdd();
await totp.enable(credentials);
const { secret } = await totp.fillTwoStepAuthenticationForm();
credentials.secret = secret;
await settings.signOut();
// Sync sign in