8000 test: add end-to-end tests for delete own account functionality by jessicaschelly · Pull Request #36129 · RocketChat/Rocket.Chat · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content
8000

test: add end-to-end tests for delete own account functionality #36129

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Box, PasswordInput, TextInput, FieldGroup, Field, FieldRow, FieldError } from '@rocket.chat/fuselage';
import type { ChangeEvent } from 'react';
import { useState, useCallback } from 'react';
import { useState, useCallback, useId } from 'react';
import { useTranslation } from 'react-i18next';

import GenericModal from '../../../components/GenericModal';
Expand Down Expand Up @@ -38,6 +38,7 @@ const ActionConfirmModal = ({ isPassword, onConfirm, onCancel }: ActionConfirmMo
[inputText, onConfirm, onCancel, t],
);

const actionTextId = useId();
return (
<GenericModal
wrapperFunction={(props) => <Box is='form' {...props} />}
Expand All @@ -48,12 +49,14 @@ const ActionConfirmModal = ({ isPassword, onConfirm, onCancel }: ActionConfirmMo
title={t('Delete_account?')}
confirmText={t('Delete_account')}
>
<Box mb={8}>{isPassword ? t('Enter_your_password_to_delete_your_account') : t('Enter_your_username_to_delete_your_account')}</Box>
<Box mb={8} id={actionTextId}>
{isPassword ? t('Enter_your_password_to_delete_your_account') : t('Enter_your_username_to_delete_your_account')}
</Box>
<FieldGroup w='full'>
<Field>
<FieldRow>
{isPassword && <PasswordInput value={inputText} />}
{!isPassword && <TextInput value={inputText} />}
{isPassword && <PasswordInput value={inputText} aria-labelledby={actionTextId} />}
{!isPassword && <TextInput value={inputText} aria-labelledby={actionTextId} />}
</FieldRow>
<FieldError>{inputError}</FieldError>
</Field>
Expand Down
133 changes: 133 additions & 0 deletions apps/meteor/tests/e2e/delete-account.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { DEFAULT_USER_CREDENTIALS } from './config/constants';
import { AccountProfile, Registration, Utils } from './page-objects';
import { ToastBar } from './page-objects/toastBar';
import { test, expect } from './utils/test';
import { createTestUser, type ITestUser } from './utils/user-helpers';

test.describe('Delete Own Account', () => {
let poAccountProfile: AccountProfile;
let poRegistration: Registration;
let poToastBar: ToastBar;
let poUtils: Utils;
let userToDelete: ITestUser;
let userWithInvalidPassword: ITestUser;
let userWithoutPermissions: ITestUser;

test.beforeAll(async ({ api }) => {
const response = await api.post('/settings/Accounts_AllowDeleteOwnAccount', { value: true });
expect(response.status()).toBe(200);
userToDelete = await createTestUser(api, { username: 'user-to-delete' });
userWithInvalidPassword = await createTestUser(api, { username: 'user-with-invalid-password' });
userWithoutPermissions = await createTestUser(api, { username: 'user-without-permissions' });
});

test.beforeEach(async ({ page }) => {
poAccountProfile = new AccountProfile(page);
poRegistration = new Registration(page);
poToastBar = new ToastBar(page);
poUtils = new Utils(page);
await page.goto('/home');
});

test.afterAll(async ({ api }) => {
const response = await api.post('/settings/Accounts_AllowDeleteOwnAccount', { value: false });
expect(response.status()).toBe(200);
await userToDelete.delete();
await userWithInvalidPassword.delete();
await userWithoutPermissions.delete();
});

test('should not delete account when invalid password is provided', async ({ page }) => {
await test.step('login with the user to delete', async () => {
await poRegistration.username.type(userWithInvalidPassword.data.username);
await poRegistration.inputPassword.type(DEFAULT_USER_CREDENTIALS.password);
await poRegistration.btnLogin.click();
await expect(poUtils.mainContent).toBeVisible();
});

await test.step('navigate to profile and locate Delete My Account button', async () => {
await page.goto('/account/profile');
await poAccountProfile.profileTitle.waitFor({ state: 'visible' });
await poAccountProfile.btnDeleteMyAccount.click();
await expect(poAccountProfile.deleteAccountDialog).toBeVisible();
});

await test.step('verify delete confirmation dialog appears', async () => {
await expect(poAccountProfile.deleteAccountDialogMessageWithPassword).toBeVisible();
await expect(poAccountProfile.inputDeleteAccountPassword).toBeVisible();
await expect(poAccountProfile.btnDeleteAccountConfirm).toBeVisible();
await expect(poAccountProfile.btnDeleteAccountCancel).toBeVisible();
});

await test.step('enter invalid password in the confirmation field and click delete account', async () => {
await poAccountProfile.inputDeleteAccountPassword.fill('invalid-password');
await expect(poAccountProfile.inputDeleteAccountPassword).toHaveValue('invalid-password');
await poAccountProfile.btnDeleteAccountConfirm.click();
});

await test.step('verify error message appears', async () => {
await expect(poToastBar.alert).toBeVisible();
await expect(poToastBar.alert).toHaveText('Invalid password [error-invalid-password]');
});

await test.step('verify user is still on the profile page', async () => {
await expect(poAccountProfile.profileTitle).toBeVisible();
});
});

test('should delete account when valid password is provided and permission is enabled', async ({ page }) => {
await test.step('login with the user to delete', async () => {
await poRegistration.username.type(userToDelete.data.username);
await poRegistration.inputPassword.type(DEFAULT_USER_CREDENTIALS.password);
await poRegistration.btnLogin.click();
await expect(poUtils.mainContent).toBeVisible();
});

await test.step('navigate to profile and locate Delete My Account button', async () => {
await page.goto('/account/profile');
await poAccountProfile.profileTitle.waitFor({ state: 'visible' });
await poAccountProfile.btnDeleteMyAccount.click();
await expect(poAccountProfile.deleteAccountDialog).toBeVisible();
});

await test.step('verify delete confirmation dialog appears', async () => {
await expect(poAccountProfile.deleteAccountDialogMessageWithPassword).toBeVisible();
await expect(poAccountProfile.inputDeleteAccountPassword).toBeVisible();
await expect(poAccountProfile.btnDeleteAccountConfirm).toBeVisible();
await expect(poAccountProfile.btnDeleteAccountCancel).toBeVisible();
});

await test.step('enter password in the confirmation field and click delete account', async () => {
await poAccountProfile.inputDeleteAccountPassword.fill(DEFAULT_USER_CREDENTIALS.password);
await expect(poAccountProfile.inputDeleteAccountPassword).toHaveValue(DEFAULT_USER_CREDENTIALS.password);
await poAccountProfile.btnDeleteAccountConfirm.click();
});

await test.step('verify user is redirected to login page', async () => {
await expect(poRegistration.btnLogin).toBeVisible();
userToDelete.markAsDeleted();
});
});

test.describe('Delete Own Account - Permission Disabled', () => {
test.beforeAll(async ({ api }) => {
const response = await api.post('/settings/Accounts_AllowDeleteOwnAccount', { value: false });
expect(response.status()).toBe(200);
});

test('should not show delete account button when permission is disabled', async ({ page }) => {
await test.step('login with the user to delete', async () => {
await poRegistration.username.type(userWithoutPermissions.data.username);
await poRegistration.inputPassword.type(DEFAULT_USER_CREDENTIALS.password);
await poRegistration.btnLogin.click();
await expect(poUtils.mainContent).toBeVisible();
});

await test.step('navigate to profile and locate Delete My Account button', async () => {
await page.goto('/account/profile');
await poAccountProfile.profileTitle.waitFor({ state: 'visible' });
await expect(poAccountProfile.btnDeleteMyAccount).not.toBeVisible();
});
});
});
});
28 changes: 28 additions & 0 deletions apps/meteor/tests/e2e/page-objects/account-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,32 @@ export class AccountProfile {
get required2faModalSetUpButton(): Locator {
return this.page.locator('dialog >> button');
}

get btnDeleteMyAccount(): Locator {
return this.page.getByRole('button', { name: 'Delete my account' });
}

get deleteAccountDialog(): Locator {
return this.page.getByRole('dialog', { name: 'Delete account?' });
}

get deleteAccountDialogMessageWithPassword(): Locator {
return this.deleteAccountDialog.getByText('Enter your password to delete your account. This cannot be undone.');
}

get inputDeleteAccountPassword(): Locator {
return this.deleteAccountDialog.getByRole('textbox', { name: 'Enter your password to delete your account. This cannot be undone.' });
}

get btnDeleteAccountConfirm(): Locator {
return this.deleteAccountDialog.getByRole('button', { name: 'Delete Account' });
}

get btnDeleteAccountCancel(): Locator {
return this.deleteAccountDialog.getByRole('button', { name: 'Cancel' });
}

get profileTitle(): Locator {
return this.page.getByRole('heading', { name: 'Profile' });
}
}
75 changes: 75 additions & 0 deletions apps/meteor/tests/e2e/utils/user-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { faker } from '@faker-js/faker';
import type { APIResponse } from '@playwright/test';
import type { IUser } from '@rocket.chat/core-typings';

import type { BaseTest } from './test';
import { DEFAULT_USER_CREDENTIALS } from '../config/constants';

export interface ICreateUserOptions {
username?: string;
email?: string;
name?: string;
password?: string;
roles?: string[];
}

export interface ITestUser {
response: APIResponse;
data: IUser & { username: string };
deleted: boolean;
delete: () => Promise<APIResponse | undefined>;
markAsDeleted: () => void;
}

/**
* Creates a test user with optional customizations
*/
export async function createTestUser(api: BaseTest['api'], options: ICreateUserOptions = {}): Promise<ITestUser> {
const userData = {
email: options.email || faker.internet.email(),
name: options.name || faker.person.fullName(),
password: options.password || DEFAULT_USER_CREDENTIALS.password,
username: options.username || `test-user-${faker.string.uuid()}`,
roles: options.roles || ['user'],
};

const response = await api.post('/users.create', userData);

if (response.status() !== 200) {
throw new Error(`Failed to create user: ${response.status()}, response: ${await response.text()}`);
}

const { user } = await response.json();

return {
response,
data: user,
deleted: false,
markAsDeleted(this: ITestUser) {
this.deleted = true;
},
async delete(this: ITestUser) {
if (this.deleted) {
return;
}

const response = await api.post('/users.delete', { userId: user._id });
this.markAsDeleted();
return response;
},
};
}

/**
* Creates multiple test users at once
*/
export async function createTestUsers(api: BaseTest['api'], count: number, options: ICreateUserOptions = {}): Promise<ITestUser[]> {
const promises = Array.from({ length: count }, (_, i) =>
createTestUser(api, {
...options,
username: options.username ? `${options.username}-${i + 1}` : undefined,
}),
);

return Promise.all(promises);
}
Loading
0