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
*.local
.env
.cache
server/dist
public/dist
.vscode
.idea
.vercel
.env*.local
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/out
/docker

View File

@@ -94,6 +94,8 @@ pnpm run-oidcop
### 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:
- 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
export COMPOSE_BAKE=true
export UID := $(id -u)
export GID := $(id -g)
export LOGIN_TEST_ACCEPTANCE_BUILD_CONTEXT := apps/login-test-acceptance
@@ -47,7 +49,7 @@ login-test-unit:
login-test-integration-build:
$(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
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-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
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:
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
./scripts/run_or_skip.sh login-test-acceptance-run \

View File

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

View File

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

View File

View File

@@ -3,14 +3,15 @@
"private": true,
"scripts": {
"test:acceptance": "pnpm exec playwright",
"test:acceptance:setup": "pnpm exec playwright"
"test:acceptance:setup": "cd ../.. && make login-test-acceptance-dev"
},
"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",
"@otplib/core": "^12.0.0",
"@otplib/plugin-crypto": "^12.0.0",
"@otplib/plugin-thirty-two": "^12.0.0",
"@playwright/test": "^1.52.0",
"gaxios": "^7.1.0",
"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 dotenv from "dotenv";
import path from "path";
/**
* 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') });
dotenv.config({ path: path.resolve(__dirname, "./env/.env") });
/**
* See https://playwright.dev/docs/test-configuration.

View File

@@ -1,6 +1,6 @@
#!/bin/sh
set -e
set -ex pipefail
PAT_FILE=${PAT_FILE:-./pat/zitadel-admin-sa.pat}
LOGIN_BASE_URL=${LOGIN_BASE_URL:-"http://localhost:3000"}
@@ -68,6 +68,9 @@ SINK_NOTIFICATION_URL=${SINK_NOTIFICATION_URL}
EMAIL_VERIFICATION=true
DEBUG=false
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
echo "Wrote environment file ${WRITE_ENVIRONMENT_FILE}"

View File

@@ -84,12 +84,17 @@ func main() {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
serializableData, err := json.Marshal(messages[response.Recipient])
msg, ok := 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 {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
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";
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");
});

View File

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

View File

@@ -9,7 +9,7 @@ import { getCodeFromSink } from "./sink";
import { PasswordUser } from "./user";
// 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 }>({
user: async ({ page }, use) => {
@@ -32,11 +32,10 @@ const test = base.extend<{ user: PasswordUser }>({
test("user email not verified, verify", async ({ user, page }) => {
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());
await emailVerify(page, c);
// wait for resend of the code
await page.waitForTimeout(2000);
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());
// auto-redirect on /verify
await emailVerifyResend(page);
// wait for send of the code
await page.waitForTimeout(10000);
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());
});
test("user email not verified, resend, old code", async ({ user, page }) => {
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());
await emailVerifyResend(page);
// wait for resend of the code
await page.waitForTimeout(10000);
await page.waitForTimeout(2000);
await emailVerify(page, c);
await emailVerifyScreenExpect(page, c);
});

View File

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

View File

@@ -3,7 +3,6 @@ import { getCodeFromSink } from "./sink";
const codeField = "code-text-input";
const passwordField = "password-text-input";
const passwordConfirmField = "password-confirm-text-input";
const passwordChangeField = "password-change-text-input";
const passwordChangeConfirmField = "password-change-confirm-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) {
// wait for send of the code
await page.waitForTimeout(10000);
const c = await getCodeFromSink(username);
await page.getByTestId(codeField).pressSequentially(c);
await page.getByTestId(passwordSetField).pressSequentially(password1);

View File

@@ -7,7 +7,7 @@ import { registerWithPasskey, registerWithPassword } from "./register";
import { removeUserByUsername } from "./zitadel";
// 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 }) => {
const username = faker.internet.email();

View File

@@ -17,8 +17,6 @@ export async function registerWithPassword(
await page.getByTestId("submit-button").click();
await registerPasswordScreen(page, password1, password2);
await page.getByTestId("submit-button").click();
await page.waitForTimeout(10000);
await verifyEmail(page, email);
}
@@ -36,7 +34,6 @@ export async function registerWithPasskey(page: Page, firstname: string, lastnam
}
async function verifyEmail(page: Page, email: string) {
await page.waitForTimeout(10000);
const c = await getCodeFromSink(email);
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> {
try {
const response = await axios.post(
process.env.SINK_NOTIFICATION_URL!,
{
recipient: key,
},
{
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);
const awaitNotification = new Gaxios({
url: process.env.SINK_NOTIFICATION_URL,
method: 'POST',
retryConfig: {
httpMethodsToRetry: ['POST'],
statusCodesToRetry: [[404, 404]],
retry: Number.MAX_SAFE_INTEGER, // totalTimeout limits the number of retries
totalTimeout: 10000, // 10 seconds
onRetryAttempt: (error) => {
console.warn(`Retrying request to sink notification service: ${error.message}`);
}
}
return response.data.args.otp;
} catch (error) {
console.error("Error making request:", error);
throw error;
}
});
export async function getOtpFromSink(recipient: string): Promise<any> {
return awaitNotification.request({data: {recipient}}).then((response) => {
expectSuccess(response);
const otp = response?.data?.args?.otp
if (!otp) {
throw new Error(`Response does not contain an otp property: ${JSON.stringify(response.data, null, 2)}`);
}
return otp;
})
}
export async function getCodeFromSink(key: string): Promise<any> {
try {
const response = await axios.post(
process.env.SINK_NOTIFICATION_URL!,
{
recipient: key,
},
{
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);
export async function getCodeFromSink(recipient: string): Promise<any> {
return awaitNotification.request({data: {recipient}}).then((response) => {
expectSuccess(response);
const code = response?.data?.args?.code
if (!code) {
throw new Error(`Response does not contain a code property: ${JSON.stringify(response.data, null, 2)}`);
}
return response.data.args.code;
} catch (error) {
console.error("Error making request:", error);
throw error;
return code;
})
}
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 { 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 {
email: string;
@@ -68,8 +69,7 @@ class User {
export class PasswordUser extends User {
async ensure(page: Page) {
await super.ensure(page);
// wait for projection of user
await page.waitForTimeout(10000);
await eventualNewUser(this.getUserId());
}
}
@@ -111,11 +111,8 @@ export class PasswordUserWithOTP extends User {
async ensure(page: Page) {
await super.ensure(page);
await activateOTP(this.getUserId(), this.type);
// wait for projection of user
await page.waitForTimeout(10000);
await eventualNewUser(this.getUserId())
}
}
@@ -124,11 +121,8 @@ export class PasswordUserWithTOTP extends User {
async ensure(page: Page) {
await super.ensure(page);
this.secret = await addTOTP(this.getUserId());
// wait for projection of user
await page.waitForTimeout(10000);
await eventualNewUser(this.getUserId())
}
public getSecret(): string {

View File

@@ -6,7 +6,7 @@ import { loginScreenExpect, loginWithPasskey } from "./login";
import { PasskeyUser } from "./user";
// 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 }>({
user: async ({ page }, use) => {

View File

@@ -7,7 +7,7 @@ import { changePassword } from "./password";
import { PasswordUser } from "./user";
// 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 }>({
user: async ({ page }, use) => {

View File

@@ -8,7 +8,7 @@ import { changePasswordScreen, changePasswordScreenExpect } from "./password-scr
import { PasswordUser } from "./user";
// 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 }>({
user: async ({ page }, use) => {

View File

@@ -8,7 +8,7 @@ import { loginScreenExpect, loginWithPassword, loginWithPasswordAndEmailOTP } fr
import { OtpType, PasswordUserWithOTP } from "./user";
// 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 }>({
user: async ({ page }, use) => {

View File

@@ -8,7 +8,7 @@ import { loginScreenExpect, loginWithPassword, loginWithPasswordAndPhoneOTP } fr
import { OtpType, PasswordUserWithOTP } from "./user";
// 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 }>({
user: async ({ page }, use) => {

View File

@@ -9,7 +9,7 @@ import { resetPasswordScreen, resetPasswordScreenExpect } from "./password-scree
import { PasswordUser } from "./user";
// 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 }>({
user: async ({ page }, use) => {

View File

@@ -8,7 +8,7 @@ import { loginScreenExpect, loginWithPassword, loginWithPasswordAndTOTP } from "
import { PasswordUserWithTOTP } from "./user";
// 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 }>({
user: async ({ page }, use) => {

View File

@@ -10,7 +10,7 @@ import { passwordScreenExpect } from "./password-screen";
import { PasswordUser } from "./user";
// 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 }>({
user: async ({ page }, use) => {

View File

@@ -5,8 +5,9 @@ import axios from "axios";
import dotenv from "dotenv";
import path from "path";
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) {
const body = {
@@ -168,3 +169,22 @@ export function totp(secret: string) {
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
ExternalPort: 443

View File

@@ -93,7 +93,7 @@ describe("verify invite", () => {
stub("zitadel.user.v2.UserService", "VerifyInviteCode");
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", () => {

View File

@@ -95,7 +95,7 @@ describe("login", () => {
});
it("should redirect a user with password authentication to /password", () => {
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", () => {
beforeEach(() => {
@@ -166,7 +166,7 @@ describe("login", () => {
it("should redirect a user with passwordless authentication to /passkey", () => {
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="tos"]').check();
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",
"private": true,
"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: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: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"
"test:integration": "pnpm exec cypress",
"test:integration:setup": "cd ../.. && make login-test-integration-dev"
},
"devDependencies": {
"@types/node": "^22.14.1",

View File

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

View File

@@ -5,4 +5,4 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
cd apps/login-test-acceptance && \
pnpm exec playwright install --with-deps chromium
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
WORKDIR /opt/app
COPY --from=login-test-integration-dependencies /build/apps/login-test-integration .
COPY ./apps/login-test-integration .
RUN npm install cypress
RUN npx cypress install
COPY ./apps/login-test-integration .
CMD ["npx", "cypress", "run"]

View File

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

View File

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

81
pnpm-lock.yaml generated
View File

@@ -29,6 +29,9 @@ importers:
dotenv:
specifier: ^16.5.0
version: 16.5.0
dotenv-cli:
specifier: ^8.0.0
version: 8.0.0
eslint:
specifier: 8.57.1
version: 8.57.1
@@ -219,6 +222,9 @@ importers:
'@playwright/test':
specifier: ^1.52.0
version: 1.52.0
gaxios:
specifier: ^7.1.0
version: 7.1.0
typescript:
specifier: ^5.8.3
version: 5.8.3
@@ -2063,6 +2069,10 @@ packages:
resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==}
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:
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
engines: {node: '>=18'}
@@ -2186,6 +2196,14 @@ packages:
dom-accessibility-api@0.6.3:
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:
resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==}
engines: {node: '>=12'}
@@ -2539,6 +2557,10 @@ packages:
picomatch:
optional: true
fetch-blob@3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
@@ -2596,6 +2618,10 @@ packages:
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
engines: {node: '>= 6'}
formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'}
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
@@ -2646,6 +2672,10 @@ packages:
engines: {node: '>=10'}
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:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
@@ -3415,6 +3445,11 @@ packages:
node-addon-api@7.1.1:
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:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
@@ -3424,6 +3459,10 @@ packages:
encoding:
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:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
@@ -4642,6 +4681,10 @@ packages:
engines: {node: '>=12.0.0'}
hasBin: true
web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@@ -6560,6 +6603,8 @@ snapshots:
dependencies:
assert-plus: 1.0.0
data-uri-to-buffer@4.0.1: {}
data-urls@5.0.0:
dependencies:
whatwg-mimetype: 4.0.0
@@ -6683,6 +6728,15 @@ snapshots:
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.5.0: {}
@@ -7232,6 +7286,11 @@ snapshots:
optionalDependencies:
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: {}
figures@3.2.0:
@@ -7291,6 +7350,10 @@ snapshots:
es-set-tostringtag: 2.1.0
mime-types: 2.1.35
formdata-polyfill@4.0.10:
dependencies:
fetch-blob: 3.2.0
fraction.js@4.3.7: {}
from@0.1.7: {}
@@ -7349,6 +7412,14 @@ snapshots:
strip-ansi: 6.0.1
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: {}
get-caller-file@2.0.5: {}
@@ -8120,10 +8191,18 @@ snapshots:
node-addon-api@7.1.1:
optional: true
node-domexception@1.0.0: {}
node-fetch@2.7.0:
dependencies:
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: {}
nodemon@3.1.9:
@@ -9356,6 +9435,8 @@ snapshots:
transitivePeerDependencies:
- debug
web-streams-polyfill@3.3.3: {}
webidl-conversions@3.0.1: {}
webidl-conversions@4.0.2: {}

View File

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