Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/stupid-fishes-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Fixes redirection not being triggered after a required password change
11 changes: 10 additions & 1 deletion apps/meteor/server/methods/setUserPassword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Meteor } from 'meteor/meteor';
import type { UpdateResult } from 'mongodb';

import { passwordPolicy } from '../../app/lib/server';
import { notifyOnUserChange } from '../../app/lib/server/lib/notifyListener';
import { compareUserPassword } from '../lib/compareUserPassword';

declare module '@rocket.chat/ddp-client' {
Expand Down Expand Up @@ -52,6 +53,14 @@ Meteor.methods<ServerMethods>({
logout: false,
});

return Users.unsetRequirePasswordChange(userId);
const update = await Users.unsetRequirePasswordChange(userId);

void notifyOnUserChange({
clientAction: 'updated',
id: userId,
diff: { requirePasswordChange: false, requirePasswordChangeReason: false },
});

return update;
},
});
166 changes: 166 additions & 0 deletions apps/meteor/tests/e2e/user-required-password-change.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { faker } from '@faker-js/faker';

import { DEFAULT_USER_CREDENTIALS } from './config/constants';
import { Registration } from './page-objects';
import { HomeSidenav } from './page-objects/fragments/home-sidenav';
import { getSettingValueById, setSettingValueById } from './utils';
import { test, expect } from './utils/test';
import type { ITestUser } from './utils/user-helpers';
import { createTestUser } from './utils/user-helpers';

test.describe('User - Password change required', () => {
let poRegistration: Registration;
let poSidenav: HomeSidenav;
let userRequiringPasswordChange: ITestUser;
let userNotRequiringPasswordChange: ITestUser;
let userNotAbleToLogin: ITestUser;
let settingDefaultValue: unknown;

test.beforeAll(async ({ api }) => {
settingDefaultValue = await getSettingValueById(api, 'Accounts_RequirePasswordConfirmation');
await setSettingValueById(api, 'Accounts_RequirePasswordConfirmation', true);
userRequiringPasswordChange = await createTestUser(api, { data: { requirePasswordChange: true } });
userNotRequiringPasswordChange = await createTestUser(api, { data: { requirePasswordChange: false } });
userNotAbleToLogin = await createTestUser(api, { data: { requirePasswordChange: true } });
});

test.beforeEach(async ({ page }) => {
poRegistration = new Registration(page);
poSidenav = new HomeSidenav(page);
await page.goto('/home');
});

test.afterAll(async ({ api }) => {
await Promise.all([
setSettingValueById(api, 'Accounts_RequirePasswordConfirmation', settingDefaultValue),
userRequiringPasswordChange.delete(),
userNotRequiringPasswordChange.delete(),
userNotAbleToLogin.delete(),
]);
});

test('should redirect to home after successful password change for new user', async ({ page }) => {
await test.step('login with temporary password', async () => {
await poRegistration.username.fill(userRequiringPasswordChange.data.username);
await poRegistration.inputPassword.fill(DEFAULT_USER_CREDENTIALS.password);
await poRegistration.btnLogin.click();
});

await test.step('should be redirected to password change screen', async () => {
await expect(poRegistration.inputPassword).toBeVisible();
await expect(poRegistration.inputPasswordConfirm).toBeVisible();
});

await test.step('enter new password and confirm', async () => {
const newPassword = faker.internet.password();
await poRegistration.inputPassword.fill(newPassword);
await poRegistration.inputPasswordConfirm.fill(newPassword);
});

await test.step('click reset button', async () => {
await poRegistration.btnReset.click();
});

await test.step('verify password reset form is not visible', async () => {
await page.waitForURL('/home');
await expect(poRegistration.inputPassword).not.toBeVisible();
await expect(poRegistration.inputPasswordConfirm).not.toBeVisible();
});

await test.step('verify user is properly logged in', async () => {
await expect(poSidenav.userProfileMenu).toBeVisible();
await expect(poSidenav.sidebarChannelsList).toBeVisible();
});
});

test('should show error when password confirmation does not match', async () => {
await test.step('login with temporary password', async () => {
await poRegistration.username.fill(userNotAbleToLogin.data.username);
await poRegistration.inputPassword.fill(DEFAULT_USER_CREDENTIALS.password);
await poRegistration.btnLogin.click();
});

await test.step('should be redirected to password change screen', async () => {
await expect(poRegistration.inputPassword).toBeVisible();
await expect(poRegistration.inputPasswordConfirm).toBeVisible();
});

await test.step('enter different passwords in password and confirm fields', async () => {
await poRegistration.inputPassword.fill(DEFAULT_USER_CREDENTIALS.password);
await poRegistration.inputPasswordConfirm.fill(faker.internet.password());
});

await test.step('attempt to submit password change', async () => {
await poRegistration.btnReset.click();
});

await test.step('verify error message appears for password mismatch', async () => {
await expect(poRegistration.inputPasswordConfirm).toBeInvalid();
});
});

test('should show error when user tries to use the same password as current', async () => {
await test.step('login with temporary password', async () => {
await poRegistration.username.fill(userNotAbleToLogin.data.username);
await poRegistration.inputPassword.fill(DEFAULT_USER_CREDENTIALS.password);
await poRegistration.btnLogin.click();
});

await test.step('should be redirected to password change screen', async () => {
await expect(poRegistration.inputPassword).toBeVisible();
await expect(poRegistration.inputPasswordConfirm).toBeVisible();
});

await test.step('enter the same password as current password', async () => {
await poRegistration.inputPassword.fill(DEFAULT_USER_CREDENTIALS.password);
await poRegistration.inputPasswordConfirm.fill(DEFAULT_USER_CREDENTIALS.password);
});

await test.step('attempt to submit password change', async () => {
await poRegistration.btnReset.click();
});

await test.step('verify error message appears for same password', async () => {
await expect(poRegistration.inputPassword).toBeInvalid();
});
});
});

test.describe('User - Password change not required', () => {
let poRegistration: Registration;
let poSidenav: HomeSidenav;
let userNotRequiringPasswordChange: ITestUser;
let settingDefaultValue: unknown;

test.beforeAll(async ({ api }) => {
settingDefaultValue = await getSettingValueById(api, 'Accounts_RequirePasswordConfirmation');
userNotRequiringPasswordChange = await createTestUser(api, { data: { requirePasswordChange: false } });
});

test.beforeEach(async ({ page }) => {
poRegistration = new Registration(page);
poSidenav = new HomeSidenav(page);
await page.goto('/home');
});

test.afterAll(async ({ api }) => {
await Promise.all([
setSettingValueById(api, 'Accounts_RequirePasswordConfirmation', settingDefaultValue),
userNotRequiringPasswordChange.delete(),
]);
});

test('should not require password change if the requirePasswordChange is disabled', async ({ page }) => {
await test.step('login with user not requiring password change', async () => {
await poRegistration.username.fill(userNotRequiringPasswordChange.data.username);
await poRegistration.inputPassword.fill(DEFAULT_USER_CREDENTIALS.password);
await poRegistration.btnLogin.click();
});

await test.step('verify user is properly logged in', async () => {
await page.waitForURL('/home');
await expect(poSidenav.userProfileMenu).toBeVisible();
await expect(poSidenav.sidebarChannelsList).toBeVisible();
});
});
});
2 changes: 2 additions & 0 deletions apps/meteor/tests/e2e/utils/user-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface ICreateUserOptions {
name?: string;
password?: string;
roles?: string[];
data?: Record<string, any>;
}

export interface ITestUser {
Expand All @@ -31,6 +32,7 @@ export async function createTestUser(api: BaseTest['api'], options: ICreateUserO
password: options.password || DEFAULT_USER_CREDENTIALS.password,
username: options.username || `test-user-${faker.string.uuid()}`,
roles: options.roles || ['user'],
...options.data,
};

const response = await api.post('/users.create', userData);
Expand Down
2 changes: 1 addition & 1 deletion packages/ui-contexts/src/hooks/useMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ type ServerMethodFunction<MethodName extends ServerMethodName> = (
...args: ServerMethodParameters<MethodName>
) => Promise<ServerMethodReturn<MethodName>>;

/* @deprecated prefer the use of api endpoints (useEndpoint) */
/** @deprecated prefer the use of api endpoints (useEndpoint) */
export const useMethod = <MethodName extends keyof ServerMethods>(methodName: MethodName): ServerMethodFunction<MethodName> => {
const { callMethod } = useContext(ServerContext);

Expand Down
Loading