Skip to content

Commit 4bd4025

Browse files
authored
Changes to prevent page reload on entering invalid current password and to disable reset button when current or new password is empty (opensearch-project#2238)
* Changes to prevent page reload on invalid current password validation during reset password Signed-off-by: nishthm <nishthamittal04@gmail.com> * Changes to disable reset button when current or new password is empty Signed-off-by: nishthm <nishthamittal04@gmail.com> --------- Signed-off-by: nishthm <nishthamittal04@gmail.com>
1 parent 6a259e7 commit 4bd4025

File tree

4 files changed

+177
-7
lines changed

4 files changed

+177
-7
lines changed

public/apps/account/password-reset-panel.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ import { FormRow } from '../configuration/utils/form-row';
3434
import { logout, updateNewPassword } from './utils';
3535
import { PASSWORD_INSTRUCTION } from '../apps-constants';
3636
import { constructErrorMessageAndLog } from '../error-utils';
37-
import { validateCurrentPassword } from '../../utils/login-utils';
37+
import {
38+
isResetButtonDisabled,
39+
validateCurrentPassword,
40+
} from '../../utils/password-reset-panel-utils';
3841
import { getDashboardsInfo } from '../../utils/dashboards-info-utils';
3942
import { PasswordStrengthBar } from '../configuration/utils/password-strength-bar';
4043

@@ -85,7 +88,8 @@ export function PasswordResetPanel(props: PasswordResetPanelProps) {
8588
await validateCurrentPassword(http, props.username, currentPassword);
8689
} catch (e) {
8790
setIsCurrentPasswordInvalid(true);
88-
setCurrentPasswordError([constructErrorMessageAndLog(e, 'Invalid current password.')]);
91+
setCurrentPasswordError([`Invalid current password. ${e.message}`]);
92+
return;
8993
}
9094

9195
// update new password
@@ -184,7 +188,11 @@ export function PasswordResetPanel(props: PasswordResetPanelProps) {
184188
<EuiSmallButton
185189
data-test-subj="reset"
186190
fill
187-
disabled={isRepeatNewPasswordInvalid}
191+
disabled={isResetButtonDisabled(
192+
currentPassword,
193+
newPassword,
194+
isRepeatNewPasswordInvalid
195+
)}
188196
onClick={handleReset}
189197
>
190198
Reset

public/apps/account/test/password-reset-panel.test.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import { shallow } from 'enzyme';
1717
import React from 'react';
18-
import { validateCurrentPassword } from '../../../utils/login-utils';
18+
import { validateCurrentPassword } from '../../../utils/password-reset-panel-utils';
1919
import { PasswordResetPanel } from '../password-reset-panel';
2020
import { logout, updateNewPassword } from '../utils';
2121

@@ -24,7 +24,8 @@ jest.mock('../utils', () => ({
2424
updateNewPassword: jest.fn(),
2525
}));
2626

27-
jest.mock('../../../utils/login-utils', () => ({
27+
jest.mock('../../../utils/password-reset-panel-utils', () => ({
28+
isResetButtonDisabled: jest.fn(),
2829
validateCurrentPassword: jest.fn(),
2930
}));
3031

@@ -71,15 +72,16 @@ describe('Account menu - Password reset panel', () => {
7172

7273
it('invalid current password', (done) => {
7374
(validateCurrentPassword as jest.Mock).mockImplementationOnce(() => {
74-
throw new Error();
75+
throw new Error('Unauthorized');
7576
});
7677

7778
// Hide the error message
7879
jest.spyOn(console, 'error').mockImplementationOnce(() => {});
7980

8081
component.find('[data-test-subj="reset"]').simulate('click');
8182
process.nextTick(() => {
82-
expect(setState).toHaveBeenCalledWith(true);
83+
expect(setState).toHaveBeenNthCalledWith(1, true);
84+
expect(setState).toHaveBeenNthCalledWith(2, ['Invalid current password. Unauthorized']);
8385
expect(setState).toBeTruthy();
8486
done();
8587
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
import { HttpStart } from 'opensearch-dashboards/public';
17+
18+
export async function validateCurrentPassword(
19+
http: HttpStart,
20+
userName: string,
21+
currentPassword: string
22+
) {
23+
const url = http.basePath.prepend('/auth/login');
24+
const res = await fetch(url, {
25+
method: 'POST',
26+
credentials: 'include',
27+
headers: {
28+
'Content-Type': 'application/json',
29+
'osd-xsrf': 'true',
30+
},
31+
body: JSON.stringify({ username: userName, password: currentPassword }),
32+
});
33+
34+
if (!res.ok) {
35+
const data = await res.json();
36+
throw new Error(data.message);
37+
}
38+
}
39+
40+
export function isResetButtonDisabled(
41+
currentPassword: string,
42+
newPassword: string,
43+
isRepeatNewPasswordInvalid: boolean
44+
): boolean {
45+
return !currentPassword || !newPassword || isRepeatNewPasswordInvalid;
46+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
import { isResetButtonDisabled, validateCurrentPassword } from '../password-reset-panel-utils';
17+
import { HttpStart } from 'opensearch-dashboards/public';
18+
19+
describe('Test Password Reset Panel Utlis', () => {
20+
const mockUrl = 'http://localhost:5601/auth/login';
21+
let http: HttpStart;
22+
beforeEach(() => {
23+
http = {
24+
basePath: {
25+
prepend: jest.fn(() => mockUrl),
26+
},
27+
} as any;
28+
global.fetch = jest.fn();
29+
});
30+
31+
it('validate current password resolves when the server returns 2xx', async () => {
32+
(global.fetch as jest.Mock).mockResolvedValue({
33+
ok: true,
34+
});
35+
36+
await expect(validateCurrentPassword(http, 'user', 'password')).resolves.toBeUndefined();
37+
38+
expect(global.fetch).toHaveBeenCalledWith(
39+
mockUrl,
40+
expect.objectContaining({
41+
method: 'POST',
42+
credentials: 'include',
43+
headers: {
44+
'Content-Type': 'application/json',
45+
'osd-xsrf': 'true',
46+
},
47+
body: JSON.stringify({ username: 'user', password: 'password' }),
48+
})
49+
);
50+
});
51+
52+
it('validate current password rejects with the server message on 4xx', async () => {
53+
const serverBody = { message: 'Unauthorized' };
54+
(global.fetch as jest.Mock).mockResolvedValue({
55+
ok: false,
56+
json: jest.fn().mockResolvedValue(serverBody),
57+
status: 401,
58+
});
59+
60+
await expect(validateCurrentPassword(http, 'user', 'incorrectpassword')).rejects.toThrow(
61+
'Unauthorized'
62+
);
63+
});
64+
});
65+
66+
describe('Validates password reset button state based on input conditions', () => {
67+
const testScenarios = [
68+
{
69+
description: 'should be disabled when currentPassword is empty',
70+
currentPassword: '',
71+
newPassword: 'newpass',
72+
isRepeatNewPasswordInvalid: false,
73+
expected: true,
74+
},
75+
{
76+
description: 'should be disabled when newPassword is empty',
77+
currentPassword: 'current',
78+
newPassword: '',
79+
isRepeatNewPasswordInvalid: false,
80+
expected: true,
81+
},
82+
{
83+
description: 'should be disabled when isRepeatNewPasswordInvalid is true',
84+
currentPassword: 'current',
85+
newPassword: 'newpass',
86+
isRepeatNewPasswordInvalid: true,
87+
expected: true,
88+
},
89+
{
90+
description: 'should be enabled when all conditions are valid',
91+
currentPassword: 'current',
92+
newPassword: 'newpass',
93+
isRepeatNewPasswordInvalid: false,
94+
expected: false,
95+
},
96+
{
97+
description: 'should be disabled when all fields are invalid',
98+
currentPassword: '',
99+
newPassword: '',
100+
isRepeatNewPasswordInvalid: true,
101+
expected: true,
102+
},
103+
] as const;
104+
105+
it.each(testScenarios)('$description', async function ({
106+
currentPassword,
107+
newPassword,
108+
isRepeatNewPasswordInvalid,
109+
expected,
110+
}) {
111+
const result = isResetButtonDisabled(currentPassword, newPassword, isRepeatNewPasswordInvalid);
112+
expect(result).toBe(expected);
113+
});
114+
});

0 commit comments

Comments
 (0)