mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-11 20:02:34 +00:00
chore: add sms otp and password set acceptance tests
This commit is contained in:
18
README.md
18
README.md
@@ -230,6 +230,24 @@ pnpm test
|
||||
|
||||
To satisfy your unique workflow requirements, check out the package.json in the root directory for more detailed scripts.
|
||||
|
||||
### Run Login UI Acceptance tests
|
||||
|
||||
To run the acceptance tests you need a running ZITADEL environment and a component which receives HTTP requests for the emails and sms's.
|
||||
This component should also be able to return the content of these notifications, as the codes and links are used in the login flows.
|
||||
There is a basic implementation in Golang available under [the sink package](./acceptance/sink).
|
||||
|
||||
To setup ZITADEL with the additional Sink container for handling the notifications:
|
||||
|
||||
```sh
|
||||
pnpm run-sink
|
||||
```
|
||||
|
||||
Then you can start the acceptance tests with:
|
||||
|
||||
```sh
|
||||
pnpm test:acceptance
|
||||
```
|
||||
|
||||
### Deploy to Vercel
|
||||
|
||||
To deploy your own version on Vercel, navigate to your instance and create a service user.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Page } from "@playwright/test";
|
||||
import { codeScreen } from "./code-screen";
|
||||
import { getCodeFromSink } from "./sink";
|
||||
import { getOtpFromSink } from "./sink";
|
||||
|
||||
export async function codeFromSink(page: Page, key: string) {
|
||||
export async function otpFromSink(page: Page, key: string) {
|
||||
// wait for send of the code
|
||||
await page.waitForTimeout(3000);
|
||||
const c = await getCodeFromSink(key);
|
||||
const c = await getOtpFromSink(key);
|
||||
await code(page, c);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, Page } from "@playwright/test";
|
||||
import { codeFromSink } from "./code";
|
||||
import { otpFromSink } from "./code";
|
||||
import { loginname } from "./loginname";
|
||||
import { password } from "./password";
|
||||
|
||||
@@ -26,10 +26,10 @@ export async function loginScreenExpect(page: Page, fullName: string) {
|
||||
|
||||
export async function loginWithPasswordAndEmailOTP(page: Page, username: string, password: string, email: string) {
|
||||
await loginWithPassword(page, username, password);
|
||||
await codeFromSink(page, email);
|
||||
await otpFromSink(page, email);
|
||||
}
|
||||
|
||||
export async function loginWithPasswordAndPhoneOTP(page: Page, username: string, password: string, phone: string) {
|
||||
await loginWithPassword(page, username, password);
|
||||
await codeFromSink(page, phone);
|
||||
await otpFromSink(page, phone);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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 lengthCheck = "length-check";
|
||||
@@ -55,3 +57,26 @@ async function checkContent(page: Page, testid: string, match: boolean) {
|
||||
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);
|
||||
}
|
||||
|
||||
export async function resetPasswordScreenExpect(
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Page } from "@playwright/test";
|
||||
import { changePasswordScreen, passwordScreen } from "./password-screen";
|
||||
import { changePasswordScreen, passwordScreen, resetPasswordScreen } from "./password-screen";
|
||||
|
||||
const passwordSubmitButton = "submit-button";
|
||||
const passwordResetButton = "reset-button";
|
||||
|
||||
export async function startChangePassword(page: Page, loginname: string) {
|
||||
await page.goto("/password/change?" + new URLSearchParams({ loginName: loginname }));
|
||||
@@ -17,3 +18,13 @@ export async function password(page: Page, password: string) {
|
||||
await passwordScreen(page, password);
|
||||
await page.getByTestId(passwordSubmitButton).click();
|
||||
}
|
||||
|
||||
export async function startResetPassword(page: Page) {
|
||||
await page.getByTestId(passwordResetButton).click();
|
||||
}
|
||||
|
||||
export async function resetPassword(page: Page, username: string, password: string) {
|
||||
await startResetPassword(page);
|
||||
await resetPasswordScreen(page, username, password, password);
|
||||
await page.getByTestId(passwordSubmitButton).click();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import axios from "axios";
|
||||
|
||||
export async function getCodeFromSink(key: string): Promise<any> {
|
||||
export async function getOtpFromSink(key: string): Promise<any> {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
process.env.SINK_NOTIFICATION_URL!,
|
||||
@@ -26,3 +26,30 @@ export async function getCodeFromSink(key: string): Promise<any> {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
return response.data.args.code;
|
||||
} catch (error) {
|
||||
console.error("Error making request:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@ class User {
|
||||
}
|
||||
|
||||
async ensure(page: Page) {
|
||||
await this.remove();
|
||||
|
||||
const body = {
|
||||
username: this.props.email,
|
||||
organization: {
|
||||
@@ -53,7 +51,7 @@ class User {
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status >= 400 && response.status !== 409) {
|
||||
if (response.status >= 400) {
|
||||
const error = `HTTP Error: ${response.status} - ${response.statusText}`;
|
||||
console.error(error);
|
||||
throw new Error(error);
|
||||
@@ -65,7 +63,7 @@ class User {
|
||||
}
|
||||
|
||||
// wait for projection of user
|
||||
await page.waitForTimeout(3000);
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
|
||||
async remove() {
|
||||
@@ -166,7 +164,7 @@ export class PasswordUserWithOTP extends User {
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status >= 400 && response.status !== 409) {
|
||||
if (response.status >= 400) {
|
||||
const error = `HTTP Error: ${response.status} - ${response.statusText}`;
|
||||
console.error(error);
|
||||
throw new Error(error);
|
||||
@@ -204,7 +202,6 @@ export class PasskeyUser extends User {
|
||||
}
|
||||
|
||||
public async ensure(page: Page) {
|
||||
await this.remove();
|
||||
const authId = await registerWithPasskey(page, this.getFirstname(), this.getLastname(), this.getUsername());
|
||||
this.authenticatorId = authId;
|
||||
|
||||
@@ -212,10 +209,6 @@ export class PasskeyUser extends User {
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
|
||||
public async remove() {
|
||||
await super.remove();
|
||||
}
|
||||
|
||||
public getAuthenticatorId(): string {
|
||||
return this.authenticatorId;
|
||||
}
|
||||
|
||||
@@ -27,15 +27,6 @@ test("username and passkey login", async ({ user, page }) => {
|
||||
await loginScreenExpect(page, user.getFullName());
|
||||
});
|
||||
|
||||
test("username and passkey login, if passkey enabled", async ({ page }) => {
|
||||
// Given passkey is enabled on the organization of the user
|
||||
// Given the user has only passkey enabled as authentication
|
||||
// enter username
|
||||
// passkey popup is directly shown
|
||||
// user verifies passkey
|
||||
// user is redirected to app
|
||||
});
|
||||
|
||||
test("username and passkey login, multiple auth methods", async ({ page }) => {
|
||||
// Given passkey and password is enabled on the organization of the user
|
||||
// Given the user has password and passkey registered
|
||||
|
||||
@@ -42,7 +42,7 @@ test("username and password changed login", async ({ user, page }) => {
|
||||
*/
|
||||
});
|
||||
|
||||
test("password not with desired complexity", async ({ user, page }) => {
|
||||
test("password change not with desired complexity", async ({ user, page }) => {
|
||||
const changedPw1 = "change";
|
||||
const changedPw2 = "chang";
|
||||
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||
|
||||
@@ -2,7 +2,7 @@ import { faker } from "@faker-js/faker";
|
||||
import { test as base } from "@playwright/test";
|
||||
import dotenv from "dotenv";
|
||||
import path from "path";
|
||||
import { code, codeFromSink, codeResend } from "./code";
|
||||
import { code, codeResend, otpFromSink } from "./code";
|
||||
import { codeScreenExpect } from "./code-screen";
|
||||
import { loginScreenExpect, loginWithPassword, loginWithPasswordAndEmailOTP } from "./login";
|
||||
import { OtpType, PasswordUserWithOTP } from "./user";
|
||||
@@ -61,7 +61,7 @@ test("username, password and email otp login, resend code", async ({ user, page
|
||||
// User is redirected to the app (default redirect url)
|
||||
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||
await codeResend(page);
|
||||
await codeFromSink(page, user.getUsername());
|
||||
await otpFromSink(page, user.getUsername());
|
||||
await loginScreenExpect(page, user.getFullName());
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ 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, loginWithPasswordAndPhoneOTP } from "./login";
|
||||
import { OtpType, PasswordUserWithOTP } from "./user";
|
||||
|
||||
// Read from ".env" file.
|
||||
@@ -14,7 +17,7 @@ const test = base.extend<{ user: PasswordUserWithOTP; sink: any }>({
|
||||
firstName: faker.person.firstName(),
|
||||
lastName: faker.person.lastName(),
|
||||
organization: "",
|
||||
phone: faker.phone.number(),
|
||||
phone: faker.phone.number({ style: "international" }),
|
||||
password: "Password1!",
|
||||
type: OtpType.sms,
|
||||
});
|
||||
@@ -32,10 +35,8 @@ test("username, password and sms otp login, enter code manually", async ({ user,
|
||||
// User receives a sms with a verification code
|
||||
// User enters the code into the ui
|
||||
// User is redirected to the app (default redirect url)
|
||||
/* TODO fix on login, that sms is sent
|
||||
await loginWithPasswordAndPhoneOTP(page, user.getUsername(), user.getPassword(), user.getPhone());
|
||||
await loginScreenExpect(page, user.getFullName());
|
||||
*/
|
||||
});
|
||||
|
||||
test("username, password and sms otp login, resend code", async ({ user, page }) => {
|
||||
@@ -47,11 +48,9 @@ test("username, password and sms otp login, resend code", async ({ user, page })
|
||||
// User clicks resend code
|
||||
// User receives a new sms with a verification code
|
||||
// User is redirected to the app (default redirect url)
|
||||
/* TODO fix on login, that sms is sent
|
||||
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||
await loginWithPasswordAndPhoneOTP(page, user.getUsername(), user.getPassword(), user.getPhone());
|
||||
await loginScreenExpect(page, user.getFullName());
|
||||
*/
|
||||
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||
await loginWithPasswordAndPhoneOTP(page, user.getUsername(), user.getPassword(), user.getPhone());
|
||||
await loginScreenExpect(page, user.getFullName());
|
||||
});
|
||||
|
||||
test("username, password and sms otp login, wrong code", async ({ user, page }) => {
|
||||
@@ -62,10 +61,8 @@ test("username, password and sms otp login, wrong code", async ({ user, page })
|
||||
// User receives a sms with a verification code
|
||||
// User enters a wrong code
|
||||
// Error message - "Invalid code" is shown
|
||||
/* TODO fix on login, that sms is sent
|
||||
const c = "wrongcode";
|
||||
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||
await code(page, c);
|
||||
await codeScreenExpect(page, c);
|
||||
*/
|
||||
const c = "wrongcode";
|
||||
await loginWithPassword(page, user.getUsername(), user.getPassword());
|
||||
await code(page, c);
|
||||
await codeScreenExpect(page, c);
|
||||
});
|
||||
|
||||
50
acceptance/tests/username-password-set.spec.ts
Normal file
50
acceptance/tests/username-password-set.spec.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
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 { 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(),
|
||||
firstName: faker.person.firstName(),
|
||||
lastName: faker.person.lastName(),
|
||||
organization: "",
|
||||
phone: faker.phone.number(),
|
||||
password: "Password1!",
|
||||
});
|
||||
await user.ensure(page);
|
||||
await use(user);
|
||||
},
|
||||
});
|
||||
|
||||
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());
|
||||
await resetPassword(page, user.getUsername(), changedPw);
|
||||
await loginScreenExpect(page, user.getFullName());
|
||||
|
||||
await loginWithPassword(page, user.getUsername(), changedPw);
|
||||
await loginScreenExpect(page, user.getFullName());
|
||||
});
|
||||
|
||||
test("password set not with desired complexity", async ({ user, page }) => {
|
||||
const changedPw1 = "change";
|
||||
const changedPw2 = "chang";
|
||||
await startLogin(page);
|
||||
await loginname(page, user.getUsername());
|
||||
await startResetPassword(page);
|
||||
await resetPasswordScreen(page, user.getUsername(), changedPw1, changedPw2);
|
||||
await resetPasswordScreenExpect(page, changedPw1, changedPw2, false, false, false, false, true, false);
|
||||
});
|
||||
@@ -77,7 +77,7 @@ export function LoginOTP({
|
||||
|
||||
if (method === "sms") {
|
||||
challenges = create(RequestChallengesSchema, {
|
||||
otpSms: { returnCode: true },
|
||||
otpSms: {},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ export function PasswordForm({
|
||||
onClick={() => resetPasswordAndContinue()}
|
||||
type="button"
|
||||
disabled={loading}
|
||||
data-testid="reset-button"
|
||||
>
|
||||
{t("verify.resetPassword")}
|
||||
</button>
|
||||
|
||||
@@ -170,11 +170,15 @@ export function SetPasswordForm({
|
||||
label="Code"
|
||||
autoComplete="one-time-code"
|
||||
error={errors.code?.message as string}
|
||||
data-testid="code-text-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="ml-4 mb-1">
|
||||
<Button variant={ButtonVariants.Secondary}>
|
||||
<Button
|
||||
variant={ButtonVariants.Secondary}
|
||||
data-testid="resend-button"
|
||||
>
|
||||
{t("set.resend")}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -190,6 +194,7 @@ export function SetPasswordForm({
|
||||
})}
|
||||
label="New Password"
|
||||
error={errors.password?.message as string}
|
||||
data-testid="password-text-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="">
|
||||
@@ -202,6 +207,7 @@ export function SetPasswordForm({
|
||||
})}
|
||||
label="Confirm Password"
|
||||
error={errors.confirmPassword?.message as string}
|
||||
data-testid="password-confirm-text-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user