diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2023-06-12 14:24:15 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2023-06-12 14:24:15 +0200 |
| commit | 601109ac7845bf603e4b2d17bb72f30401706b71 (patch) | |
| tree | 831b339f0e38614e129fcce89079bb55a4af44b0 | |
| parent | e41d6ff007f667c8efcdf885b71d323a93e16df5 (diff) | |
| parent | 1822c81e8f87ecce858603b78492f52df1eba450 (diff) | |
| download | mullvadvpn-601109ac7845bf603e4b2d17bb72f30401706b71.tar.xz mullvadvpn-601109ac7845bf603e4b2d17bb72f30401706b71.zip | |
Merge branch 'create-gui-tests-for-tunnel-state-des-61'
| -rw-r--r-- | gui/src/config.json | 6 | ||||
| -rw-r--r-- | gui/src/renderer/components/ConnectionPanel.tsx | 4 | ||||
| -rw-r--r-- | gui/test/e2e/installed/state-dependent/login.spec.ts | 4 | ||||
| -rw-r--r-- | gui/test/e2e/installed/state-dependent/tunnel-state.spec.ts | 177 | ||||
| -rw-r--r-- | gui/test/e2e/shared/tunnel-state.ts | 126 | ||||
| -rw-r--r-- | gui/test/e2e/utils.ts | 8 |
6 files changed, 266 insertions, 59 deletions
diff --git a/gui/src/config.json b/gui/src/config.json index 4bb1e7a6ce..822e00a12f 100644 --- a/gui/src/config.json +++ b/gui/src/config.json @@ -28,9 +28,9 @@ "blue60": "rgba(41, 77, 115, 0.6)", "blue80": "rgba(41, 77, 115, 0.8)", "red95": "rgba(227, 64, 57, 0.95)", - "red80": "rgba(227, 64, 57, 0.80)", - "red60": "rgba(227, 64, 57, 0.60)", - "red40": "rgba(227, 64, 57, 0.40)", + "red80": "rgba(227, 64, 57, 0.8)", + "red60": "rgba(227, 64, 57, 0.6)", + "red40": "rgba(227, 64, 57, 0.4)", "red45": "rgba(227, 64, 57, 0.45)", "green90": "rgba(68, 173, 77, 0.9)", "green40": "rgba(68, 173, 77, 0.4)" diff --git a/gui/src/renderer/components/ConnectionPanel.tsx b/gui/src/renderer/components/ConnectionPanel.tsx index 552a9e3187..a366a9058d 100644 --- a/gui/src/renderer/components/ConnectionPanel.tsx +++ b/gui/src/renderer/components/ConnectionPanel.tsx @@ -95,7 +95,7 @@ export default class ConnectionPanel extends React.Component<IProps> { {this.props.hostname && ( <Header> <ConnectionPanelDisclosure pointsUp={this.props.isOpen} onToggle={this.props.onToggle}> - <Marquee>{this.hostnameLine()}</Marquee> + <Marquee data-testid="hostname-line">{this.hostnameLine()}</Marquee> </ConnectionPanelDisclosure> </Header> )} @@ -104,7 +104,7 @@ export default class ConnectionPanel extends React.Component<IProps> { <React.Fragment> {this.props.inAddress && ( <Row> - <Text>{this.transportLine()}</Text> + <Text data-testid="tunnel-protocol">{this.transportLine()}</Text> </Row> )} diff --git a/gui/test/e2e/installed/state-dependent/login.spec.ts b/gui/test/e2e/installed/state-dependent/login.spec.ts index 9b1642dbd4..6701cfbd1b 100644 --- a/gui/test/e2e/installed/state-dependent/login.spec.ts +++ b/gui/test/e2e/installed/state-dependent/login.spec.ts @@ -87,10 +87,6 @@ test('App should log in', async () => { await expect(subtitle).toHaveText('Valid account number'); expect(await util.waitForNavigation()).toEqual(RoutePath.main); - - // Prevent the connect-button from being hovered, and therefore not have the correct color. - await page.hover('div'); - await assertDisconnected(page); }); diff --git a/gui/test/e2e/installed/state-dependent/tunnel-state.spec.ts b/gui/test/e2e/installed/state-dependent/tunnel-state.spec.ts new file mode 100644 index 0000000000..b2c196b8dd --- /dev/null +++ b/gui/test/e2e/installed/state-dependent/tunnel-state.spec.ts @@ -0,0 +1,177 @@ +import { exec as execAsync } from 'child_process'; +import { promisify } from 'util'; +import { expect, test } from '@playwright/test'; +import { Page } from 'playwright'; +import { + assertConnected, + assertConnectedPq, + assertConnecting, + assertConnectingPq, + assertDisconnected, + assertDisconnecting, + assertError, +} from '../../shared/tunnel-state'; + +import { startInstalledApp } from '../installed-utils'; +import { escapeRegExp } from '../../utils'; + +const exec = promisify(execAsync); + +// This test expects the daemon to be logged into an account that has time left and to be +// disconnected. Env parameters: +// HOSTNAME: hostname of the currently selected WireGuard relay +// IN_IP: In ip of the relay passed in `HOSTNAME` +// CONNECTION_CHECK_URL: Url to the connection check + +let page: Page; + +test.beforeAll(async () => { + ({ page } = await startInstalledApp()); +}); + +test.afterAll(async () => { + await page.close(); +}); + +test('App should show disconnected tunnel state', async () => { + await assertDisconnected(page); +}); + +test('App should connect', async () => { + await page.getByText('Secure my connection').click(); + + await assertConnecting(page); + await assertConnected(page); + + const relay = page.getByTestId('hostname-line'); + const inIp = page.locator(':text("In") + span'); + const outIp = page.locator(':text("Out") + div > span'); + + await expect(relay).toHaveText(process.env.HOSTNAME!); + await expect(inIp).not.toBeVisible(); + await relay.click(); + + await expect(inIp).toBeVisible(); + expect(await inIp.textContent()).toMatch(new RegExp(`^${process.env.IN_IP!}`)); + + await expect(outIp).toBeVisible(); + + const ipResponse = await fetch(`${process.env.CONNECTION_CHECK_URL!}/ip`); + const ip = await ipResponse.text(); + + expect(await outIp.textContent()).toBe(ip.trim()); +}); + +test('App should show correct WireGuard port', async () => { + const inData = page.locator(':text("In") + span'); + + await expect(inData).toContainText(new RegExp(':[0-9]+')); + + await exec('mullvad relay set tunnel wireguard --port=53'); + await assertConnected(page); + await expect(inData).toContainText(new RegExp(':53')); + + await exec('mullvad relay set tunnel wireguard --port=51820'); + await assertConnected(page); + await expect(inData).toContainText(new RegExp(':51820')); + + await exec('mullvad relay set tunnel wireguard --port=any'); +}); + +test('App should show correct WireGuard transport protocol', async () => { + const inData = page.locator(':text("In") + span'); + + await exec('mullvad obfuscation set mode udp2tcp'); + await expect(inData).toContainText(new RegExp('TCP')); + + await exec('mullvad obfuscation set mode off'); + await expect(inData).toContainText(new RegExp('UDP$')); +}); + +test('App should show correct tunnel protocol', async () => { + const tunnelProtocol = page.getByTestId('tunnel-protocol'); + await expect(tunnelProtocol).toHaveText('WireGuard'); + + await exec('mullvad relay set tunnel-protocol openvpn'); + await exec('mullvad relay set location se'); + await assertConnected(page); + await expect(tunnelProtocol).toHaveText('OpenVPN'); +}); + +test('App should show correct OpenVPN transport protocol and port', async () => { + const inData = page.locator(':text("In") + span'); + + await expect(inData).toContainText(new RegExp(':[0-9]+')); + await expect(inData).toContainText(new RegExp('(TCP|UDP)$')); + await exec('mullvad relay set tunnel openvpn --transport-protocol udp --port 1195'); + await assertConnected(page); + await expect(inData).toContainText(new RegExp(':1195')); + + await exec('mullvad relay set tunnel openvpn --transport-protocol udp --port 1300'); + await assertConnected(page); + await expect(inData).toContainText(new RegExp(':1300')); + + await exec('mullvad relay set tunnel openvpn --transport-protocol tcp --port any'); + await expect(inData).toContainText(new RegExp(':[0-9]+')); + await expect(inData).toContainText(new RegExp('TCP$')); + + await exec('mullvad relay set tunnel openvpn --transport-protocol tcp --port 80'); + await assertConnected(page); + await expect(inData).toContainText(new RegExp(':80')); + + await exec('mullvad relay set tunnel openvpn --transport-protocol tcp --port 443'); + await assertConnected(page); + await expect(inData).toContainText(new RegExp(':443')); + + await exec('mullvad relay set tunnel openvpn --transport-protocol any'); +}); + +test('App should show bridge mode', async () => { + await exec('mullvad bridge set state on'); + const relay = page.getByTestId('hostname-line'); + await expect(relay).toHaveText(new RegExp(' via ', 'i')); + await exec('mullvad bridge set state off'); + + await exec('mullvad relay set tunnel-protocol wireguard'); +}); + +test('App should enter blocked state', async () => { + await exec('mullvad relay set location xx'); + await assertError(page); + + await exec(`mullvad relay set hostname ${process.env.HOSTNAME}`); + await assertConnected(page); +}); + +test('App should disconnect', async () => { + await page.getByText('Disconnect').click(); + await assertDisconnected(page); +}); + +test('App should create quantum secure connection', async () => { + await exec('mullvad tunnel set wireguard --quantum-resistant on'); + await page.getByText('Secure my connection').click(); + + await assertConnectingPq(page); + await assertConnectedPq(page); +}); + +test('App should show multihop', async () => { + await exec('mullvad relay set tunnel wireguard --use-multihop=on'); + const relay = page.getByTestId('hostname-line'); + await expect(relay).toHaveText(new RegExp('^' + escapeRegExp(`${process.env.HOSTNAME} via`), 'i')); + await exec('mullvad relay set tunnel wireguard --use-multihop=off'); + + await exec('mullvad tunnel set wireguard --quantum-resistant off'); + await page.getByText('Disconnect').click(); +}); + + +test('App should become connected when other frontend connects', async () => { + await assertDisconnected(page); + await Promise.all([assertConnecting(page), exec('mullvad connect')]); + await assertConnected(page); + + await Promise.all([assertDisconnecting(page), exec('mullvad disconnect')]); + await assertDisconnected(page); +}); diff --git a/gui/test/e2e/shared/tunnel-state.ts b/gui/test/e2e/shared/tunnel-state.ts index bd59aa248a..09cbcd4999 100644 --- a/gui/test/e2e/shared/tunnel-state.ts +++ b/gui/test/e2e/shared/tunnel-state.ts @@ -1,80 +1,106 @@ import { expect } from '@playwright/test'; import { Page } from 'playwright'; import { colors } from '../../../src/config.json'; -import { getBackgroundColor, getColor } from '../utils'; +import { anyOf } from '../utils'; const UNSECURED_COLOR = colors.red; const SECURE_COLOR = colors.green; const WHITE_COLOR = colors.white; +const UNSECURE_BUTTON_COLOR = anyOf(colors.red60, colors.red80); +const SECURE_BUTTON_COLOR = anyOf(colors.green, colors.green90); + const getLabel = (page: Page) => page.locator('span[role="status"]'); const getHeader = (page: Page) => page.locator('header'); export async function assertDisconnected(page: Page) { - const statusLabel = getLabel(page); - await expect(statusLabel).toContainText(/unsecured connection/i); - const labelColor = await getColor(statusLabel); - expect(labelColor).toBe(UNSECURED_COLOR); - - const header = getHeader(page); - const headerColor = await getBackgroundColor(header); - expect(headerColor).toBe(UNSECURED_COLOR); - - const button = page.locator('button', { hasText: /secure my connection/i }); - const buttonColor = await getBackgroundColor(button); - expect(buttonColor).toBe(SECURE_COLOR); + await assertTunnelState(page, { + labelText: 'unsecured connection', + labelColor: UNSECURED_COLOR, + headerColor: UNSECURED_COLOR, + buttonText: 'secure my connection', + buttonColor: SECURE_BUTTON_COLOR, + }); } export async function assertConnecting(page: Page) { - const statusLabel = getLabel(page); - await expect(statusLabel).toContainText(/creating secure connection/i); - const labelColor = await getColor(statusLabel); - expect(labelColor).toBe(WHITE_COLOR); - - const header = getHeader(page); - const headerColor = await getBackgroundColor(header); - expect(headerColor).toBe(SECURE_COLOR); - - const button = page.locator('button', { hasText: /cancel/i }); - const buttonColor = await getBackgroundColor(button); - expect(buttonColor).toBe('rgba(227, 64, 57, 0.6)'); + await assertTunnelState(page, { + labelText: 'creating secure connection', + labelColor: WHITE_COLOR, + headerColor: SECURE_COLOR, + buttonText: 'cancel', + buttonColor: UNSECURE_BUTTON_COLOR, + }); } export async function assertConnected(page: Page) { - const statusLabel = getLabel(page); - await expect(statusLabel).toContainText(/secure connection/i); - const labelColor = await getColor(statusLabel); - expect(labelColor).toBe(SECURE_COLOR); + await assertTunnelState(page, { + labelText: 'secure connection', + labelColor: SECURE_COLOR, + headerColor: SECURE_COLOR, + buttonText: 'disconnect', + buttonColor: UNSECURE_BUTTON_COLOR, + }); +} - const header = getHeader(page); - const headerColor = await getBackgroundColor(header); - expect(headerColor).toBe(SECURE_COLOR); +export async function assertDisconnecting(page: Page) { + await assertTunnelState(page, { + headerColor: UNSECURED_COLOR, + buttonText: 'secure my connection', + buttonColor: SECURE_BUTTON_COLOR, + }); +} - const button = page.locator('button', { hasText: /switch location/i }); - const buttonColor = await getBackgroundColor(button); - expect(buttonColor).toBe('rgba(255, 255, 255, 0.2)'); +export async function assertError(page: Page) { + await assertTunnelState(page, { + labelText: 'blocked connection', + labelColor: WHITE_COLOR, + headerColor: SECURE_COLOR, + }); } -export async function assertDisconnecting(page: Page) { - const statusLabel = getLabel(page); - await expect(statusLabel).toBeEmpty(); +export async function assertConnectingPq(page: Page) { + await assertTunnelState(page, { + labelText: 'creating quantum secure connection', + labelColor: WHITE_COLOR, + headerColor: SECURE_COLOR, + buttonText: 'cancel', + buttonColor: UNSECURE_BUTTON_COLOR, + }); +} - const header = getHeader(page); - const headerColor = await getBackgroundColor(header); - expect(headerColor).toBe(UNSECURED_COLOR); +export async function assertConnectedPq(page: Page) { + await assertTunnelState(page, { + labelText: 'quantum secure connection', + labelColor: SECURE_COLOR, + headerColor: SECURE_COLOR, + buttonText: 'disconnect', + buttonColor: UNSECURE_BUTTON_COLOR, + }); +} - const button = page.locator('button', { hasText: /secure my connection/i }); - const buttonColor = await getBackgroundColor(button); - expect(buttonColor).toBe(SECURE_COLOR); +interface TunnelStateContent { + labelText?: string | RegExp; + labelColor?: string; + headerColor: string; + buttonText?: string; + buttonColor?: string | RegExp; } -export async function assertError(page: Page) { +export async function assertTunnelState(page: Page, content: TunnelStateContent) { const statusLabel = getLabel(page); - await expect(statusLabel).toContainText(/blocked connection/i); - const labelColor = await getColor(statusLabel); - expect(labelColor).toBe(WHITE_COLOR); + if (content.labelText && content.labelColor) { + await expect(statusLabel).toContainText(new RegExp(content.labelText, 'i')); + await expect(statusLabel).toHaveCSS('color', content.labelColor); + } else { + await expect(statusLabel).toBeEmpty(); + } const header = getHeader(page); - const headerColor = await getBackgroundColor(header); - expect(headerColor).toBe(SECURE_COLOR); + await expect(header).toHaveCSS('background-color', content.headerColor); + + if (content.buttonText && content.buttonColor) { + const button = page.locator('button', { hasText: new RegExp(content.buttonText, 'i') }); + await expect(button).toHaveCSS('background-color', content.buttonColor); + } } diff --git a/gui/test/e2e/utils.ts b/gui/test/e2e/utils.ts index b0e4f9ec0f..dab83b2629 100644 --- a/gui/test/e2e/utils.ts +++ b/gui/test/e2e/utils.ts @@ -112,3 +112,11 @@ export const getColor = (locator: Locator) => { export const getBackgroundColor = (locator: Locator) => { return getStyleProperty(locator, 'background-color'); }; + +export function anyOf(...values: string[]): RegExp { + return new RegExp(values.map(escapeRegExp).join('|')); +} + +export function escapeRegExp(regexp: string): string { + return regexp.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} |
