acceptance

This commit is contained in:
Elio Bischof
2025-06-23 09:40:11 +02:00
parent 2dba67292a
commit 2a0fd5f9ac
50 changed files with 336 additions and 237 deletions

4
.gitignore vendored
View File

@@ -7,16 +7,12 @@ dist
dist-ssr dist-ssr
*.local *.local
.env .env
.cache
server/dist server/dist
public/dist public/dist
.vscode .vscode
.idea .idea
.vercel .vercel
.env*.local .env*.local
/test-results/
/playwright-report/
/blob-report/ /blob-report/
/playwright/.cache/
/out /out
/docker /docker

View File

@@ -94,6 +94,8 @@ pnpm run-oidcop
### Testing ### Testing
To test the quality of your code, make sure
You can execute the following commands `pnpm test` for a single test run or `pnpm test:watch` in the following directories: You can execute the following commands `pnpm test` for a single test run or `pnpm test:watch` in the following directories:
- apps/login - apps/login

View File

@@ -5,6 +5,8 @@ export BAKE_CLI ?= docker buildx bake
BAKE_CLI_WITH_COMMON_ARGS := $(BAKE_CLI) --file ./docker-bake.hcl --file ./apps/login-test-acceptance/docker-compose.yaml BAKE_CLI_WITH_COMMON_ARGS := $(BAKE_CLI) --file ./docker-bake.hcl --file ./apps/login-test-acceptance/docker-compose.yaml
export COMPOSE_BAKE=true export COMPOSE_BAKE=true
export UID := $(id -u)
export GID := $(id -g)
export LOGIN_TEST_ACCEPTANCE_BUILD_CONTEXT := apps/login-test-acceptance export LOGIN_TEST_ACCEPTANCE_BUILD_CONTEXT := apps/login-test-acceptance
@@ -47,7 +49,7 @@ login-test-unit:
login-test-integration-build: login-test-integration-build:
$(BAKE_CLI_WITH_COMMON_ARGS) core-mock login-test-integration login-standalone $(BAKE_CLI_WITH_COMMON_ARGS) core-mock login-test-integration login-standalone
login-test-integration-dev: login-test-integration-dev: login-test-integration-cleanup
$(BAKE_CLI_WITH_COMMON_ARGS) core-mock && docker compose --file ./apps/login-test-integration/docker-compose.yaml run --service-ports --rm core-mock $(BAKE_CLI_WITH_COMMON_ARGS) core-mock && docker compose --file ./apps/login-test-integration/docker-compose.yaml run --service-ports --rm core-mock
login-test-integration-run: login-test-integration-cleanup login-test-integration-run: login-test-integration-cleanup
@@ -72,11 +74,14 @@ login-test-acceptance-build-compose:
login-test-acceptance-build: login-test-acceptance-build-compose login-test-acceptance-build-bake login-test-acceptance-build: login-test-acceptance-build-compose login-test-acceptance-build-bake
@: @:
login-test-acceptance-dev: login-test-acceptance-build-compose login-test-acceptance-cleanup
docker compose --file ./apps/login-test-acceptance/docker-compose.yaml up zitadel setup traefik setup sink
login-test-acceptance-run: login-test-acceptance-cleanup login-test-acceptance-run: login-test-acceptance-cleanup
docker compose --file ./apps/login-test-acceptance/docker-compose.yaml run --rm --service-ports acceptance docker compose --file ./apps/login-test-acceptance/docker-compose.yaml --file ./apps/login-test-acceptance/docker-compose-ci.yaml run --rm --service-ports acceptance
login-test-acceptance-cleanup: login-test-acceptance-cleanup:
docker compose --file ./apps/login-test-acceptance/docker-compose.yaml down --volumes docker compose --file ./apps/login-test-acceptance/docker-compose.yaml --file ./apps/login-test-acceptance/docker-compose-ci.yaml down --volumes
login-test-acceptance: login-test-acceptance-build login-test-acceptance: login-test-acceptance-build
./scripts/run_or_skip.sh login-test-acceptance-run \ ./scripts/run_or_skip.sh login-test-acceptance-run \

View File

@@ -1,3 +1 @@
go-command go-command
.env.local
test-results

View File

@@ -0,0 +1,57 @@
services:
zitadel:
environment:
ZITADEL_EXTERNALDOMAIN: traefik
traefik:
labels: !reset []
setup:
environment:
WRITE_ENVIRONMENT_FILE: /login-env/.env
ZITADEL_API_DOMAIN: traefik
ZITADEL_API_URL: https://traefik
LOGIN_BASE_URL: https://traefik/ui/v2/login/
SINK_NOTIFICATION_URL: http://sink:3333/notification
ZITADEL_ADMIN_USER: zitadel-admin@zitadel.traefik
login:
image: "${LOGIN_TAG:-login:local}"
container_name: acceptance-login
labels:
- "traefik.enable=true"
- "traefik.http.routers.login.rule=PathPrefix(`/ui/v2/login`)"
ports:
- "3000:3000"
environment:
- NODE_TLS_REJECT_UNAUTHORIZED=0
depends_on:
setup:
condition: service_completed_successfully
acceptance:
user: "${UID:-1000}:${GID:-1000}"
image: "${LOGIN_TEST_ACCEPTANCE_TAG:-login-test-acceptance:local}"
container_name: acceptance
environment:
- CI
- LOGIN_BASE_URL=https://traefik/ui/v2/login/
- NODE_TLS_REJECT_UNAUTHORIZED=0
ports:
- 9323:9323
ipc: "host"
init: true
depends_on:
login:
condition: "service_healthy"
sink:
condition: service_healthy
# oidcrp:
# condition: service_healthy
# oidcop:
# condition: service_healthy
# samlsp:
# condition: service_healthy
# samlidp:
# condition: service_healthy

View File

@@ -1,14 +0,0 @@
services:
traefik:
extra_hosts:
- host.docker.internal:host-gateway
setup:
environment:
LOGIN_BASE_URL: https://localhost/ui/v2/login/
ZITADEL_API_INTERNAL_URL: http://zitadel:8080
ZITADEL_API_URL: https://localhost
ZITADEL_API_DOMAIN: localhost
volumes:
- pat:/pat # Read the PAT file from zitadels setup
- ./env:/acceptance-env # Write the environment variables file for the tests
- ../login:/login-env # Write the environment variables file for the login

View File

@@ -1,6 +1,7 @@
services: services:
zitadel: zitadel:
user: "root" user: "${UID:-1000}:${GID:-1000}"
image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:latest}" image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:latest}"
container_name: acceptance-zitadel container_name: acceptance-zitadel
pull_policy: always pull_policy: always
@@ -8,12 +9,14 @@ services:
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.zitadel.rule=!PathPrefix(`/ui/v2/login`)" - "traefik.http.routers.zitadel.rule=!PathPrefix(`/ui/v2/login`)"
# - "traefik.http.middlewares.zitadel.headers.customrequestheaders.Host=localhost"
# - "traefik.http.routers.zitadel.middlewares=zitadel@docker"
- "traefik.http.services.zitadel-service.loadbalancer.server.scheme=h2c" - "traefik.http.services.zitadel-service.loadbalancer.server.scheme=h2c"
- "traefik.http.middlewares.zitadel-headers.headers.customrequestheaders.Host=zitadel" - "traefik.http.services.zitadel-service.loadbalancer.passHostHeader=false"
ports: ports:
- "8080:8080" - "8080:8080"
volumes: volumes:
- pat:/pat - ./pat:/pat
- ./zitadel.yaml:/zitadel.yaml - ./zitadel.yaml:/zitadel.yaml
depends_on: depends_on:
db: db:
@@ -48,13 +51,16 @@ services:
traefik: traefik:
image: "traefik:v3.4" image: "traefik:v3.4"
container_name: "acceptance-traefik" container_name: "acceptance-traefik"
labels:
- "traefik.enable=true"
- "traefik.http.routers.login.rule=PathPrefix(`/ui/v2/login`)"
- "traefik.http.services.login-service.loadbalancer.server.url=http://host.docker.internal:3000"
command: command:
- "--log.level=DEBUG" - "--log.level=DEBUG"
- "--ping" - "--ping"
- "--api.insecure=true" - "--api.insecure=true"
- "--providers.docker=true" - "--providers.docker=true"
- "--providers.docker.exposedbydefault=false" - "--providers.docker.exposedbydefault=false"
- "--entryPoints.web.address=:80"
- "--entrypoints.websecure.http.tls=true" - "--entrypoints.websecure.http.tls=true"
- "--entryPoints.websecure.address=:443" - "--entryPoints.websecure.address=:443"
healthcheck: healthcheck:
@@ -67,51 +73,39 @@ services:
- "443:443" - "443:443"
volumes: volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro" - "/var/run/docker.sock:/var/run/docker.sock:ro"
depends_on: extra_hosts:
wait-for-zitadel: - host.docker.internal:host-gateway
condition: "service_completed_successfully"
setup: setup:
user: "${UID:-1000}:${GID:-1000}"
image: ${LOGIN_TEST_ACCEPTANCE_SETUP_TAG:-login-test-acceptance-setup:local} image: ${LOGIN_TEST_ACCEPTANCE_SETUP_TAG:-login-test-acceptance-setup:local}
container_name: acceptance-setup container_name: acceptance-setup
restart: no
build: build:
context: "${LOGIN_TEST_ACCEPTANCE_BUILD_CONTEXT:-.}/setup" context: "${LOGIN_TEST_ACCEPTANCE_BUILD_CONTEXT:-.}/setup"
dockerfile: ../go-command.Dockerfile dockerfile: ../go-command.Dockerfile
entrypoint: "./setup.sh" entrypoint: "./setup.sh"
environment: environment:
PAT_FILE: /pat/zitadel-admin-sa.pat PAT_FILE: /pat/zitadel-admin-sa.pat
LOGIN_BASE_URL: https://traefik/ui/v2/login/ ZITADEL_API_INTERNAL_URL: http://zitadel:8080
ZITADEL_API_INTERNAL_URL: http://traefik WRITE_ENVIRONMENT_FILE: /login-env/.env.local
WRITE_ENVIRONMENT_FILE: /login-env/.env
WRITE_TEST_ENVIRONMENT_FILE: /acceptance-env/.env WRITE_TEST_ENVIRONMENT_FILE: /acceptance-env/.env
SINK_EMAIL_INTERNAL_URL: http://sink:3333/email SINK_EMAIL_INTERNAL_URL: http://sink:3333/email
SINK_SMS_INTERNAL_URL: http://sink:3333/sms SINK_SMS_INTERNAL_URL: http://sink:3333/sms
SINK_NOTIFICATION_URL: http://sink:3333/notification SINK_NOTIFICATION_URL: http://localhost:3333/notification
ZITADEL_API_DOMAIN: traefik LOGIN_BASE_URL: https://localhost/ui/v2/login/
ZITADEL_API_URL: https://traefik ZITADEL_API_URL: https://localhost
ZITADEL_API_DOMAIN: localhost
ZITADEL_ADMIN_USER: zitadel-admin@zitadel.localhost
volumes: volumes:
- "pat:/pat" # Read the PAT file from zitadels setup - ./pat:/pat # Read the PAT file from zitadels setup
- "acceptance-env:/acceptance-env" # Write the environment variables file for the tests - ./env:/acceptance-env # Write the environment variables file for the tests
- "login-env:/login-env" # Write the environment variables file for the login - ../login:/login-env # Write the environment variables file for the login
depends_on: depends_on:
traefik: traefik:
condition: "service_healthy" condition: "service_healthy"
wait-for-zitadel:
login: condition: "service_completed_successfully"
image: "${LOGIN_TAG:-login:local}"
container_name: acceptance-login
labels:
- "traefik.enable=true"
- "traefik.http.routers.login.rule=PathPrefix(`/ui/v2/login`)"
ports:
- "3000:3000"
volumes:
- "login-env:/.env-file/"
environment:
- NODE_TLS_REJECT_UNAUTHORIZED=0
depends_on:
setup:
condition: service_completed_successfully
sink: sink:
image: ${LOGIN_TEST_ACCEPTANCE_SINK_TAG:-login-test-acceptance-sink:local} image: ${LOGIN_TEST_ACCEPTANCE_SINK_TAG:-login-test-acceptance-sink:local}
@@ -139,6 +133,7 @@ services:
condition: "service_completed_successfully" condition: "service_completed_successfully"
oidcrp: oidcrp:
user: "${UID:-1000}:${GID:-1000}"
image: ${LOGIN_TEST_ACCEPTANCE_OIDCRP_TAG:-login-test-acceptance-oidcrp:local} image: ${LOGIN_TEST_ACCEPTANCE_OIDCRP_TAG:-login-test-acceptance-oidcrp:local}
container_name: acceptance-oidcrp container_name: acceptance-oidcrp
build: build:
@@ -158,14 +153,15 @@ services:
ports: ports:
- "8000:8000" - "8000:8000"
volumes: volumes:
- "pat:/pat" - "./pat:/pat"
depends_on: depends_on:
traefik: traefik:
condition: "service_healthy" condition: "service_healthy"
login: setup:
condition: "service_healthy" condition: "service_completed_successfully"
oidcop: oidcop:
user: "${UID:-1000}:${GID:-1000}"
image: ${LOGIN_TEST_ACCEPTANCE_OIDCOP_TAG:-login-test-acceptance-oidcop:local} image: ${LOGIN_TEST_ACCEPTANCE_OIDCOP_TAG:-login-test-acceptance-oidcop:local}
container_name: acceptance-oidcop container_name: acceptance-oidcop
build: build:
@@ -183,14 +179,15 @@ services:
ports: ports:
- 8004:8004 - 8004:8004
volumes: volumes:
- "pat:/pat" - "./pat:/pat"
depends_on: depends_on:
traefik: traefik:
condition: "service_healthy" condition: "service_healthy"
login: setup:
condition: "service_healthy" condition: "service_completed_successfully"
samlsp: samlsp:
user: "${UID:-1000}:${GID:-1000}"
image: "${LOGIN_TEST_ACCEPTANCE_SAMLSP_TAG:-login-test-acceptance-samlsp:local}" image: "${LOGIN_TEST_ACCEPTANCE_SAMLSP_TAG:-login-test-acceptance-samlsp:local}"
container_name: acceptance-samlsp container_name: acceptance-samlsp
build: build:
@@ -203,18 +200,21 @@ services:
API_DOMAIN: 'traefik' API_DOMAIN: 'traefik'
PAT_FILE: '/pat/zitadel-admin-sa.pat' PAT_FILE: '/pat/zitadel-admin-sa.pat'
LOGIN_URL: 'https://traefik/ui/v2/login' LOGIN_URL: 'https://traefik/ui/v2/login'
IDP_URL: 'http://traefik/saml/v2/metadata' IDP_URL: 'http://zitadel:8080/saml/v2/metadata'
HOST: 'https://traefik' HOST: 'https://traefik'
PORT: '8001' PORT: '8001'
ports: ports:
- 8001:8001 - 8001:8001
volumes: volumes:
- "pat:/pat" - "./pat:/pat"
depends_on: depends_on:
traefik: traefik:
condition: "service_healthy" condition: "service_healthy"
setup:
condition: "service_completed_successfully"
samlidp: samlidp:
user: "${UID:-1000}:${GID:-1000}"
image: "${LOGIN_TEST_ACCEPTANCE_SAMLIDP_TAG:-login-test-acceptance-samlidp:local}" image: "${LOGIN_TEST_ACCEPTANCE_SAMLIDP_TAG:-login-test-acceptance-samlidp:local}"
container_name: acceptance-samlidp container_name: acceptance-samlidp
build: build:
@@ -232,41 +232,9 @@ services:
ports: ports:
- 8003:8003 - 8003:8003
volumes: volumes:
- "pat:/pat" - "./pat:/pat"
depends_on: depends_on:
traefik: traefik:
condition: "service_healthy" condition: "service_healthy"
setup:
acceptance: condition: "service_completed_successfully"
image: "${LOGIN_TEST_ACCEPTANCE_TAG:-login-test-acceptance:local}"
container_name: acceptance
environment:
- CI
- LOGIN_BASE_URL=https://traefik/ui/v2/login/
- NODE_TLS_REJECT_UNAUTHORIZED=0
volumes:
- "acceptance-env:/build/apps/login-test-acceptance/.env-file/"
- "pat:/pat"
- "./test-results:/build/apps/login-test-acceptance/test-results"
ports:
- 9323:9323
ipc: "host"
init: true
depends_on:
login:
condition: "service_healthy"
sink:
condition: service_healthy
# oidcrp:
# condition: service_healthy
# oidcop:
# condition: service_healthy
# samlsp:
# condition: service_healthy
# samlidp:
# condition: service_healthy
volumes:
pat:
login-env:
acceptance-env:

View File

@@ -0,0 +1,2 @@
*
!.gitkeep

View File

View File

@@ -3,14 +3,15 @@
"private": true, "private": true,
"scripts": { "scripts": {
"test:acceptance": "pnpm exec playwright", "test:acceptance": "pnpm exec playwright",
"test:acceptance:setup": "pnpm exec playwright" "test:acceptance:setup": "cd ../.. && make login-test-acceptance-dev"
}, },
"devDependencies": { "devDependencies": {
"@otplib/core": "^12.0.0",
"@otplib/plugin-thirty-two": "^12.0.0",
"@otplib/plugin-crypto": "^12.0.0",
"@faker-js/faker": "^9.7.0", "@faker-js/faker": "^9.7.0",
"@otplib/core": "^12.0.0",
"@otplib/plugin-crypto": "^12.0.0",
"@otplib/plugin-thirty-two": "^12.0.0",
"@playwright/test": "^1.52.0", "@playwright/test": "^1.52.0",
"gaxios": "^7.1.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -0,0 +1,2 @@
*
!.gitkeep

View File

@@ -0,0 +1,2 @@
*
!.gitkeep

View File

@@ -1,12 +1,8 @@
import { defineConfig, devices } from "@playwright/test"; import { defineConfig, devices } from "@playwright/test";
import dotenv from "dotenv";
import path from "path";
/** dotenv.config({ path: path.resolve(__dirname, "./env/.env") });
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/** /**
* See https://playwright.dev/docs/test-configuration. * See https://playwright.dev/docs/test-configuration.

View File

@@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
set -e set -ex pipefail
PAT_FILE=${PAT_FILE:-./pat/zitadel-admin-sa.pat} PAT_FILE=${PAT_FILE:-./pat/zitadel-admin-sa.pat}
LOGIN_BASE_URL=${LOGIN_BASE_URL:-"http://localhost:3000"} LOGIN_BASE_URL=${LOGIN_BASE_URL:-"http://localhost:3000"}
@@ -68,6 +68,9 @@ SINK_NOTIFICATION_URL=${SINK_NOTIFICATION_URL}
EMAIL_VERIFICATION=true EMAIL_VERIFICATION=true
DEBUG=false DEBUG=false
LOGIN_BASE_URL=${LOGIN_BASE_URL} LOGIN_BASE_URL=${LOGIN_BASE_URL}
NODE_TLS_REJECT_UNAUTHORIZED=0
ZITADEL_ADMIN_USER=${ZITADEL_ADMIN_USER:-"zitadel-admin@zitadel.localhost"}
NEXT_PUBLIC_BASE_PATH=/ui/v2/login
" | tee "${WRITE_ENVIRONMENT_FILE}" "${WRITE_TEST_ENVIRONMENT_FILE}" > /dev/null " | tee "${WRITE_ENVIRONMENT_FILE}" "${WRITE_TEST_ENVIRONMENT_FILE}" > /dev/null
echo "Wrote environment file ${WRITE_ENVIRONMENT_FILE}" echo "Wrote environment file ${WRITE_ENVIRONMENT_FILE}"

View File

@@ -84,12 +84,17 @@ func main() {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
msg, ok := messages[response.Recipient]
serializableData, err := json.Marshal(messages[response.Recipient]) if !ok {
http.Error(w, "No messages found for recipient: "+response.Recipient, http.StatusNotFound)
return
}
serializableData, err := json.Marshal(msg)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
w.Header().Set("Content-Type", "application/json")
io.WriteString(w, string(serializableData)) io.WriteString(w, string(serializableData))
}) })

View File

@@ -0,0 +1,2 @@
*
!.gitkeep

View File

@@ -2,6 +2,6 @@ import { test } from "@playwright/test";
import { loginScreenExpect, loginWithPassword } from "./login"; import { loginScreenExpect, loginWithPassword } from "./login";
test("admin login", async ({ page }) => { test("admin login", async ({ page }) => {
await loginWithPassword(page, "zitadel-admin@zitadel.traefik", "Password1!"); await loginWithPassword(page, process.env["ZITADEL_ADMIN_USER"], "Password1!");
await loginScreenExpect(page, "ZITADEL Admin"); await loginScreenExpect(page, "ZITADEL Admin");
}); });

View File

@@ -3,8 +3,6 @@ import { codeScreen } from "./code-screen";
import { getOtpFromSink } from "./sink"; import { getOtpFromSink } from "./sink";
export async function otpFromSink(page: Page, key: string) { export async function otpFromSink(page: Page, key: string) {
// wait for send of the code
await page.waitForTimeout(10000);
const c = await getOtpFromSink(key); const c = await getOtpFromSink(key);
await code(page, c); await code(page, c);
} }

View File

@@ -9,7 +9,7 @@ import { getCodeFromSink } from "./sink";
import { PasswordUser } from "./user"; import { PasswordUser } from "./user";
// Read from ".env" file. // Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../.env-file/.env") }); dotenv.config({ path: path.resolve(__dirname, "../env/.env") });
const test = base.extend<{ user: PasswordUser }>({ const test = base.extend<{ user: PasswordUser }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {
@@ -32,11 +32,10 @@ const test = base.extend<{ user: PasswordUser }>({
test("user email not verified, verify", async ({ user, page }) => { test("user email not verified, verify", async ({ user, page }) => {
await loginWithPassword(page, user.getUsername(), user.getPassword()); await loginWithPassword(page, user.getUsername(), user.getPassword());
// auto-redirect on /verify
// wait for send of the code
await page.waitForTimeout(10000);
const c = await getCodeFromSink(user.getUsername()); const c = await getCodeFromSink(user.getUsername());
await emailVerify(page, c); await emailVerify(page, c);
// wait for resend of the code
await page.waitForTimeout(2000);
await loginScreenExpect(page, user.getFullName()); await loginScreenExpect(page, user.getFullName());
}); });
@@ -44,22 +43,18 @@ test("user email not verified, resend, verify", async ({ user, page }) => {
await loginWithPassword(page, user.getUsername(), user.getPassword()); await loginWithPassword(page, user.getUsername(), user.getPassword());
// auto-redirect on /verify // auto-redirect on /verify
await emailVerifyResend(page); await emailVerifyResend(page);
// wait for send of the code
await page.waitForTimeout(10000);
const c = await getCodeFromSink(user.getUsername()); const c = await getCodeFromSink(user.getUsername());
await emailVerify(page, c); // wait for resend of the code
await page.waitForTimeout(2000); await emailVerify(page, c);
await loginScreenExpect(page, user.getFullName()); await loginScreenExpect(page, user.getFullName());
}); });
test("user email not verified, resend, old code", async ({ user, page }) => { test("user email not verified, resend, old code", async ({ user, page }) => {
await loginWithPassword(page, user.getUsername(), user.getPassword()); await loginWithPassword(page, user.getUsername(), user.getPassword());
// auto-redirect on /verify
// wait for send of the code
await page.waitForTimeout(10000);
const c = await getCodeFromSink(user.getUsername()); const c = await getCodeFromSink(user.getUsername());
await emailVerifyResend(page); await emailVerifyResend(page);
// wait for resend of the code // wait for resend of the code
await page.waitForTimeout(10000); await page.waitForTimeout(2000);
await emailVerify(page, c); await emailVerify(page, c);
await emailVerifyScreenExpect(page, c); await emailVerifyScreenExpect(page, c);
}); });

View File

@@ -1,13 +1,9 @@
import { expect, Page } from "@playwright/test"; import { expect, Page } from "@playwright/test";
import dotenv from "dotenv";
import path from "path";
import { code, otpFromSink } from "./code"; import { code, otpFromSink } from "./code";
import { loginname } from "./loginname"; import { loginname } from "./loginname";
import { password } from "./password"; import { password } from "./password";
import { totp } from "./zitadel"; import { totp } from "./zitadel";
dotenv.config({ path: path.resolve(__dirname, "../.env-file/.env") });
export async function startLogin(page: Page) { export async function startLogin(page: Page) {
await page.goto(`./loginname`); await page.goto(`./loginname`);
} }

View File

@@ -3,7 +3,6 @@ import { getCodeFromSink } from "./sink";
const codeField = "code-text-input"; const codeField = "code-text-input";
const passwordField = "password-text-input"; const passwordField = "password-text-input";
const passwordConfirmField = "password-confirm-text-input";
const passwordChangeField = "password-change-text-input"; const passwordChangeField = "password-change-text-input";
const passwordChangeConfirmField = "password-change-confirm-text-input"; const passwordChangeConfirmField = "password-change-confirm-text-input";
const passwordSetField = "password-set-text-input"; const passwordSetField = "password-set-text-input";
@@ -75,8 +74,6 @@ async function checkContent(page: Page, testid: string, match: boolean) {
} }
export async function resetPasswordScreen(page: Page, username: string, password1: string, password2: string) { export async function resetPasswordScreen(page: Page, username: string, password1: string, password2: string) {
// wait for send of the code
await page.waitForTimeout(10000);
const c = await getCodeFromSink(username); const c = await getCodeFromSink(username);
await page.getByTestId(codeField).pressSequentially(c); await page.getByTestId(codeField).pressSequentially(c);
await page.getByTestId(passwordSetField).pressSequentially(password1); await page.getByTestId(passwordSetField).pressSequentially(password1);

View File

@@ -7,7 +7,7 @@ import { registerWithPasskey, registerWithPassword } from "./register";
import { removeUserByUsername } from "./zitadel"; import { removeUserByUsername } from "./zitadel";
// Read from ".env" file. // Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../env-file/.env") }); dotenv.config({ path: path.resolve(__dirname, "../env/.env") });
test("register with password", async ({ page }) => { test("register with password", async ({ page }) => {
const username = faker.internet.email(); const username = faker.internet.email();

View File

@@ -17,8 +17,6 @@ export async function registerWithPassword(
await page.getByTestId("submit-button").click(); await page.getByTestId("submit-button").click();
await registerPasswordScreen(page, password1, password2); await registerPasswordScreen(page, password1, password2);
await page.getByTestId("submit-button").click(); await page.getByTestId("submit-button").click();
await page.waitForTimeout(10000);
await verifyEmail(page, email); await verifyEmail(page, email);
} }
@@ -36,7 +34,6 @@ export async function registerWithPasskey(page: Page, firstname: string, lastnam
} }
async function verifyEmail(page: Page, email: string) { async function verifyEmail(page: Page, email: string) {
await page.waitForTimeout(10000);
const c = await getCodeFromSink(email); const c = await getCodeFromSink(email);
await emailVerify(page, c); await emailVerify(page, c);
} }

View File

@@ -1,55 +1,43 @@
import axios from "axios"; import {Gaxios, GaxiosResponse} from 'gaxios';
export async function getOtpFromSink(key: string): Promise<any> { const awaitNotification = new Gaxios({
try { url: process.env.SINK_NOTIFICATION_URL,
const response = await axios.post( method: 'POST',
process.env.SINK_NOTIFICATION_URL!, retryConfig: {
{ httpMethodsToRetry: ['POST'],
recipient: key, statusCodesToRetry: [[404, 404]],
}, retry: Number.MAX_SAFE_INTEGER, // totalTimeout limits the number of retries
{ totalTimeout: 10000, // 10 seconds
headers: { onRetryAttempt: (error) => {
"Content-Type": "application/json", console.warn(`Retrying request to sink notification service: ${error.message}`);
Authorization: `Bearer ${process.env.ZITADEL_SERVICE_USER_TOKEN}`, }
}, }
}, });
);
if (response.status >= 400) { export async function getOtpFromSink(recipient: string): Promise<any> {
const error = `HTTP Error: ${response.status} - ${response.statusText}`; return awaitNotification.request({data: {recipient}}).then((response) => {
console.error(error); expectSuccess(response);
throw new Error(error); const otp = response?.data?.args?.otp
} if (!otp) {
return response.data.args.otp; throw new Error(`Response does not contain an otp property: ${JSON.stringify(response.data, null, 2)}`);
} catch (error) {
console.error("Error making request:", error);
throw error;
} }
return otp;
})
} }
export async function getCodeFromSink(key: string): Promise<any> { export async function getCodeFromSink(recipient: string): Promise<any> {
try { return awaitNotification.request({data: {recipient}}).then((response) => {
const response = await axios.post( expectSuccess(response);
process.env.SINK_NOTIFICATION_URL!, const code = response?.data?.args?.code
{ if (!code) {
recipient: key, throw new Error(`Response does not contain a code property: ${JSON.stringify(response.data, null, 2)}`);
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.ZITADEL_SERVICE_USER_TOKEN}`,
},
},
);
if (response.status >= 400) {
const error = `HTTP Error: ${response.status} - ${response.statusText}`;
console.error(error);
throw new Error(error);
} }
return response.data.args.code; return code;
} catch (error) { })
console.error("Error making request:", error); }
throw error;
function expectSuccess(response: GaxiosResponse): void {
if (response.status !== 200) {
throw new Error(`Expected HTTP status 200, but got: ${response.status} - ${response.statusText}`);
} }
} }

View File

@@ -1,6 +1,7 @@
import { Page } from "@playwright/test"; import { Page } from "@playwright/test";
import { registerWithPasskey } from "./register"; import { registerWithPasskey } from "./register";
import { activateOTP, addTOTP, addUser, getUserByUsername, removeUser } from "./zitadel"; import {activateOTP, addTOTP, addUser, eventualNewUser, getUserByUsername, removeUser} from "./zitadel";
import {request} from 'gaxios';
export interface userProps { export interface userProps {
email: string; email: string;
@@ -68,8 +69,7 @@ class User {
export class PasswordUser extends User { export class PasswordUser extends User {
async ensure(page: Page) { async ensure(page: Page) {
await super.ensure(page); await super.ensure(page);
// wait for projection of user await eventualNewUser(this.getUserId());
await page.waitForTimeout(10000);
} }
} }
@@ -111,11 +111,8 @@ export class PasswordUserWithOTP extends User {
async ensure(page: Page) { async ensure(page: Page) {
await super.ensure(page); await super.ensure(page);
await activateOTP(this.getUserId(), this.type); await activateOTP(this.getUserId(), this.type);
await eventualNewUser(this.getUserId())
// wait for projection of user
await page.waitForTimeout(10000);
} }
} }
@@ -124,11 +121,8 @@ export class PasswordUserWithTOTP extends User {
async ensure(page: Page) { async ensure(page: Page) {
await super.ensure(page); await super.ensure(page);
this.secret = await addTOTP(this.getUserId()); this.secret = await addTOTP(this.getUserId());
await eventualNewUser(this.getUserId())
// wait for projection of user
await page.waitForTimeout(10000);
} }
public getSecret(): string { public getSecret(): string {

View File

@@ -6,7 +6,7 @@ import { loginScreenExpect, loginWithPasskey } from "./login";
import { PasskeyUser } from "./user"; import { PasskeyUser } from "./user";
// Read from ".env" file. // Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../env-file/.env") }); dotenv.config({ path: path.resolve(__dirname, "../env/.env") });
const test = base.extend<{ user: PasskeyUser }>({ const test = base.extend<{ user: PasskeyUser }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {

View File

@@ -7,7 +7,7 @@ import { changePassword } from "./password";
import { PasswordUser } from "./user"; import { PasswordUser } from "./user";
// Read from ".env" file. // Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../env-file/.env") }); dotenv.config({ path: path.resolve(__dirname, "../env/.env") });
const test = base.extend<{ user: PasswordUser }>({ const test = base.extend<{ user: PasswordUser }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {

View File

@@ -8,7 +8,7 @@ import { changePasswordScreen, changePasswordScreenExpect } from "./password-scr
import { PasswordUser } from "./user"; import { PasswordUser } from "./user";
// Read from ".env" file. // Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../env-file/.env") }); dotenv.config({ path: path.resolve(__dirname, "../env/.env") });
const test = base.extend<{ user: PasswordUser }>({ const test = base.extend<{ user: PasswordUser }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {

View File

@@ -8,7 +8,7 @@ import { loginScreenExpect, loginWithPassword, loginWithPasswordAndEmailOTP } fr
import { OtpType, PasswordUserWithOTP } from "./user"; import { OtpType, PasswordUserWithOTP } from "./user";
// Read from ".env" file. // Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../env-file/.env") }); dotenv.config({ path: path.resolve(__dirname, "../env/.env") });
const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({ const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {

View File

@@ -8,7 +8,7 @@ import { loginScreenExpect, loginWithPassword, loginWithPasswordAndPhoneOTP } fr
import { OtpType, PasswordUserWithOTP } from "./user"; import { OtpType, PasswordUserWithOTP } from "./user";
// Read from ".env" file. // Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../env-file/.env") }); dotenv.config({ path: path.resolve(__dirname, "../env/.env") });
const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({ const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {

View File

@@ -9,7 +9,7 @@ import { resetPasswordScreen, resetPasswordScreenExpect } from "./password-scree
import { PasswordUser } from "./user"; import { PasswordUser } from "./user";
// Read from ".env" file. // Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../env-file/.env") }); dotenv.config({ path: path.resolve(__dirname, "../env/.env") });
const test = base.extend<{ user: PasswordUser }>({ const test = base.extend<{ user: PasswordUser }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {

View File

@@ -8,7 +8,7 @@ import { loginScreenExpect, loginWithPassword, loginWithPasswordAndTOTP } from "
import { PasswordUserWithTOTP } from "./user"; import { PasswordUserWithTOTP } from "./user";
// Read from ".env" file. // Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../env-file/.env") }); dotenv.config({ path: path.resolve(__dirname, "../env/.env") });
const test = base.extend<{ user: PasswordUserWithTOTP; sink: any }>({ const test = base.extend<{ user: PasswordUserWithTOTP; sink: any }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {

View File

@@ -10,7 +10,7 @@ import { passwordScreenExpect } from "./password-screen";
import { PasswordUser } from "./user"; import { PasswordUser } from "./user";
// Read from ".env" file. // Read from ".env" file.
dotenv.config({ path: path.resolve(__dirname, "../env-file/.env") }); dotenv.config({ path: path.resolve(__dirname, "../env/.env") });
const test = base.extend<{ user: PasswordUser }>({ const test = base.extend<{ user: PasswordUser }>({
user: async ({ page }, use) => { user: async ({ page }, use) => {

View File

@@ -5,8 +5,9 @@ import axios from "axios";
import dotenv from "dotenv"; import dotenv from "dotenv";
import path from "path"; import path from "path";
import { OtpType, userProps } from "./user"; import { OtpType, userProps } from "./user";
import {request} from "gaxios";
dotenv.config({ path: path.resolve(__dirname, "../.env-file/.env") }); dotenv.config({ path: path.resolve(__dirname, "../env/.env") })
export async function addUser(props: userProps) { export async function addUser(props: userProps) {
const body = { const body = {
@@ -168,3 +169,22 @@ export function totp(secret: string) {
return token; return token;
} }
export async function eventualNewUser(id: string) {
return request({
url: `${process.env.ZITADEL_API_URL}/v2/users/${id}`,
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
'Content-Type': 'application/json',
},
retryConfig: {
statusCodesToRetry: [[404, 404]],
retry: Number.MAX_SAFE_INTEGER, // totalTimeout limits the number of retries
totalTimeout: 10000, // 10 seconds
onRetryAttempt: (error) => {
console.warn(`Retrying to query new user ${id}: ${error.message}`);
}
}
})
}

View File

@@ -0,0 +1,11 @@
{
"extends": ["//"],
"tasks": {
"test:acceptance:setup": {
"interactive": true,
"cache": false,
"persistent": true,
"with": ["@zitadel/login#dev"]
}
}
}

View File

@@ -1,4 +1,4 @@
ExternalDomain: zitadel ExternalDomain: localhost
ExternalSecure: true ExternalSecure: true
ExternalPort: 443 ExternalPort: 443

View File

@@ -93,7 +93,7 @@ describe("verify invite", () => {
stub("zitadel.user.v2.UserService", "VerifyInviteCode"); stub("zitadel.user.v2.UserService", "VerifyInviteCode");
cy.visit("/verify?userId=221394658884845598&code=abc&invite=true"); cy.visit("/verify?userId=221394658884845598&code=abc&invite=true");
cy.location("pathname", { timeout: 10_000 }).should("eq", "/ui/v2/login/authenticator/set"); cy.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl +"/authenticator/set");
}); });
it("shows an error if invite code validation failed", () => { it("shows an error if invite code validation failed", () => {

View File

@@ -95,7 +95,7 @@ describe("login", () => {
}); });
it("should redirect a user with password authentication to /password", () => { it("should redirect a user with password authentication to /password", () => {
cy.visit("/loginname?loginName=john%40zitadel.com&submit=true"); cy.visit("/loginname?loginName=john%40zitadel.com&submit=true");
cy.location("pathname", { timeout: 10_000 }).should("eq", "/ui/v2/login/password"); cy.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl +"/password");
}); });
describe("with passkey prompt", () => { describe("with passkey prompt", () => {
beforeEach(() => { beforeEach(() => {
@@ -166,7 +166,7 @@ describe("login", () => {
it("should redirect a user with passwordless authentication to /passkey", () => { it("should redirect a user with passwordless authentication to /passkey", () => {
cy.visit("/loginname?loginName=john%40zitadel.com&submit=true"); cy.visit("/loginname?loginName=john%40zitadel.com&submit=true");
cy.location("pathname", { timeout: 10_000 }).should("eq", "/ui/v2/login/passkey"); cy.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl +"/passkey");
}); });
}); });
}); });

View File

@@ -68,6 +68,6 @@ describe("register", () => {
cy.get('input[type="checkbox"][value="privacypolicy"]').check(); cy.get('input[type="checkbox"][value="privacypolicy"]').check();
cy.get('input[type="checkbox"][value="tos"]').check(); cy.get('input[type="checkbox"][value="tos"]').check();
cy.get('button[type="submit"]').click(); cy.get('button[type="submit"]').click();
cy.location("pathname", { timeout: 10_000 }).should("eq", "/ui/v2/login/passkey/set"); cy.url({ timeout: 10_000 }).should("include", Cypress.config().baseUrl +"/passkey/set");
}); });
}); });

View File

@@ -2,13 +2,8 @@
"name": "login-test-integration", "name": "login-test-integration",
"private": true, "private": true,
"scripts": { "scripts": {
"test:integration": "pnpm exec concurrently --names 'mock,test' --success command-test --kill-others 'pnpm:mock' 'env-cmd -f ./.env.integration start-server-and-test start http://localhost:3000 \"test:integration:run\"'", "test:integration": "pnpm exec cypress",
"test:integration:watch:run": "pnpm exec concurrently --names 'mock,test' --kill-others 'pnpm:mock' 'env-cmd -f ./.env.integration start-server-and-test dev http://localhost:3000 \"pnpm nodemon -e js,jsx,ts,tsx,css,scss --ignore \\\"__test__/**\\\" --exec \\\"pnpm test:integration:run\\\"\"'", "test:integration:setup": "cd ../.. && make login-test-integration-dev"
"test:integration:watch:open": "pnpm exec concurrently --names 'mock,test' --kill-others 'pnpm:mock' 'env-cmd -f ./.env.integration start-server-and-test dev http://localhost:3000 \"pnpm nodemon -e js,jsx,ts,tsx,css,scss --ignore \\\"__test__/**\\\" --exec \\\"pnpm test:integration:open\\\"\"'",
"test:integration:run": "pnpm exec cypress run --quiet",
"test:integration:open": "pnpm exec cypress open",
"mock": "make login-test-integration-build-dev",
"mock:stop": "docker compose down core-mock"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.14.1", "@types/node": "^22.14.1",

View File

@@ -1,11 +1,11 @@
{ {
"extends": ["//"], "extends": ["//"],
"tasks": { "tasks": {
"test:integration": { "test:integration:setup": {
"dependsOn": ["@zitadel/client#build"] "interactive": true,
}, "cache": false,
"test:integration:run": { "persistent": true,
"dependsOn": ["@zitadel/client#build"] "with": ["@zitadel/login#dev"]
} }
} }
} }

View File

@@ -5,4 +5,4 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
cd apps/login-test-acceptance && \ cd apps/login-test-acceptance && \
pnpm exec playwright install --with-deps chromium pnpm exec playwright install --with-deps chromium
COPY ./apps/login-test-acceptance ./apps/login-test-acceptance COPY ./apps/login-test-acceptance ./apps/login-test-acceptance
CMD ["bash", "-c", "cd apps/login-test-acceptance && pnpm test:acceptance"] CMD ["bash", "-c", "cd apps/login-test-acceptance && pnpm test:acceptance test"]

View File

@@ -5,7 +5,7 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
FROM cypress/factory:5.10.0 AS login-test-integration FROM cypress/factory:5.10.0 AS login-test-integration
WORKDIR /opt/app WORKDIR /opt/app
COPY --from=login-test-integration-dependencies /build/apps/login-test-integration . COPY --from=login-test-integration-dependencies /build/apps/login-test-integration .
COPY ./apps/login-test-integration .
RUN npm install cypress RUN npm install cypress
RUN npx cypress install RUN npx cypress install
COPY ./apps/login-test-integration .
CMD ["npx", "cypress", "run"] CMD ["npx", "cypress", "run"]

View File

@@ -1,6 +1,8 @@
* *
!/apps/login-test-integration/*.json
!/apps/login-test-integration/*.ts !/apps/login-test-integration
!/apps/login-test-integration/integration
!/apps/login-test-integration/fixtures **/*.md
!/apps/login-test-integration/support **/*.png
**/node_modules
**/.turbo

View File

@@ -13,10 +13,10 @@
"start": "pnpm exec turbo run start", "start": "pnpm exec turbo run start",
"start:built": "pnpm exec turbo run start:built", "start:built": "pnpm exec turbo run start:built",
"test:unit": "pnpm exec turbo run test:unit -- --passWithNoTests", "test:unit": "pnpm exec turbo run test:unit -- --passWithNoTests",
"test:unit:standalone": "pnpm exec turbo run test:unit:standalone -- --passWithNoTests", "test:integration:setup": "dotenv -e ./apps/login-test-integration/.env pnpm exec turbo run test:integration:setup",
"test:integration": "pnpm exec turbo run test:integration", "test:integration": "cd apps/login-test-integration && dotenv -e ./.env pnpm test:integration",
"test:integration:run": "pnpm exec turbo run test:integration:run", "test:acceptance:setup": "pnpm exec turbo run test:acceptance:setup",
"test:acceptance": "pnpm exec turbo run test:acceptance", "test:acceptance": "cd apps/login-test-acceptance && dotenv -e ./env/.env pnpm test:acceptance",
"test:watch": "pnpm exec turbo run test:watch", "test:watch": "pnpm exec turbo run test:watch",
"dev": "pnpm exec turbo run dev --no-cache --continue", "dev": "pnpm exec turbo run dev --no-cache --continue",
"lint": "pnpm exec turbo run lint", "lint": "pnpm exec turbo run lint",
@@ -36,11 +36,12 @@
"devDependencies": { "devDependencies": {
"@changesets/cli": "^2.29.2", "@changesets/cli": "^2.29.2",
"@vitejs/plugin-react": "^4.4.1", "@vitejs/plugin-react": "^4.4.1",
"@zitadel/eslint-config": "workspace:*",
"@zitadel/prettier-config": "workspace:*", "@zitadel/prettier-config": "workspace:*",
"axios": "^1.8.4", "axios": "^1.8.4",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"dotenv-cli": "^8.0.0",
"eslint": "8.57.1", "eslint": "8.57.1",
"@zitadel/eslint-config": "workspace:*",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-organize-imports": "^4.1.0", "prettier-plugin-organize-imports": "^4.1.0",
"tsup": "^8.4.0", "tsup": "^8.4.0",

81
pnpm-lock.yaml generated
View File

@@ -29,6 +29,9 @@ importers:
dotenv: dotenv:
specifier: ^16.5.0 specifier: ^16.5.0
version: 16.5.0 version: 16.5.0
dotenv-cli:
specifier: ^8.0.0
version: 8.0.0
eslint: eslint:
specifier: 8.57.1 specifier: 8.57.1
version: 8.57.1 version: 8.57.1
@@ -219,6 +222,9 @@ importers:
'@playwright/test': '@playwright/test':
specifier: ^1.52.0 specifier: ^1.52.0
version: 1.52.0 version: 1.52.0
gaxios:
specifier: ^7.1.0
version: 7.1.0
typescript: typescript:
specifier: ^5.8.3 specifier: ^5.8.3
version: 5.8.3 version: 5.8.3
@@ -2063,6 +2069,10 @@ packages:
resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==}
engines: {node: '>=0.10'} engines: {node: '>=0.10'}
data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'}
data-urls@5.0.0: data-urls@5.0.0:
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -2186,6 +2196,14 @@ packages:
dom-accessibility-api@0.6.3: dom-accessibility-api@0.6.3:
resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
dotenv-cli@8.0.0:
resolution: {integrity: sha512-aLqYbK7xKOiTMIRf1lDPbI+Y+Ip/wo5k3eyp6ePysVaSqbyxjyK3dK35BTxG+rmd7djf5q2UPs4noPNH+cj0Qw==}
hasBin: true
dotenv-expand@10.0.0:
resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==}
engines: {node: '>=12'}
dotenv@16.0.3: dotenv@16.0.3:
resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -2539,6 +2557,10 @@ packages:
picomatch: picomatch:
optional: true optional: true
fetch-blob@3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
fflate@0.8.2: fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
@@ -2596,6 +2618,10 @@ packages:
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'}
fraction.js@4.3.7: fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
@@ -2646,6 +2672,10 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
deprecated: This package is no longer supported. deprecated: This package is no longer supported.
gaxios@7.1.0:
resolution: {integrity: sha512-y1Q0MX1Ba6eg67Zz92kW0MHHhdtWksYckQy1KJsI6P4UlDQ8cvdvpLEPslD/k7vFkdPppMESFGTvk7XpSiKj8g==}
engines: {node: '>=18'}
gensync@1.0.0-beta.2: gensync@1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -3415,6 +3445,11 @@ packages:
node-addon-api@7.1.1: node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
deprecated: Use your platform's native DOMException instead
node-fetch@2.7.0: node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0} engines: {node: 4.x || >=6.0.0}
@@ -3424,6 +3459,10 @@ packages:
encoding: encoding:
optional: true optional: true
node-fetch@3.3.2:
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
node-releases@2.0.19: node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
@@ -4642,6 +4681,10 @@ packages:
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
hasBin: true hasBin: true
web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
webidl-conversions@3.0.1: webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@@ -6560,6 +6603,8 @@ snapshots:
dependencies: dependencies:
assert-plus: 1.0.0 assert-plus: 1.0.0
data-uri-to-buffer@4.0.1: {}
data-urls@5.0.0: data-urls@5.0.0:
dependencies: dependencies:
whatwg-mimetype: 4.0.0 whatwg-mimetype: 4.0.0
@@ -6683,6 +6728,15 @@ snapshots:
dom-accessibility-api@0.6.3: {} dom-accessibility-api@0.6.3: {}
dotenv-cli@8.0.0:
dependencies:
cross-spawn: 7.0.6
dotenv: 16.5.0
dotenv-expand: 10.0.0
minimist: 1.2.8
dotenv-expand@10.0.0: {}
dotenv@16.0.3: {} dotenv@16.0.3: {}
dotenv@16.5.0: {} dotenv@16.5.0: {}
@@ -7232,6 +7286,11 @@ snapshots:
optionalDependencies: optionalDependencies:
picomatch: 4.0.2 picomatch: 4.0.2
fetch-blob@3.2.0:
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
fflate@0.8.2: {} fflate@0.8.2: {}
figures@3.2.0: figures@3.2.0:
@@ -7291,6 +7350,10 @@ snapshots:
es-set-tostringtag: 2.1.0 es-set-tostringtag: 2.1.0
mime-types: 2.1.35 mime-types: 2.1.35
formdata-polyfill@4.0.10:
dependencies:
fetch-blob: 3.2.0
fraction.js@4.3.7: {} fraction.js@4.3.7: {}
from@0.1.7: {} from@0.1.7: {}
@@ -7349,6 +7412,14 @@ snapshots:
strip-ansi: 6.0.1 strip-ansi: 6.0.1
wide-align: 1.1.5 wide-align: 1.1.5
gaxios@7.1.0:
dependencies:
extend: 3.0.2
https-proxy-agent: 7.0.6
node-fetch: 3.3.2
transitivePeerDependencies:
- supports-color
gensync@1.0.0-beta.2: {} gensync@1.0.0-beta.2: {}
get-caller-file@2.0.5: {} get-caller-file@2.0.5: {}
@@ -8120,10 +8191,18 @@ snapshots:
node-addon-api@7.1.1: node-addon-api@7.1.1:
optional: true optional: true
node-domexception@1.0.0: {}
node-fetch@2.7.0: node-fetch@2.7.0:
dependencies: dependencies:
whatwg-url: 5.0.0 whatwg-url: 5.0.0
node-fetch@3.3.2:
dependencies:
data-uri-to-buffer: 4.0.1
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
node-releases@2.0.19: {} node-releases@2.0.19: {}
nodemon@3.1.9: nodemon@3.1.9:
@@ -9356,6 +9435,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- debug - debug
web-streams-polyfill@3.3.3: {}
webidl-conversions@3.0.1: {} webidl-conversions@3.0.1: {}
webidl-conversions@4.0.2: {} webidl-conversions@4.0.2: {}

View File

@@ -27,9 +27,8 @@
"start:built": {}, "start:built": {},
"test:unit": {}, "test:unit": {},
"test:unit:standalone": {}, "test:unit:standalone": {},
"test:integration": {}, "test:integration:setup": {},
"test:integration:run": {}, "test:acceptance:setup": {},
"test:acceptance": {},
"test:watch": { "test:watch": {
"persistent": true "persistent": true
}, },