From c6601eebf13510057fd338e60166322b2ddcf908 Mon Sep 17 00:00:00 2001 From: Oskar Date: Mon, 9 Sep 2024 21:50:08 +0200 Subject: [PATCH 1/5] Add settings test --- .../state-dependent/settings.spec.ts | 85 +++++++++++++++++++ gui/test/e2e/utils.ts | 13 ++- test/test-manager/src/tests/ui.rs | 8 ++ 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 gui/test/e2e/installed/state-dependent/settings.spec.ts diff --git a/gui/test/e2e/installed/state-dependent/settings.spec.ts b/gui/test/e2e/installed/state-dependent/settings.spec.ts new file mode 100644 index 000000000000..2b5643a310ca --- /dev/null +++ b/gui/test/e2e/installed/state-dependent/settings.spec.ts @@ -0,0 +1,85 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { expect, Page, test } from '@playwright/test'; +import { startInstalledApp } from '../installed-utils'; +import { fileExists, TestUtils } from '../../utils'; + +if (process.env.HOME === undefined) { + throw new Error('$HOME not set'); +} + +const AUTOSTART_PATH = path.join(process.env.HOME, '.config', 'autostart', 'mullvad-vpn.desktop'); + +let page: Page; +let util: TestUtils; + +test.beforeAll(async () => { + ({ page, util } = await startInstalledApp()); +}); + +test.afterAll(async () => { + await page.close(); +}); + +test.describe('VPN Settings', () => { + test('Auto-connect setting', async () => { + // Navigate to the VPN settings view + await util.waitForNavigation(async () => await page.click('button[aria-label="Settings"]')); + await util.waitForNavigation(async () => await page.click('text=VPN settings')); + + // Find the auto-connect toggle + const autoConnectToggle = page.getByText('Auto-connect').locator('..').getByRole('checkbox'); + + // Check initial state + const initialCliState = execSync('mullvad auto-connect get').toString().trim(); + expect(initialCliState).toMatch(/off$/); + await expect(autoConnectToggle).toHaveAttribute('aria-checked', 'false') + + // Toggle auto-connect + await autoConnectToggle.click(); + + // Verify the setting was applied correctly + await expect(autoConnectToggle).toHaveAttribute('aria-checked', 'true') + const newCliState = execSync('mullvad auto-connect get').toString().trim(); + expect(newCliState).toMatch(/off$/); + }); + + test('Launch on startup setting', async () => { + // Find the auto-connect toggle + const launchOnStartupToggle = + page.getByText('Launch app on start-up').locator('..').getByRole('checkbox'); + + // Check initial state + const initialCliState = execSync('mullvad auto-connect get').toString().trim(); + expect(initialCliState).toMatch(/off$/); + await expect(launchOnStartupToggle).toHaveAttribute('aria-checked', 'false') + expect(fileExists(AUTOSTART_PATH)).toBeFalsy(); + + // Toggle auto-connect + await launchOnStartupToggle.click(); + + // Verify the setting was applied correctly + await expect(launchOnStartupToggle).toHaveAttribute('aria-checked', 'true') + expect(fileExists(AUTOSTART_PATH)).toBeTruthy(); + const newCliState = execSync('mullvad auto-connect get').toString().trim(); + expect(newCliState).toMatch(/on$/); + }); + + test('LAN settings', async () => { + // Find the LAN toggle + const lanToggle = page.getByText('Local network sharing').locator('..').getByRole('checkbox'); + + // Check initial state + const initialCliState = execSync('mullvad lan get').toString().trim(); + expect(initialCliState).toMatch(/block$/); + await expect(lanToggle).toHaveAttribute('aria-checked', 'false') + + // Toggle LAN setting + await lanToggle.click(); + + // Verify the setting was applied correctly + await expect(lanToggle).toHaveAttribute('aria-checked', 'true') + const newState = execSync('mullvad lan get').toString().trim(); + expect(newState).toMatch(/allow$/); + }); +}); diff --git a/gui/test/e2e/utils.ts b/gui/test/e2e/utils.ts index 2fb33319f20e..3e5e40e75f07 100644 --- a/gui/test/e2e/utils.ts +++ b/gui/test/e2e/utils.ts @@ -1,4 +1,5 @@ -import { _electron as electron, ElectronApplication, Locator, Page } from 'playwright'; +import fs from 'fs'; +import { Locator, Page, _electron as electron, ElectronApplication } from 'playwright'; export interface StartAppResponse { app: ElectronApplication; @@ -122,3 +123,13 @@ export function anyOf(...values: string[]): RegExp { export function escapeRegExp(regexp: string): string { return regexp.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string } + + +export function fileExists(filePath: string): boolean { + try { + fs.accessSync(filePath); + return true; + } catch (e) { + return false; + } +} diff --git a/test/test-manager/src/tests/ui.rs b/test/test-manager/src/tests/ui.rs index 234fe7706565..d4758d9ced61 100644 --- a/test/test-manager/src/tests/ui.rs +++ b/test/test-manager/src/tests/ui.rs @@ -284,3 +284,11 @@ pub async fn test_obfuscation_settings_ui(_: TestContext, rpc: ServiceClient) -> assert!(ui_result.success()); Ok(()) } + +/// Test settings in the GUI +#[test_function] +pub async fn test_settings_ui(_: TestContext, rpc: ServiceClient) -> Result<(), Error> { + let ui_result = run_test(&rpc, &["settings.spec"]).await?; + assert!(ui_result.success()); + Ok(()) +} From 660b55b00121e2412a2fcc61f94088317525a8e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20L=C3=B6nnhager?= Date: Mon, 7 Oct 2024 13:15:11 +0200 Subject: [PATCH 2/5] Toggle off GUI auto-connect after test GUI settings are not cleaned up between tests --- .../e2e/installed/state-dependent/settings.spec.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/gui/test/e2e/installed/state-dependent/settings.spec.ts b/gui/test/e2e/installed/state-dependent/settings.spec.ts index 2b5643a310ca..e3cd634b7c73 100644 --- a/gui/test/e2e/installed/state-dependent/settings.spec.ts +++ b/gui/test/e2e/installed/state-dependent/settings.spec.ts @@ -45,7 +45,7 @@ test.describe('VPN Settings', () => { }); test('Launch on startup setting', async () => { - // Find the auto-connect toggle + // Find the launch on start-up toggle const launchOnStartupToggle = page.getByText('Launch app on start-up').locator('..').getByRole('checkbox'); @@ -55,7 +55,7 @@ test.describe('VPN Settings', () => { await expect(launchOnStartupToggle).toHaveAttribute('aria-checked', 'false') expect(fileExists(AUTOSTART_PATH)).toBeFalsy(); - // Toggle auto-connect + // Toggle launch on start-up await launchOnStartupToggle.click(); // Verify the setting was applied correctly @@ -63,6 +63,14 @@ test.describe('VPN Settings', () => { expect(fileExists(AUTOSTART_PATH)).toBeTruthy(); const newCliState = execSync('mullvad auto-connect get').toString().trim(); expect(newCliState).toMatch(/on$/); + + await launchOnStartupToggle.click(); + + // Toggle auto-connect back off + // NOTE: This must be done to clean up the auto-start file + // TODO: Reset GUI settings between all tests + const autoConnectToggle = page.getByText('Auto-connect').locator('..').getByRole('checkbox'); + await autoConnectToggle.click(); }); test('LAN settings', async () => { From 227098cec09eecd09427bca8f838fa828b6cabf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20L=C3=B6nnhager?= Date: Mon, 7 Oct 2024 11:29:31 +0200 Subject: [PATCH 3/5] Use 'os.homedir' instead of HOME environment variable --- .../installed/state-dependent/settings.spec.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/gui/test/e2e/installed/state-dependent/settings.spec.ts b/gui/test/e2e/installed/state-dependent/settings.spec.ts index e3cd634b7c73..457559480364 100644 --- a/gui/test/e2e/installed/state-dependent/settings.spec.ts +++ b/gui/test/e2e/installed/state-dependent/settings.spec.ts @@ -1,14 +1,17 @@ import path from 'path'; +import os from 'os'; import { execSync } from 'child_process'; import { expect, Page, test } from '@playwright/test'; import { startInstalledApp } from '../installed-utils'; import { fileExists, TestUtils } from '../../utils'; -if (process.env.HOME === undefined) { - throw new Error('$HOME not set'); +function getAutoStartPath() { + return path.join(os.homedir(), '.config', 'autostart', 'mullvad-vpn.desktop'); } -const AUTOSTART_PATH = path.join(process.env.HOME, '.config', 'autostart', 'mullvad-vpn.desktop'); +function autoStartPathExists() { + return fileExists(getAutoStartPath()); +} let page: Page; let util: TestUtils; @@ -53,14 +56,18 @@ test.describe('VPN Settings', () => { const initialCliState = execSync('mullvad auto-connect get').toString().trim(); expect(initialCliState).toMatch(/off$/); await expect(launchOnStartupToggle).toHaveAttribute('aria-checked', 'false') - expect(fileExists(AUTOSTART_PATH)).toBeFalsy(); + if (process.platform === 'linux') { + expect(autoStartPathExists()).toBeFalsy(); + } // Toggle launch on start-up await launchOnStartupToggle.click(); // Verify the setting was applied correctly await expect(launchOnStartupToggle).toHaveAttribute('aria-checked', 'true') - expect(fileExists(AUTOSTART_PATH)).toBeTruthy(); + if (process.platform === 'linux') { + expect(autoStartPathExists()).toBeTruthy(); + } const newCliState = execSync('mullvad auto-connect get').toString().trim(); expect(newCliState).toMatch(/on$/); From 1c209ec4d2c869c220f40f0aac04dfd363883678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20L=C3=B6nnhager?= Date: Mon, 7 Oct 2024 10:40:02 +0200 Subject: [PATCH 4/5] Set HOME env var in test runner --- test/Cargo.lock | 1 + test/scripts/ssh-setup.sh | 5 +++-- test/test-runner/Cargo.toml | 1 + test/test-runner/src/main.rs | 18 +++++++++++++++--- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/test/Cargo.lock b/test/Cargo.lock index 6209028fde59..67f538e39aa0 100644 --- a/test/Cargo.lock +++ b/test/Cargo.lock @@ -3409,6 +3409,7 @@ version = "0.0.0" dependencies = [ "bytes", "chrono", + "dirs", "futures", "libc", "log", diff --git a/test/scripts/ssh-setup.sh b/test/scripts/ssh-setup.sh index 08887d4aba6d..66809407d8ea 100644 --- a/test/scripts/ssh-setup.sh +++ b/test/scripts/ssh-setup.sh @@ -22,8 +22,9 @@ for file in test-runner connection-checker $APP_PACKAGE $PREVIOUS_APP $UI_RUNNER cp -f "$SCRIPT_DIR/$file" "$RUNNER_DIR" done -# Unprivileged users need execute rights for connection checker -chmod 551 "${RUNNER_DIR}/connection-checker" +# Unprivileged users need execute rights for some executables +chmod 775 "${RUNNER_DIR}/connection-checker" +chmod 775 "${RUNNER_DIR}/$UI_RUNNER" chown -R root "$RUNNER_DIR/" diff --git a/test/test-runner/Cargo.toml b/test/test-runner/Cargo.toml index 7206cb394a8d..8df61e7164f8 100644 --- a/test/test-runner/Cargo.toml +++ b/test/test-runner/Cargo.toml @@ -11,6 +11,7 @@ rust-version.workspace = true workspace = true [dependencies] +dirs = "5.0.1" futures = { workspace = true } tarpc = { workspace = true } tokio = { workspace = true } diff --git a/test/test-runner/src/main.rs b/test/test-runner/src/main.rs index 79bc17b4ef50..735e61360ec1 100644 --- a/test/test-runner/src/main.rs +++ b/test/test-runner/src/main.rs @@ -92,12 +92,24 @@ impl Service for TestServer { log::debug!("Exec {} (args: {args:?})", path); let mut cmd = Command::new(&path); + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + cmd.stdin(Stdio::piped()); cmd.args(args); - // Make sure that PATH is updated - // TODO: We currently do not need this on non-Windows #[cfg(target_os = "windows")] - cmd.env("PATH", sys::get_system_path_var()?); + { + // Make sure that PATH is updated + cmd.env("PATH", sys::get_system_path_var()?); + if let Some(home_dir) = dirs::home_dir() { + cmd.env("USERPROFILE", home_dir); + } + } + + #[cfg(unix)] + if let Some(home_dir) = dirs::home_dir() { + cmd.env("HOME", home_dir); + } cmd.envs(env); From 95c0f9b0392802ee64a7c8a0f42ac0da3f94bed4 Mon Sep 17 00:00:00 2001 From: Oskar Date: Thu, 10 Oct 2024 10:35:47 +0200 Subject: [PATCH 5/5] Fix linter errors --- .../state-dependent/settings.spec.ts | 31 ++++++++++--------- gui/test/e2e/utils.ts | 5 ++- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/gui/test/e2e/installed/state-dependent/settings.spec.ts b/gui/test/e2e/installed/state-dependent/settings.spec.ts index 457559480364..343d0fc4301d 100644 --- a/gui/test/e2e/installed/state-dependent/settings.spec.ts +++ b/gui/test/e2e/installed/state-dependent/settings.spec.ts @@ -1,9 +1,10 @@ -import path from 'path'; -import os from 'os'; -import { execSync } from 'child_process'; import { expect, Page, test } from '@playwright/test'; -import { startInstalledApp } from '../installed-utils'; +import { execSync } from 'child_process'; +import os from 'os'; +import path from 'path'; + import { fileExists, TestUtils } from '../../utils'; +import { startInstalledApp } from '../installed-utils'; function getAutoStartPath() { return path.join(os.homedir(), '.config', 'autostart', 'mullvad-vpn.desktop'); @@ -27,8 +28,8 @@ test.afterAll(async () => { test.describe('VPN Settings', () => { test('Auto-connect setting', async () => { // Navigate to the VPN settings view - await util.waitForNavigation(async () => await page.click('button[aria-label="Settings"]')); - await util.waitForNavigation(async () => await page.click('text=VPN settings')); + await util.waitForNavigation(() => page.click('button[aria-label="Settings"]')); + await util.waitForNavigation(() => page.click('text=VPN settings')); // Find the auto-connect toggle const autoConnectToggle = page.getByText('Auto-connect').locator('..').getByRole('checkbox'); @@ -36,26 +37,28 @@ test.describe('VPN Settings', () => { // Check initial state const initialCliState = execSync('mullvad auto-connect get').toString().trim(); expect(initialCliState).toMatch(/off$/); - await expect(autoConnectToggle).toHaveAttribute('aria-checked', 'false') + await expect(autoConnectToggle).toHaveAttribute('aria-checked', 'false'); // Toggle auto-connect await autoConnectToggle.click(); // Verify the setting was applied correctly - await expect(autoConnectToggle).toHaveAttribute('aria-checked', 'true') + await expect(autoConnectToggle).toHaveAttribute('aria-checked', 'true'); const newCliState = execSync('mullvad auto-connect get').toString().trim(); expect(newCliState).toMatch(/off$/); }); test('Launch on startup setting', async () => { // Find the launch on start-up toggle - const launchOnStartupToggle = - page.getByText('Launch app on start-up').locator('..').getByRole('checkbox'); + const launchOnStartupToggle = page + .getByText('Launch app on start-up') + .locator('..') + .getByRole('checkbox'); // Check initial state const initialCliState = execSync('mullvad auto-connect get').toString().trim(); expect(initialCliState).toMatch(/off$/); - await expect(launchOnStartupToggle).toHaveAttribute('aria-checked', 'false') + await expect(launchOnStartupToggle).toHaveAttribute('aria-checked', 'false'); if (process.platform === 'linux') { expect(autoStartPathExists()).toBeFalsy(); } @@ -64,7 +67,7 @@ test.describe('VPN Settings', () => { await launchOnStartupToggle.click(); // Verify the setting was applied correctly - await expect(launchOnStartupToggle).toHaveAttribute('aria-checked', 'true') + await expect(launchOnStartupToggle).toHaveAttribute('aria-checked', 'true'); if (process.platform === 'linux') { expect(autoStartPathExists()).toBeTruthy(); } @@ -87,13 +90,13 @@ test.describe('VPN Settings', () => { // Check initial state const initialCliState = execSync('mullvad lan get').toString().trim(); expect(initialCliState).toMatch(/block$/); - await expect(lanToggle).toHaveAttribute('aria-checked', 'false') + await expect(lanToggle).toHaveAttribute('aria-checked', 'false'); // Toggle LAN setting await lanToggle.click(); // Verify the setting was applied correctly - await expect(lanToggle).toHaveAttribute('aria-checked', 'true') + await expect(lanToggle).toHaveAttribute('aria-checked', 'true'); const newState = execSync('mullvad lan get').toString().trim(); expect(newState).toMatch(/allow$/); }); diff --git a/gui/test/e2e/utils.ts b/gui/test/e2e/utils.ts index 3e5e40e75f07..97524f69bcda 100644 --- a/gui/test/e2e/utils.ts +++ b/gui/test/e2e/utils.ts @@ -1,5 +1,5 @@ import fs from 'fs'; -import { Locator, Page, _electron as electron, ElectronApplication } from 'playwright'; +import { _electron as electron, ElectronApplication, Locator, Page } from 'playwright'; export interface StartAppResponse { app: ElectronApplication; @@ -124,12 +124,11 @@ export function escapeRegExp(regexp: string): string { return regexp.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string } - export function fileExists(filePath: string): boolean { try { fs.accessSync(filePath); return true; - } catch (e) { + } catch { return false; } }