Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
name: E2E tests

on:
workflow_dispatch:
push:
branches: [main, dev, 'release/**']
paths-ignore: ['*.md', 'LICENSE']
pull_request:
branches: [main, dev, 'release/**']
paths-ignore: ['*.md', 'LICENSE']

concurrency:
group: e2e-${{ github.ref }}
cancel-in-progress: true

jobs:
e2e:
runs-on: [self-hosted, Linux, X64]
timeout-minutes: 90
container:
image: ubuntu:24.04
options: --privileged
env:
DEBIAN_FRONTEND: noninteractive
CLIENT_BINARY: ${{ github.workspace }}/src-tauri/target/release/defguard-client
NATIVE_DRIVER: /usr/bin/WebKitWebDriver
SQLX_OFFLINE: '1'
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: 'true'
steps:
- name: Install system dependencies
run: |
apt-get update
apt-get install -y git curl ca-certificates build-essential \
libgtk-3-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev \
librsvg2-dev patchelf libssl-dev libxdo-dev protobuf-compiler \
libprotobuf-dev webkit2gtk-driver xvfb wireguard-tools iproute2 \
iputils-ping procps xclip desktop-file-utils xdg-utils

- uses: actions/checkout@v6
with:
submodules: recursive

- uses: actions/setup-node@v6
with:
node-version: 24

- name: Install pnpm
uses: pnpm/action-setup@v6
with:
cache: true
version: 11
run_install: false

- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable

- name: Install sccache
uses: mozilla-actions/sccache-action@v0.0.10

- name: Install tauri-driver
run: cargo install tauri-driver --locked

- name: Build new-ui
run: pnpm install --no-frozen-lockfile && pnpm build
working-directory: new-ui

- name: Build client
run: pnpm install --no-frozen-lockfile && pnpm tauri build

- name: Prepare e2e environment
run: |
cp e2e/.env.example e2e/.env
echo "CORE_URL=${{ secrets.E2E_CORE_URL }}" >> e2e/.env
echo "PROXY_URL=${{ secrets.E2E_PROXY_URL }}" >> e2e/.env
echo "CORE_ADMIN_PASSWORD=${{ secrets.E2E_CORE_ADMIN_PASSWORD }}" >> e2e/.env
echo "NETWORK_ENDPOINT=${{ secrets.E2E_NETWORK_ENDPOINT }}" >> e2e/.env

- name: Check deployment is reachable
run: curl -sf --max-time 15 "${{ secrets.E2E_CORE_URL }}/api/v1/health"

- name: Stub resolvconf
run: |
printf '#!/bin/sh\ncat >/dev/null 2>&1 || true\nexit 0\n' > /usr/local/sbin/resolvconf
chmod +x /usr/local/sbin/resolvconf

- name: Start defguard-service
run: |
groupadd -f defguard
modprobe wireguard || true
setsid src-tauri/target/release/defguard-service < /dev/null &
for _ in $(seq 1 40); do [ -S /var/run/defguard.socket ] && break; sleep 0.5; done
test -S /var/run/defguard.socket

- name: Install e2e dependencies
run: pnpm install --no-frozen-lockfile
working-directory: e2e

- name: Provision core (network + gateway check)
run: pnpm provision
working-directory: e2e

- name: Run e2e tests
run: xvfb-run -a pnpm test
working-directory: e2e

lint-e2e:
runs-on:
- codebuild-defguard-client-runner-${{ github.run_id }}-${{ github.run_attempt }}
steps:
- uses: actions/checkout@v6

- uses: actions/setup-node@v6
with:
node-version: 24

- name: Install pnpm
uses: pnpm/action-setup@v6
with:
version: 11
run_install: false

- name: Install e2e dependencies
run: pnpm install --no-frozen-lockfile
working-directory: e2e

- name: Lint e2e
run: pnpm lint
working-directory: e2e
11 changes: 11 additions & 0 deletions e2e/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CORE_URL=
PROXY_URL=
CORE_ADMIN_USER=admin
CORE_ADMIN_PASSWORD=
TEST_USERNAME=e2e_test_user
GATEWAY_VPN_IP=10.10.10.1
NETWORK_ENDPOINT=
NETWORK_NAME=e2e
NETWORK_ADDRESS=10.10.10.1/24
NETWORK_PORT=50051
NETWORK_ALLOWED_IPS=10.10.10.0/24
3 changes: 3 additions & 0 deletions e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
logs/
.env
47 changes: 47 additions & 0 deletions e2e/biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
"root": false,
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"includes": ["helpers/**", "tests/**", "scripts/**", "wdio.conf.ts"]
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 90,
"bracketSpacing": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"quoteProperties": "asNeeded",
"trailingCommas": "all",
"semicolons": "always",
"arrowParentheses": "always",
"bracketSameLine": false,
"bracketSpacing": true
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}
19 changes: 19 additions & 0 deletions e2e/helpers/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { $, browser } from '@wdio/globals';
import { switchToFullView } from './windows.js';

type TauriWindow = {
__TAURI_INTERNALS__: { invoke: (cmd: string, args?: unknown) => Promise<unknown> };
};

export const resetInstances = async () => {
await switchToFullView();
await $('a[href="/full/add"]').click();
await $('#add-page-view').waitForDisplayed();
await browser.execute(async () => {
const { invoke } = (window as unknown as TauriWindow).__TAURI_INTERNALS__;
const instances = (await invoke('all_instances')) as Array<{ id: number }>;
await Promise.all(
instances.map((instance) => invoke('delete_instance', { instanceId: instance.id })),
);
});
};
9 changes: 9 additions & 0 deletions e2e/helpers/clipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { spawnSync } from 'node:child_process';

export const readClipboard = (): string => {
const result = spawnSync('xclip', ['-selection', 'clipboard', '-o'], {
encoding: 'utf8',
timeout: 5_000,
});
return result.status === 0 ? result.stdout : '';
};
40 changes: 40 additions & 0 deletions e2e/helpers/connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { $, browser } from '@wdio/globals';
import { submitTotpCode } from './mfa.js';
import { canPingGateway } from './tunnel.js';

export const FULL_MFA_VIEW = '#mfa-totp-view';
export const TRAY_MFA_VIEW = '.location-card-mfa-totp-view';

export const connectAndPing = async (mfaView: string, totpSecret?: string) => {
const button = $('.connect-button');
await button.waitForClickable();
await button.click();

if (totpSecret) {
const view = $(mfaView);
await view.waitForDisplayed();
await submitTotpCode(
totpSecret,
mfaView,
async () => {
const verify = view.$('button=Verify');
await verify.waitForClickable();
await verify.click();
},
async () => !(await view.isDisplayed().catch(() => false)),
);
}

await browser.waitUntil(() => canPingGateway(), {
timeout: 30_000,
interval: 2_000,
timeoutMsg: 'Could not ping the gateway through the VPN',
});
};

export const disconnect = async () => {
const button = $('.connect-button.connected');
await button.waitForClickable();
await button.click();
await $('.connect-button.disconnected').waitForDisplayed();
};
Loading
Loading