From 11c6c9ed165be8d9838d62e404507328d0871383 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Thu, 9 Jan 2025 15:42:30 +0100 Subject: [PATCH 1/3] test: add verify email and password change required --- acceptance/docker-compose.yaml | 2 +- acceptance/setup.sh | 1 + acceptance/tests/email-verify-screen.ts | 12 + acceptance/tests/email-verify.spec.ts | 73 +++++ acceptance/tests/email-verify.ts | 16 ++ acceptance/tests/password-screen.ts | 111 ++++---- acceptance/tests/password.ts | 3 +- acceptance/tests/register.ts | 54 ++-- acceptance/tests/user.ts | 13 + acceptance/tests/username-passkey.spec.ts | 2 + .../username-password-change-required.spec.ts | 43 +++ .../tests/username-password-changed.spec.ts | 65 ++--- .../tests/username-password-otp_email.spec.ts | 3 + .../tests/username-password-otp_sms.spec.ts | 3 + .../tests/username-password-set.spec.ts | 9 +- .../tests/username-password-totp.spec.ts | 109 ++++---- acceptance/tests/username-password.spec.ts | 3 + acceptance/tests/zitadel.ts | 257 +++++++++--------- .../src/components/change-password-form.tsx | 4 +- .../src/components/set-password-form.tsx | 4 +- apps/login/src/components/verify-form.tsx | 6 +- 21 files changed, 504 insertions(+), 289 deletions(-) create mode 100644 acceptance/tests/email-verify-screen.ts create mode 100644 acceptance/tests/email-verify.spec.ts create mode 100644 acceptance/tests/email-verify.ts create mode 100644 acceptance/tests/username-password-change-required.spec.ts diff --git a/acceptance/docker-compose.yaml b/acceptance/docker-compose.yaml index de699038..43bd4757 100644 --- a/acceptance/docker-compose.yaml +++ b/acceptance/docker-compose.yaml @@ -1,7 +1,7 @@ services: zitadel: user: "${ZITADEL_DEV_UID}" - image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:v2.65.0}" + image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:v2.67.1}" command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled --config /zitadel.yaml --steps /zitadel.yaml' ports: - "8080:8080" diff --git a/acceptance/setup.sh b/acceptance/setup.sh index e6277854..596c985d 100755 --- a/acceptance/setup.sh +++ b/acceptance/setup.sh @@ -40,6 +40,7 @@ echo "ZITADEL_API_URL=${ZITADEL_API_URL} ZITADEL_SERVICE_USER_ID=${ZITADEL_SERVICE_USER_ID} ZITADEL_SERVICE_USER_TOKEN=${PAT} SINK_NOTIFICATION_URL=${SINK_NOTIFICATION_URL} +EMAIL_VERIFICATION=true DEBUG=true"| tee "${WRITE_ENVIRONMENT_FILE}" "${WRITE_TEST_ENVIRONMENT_FILE}" > /dev/null echo "Wrote environment file ${WRITE_ENVIRONMENT_FILE}" cat ${WRITE_ENVIRONMENT_FILE} diff --git a/acceptance/tests/email-verify-screen.ts b/acceptance/tests/email-verify-screen.ts new file mode 100644 index 00000000..5431f1f3 --- /dev/null +++ b/acceptance/tests/email-verify-screen.ts @@ -0,0 +1,12 @@ +import {expect, Page} from "@playwright/test"; + +const codeTextInput = "code-text-input"; + +export async function emailVerifyScreen(page: Page, code: string) { + await page.getByTestId(codeTextInput).pressSequentially(code); +} + +export async function emailVerifyScreenExpect(page: Page, code: string) { + await expect(page.getByTestId(codeTextInput)).toHaveValue(code); + await expect(page.getByTestId("error").locator("div")).toContainText("Could not verify email"); +} diff --git a/acceptance/tests/email-verify.spec.ts b/acceptance/tests/email-verify.spec.ts new file mode 100644 index 00000000..fc6887db --- /dev/null +++ b/acceptance/tests/email-verify.spec.ts @@ -0,0 +1,73 @@ +import {faker} from "@faker-js/faker"; +import {test as base} from "@playwright/test"; +import dotenv from "dotenv"; +import path from "path"; +import {loginScreenExpect, loginWithPassword} from "./login"; +import {PasswordUser} from "./user"; +import {emailVerify, emailVerifyResend} from "./email-verify"; +import {emailVerifyScreenExpect} from "./email-verify-screen"; +import {getCodeFromSink} from "./sink" + +// Read from ".env" file. +dotenv.config({path: path.resolve(__dirname, ".env.local")}); + +const test = base.extend<{ user: PasswordUser }>({ + user: async ({page}, use) => { + const user = new PasswordUser({ + email: faker.internet.email(), + isEmailVerified: false, + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + organization: "", + phone: faker.phone.number(), + isPhoneVerified: false, + password: "Password1!", + passwordChangeRequired: false, + }); + await user.ensure(page); + await use(user); + await user.cleanup(); + }, +}); + +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(3000); + const c = await getCodeFromSink(user.getUsername()); + await emailVerify(page, c) + await loginScreenExpect(page, user.getFullName()); +}); + +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(3000); + const c = await getCodeFromSink(user.getUsername()); + 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(3000); + const c = await getCodeFromSink(user.getUsername()); + await emailVerifyResend(page); + // wait for resend of the code + await page.waitForTimeout(1000); + await emailVerify(page, c) + await emailVerifyScreenExpect(page, c); +}); + +test("user email not verified, wrong code", async ({user, page}) => { + await loginWithPassword(page, user.getUsername(), user.getPassword()); + // auto-redirect on /verify + const code = "wrong" + await emailVerify(page, code) + await emailVerifyScreenExpect(page, code); +}); diff --git a/acceptance/tests/email-verify.ts b/acceptance/tests/email-verify.ts new file mode 100644 index 00000000..9ef77eda --- /dev/null +++ b/acceptance/tests/email-verify.ts @@ -0,0 +1,16 @@ +import { Page } from "@playwright/test"; +import { emailVerifyScreen } from "./email-verify-screen"; +import { getOtpFromSink } from "./sink"; + +export async function startEmailVerify(page: Page, loginname: string) { + await page.goto("/verify"); +} + +export async function emailVerify(page: Page, code: string) { + await emailVerifyScreen(page, code); + await page.getByTestId("submit-button").click(); +} + +export async function emailVerifyResend(page: Page) { + await page.getByTestId("resend-button").click(); +} diff --git a/acceptance/tests/password-screen.ts b/acceptance/tests/password-screen.ts index 57334a07..6d294e36 100644 --- a/acceptance/tests/password-screen.ts +++ b/acceptance/tests/password-screen.ts @@ -1,9 +1,13 @@ -import { expect, Page } from "@playwright/test"; -import { getCodeFromSink } from "./sink"; +import {expect, Page} from "@playwright/test"; +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"; +const passwordSetConfirmField = "password-set-confirm-text-input"; const lengthCheck = "length-check"; const symbolCheck = "symbol-check"; const numberCheck = "number-check"; @@ -15,68 +19,83 @@ const matchText = "Matches"; const noMatchText = "Doesn't match"; export async function changePasswordScreen(page: Page, password1: string, password2: string) { - await page.getByTestId(passwordField).pressSequentially(password1); - await page.getByTestId(passwordConfirmField).pressSequentially(password2); + await page.getByTestId(passwordChangeField).pressSequentially(password1); + await page.getByTestId(passwordChangeConfirmField).pressSequentially(password2); } export async function passwordScreen(page: Page, password: string) { - await page.getByTestId(passwordField).pressSequentially(password); + await page.getByTestId(passwordField).pressSequentially(password); } export async function passwordScreenExpect(page: Page, password: string) { - await expect(page.getByTestId(passwordField)).toHaveValue(password); - await expect(page.getByTestId("error").locator("div")).toContainText("Could not verify password"); + await expect(page.getByTestId(passwordField)).toHaveValue(password); + await expect(page.getByTestId("error").locator("div")).toContainText("Could not verify password"); } export async function changePasswordScreenExpect( - page: Page, - password1: string, - password2: string, - length: boolean, - symbol: boolean, - number: boolean, - uppercase: boolean, - lowercase: boolean, - equals: boolean, + page: Page, + password1: string, + password2: string, + length: boolean, + symbol: boolean, + number: boolean, + uppercase: boolean, + lowercase: boolean, + equals: boolean, ) { - await expect(page.getByTestId(passwordField)).toHaveValue(password1); - await expect(page.getByTestId(passwordConfirmField)).toHaveValue(password2); + await expect(page.getByTestId(passwordChangeField)).toHaveValue(password1); + await expect(page.getByTestId(passwordChangeConfirmField)).toHaveValue(password2); - await checkContent(page, lengthCheck, length); - await checkContent(page, symbolCheck, symbol); - await checkContent(page, numberCheck, number); - await checkContent(page, uppercaseCheck, uppercase); - await checkContent(page, lowercaseCheck, lowercase); - await checkContent(page, equalCheck, equals); + await checkComplexity(page, length, symbol, number, uppercase, lowercase, equals); +} + +async function checkComplexity( + page: Page, + length: boolean, + symbol: boolean, + number: boolean, + uppercase: boolean, + lowercase: boolean, + equals: boolean, +) { + await checkContent(page, lengthCheck, length); + await checkContent(page, symbolCheck, symbol); + await checkContent(page, numberCheck, number); + await checkContent(page, uppercaseCheck, uppercase); + await checkContent(page, lowercaseCheck, lowercase); + await checkContent(page, equalCheck, equals); } async function checkContent(page: Page, testid: string, match: boolean) { - if (match) { - await expect(page.getByTestId(testid)).toContainText(matchText); - } else { - await expect(page.getByTestId(testid)).toContainText(noMatchText); - } + if (match) { + await expect(page.getByTestId(testid)).toContainText(matchText); + } else { + await expect(page.getByTestId(testid)).toContainText(noMatchText); + } } export async function resetPasswordScreen(page: Page, username: string, password1: string, password2: string) { - // wait for send of the code - await page.waitForTimeout(3000); - const c = await getCodeFromSink(username); - await page.getByTestId(codeField).pressSequentially(c); - await page.getByTestId(passwordField).pressSequentially(password1); - await page.getByTestId(passwordConfirmField).pressSequentially(password2); + // wait for send of the code + await page.waitForTimeout(3000); + const c = await getCodeFromSink(username); + await page.getByTestId(codeField).pressSequentially(c); + await page.getByTestId(passwordSetField).pressSequentially(password1); + await page.getByTestId(passwordSetConfirmField).pressSequentially(password2); } export async function resetPasswordScreenExpect( - page: Page, - password1: string, - password2: string, - length: boolean, - symbol: boolean, - number: boolean, - uppercase: boolean, - lowercase: boolean, - equals: boolean, + page: Page, + password1: string, + password2: string, + length: boolean, + symbol: boolean, + number: boolean, + uppercase: boolean, + lowercase: boolean, + equals: boolean, ) { - await changePasswordScreenExpect(page, password1, password2, length, symbol, number, uppercase, lowercase, equals); -} + await expect(page.getByTestId(passwordSetField)).toHaveValue(password1); + await expect(page.getByTestId(passwordSetConfirmField)).toHaveValue(password2); + + await checkComplexity(page, length, symbol, number, uppercase, lowercase, equals); +} \ No newline at end of file diff --git a/acceptance/tests/password.ts b/acceptance/tests/password.ts index b3d31fca..1dc304cc 100644 --- a/acceptance/tests/password.ts +++ b/acceptance/tests/password.ts @@ -8,8 +8,7 @@ export async function startChangePassword(page: Page, loginname: string) { await page.goto("/password/change?" + new URLSearchParams({ loginName: loginname })); } -export async function changePassword(page: Page, loginname: string, password: string) { - await startChangePassword(page, loginname); +export async function changePassword(page: Page, password: string) { await changePasswordScreen(page, password, password); await page.getByTestId(passwordSubmitButton).click(); } diff --git a/acceptance/tests/register.ts b/acceptance/tests/register.ts index 693bdfbc..2bc2c79e 100644 --- a/acceptance/tests/register.ts +++ b/acceptance/tests/register.ts @@ -1,29 +1,43 @@ -import { Page } from "@playwright/test"; -import { passkeyRegister } from "./passkey"; -import { registerPasswordScreen, registerUserScreenPasskey, registerUserScreenPassword } from "./register-screen"; +import {Page} from "@playwright/test"; +import {passkeyRegister} from "./passkey"; +import {registerPasswordScreen, registerUserScreenPasskey, registerUserScreenPassword} from "./register-screen"; +import {getCodeFromSink} from "./sink"; +import {emailVerify} from "./email-verify"; export async function registerWithPassword( - page: Page, - firstname: string, - lastname: string, - email: string, - password1: string, - password2: string, + page: Page, + firstname: string, + lastname: string, + email: string, + password1: string, + password2: string, ) { - await page.goto("/register"); - await registerUserScreenPassword(page, firstname, lastname, email); - await page.getByTestId("submit-button").click(); - await registerPasswordScreen(page, password1, password2); - await page.getByTestId("submit-button").click(); + await page.goto("/register"); + await registerUserScreenPassword(page, firstname, lastname, email); + await page.getByTestId("submit-button").click(); + await registerPasswordScreen(page, password1, password2); + await page.getByTestId("submit-button").click(); + await page.waitForTimeout(3000); + + await verifyEmail(page, email) } export async function registerWithPasskey(page: Page, firstname: string, lastname: string, email: string): Promise { - await page.goto("/register"); - await registerUserScreenPasskey(page, firstname, lastname, email); - await page.getByTestId("submit-button").click(); + await page.goto("/register"); + await registerUserScreenPasskey(page, firstname, lastname, email); + await page.getByTestId("submit-button").click(); - // wait for projection of user - await page.waitForTimeout(2000); + // wait for projection of user + await page.waitForTimeout(3000); + const authId = await passkeyRegister(page); - return await passkeyRegister(page); + await verifyEmail(page, email) + return authId } + + +async function verifyEmail(page: Page, email: string) { + await page.waitForTimeout(1000); + const c = await getCodeFromSink(email); + await emailVerify(page, c) +} \ No newline at end of file diff --git a/acceptance/tests/user.ts b/acceptance/tests/user.ts index 3daefdff..68a8eecd 100644 --- a/acceptance/tests/user.ts +++ b/acceptance/tests/user.ts @@ -4,11 +4,14 @@ import { activateOTP, addTOTP, addUser, getUserByUsername, removeUser } from "./ export interface userProps { email: string; + isEmailVerified?: boolean; firstName: string; lastName: string; organization: string; password: string; + passwordChangeRequired?: boolean; phone: string; + isPhoneVerified?: boolean; } class User { @@ -77,11 +80,14 @@ export enum OtpType { export interface otpUserProps { email: string; + isEmailVerified?: boolean; firstName: string; lastName: string; organization: string; password: string; + passwordChangeRequired?: boolean; phone: string; + isPhoneVerified?: boolean; type: OtpType; } @@ -96,6 +102,9 @@ export class PasswordUserWithOTP extends User { organization: props.organization, password: props.password, phone: props.phone, + isEmailVerified: props.isEmailVerified, + isPhoneVerified: props.isPhoneVerified, + passwordChangeRequired: props.passwordChangeRequired, }); this.type = props.type; } @@ -133,6 +142,8 @@ export interface passkeyUserProps { lastName: string; organization: string; phone: string; + isEmailVerified?: boolean; + isPhoneVerified?: boolean; } export class PasskeyUser extends User { @@ -146,6 +157,8 @@ export class PasskeyUser extends User { organization: props.organization, password: "", phone: props.phone, + isEmailVerified: props.isEmailVerified, + isPhoneVerified: props.isPhoneVerified, }); } diff --git a/acceptance/tests/username-passkey.spec.ts b/acceptance/tests/username-passkey.spec.ts index e73de354..54b1bf0a 100644 --- a/acceptance/tests/username-passkey.spec.ts +++ b/acceptance/tests/username-passkey.spec.ts @@ -12,10 +12,12 @@ const test = base.extend<{ user: PasskeyUser }>({ user: async ({ page }, use) => { const user = new PasskeyUser({ email: faker.internet.email(), + isEmailVerified: true, firstName: faker.person.firstName(), lastName: faker.person.lastName(), organization: "", phone: faker.phone.number(), + isPhoneVerified: false, }); await user.ensure(page); await use(user); diff --git a/acceptance/tests/username-password-change-required.spec.ts b/acceptance/tests/username-password-change-required.spec.ts new file mode 100644 index 00000000..faf00723 --- /dev/null +++ b/acceptance/tests/username-password-change-required.spec.ts @@ -0,0 +1,43 @@ +import { faker } from "@faker-js/faker"; +import { test as base } from "@playwright/test"; +import dotenv from "dotenv"; +import path from "path"; +import { loginScreenExpect, loginWithPassword, startLogin } from "./login"; +import { loginname } from "./loginname"; +import {changePassword, resetPassword, startResetPassword} from "./password"; +import { resetPasswordScreen, resetPasswordScreenExpect } from "./password-screen"; +import { PasswordUser } from "./user"; + +// Read from ".env" file. +dotenv.config({ path: path.resolve(__dirname, ".env.local") }); + +const test = base.extend<{ user: PasswordUser }>({ + user: async ({ page }, use) => { + const user = new PasswordUser({ + email: faker.internet.email(), + isEmailVerified: true, + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + organization: "", + phone: faker.phone.number(), + isPhoneVerified: false, + password: "Password1!", + passwordChangeRequired: true, + }); + await user.ensure(page); + await use(user); + await user.cleanup(); + }, +}); + +test("username and password login, change required", async ({ user, page }) => { + const changedPw = "ChangedPw1!"; + + await loginWithPassword(page, user.getUsername(), user.getPassword()); + await page.waitForTimeout(100); + await changePassword(page, changedPw); + await loginScreenExpect(page, user.getFullName()); + + await loginWithPassword(page, user.getUsername(), changedPw); + await loginScreenExpect(page, user.getFullName()); +}); diff --git a/acceptance/tests/username-password-changed.spec.ts b/acceptance/tests/username-password-changed.spec.ts index c185e51e..acbe1f91 100644 --- a/acceptance/tests/username-password-changed.spec.ts +++ b/acceptance/tests/username-password-changed.spec.ts @@ -1,53 +1,54 @@ -import { faker } from "@faker-js/faker"; -import { test as base } from "@playwright/test"; +import {faker} from "@faker-js/faker"; +import {test as base} from "@playwright/test"; import dotenv from "dotenv"; import path from "path"; -import { loginWithPassword } from "./login"; -import { startChangePassword } from "./password"; -import { changePasswordScreen, changePasswordScreenExpect } from "./password-screen"; -import { PasswordUser } from "./user"; +import {loginScreenExpect, loginWithPassword} from "./login"; +import {changePassword, startChangePassword} from "./password"; +import {changePasswordScreen, changePasswordScreenExpect} from "./password-screen"; +import {PasswordUser} from "./user"; // Read from ".env" file. -dotenv.config({ path: path.resolve(__dirname, ".env.local") }); +dotenv.config({path: path.resolve(__dirname, ".env.local")}); const test = base.extend<{ user: PasswordUser }>({ - user: async ({ page }, use) => { - const user = new PasswordUser({ - email: faker.internet.email(), - firstName: faker.person.firstName(), - lastName: faker.person.lastName(), - organization: "", - phone: faker.phone.number(), - password: "Password1!", - }); - await user.ensure(page); - await use(user); - await user.cleanup(); - }, + user: async ({page}, use) => { + const user = new PasswordUser({ + email: faker.internet.email(), + isEmailVerified: true, + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + organization: "", + phone: faker.phone.number(), + isPhoneVerified: false, + password: "Password1!", + passwordChangeRequired: false, + }); + await user.ensure(page); + await use(user); + await user.cleanup(); + }, }); -test("username and password changed login", async ({ user, page }) => { - // commented, fix in https://github.com/zitadel/zitadel/pull/8807 - /* +test("username and password changed login", async ({user, page}) => { const changedPw = "ChangedPw1!"; await loginWithPassword(page, user.getUsername(), user.getPassword()); // wait for projection of token await page.waitForTimeout(2000); - await changePassword(page, user.getUsername(), changedPw); + await startChangePassword(page, user.getUsername()); + await changePassword(page, changedPw); await loginScreenExpect(page, user.getFullName()); await loginWithPassword(page, user.getUsername(), changedPw); await loginScreenExpect(page, user.getFullName()); - */ }); -test("password change not with desired complexity", async ({ user, page }) => { - const changedPw1 = "change"; - const changedPw2 = "chang"; - await loginWithPassword(page, user.getUsername(), user.getPassword()); - await startChangePassword(page, user.getUsername()); - await changePasswordScreen(page, changedPw1, changedPw2); - await changePasswordScreenExpect(page, changedPw1, changedPw2, false, false, false, false, true, false); +test("password change not with desired complexity", async ({user, page}) => { + const changedPw1 = "change"; + const changedPw2 = "chang"; + await loginWithPassword(page, user.getUsername(), user.getPassword()); + await startChangePassword(page, user.getUsername()); + await changePasswordScreen(page, changedPw1, changedPw2); + await changePasswordScreenExpect(page, changedPw1, changedPw2, false, false, false, false, true, false); }); diff --git a/acceptance/tests/username-password-otp_email.spec.ts b/acceptance/tests/username-password-otp_email.spec.ts index daa2e0a4..d06cc878 100644 --- a/acceptance/tests/username-password-otp_email.spec.ts +++ b/acceptance/tests/username-password-otp_email.spec.ts @@ -14,11 +14,14 @@ const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({ user: async ({ page }, use) => { const user = new PasswordUserWithOTP({ email: faker.internet.email(), + isEmailVerified: true, firstName: faker.person.firstName(), lastName: faker.person.lastName(), organization: "", phone: faker.phone.number(), + isPhoneVerified: false, password: "Password1!", + passwordChangeRequired: false, type: OtpType.email, }); diff --git a/acceptance/tests/username-password-otp_sms.spec.ts b/acceptance/tests/username-password-otp_sms.spec.ts index 8fb91a66..ac69b25f 100644 --- a/acceptance/tests/username-password-otp_sms.spec.ts +++ b/acceptance/tests/username-password-otp_sms.spec.ts @@ -14,11 +14,14 @@ const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({ user: async ({ page }, use) => { const user = new PasswordUserWithOTP({ email: faker.internet.email(), + isEmailVerified: true, firstName: faker.person.firstName(), lastName: faker.person.lastName(), organization: "", phone: faker.phone.number({ style: "international" }), + isPhoneVerified: true, password: "Password1!", + passwordChangeRequired: false, type: OtpType.sms, }); diff --git a/acceptance/tests/username-password-set.spec.ts b/acceptance/tests/username-password-set.spec.ts index 30d442df..c622ab71 100644 --- a/acceptance/tests/username-password-set.spec.ts +++ b/acceptance/tests/username-password-set.spec.ts @@ -4,8 +4,8 @@ import dotenv from "dotenv"; import path from "path"; import { loginScreenExpect, loginWithPassword, startLogin } from "./login"; import { loginname } from "./loginname"; -import { resetPassword, startResetPassword } from "./password"; -import { resetPasswordScreen, resetPasswordScreenExpect } from "./password-screen"; +import {changePassword, resetPassword, startResetPassword} from "./password"; +import {changePasswordScreen, resetPasswordScreen, resetPasswordScreenExpect} from "./password-screen"; import { PasswordUser } from "./user"; // Read from ".env" file. @@ -15,11 +15,14 @@ const test = base.extend<{ user: PasswordUser }>({ user: async ({ page }, use) => { const user = new PasswordUser({ email: faker.internet.email(), + isEmailVerified: true, firstName: faker.person.firstName(), lastName: faker.person.lastName(), organization: "", phone: faker.phone.number(), + isPhoneVerified: false, password: "Password1!", + passwordChangeRequired: false, }); await user.ensure(page); await use(user); @@ -28,8 +31,6 @@ const test = base.extend<{ user: PasswordUser }>({ }); test("username and password set login", async ({ user, page }) => { - // commented, fix in https://github.com/zitadel/zitadel/pull/8807 - const changedPw = "ChangedPw1!"; await startLogin(page); await loginname(page, user.getUsername()); diff --git a/acceptance/tests/username-password-totp.spec.ts b/acceptance/tests/username-password-totp.spec.ts index 4b6e6789..2eb5ff4e 100644 --- a/acceptance/tests/username-password-totp.spec.ts +++ b/acceptance/tests/username-password-totp.spec.ts @@ -1,67 +1,70 @@ -import { faker } from "@faker-js/faker"; -import { test as base } from "@playwright/test"; +import {faker} from "@faker-js/faker"; +import {test as base} from "@playwright/test"; import dotenv from "dotenv"; import path from "path"; -import { code } from "./code"; -import { codeScreenExpect } from "./code-screen"; -import { loginScreenExpect, loginWithPassword, loginWithPasswordAndTOTP } from "./login"; -import { PasswordUserWithTOTP } from "./user"; +import {code} from "./code"; +import {codeScreenExpect} from "./code-screen"; +import {loginScreenExpect, loginWithPassword, loginWithPasswordAndTOTP} from "./login"; +import {PasswordUserWithTOTP} from "./user"; // Read from ".env" file. -dotenv.config({ path: path.resolve(__dirname, ".env.local") }); +dotenv.config({path: path.resolve(__dirname, ".env.local")}); const test = base.extend<{ user: PasswordUserWithTOTP; sink: any }>({ - user: async ({ page }, use) => { - const user = new PasswordUserWithTOTP({ - email: faker.internet.email(), - firstName: faker.person.firstName(), - lastName: faker.person.lastName(), - organization: "", - phone: faker.phone.number({ style: "international" }), - password: "Password1!", - }); + user: async ({page}, use) => { + const user = new PasswordUserWithTOTP({ + email: faker.internet.email(), + isEmailVerified: true, + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + organization: "", + phone: faker.phone.number({style: "international"}), + isPhoneVerified: true, + password: "Password1!", + passwordChangeRequired: false, + }); - await user.ensure(page); - await use(user); - await user.cleanup(); - }, + await user.ensure(page); + await use(user); + await user.cleanup(); + }, }); -test("username, password and totp login", async ({ user, page }) => { - // Given totp is enabled on the organization of the user - // Given the user has only totp configured as second factor - // User enters username - // User enters password - // Screen for entering the code is shown directly - // User enters the code into the ui - // User is redirected to the app (default redirect url) - await loginWithPasswordAndTOTP(page, user.getUsername(), user.getPassword(), user.getSecret()); - await loginScreenExpect(page, user.getFullName()); +test("username, password and totp login", async ({user, page}) => { + // Given totp is enabled on the organization of the user + // Given the user has only totp configured as second factor + // User enters username + // User enters password + // Screen for entering the code is shown directly + // User enters the code into the ui + // User is redirected to the app (default redirect url) + await loginWithPasswordAndTOTP(page, user.getUsername(), user.getPassword(), user.getSecret()); + await loginScreenExpect(page, user.getFullName()); }); -test("username, password and totp otp login, wrong code", async ({ user, page }) => { - // Given totp is enabled on the organization of the user - // Given the user has only totp configured as second factor - // User enters username - // User enters password - // Screen for entering the code is shown directly - // User enters a wrond code - // Error message - "Invalid code" is shown - const c = "wrongcode"; - await loginWithPassword(page, user.getUsername(), user.getPassword()); - await code(page, c); - await codeScreenExpect(page, c); +test("username, password and totp otp login, wrong code", async ({user, page}) => { + // Given totp is enabled on the organization of the user + // Given the user has only totp configured as second factor + // User enters username + // User enters password + // Screen for entering the code is shown directly + // User enters a wrond code + // Error message - "Invalid code" is shown + const c = "wrongcode"; + await loginWithPassword(page, user.getUsername(), user.getPassword()); + await code(page, c); + await codeScreenExpect(page, c); }); -test("username, password and totp login, multiple mfa options", async ({ page }) => { - // Given totp and email otp is enabled on the organization of the user - // Given the user has totp and email otp configured as second factor - // User enters username - // User enters password - // Screen for entering the code is shown directly - // Button to switch to email otp is shown - // User clicks button to use email otp instead - // User receives an email with a verification code - // User enters code in ui - // User is redirected to the app (default redirect url) +test("username, password and totp login, multiple mfa options", async ({page}) => { + // Given totp and email otp is enabled on the organization of the user + // Given the user has totp and email otp configured as second factor + // User enters username + // User enters password + // Screen for entering the code is shown directly + // Button to switch to email otp is shown + // User clicks button to use email otp instead + // User receives an email with a verification code + // User enters code in ui + // User is redirected to the app (default redirect url) }); diff --git a/acceptance/tests/username-password.spec.ts b/acceptance/tests/username-password.spec.ts index fcb6aad0..209c4155 100644 --- a/acceptance/tests/username-password.spec.ts +++ b/acceptance/tests/username-password.spec.ts @@ -16,11 +16,14 @@ const test = base.extend<{ user: PasswordUser }>({ user: async ({ page }, use) => { const user = new PasswordUser({ email: faker.internet.email(), + isEmailVerified: true, firstName: faker.person.firstName(), lastName: faker.person.lastName(), organization: "", phone: faker.phone.number(), + isPhoneVerified: false, password: "Password1!", + passwordChangeRequired: false, }); await user.ensure(page); await use(user); diff --git a/acceptance/tests/zitadel.ts b/acceptance/tests/zitadel.ts index 587723f1..8e153393 100644 --- a/acceptance/tests/zitadel.ts +++ b/acceptance/tests/zitadel.ts @@ -1,159 +1,166 @@ -import { Authenticator } from "@otplib/core"; -import { createDigest, createRandomBytes } from "@otplib/plugin-crypto"; -import { keyDecoder, keyEncoder } from "@otplib/plugin-thirty-two"; // use your chosen base32 plugin +import {Authenticator} from "@otplib/core"; +import {createDigest, createRandomBytes} from "@otplib/plugin-crypto"; +import {keyDecoder, keyEncoder} from "@otplib/plugin-thirty-two"; // use your chosen base32 plugin import axios from "axios"; -import { OtpType, userProps } from "./user"; +import {OtpType, userProps} from "./user"; export async function addUser(props: userProps) { - const body = { - username: props.email, - organization: { - orgId: props.organization, - }, - profile: { - givenName: props.firstName, - familyName: props.lastName, - }, - email: { - email: props.email, - isVerified: true, - }, - phone: { - phone: props.phone!, - isVerified: true, - }, - password: { - password: props.password!, - }, - }; - - return await listCall(`${process.env.ZITADEL_API_URL}/v2/users/human`, body); + const body = { + username: props.email, + organization: { + orgId: props.organization, + }, + profile: { + givenName: props.firstName, + familyName: props.lastName, + }, + email: { + email: props.email, + isVerified: true, + }, + phone: { + phone: props.phone, + isVerified: true, + }, + password: { + password: props.password, + changeRequired: props.passwordChangeRequired ?? false, + }, + }; + if (!props.isEmailVerified) { + delete body.email.isVerified; + } + if (!props.isPhoneVerified) { + delete body.phone.isVerified; + } + + return await listCall(`${process.env.ZITADEL_API_URL}/v2/users/human`, body); } export async function removeUserByUsername(username: string) { - const resp = await getUserByUsername(username); - if (!resp || !resp.result || !resp.result[0]) { - return; - } - await removeUser(resp.result[0].userId); + const resp = await getUserByUsername(username); + if (!resp || !resp.result || !resp.result[0]) { + return; + } + await removeUser(resp.result[0].userId); } export async function removeUser(id: string) { - await deleteCall(`${process.env.ZITADEL_API_URL}/v2/users/${id}`); + await deleteCall(`${process.env.ZITADEL_API_URL}/v2/users/${id}`); } async function deleteCall(url: string) { - try { - const response = await axios.delete(url, { - headers: { - Authorization: `Bearer ${process.env.ZITADEL_SERVICE_USER_TOKEN}`, - }, - }); - - if (response.status >= 400 && response.status !== 404) { - const error = `HTTP Error: ${response.status} - ${response.statusText}`; - console.error(error); - throw new Error(error); + try { + const response = await axios.delete(url, { + headers: { + Authorization: `Bearer ${process.env.ZITADEL_SERVICE_USER_TOKEN}`, + }, + }); + + if (response.status >= 400 && response.status !== 404) { + const error = `HTTP Error: ${response.status} - ${response.statusText}`; + console.error(error); + throw new Error(error); + } + } catch (error) { + console.error("Error making request:", error); + throw error; } - } catch (error) { - console.error("Error making request:", error); - throw error; - } } export async function getUserByUsername(username: string): Promise { - const listUsersBody = { - queries: [ - { - userNameQuery: { - userName: username, - }, - }, - ], - }; - - return await listCall(`${process.env.ZITADEL_API_URL}/v2/users`, listUsersBody); + const listUsersBody = { + queries: [ + { + userNameQuery: { + userName: username, + }, + }, + ], + }; + + return await listCall(`${process.env.ZITADEL_API_URL}/v2/users`, listUsersBody); } async function listCall(url: string, data: any): Promise { - try { - const response = await axios.post(url, data, { - 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); + try { + const response = await axios.post(url, data, { + 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; + } catch (error) { + console.error("Error making request:", error); + throw error; } - - return response.data; - } catch (error) { - console.error("Error making request:", error); - throw error; - } } export async function activateOTP(userId: string, type: OtpType) { - let url = "otp_"; - switch (type) { - case OtpType.sms: - url = url + "sms"; - break; - case OtpType.email: - url = url + "email"; - break; - } - - await pushCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/${url}`, {}); + let url = "otp_"; + switch (type) { + case OtpType.sms: + url = url + "sms"; + break; + case OtpType.email: + url = url + "email"; + break; + } + + await pushCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/${url}`, {}); } async function pushCall(url: string, data: any) { - try { - const response = await axios.post(url, data, { - 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); + try { + const response = await axios.post(url, data, { + 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); + } + } catch (error) { + console.error("Error making request:", error); + throw error; } - } catch (error) { - console.error("Error making request:", error); - throw error; - } } export async function addTOTP(userId: string): Promise { - const response = await listCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/totp`, {}); - const code = totp(response.secret); - await pushCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/totp/verify`, { code: code }); - return response.secret; + const response = await listCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/totp`, {}); + const code = totp(response.secret); + await pushCall(`${process.env.ZITADEL_API_URL}/v2/users/${userId}/totp/verify`, {code: code}); + return response.secret; } export function totp(secret: string) { - const authenticator = new Authenticator({ - createDigest, - createRandomBytes, - keyDecoder, - keyEncoder, - }); - // google authenticator usage - const token = authenticator.generate(secret); - - // check if token can be used - if (!authenticator.verify({ token: token, secret: secret })) { - const error = `Generated token could not be verified`; - console.error(error); - throw new Error(error); - } - - return token; + const authenticator = new Authenticator({ + createDigest, + createRandomBytes, + keyDecoder, + keyEncoder, + }); + // google authenticator usage + const token = authenticator.generate(secret); + + // check if token can be used + if (!authenticator.verify({token: token, secret: secret})) { + const error = `Generated token could not be verified`; + console.error(error); + throw new Error(error); + } + + return token; } diff --git a/apps/login/src/components/change-password-form.tsx b/apps/login/src/components/change-password-form.tsx index f8c22029..c581a40b 100644 --- a/apps/login/src/components/change-password-form.tsx +++ b/apps/login/src/components/change-password-form.tsx @@ -161,7 +161,7 @@ export function ChangePasswordForm({ })} label="New Password" error={errors.password?.message as string} - data-testid="password-text-input" + data-testid="password-change-text-input" />
@@ -174,7 +174,7 @@ export function ChangePasswordForm({ })} label="Confirm Password" error={errors.confirmPassword?.message as string} - data-testid="password-confirm-text-input" + data-testid="password-change-confirm-text-input" />
diff --git a/apps/login/src/components/set-password-form.tsx b/apps/login/src/components/set-password-form.tsx index d093d1d1..ec6cf3cc 100644 --- a/apps/login/src/components/set-password-form.tsx +++ b/apps/login/src/components/set-password-form.tsx @@ -237,7 +237,7 @@ export function SetPasswordForm({ })} label="New Password" error={errors.password?.message as string} - data-testid="password-text-input" + data-testid="password-set-text-input" />
@@ -250,7 +250,7 @@ export function SetPasswordForm({ })} label="Confirm Password" error={errors.confirmPassword?.message as string} - data-testid="password-confirm-text-input" + data-testid="password-set-confirm-text-input" />
diff --git a/apps/login/src/components/verify-form.tsx b/apps/login/src/components/verify-form.tsx index 6b618929..1982375b 100644 --- a/apps/login/src/components/verify-form.tsx +++ b/apps/login/src/components/verify-form.tsx @@ -115,7 +115,7 @@ export function VerifyForm({ {t("verify.noCodeReceived")}