mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-11 18:02:22 +00:00
test: add verify email and password change required
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
12
acceptance/tests/email-verify-screen.ts
Normal file
12
acceptance/tests/email-verify-screen.ts
Normal file
@@ -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");
|
||||
}
|
||||
73
acceptance/tests/email-verify.spec.ts
Normal file
73
acceptance/tests/email-verify.spec.ts
Normal file
@@ -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);
|
||||
});
|
||||
16
acceptance/tests/email-verify.ts
Normal file
16
acceptance/tests/email-verify.ts
Normal file
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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<string> {
|
||||
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)
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
43
acceptance/tests/username-password-change-required.spec.ts
Normal file
43
acceptance/tests/username-password-change-required.spec.ts
Normal file
@@ -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());
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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!,
|
||||
},
|
||||
};
|
||||
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);
|
||||
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}`,
|
||||
},
|
||||
});
|
||||
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);
|
||||
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<any> {
|
||||
const listUsersBody = {
|
||||
queries: [
|
||||
{
|
||||
userNameQuery: {
|
||||
userName: username,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const listUsersBody = {
|
||||
queries: [
|
||||
{
|
||||
userNameQuery: {
|
||||
userName: username,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return await listCall(`${process.env.ZITADEL_API_URL}/v2/users`, listUsersBody);
|
||||
return await listCall(`${process.env.ZITADEL_API_URL}/v2/users`, listUsersBody);
|
||||
}
|
||||
|
||||
async function listCall(url: string, data: any): Promise<any> {
|
||||
try {
|
||||
const response = await axios.post(url, data, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${process.env.ZITADEL_SERVICE_USER_TOKEN}`,
|
||||
},
|
||||
});
|
||||
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);
|
||||
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;
|
||||
}
|
||||
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}`, {});
|
||||
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}`,
|
||||
},
|
||||
});
|
||||
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);
|
||||
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<string> {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
// 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;
|
||||
return token;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
</div>
|
||||
<div className="">
|
||||
@@ -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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -115,7 +115,7 @@ export function VerifyForm({
|
||||
{t("verify.noCodeReceived")}
|
||||
</span>
|
||||
<button
|
||||
aria-label="Resend OTP Code"
|
||||
aria-label="Resend Code"
|
||||
disabled={loading}
|
||||
type="button"
|
||||
className="ml-4 text-primary-light-500 dark:text-primary-dark-500 hover:dark:text-primary-dark-400 hover:text-primary-light-400 cursor-pointer disabled:cursor-default disabled:text-gray-400 dark:disabled:text-gray-700"
|
||||
@@ -134,11 +134,12 @@ export function VerifyForm({
|
||||
autoComplete="one-time-code"
|
||||
{...register("code", { required: "This field is required" })}
|
||||
label="Code"
|
||||
data-testid="code-text-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="py-4">
|
||||
<div className="py-4" data-testid="error">
|
||||
<Alert>{error}</Alert>
|
||||
</div>
|
||||
)}
|
||||
@@ -152,6 +153,7 @@ export function VerifyForm({
|
||||
variant={ButtonVariants.Primary}
|
||||
disabled={loading || !formState.isValid}
|
||||
onClick={handleSubmit(fcn)}
|
||||
data-testid="submit-button"
|
||||
>
|
||||
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
||||
{t("verify.submit")}
|
||||
|
||||
Reference in New Issue
Block a user