From 20a4688d6fedb38f116a90e9a97320d475efd0fd Mon Sep 17 00:00:00 2001 From: Timshel Date: Tue, 20 Feb 2024 18:27:24 +0100 Subject: [PATCH] Add playwright create/login test for all db --- test/scenarios/.gitignore | 5 + test/scenarios/README.md | 34 +++++ test/scenarios/global-setup.ts | 83 +++++++++++ test/scenarios/global-utils.ts | 65 ++++++++ test/scenarios/package-lock.json | 155 ++++++++++++++++++++ test/scenarios/package.json | 18 +++ test/scenarios/playwright.config.ts | 48 ++++++ test/scenarios/test.env | 46 ++++++ test/scenarios/tests/login-common.ts | 43 ++++++ test/scenarios/tests/login.spec.ts | 21 +++ test/scenarios/tests/login_mariadb.spec.ts | 37 +++++ test/scenarios/tests/login_postgres.spec.ts | 37 +++++ 12 files changed, 592 insertions(+) create mode 100644 test/scenarios/.gitignore create mode 100644 test/scenarios/README.md create mode 100644 test/scenarios/global-setup.ts create mode 100644 test/scenarios/global-utils.ts create mode 100644 test/scenarios/package-lock.json create mode 100644 test/scenarios/package.json create mode 100644 test/scenarios/playwright.config.ts create mode 100644 test/scenarios/test.env create mode 100644 test/scenarios/tests/login-common.ts create mode 100644 test/scenarios/tests/login.spec.ts create mode 100644 test/scenarios/tests/login_mariadb.spec.ts create mode 100644 test/scenarios/tests/login_postgres.spec.ts diff --git a/test/scenarios/.gitignore b/test/scenarios/.gitignore new file mode 100644 index 00000000000..c50d09fa10f --- /dev/null +++ b/test/scenarios/.gitignore @@ -0,0 +1,5 @@ +logs +node_modules/ +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/test/scenarios/README.md b/test/scenarios/README.md new file mode 100644 index 00000000000..2c5bd411c4a --- /dev/null +++ b/test/scenarios/README.md @@ -0,0 +1,34 @@ +# OpenID Keycloak scenarios + +This allows running integration tests using [Playwright](https://playwright.dev/). +\ +It usse its own [test.env](/test/scenarios/test.env) with different ports to not collide with a running dev instance. + +## Install + +```bash +npm install +``` + +## Usage + +To run all the tests: + +```bash +npx playwright test +``` + +To access the ui to easily run test individually and debug if needed: + +```bash +npx playwright test --ui +``` + +## Writing scenario + +When creating new scenario use the recorder to more easily identify elements (in general try to rely on visible hint to identify elements and not hidden ids). +This does not start the server, you will need to start it manually. + +```bash +npx playwright codegen "http://127.0.0.1:8000" +``` diff --git a/test/scenarios/global-setup.ts b/test/scenarios/global-setup.ts new file mode 100644 index 00000000000..0525040f3a9 --- /dev/null +++ b/test/scenarios/global-setup.ts @@ -0,0 +1,83 @@ +import { firefox, type FullConfig } from '@playwright/test'; +import { exec, execSync } from 'node:child_process'; +import fs from 'fs'; +import yaml from 'js-yaml'; + +const utils = require('./global-utils'); + +utils.loadEnv(); +var kcPath = process.env.KC_SETUP_PATH; + +function readCurrentVersion(){ + try { + const vw_version_file = fs.readFileSync('data/web-vault/vw-version.json', { + encoding: 'utf8', + flag: 'r' + }); + + return JSON.parse(vw_version_file)["version"]; + } catch(err) { + console.log(`Failed to read frontend current version: ${err}`); + } +} + +function readDockerVersion(){ + try { + const docker_settings = fs.readFileSync('../../docker/DockerSettings.yaml', { + encoding: 'utf8', + flag: 'r' + }); + + const settings = yaml.load(docker_settings); + return settings["vault_version"]; + } catch(err) { + console.log(`Failed to read docker frontend current version: ${err}`); + } +} + +function retrieveFrontend(){ + const vw_version = readCurrentVersion(); + const vv = readDockerVersion() + + if( !vv ){ + console.log("Empty docker frontend version"); + process.exit(1); + } + + try { + if( vv != `v${vw_version}`) { + fs.rmSync("./data/web-vault", { recursive: true, force: true }); + + execSync(`cd data && wget -c https://github.com/dani-garcia/bw_web_builds/releases/download/${vv}/bw_web_${vv}.tar.gz -O - | tar xz`, { stdio: "inherit" }); + + // Make the SSO button visible + execSync(`bash -c "sed -i 's#a.routerlink=./sso..,##' data/web-vault/app/main.*.css"`, { stdio: "inherit" }); + + console.log(`Retrieved bw_web_builds-${vv}`); + } else { + console.log(`Using existing bw_web_builds-${vv}`); + } + } catch(err) { + console.log(`Failed to retrieve frontend: ${err}`); + process.exit(1); + } +} + +function buildServer(){ + if( !fs.existsSync('data/vaultwarden') ){ + console.log("Rebuilding server"); + execSync(`cd ../.. && cargo build --features sqlite,mysql,postgresql --release`, { stdio: "inherit" }); + execSync(`cp ../../target/release/vaultwarden data/vaultwarden`, { stdio: "inherit" }); + } else { + console.log("Using existing server"); + } +} + +async function globalSetup(config: FullConfig) { + execSync("mkdir -p data/logs"); + + buildServer(); + retrieveFrontend(); +} + +export default globalSetup; diff --git a/test/scenarios/global-utils.ts b/test/scenarios/global-utils.ts new file mode 100644 index 00000000000..03adf94a158 --- /dev/null +++ b/test/scenarios/global-utils.ts @@ -0,0 +1,65 @@ +import { type Browser } from '@playwright/test'; +import dotenv from 'dotenv'; +import dotenvExpand from 'dotenv-expand'; + +const fs = require("fs"); +const { spawn } = require('node:child_process'); + +function loadEnv(){ + var myEnv = dotenv.config({ path: 'test.env' }); + dotenvExpand.expand(myEnv); +} + +async function waitFor(url: String, browser: Browser) { + var ready = false; + var context; + + do { + try { + context = await browser.newContext(); + const page = await context.newPage(); + await page.waitForTimeout(500); + const result = await page.goto(url); + ready = result.status() === 200; + } catch(e) { + if( !e.message.includes("NS_ERROR_CONNECTION_REFUSED") ){ + throw e; + } + } finally { + await context.close(); + } + } while(!ready); +} + +async function startVaultWarden(browser: Browser, env = {}, reset: Boolean = true) { + if( reset ){ + fs.rmSync("data/db.sqlite3", { force: true }); + fs.rmSync("data/db.sqlite3-shm", { force: true }); + fs.rmSync("data/db.sqlite3-wal", { force: true }); + } + + const vw_log = fs.openSync("data/logs/vaultwarden.log", "a"); + var proc = spawn("../../target/release/vaultwarden", { + env: { ...process.env, ...env }, + stdio: [process.stdin, vw_log, vw_log] + }); + + await waitFor("/", browser); + + console.log(`VaultWarden running on: ${process.env.DOMAIN}`); + + return proc; +} + +async function stopVaultWarden(proc) { + console.log(`VaultWarden stopping`); + proc.kill(); +} + +async function restartVaultWarden(proc, browser: Browser, env) { + stopVaultWarden(proc); + return startVaultWarden(browser, env, false); +} + + +export { loadEnv, waitFor, startVaultWarden, stopVaultWarden, restartVaultWarden }; diff --git a/test/scenarios/package-lock.json b/test/scenarios/package-lock.json new file mode 100644 index 00000000000..0b5fd2e9861 --- /dev/null +++ b/test/scenarios/package-lock.json @@ -0,0 +1,155 @@ +{ + "name": "scenarios", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "scenarios", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.41.2", + "@types/node": "^20.11.16", + "abort-controller": "3.0.0", + "dotenv": "^16.4.1", + "dotenv-expand": "^10.0.0", + "js-yaml": "4.1.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz", + "integrity": "sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==", + "dev": true, + "dependencies": { + "playwright": "1.41.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@types/node": { + "version": "20.11.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz", + "integrity": "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/dotenv": { + "version": "16.4.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", + "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/playwright": { + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz", + "integrity": "sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==", + "dev": true, + "dependencies": { + "playwright-core": "1.41.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.41.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.2.tgz", + "integrity": "sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + } + } +} diff --git a/test/scenarios/package.json b/test/scenarios/package.json new file mode 100644 index 00000000000..2e8c8ffaf5d --- /dev/null +++ b/test/scenarios/package.json @@ -0,0 +1,18 @@ +{ + "name": "scenarios", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.41.2", + "@types/node": "^20.11.16", + "abort-controller": "3.0.0", + "dotenv": "^16.4.1", + "dotenv-expand": "^10.0.0", + "js-yaml": "4.1.0" + } +} diff --git a/test/scenarios/playwright.config.ts b/test/scenarios/playwright.config.ts new file mode 100644 index 00000000000..e1e65df8d7b --- /dev/null +++ b/test/scenarios/playwright.config.ts @@ -0,0 +1,48 @@ +import { defineConfig, devices } from '@playwright/test'; +import dotenv from 'dotenv'; +import dotenvExpand from 'dotenv-expand'; +import { exec } from 'node:child_process'; + +const utils = require('./global-utils'); + +utils.loadEnv(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: 'tests', + /* Run tests in files in parallel */ + fullyParallel: false, + + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + workers: 1, + + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + timeout: 10 * 1000, + expect: { timeout: 10 * 1000 }, + + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: process.env.DOMAIN, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + ], + + globalSetup: require.resolve('./global-setup'), +}); diff --git a/test/scenarios/test.env b/test/scenarios/test.env new file mode 100644 index 00000000000..050e743bc3a --- /dev/null +++ b/test/scenarios/test.env @@ -0,0 +1,46 @@ +################################################################## +### Shared Playwright conf test file VaultWarden and Databases ### +################################################################## + +############# +# Test user # +############# +TEST_USER=test +TEST_USER_PASSWORD=${TEST_USER} +TEST_USER_MAIL="${TEST_USER}@yopmail.com" + +###################### +# VaultWarden Exec # +###################### + +###################### +# VaultWarden Config # +###################### +ROCKET_PORT=8001 +DOMAIN=http://127.0.0.1:${ROCKET_PORT} +I_REALLY_WANT_VOLATILE_STORAGE=true +SSO_ENABLED=true +SSO_ONLY=false +SSO_CLIENT_ID=VaultWarden +SSO_CLIENT_SECRET=VaultWarden +SSO_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${TEST_REALM} + +WEB_VAULT_FOLDER=data/web-vault/ + +########################### +# Docker MariaDb container# +########################### +MARIADB_CONTAINER=vw-mariadb-test +MARIADB_PORT=3307 +MARIADB_USER=vaultwarden +MARIADB_PWD=vaultwarden +MARIADB_DB=vaultwarden + +############################ +# Docker Postgres container# +############################ +POSTGRES_CONTAINER=vw-postgres-test +POSTGRES_PORT=5433 +POSTGRES_USER=vaultwarden +POSTGRES_PWD=vaultwarden +POSTGRES_DB=vaultwarden diff --git a/test/scenarios/tests/login-common.ts b/test/scenarios/tests/login-common.ts new file mode 100644 index 00000000000..7593450d14b --- /dev/null +++ b/test/scenarios/tests/login-common.ts @@ -0,0 +1,43 @@ +import { expect } from '@playwright/test'; + +async function create_account({ page }){ + // Landing page + await page.goto('/'); + await page.getByRole('link', { name: 'Create account' }).click(); + + // Back to Vault create account + await expect(page).toHaveTitle(/Create account/); + await page.getByLabel(/Email address/).fill(process.env.TEST_USER_MAIL); + await page.getByLabel('Name').fill(process.env.TEST_USER); + await page.getByLabel('Master password\n (required)', { exact: true }).fill('Master password'); + await page.getByLabel('Re-type master password').fill('Master password'); + await page.getByRole('button', { name: 'Create account' }).click(); + + // Back to the login page + await expect(page).toHaveTitle('Vaultwarden Web'); + await page.getByLabel('Your new account has been created') + await page.getByRole('button', { name: 'Continue' }).click(); + + // Unlock page + await page.getByLabel('Master password').fill('Master password'); + await page.getByRole('button', { name: 'Log in with master password' }).click(); + + // We are now in the default vault page + await expect(page).toHaveTitle(/Vaults/); +} + +async function login({ page }){ + // Landing page + await page.goto('/'); + await page.getByLabel(/Email address/).fill(process.env.TEST_USER_MAIL); + await page.getByRole('button', { name: 'Continue' }).click(); + + // Unlock page + await page.getByLabel('Master password').fill('Master password'); + await page.getByRole('button', { name: 'Log in with master password' }).click(); + + // We are now in the default vault page + await expect(page).toHaveTitle(/Vaults/); +} + +export { create_account, login }; diff --git a/test/scenarios/tests/login.spec.ts b/test/scenarios/tests/login.spec.ts new file mode 100644 index 00000000000..08a8da3d896 --- /dev/null +++ b/test/scenarios/tests/login.spec.ts @@ -0,0 +1,21 @@ +import { test, expect } from '@playwright/test'; +const utils = require('../global-utils'); +const { create_account, login } = require('./login-common'); + +utils.loadEnv(); + +var proc; + +test.beforeAll('Setup', async ({ browser }) => { + proc = await utils.startVaultWarden(browser, { + SSO_ENABLED: false + }); +}); + +test.afterAll('Teardown', async () => { + utils.stopVaultWarden(proc); +}); + +test('Account creation', create_account); + +test('Master password login', login); diff --git a/test/scenarios/tests/login_mariadb.spec.ts b/test/scenarios/tests/login_mariadb.spec.ts new file mode 100644 index 00000000000..067ef4558a7 --- /dev/null +++ b/test/scenarios/tests/login_mariadb.spec.ts @@ -0,0 +1,37 @@ +import { test, expect, type Page } from '@playwright/test'; +import { execSync } from 'node:child_process'; + +const utils = require('../global-utils'); +const { create_account, login } = require('./login-common'); + +utils.loadEnv(); + +var proc; + +test.beforeAll('Setup', async ({ browser }) => { + console.log(`Stopping MariaDB if it's already running`); + execSync(`docker stop ${process.env.MARIADB_CONTAINER} || true`); + console.log(`Starting MariaDB`); + execSync(`docker run --rm --name ${process.env.MARIADB_CONTAINER} \ + -e MARIADB_ROOT_PASSWORD=${process.env.MARIADB_PWD} \ + -e MARIADB_USER=${process.env.MARIADB_USER} \ + -e MARIADB_PASSWORD=${process.env.MARIADB_PWD} \ + -e MARIADB_DATABASE=${process.env.MARIADB_DB} \ + -p ${process.env.MARIADB_PORT}:3306 \ + -d mariadb:10.4` + ); + proc = await utils.startVaultWarden(browser, { + SSO_ENABLED: false, + DATABASE_URL: `mysql://${process.env.MARIADB_USER}:${process.env.MARIADB_PWD}@127.0.0.1:${process.env.MARIADB_PORT}/${process.env.MARIADB_DB}` + }); +}); + +test.afterAll('Teardown', async () => { + utils.stopVaultWarden(proc); + console.log(`Stopping MariaDB`); + execSync(`docker stop ${process.env.MARIADB_CONTAINER}`); +}); + +test('Account creation', create_account); + +test('Master password login', login); diff --git a/test/scenarios/tests/login_postgres.spec.ts b/test/scenarios/tests/login_postgres.spec.ts new file mode 100644 index 00000000000..4a6d4f97b46 --- /dev/null +++ b/test/scenarios/tests/login_postgres.spec.ts @@ -0,0 +1,37 @@ +import { test, expect, type Page } from '@playwright/test'; +import { execSync } from 'node:child_process'; + +const utils = require('../global-utils'); +const { create_account, login } = require('./login-common'); + +utils.loadEnv(); + +var proc; + +test.beforeAll('Setup', async ({ browser }) => { + console.log(`Stopping Postgres if it's already running`); + execSync(`docker stop ${process.env.POSTGRES_CONTAINER} || true`); + console.log(`Starting Postgres`); + execSync(`docker run --rm --name ${process.env.POSTGRES_CONTAINER} \ + -e POSTGRES_USER=${process.env.POSTGRES_USER} \ + -e POSTGRES_PASSWORD=${process.env.POSTGRES_PWD} \ + -e POSTGRES_DB=${process.env.POSTGRES_DB} \ + -p ${process.env.POSTGRES_PORT}:5432 \ + -d postgres:16.2` + ); + + proc = await utils.startVaultWarden(browser, { + SSO_ENABLED: false, + DATABASE_URL: `postgresql://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PWD}@127.0.0.1:${process.env.POSTGRES_PORT}/${process.env.POSTGRES_DB}` + }); +}); + +test.afterAll('Teardown', async () => { + utils.stopVaultWarden(proc); + console.log(`Stopping Postgres`); + execSync(`docker stop ${process.env.POSTGRES_CONTAINER}`); +}); + +test('Account creation', create_account); + +test('Master password login', login);