From 455b6f4d193d01e35b37ba8cc285e745797ec11d Mon Sep 17 00:00:00 2001 From: Timshel Date: Wed, 25 Sep 2024 15:42:05 +0200 Subject: [PATCH] Add playwright tests --- SSO.md | 7 + playwright/.env.template | 63 + playwright/.gitignore | 6 + playwright/README.md | 177 ++ playwright/compose/keycloak/Dockerfile | 40 + playwright/compose/keycloak/setup.sh | 36 + playwright/compose/playwright/Dockerfile | 40 + playwright/compose/vaultwarden/Dockerfile | 39 + playwright/compose/vaultwarden/build.sh | 24 + playwright/docker-compose.yml | 121 + playwright/global-setup.ts | 22 + playwright/global-utils.ts | 219 ++ playwright/package-lock.json | 2370 +++++++++++++++++ playwright/package.json | 21 + playwright/playwright.config.ts | 132 + playwright/test.env | 90 + playwright/tests/login.smtp.spec.ts | 159 ++ playwright/tests/login.spec.ts | 97 + playwright/tests/organization.spec.ts | 159 ++ playwright/tests/setups/db-setup.ts | 7 + playwright/tests/setups/db-teardown.ts | 11 + playwright/tests/setups/db-test.ts | 9 + playwright/tests/setups/sso-setup.ts | 19 + playwright/tests/setups/sso-teardown.ts | 15 + playwright/tests/setups/sso.ts | 82 + playwright/tests/setups/user.ts | 47 + playwright/tests/sso_login.spec.ts | 74 + playwright/tests/sso_organization.spec.ts | 152 ++ .../templates/email/send_org_invite.html.hbs | 2 +- .../templates/email/twofactor_email.html.hbs | 2 +- .../templates/email/verify_email.html.hbs | 2 +- 31 files changed, 4241 insertions(+), 3 deletions(-) create mode 100644 playwright/.env.template create mode 100644 playwright/.gitignore create mode 100644 playwright/README.md create mode 100644 playwright/compose/keycloak/Dockerfile create mode 100755 playwright/compose/keycloak/setup.sh create mode 100644 playwright/compose/playwright/Dockerfile create mode 100644 playwright/compose/vaultwarden/Dockerfile create mode 100755 playwright/compose/vaultwarden/build.sh create mode 100644 playwright/docker-compose.yml create mode 100644 playwright/global-setup.ts create mode 100644 playwright/global-utils.ts create mode 100644 playwright/package-lock.json create mode 100644 playwright/package.json create mode 100644 playwright/playwright.config.ts create mode 100644 playwright/test.env create mode 100644 playwright/tests/login.smtp.spec.ts create mode 100644 playwright/tests/login.spec.ts create mode 100644 playwright/tests/organization.spec.ts create mode 100644 playwright/tests/setups/db-setup.ts create mode 100644 playwright/tests/setups/db-teardown.ts create mode 100644 playwright/tests/setups/db-test.ts create mode 100644 playwright/tests/setups/sso-setup.ts create mode 100644 playwright/tests/setups/sso-teardown.ts create mode 100644 playwright/tests/setups/sso.ts create mode 100644 playwright/tests/setups/user.ts create mode 100644 playwright/tests/sso_login.spec.ts create mode 100644 playwright/tests/sso_organization.spec.ts diff --git a/SSO.md b/SSO.md index dc44d6de..d185c351 100644 --- a/SSO.md +++ b/SSO.md @@ -99,6 +99,13 @@ Server configuration, nothing specific just set: - `SSO_CLIENT_SECRET` - `SSO_PKCE=true` +### Testing + +If you want to run a testing instance of Keycloak the Playwright [docker-compose](playwright/docker-compose.yml) can be used. +\ +More details on how to use it in [README.md](playwright/README.md#openid-connect-test-setup). + + ## Auth0 Not working due to the following issue https://github.com/ramosbugs/openidconnect-rs/issues/23 (they appear not to follow the spec). diff --git a/playwright/.env.template b/playwright/.env.template new file mode 100644 index 00000000..5b6c0c9e --- /dev/null +++ b/playwright/.env.template @@ -0,0 +1,63 @@ +################################# +### Conf to run dev instances ### +################################# +ENV=dev +DC_ENV_FILE=.env +COMPOSE_IGNORE_ORPHANS=True +DOCKER_BUILDKIT=1 + +################ +# Users Config # +################ +TEST_USER=test +TEST_USER_PASSWORD=${TEST_USER} +TEST_USER_MAIL=${TEST_USER}@yopmail.com + +TEST_USER2=test2 +TEST_USER2_PASSWORD=${TEST_USER2} +TEST_USER2_MAIL=${TEST_USER2}@yopmail.com + +TEST_USER3=test3 +TEST_USER3_PASSWORD=${TEST_USER3} +TEST_USER3_MAIL=${TEST_USER3}@yopmail.com + +################### +# Keycloak Config # +################### +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN} +KC_HTTP_HOST=127.0.0.1 +KC_HTTP_PORT=8080 + +# Script parameters (use Keycloak and VaultWarden config too) +TEST_REALM=test +DUMMY_REALM=dummy +DUMMY_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${DUMMY_REALM} + +###################### +# Vaultwarden Config # +###################### +ROCKET_ADDRESS=0.0.0.0 +ROCKET_PORT=8000 +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} + +SMTP_HOST=127.0.0.1 +SMTP_PORT=1025 +SMTP_SECURITY=off +SMTP_TIMEOUT=5 +SMTP_FROM=vaultwarden@test +SMTP_FROM_NAME=Vaultwarden + +######################################################## +# DUMMY values for docker-compose to stop bothering us # +######################################################## +MARIADB_PORT=3305 +MYSQL_PORT=3307 +POSTGRES_PORT=5432 diff --git a/playwright/.gitignore b/playwright/.gitignore new file mode 100644 index 00000000..8746d597 --- /dev/null +++ b/playwright/.gitignore @@ -0,0 +1,6 @@ +logs +node_modules/ +/test-results/ +/playwright-report/ +/playwright/.cache/ +temp diff --git a/playwright/README.md b/playwright/README.md new file mode 100644 index 00000000..c470fbae --- /dev/null +++ b/playwright/README.md @@ -0,0 +1,177 @@ +# Integration tests + +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 + +This rely on `docker` and the `compose` [plugin](https://docs.docker.com/compose/install/). +Databases (`Mariadb`, `Mysql` and `Postgres`) and `Playwright` will run in containers. + +### Running Playwright outside docker + +It's possible to run `Playwright` outside of the container, this remove the need to rebuild the image for each change. +You'll additionally need `nodejs` then run: + +```bash +npm install +npx playwright install-deps +npx playwright install firefox +``` + +## Usage + +To run all the tests: + +```bash +DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright +``` + +To force a rebuild of the Playwright image: +```bash +DOCKER_BUILDKIT=1 docker compose --env-file test.env build Playwright +``` + +To access the ui to easily run test individually and debug if needed (will not work in docker): + +```bash +npx playwright test --ui +``` + +### DB + +Projects are configured to allow to run tests only on specific database. +\ +You can use: + +```bash +DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=mariadb +DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=mysql +DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=postgres +DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite +``` + +### SSO + +To run the SSO tests: + +```bash +DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project sso-sqlite +``` + +### Keep services running + +If you want you can keep the Db and Keycloak runnning (states are not impacted by the tests): + +```bash +PW_KEEP_SERVICE_RUNNNING=true npx playwright test +``` + +### Running specific tests + +To run a whole file you can : + +```bash +DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite tests/login.spec.ts +DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite login +``` + +To run only a specifc test (It might fail if it has dependency): + +```bash +DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite -g "Account creation" +DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env run Playwright test --project=sqlite tests/login.spec.ts:16 +``` + +## 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" +``` + +## Override web-vault + +It's possible to change the `web-vault` used by referencing a different `bw_web_builds` commit. + +```bash +export PW_WV_REPO_URL=https://github.com/Timshel/oidc_web_builds.git +export PW_WV_COMMIT_HASH=8707dc76df3f0cceef2be5bfae37bb29bd17fae6 +DOCKER_BUILDKIT=1 docker compose --profile playwright --env-file test.env build Playwright +``` + +# OpenID Connect test setup + +Additionnaly this `docker-compose` template allow to run locally `VaultWarden`, [Keycloak](https://www.keycloak.org/) and [Maildev](https://github.com/timshel/maildev) to test OIDC. + +## Setup + +This rely on `docker` and the `compose` [plugin](https://docs.docker.com/compose/install/). +First create a copy of `.env.template` as `.env` (This is done to prevent commiting your custom settings, Ex `SMTP_`). + +## Usage + +Then start the stack (the `profile` is required to run `Vaultwarden`) : + +```bash +> docker compose --profile vaultwarden --env-file .env up +.... +keycloakSetup_1 | Logging into http://127.0.0.1:8080 as user admin of realm master +keycloakSetup_1 | Created new realm with id 'test' +keycloakSetup_1 | 74af4933-e386-4e64-ba15-a7b61212c45e +oidc_keycloakSetup_1 exited with code 0 +``` + +Wait until `oidc_keycloakSetup_1 exited with code 0` which indicate the correct setup of the Keycloak realm, client and user (It's normal for this container to stop once the configuration is done). + +Then you can access : + +- `VaultWarden` on http://0.0.0.0:8000 with the default user `test@yopmail.com/test`. +- `Keycloak` on http://0.0.0.0:8080/admin/master/console/ with the default user `admin/admin` +- `Maildev` on http://0.0.0.0:1080 + +To proceed with an SSO login after you enter the email, on the screen prompting for `Master Password` the SSO button should be visible. +To use your computer external ip (for example when testing with a phone) you will have to configure `KC_HTTP_HOST` and `DOMAIN`. + +## Running only Keycloak + +You can run just `Keycloak` with `--profile keycloak`: + +```bash +> docker compose --profile keycloak --env-file .env up +``` + +When running with a local VaultWarden and the default `web-vault` you'll need to make the SSO button visible using : + +```bash +sed -i 's#a\[routerlink="/sso"\],##' web-vault/app/main.*.css +``` + +Otherwise you'll need to reveal the SSO login button using the debug console (F12) + + ```js + document.querySelector('a[routerlink="/sso"]').style.setProperty("display", "inline-block", "important"); + ``` + +## Rebuilding the Vaultwarden + +To force rebuilding the Vaultwarden image you can run + +```bash +docker compose --profile vaultwarden --env-file .env build VaultwardenPrebuild Vaultwarden +``` + +## Configuration + +All configuration for `keycloak` / `VaultWarden` / `keycloak_setup.sh` can be found in [.env](.env.template). +The content of the file will be loaded as environment variables in all containers. + +- `keycloak` [configuration](https://www.keycloak.org/server/all-config) include `KEYCLOAK_ADMIN` / `KEYCLOAK_ADMIN_PASSWORD` and any variable prefixed `KC_` ([more information](https://www.keycloak.org/server/configuration#_example_configuring_the_db_url_host_parameter)). +- All `VaultWarden` configuration can be set (EX: `SMTP_*`) + +## Cleanup + +Use `docker compose --profile vaultWarden down`. diff --git a/playwright/compose/keycloak/Dockerfile b/playwright/compose/keycloak/Dockerfile new file mode 100644 index 00000000..35888950 --- /dev/null +++ b/playwright/compose/keycloak/Dockerfile @@ -0,0 +1,40 @@ +FROM docker.io/library/debian:bookworm-slim as build + +ENV DEBIAN_FRONTEND=noninteractive +ARG KEYCLOAK_VERSION + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +RUN apt-get update \ + && apt-get install -y ca-certificates curl wget \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR / + +RUN wget -c https://github.com/keycloak/keycloak/releases/download/${KEYCLOAK_VERSION}/keycloak-${KEYCLOAK_VERSION}.tar.gz -O - | tar -xz + +FROM docker.io/library/debian:bookworm-slim + +ENV DEBIAN_FRONTEND=noninteractive +ARG KEYCLOAK_VERSION + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +RUN apt-get update \ + && apt-get install -y ca-certificates curl wget \ + && rm -rf /var/lib/apt/lists/* + +ARG JAVA_URL +ARG JAVA_VERSION + +ENV JAVA_VERSION=${JAVA_VERSION} + +RUN mkdir -p /opt/openjdk && cd /opt/openjdk \ + && wget -c "${JAVA_URL}" -O - | tar -xz + +WORKDIR / + +COPY setup.sh /setup.sh +COPY --from=build /keycloak-${KEYCLOAK_VERSION}/bin /opt/keycloak/bin + +CMD "/setup.sh" diff --git a/playwright/compose/keycloak/setup.sh b/playwright/compose/keycloak/setup.sh new file mode 100755 index 00000000..36597b1d --- /dev/null +++ b/playwright/compose/keycloak/setup.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +export PATH=/opt/keycloak/bin:/opt/openjdk/jdk-${JAVA_VERSION}/bin:$PATH +export JAVA_HOME=/opt/openjdk/jdk-${JAVA_VERSION} + +STATUS_CODE=0 +while [[ "$STATUS_CODE" != "404" ]] ; do + echo "Will retry in 2 seconds" + sleep 2 + + STATUS_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$DUMMY_AUTHORITY") + + if [[ "$STATUS_CODE" = "200" ]]; then + echo "Setup should already be done. Will not run." + exit 0 + fi +done + +set -e + +kcadm.sh config credentials --server "http://${KC_HTTP_HOST}:${KC_HTTP_PORT}" --realm master --user "$KEYCLOAK_ADMIN" --password "$KEYCLOAK_ADMIN_PASSWORD" --client admin-cli + +kcadm.sh create realms -s realm="$TEST_REALM" -s enabled=true -s "accessTokenLifespan=600" +kcadm.sh create clients -r test -s "clientId=$SSO_CLIENT_ID" -s "secret=$SSO_CLIENT_SECRET" -s "redirectUris=[\"$DOMAIN/*\"]" -i + +TEST_USER_ID=$(kcadm.sh create users -r "$TEST_REALM" -s "username=$TEST_USER" -s "firstName=$TEST_USER" -s "lastName=$TEST_USER" -s "email=$TEST_USER_MAIL" -s emailVerified=true -s enabled=true -i) +kcadm.sh update users/$TEST_USER_ID/reset-password -r "$TEST_REALM" -s type=password -s "value=$TEST_USER_PASSWORD" -n + +TEST_USER2_ID=$(kcadm.sh create users -r "$TEST_REALM" -s "username=$TEST_USER2" -s "firstName=$TEST_USER2" -s "lastName=$TEST_USER2" -s "email=$TEST_USER2_MAIL" -s emailVerified=true -s enabled=true -i) +kcadm.sh update users/$TEST_USER2_ID/reset-password -r "$TEST_REALM" -s type=password -s "value=$TEST_USER2_PASSWORD" -n + +TEST_USER3_ID=$(kcadm.sh create users -r "$TEST_REALM" -s "username=$TEST_USER3" -s "firstName=$TEST_USER3" -s "lastName=$TEST_USER3" -s "email=$TEST_USER3_MAIL" -s emailVerified=true -s enabled=true -i) +kcadm.sh update users/$TEST_USER3_ID/reset-password -r "$TEST_REALM" -s type=password -s "value=$TEST_USER3_PASSWORD" -n + +# Dummy realm to mark end of setup +kcadm.sh create realms -s realm="$DUMMY_REALM" -s enabled=true -s "accessTokenLifespan=600" diff --git a/playwright/compose/playwright/Dockerfile b/playwright/compose/playwright/Dockerfile new file mode 100644 index 00000000..1a4b1ddb --- /dev/null +++ b/playwright/compose/playwright/Dockerfile @@ -0,0 +1,40 @@ +FROM docker.io/library/debian:bookworm-slim + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update \ + && apt-get install -y ca-certificates curl \ + && curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc \ + && chmod a+r /etc/apt/keyrings/docker.asc \ + && echo "deb [signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian bookworm stable" | tee /etc/apt/sources.list.d/docker.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + containerd.io \ + docker-buildx-plugin \ + docker-ce \ + docker-ce-cli \ + docker-compose-plugin \ + git \ + libmariadb-dev-compat \ + libpq5 \ + nodejs \ + npm \ + openssl \ + && rm -rf /var/lib/apt/lists/* + +RUN mkdir /playwright +WORKDIR /playwright + +COPY package.json . +RUN npm install && npx playwright install-deps && npx playwright install firefox + +COPY docker-compose.yml test.env ./ +COPY compose ./compose + +COPY *.ts test.env ./ +COPY tests ./tests + +ENTRYPOINT ["/usr/bin/npx", "playwright"] +CMD ["test"] diff --git a/playwright/compose/vaultwarden/Dockerfile b/playwright/compose/vaultwarden/Dockerfile new file mode 100644 index 00000000..4606ae36 --- /dev/null +++ b/playwright/compose/vaultwarden/Dockerfile @@ -0,0 +1,39 @@ +FROM playwright_oidc_vaultwarden_prebuilt AS vaultwarden + +FROM node:18-bookworm AS build + +arg REPO_URL +arg COMMIT_HASH + +ENV REPO_URL=$REPO_URL +ENV COMMIT_HASH=$COMMIT_HASH + +COPY --from=vaultwarden /web-vault /web-vault +COPY build.sh /build.sh +RUN /build.sh + +######################## RUNTIME IMAGE ######################## +FROM docker.io/library/debian:bookworm-slim + +ENV DEBIAN_FRONTEND=noninteractive + +# Create data folder and Install needed libraries +RUN mkdir /data && \ + apt-get update && apt-get install -y \ + --no-install-recommends \ + ca-certificates \ + curl \ + libmariadb-dev-compat \ + libpq5 \ + openssl && \ + rm -rf /var/lib/apt/lists/* + +# Copies the files from the context (Rocket.toml file and web-vault) +# and the binary from the "build" stage to the current stage +WORKDIR / + +COPY --from=vaultwarden /start.sh . +COPY --from=vaultwarden /vaultwarden . +COPY --from=build /web-vault ./web-vault + +ENTRYPOINT ["/start.sh"] diff --git a/playwright/compose/vaultwarden/build.sh b/playwright/compose/vaultwarden/build.sh new file mode 100755 index 00000000..da354112 --- /dev/null +++ b/playwright/compose/vaultwarden/build.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +echo $REPO_URL +echo $COMMIT_HASH + +if [[ ! -z "$REPO_URL" ]] && [[ ! -z "$COMMIT_HASH" ]] ; then + rm -rf /web-vault + + mkdir bw_web_builds; + cd bw_web_builds; + + git -c init.defaultBranch=main init + git remote add origin "$REPO_URL" + git fetch --depth 1 origin "$COMMIT_HASH" + git -c advice.detachedHead=false checkout FETCH_HEAD + + export VAULT_VERSION=$(cat Dockerfile | grep "ARG VAULT_VERSION" | cut -d "=" -f2) + ./scripts/checkout_web_vault.sh + ./scripts/patch_web_vault.sh + ./scripts/build_web_vault.sh + printf '{"version":"%s"}' "$COMMIT_HASH" > ./web-vault/apps/web/build/vw-version.json + + mv ./web-vault/apps/web/build /web-vault +fi diff --git a/playwright/docker-compose.yml b/playwright/docker-compose.yml new file mode 100644 index 00000000..ea2ab5e9 --- /dev/null +++ b/playwright/docker-compose.yml @@ -0,0 +1,121 @@ +services: + VaultwardenPrebuild: + profiles: ["playwright", "vaultwarden"] + container_name: playwright_oidc_vaultwarden_prebuilt + image: playwright_oidc_vaultwarden_prebuilt + build: + context: .. + dockerfile: Dockerfile + entrypoint: /bin/bash + restart: "no" + + Vaultwarden: + profiles: ["playwright", "vaultwarden"] + container_name: playwright_oidc_vaultwarden-${ENV:-dev} + image: playwright_oidc_vaultwarden-${ENV:-dev} + network_mode: "host" + build: + context: compose/vaultwarden + dockerfile: Dockerfile + args: + REPO_URL: ${PW_WV_REPO_URL:-} + COMMIT_HASH: ${PW_WV_COMMIT_HASH:-} + env_file: ${DC_ENV_FILE:-.env} + environment: + - DATABASE_URL + - I_REALLY_WANT_VOLATILE_STORAGE + - SMTP_HOST + - SMTP_FROM + - SMTP_DEBUG + - SSO_FRONTEND + - SSO_ENABLED + - SSO_ONLY + restart: "no" + depends_on: + - VaultwardenPrebuild + + Playwright: + profiles: ["playwright"] + container_name: playwright_oidc_playwright + image: playwright_oidc_playwright + network_mode: "host" + build: + context: . + dockerfile: compose/playwright/Dockerfile + environment: + - PW_WV_REPO_URL + - PW_WV_COMMIT_HASH + restart: "no" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ..:/project + + Mariadb: + profiles: ["playwright"] + container_name: playwright_mariadb + image: mariadb:11.2.4 + env_file: test.env + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + start_period: 10s + interval: 10s + ports: + - ${MARIADB_PORT}:3306 + + Mysql: + profiles: ["playwright"] + container_name: playwright_mysql + image: mysql:8.4.1 + env_file: test.env + healthcheck: + test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] + start_period: 10s + interval: 10s + ports: + - ${MYSQL_PORT}:3306 + + Postgres: + profiles: ["playwright"] + container_name: playwright_postgres + image: postgres:16.3 + env_file: test.env + healthcheck: + test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] + start_period: 20s + interval: 30s + ports: + - ${POSTGRES_PORT}:5432 + + Maildev: + profiles: ["vaultwarden", "maildev"] + container_name: maildev + image: timshel/maildev + ports: + - ${SMTP_PORT}:1025 + - 1080:1080 + + Keycloak: + profiles: ["keycloak", "vaultwarden"] + container_name: keycloak-${ENV:-dev} + image: quay.io/keycloak/keycloak:25.0.4 + network_mode: "host" + command: + - start-dev + env_file: ${DC_ENV_FILE:-.env} + + KeycloakSetup: + profiles: ["keycloak", "vaultwarden"] + container_name: keycloakSetup-${ENV:-dev} + image: keycloak_setup-${ENV:-dev} + build: + context: compose/keycloak + dockerfile: Dockerfile + args: + KEYCLOAK_VERSION: 25.0.4 + JAVA_URL: https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-x64_bin.tar.gz + JAVA_VERSION: 21.0.2 + network_mode: "host" + depends_on: + - Keycloak + restart: "no" + env_file: ${DC_ENV_FILE:-.env} diff --git a/playwright/global-setup.ts b/playwright/global-setup.ts new file mode 100644 index 00000000..89405f12 --- /dev/null +++ b/playwright/global-setup.ts @@ -0,0 +1,22 @@ +import { firefox, type FullConfig } from '@playwright/test'; +import { execSync } from 'node:child_process'; +import fs from 'fs'; + +const utils = require('./global-utils'); + +utils.loadEnv(); + +async function globalSetup(config: FullConfig) { + // Are we running in docker and the project is mounted ? + const path = (fs.existsSync("/project/playwright/playwright.config.ts") ? "/project/playwright" : "."); + execSync(`docker compose --project-directory ${path} --profile playwright --env-file test.env build VaultwardenPrebuild`, { + env: { ...process.env }, + stdio: "inherit" + }); + execSync(`docker compose --project-directory ${path} --profile playwright --env-file test.env build Vaultwarden`, { + env: { ...process.env }, + stdio: "inherit" + }); +} + +export default globalSetup; diff --git a/playwright/global-utils.ts b/playwright/global-utils.ts new file mode 100644 index 00000000..724b0877 --- /dev/null +++ b/playwright/global-utils.ts @@ -0,0 +1,219 @@ +import { type Browser, type TestInfo } from '@playwright/test'; +import { EventEmitter } from "events"; +import { type Mail, MailServer } from 'maildev'; +import { execSync } from 'node:child_process'; + +import dotenv from 'dotenv'; +import dotenvExpand from 'dotenv-expand'; + +const fs = require("fs"); +const { spawn } = require('node:child_process'); + +export function loadEnv(){ + var myEnv = dotenv.config({ path: 'test.env' }); + dotenvExpand.expand(myEnv); + + return { + user1: { + email: process.env.TEST_USER_MAIL, + name: process.env.TEST_USER, + password: process.env.TEST_USER_PASSWORD, + }, + user2: { + email: process.env.TEST_USER2_MAIL, + name: process.env.TEST_USER2, + password: process.env.TEST_USER2_PASSWORD, + }, + user3: { + email: process.env.TEST_USER3_MAIL, + name: process.env.TEST_USER3, + password: process.env.TEST_USER3_PASSWORD, + }, + } +} + +export function closeMails(mailServer: MailServer, mailIterators: AsyncIterator[]) { + if( mailServer ) { + mailServer.close(); + } + if( mailIterators ) { + for (const mails of mailIterators) { + if(mails){ + mails.return(); + } + } + } +} + +export 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("CONNECTION_REFUSED") ){ + throw e; + } + } finally { + await context.close(); + } + } while(!ready); +} + +export function startComposeService(serviceName: String){ + console.log(`Starting ${serviceName}`); + execSync(`docker compose --profile playwright --env-file test.env up -d ${serviceName}`); +} + +export function stopComposeService(serviceName: String){ + console.log(`Stopping ${serviceName}`); + execSync(`docker compose --profile playwright --env-file test.env stop ${serviceName}`); +} + +function wipeSqlite(){ + console.log(`Delete Vaultwarden container to wipe sqlite`); + execSync(`docker compose --env-file test.env stop Vaultwarden`); + execSync(`docker compose --env-file test.env rm -f Vaultwarden`); +} + +async function wipeMariaDB(){ + var mysql = require('mysql2/promise'); + var ready = false; + var connection; + + do { + try { + connection = await mysql.createConnection({ + user: process.env.MARIADB_USER, + host: "127.0.0.1", + database: process.env.MARIADB_DATABASE, + password: process.env.MARIADB_PASSWORD, + port: process.env.MARIADB_PORT, + }); + + await connection.execute(`DROP DATABASE ${process.env.MARIADB_DATABASE}`); + await connection.execute(`CREATE DATABASE ${process.env.MARIADB_DATABASE}`); + console.log('Successfully wiped mariadb'); + ready = true; + } catch (err) { + console.log(`Error when wiping mariadb: ${err}`); + } finally { + if( connection ){ + connection.end(); + } + } + await new Promise(r => setTimeout(r, 1000)); + } while(!ready); +} + +async function wipeMysqlDB(){ + var mysql = require('mysql2/promise'); + var ready = false; + var connection; + + do{ + try { + connection = await mysql.createConnection({ + user: process.env.MYSQL_USER, + host: "127.0.0.1", + database: process.env.MYSQL_DATABASE, + password: process.env.MYSQL_PASSWORD, + port: process.env.MYSQL_PORT, + }); + + await connection.execute(`DROP DATABASE ${process.env.MYSQL_DATABASE}`); + await connection.execute(`CREATE DATABASE ${process.env.MYSQL_DATABASE}`); + console.log('Successfully wiped mysql'); + ready = true; + } catch (err) { + console.log(`Error when wiping mysql: ${err}`); + } finally { + if( connection ){ + connection.end(); + } + } + await new Promise(r => setTimeout(r, 1000)); + } while(!ready); +} + +async function wipePostgres(){ + const { Client } = require('pg'); + + const client = new Client({ + user: process.env.POSTGRES_USER, + host: "127.0.0.1", + database: "postgres", + password: process.env.POSTGRES_PASSWORD, + port: process.env.POSTGRES_PORT, + }); + + try { + await client.connect(); + await client.query(`DROP DATABASE ${process.env.POSTGRES_DB}`); + await client.query(`CREATE DATABASE ${process.env.POSTGRES_DB}`); + console.log('Successfully wiped postgres'); + } catch (err) { + console.log(`Error when wiping postgres: ${err}`); + } finally { + client.end(); + } +} + +function dbConfig(testInfo: TestInfo){ + switch(testInfo.project.name) { + case "postgres": return { + DATABASE_URL: `postgresql://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PASSWORD}@127.0.0.1:${process.env.POSTGRES_PORT}/${process.env.POSTGRES_DB}` + } + case "mariadb": return { + DATABASE_URL: `mysql://${process.env.MARIADB_USER}:${process.env.MARIADB_PASSWORD}@127.0.0.1:${process.env.MARIADB_PORT}/${process.env.MARIADB_DATABASE}` + } + case "mysql": return { + DATABASE_URL: `mysql://${process.env.MYSQL_USER}:${process.env.MYSQL_PASSWORD}@127.0.0.1:${process.env.MYSQL_PORT}/${process.env.MYSQL_DATABASE}` + } + default: return { I_REALLY_WANT_VOLATILE_STORAGE: true } + } +} + +/** + * All parameters passed in `env` need to be added to the docker-compose.yml + **/ +export async function startVaultwarden(browser: Browser, testInfo: TestInfo, env = {}, resetDB: Boolean = true) { + if( resetDB ){ + switch(testInfo.project.name) { + case "postgres": + await wipePostgres(); + break; + case "mariadb": + await wipeMariaDB(); + break; + case "mysql": + await wipeMysqlDB(); + break; + default: + wipeSqlite(); + } + } + + console.log(`Starting Vaultwarden`); + execSync(`docker compose --profile playwright --env-file test.env up -d Vaultwarden`, { + env: { ...env, ...dbConfig(testInfo) }, + }); + await waitFor("/", browser); + console.log(`Vaultwarden running on: ${process.env.DOMAIN}`); +} + +export async function stopVaultwarden() { + console.log(`Vaultwarden stopping`); + execSync(`docker compose --profile playwright --env-file test.env stop Vaultwarden`); +} + +export async function restartVaultwarden(page: Page, testInfo: TestInfo, env, resetDB: Boolean = true) { + stopVaultwarden(); + return startVaultwarden(page.context().browser(), testInfo, env, resetDB); +} diff --git a/playwright/package-lock.json b/playwright/package-lock.json new file mode 100644 index 00000000..96a4137b --- /dev/null +++ b/playwright/package-lock.json @@ -0,0 +1,2370 @@ +{ + "name": "scenarios", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "scenarios", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "mysql2": "^3.10.2", + "otpauth": "^9.3.2", + "pg": "^8.12.0" + }, + "devDependencies": { + "@playwright/test": "^1.45.1", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "maildev": "github:timshel/maildev#3.0.0-rc1" + } + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@playwright/test": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.1.tgz", + "integrity": "sha512-Wo1bWTzQvGA7LyKGIZc8nFSTFf2TkthGIFBR+QVNilvwouGzFd4PYukZe3rvf5PSqjHi1+1NyKSDZKcQWETzaA==", + "dev": true, + "dependencies": { + "playwright": "1.45.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", + "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "dev": true, + "dependencies": { + "domhandler": "^5.0.3", + "selderee": "^0.11.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mailparser": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@types/mailparser/-/mailparser-3.4.4.tgz", + "integrity": "sha512-C6Znp2QVS25JqtuPyxj38Qh+QoFcLycdxsvcc6IZCGekhaMBzbdTXzwGzhGoYb3TfKu8IRCNV0sV1o3Od97cEQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "iconv-lite": "^0.6.3" + } + }, + "node_modules/@types/node": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", + "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==", + "dev": true, + "dependencies": { + "undici-types": "~6.13.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/addressparser": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/addressparser/-/addressparser-1.0.1.tgz", + "integrity": "sha512-aQX7AISOMM7HFE0iZ3+YnD07oIeJqWGVnJ+ZIKaBZAk03ftmVYVqsGas/rbXKR21n4D/hKCSHypvcyOkds/xzg==", + "dev": true + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/base32.js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz", + "integrity": "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cssstyle": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz", + "integrity": "sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==", + "dev": true, + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", + "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==", + "dev": true + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.6", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.6.tgz", + "integrity": "sha512-8NHi73otpWsZGBSZwwknTXS5pqMOrk9+Ssrna8xCaxkzEpU9OTf9R5ArQGVw03//Zmk9MOwLPng9WwndvpAJ5g==", + "dev": true, + "dependencies": { + "dotenv": "^16.4.4" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding-japanese": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.1.0.tgz", + "integrity": "sha512-58XySVxUgVlBikBTbQ8WdDxBDHIdXucB16LO5PBHR8t75D54wQrNo4cg+58+R1CtJfKnsVsvt9XlteRaR8xw1w==", + "dev": true, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/engine.io": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", + "dev": true, + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-to-text": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", + "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", + "dev": true, + "dependencies": { + "@selderee/plugin-htmlparser2": "^0.11.0", + "deepmerge": "^4.3.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^8.0.2", + "selderee": "^0.11.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/ipv6-normalize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ipv6-normalize/-/ipv6-normalize-1.0.1.tgz", + "integrity": "sha512-Bm6H79i01DjgGTCWjUuCjJ6QDo1HB96PT/xCYuyJUP9WFbVDrLSbG4EZCvOCun2rNswZb0c3e4Jt/ws795esHA==", + "dev": true + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, + "node_modules/jsdom": { + "version": "24.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.1.tgz", + "integrity": "sha512-5O1wWV99Jhq4DV7rCLIoZ/UIhyQeDR7wHVyZAHAshbrvZsLs+Xzz7gtwnlJTJDjleiTKh54F4dXrX70vJQTyJQ==", + "dev": true, + "dependencies": { + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.4", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/leac": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", + "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", + "dev": true, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/libbase64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.3.0.tgz", + "integrity": "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==", + "dev": true + }, + "node_modules/libmime": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.5.tgz", + "integrity": "sha512-nSlR1yRZ43L3cZCiWEw7ali3jY29Hz9CQQ96Oy+sSspYnIP5N54ucOPHqooBsXzwrX1pwn13VUE05q4WmzfaLg==", + "dev": true, + "dependencies": { + "encoding-japanese": "2.1.0", + "iconv-lite": "0.6.3", + "libbase64": "1.3.0", + "libqp": "2.1.0" + } + }, + "node_modules/libqp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/libqp/-/libqp-2.1.0.tgz", + "integrity": "sha512-O6O6/fsG5jiUVbvdgT7YX3xY3uIadR6wEZ7+vy9u7PKHAlSEB6blvC1o5pHBjgsi95Uo0aiBBdkyFecj6jtb7A==", + "dev": true + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "node_modules/lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", + "engines": { + "node": ">=16.14" + } + }, + "node_modules/maildev": { + "version": "3.0.0-rc0", + "resolved": "git+ssh://git@github.com/timshel/maildev.git#b18050e3b980886f093a068a7453446610fdf12a", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mailparser": "^3.4.4", + "addressparser": "1.0.1", + "async": "^3.2.3", + "commander": "^12.1.0", + "compression": "^1.7.4", + "cors": "^2.8.5", + "dompurify": "^3.1.6", + "express": "^4.19.2", + "jsdom": "^24.1.1", + "mailparser": "^3.7.1", + "mime": "1.6.0", + "nodemailer": "^6.9.14", + "smtp-server": "^3.13.4", + "socket.io": "^4.7.5", + "wildstring": "1.0.9" + }, + "bin": { + "maildev": "bin/maildev" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/mailparser": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.7.1.tgz", + "integrity": "sha512-RCnBhy5q8XtB3mXzxcAfT1huNqN93HTYYyL6XawlIKycfxM/rXPg9tXoZ7D46+SgCS1zxKzw+BayDQSvncSTTw==", + "dev": true, + "dependencies": { + "encoding-japanese": "2.1.0", + "he": "1.2.0", + "html-to-text": "9.0.5", + "iconv-lite": "0.6.3", + "libmime": "5.3.5", + "linkify-it": "5.0.0", + "mailsplit": "5.4.0", + "nodemailer": "6.9.13", + "punycode.js": "2.3.1", + "tlds": "1.252.0" + } + }, + "node_modules/mailparser/node_modules/nodemailer": { + "version": "6.9.13", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.13.tgz", + "integrity": "sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/mailsplit": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-5.4.0.tgz", + "integrity": "sha512-wnYxX5D5qymGIPYLwnp6h8n1+6P6vz/MJn5AzGjZ8pwICWssL+CCQjWBIToOVHASmATot4ktvlLo6CyLfOXWYA==", + "dev": true, + "dependencies": { + "libbase64": "1.2.1", + "libmime": "5.2.0", + "libqp": "2.0.1" + } + }, + "node_modules/mailsplit/node_modules/encoding-japanese": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.0.0.tgz", + "integrity": "sha512-++P0RhebUC8MJAwJOsT93dT+5oc5oPImp1HubZpAuCZ5kTLnhuuBhKHj2jJeO/Gj93idPBWmIuQ9QWMe5rX3pQ==", + "dev": true, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/mailsplit/node_modules/libbase64": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.2.1.tgz", + "integrity": "sha512-l+nePcPbIG1fNlqMzrh68MLkX/gTxk/+vdvAb388Ssi7UuUN31MI44w4Yf33mM3Cm4xDfw48mdf3rkdHszLNew==", + "dev": true + }, + "node_modules/mailsplit/node_modules/libmime": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.2.0.tgz", + "integrity": "sha512-X2U5Wx0YmK0rXFbk67ASMeqYIkZ6E5vY7pNWRKtnNzqjvdYYG8xtPDpCnuUEnPU9vlgNev+JoSrcaKSUaNvfsw==", + "dev": true, + "dependencies": { + "encoding-japanese": "2.0.0", + "iconv-lite": "0.6.3", + "libbase64": "1.2.1", + "libqp": "2.0.1" + } + }, + "node_modules/mailsplit/node_modules/libqp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/libqp/-/libqp-2.0.1.tgz", + "integrity": "sha512-Ka0eC5LkF3IPNQHJmYBWljJsw0UvM6j+QdKRbWyCdTmYwvIDE6a7bCm0UkTAL/K+3KXK5qXT/ClcInU01OpdLg==", + "dev": true + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", + "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/mysql2": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.10.2.tgz", + "integrity": "sha512-KCXPEvAkO0RcHPr362O5N8tFY2fXvbjfkPvRY/wGumh4EOemo9Hm5FjQZqv/pCmrnuxGu5OxnSENG0gTXqKMgQ==", + "dependencies": { + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru-cache": "^8.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemailer": { + "version": "6.9.14", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.14.tgz", + "integrity": "sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nwsapi": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz", + "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==", + "dev": true + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/otpauth": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.3.2.tgz", + "integrity": "sha512-KixtXWN9RGdS8WHPfDo7qsOYiivCbl+VeLBT+7HBTtJebBO6aXr/bpZXr+TwY2COecdY82VeBghm31mLYQVZlQ==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://github.com/hectorm/otpauth?sponsor=1" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseley": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", + "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", + "dev": true, + "dependencies": { + "leac": "^0.6.0", + "peberminta": "^0.9.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true + }, + "node_modules/peberminta": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", + "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", + "dev": true, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/pg": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", + "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==", + "dependencies": { + "pg-connection-string": "^2.6.4", + "pg-pool": "^3.6.2", + "pg-protocol": "^1.6.1", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", + "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz", + "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz", + "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/playwright": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.1.tgz", + "integrity": "sha512-Hjrgae4kpSQBr98nhCj3IScxVeVUixqj+5oyif8TdIn2opTCPEzqAqNMeK42i3cWDCVu9MI+ZsGWw+gVR4ISBg==", + "dev": true, + "dependencies": { + "playwright-core": "1.45.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.1.tgz", + "integrity": "sha512-LF4CUUtrUu2TCpDw4mcrAIuYrEjVDfT1cHbJMfwnE2+1b8PZcFzPNgvZCvq2JfQ4aTjRCCHw5EJ2tmr2NSzdPg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/selderee": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", + "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", + "dev": true, + "dependencies": { + "parseley": "^0.12.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/smtp-server": { + "version": "3.13.4", + "resolved": "https://registry.npmjs.org/smtp-server/-/smtp-server-3.13.4.tgz", + "integrity": "sha512-BbElv5UP+HgPtCZtcRW35N/GFoc4DzPkrbSMLioXsrVMmQT1mMBoO0k+egl264hxWaWczoVvadSPY2pLUINFXg==", + "dev": true, + "dependencies": { + "base32.js": "0.1.0", + "ipv6-normalize": "1.0.1", + "nodemailer": "6.9.13", + "punycode": "2.3.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/smtp-server/node_modules/nodemailer": { + "version": "6.9.13", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.13.tgz", + "integrity": "sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dev": true, + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "node_modules/tlds": { + "version": "1.252.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.252.0.tgz", + "integrity": "sha512-GA16+8HXvqtfEnw/DTcwB0UU354QE1n3+wh08oFjr6Znl7ZLAeUgYzCcK+/CCrOyE0vnHR8/pu3XXG3vDijXpQ==", + "dev": true, + "bin": { + "tlds": "bin.js" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true + }, + "node_modules/undici-types": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", + "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==", + "dev": true + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dev": true, + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/wildstring": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/wildstring/-/wildstring-1.0.9.tgz", + "integrity": "sha512-XBNxKIMLO6uVHf1Xvo++HGWAZZoiVCHmEMCmZJzJ82vQsuUJCLw13Gzq0mRCATk7a3+ZcgeOKSDioavuYqtlfA==", + "dev": true + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/playwright/package.json b/playwright/package.json new file mode 100644 index 00000000..e1b3a884 --- /dev/null +++ b/playwright/package.json @@ -0,0 +1,21 @@ +{ + "name": "scenarios", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.45.1", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "maildev": "github:timshel/maildev#3.0.0-rc1" + }, + "dependencies": { + "mysql2": "^3.10.2", + "otpauth": "^9.3.2", + "pg": "^8.12.0" + } +} diff --git a/playwright/playwright.config.ts b/playwright/playwright.config.ts new file mode 100644 index 00000000..ea3f35c7 --- /dev/null +++ b/playwright/playwright.config.ts @@ -0,0 +1,132 @@ +import { defineConfig, devices } from '@playwright/test'; +import { exec } from 'node:child_process'; + +const utils = require('./global-utils'); + +utils.loadEnv(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './.', + /* 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: 20 * 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, + browserName: 'firefox', + locale: 'en-GB', + timezoneId: 'Europe/London', + /* 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: 'mariadb-setup', + testMatch: 'tests/setups/db-setup.ts', + use: { serviceName: "Mariadb" }, + teardown: 'mariadb-teardown', + }, + { + name: 'mysql-setup', + testMatch: 'tests/setups/db-setup.ts', + use: { serviceName: "Mysql" }, + teardown: 'mysql-teardown', + }, + { + name: 'postgres-setup', + testMatch: 'tests/setups/db-setup.ts', + use: { serviceName: "Postgres" }, + teardown: 'postgres-teardown', + }, + { + name: 'sso-setup', + testMatch: 'tests/setups/sso-setup.ts', + teardown: 'sso-teardown', + }, + + { + name: 'mariadb', + testMatch: 'tests/*.spec.ts', + testIgnore: 'tests/sso_*.spec.ts', + dependencies: ['mariadb-setup'], + }, + { + name: 'mysql', + testMatch: 'tests/*.spec.ts', + testIgnore: 'tests/sso_*.spec.ts', + dependencies: ['mysql-setup'], + }, + { + name: 'postgres', + testMatch: 'tests/*.spec.ts', + testIgnore: 'tests/sso_*.spec.ts', + dependencies: ['postgres-setup'], + }, + { + name: 'sqlite', + testMatch: 'tests/*.spec.ts', + testIgnore: 'tests/sso_*.spec.ts', + }, + + { + name: 'sso-mariadb', + testMatch: 'tests/sso_*.spec.ts', + dependencies: ['sso-setup', 'mariadb-setup'], + }, + { + name: 'sso-mysql', + testMatch: 'tests/sso_*.spec.ts', + dependencies: ['sso-setup', 'mysql-setup'], + }, + { + name: 'sso-postgres', + testMatch: 'tests/sso_*.spec.ts', + dependencies: ['sso-setup', 'postgres-setup'], + }, + { + name: 'sso-sqlite', + testMatch: 'tests/sso_*.spec.ts', + dependencies: ['sso-setup'], + }, + + { + name: 'mariadb-teardown', + testMatch: 'tests/setups/db-teardown.ts', + use: { serviceName: "Mariadb" }, + }, + { + name: 'mysql-teardown', + testMatch: 'tests/setups/db-teardown.ts', + use: { serviceName: "Mysql" }, + }, + { + name: 'postgres-teardown', + testMatch: 'tests/setups/db-teardown.ts', + use: { serviceName: "Postgres" }, + }, + { + name: 'sso-teardown', + testMatch: 'tests/setups/sso-teardown.ts', + }, + ], + + globalSetup: require.resolve('./global-setup'), +}); diff --git a/playwright/test.env b/playwright/test.env new file mode 100644 index 00000000..c67aa82e --- /dev/null +++ b/playwright/test.env @@ -0,0 +1,90 @@ +################################################################## +### Shared Playwright conf test file Vaultwarden and Databases ### +################################################################## + +ENV=test +DC_ENV_FILE=test.env +COMPOSE_IGNORE_ORPHANS=True +DOCKER_BUILDKIT=1 + +##################### +# Playwright Config # +##################### +PW_KEEP_SERVICE_RUNNNING=${PW_KEEP_SERVICE_RUNNNING:-false} +VAULTWARDEN_SMTP_FROM=vaultwarden@playwright.test + +##################### +# Maildev Config # +##################### +MAILDEV_HTTP_PORT=1081 +MAILDEV_SMTP_PORT=1026 +MAILDEV_HOST=127.0.0.1 + +################ +# Users Config # +################ +TEST_USER=test +TEST_USER_PASSWORD=Master Password +TEST_USER_MAIL=${TEST_USER}@example.com + +TEST_USER2=test2 +TEST_USER2_PASSWORD=Master Password +TEST_USER2_MAIL=${TEST_USER2}@example.com + +TEST_USER3=test3 +TEST_USER3_PASSWORD=Master Password +TEST_USER3_MAIL=${TEST_USER3}@example.com + +################### +# Keycloak Config # +################### +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN} +KC_HTTP_HOST=127.0.0.1 +KC_HTTP_PORT=8081 + +# Script parameters (use Keycloak and VaultWarden config too) +TEST_REALM=test +DUMMY_REALM=dummy +DUMMY_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${DUMMY_REALM} + +###################### +# Vaultwarden Config # +###################### +ROCKET_PORT=8003 +DOMAIN=http://127.0.0.1:${ROCKET_PORT} +SMTP_SECURITY=off +SMTP_PORT=${MAILDEV_SMTP_PORT} +SMTP_FROM_NAME=Vaultwarden +SMTP_TIMEOUT=5 + +SSO_CLIENT_ID=VaultWarden +SSO_CLIENT_SECRET=VaultWarden +SSO_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${TEST_REALM} +SSO_PKCE=true + +########################### +# Docker MariaDb container# +########################### +MARIADB_PORT=3307 +MARIADB_ROOT_PASSWORD=vaultwarden +MARIADB_USER=vaultwarden +MARIADB_PASSWORD=vaultwarden +MARIADB_DATABASE=vaultwarden + +########################### +# Docker Mysql container# +########################### +MYSQL_PORT=3309 +MYSQL_ROOT_PASSWORD=vaultwarden +MYSQL_USER=vaultwarden +MYSQL_PASSWORD=vaultwarden +MYSQL_DATABASE=vaultwarden + +############################ +# Docker Postgres container# +############################ +POSTGRES_PORT=5433 +POSTGRES_USER=vaultwarden +POSTGRES_PASSWORD=vaultwarden +POSTGRES_DB=vaultwarden diff --git a/playwright/tests/login.smtp.spec.ts b/playwright/tests/login.smtp.spec.ts new file mode 100644 index 00000000..010b8095 --- /dev/null +++ b/playwright/tests/login.smtp.spec.ts @@ -0,0 +1,159 @@ +import { test, expect, type TestInfo } from '@playwright/test'; +import { MailDev } from 'maildev'; + +const utils = require('../global-utils'); +import { createAccount, logUser } from './setups/user'; + +let users = utils.loadEnv(); + +let mailserver; + +test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => { + mailserver = new MailDev({ + port: process.env.MAILDEV_SMTP_PORT, + web: { port: process.env.MAILDEV_HTTP_PORT }, + }) + + await mailserver.listen(); + + await utils.startVaultwarden(browser, testInfo, { + SMTP_HOST: process.env.MAILDEV_HOST, + SMTP_FROM: process.env.VAULTWARDEN_SMTP_FROM, + }); +}); + +test.afterAll('Teardown', async ({}) => { + utils.stopVaultwarden(); + if( mailserver ){ + await mailserver.close(); + } +}); + +test('Account creation', async ({ page }) => { + const emails = mailserver.iterator(users.user1.email); + + await createAccount(test, page, users.user1); + + const { value: created } = await emails.next(); + expect(created.subject).toBe("Welcome"); + expect(created.from[0]?.address).toBe(process.env.VAULTWARDEN_SMTP_FROM); + + // Back to the login page + await expect(page).toHaveTitle('Vaultwarden Web'); + await expect(page.getByTestId("toast-message")).toHaveText(/Your new account has been created/); + await page.getByRole('button', { name: 'Continue' }).click(); + + // Unlock page + await page.getByLabel('Master password').fill(users.user1.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/); + + const { value: logged } = await emails.next(); + expect(logged.subject).toBe("New Device Logged In From Firefox"); + expect(logged.to[0]?.address).toBe(process.env.TEST_USER_MAIL); + expect(logged.from[0]?.address).toBe(process.env.VAULTWARDEN_SMTP_FROM); + + emails.return(); +}); + +test('Login', async ({ context, page }) => { + const emails = mailserver.iterator(users.user1.email); + + await logUser(test, page, users.user1); + + await test.step('new device email', async () => { + const { value: logged } = await emails.next(); + expect(logged.subject).toBe("New Device Logged In From Firefox"); + expect(logged.from[0]?.address).toBe(process.env.VAULTWARDEN_SMTP_FROM); + }); + + await test.step('verify email', async () => { + await page.getByText('Verify your account\'s email').click(); + await expect(page.getByTestId("toast-message")).toHaveText(/Check your email inbox for a verification link/); + await page.locator('#toast-container').getByRole('button').click(); + await expect(page.getByTestId("toast-message")).toHaveCount(0); + + const { value: verify } = await emails.next(); + expect(verify.subject).toBe("Verify Your Email"); + expect(verify.from[0]?.address).toBe(process.env.VAULTWARDEN_SMTP_FROM); + + const page2 = await context.newPage(); + await page2.setContent(verify.html); + const link = await page2.getByTestId("verify").getAttribute("href"); + await page2.close(); + + await page.goto(link); + await expect(page.getByTestId("toast-message")).toHaveText("Account email verified"); + }); + + emails.return(); +}); + +test('Activaite 2fa', async ({ context, page }) => { + const emails = mailserver.buffer(users.user1.email); + + await logUser(test, page, users.user1); + + await page.getByRole('button', { name: users.user1.name }).click(); + await page.getByRole('menuitem', { name: 'Account settings' }).click(); + await page.getByLabel('Security').click(); + await page.getByRole('link', { name: 'Two-step login' }).click(); + await page.locator('li').filter({ hasText: 'Email Verification codes will' }).getByRole('button').click(); + await page.getByLabel('Master password (required)').fill(users.user1.password); + await page.getByRole('button', { name: 'Continue' }).click(); + await page.getByRole('button', { name: 'Send email' }).click(); + + const codeMail = await emails.next((mail) => mail.subject === "Vaultwarden Login Verification Code"); + const page2 = await context.newPage(); + await page2.setContent(codeMail.html); + const code = await page2.getByTestId("2fa").innerText(); + await page2.close(); + + await page.getByLabel('2. Enter the resulting 6').fill(code); + await page.getByRole('button', { name: 'Turn on' }).click(); + await page.getByRole('heading', { name: 'Turned on', exact: true }); + + emails.close(); +}); + +test('2fa', async ({ context, page }) => { + const emails = mailserver.buffer(users.user1.email); + + await test.step('login', async () => { + await page.goto('/'); + + await page.getByLabel(/Email address/).fill(users.user1.email); + await page.getByRole('button', { name: 'Continue' }).click(); + await page.getByLabel('Master password').fill(users.user1.password); + await page.getByRole('button', { name: 'Log in with master password' }).click(); + + const codeMail = await emails.next((mail) => mail.subject === "Vaultwarden Login Verification Code"); + const page2 = await context.newPage(); + await page2.setContent(codeMail.html); + const code = await page2.getByTestId("2fa").innerText(); + await page2.close(); + + await page.getByLabel('Verification code').fill(code); + await page.getByRole('button', { name: 'Continue' }).click(); + + await expect(page).toHaveTitle(/Vaults/); + }) + + await test.step('disable', async () => { + await page.getByRole('button', { name: 'Test' }).click(); + await page.getByRole('menuitem', { name: 'Account settings' }).click(); + await page.getByLabel('Security').click(); + await page.getByRole('link', { name: 'Two-step login' }).click(); + await page.locator('li').filter({ hasText: 'Email Turned on Verification' }).getByRole('button').click(); + await page.getByLabel('Master password (required)').click(); + await page.getByLabel('Master password (required)').fill(users.user1.password); + await page.getByRole('button', { name: 'Continue' }).click(); + await page.getByRole('button', { name: 'Turn off' }).click(); + await page.getByRole('button', { name: 'Yes' }).click(); + await expect(page.getByTestId("toast-message")).toHaveText(/Two-step login provider turned off/); + }); + + emails.close(); +}); diff --git a/playwright/tests/login.spec.ts b/playwright/tests/login.spec.ts new file mode 100644 index 00000000..b8cfbc1a --- /dev/null +++ b/playwright/tests/login.spec.ts @@ -0,0 +1,97 @@ +import { test, expect, type Page, type TestInfo } from '@playwright/test'; +import * as OTPAuth from "otpauth"; + +import * as utils from "../global-utils"; +import { createAccount, logUser } from './setups/user'; + +let users = utils.loadEnv(); +let totp; + +test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => { + await utils.startVaultwarden(browser, testInfo, {}); +}); + +test.afterAll('Teardown', async ({}, testInfo: TestInfo) => { + utils.stopVaultwarden(testInfo); +}); + +test('Account creation', async ({ page }) => { + // Landing page + await createAccount(test, page, users.user1); + + await page.getByRole('button', { name: 'Continue' }).click(); + + // Unlock page + await page.getByLabel('Master password').fill(users.user1.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/); +}); + +test('Master password login', async ({ page }) => { + await logUser(test, page, users.user1); +}); + +test('Authenticator 2fa', async ({ context, page }) => { + let totp; + + await test.step('Login', async () => { + await logUser(test, page, users.user1); + }); + + await test.step('Activate', async () => { + await page.getByRole('button', { name: users.user1.name }).click(); + await page.getByRole('menuitem', { name: 'Account settings' }).click(); + await page.getByLabel('Security').click(); + await page.getByRole('link', { name: 'Two-step login' }).click(); + await page.locator('li').filter({ hasText: 'Authenticator app Use an' }).getByRole('button').click(); + await page.getByLabel('Master password (required)').fill(users.user1.password); + await page.getByRole('button', { name: 'Continue' }).click(); + + const secret = await page.getByLabel('Key').innerText(); + totp = new OTPAuth.TOTP({ secret, period: 30 }); + + await page.getByLabel('3. Enter the resulting 6').fill(totp.generate()); + await page.getByRole('button', { name: 'Turn on' }).click(); + await page.getByRole('heading', { name: 'Turned on', exact: true }); + await page.getByLabel('Close').click(); + }) + + await test.step('logout', async () => { + await page.getByRole('button', { name: users.user1.name }).click(); + await page.getByRole('menuitem', { name: 'Log out' }).click(); + await expect(page.getByTestId("toast-title")).toHaveText("Logged out"); + await page.locator('#toast-container').getByRole('button').click(); + await expect(page.getByTestId("toast-title")).toHaveCount(0); + }); + + await test.step('login', async () => { + let timestamp = Date.now(); // Need to use the next token + timestamp = timestamp + (totp.period - (Math.floor(timestamp / 1000) % totp.period) + 1) * 1000; + + await page.getByLabel(/Email address/).fill(users.user1.email); + await page.getByRole('button', { name: 'Continue' }).click(); + await page.getByLabel('Master password').fill(users.user1.password); + await page.getByRole('button', { name: 'Log in with master password' }).click(); + + await page.getByLabel('Verification code').fill(totp.generate({timestamp})); + await page.getByRole('button', { name: 'Continue' }).click(); + + await expect(page).toHaveTitle(/Vaults/); + }); + + await test.step('disable', async () => { + await page.getByRole('button', { name: 'Test' }).click(); + await page.getByRole('menuitem', { name: 'Account settings' }).click(); + await page.getByLabel('Security').click(); + await page.getByRole('link', { name: 'Two-step login' }).click(); + await page.locator('li').filter({ hasText: /Authenticator app/ }).getByRole('button').click(); + await page.getByLabel('Master password (required)').click(); + await page.getByLabel('Master password (required)').fill(users.user1.password); + await page.getByRole('button', { name: 'Continue' }).click(); + await page.getByRole('button', { name: 'Turn off' }).click(); + await page.getByRole('button', { name: 'Yes' }).click(); + await expect(page.getByTestId("toast-message")).toHaveText(/Two-step login provider turned off/); + }); +}); diff --git a/playwright/tests/organization.spec.ts b/playwright/tests/organization.spec.ts new file mode 100644 index 00000000..035d3bf7 --- /dev/null +++ b/playwright/tests/organization.spec.ts @@ -0,0 +1,159 @@ +import { test, expect, type TestInfo } from '@playwright/test'; +import { MailDev } from 'maildev'; + +import * as utils from "../global-utils"; +import { createAccount, logUser } from './setups/user'; + +let users = utils.loadEnv(); + +let mailserver, user1Mails, user2Mails, user3Mails; + +test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => { + mailserver = new MailDev({ + port: process.env.MAILDEV_SMTP_PORT, + web: { port: process.env.MAILDEV_HTTP_PORT }, + }) + + await mailserver.listen(); + + await utils.startVaultwarden(browser, testInfo, { + SMTP_HOST: process.env.MAILDEV_HOST, + SMTP_FROM: process.env.VAULTWARDEN_SMTP_FROM, + }); + + user1Mails = mailserver.iterator(users.user1.email); + user2Mails = mailserver.iterator(users.user2.email); + user3Mails = mailserver.iterator(users.user3.email); +}); + +test.afterAll('Teardown', async ({}, testInfo: TestInfo) => { + utils.stopVaultwarden(testInfo); + utils.closeMails(mailserver, [user1Mails, user2Mails, user3Mails]); +}); + +test('Create user3', async ({ page }) => { + await createAccount(test, page, users.user3, user3Mails); +}); + +test('Invite users', async ({ page }) => { + await createAccount(test, page, users.user1, user1Mails); + await logUser(test, page, users.user1, user1Mails); + + await test.step('Create Org', async () => { + await page.getByRole('link', { name: 'New organisation' }).click(); + await page.getByLabel('Organisation name (required)').fill('Test'); + await page.getByRole('button', { name: 'Submit' }).click(); + await page.locator('div').filter({ hasText: 'Members' }).nth(2).click(); + }); + + await test.step('Invite user2', async () => { + await page.getByRole('button', { name: 'Invite member' }).click(); + await page.getByLabel('Email (required)').fill(users.user2.email); + await page.getByRole('tab', { name: 'Collections' }).click(); + await page.locator('label').filter({ hasText: 'Grant access to all current' }).click(); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.getByTestId("toast-message")).toHaveText('User(s) invited'); + }); + + await test.step('Invite user3', async () => { + await page.getByRole('button', { name: 'Invite member' }).click(); + await page.getByLabel('Email (required)').fill(users.user3.email); + await page.getByRole('tab', { name: 'Collections' }).click(); + await page.locator('label').filter({ hasText: 'Grant access to all current' }).click(); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.getByTestId("toast-message")).toHaveText('User(s) invited'); + }); +}); + +test('invited with new account', async ({ page }) => { + const { value: invited } = await user2Mails.next(); + expect(invited.subject).toContain("Join Test") + + await test.step('Create account', async () => { + await page.setContent(invited.html); + const link = await page.getByTestId("invite").getAttribute("href"); + await page.goto(link); + await expect(page).toHaveTitle(/Create account | Vaultwarden Web/); + + await page.getByLabel('Name').fill(users.user2.name); + await page.getByLabel('Master password\n (required)', { exact: true }).fill(users.user2.password); + await page.getByLabel('Re-type master password').fill(users.user2.password); + await page.getByRole('button', { name: 'Create account' }).click(); + + // Back to the login page + await expect(page).toHaveTitle('Vaultwarden Web'); + await expect(page.getByTestId("toast-message")).toHaveText(/Your new account has been created/); + + const { value: welcome } = await user2Mails.next(); + expect(welcome.subject).toContain("Welcome") + }); + + await test.step('Login', async () => { + await page.getByLabel(/Email address/).fill(users.user2.email); + await page.getByRole('button', { name: 'Continue' }).click(); + + // Unlock page + await page.getByLabel('Master password').fill(users.user2.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/); + await expect(page.getByTestId("toast-title")).toHaveText("Invitation accepted"); + + const { value: logged } = await user2Mails.next(); + expect(logged.subject).toContain("New Device Logged"); + }); + + const { value: accepted } = await user1Mails.next(); + expect(accepted.subject).toContain("Invitation to Test accepted") +}); + +test('invited with existing account', async ({ page }) => { + const { value: invited } = await user3Mails.next(); + expect(invited.subject).toContain("Join Test") + + await page.setContent(invited.html); + const link = await page.getByTestId("invite").getAttribute("href"); + + await page.goto(link); + + // We should be on login page with email prefilled + await expect(page).toHaveTitle(/Vaultwarden Web/); + await page.getByRole('button', { name: 'Continue' }).click(); + + // Unlock page + await page.getByLabel('Master password').fill(users.user3.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/); + await expect(page.getByTestId("toast-title")).toHaveText("Invitation accepted"); + + const { value: logged } = await user3Mails.next(); + expect(logged.subject).toContain("New Device Logged") + + const { value: accepted } = await user1Mails.next(); + expect(accepted.subject).toContain("Invitation to Test accepted") +}); + +test('Confirm invited user', async ({ page }) => { + await logUser(test, page, users.user1, user1Mails); + await page.getByLabel('Switch products').click(); + await page.getByRole('link', { name: ' Admin Console' }).click(); + await page.getByLabel('Members').click(); + + await test.step('Accept user2', async () => { + await page.getByRole('row', { name: users.user2.name }).getByLabel('Options').click(); + await page.getByRole('menuitem', { name: 'Confirm' }).click(); + await page.getByRole('button', { name: 'Confirm' }).click(); + await expect(page.getByTestId("toast-message")).toHaveText(/confirmed/); + + const { value: logged } = await user2Mails.next(); + expect(logged.subject).toContain("Invitation to Test confirmed"); + }); +}); + +test('Organization is visible', async ({ page }) => { + await logUser(test, page, users.user2, user2Mails); + await page.getByLabel('vault: Test').click(); +}); diff --git a/playwright/tests/setups/db-setup.ts b/playwright/tests/setups/db-setup.ts new file mode 100644 index 00000000..eb37fdc1 --- /dev/null +++ b/playwright/tests/setups/db-setup.ts @@ -0,0 +1,7 @@ +import { test } from './db-test'; + +const utils = require('../../global-utils'); + +test('DB start', async ({ serviceName }) => { + utils.startComposeService(serviceName); +}); diff --git a/playwright/tests/setups/db-teardown.ts b/playwright/tests/setups/db-teardown.ts new file mode 100644 index 00000000..5f753a9d --- /dev/null +++ b/playwright/tests/setups/db-teardown.ts @@ -0,0 +1,11 @@ +import { test } from './db-test'; + +const utils = require('../../global-utils'); + +utils.loadEnv(); + +test('DB teardown ?', async ({ serviceName }) => { + if( process.env.PW_KEEP_SERVICE_RUNNNING !== "true" ) { + utils.stopComposeService(serviceName); + } +}); diff --git a/playwright/tests/setups/db-test.ts b/playwright/tests/setups/db-test.ts new file mode 100644 index 00000000..4a72d37c --- /dev/null +++ b/playwright/tests/setups/db-test.ts @@ -0,0 +1,9 @@ +import { test as base } from '@playwright/test'; + +export type TestOptions = { + serviceName: string; +}; + +export const test = base.extend({ + serviceName: ['', { option: true }], +}); diff --git a/playwright/tests/setups/sso-setup.ts b/playwright/tests/setups/sso-setup.ts new file mode 100644 index 00000000..0d25140e --- /dev/null +++ b/playwright/tests/setups/sso-setup.ts @@ -0,0 +1,19 @@ +import { test, expect, type TestInfo } from '@playwright/test'; + +const { exec } = require('node:child_process'); +const utils = require('../../global-utils'); + +utils.loadEnv(); + +test.beforeAll('Setup', async () => { + console.log("Starting Keycloak"); + exec(`docker compose --profile keycloak --env-file test.env up`); +}); + +test('Keycloak is up', async ({ page }) => { + test.setTimeout(60000); + await utils.waitFor(process.env.SSO_AUTHORITY, page.context().browser()); + // Dummy authority is created at the end of the setup + await utils.waitFor(process.env.DUMMY_AUTHORITY, page.context().browser()); + console.log(`Keycloak running on: ${process.env.SSO_AUTHORITY}`); +}); diff --git a/playwright/tests/setups/sso-teardown.ts b/playwright/tests/setups/sso-teardown.ts new file mode 100644 index 00000000..2899afff --- /dev/null +++ b/playwright/tests/setups/sso-teardown.ts @@ -0,0 +1,15 @@ +import { test, type FullConfig } from '@playwright/test'; + +const { execSync } = require('node:child_process'); +const utils = require('../../global-utils'); + +utils.loadEnv(); + +test('Keycloak teardown', async () => { + if( process.env.PW_KEEP_SERVICE_RUNNNING === "true" ) { + console.log("Keep Keycloak running"); + } else { + console.log("Keycloak stopping"); + execSync(`docker compose --profile keycloak --env-file test.env stop Keycloak`); + } +}); diff --git a/playwright/tests/setups/sso.ts b/playwright/tests/setups/sso.ts new file mode 100644 index 00000000..90c15626 --- /dev/null +++ b/playwright/tests/setups/sso.ts @@ -0,0 +1,82 @@ +import { expect, type Page, Test } from '@playwright/test'; +import { type Mail } from 'maildev'; + +export async function createAccount(test: Test, page: Page, user: { email: string, name: string, password: string }, emails: AsyncIterator) { + await test.step('Create user', async () => { + await test.step('Landing page', async () => { + await page.goto('/'); + await page.getByLabel(/Email address/).fill(user.email); + await page.getByRole('button', { name: 'Continue' }).click(); + }); + + await test.step('SSo start page', async () => { + await page.getByRole('link', { name: /Enterprise single sign-on/ }).click(); + }); + + await test.step('Keycloak login', async () => { + await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible(); + await page.getByLabel(/Username/).fill(user.name); + await page.getByLabel('Password', { exact: true }).fill(user.password); + await page.getByRole('button', { name: 'Sign In' }).click(); + }); + + await test.step('Create Vault account', async () => { + await expect(page.getByText('Set master password')).toBeVisible(); + await page.getByLabel('Master password', { exact: true }).fill(user.password); + await page.getByLabel('Re-type master password').fill(user.password); + await page.getByRole('button', { name: 'Submit' }).click(); + }); + + await test.step('Default vault page', async () => { + await expect(page).toHaveTitle(/Vaults/); + }); + + if( emails ){ + await test.step('Check emails', async () => { + const { value: logged } = await emails.next(); + expect(logged.subject).toContain("New Device Logged"); + + const { value: password } = await emails.next(); + expect(password.subject).toContain("Master Password Has Been Changed"); + }); + } + }); +} + +export async function logUser(test: Test, page: Page, user: { email: string, password: string }, emails: AsyncIterator) { + await test.step('Log user', async () => { + await test.step('Landing page', async () => { + await page.goto('/'); + await page.getByLabel(/Email address/).fill(user.email); + await page.getByRole('button', { name: 'Continue' }).click(); + }); + + await test.step('SSo start page', async () => { + await page.getByRole('link', { name: /Enterprise single sign-on/ }).click(); + }); + + await test.step('Keycloak login', async () => { + await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible(); + await page.getByLabel(/Username/).fill(user.name); + await page.getByLabel('Password', { exact: true }).fill(user.password); + await page.getByRole('button', { name: 'Sign In' }).click(); + }); + + await test.step('Unlock vault', async () => { + await expect(page).toHaveTitle('Vaultwarden Web'); + await page.getByLabel('Master password').fill(user.password); + await page.getByRole('button', { name: 'Unlock' }).click(); + }); + + await test.step('Default vault page', async () => { + await expect(page).toHaveTitle(/Vaults/); + }); + + if( emails ){ + await test.step('Check email', async () => { + const { value: logged } = await emails.next(); + expect(logged.subject).toContain("New Device Logged"); + }); + } + }); +} diff --git a/playwright/tests/setups/user.ts b/playwright/tests/setups/user.ts new file mode 100644 index 00000000..4c17849f --- /dev/null +++ b/playwright/tests/setups/user.ts @@ -0,0 +1,47 @@ +import { expect, type Browser,Page } from '@playwright/test'; + +export async function createAccount(test, page: Page, user: { email: string, name: string, password: string }, emails) { + await test.step('Create user', async () => { + // Landing page + await page.goto('/'); + await page.getByRole('link', { name: 'Create account' }).click(); + + // Back to Vault create account + await expect(page).toHaveTitle(/Create account | Vaultwarden Web/); + await page.getByLabel(/Email address/).fill(user.email); + await page.getByLabel('Name').fill(user.name); + await page.getByLabel('Master password\n (required)', { exact: true }).fill(user.password); + await page.getByLabel('Re-type master password').fill(user.password); + await page.getByRole('button', { name: 'Create account' }).click(); + + // Back to the login page + await expect(page).toHaveTitle('Vaultwarden Web'); + await expect(page.getByTestId("toast-message")).toHaveText(/Your new account has been created/); + + if( emails ){ + const { value: welcome } = await emails.next(); + expect(welcome.subject).toContain("Welcome"); + } + }); +} + +export async function logUser(test, page: Page, user: { email: string, password: string }, emails) { + await test.step('Log user', async () => { + // Landing page + await page.goto('/'); + await page.getByLabel(/Email address/).fill(user.email); + await page.getByRole('button', { name: 'Continue' }).click(); + + // Unlock page + await page.getByLabel('Master password').fill(user.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/); + + if( emails ){ + const { value: logged } = await emails.next(); + expect(logged.subject).toContain("New Device Logged"); + } + }); +} diff --git a/playwright/tests/sso_login.spec.ts b/playwright/tests/sso_login.spec.ts new file mode 100644 index 00000000..14f7ff35 --- /dev/null +++ b/playwright/tests/sso_login.spec.ts @@ -0,0 +1,74 @@ +import { test, expect, type TestInfo } from '@playwright/test'; +import { createAccount, logUser } from './setups/user'; +import * as utils from "../global-utils"; + +let users = utils.loadEnv(); + +test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => { + await utils.startVaultwarden(browser, testInfo, { + SSO_ENABLED: true, + SSO_ONLY: false + }); +}); + +test.afterAll('Teardown', async ({}, testInfo: TestInfo) => { + utils.stopVaultwarden(testInfo); +}); + +test('Account creation using SSO', async ({ page }) => { + // Landing page + await createAccount(test, page, users.user1); +}); + +test('SSO login', async ({ page }) => { + await logUser(test, page, users.user1); +}); + +test('Non SSO login', async ({ page }) => { + // Landing page + await page.goto('/'); + await page.getByLabel(/Email address/).fill(users.user1.email); + await page.getByRole('button', { name: 'Continue' }).click(); + + // Unlock page + await page.getByLabel('Master password').fill(users.user1.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/); +}); + + +test('Non SSO login Failure', async ({ page, browser }, testInfo: TestInfo) => { + await utils.restartVaultwarden(page, testInfo, { + SSO_ENABLED: true, + SSO_ONLY: true + }, false); + + // Landing page + await page.goto('/'); + await page.getByLabel(/Email address/).fill(users.user1.email); + await page.getByRole('button', { name: 'Continue' }).click(); + + // Unlock page + await page.getByLabel('Master password').fill(users.user1.password); + await page.getByRole('button', { name: 'Log in with master password' }).click(); + + // An error should appear + await page.getByLabel('SSO sign-in is required') +}); + +test('No SSO login', async ({ page }, testInfo: TestInfo) => { + await utils.restartVaultwarden(page, testInfo, { + SSO_ENABLED: false + }, false); + + // Landing page + await page.goto('/'); + await page.getByLabel(/Email address/).fill(users.user1.email); + await page.getByRole('button', { name: 'Continue' }).click(); + + // No SSO button + await page.getByLabel('Master password'); + await expect(page.getByRole('link', { name: /Enterprise single sign-on/ })).toHaveCount(0); +}); diff --git a/playwright/tests/sso_organization.spec.ts b/playwright/tests/sso_organization.spec.ts new file mode 100644 index 00000000..720e4d5f --- /dev/null +++ b/playwright/tests/sso_organization.spec.ts @@ -0,0 +1,152 @@ +import { test, expect, type TestInfo } from '@playwright/test'; +import { MailDev } from 'maildev'; + +import * as utils from "../global-utils"; +import { createAccount, logUser } from './setups/sso'; + +let users = utils.loadEnv(); + +let mailserver, user1Mails, user2Mails, user3Mails; + +test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => { + mailserver = new MailDev({ + port: process.env.MAILDEV_SMTP_PORT, + web: { port: process.env.MAILDEV_HTTP_PORT }, + }) + + await mailserver.listen(); + + await utils.startVaultwarden(browser, testInfo, { + SMTP_HOST: process.env.MAILDEV_HOST, + SMTP_FROM: process.env.VAULTWARDEN_SMTP_FROM, + SSO_ENABLED: true, + SSO_ONLY: true, + }); + + user1Mails = mailserver.iterator(users.user1.email); + user2Mails = mailserver.iterator(users.user2.email); + user3Mails = mailserver.iterator(users.user3.email); +}); + +test.afterAll('Teardown', async ({}, testInfo: TestInfo) => { + utils.stopVaultwarden(testInfo); + utils.closeMails(mailserver, [user1Mails, user2Mails, user3Mails]); +}); + +test('Create user2', async ({ page }) => { + await createAccount(test, page, users.user2, user2Mails); +}); + +test('Invite users', async ({ page }) => { + await createAccount(test, page, users.user1, user1Mails); + + await test.step('Create Org', async () => { + await page.getByRole('link', { name: 'New organisation' }).click(); + await page.getByLabel('Organisation name (required)').fill('Test'); + await page.getByRole('button', { name: 'Submit' }).click(); + await page.locator('div').filter({ hasText: 'Members' }).nth(2).click(); + }); + + await test.step('Invite user2', async () => { + await page.getByRole('button', { name: 'Invite member' }).click(); + await page.getByLabel('Email (required)').fill(users.user2.email); + await page.getByRole('tab', { name: 'Collections' }).click(); + await page.locator('label').filter({ hasText: 'Grant access to all current' }).click(); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.getByTestId("toast-message")).toHaveText('User(s) invited'); + }); + + await test.step('Invite user3', async () => { + await page.getByRole('button', { name: 'Invite member' }).click(); + await page.getByLabel('Email (required)').fill(users.user3.email); + await page.getByRole('tab', { name: 'Collections' }).click(); + await page.locator('label').filter({ hasText: 'Grant access to all current' }).click(); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.getByTestId("toast-message")).toHaveText('User(s) invited'); + }); +}); + +test('invited with existing account', async ({ page }) => { + const link = await test.step('Extract email link', async () => { + const { value: invited } = await user2Mails.next(); + expect(invited.subject).toContain("Join Test") + + await page.setContent(invited.html); + return await page.getByTestId("invite").getAttribute("href"); + }); + + await test.step('Redirect to Keycloak', async () => { + await page.goto(link); + await expect(page).toHaveTitle("Enterprise single sign-on | Vaultwarden Web"); + await page.getByRole('button', { name: 'Log in' }).click(); + }); + + await test.step('Keycloak login', async () => { + await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible(); + await page.getByLabel(/Username/).fill(users.user2.name); + await page.getByLabel('Password', { exact: true }).fill(users.user2.password); + await page.getByRole('button', { name: 'Sign In' }).click(); + }); + + await test.step('Unlock vault', async () => { + await expect(page).toHaveTitle('Vaultwarden Web'); + await page.getByLabel('Master password').fill(users.user2.password); + await page.getByRole('button', { name: 'Unlock' }).click(); + }); + + await test.step('Default vault page', async () => { + await expect(page).toHaveTitle(/Vaults/); + await expect(page.getByTestId("toast-title")).toHaveText("Invitation accepted"); + }); + + await test.step('Check mails', async () => { + const { value: logged } = await user2Mails.next(); + expect(logged.subject).toContain("New Device Logged") + + const { value: accepted } = await user1Mails.next(); + expect(accepted.subject).toContain("Invitation to Test accepted") + }); +}); + +test('invited with new account', async ({ page }) => { + const link = await test.step('Extract email link', async () => { + const { value: invited } = await user3Mails.next(); + expect(invited.subject).toContain("Join Test") + + await page.setContent(invited.html); + return await page.getByTestId("invite").getAttribute("href"); + }); + + await test.step('Redirect to Keycloak', async () => { + await page.goto(link); + await expect(page).toHaveTitle("Enterprise single sign-on | Vaultwarden Web"); + await page.getByRole('button', { name: 'Log in' }).click(); + }); + + await test.step('Keycloak login', async () => { + await expect(page.getByRole('heading', { name: 'Sign in to your account' })).toBeVisible(); + await page.getByLabel(/Username/).fill(users.user3.name); + await page.getByLabel('Password', { exact: true }).fill(users.user3.password); + await page.getByRole('button', { name: 'Sign In' }).click(); + }); + + await test.step('Create Vault account', async () => { + await expect(page.getByText('Set master password')).toBeVisible(); + await page.getByLabel('Master password', { exact: true }).fill(users.user3.password); + await page.getByLabel('Re-type master password').fill(users.user3.password); + await page.getByRole('button', { name: 'Submit' }).click(); + }); + + await test.step('Default vault page', async () => { + await expect(page).toHaveTitle(/Vaults/); + await expect(page.getByTestId("toast-title")).toHaveText("Invitation accepted"); + }); + + await test.step('Check mails', async () => { + const { value: logged } = await user3Mails.next(); + expect(logged.subject).toContain("New Device Logged") + + const { value: accepted } = await user1Mails.next(); + expect(accepted.subject).toContain("Invitation to Test accepted") + }); +}); diff --git a/src/static/templates/email/send_org_invite.html.hbs b/src/static/templates/email/send_org_invite.html.hbs index a78a89e9..b4fe1731 100644 --- a/src/static/templates/email/send_org_invite.html.hbs +++ b/src/static/templates/email/send_org_invite.html.hbs @@ -9,7 +9,7 @@ Join {{{org_name}}} - Join Organization Now diff --git a/src/static/templates/email/twofactor_email.html.hbs b/src/static/templates/email/twofactor_email.html.hbs index 30990d9e..672daa32 100644 --- a/src/static/templates/email/twofactor_email.html.hbs +++ b/src/static/templates/email/twofactor_email.html.hbs @@ -4,7 +4,7 @@ Vaultwarden Login Verification Code diff --git a/src/static/templates/email/verify_email.html.hbs b/src/static/templates/email/verify_email.html.hbs index c37cf36d..29a1377f 100644 --- a/src/static/templates/email/verify_email.html.hbs +++ b/src/static/templates/email/verify_email.html.hbs @@ -9,7 +9,7 @@ Verify Your Email
- Your two-step verification code is: {{token}} + Your two-step verification code is: {{token}}
- Verify Email Address Now