summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2023-06-12 14:24:15 +0200
committerOskar Nyberg <oskar@mullvad.net>2023-06-12 14:24:15 +0200
commit601109ac7845bf603e4b2d17bb72f30401706b71 (patch)
tree831b339f0e38614e129fcce89079bb55a4af44b0
parente41d6ff007f667c8efcdf885b71d323a93e16df5 (diff)
parent1822c81e8f87ecce858603b78492f52df1eba450 (diff)
downloadmullvadvpn-601109ac7845bf603e4b2d17bb72f30401706b71.tar.xz
mullvadvpn-601109ac7845bf603e4b2d17bb72f30401706b71.zip
Merge branch 'create-gui-tests-for-tunnel-state-des-61'
-rw-r--r--gui/src/config.json6
-rw-r--r--gui/src/renderer/components/ConnectionPanel.tsx4
-rw-r--r--gui/test/e2e/installed/state-dependent/login.spec.ts4
-rw-r--r--gui/test/e2e/installed/state-dependent/tunnel-state.spec.ts177
-rw-r--r--gui/test/e2e/shared/tunnel-state.ts126
-rw-r--r--gui/test/e2e/utils.ts8
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
+}