test(force auth): add functional tests for Sync V3 force auth

Because:
 - we want convert the desktop Sync V3 force auth tests from the Intern
   to Playwright

This commit:
 - add Playwright tests for desktop Sync V3 force auth flows
This commit is contained in:
Barry Chen 2022-10-27 10:48:21 -07:00
Родитель 6941fd4d64
Коммит bed52151ce
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 228DB2785954A0D0
12 изменённых файлов: 381 добавлений и 13 удалений

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

@ -43,7 +43,7 @@ test('mocha tests', async ({ target, page }, info) => {
We have a standard [fixture](https://playwright.dev/docs/test-fixtures) for the most common kind of tests.
It's job is to:
Its job is to:
- Connect to the target environment
- Create and verify an account for each test

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

@ -0,0 +1,28 @@
/* 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/. */
export default {
android_chrome:
'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19',
android_firefox:
'Mozilla/5.0 (Android 4.4; Mobile; rv:43.0) Gecko/41.0 Firefox/43.0',
desktop_chrome:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.59 Safari/537.36',
desktop_firefox_58:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:58.0) Gecko/20100101 Firefox/58.0',
desktop_firefox_71:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:71.0) Gecko/20100101 Firefox/71.0',
ios_firefox:
'Mozilla/5.0 (iPhone; CPU iPhone OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) FxiOS/1.0 Mobile/12F69 Safari/600.1.4',
ios_firefox_11_0:
'Mozilla/5.0 (iPhone; CPU iPhone OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) FxiOS/11.0 Mobile/12F69 Safari/600.1.4',
ios_firefox_6_1:
'Mozilla/5.0 (iPhone; CPU iPhone OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) FxiOS/6.1 Mobile/12F69 Safari/600.1.4',
ios_firefox_9:
'Mozilla/5.0 (iPhone; CPU iPhone OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) FxiOS/9.0 Mobile/12F69 Safari/600.1.4',
ios_firefox_10:
'Mozilla/5.0 (iPhone; CPU iPhone OS 8_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) FxiOS/10.0 Mobile/12F69 Safari/600.1.4', // eslint-disable-line
ios_safari:
'Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3',
};

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

@ -1,3 +1,7 @@
/* 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 { BaseLayout } from './layout';
export class ConnectAnotherDevicePage extends BaseLayout {
@ -6,6 +10,7 @@ export class ConnectAnotherDevicePage extends BaseLayout {
readonly selectors = {
CONNECT_ANOTHER_DEVICE_HEADER: '#fxa-connect-another-device-header',
CONNECT_ANOTHER_DEVICE_SIGNIN_BUTTON: 'form div a',
FXA_CONNECTED_HEADER: '#fxa-connected-heading',
TEXT_INSTALL_FX_DESKTOP: '#install-mobile-firefox-desktop',
SUCCESS: '.success',
};
@ -14,6 +19,10 @@ export class ConnectAnotherDevicePage extends BaseLayout {
return this.page.locator(this.selectors.CONNECT_ANOTHER_DEVICE_HEADER);
}
get fxaConnected() {
return this.page.locator(this.selectors.FXA_CONNECTED_HEADER);
}
get signInButton() {
return this.page.locator(
this.selectors.CONNECT_ANOTHER_DEVICE_SIGNIN_BUTTON

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

@ -0,0 +1,10 @@
/* 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 { ForceAuthPage } from '.';
export class FxDesktopV3ForceAuthPage extends ForceAuthPage {
context = 'fx_desktop_v3';
service = 'sync';
}

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

@ -0,0 +1,59 @@
/* 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 { BaseLayout } from '../layout';
import uaStrings from '../../lib/ua-strings';
import { Credentials } from '../../lib/targets/base';
export abstract class ForceAuthPage extends BaseLayout {
readonly path = 'force_auth';
context;
service;
async openWithReplacementParams(
credentials: Credentials,
replacementParams: { [keys: string]: string | undefined }
) {
let params = this.buildQueryParams(credentials);
params = { ...params, ...replacementParams };
Object.keys(params).forEach((k) => {
if (params[k] === undefined) {
delete params[k];
}
});
await this.openWithParams(params);
}
async open(credentials: Credentials) {
await this.openWithParams(this.buildQueryParams(credentials));
}
private async openWithParams(
params: Partial<ReturnType<ForceAuthPage['buildQueryParams']>>
) {
await this.openWithQueryParams(params);
await this.listenToWebChannelMessages();
}
private buildQueryParams(credentials: Credentials) {
return {
automatedBrowser: true,
context: this.context,
email: credentials.email,
forceUA: uaStrings['desktop_firefox_71'],
service: this.service,
uid: credentials.uid,
};
}
private openWithQueryParams(
queryParam: Partial<ReturnType<ForceAuthPage['buildQueryParams']>>
) {
const query = Object.keys(queryParam)
.map((k) => `${k}=${encodeURIComponent(queryParam[k])}`)
.join('&');
const url = `${this.url}?${query}`;
return this.page.goto(url, { waitUntil: 'domcontentloaded' });
}
}

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

@ -1,38 +1,40 @@
import { Page } from '@playwright/test';
import { AvatarPage } from './settings/avatar';
import { BaseTarget } from '../lib/targets/base';
import { ConnectAnotherDevicePage } from './connectAnotherDevice';
import { ChangePasswordPage } from './settings/changePassword';
import { DeleteAccountPage } from './settings/deleteAccount';
import { DisplayNamePage } from './settings/displayName';
import { FourOhFourPage } from './fourOhFour';
import { FxDesktopV3ForceAuthPage } from './forceAuth/fxDesktopV3';
import { LoginPage } from './login';
import { ConnectAnotherDevicePage } from './connectAnotherDevice';
import { RecoveryKeyPage } from './settings/recoveryKey';
import { RelierPage } from './relier';
import { SecondaryEmailPage } from './settings/secondaryEmail';
import { SettingsPage } from './settings';
import { SignInPage } from './signin';
import { SigninTokenCodePage } from './signinTokenCode';
import { SubscribePage } from './products';
import { TotpPage } from './settings/totp';
import { AvatarPage } from './settings/avatar';
import { SigninTokenCodePage } from './signinTokenCode';
import { FourOhFourPage } from './fourOhFour';
import { SignInPage } from './signin';
export function create(page: Page, target: BaseTarget) {
return {
page,
avatar: new AvatarPage(page, target),
changePassword: new ChangePasswordPage(page, target),
connectAnotherDevice: new ConnectAnotherDevicePage(page, target),
deleteAccount: new DeleteAccountPage(page, target),
displayName: new DisplayNamePage(page, target),
fourOhFour: new FourOhFourPage(page, target),
fxDesktopV3ForceAuth: new FxDesktopV3ForceAuthPage(page, target),
login: new LoginPage(page, target),
page,
recoveryKey: new RecoveryKeyPage(page, target),
relier: new RelierPage(page, target),
secondaryEmail: new SecondaryEmailPage(page, target),
settings: new SettingsPage(page, target),
signIn: new SignInPage(page, target),
signinTokenCode: new SigninTokenCodePage(page, target),
subscribe: new SubscribePage(page, target),
recoveryKey: new RecoveryKeyPage(page, target),
relier: new RelierPage(page, target),
totp: new TotpPage(page, target),
};
}

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

@ -21,4 +21,80 @@ export abstract class BaseLayout {
screenshot() {
return this.page.screenshot({ fullPage: true });
}
async checkWebChannelMessage(command) {
await this.page.evaluate(async (command) => {
const noNotificationError = new Error(
`NoSuchBrowserNotification - ${command}`
);
await new Promise((resolve, reject) => {
const timeoutHandle = setTimeout(
() => reject(noNotificationError),
2000
);
function findMessage() {
const messages = JSON.parse(
sessionStorage.getItem('webChannelEvents') || '[]'
);
const m = messages.find((x) => x.command === command);
if (m) {
clearTimeout(timeoutHandle);
resolve(m);
} else {
setTimeout(findMessage, 50);
}
}
findMessage();
});
}, command);
}
async noSuchWebChannelMessage(command) {
await this.page.evaluate(async (command) => {
const unexpectedNotificationError = new Error(
`UnepxectedBrowserNotification - ${command}`
);
await new Promise((resolve, reject) => {
const timeoutHandle = setTimeout(resolve, 1000);
function findMessage() {
const messages = JSON.parse(
sessionStorage.getItem('webChannelEvents') || '[]'
);
const m = messages.find((x) => x.command === command);
if (m) {
clearTimeout(timeoutHandle);
reject(unexpectedNotificationError);
} else {
setTimeout(findMessage, 50);
}
}
findMessage();
});
}, command);
}
async listenToWebChannelMessages() {
await this.page.evaluate(() => {
function listener(msg) {
const detail = JSON.parse(msg.detail);
const events = JSON.parse(
sessionStorage.getItem('webChannelEvents') || '[]'
);
events.push({
command: detail.message.command,
detail: detail.message.data,
});
sessionStorage.setItem('webChannelEvents', JSON.stringify(events));
}
addEventListener('WebChannelMessageToChrome', listener);
});
}
}

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

@ -108,9 +108,16 @@ export class LoginPage extends BaseLayout {
}
}
async fillOutFirstSignUp(email: string, password: string, verify = true) {
await this.setEmail(email);
await this.submit();
async fillOutFirstSignUp(
email: string,
password: string,
verify = true,
enterEmail = true
) {
if (enterEmail) {
await this.setEmail(email);
await this.submit();
}
await this.page.fill(selectors.PASSWORD, password);
await this.page.fill(selectors.VPASSWORD, password);
await this.page.fill(selectors.AGE, '24');
@ -168,6 +175,10 @@ export class LoginPage extends BaseLayout {
return error.textContent();
}
async getUseDifferentAccountLink() {
return this.page.locator(selectors.LINK_USE_DIFFERENT);
}
async useDifferentAccountLink() {
return this.page.click(selectors.LINK_USE_DIFFERENT);
}
@ -281,6 +292,10 @@ export class LoginPage extends BaseLayout {
return this.page.innerText(selectors.EMAIL_PREFILLED);
}
async getEmailInputElement() {
return this.page.locator(selectors.EMAIL);
}
async getEmailInput() {
return this.page.inputValue(selectors.EMAIL);
}

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

@ -1,3 +1,7 @@
/* 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 { BaseLayout } from './layout';
export class SigninTokenCodePage extends BaseLayout {

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

@ -9,6 +9,7 @@ const CI = !!process.env.CI;
// The DEBUG env is used to debug without the playwright inspector, like in vscode
// see .vscode/launch.json
const DEBUG = !!process.env.DEBUG;
const SLOWMO = parseInt(process.env.PLAYWRIGHT_SLOWMO || '0');
const config: PlaywrightTestConfig<TestOptions, WorkerOptions> = {
outputDir: path.resolve(__dirname, '../../artifacts/functional'),
@ -31,6 +32,7 @@ const config: PlaywrightTestConfig<TestOptions, WorkerOptions> = {
args: DEBUG ? ['-start-debugger-server'] : undefined,
firefoxUserPrefs: getFirefoxUserPrefs(name, DEBUG),
headless: !DEBUG,
slowMo: SLOWMO,
},
trace: CI ? 'on-first-retry' : 'retain-on-failure',
},

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

@ -0,0 +1,163 @@
/* 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 { test, expect } from '../../lib/fixtures/standard';
const makeUid = () =>
[...Array(32)]
.map(() => Math.floor(Math.random() * 16).toString(16))
.join('');
test.describe('Desktop Sync V3 force auth', () => {
test('sync v3 with a registered email, no uid', async ({
credentials,
pages: {
fxDesktopV3ForceAuth,
login,
signinTokenCode,
connectAnotherDevice,
},
}) => {
await fxDesktopV3ForceAuth.openWithReplacementParams(credentials, {
uid: undefined,
});
await login.setPassword(credentials.password);
await login.submit();
expect(await signinTokenCode.tokenCodeHeader.isVisible()).toBeTruthy();
await fxDesktopV3ForceAuth.checkWebChannelMessage(
'fxaccounts:can_link_account'
);
await login.fillOutSignInCode(credentials.email);
expect(await connectAnotherDevice.fxaConnected.isVisible()).toBeTruthy();
await fxDesktopV3ForceAuth.checkWebChannelMessage('fxaccounts:login');
});
test('sync v3 with a registered email, registered uid', async ({
credentials,
pages: {
fxDesktopV3ForceAuth,
login,
signinTokenCode,
connectAnotherDevice,
},
}) => {
await fxDesktopV3ForceAuth.open(credentials);
await login.setPassword(credentials.password);
await login.submit();
expect(await signinTokenCode.tokenCodeHeader.isVisible()).toBeTruthy();
await fxDesktopV3ForceAuth.checkWebChannelMessage(
'fxaccounts:can_link_account'
);
await login.fillOutSignInCode(credentials.email);
expect(await connectAnotherDevice.fxaConnected.isVisible()).toBeTruthy();
await fxDesktopV3ForceAuth.checkWebChannelMessage('fxaccounts:login');
});
test('sync v3 with a registered email, unregistered uid', async ({
credentials,
pages: {
fxDesktopV3ForceAuth,
login,
signinTokenCode,
connectAnotherDevice,
},
}) => {
const uid = makeUid();
await fxDesktopV3ForceAuth.openWithReplacementParams(credentials, { uid });
await fxDesktopV3ForceAuth.noSuchWebChannelMessage('fxaccounts:logout');
await login.setPassword(credentials.password);
await login.submit();
expect(await signinTokenCode.tokenCodeHeader.isVisible()).toBeTruthy();
await fxDesktopV3ForceAuth.checkWebChannelMessage(
'fxaccounts:can_link_account'
);
await login.fillOutSignInCode(credentials.email);
expect(await connectAnotherDevice.fxaConnected.isVisible()).toBeTruthy();
await fxDesktopV3ForceAuth.checkWebChannelMessage('fxaccounts:login');
});
test('sync v3 with an unregistered email, no uid', async ({
credentials,
pages: { fxDesktopV3ForceAuth, login },
}) => {
const email = `sync${Math.random()}@restmail.net`;
await fxDesktopV3ForceAuth.openWithReplacementParams(credentials, {
email,
uid: undefined,
});
const error = await login.signInError();
expect(error).toContain('Recreate');
const emailInputValue = await login.getEmailInput();
expect(emailInputValue).toBe(email);
const emailInput = await login.getEmailInputElement();
expect(emailInput.isDisabled());
await expect(
await (await login.getUseDifferentAccountLink()).count()
).toEqual(0);
await login.fillOutFirstSignUp(email, credentials.password, true, false);
await fxDesktopV3ForceAuth.checkWebChannelMessage(
'fxaccounts:can_link_account'
);
await fxDesktopV3ForceAuth.checkWebChannelMessage('fxaccounts:login');
});
test('sync v3 with an unregistered email, registered uid', async ({
credentials,
pages: { fxDesktopV3ForceAuth, login },
}) => {
const email = `sync${Math.random()}@restmail.net`;
await fxDesktopV3ForceAuth.openWithReplacementParams(credentials, {
email,
});
const error = await login.signInError();
expect(error).toContain('Recreate');
const emailInputValue = await login.getEmailInput();
expect(emailInputValue).toBe(email);
const emailInput = await login.getEmailInputElement();
expect(emailInput.isDisabled());
await expect(
await (await login.getUseDifferentAccountLink()).count()
).toEqual(0);
});
test('sync v3 with an unregistered email, unregistered uid', async ({
credentials,
pages: { fxDesktopV3ForceAuth, login },
}) => {
const email = `sync${Math.random()}@restmail.net`;
const uid = makeUid();
await fxDesktopV3ForceAuth.openWithReplacementParams(credentials, {
email,
uid,
});
const error = await login.signInError();
expect(error).toContain('Recreate');
const emailInputValue = await login.getEmailInput();
expect(emailInputValue).toBe(email);
const emailInput = await login.getEmailInputElement();
expect(emailInput.isDisabled());
await expect(
await (await login.getUseDifferentAccountLink()).count()
).toEqual(0);
});
test('blocked with an registered email, unregistered uid', async ({
credentials,
pages: { fxDesktopV3ForceAuth, login, connectAnotherDevice },
}) => {
const uid = makeUid();
await fxDesktopV3ForceAuth.openWithReplacementParams(credentials, {
uid,
});
await fxDesktopV3ForceAuth.noSuchWebChannelMessage('fxaccounts:logout');
await login.setPassword(credentials.password);
await login.submit();
await fxDesktopV3ForceAuth.checkWebChannelMessage(
'fxaccounts:can_link_account'
);
await login.unblock(credentials.email);
expect(await connectAnotherDevice.fxaConnected.isVisible()).toBeTruthy();
await fxDesktopV3ForceAuth.checkWebChannelMessage('fxaccounts:login');
});
});

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

@ -55,7 +55,6 @@ module.exports = testsSettings.concat([
'tests/functional/sync_v3_email_first.js',
'tests/functional/sync_v3_sign_in.js',
'tests/functional/sync_v3_sign_up.js',
'tests/functional/sync_v3_force_auth.js',
'tests/functional/sync_v3_reset_password.js',
'tests/functional/sync_v3_settings.js',
'tests/functional/tos.js',
@ -67,6 +66,7 @@ module.exports = testsSettings.concat([
// See `/functional-test`
// 'tests/functional/oauth_handshake.js',
// 'tests/functional/oauth_force_auth.js',
// 'tests/functional/sync_v3_force_auth.js',
]);
// Mocha tests are only exposed during local dev, not on prod-like