mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-09 18:25:58 +03:00
SSO using OpenID Connect (#3899)
* Add SSO functionality using OpenID Connect Co-authored-by: Pablo Ovelleiro Corral <mail@pablo.tools> Co-authored-by: Stuart Heap <sheap13@gmail.com> Co-authored-by: Alex Moore <skiepp@my-dockerfarm.cloud> Co-authored-by: Brian Munro <brian.alexander.munro@gmail.com> Co-authored-by: Jacques B. <timshel@github.com> * Improvements and error handling * Stop rolling device token * Add playwright tests * Activate PKCE by default * Ensure result order when searching for sso_user * add SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION * Toggle SSO button in scss * Base64 encode state before sending it to providers * Prevent disabled User from SSO login * Review fixes * Remove unused UserOrganization.invited_by_email * Split SsoUser::find_by_identifier_or_email * api::Accounts::verify_password add the policy even if it's ignored * Disable signups if SSO_ONLY is activated * Add verifiedDate to organizations::get_org_domain_sso_details * Review fixes * Remove OrganizationId guard from get_master_password_policy * Add wrapper type OIDCCode OIDCState OIDCIdentifier * Membership::confirm_user_invitations fix and tests * Allow set-password only if account is unitialized * Review fixes * Prevent accepting another user invitation * Log password change event on SSO account creation * Unify master password policy resolution * Upgrade openidconnect to 4.0.0 * Revert "Remove unused UserOrganization.invited_by_email" This reverts commit 548e19995e141314af98a10d170ea7371f02fab4. * Process org enrollment in accounts::post_set_password * Improve tests * Pass the claim invited_by_email in case it was not in db * Add Slack configuration hints * Fix playwright tests * Skip broken tests * Add sso identifier in admin user panel * Remove duplicate expiration check, add a log * Augment mobile refresh_token validity * Rauthy configuration hints * Fix playwright tests * Playwright upgrade and conf improvement * Playwright tests improvements * 2FA email and device creation change * Fix and improve Playwright tests * Minor improvements * Fix enforceOnLogin org policies * Run playwright sso tests against correct db * PKCE should now work with Zitadel * Playwright upgrade maildev to use MailBuffer.expect * Upgrades playwright tests deps * Check email_verified in id_token and user_info * Add sso verified endpoint for v2025.6.0 * Fix playwright tests * Create a separate sso_client * Upgrade openidconnect to 4.0.1 * Server settings for login fields toggle * Use only css for login fields * Fix playwright test * Review fix * More review fix * Perform same checks when setting kdf --------- Co-authored-by: Felix Eckhofer <felix@eckhofer.com> Co-authored-by: Pablo Ovelleiro Corral <mail@pablo.tools> Co-authored-by: Stuart Heap <sheap13@gmail.com> Co-authored-by: Alex Moore <skiepp@my-dockerfarm.cloud> Co-authored-by: Brian Munro <brian.alexander.munro@gmail.com> Co-authored-by: Jacques B. <timshel@github.com> Co-authored-by: Timshel <timshel@480s>
This commit is contained in:
64
playwright/.env.template
Normal file
64
playwright/.env.template
Normal file
@@ -0,0 +1,64 @@
|
||||
#################################
|
||||
### 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}
|
||||
LOG_LEVEL=info,oidcwarden::sso=debug
|
||||
I_REALLY_WANT_VOLATILE_STORAGE=true
|
||||
|
||||
SSO_ENABLED=true
|
||||
SSO_ONLY=false
|
||||
SSO_CLIENT_ID=warden
|
||||
SSO_CLIENT_SECRET=warden
|
||||
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
|
6
playwright/.gitignore
vendored
Normal file
6
playwright/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
logs
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright/.cache/
|
||||
temp
|
166
playwright/README.md
Normal file
166
playwright/README.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# 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
|
||||
|
||||
Additionally 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, you can use a front-end build from [dani-garcia/bw_web_builds](https://github.com/dani-garcia/bw_web_builds/releases).
|
||||
|
||||
## 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`.
|
40
playwright/compose/keycloak/Dockerfile
Normal file
40
playwright/compose/keycloak/Dockerfile
Normal file
@@ -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"
|
36
playwright/compose/keycloak/setup.sh
Executable file
36
playwright/compose/keycloak/setup.sh
Executable file
@@ -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"
|
40
playwright/compose/playwright/Dockerfile
Normal file
40
playwright/compose/playwright/Dockerfile
Normal file
@@ -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"]
|
40
playwright/compose/warden/Dockerfile
Normal file
40
playwright/compose/warden/Dockerfile
Normal file
@@ -0,0 +1,40 @@
|
||||
FROM playwright_oidc_vaultwarden_prebuilt AS prebuilt
|
||||
|
||||
FROM node:18-bookworm AS build
|
||||
|
||||
ARG REPO_URL
|
||||
ARG COMMIT_HASH
|
||||
|
||||
ENV REPO_URL=$REPO_URL
|
||||
ENV COMMIT_HASH=$COMMIT_HASH
|
||||
|
||||
COPY --from=prebuilt /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=prebuilt /start.sh .
|
||||
COPY --from=prebuilt /vaultwarden .
|
||||
COPY --from=build /web-vault ./web-vault
|
||||
|
||||
ENTRYPOINT ["/start.sh"]
|
24
playwright/compose/warden/build.sh
Executable file
24
playwright/compose/warden/build.sh
Executable file
@@ -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
|
124
playwright/docker-compose.yml
Normal file
124
playwright/docker-compose.yml
Normal file
@@ -0,0 +1,124 @@
|
||||
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/warden
|
||||
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
|
||||
- LOG_LEVEL
|
||||
- LOGIN_RATELIMIT_MAX_BURST
|
||||
- SMTP_HOST
|
||||
- SMTP_FROM
|
||||
- SMTP_DEBUG
|
||||
- SSO_DEBUG_TOKENS
|
||||
- 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:3.0.4
|
||||
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}
|
22
playwright/global-setup.ts
Normal file
22
playwright/global-setup.ts
Normal file
@@ -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;
|
246
playwright/global-utils.ts
Normal file
246
playwright/global-utils.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import { expect, 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 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":
|
||||
case "sso-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":
|
||||
case "sso-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":
|
||||
case "sso-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}`};
|
||||
case "sqlite":
|
||||
case "sso-sqlite":
|
||||
return { I_REALLY_WANT_VOLATILE_STORAGE: true };
|
||||
default:
|
||||
throw new Error(`Unknow database name: ${testInfo.project.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* All parameters passed in `env` need to be added to the docker-compose.yml
|
||||
**/
|
||||
export async function startVault(browser: Browser, testInfo: TestInfo, env = {}, resetDB: Boolean = true) {
|
||||
if( resetDB ){
|
||||
switch(testInfo.project.name) {
|
||||
case "postgres":
|
||||
case "sso-postgres":
|
||||
await wipePostgres();
|
||||
break;
|
||||
case "mariadb":
|
||||
case "sso-mariadb":
|
||||
await wipeMariaDB();
|
||||
break;
|
||||
case "mysql":
|
||||
case "sso-mysql":
|
||||
await wipeMysqlDB();
|
||||
break;
|
||||
case "sqlite":
|
||||
case "sso-sqlite":
|
||||
wipeSqlite();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknow database name: ${testInfo.project.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
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 stopVault(force: boolean = false) {
|
||||
if( force === false && process.env.PW_KEEP_SERVICE_RUNNNING === "true" ) {
|
||||
console.log(`Keep vaultwarden running on: ${process.env.DOMAIN}`);
|
||||
} else {
|
||||
console.log(`Vaultwarden stopping`);
|
||||
execSync(`docker compose --profile playwright --env-file test.env stop Vaultwarden`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function restartVault(page: Page, testInfo: TestInfo, env, resetDB: Boolean = true) {
|
||||
stopVault(true);
|
||||
return startVault(page.context().browser(), testInfo, env, resetDB);
|
||||
}
|
||||
|
||||
export async function checkNotification(page: Page, hasText: string) {
|
||||
await expect(page.locator('bit-toast').filter({ hasText })).toBeVisible();
|
||||
await page.locator('bit-toast').filter({ hasText }).getByRole('button').click();
|
||||
await expect(page.locator('bit-toast').filter({ hasText })).toHaveCount(0);
|
||||
}
|
||||
|
||||
export async function cleanLanding(page: Page) {
|
||||
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.getByRole('button').nth(0)).toBeVisible();
|
||||
|
||||
const logged = await page.getByRole('button', { name: 'Log out' }).count();
|
||||
if( logged > 0 ){
|
||||
await page.getByRole('button', { name: 'Log out' }).click();
|
||||
await page.getByRole('button', { name: 'Log out' }).click();
|
||||
}
|
||||
}
|
||||
|
||||
export async function logout(test: Test, page: Page, user: { name: string }) {
|
||||
await test.step('logout', async () => {
|
||||
await page.getByRole('button', { name: user.name, exact: true }).click();
|
||||
await page.getByRole('menuitem', { name: 'Log out' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Log in' })).toBeVisible();
|
||||
});
|
||||
}
|
2547
playwright/package-lock.json
generated
Normal file
2547
playwright/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
playwright/package.json
Normal file
21
playwright/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "scenarios",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.53.0",
|
||||
"dotenv": "^16.5.0",
|
||||
"dotenv-expand": "^12.0.2",
|
||||
"maildev": "npm:@timshel_npm/maildev@^3.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"mysql2": "^3.14.1",
|
||||
"otpauth": "^9.4.0",
|
||||
"pg": "^8.16.0"
|
||||
}
|
||||
}
|
143
playwright/playwright.config.ts
Normal file
143
playwright/playwright.config.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
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,
|
||||
|
||||
retries: 0,
|
||||
workers: 1,
|
||||
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
|
||||
/* Long global timeout for complex tests
|
||||
* But short action/nav/expect timeouts to fail on specific step (raise locally if not enough).
|
||||
*/
|
||||
timeout: 120 * 1000,
|
||||
actionTimeout: 10 * 1000,
|
||||
navigationTimeout: 10 * 1000,
|
||||
expect: { timeout: 10 * 1000 },
|
||||
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: process.env.DOMAIN,
|
||||
browserName: 'firefox',
|
||||
locale: 'en-GB',
|
||||
timezoneId: 'Europe/London',
|
||||
|
||||
/* Always collect trace (other values add random test failures) See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on',
|
||||
viewport: {
|
||||
width: 1080,
|
||||
height: 720,
|
||||
},
|
||||
video: "on",
|
||||
},
|
||||
|
||||
/* 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'),
|
||||
});
|
93
playwright/test.env
Normal file
93
playwright/test.env
Normal file
@@ -0,0 +1,93 @@
|
||||
##################################################################
|
||||
### 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}
|
||||
PW_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}
|
||||
LOG_LEVEL=info,oidcwarden::sso=debug
|
||||
LOGIN_RATELIMIT_MAX_BURST=100
|
||||
|
||||
SMTP_SECURITY=off
|
||||
SMTP_PORT=${MAILDEV_SMTP_PORT}
|
||||
SMTP_FROM_NAME=Vaultwarden
|
||||
SMTP_TIMEOUT=5
|
||||
|
||||
SSO_CLIENT_ID=warden
|
||||
SSO_CLIENT_SECRET=warden
|
||||
SSO_AUTHORITY=http://${KC_HTTP_HOST}:${KC_HTTP_PORT}/realms/${TEST_REALM}
|
||||
SSO_DEBUG_TOKENS=true
|
||||
|
||||
###########################
|
||||
# Docker MariaDb container#
|
||||
###########################
|
||||
MARIADB_PORT=3307
|
||||
MARIADB_ROOT_PASSWORD=warden
|
||||
MARIADB_USER=warden
|
||||
MARIADB_PASSWORD=warden
|
||||
MARIADB_DATABASE=warden
|
||||
|
||||
###########################
|
||||
# Docker Mysql container#
|
||||
###########################
|
||||
MYSQL_PORT=3309
|
||||
MYSQL_ROOT_PASSWORD=warden
|
||||
MYSQL_USER=warden
|
||||
MYSQL_PASSWORD=warden
|
||||
MYSQL_DATABASE=warden
|
||||
|
||||
############################
|
||||
# Docker Postgres container#
|
||||
############################
|
||||
POSTGRES_PORT=5433
|
||||
POSTGRES_USER=warden
|
||||
POSTGRES_PASSWORD=warden
|
||||
POSTGRES_DB=warden
|
37
playwright/tests/collection.spec.ts
Normal file
37
playwright/tests/collection.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { test, expect, type TestInfo } from '@playwright/test';
|
||||
|
||||
import * as utils from "../global-utils";
|
||||
import { createAccount } from './setups/user';
|
||||
|
||||
let users = utils.loadEnv();
|
||||
|
||||
test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
|
||||
await utils.startVault(browser, testInfo);
|
||||
});
|
||||
|
||||
test.afterAll('Teardown', async ({}) => {
|
||||
utils.stopVault();
|
||||
});
|
||||
|
||||
test('Create', async ({ page }) => {
|
||||
await createAccount(test, page, users.user1);
|
||||
|
||||
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 utils.checkNotification(page, 'Organisation created');
|
||||
});
|
||||
|
||||
await test.step('Create Collection', async () => {
|
||||
await page.getByRole('link', { name: 'Collections' }).click();
|
||||
await page.getByRole('button', { name: 'New' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Collection' }).click();
|
||||
await page.getByLabel('Name (required)').fill('RandomCollec');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await utils.checkNotification(page, 'Created collection RandomCollec');
|
||||
await expect(page.getByRole('button', { name: 'RandomCollec' })).toBeVisible();
|
||||
});
|
||||
});
|
100
playwright/tests/login.smtp.spec.ts
Normal file
100
playwright/tests/login.smtp.spec.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { test, expect, type TestInfo } from '@playwright/test';
|
||||
import { MailDev } from 'maildev';
|
||||
|
||||
const utils = require('../global-utils');
|
||||
import { createAccount, logUser } from './setups/user';
|
||||
import { activateEmail, retrieveEmailCode, disableEmail } from './setups/2fa';
|
||||
|
||||
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.startVault(browser, testInfo, {
|
||||
SMTP_HOST: process.env.MAILDEV_HOST,
|
||||
SMTP_FROM: process.env.PW_SMTP_FROM,
|
||||
});
|
||||
});
|
||||
|
||||
test.afterAll('Teardown', async ({}) => {
|
||||
utils.stopVault();
|
||||
if( mailserver ){
|
||||
await mailserver.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Account creation', async ({ page }) => {
|
||||
const mailBuffer = mailserver.buffer(users.user1.email);
|
||||
|
||||
await createAccount(test, page, users.user1, mailBuffer);
|
||||
|
||||
mailBuffer.close();
|
||||
});
|
||||
|
||||
test('Login', async ({ context, page }) => {
|
||||
const mailBuffer = mailserver.buffer(users.user1.email);
|
||||
|
||||
await logUser(test, page, users.user1, mailBuffer);
|
||||
|
||||
await test.step('verify email', async () => {
|
||||
await page.getByText('Verify your account\'s email').click();
|
||||
await expect(page.getByText('Verify your account\'s email')).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Send email' }).click();
|
||||
|
||||
await utils.checkNotification(page, 'Check your email inbox for a verification link');
|
||||
|
||||
const verify = await mailBuffer.expect((m) => m.subject === "Verify Your Email");
|
||||
expect(verify.from[0]?.address).toBe(process.env.PW_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 utils.checkNotification(page, 'Account email verified');
|
||||
});
|
||||
|
||||
mailBuffer.close();
|
||||
});
|
||||
|
||||
test('Activate 2fa', async ({ page }) => {
|
||||
const emails = mailserver.buffer(users.user1.email);
|
||||
|
||||
await logUser(test, page, users.user1);
|
||||
|
||||
await activateEmail(test, page, users.user1, emails);
|
||||
|
||||
emails.close();
|
||||
});
|
||||
|
||||
test('2fa', async ({ 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();
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible();
|
||||
const code = await retrieveEmailCode(test, page, emails);
|
||||
await page.getByLabel(/Verification code/).fill(code);
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
await expect(page).toHaveTitle(/Vaults/);
|
||||
})
|
||||
|
||||
await disableEmail(test, page, users.user1);
|
||||
|
||||
emails.close();
|
||||
});
|
51
playwright/tests/login.spec.ts
Normal file
51
playwright/tests/login.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
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';
|
||||
import { activateTOTP, disableTOTP } from './setups/2fa';
|
||||
|
||||
let users = utils.loadEnv();
|
||||
let totp;
|
||||
|
||||
test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
|
||||
await utils.startVault(browser, testInfo, {});
|
||||
});
|
||||
|
||||
test.afterAll('Teardown', async ({}) => {
|
||||
utils.stopVault();
|
||||
});
|
||||
|
||||
test('Account creation', async ({ page }) => {
|
||||
await createAccount(test, page, users.user1);
|
||||
});
|
||||
|
||||
test('Master password login', async ({ page }) => {
|
||||
await logUser(test, page, users.user1);
|
||||
});
|
||||
|
||||
test('Authenticator 2fa', async ({ page }) => {
|
||||
await logUser(test, page, users.user1);
|
||||
|
||||
let totp = await activateTOTP(test, page, users.user1);
|
||||
|
||||
await utils.logout(test, page, users.user1);
|
||||
|
||||
await test.step('login', async () => {
|
||||
let timestamp = Date.now(); // Needed 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 expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible();
|
||||
await page.getByLabel(/Verification code/).fill(totp.generate({timestamp}));
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
||||
});
|
||||
|
||||
await disableTOTP(test, page, users.user1);
|
||||
});
|
115
playwright/tests/organization.smtp.spec.ts
Normal file
115
playwright/tests/organization.smtp.spec.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { test, expect, type TestInfo } from '@playwright/test';
|
||||
import { MailDev } from 'maildev';
|
||||
|
||||
import * as utils from '../global-utils';
|
||||
import * as orgs from './setups/orgs';
|
||||
import { createAccount, logUser } from './setups/user';
|
||||
|
||||
let users = utils.loadEnv();
|
||||
|
||||
let mailServer, mail1Buffer, mail2Buffer, mail3Buffer;
|
||||
|
||||
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.startVault(browser, testInfo, {
|
||||
SMTP_HOST: process.env.MAILDEV_HOST,
|
||||
SMTP_FROM: process.env.PW_SMTP_FROM,
|
||||
});
|
||||
|
||||
mail1Buffer = mailServer.buffer(users.user1.email);
|
||||
mail2Buffer = mailServer.buffer(users.user2.email);
|
||||
mail3Buffer = mailServer.buffer(users.user3.email);
|
||||
});
|
||||
|
||||
test.afterAll('Teardown', async ({}, testInfo: TestInfo) => {
|
||||
utils.stopVault(testInfo);
|
||||
[mail1Buffer, mail2Buffer, mail3Buffer, mailServer].map((m) => m?.close());
|
||||
});
|
||||
|
||||
test('Create user3', async ({ page }) => {
|
||||
await createAccount(test, page, users.user3, mail3Buffer);
|
||||
});
|
||||
|
||||
test('Invite users', async ({ page }) => {
|
||||
await createAccount(test, page, users.user1, mail1Buffer);
|
||||
|
||||
await orgs.create(test, page, 'Test');
|
||||
await orgs.members(test, page, 'Test');
|
||||
await orgs.invite(test, page, 'Test', users.user2.email);
|
||||
await orgs.invite(test, page, 'Test', users.user3.email, {
|
||||
navigate: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('invited with new account', async ({ page }) => {
|
||||
const invited = await mail2Buffer.expect((mail) => mail.subject === '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('New master password (required)', { exact: true }).fill(users.user2.password);
|
||||
await page.getByLabel('Confirm new master password (').fill(users.user2.password);
|
||||
await page.getByRole('button', { name: 'Create account' }).click();
|
||||
await utils.checkNotification(page, 'Your new account has been created');
|
||||
|
||||
// Redirected to the vault
|
||||
await expect(page).toHaveTitle('Vaults | Vaultwarden Web');
|
||||
await utils.checkNotification(page, 'You have been logged in!');
|
||||
await utils.checkNotification(page, 'Invitation accepted');
|
||||
});
|
||||
|
||||
await test.step('Check mails', async () => {
|
||||
await mail2Buffer.expect((m) => m.subject === 'Welcome');
|
||||
await mail2Buffer.expect((m) => m.subject === 'New Device Logged In From Firefox');
|
||||
await mail1Buffer.expect((m) => m.subject.includes('Invitation to Test accepted'));
|
||||
});
|
||||
});
|
||||
|
||||
test('invited with existing account', async ({ page }) => {
|
||||
const invited = await mail3Buffer.expect((mail) => mail.subject === '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(/Vaultwarden Web/);
|
||||
await utils.checkNotification(page, 'Invitation accepted');
|
||||
|
||||
await mail3Buffer.expect((m) => m.subject === 'New Device Logged In From Firefox');
|
||||
await mail1Buffer.expect((m) => m.subject.includes('Invitation to Test accepted'));
|
||||
});
|
||||
|
||||
test('Confirm invited user', async ({ page }) => {
|
||||
await logUser(test, page, users.user1, mail1Buffer);
|
||||
|
||||
await orgs.members(test, page, 'Test');
|
||||
await orgs.confirm(test, page, 'Test', users.user2.email);
|
||||
|
||||
await mail2Buffer.expect((m) => m.subject.includes('Invitation to Test confirmed'));
|
||||
});
|
||||
|
||||
test('Organization is visible', async ({ page }) => {
|
||||
await logUser(test, page, users.user2, mail2Buffer);
|
||||
await page.getByRole('button', { name: 'vault: Test', exact: true }).click();
|
||||
await expect(page.getByLabel('Filter: Default collection')).toBeVisible();
|
||||
});
|
54
playwright/tests/organization.spec.ts
Normal file
54
playwright/tests/organization.spec.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { test, expect, type TestInfo } from '@playwright/test';
|
||||
import { MailDev } from 'maildev';
|
||||
|
||||
import * as utils from "../global-utils";
|
||||
import * as orgs from './setups/orgs';
|
||||
import { createAccount, logUser } from './setups/user';
|
||||
|
||||
let users = utils.loadEnv();
|
||||
|
||||
test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
|
||||
await utils.startVault(browser, testInfo);
|
||||
});
|
||||
|
||||
test.afterAll('Teardown', async ({}) => {
|
||||
utils.stopVault();
|
||||
});
|
||||
|
||||
test('Invite', async ({ page }) => {
|
||||
await createAccount(test, page, users.user3);
|
||||
await createAccount(test, page, users.user1);
|
||||
|
||||
await orgs.create(test, page, 'New organisation');
|
||||
await orgs.members(test, page, 'New organisation');
|
||||
|
||||
await test.step('missing user2', async () => {
|
||||
await orgs.invite(test, page, 'New organisation', users.user2.email);
|
||||
await expect(page.getByRole('row', { name: users.user2.email })).toHaveText(/Invited/);
|
||||
});
|
||||
|
||||
await test.step('existing user3', async () => {
|
||||
await orgs.invite(test, page, 'New organisation', users.user3.email);
|
||||
await expect(page.getByRole('row', { name: users.user3.email })).toHaveText(/Needs confirmation/);
|
||||
await orgs.confirm(test, page, 'New organisation', users.user3.email);
|
||||
});
|
||||
|
||||
await test.step('confirm user2', async () => {
|
||||
await createAccount(test, page, users.user2);
|
||||
await logUser(test, page, users.user1);
|
||||
await orgs.members(test, page, 'New organisation');
|
||||
await orgs.confirm(test, page, 'New organisation', users.user2.email);
|
||||
});
|
||||
|
||||
await test.step('Org visible user2 ', async () => {
|
||||
await logUser(test, page, users.user2);
|
||||
await page.getByRole('button', { name: 'vault: New organisation', exact: true }).click();
|
||||
await expect(page.getByLabel('Filter: Default collection')).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Org visible user3 ', async () => {
|
||||
await logUser(test, page, users.user3);
|
||||
await page.getByRole('button', { name: 'vault: New organisation', exact: true }).click();
|
||||
await expect(page.getByLabel('Filter: Default collection')).toBeVisible();
|
||||
});
|
||||
});
|
92
playwright/tests/setups/2fa.ts
Normal file
92
playwright/tests/setups/2fa.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { expect, type Page, Test } from '@playwright/test';
|
||||
import { type MailBuffer } from 'maildev';
|
||||
import * as OTPAuth from "otpauth";
|
||||
|
||||
import * as utils from '../../global-utils';
|
||||
|
||||
export async function activateTOTP(test: Test, page: Page, user: { name: string, password: string }): OTPAuth.TOTP {
|
||||
return await test.step('Activate TOTP 2FA', async () => {
|
||||
await page.getByRole('button', { name: user.name }).click();
|
||||
await page.getByRole('menuitem', { name: 'Account settings' }).click();
|
||||
await page.getByRole('link', { name: 'Security' }).click();
|
||||
await page.getByRole('link', { name: 'Two-step login' }).click();
|
||||
await page.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click();
|
||||
await page.getByLabel('Master password (required)').fill(user.password);
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
const secret = await page.getByLabel('Key').innerText();
|
||||
let totp = new OTPAuth.TOTP({ secret, period: 30 });
|
||||
|
||||
await page.getByLabel(/Verification code/).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();
|
||||
|
||||
return totp;
|
||||
})
|
||||
}
|
||||
|
||||
export async function disableTOTP(test: Test, page: Page, user: { password: string }) {
|
||||
await test.step('Disable TOTP 2FA', async () => {
|
||||
await page.getByRole('button', { name: 'Test' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Account settings' }).click();
|
||||
await page.getByRole('link', { name: 'Security' }).click();
|
||||
await page.getByRole('link', { name: 'Two-step login' }).click();
|
||||
await page.locator('bit-item').filter({ hasText: /Authenticator app/ }).getByRole('button').click();
|
||||
await page.getByLabel('Master password (required)').click();
|
||||
await page.getByLabel('Master password (required)').fill(user.password);
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await page.getByRole('button', { name: 'Turn off' }).click();
|
||||
await page.getByRole('button', { name: 'Yes' }).click();
|
||||
await utils.checkNotification(page, 'Two-step login provider turned off');
|
||||
});
|
||||
}
|
||||
|
||||
export async function activateEmail(test: Test, page: Page, user: { name: string, password: string }, mailBuffer: MailBuffer) {
|
||||
await test.step('Activate Email 2FA', async () => {
|
||||
await page.getByRole('button', { name: user.name }).click();
|
||||
await page.getByRole('menuitem', { name: 'Account settings' }).click();
|
||||
await page.getByRole('link', { name: 'Security' }).click();
|
||||
await page.getByRole('link', { name: 'Two-step login' }).click();
|
||||
await page.locator('bit-item').filter({ hasText: 'Email Email Enter a code sent' }).getByRole('button').click();
|
||||
await page.getByLabel('Master password (required)').fill(user.password);
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await page.getByRole('button', { name: 'Send email' }).click();
|
||||
});
|
||||
|
||||
let code = await retrieveEmailCode(test, page, mailBuffer);
|
||||
|
||||
await test.step('input code', async () => {
|
||||
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 });
|
||||
});
|
||||
}
|
||||
|
||||
export async function retrieveEmailCode(test: Test, page: Page, mailBuffer: MailBuffer): string {
|
||||
return await test.step('retrieve code', async () => {
|
||||
const codeMail = await mailBuffer.expect((mail) => mail.subject.includes("Login Verification Code"));
|
||||
const page2 = await page.context().newPage();
|
||||
await page2.setContent(codeMail.html);
|
||||
const code = await page2.getByTestId("2fa").innerText();
|
||||
await page2.close();
|
||||
return code;
|
||||
});
|
||||
}
|
||||
|
||||
export async function disableEmail(test: Test, page: Page, user: { password: string }) {
|
||||
await test.step('Disable Email 2FA', async () => {
|
||||
await page.getByRole('button', { name: 'Test' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Account settings' }).click();
|
||||
await page.getByRole('link', { name: 'Security' }).click();
|
||||
await page.getByRole('link', { name: 'Two-step login' }).click();
|
||||
await page.locator('bit-item').filter({ hasText: 'Email' }).getByRole('button').click();
|
||||
await page.getByLabel('Master password (required)').click();
|
||||
await page.getByLabel('Master password (required)').fill(user.password);
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await page.getByRole('button', { name: 'Turn off' }).click();
|
||||
await page.getByRole('button', { name: 'Yes' }).click();
|
||||
|
||||
await utils.checkNotification(page, 'Two-step login provider turned off');
|
||||
});
|
||||
}
|
7
playwright/tests/setups/db-setup.ts
Normal file
7
playwright/tests/setups/db-setup.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { test } from './db-test';
|
||||
|
||||
const utils = require('../../global-utils');
|
||||
|
||||
test('DB start', async ({ serviceName }) => {
|
||||
utils.startComposeService(serviceName);
|
||||
});
|
11
playwright/tests/setups/db-teardown.ts
Normal file
11
playwright/tests/setups/db-teardown.ts
Normal file
@@ -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);
|
||||
}
|
||||
});
|
9
playwright/tests/setups/db-test.ts
Normal file
9
playwright/tests/setups/db-test.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { test as base } from '@playwright/test';
|
||||
|
||||
export type TestOptions = {
|
||||
serviceName: string;
|
||||
};
|
||||
|
||||
export const test = base.extend<TestOptions>({
|
||||
serviceName: ['', { option: true }],
|
||||
});
|
77
playwright/tests/setups/orgs.ts
Normal file
77
playwright/tests/setups/orgs.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { expect, type Browser,Page } from '@playwright/test';
|
||||
|
||||
import * as utils from '../../global-utils';
|
||||
|
||||
export async function create(test, page: Page, name: string) {
|
||||
await test.step('Create Org', async () => {
|
||||
await page.locator('a').filter({ hasText: 'Password Manager' }).first().click();
|
||||
await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible();
|
||||
await page.getByRole('link', { name: 'New organisation' }).click();
|
||||
await page.getByLabel('Organisation name (required)').fill(name);
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
await utils.checkNotification(page, 'Organisation created');
|
||||
});
|
||||
}
|
||||
|
||||
export async function policies(test, page: Page, name: string) {
|
||||
await test.step(`Navigate to ${name} policies`, async () => {
|
||||
await page.locator('a').filter({ hasText: 'Admin Console' }).first().click();
|
||||
await page.locator('org-switcher').getByLabel(/Toggle collapse/).click();
|
||||
await page.locator('org-switcher').getByRole('link', { name: `${name}` }).first().click();
|
||||
await expect(page.getByRole('heading', { name: `${name} collections` })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Toggle collapse Settings' }).click();
|
||||
await page.getByRole('link', { name: 'Policies' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Policies' })).toBeVisible();
|
||||
});
|
||||
}
|
||||
|
||||
export async function members(test, page: Page, name: string) {
|
||||
await test.step(`Navigate to ${name} members`, async () => {
|
||||
await page.locator('a').filter({ hasText: 'Admin Console' }).first().click();
|
||||
await page.locator('org-switcher').getByLabel(/Toggle collapse/).click();
|
||||
await page.locator('org-switcher').getByRole('link', { name: `${name}` }).first().click();
|
||||
await expect(page.getByRole('heading', { name: `${name} collections` })).toBeVisible();
|
||||
await page.locator('div').filter({ hasText: 'Members' }).nth(2).click();
|
||||
await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'All' })).toBeVisible();
|
||||
});
|
||||
}
|
||||
|
||||
export async function invite(test, page: Page, name: string, email: string) {
|
||||
await test.step(`Invite ${email}`, async () => {
|
||||
await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Invite member' }).click();
|
||||
await page.getByLabel('Email (required)').fill(email);
|
||||
await page.getByRole('tab', { name: 'Collections' }).click();
|
||||
await page.getByRole('combobox', { name: 'Permission' }).click();
|
||||
await page.getByText('Edit items', { exact: true }).click();
|
||||
await page.getByLabel('Select collections').click();
|
||||
await page.getByText('Default collection').click();
|
||||
await page.getByRole('cell', { name: 'Collection', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await utils.checkNotification(page, 'User(s) invited');
|
||||
});
|
||||
}
|
||||
|
||||
export async function confirm(test, page: Page, name: string, user_email: string) {
|
||||
await test.step(`Confirm ${user_email}`, async () => {
|
||||
await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();
|
||||
await page.getByRole('row').filter({hasText: user_email}).getByLabel('Options').click();
|
||||
await page.getByRole('menuitem', { name: 'Confirm' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Confirm user' })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
||||
await utils.checkNotification(page, 'confirmed');
|
||||
});
|
||||
}
|
||||
|
||||
export async function revoke(test, page: Page, name: string, user_email: string) {
|
||||
await test.step(`Revoke ${user_email}`, async () => {
|
||||
await expect(page.getByRole('heading', { name: 'Members' })).toBeVisible();
|
||||
await page.getByRole('row').filter({hasText: user_email}).getByLabel('Options').click();
|
||||
await page.getByRole('menuitem', { name: 'Revoke access' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Revoke access' })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Revoke access' }).click();
|
||||
await utils.checkNotification(page, 'Revoked organisation access');
|
||||
});
|
||||
}
|
18
playwright/tests/setups/sso-setup.ts
Normal file
18
playwright/tests/setups/sso-setup.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
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 }) => {
|
||||
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}`);
|
||||
});
|
15
playwright/tests/setups/sso-teardown.ts
Normal file
15
playwright/tests/setups/sso-teardown.ts
Normal file
@@ -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`);
|
||||
}
|
||||
});
|
138
playwright/tests/setups/sso.ts
Normal file
138
playwright/tests/setups/sso.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { expect, type Page, Test } from '@playwright/test';
|
||||
import { type MailBuffer, MailServer } from 'maildev';
|
||||
import * as OTPAuth from "otpauth";
|
||||
|
||||
import * as utils from '../../global-utils';
|
||||
import { retrieveEmailCode } from './2fa';
|
||||
|
||||
/**
|
||||
* If a MailBuffer is passed it will be used and consume the expected emails
|
||||
*/
|
||||
export async function logNewUser(
|
||||
test: Test,
|
||||
page: Page,
|
||||
user: { email: string, name: string, password: string },
|
||||
options: { mailBuffer?: MailBuffer, override?: boolean } = {}
|
||||
) {
|
||||
await test.step(`Create user ${user.name}`, async () => {
|
||||
await page.context().clearCookies();
|
||||
|
||||
await test.step('Landing page', async () => {
|
||||
await utils.cleanLanding(page);
|
||||
|
||||
if( options.override ) {
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
} else {
|
||||
await page.getByLabel(/Email address/).fill(user.email);
|
||||
await page.getByRole('button', { name: /Use 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.getByRole('heading', { name: 'Join organisation' })).toBeVisible();
|
||||
await page.getByLabel('New master password (required)', { exact: true }).fill(user.password);
|
||||
await page.getByLabel('Confirm new master password (').fill(user.password);
|
||||
await page.getByRole('button', { name: 'Create account' }).click();
|
||||
});
|
||||
|
||||
await test.step('Default vault page', async () => {
|
||||
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
||||
await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
await utils.checkNotification(page, 'Account successfully created!');
|
||||
await utils.checkNotification(page, 'Invitation accepted');
|
||||
|
||||
if( options.mailBuffer ){
|
||||
let mailBuffer = options.mailBuffer;
|
||||
await test.step('Check emails', async () => {
|
||||
await mailBuffer.expect((m) => m.subject === "Welcome");
|
||||
await mailBuffer.expect((m) => m.subject.includes("New Device Logged"));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* If a MailBuffer is passed it will be used and consume the expected emails
|
||||
*/
|
||||
export async function logUser(
|
||||
test: Test,
|
||||
page: Page,
|
||||
user: { email: string, password: string },
|
||||
options: {
|
||||
mailBuffer ?: MailBuffer,
|
||||
override?: boolean,
|
||||
totp?: OTPAuth.TOTP,
|
||||
mail2fa?: boolean,
|
||||
} = {}
|
||||
) {
|
||||
let mailBuffer = options.mailBuffer;
|
||||
|
||||
await test.step(`Log user ${user.email}`, async () => {
|
||||
await page.context().clearCookies();
|
||||
|
||||
await test.step('Landing page', async () => {
|
||||
await utils.cleanLanding(page);
|
||||
|
||||
if( options.override ) {
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
} else {
|
||||
await page.getByLabel(/Email address/).fill(user.email);
|
||||
await page.getByRole('button', { name: /Use 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();
|
||||
});
|
||||
|
||||
if( options.totp || options.mail2fa ){
|
||||
let code;
|
||||
|
||||
await test.step('2FA check', async () => {
|
||||
await expect(page.getByRole('heading', { name: 'Verify your Identity' })).toBeVisible();
|
||||
|
||||
if( options.totp ) {
|
||||
const totp = options.totp;
|
||||
let timestamp = Date.now(); // Needed to use the next token
|
||||
timestamp = timestamp + (totp.period - (Math.floor(timestamp / 1000) % totp.period) + 1) * 1000;
|
||||
code = totp.generate({timestamp});
|
||||
} else if( options.mail2fa ){
|
||||
code = await retrieveEmailCode(test, page, mailBuffer);
|
||||
}
|
||||
|
||||
await page.getByLabel(/Verification code/).fill(code);
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
});
|
||||
}
|
||||
|
||||
await test.step('Unlock vault', async () => {
|
||||
await expect(page).toHaveTitle('Vaultwarden Web');
|
||||
await expect(page.getByRole('heading', { name: 'Your vault is locked' })).toBeVisible();
|
||||
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(/Vaultwarden Web/);
|
||||
await expect(page.getByTitle('All vaults', { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
if( mailBuffer ){
|
||||
await test.step('Check email', async () => {
|
||||
await mailBuffer.expect((m) => m.subject.includes("New Device Logged"));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
55
playwright/tests/setups/user.ts
Normal file
55
playwright/tests/setups/user.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { expect, type Browser, Page } from '@playwright/test';
|
||||
|
||||
import { type MailBuffer } from 'maildev';
|
||||
|
||||
import * as utils from '../../global-utils';
|
||||
|
||||
export async function createAccount(test, page: Page, user: { email: string, name: string, password: string }, mailBuffer?: MailBuffer) {
|
||||
await test.step(`Create user ${user.name}`, async () => {
|
||||
await utils.cleanLanding(page);
|
||||
|
||||
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.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
// Vault finish Creation
|
||||
await page.getByLabel('New master password (required)', { exact: true }).fill(user.password);
|
||||
await page.getByLabel('Confirm new master password (').fill(user.password);
|
||||
await page.getByRole('button', { name: 'Create account' }).click();
|
||||
|
||||
await utils.checkNotification(page, 'Your new account has been created')
|
||||
|
||||
// We are now in the default vault page
|
||||
await expect(page).toHaveTitle('Vaults | Vaultwarden Web');
|
||||
await utils.checkNotification(page, 'You have been logged in!');
|
||||
|
||||
if( mailBuffer ){
|
||||
await mailBuffer.expect((m) => m.subject === "Welcome");
|
||||
await mailBuffer.expect((m) => m.subject === "New Device Logged In From Firefox");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function logUser(test, page: Page, user: { email: string, password: string }, mailBuffer?: MailBuffer) {
|
||||
await test.step(`Log user ${user.email}`, async () => {
|
||||
await utils.cleanLanding(page);
|
||||
|
||||
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(/Vaultwarden Web/);
|
||||
|
||||
if( mailBuffer ){
|
||||
await mailBuffer.expect((m) => m.subject === "New Device Logged In From Firefox");
|
||||
}
|
||||
});
|
||||
}
|
53
playwright/tests/sso_login.smtp.spec.ts
Normal file
53
playwright/tests/sso_login.smtp.spec.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { test, expect, type TestInfo } from '@playwright/test';
|
||||
import { MailDev } from 'maildev';
|
||||
|
||||
import { logNewUser, logUser } from './setups/sso';
|
||||
import { activateEmail, disableEmail } from './setups/2fa';
|
||||
import * as utils from "../global-utils";
|
||||
|
||||
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.startVault(browser, testInfo, {
|
||||
SSO_ENABLED: true,
|
||||
SSO_ONLY: false,
|
||||
SMTP_HOST: process.env.MAILDEV_HOST,
|
||||
SMTP_FROM: process.env.PW_SMTP_FROM,
|
||||
});
|
||||
});
|
||||
|
||||
test.afterAll('Teardown', async ({}) => {
|
||||
utils.stopVault();
|
||||
if( mailserver ){
|
||||
await mailserver.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Create and activate 2FA', async ({ page }) => {
|
||||
const mailBuffer = mailserver.buffer(users.user1.email);
|
||||
|
||||
await logNewUser(test, page, users.user1, {mailBuffer: mailBuffer});
|
||||
|
||||
await activateEmail(test, page, users.user1, mailBuffer);
|
||||
|
||||
mailBuffer.close();
|
||||
});
|
||||
|
||||
test('Log and disable', async ({ page }) => {
|
||||
const mailBuffer = mailserver.buffer(users.user1.email);
|
||||
|
||||
await logUser(test, page, users.user1, {mailBuffer: mailBuffer, mail2fa: true});
|
||||
|
||||
await disableEmail(test, page, users.user1);
|
||||
|
||||
mailBuffer.close();
|
||||
});
|
94
playwright/tests/sso_login.spec.ts
Normal file
94
playwright/tests/sso_login.spec.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { test, expect, type TestInfo } from '@playwright/test';
|
||||
|
||||
import { logNewUser, logUser } from './setups/sso';
|
||||
import { activateTOTP, disableTOTP } from './setups/2fa';
|
||||
import * as utils from "../global-utils";
|
||||
|
||||
let users = utils.loadEnv();
|
||||
|
||||
test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
|
||||
await utils.startVault(browser, testInfo, {
|
||||
SSO_ENABLED: true,
|
||||
SSO_ONLY: false
|
||||
});
|
||||
});
|
||||
|
||||
test.afterAll('Teardown', async ({}) => {
|
||||
utils.stopVault();
|
||||
});
|
||||
|
||||
test('Account creation using SSO', async ({ page }) => {
|
||||
// Landing page
|
||||
await logNewUser(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(/Vaultwarden Web/);
|
||||
});
|
||||
|
||||
test('SSO login with TOTP 2fa', async ({ page }) => {
|
||||
await logUser(test, page, users.user1);
|
||||
|
||||
let totp = await activateTOTP(test, page, users.user1);
|
||||
|
||||
await logUser(test, page, users.user1, { totp });
|
||||
|
||||
await disableTOTP(test, page, users.user1);
|
||||
});
|
||||
|
||||
test('Non SSO login impossible', async ({ page, browser }, testInfo: TestInfo) => {
|
||||
await utils.restartVault(page, testInfo, {
|
||||
SSO_ENABLED: true,
|
||||
SSO_ONLY: true
|
||||
}, false);
|
||||
|
||||
// Landing page
|
||||
await page.goto('/');
|
||||
await page.getByLabel(/Email address/).fill(users.user1.email);
|
||||
|
||||
// Check that SSO login is available
|
||||
await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(1);
|
||||
|
||||
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.restartVault(page, testInfo, {
|
||||
SSO_ENABLED: false
|
||||
}, false);
|
||||
|
||||
// Landing page
|
||||
await page.goto('/');
|
||||
await page.getByLabel(/Email address/).fill(users.user1.email);
|
||||
|
||||
// No SSO button (rely on a correct selector checked in previous test)
|
||||
await page.getByLabel('Master password');
|
||||
await expect(page.getByRole('button', { name: /Use single sign-on/ })).toHaveCount(0);
|
||||
|
||||
// Can continue to Master password
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
await expect(page.getByRole('button', { name: /Log in with master password/ })).toHaveCount(1);
|
||||
});
|
121
playwright/tests/sso_organization.smtp.spec.ts
Normal file
121
playwright/tests/sso_organization.smtp.spec.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { test, expect, type TestInfo } from '@playwright/test';
|
||||
import { MailDev } from 'maildev';
|
||||
|
||||
import * as utils from "../global-utils";
|
||||
import * as orgs from './setups/orgs';
|
||||
import { logNewUser, logUser } from './setups/sso';
|
||||
|
||||
let users = utils.loadEnv();
|
||||
|
||||
let mailServer, mail1Buffer, mail2Buffer, mail3Buffer;
|
||||
|
||||
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.startVault(browser, testInfo, {
|
||||
SMTP_HOST: process.env.MAILDEV_HOST,
|
||||
SMTP_FROM: process.env.PW_SMTP_FROM,
|
||||
SSO_ENABLED: true,
|
||||
SSO_ONLY: true,
|
||||
});
|
||||
|
||||
mail1Buffer = mailServer.buffer(users.user1.email);
|
||||
mail2Buffer = mailServer.buffer(users.user2.email);
|
||||
mail3Buffer = mailServer.buffer(users.user3.email);
|
||||
});
|
||||
|
||||
test.afterAll('Teardown', async ({}) => {
|
||||
utils.stopVault();
|
||||
[mail1Buffer, mail2Buffer, mail3Buffer, mailServer].map((m) => m?.close());
|
||||
});
|
||||
|
||||
test('Create user3', async ({ page }) => {
|
||||
await logNewUser(test, page, users.user3, { mailBuffer: mail3Buffer });
|
||||
});
|
||||
|
||||
test('Invite users', async ({ page }) => {
|
||||
await logNewUser(test, page, users.user1, { mailBuffer: mail1Buffer });
|
||||
|
||||
await orgs.create(test, page, '/Test');
|
||||
await orgs.members(test, page, '/Test');
|
||||
await orgs.invite(test, page, '/Test', users.user2.email);
|
||||
await orgs.invite(test, page, '/Test', users.user3.email);
|
||||
});
|
||||
|
||||
test('invited with new account', async ({ page }) => {
|
||||
const link = await test.step('Extract email link', async () => {
|
||||
const invited = await mail2Buffer.expect((m) => m.subject === "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 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('Create Vault account', async () => {
|
||||
await expect(page.getByRole('heading', { name: 'Join organisation' })).toBeVisible();
|
||||
await page.getByLabel('New master password (required)', { exact: true }).fill(users.user2.password);
|
||||
await page.getByLabel('Confirm new master password (').fill(users.user2.password);
|
||||
await page.getByRole('button', { name: 'Create account' }).click();
|
||||
});
|
||||
|
||||
await test.step('Default vault page', async () => {
|
||||
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
||||
|
||||
await utils.checkNotification(page, 'Account successfully created!');
|
||||
await utils.checkNotification(page, 'Invitation accepted');
|
||||
});
|
||||
|
||||
await test.step('Check mails', async () => {
|
||||
await mail2Buffer.expect((m) => m.subject.includes("New Device Logged"));
|
||||
await mail1Buffer.expect((m) => m.subject === "Invitation to /Test accepted");
|
||||
});
|
||||
});
|
||||
|
||||
test('invited with existing account', async ({ page }) => {
|
||||
const link = await test.step('Extract email link', async () => {
|
||||
const invited = await mail3Buffer.expect((m) => m.subject === "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 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('Unlock vault', async () => {
|
||||
await expect(page).toHaveTitle('Vaultwarden Web');
|
||||
await page.getByLabel('Master password').fill(users.user3.password);
|
||||
await page.getByRole('button', { name: 'Unlock' }).click();
|
||||
});
|
||||
|
||||
await test.step('Default vault page', async () => {
|
||||
await expect(page).toHaveTitle(/Vaultwarden Web/);
|
||||
await utils.checkNotification(page, 'Invitation accepted');
|
||||
});
|
||||
|
||||
await test.step('Check mails', async () => {
|
||||
await mail3Buffer.expect((m) => m.subject.includes("New Device Logged"));
|
||||
await mail1Buffer.expect((m) => m.subject === "Invitation to /Test accepted");
|
||||
});
|
||||
});
|
76
playwright/tests/sso_organization.spec.ts
Normal file
76
playwright/tests/sso_organization.spec.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { test, expect, type TestInfo } from '@playwright/test';
|
||||
import { MailDev } from 'maildev';
|
||||
|
||||
import * as utils from "../global-utils";
|
||||
import * as orgs from './setups/orgs';
|
||||
import { logNewUser, logUser } from './setups/sso';
|
||||
|
||||
let users = utils.loadEnv();
|
||||
|
||||
test.beforeAll('Setup', async ({ browser }, testInfo: TestInfo) => {
|
||||
await utils.startVault(browser, testInfo, {
|
||||
SSO_ENABLED: true,
|
||||
SSO_ONLY: true,
|
||||
});
|
||||
});
|
||||
|
||||
test.afterAll('Teardown', async ({}) => {
|
||||
utils.stopVault();
|
||||
});
|
||||
|
||||
test('Create user3', async ({ page }) => {
|
||||
await logNewUser(test, page, users.user3);
|
||||
});
|
||||
|
||||
test('Invite users', async ({ page }) => {
|
||||
await logNewUser(test, page, users.user1);
|
||||
|
||||
await orgs.create(test, page, '/Test');
|
||||
await orgs.members(test, page, '/Test');
|
||||
await orgs.invite(test, page, '/Test', users.user2.email);
|
||||
await orgs.invite(test, page, '/Test', users.user3.email);
|
||||
await orgs.confirm(test, page, '/Test', users.user3.email);
|
||||
});
|
||||
|
||||
test('Create invited account', async ({ page }) => {
|
||||
await logNewUser(test, page, users.user2);
|
||||
});
|
||||
|
||||
test('Confirm invited user', async ({ page }) => {
|
||||
await logUser(test, page, users.user1);
|
||||
await orgs.members(test, page, '/Test');
|
||||
await expect(page.getByRole('row', { name: users.user2.name })).toHaveText(/Needs confirmation/);
|
||||
await orgs.confirm(test, page, '/Test', users.user2.email);
|
||||
});
|
||||
|
||||
test('Organization is visible', async ({ page }) => {
|
||||
await logUser(test, page, users.user2);
|
||||
await page.getByLabel('vault: /Test').click();
|
||||
await expect(page.getByLabel('Filter: Default collection')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Enforce password policy', async ({ page }) => {
|
||||
await logUser(test, page, users.user1);
|
||||
await orgs.policies(test, page, '/Test');
|
||||
|
||||
await test.step(`Set master password policy`, async () => {
|
||||
await page.getByRole('button', { name: 'Master password requirements' }).click();
|
||||
await page.getByRole('checkbox', { name: 'Turn on' }).check();
|
||||
await page.getByRole('checkbox', { name: 'Require existing members to' }).check();
|
||||
await page.getByRole('spinbutton', { name: 'Minimum length' }).fill('42');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await utils.checkNotification(page, 'Edited policy Master password requirements.');
|
||||
});
|
||||
|
||||
await utils.logout(test, page, users.user1);
|
||||
|
||||
await test.step(`Unlock trigger policy`, async () => {
|
||||
await page.getByRole('textbox', { name: 'Email address (required)' }).fill(users.user1.email);
|
||||
await page.getByRole('button', { name: 'Use single sign-on' }).click();
|
||||
|
||||
await page.getByRole('textbox', { name: 'Master password (required)' }).fill(users.user1.password);
|
||||
await page.getByRole('button', { name: 'Unlock' }).click();
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Update master password' })).toBeVisible();
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user