mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
ForgottenPassword: Adds better error handling (#34174)
* ForgottenPassword: Adds better error handling * Chore: updates after PR comments
This commit is contained in:
parent
c9145541b0
commit
e246ec0d5b
@ -0,0 +1,118 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { asyncScheduler, scheduled, throwError } from 'rxjs';
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
|
import { ForgottenPassword, SendResetEmailDTO } from './ForgottenPassword';
|
||||||
|
import { backendSrv } from '../../services/backend_srv';
|
||||||
|
import { toAsyncOfResult } from '../../../features/query/state/DashboardQueryRunner/testHelpers';
|
||||||
|
import { createFetchResponse } from '../../../../test/helpers/createFetchResponse';
|
||||||
|
|
||||||
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
...((jest.requireActual('@grafana/runtime') as unknown) as object),
|
||||||
|
getBackendSrv: () => backendSrv,
|
||||||
|
}));
|
||||||
|
|
||||||
|
function getTestContext() {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
const fetchMock = jest.spyOn(backendSrv, 'fetch');
|
||||||
|
const { rerender } = render(<ForgottenPassword />);
|
||||||
|
|
||||||
|
return { fetchMock, rerender };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInputField() {
|
||||||
|
return screen.getByRole('textbox', { name: /user enter your information to get a reset link sent to you/i });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSubmitButton() {
|
||||||
|
return screen.getByRole('button', { name: /send reset email/i });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enterUserNameAndSubmitForm(fetchMock: jest.SpyInstance) {
|
||||||
|
await userEvent.type(getInputField(), 'JaneDoe');
|
||||||
|
userEvent.click(getSubmitButton());
|
||||||
|
|
||||||
|
await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1));
|
||||||
|
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith({
|
||||||
|
url: '/api/user/password/send-reset-email',
|
||||||
|
method: 'POST',
|
||||||
|
data: { userOrEmail: 'JaneDoe' },
|
||||||
|
showSuccessAlert: false,
|
||||||
|
showErrorAlert: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ForgottenPassword', () => {
|
||||||
|
describe('when mounted', () => {
|
||||||
|
it('then it should show input field', () => {
|
||||||
|
getTestContext();
|
||||||
|
|
||||||
|
expect(getInputField()).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('then it should show send button', () => {
|
||||||
|
getTestContext();
|
||||||
|
|
||||||
|
expect(getSubmitButton()).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('then it should show back to login link', () => {
|
||||||
|
getTestContext();
|
||||||
|
|
||||||
|
expect(screen.getByRole('link', { name: /back to login/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when user submits form', () => {
|
||||||
|
describe('and response is ok', () => {
|
||||||
|
it('then it should show success message', async () => {
|
||||||
|
const { fetchMock } = getTestContext();
|
||||||
|
fetchMock.mockImplementation(() =>
|
||||||
|
toAsyncOfResult(
|
||||||
|
createFetchResponse<SendResetEmailDTO>({ message: 'Success' })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await enterUserNameAndSubmitForm(fetchMock);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
/an email with a reset link has been sent to the email address\. you should receive it shortly\./i
|
||||||
|
)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and response is ok but contains an error', () => {
|
||||||
|
it('then it should show alert', async () => {
|
||||||
|
const { fetchMock } = getTestContext();
|
||||||
|
fetchMock.mockImplementation(() =>
|
||||||
|
toAsyncOfResult(
|
||||||
|
createFetchResponse<SendResetEmailDTO>({ message: 'Success', error: 'Something went wrong' })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await enterUserNameAndSubmitForm(fetchMock);
|
||||||
|
|
||||||
|
expect(screen.getByLabelText(/alert error/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/couldn't send reset link to the email address/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('and response is not ok', () => {
|
||||||
|
it('then it should show alert', async () => {
|
||||||
|
const { fetchMock } = getTestContext();
|
||||||
|
fetchMock.mockImplementation(() => scheduled(throwError('Server error'), asyncScheduler));
|
||||||
|
|
||||||
|
await enterUserNameAndSubmitForm(fetchMock);
|
||||||
|
|
||||||
|
expect(screen.getByLabelText(/alert error/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/couldn't send reset link to the email address/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/server error/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,32 +1,68 @@
|
|||||||
import React, { FC, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Form, Field, Input, Button, Legend, Container, useStyles, HorizontalGroup, LinkButton } from '@grafana/ui';
|
import { Unsubscribable } from 'rxjs';
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import {
|
||||||
import config from 'app/core/config';
|
Alert,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
FadeTransition,
|
||||||
|
Field,
|
||||||
|
Form,
|
||||||
|
HorizontalGroup,
|
||||||
|
Input,
|
||||||
|
Legend,
|
||||||
|
LinkButton,
|
||||||
|
useStyles2,
|
||||||
|
} from '@grafana/ui';
|
||||||
|
import { getBackendSrv, toDataQueryError } from '@grafana/runtime';
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
|
||||||
|
import { getConfig } from 'app/core/config';
|
||||||
|
|
||||||
interface EmailDTO {
|
interface EmailDTO {
|
||||||
userOrEmail: string;
|
userOrEmail: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const paragraphStyles = (theme: GrafanaTheme) => css`
|
export interface SendResetEmailDTO {
|
||||||
color: ${theme.colors.formDescription};
|
message: string;
|
||||||
font-size: ${theme.typography.size.sm};
|
error?: string;
|
||||||
font-weight: ${theme.typography.weight.regular};
|
}
|
||||||
margin-top: ${theme.spacing.sm};
|
|
||||||
display: block;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const ForgottenPassword: FC = () => {
|
export function ForgottenPassword(): JSX.Element {
|
||||||
const [emailSent, setEmailSent] = useState(false);
|
const [emailSent, setEmailSent] = useState(false);
|
||||||
const styles = useStyles(paragraphStyles);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const loginHref = `${config.appSubUrl}/login`;
|
const styles = useStyles2(getStyles);
|
||||||
|
const loginHref = `${getConfig().appSubUrl}/login`;
|
||||||
|
|
||||||
const sendEmail = async (formModel: EmailDTO) => {
|
const sendEmail = (formModel: EmailDTO) => {
|
||||||
const res = await getBackendSrv().post('/api/user/password/send-reset-email', formModel);
|
setError(null);
|
||||||
if (res) {
|
// using fetch here so we can disable showSuccessAlert, showErrorAlert and handle those
|
||||||
setEmailSent(true);
|
const subscription: Unsubscribable = getBackendSrv()
|
||||||
}
|
.fetch<SendResetEmailDTO>({
|
||||||
|
url: '/api/user/password/send-reset-email',
|
||||||
|
method: 'POST',
|
||||||
|
data: formModel,
|
||||||
|
showSuccessAlert: false,
|
||||||
|
showErrorAlert: false,
|
||||||
|
})
|
||||||
|
.subscribe({
|
||||||
|
next: (response) => {
|
||||||
|
const { error, message } = response.data; // the backend api can respond with 200 Ok but still have an error set
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
setError(error);
|
||||||
|
} else if (message) {
|
||||||
|
setEmailSent(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription.unsubscribe(); // unsubscribe as soon as a value is received
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
const dataError = toDataQueryError(err);
|
||||||
|
setError(dataError.message ?? 'Unknown error');
|
||||||
|
subscription.unsubscribe(); // unsubscribe as soon as an error is received
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (emailSent) {
|
if (emailSent) {
|
||||||
@ -40,29 +76,54 @@ export const ForgottenPassword: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
<Form onSubmit={sendEmail}>
|
|
||||||
{({ register, errors }) => (
|
|
||||||
<>
|
|
||||||
<Legend>Reset password</Legend>
|
|
||||||
<Field
|
|
||||||
label="User"
|
|
||||||
description="Enter your information to get a reset link sent to you"
|
|
||||||
invalid={!!errors.userOrEmail}
|
|
||||||
error={errors?.userOrEmail?.message}
|
|
||||||
>
|
|
||||||
<Input placeholder="Email or username" {...register('userOrEmail', { required: true })} />
|
|
||||||
</Field>
|
|
||||||
<HorizontalGroup>
|
|
||||||
<Button>Send reset email</Button>
|
|
||||||
<LinkButton fill="text" href={loginHref}>
|
|
||||||
Back to login
|
|
||||||
</LinkButton>
|
|
||||||
</HorizontalGroup>
|
|
||||||
|
|
||||||
<p className={styles}>Did you forget your username or email? Contact your Grafana administrator.</p>
|
return (
|
||||||
</>
|
<>
|
||||||
)}
|
<Form onSubmit={sendEmail}>
|
||||||
</Form>
|
{({ register, errors }) => (
|
||||||
|
<>
|
||||||
|
<Legend>Reset password</Legend>
|
||||||
|
<Field
|
||||||
|
label="User"
|
||||||
|
description="Enter your information to get a reset link sent to you"
|
||||||
|
invalid={!!errors.userOrEmail}
|
||||||
|
error={errors?.userOrEmail?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id={'userOrEmail'}
|
||||||
|
placeholder="Email or username"
|
||||||
|
{...register('userOrEmail', { required: true })}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<HorizontalGroup>
|
||||||
|
<Button>Send reset email</Button>
|
||||||
|
<LinkButton fill="text" href={loginHref}>
|
||||||
|
Back to login
|
||||||
|
</LinkButton>
|
||||||
|
</HorizontalGroup>
|
||||||
|
|
||||||
|
<p className={styles}>Did you forget your username or email? Contact your Grafana administrator.</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
<FadeTransition duration={150} visible={Boolean(error)}>
|
||||||
|
<Alert title="Couldn't send reset link to the email address" severity={'error'}>
|
||||||
|
<span>
|
||||||
|
<strong>Reason: </strong>
|
||||||
|
{error}
|
||||||
|
</span>
|
||||||
|
</Alert>
|
||||||
|
</FadeTransition>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
function getStyles(theme: GrafanaTheme2) {
|
||||||
|
return css`
|
||||||
|
color: ${theme.colors.text.secondary};
|
||||||
|
font-size: ${theme.typography.size.sm};
|
||||||
|
font-weight: ${theme.typography.fontWeightRegular};
|
||||||
|
margin-top: ${theme.spacing(1)};
|
||||||
|
display: block;
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user