-
Notifications
You must be signed in to change notification settings - Fork 113
E2604. Finish Password Resets #153
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
johnmweisz
wants to merge
61
commits into
expertiza:main
Choose a base branch
from
johnmweisz:development
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 54 commits
Commits
Show all changes
61 commits
Select commit
Hold shift + click to select a range
6f38e94
Added page for Forgot Password
galav12 c160dd8
Added Reset Password Page
galav12 c81dc61
Merge pull request #4 from itsJash/forgot-password
itsJash 4ad0cf0
Merge remote-tracking branch 'jash/main' into E2604-password-reset
johnmweisz f686384
add reset pages
johnmweisz c8e43dd
use env base url
johnmweisz 6623f3f
add env base url and fix service path
johnmweisz 7f34383
Merge pull request #2 from johnmweisz/E2604-password-reset
JaredM2028 d02f2eb
Centralizing the Api contant
josev814 8a0f776
fixing directory name
josev814 a73c3a2
Merge pull request #3 from johnmweisz/jvargas6/api_url_configurable
josev814 7f95d34
Dispatch alert before naviation to ensure the alert renders before un…
JaredM2028 202dc06
Protection against null password tokens. Broken link, manual navigati…
JaredM2028 f711f05
API_BASE_URL constant pulled in from @/constants/Api for ResetPasswor…
JaredM2028 6bed931
Standardize styling to match Login_tsx.
JaredM2028 b85b63e
Using Yup and Formik
JaredM2028 fae498d
Had to tweak vite config file to make it run on my machine.
JaredM2028 4efd32a
Reverting config files to original
JaredM2028 95f57f9
Revert gitignore
JaredM2028 35c0192
Update vite.config.ts for backend url
josev814 ced6c9e
Merge pull request #7 from johnmweisz/josev814-patch-1
josev814 58bf0c2
Restored vite config to repo original.
JaredM2028 e9dae03
Addressed feedback for ResetPassword
JaredM2028 58d7de8
Attempting to remove vite config from commit history
JaredM2028 5634ab5
Merge branch 'development' into front_end_refinements_pt_1
JaredM2028 54c92b2
Strengthen fallback alert for all submission errors in ForgotPassword…
JaredM2028 17302b8
Merge remote-tracking branch 'refs/remotes/origin/front_end_refinemen…
JaredM2028 dca5ef4
Improve fallback and condense error catching
JaredM2028 734f24b
Merge pull request #5 from johnmweisz/front_end_refinements_pt_1
johnmweisz e645db3
use header h2
johnmweisz 8aafdbc
Merge pull request #9 from johnmweisz/jweisz/fix-header
johnmweisz 6c33f89
adjust prompt style/text
johnmweisz 07ba090
Merge pull request #10 from johnmweisz/jweisz/adjust-prompt
JaredM2028 4b5d549
Adding tests to cover ForgotPassword with vitest
josev814 8237ed6
forgot to commit forgotpassword update
josev814 8f0ffe3
Merge branch 'main' into development
johnmweisz 16e641e
Merge branch 'development' into jvargas6/vitest_forgot_password
josev814 03d8857
Merge branch 'main' into development
johnmweisz 4bebeff
revert changes
johnmweisz 296dd27
revert changes
johnmweisz caf6dfc
revert changes
johnmweisz 847c497
revert changes
johnmweisz 86b34db
Merge branch 'development' into jvargas6/vitest_forgot_password
josev814 e4db480
apply ai suggestions from PR review
josev814 ac459d3
Merge pull request #11 from johnmweisz/jvargas6/vitest_forgot_password
josev814 f27129a
Initial tests
JaredM2028 bfd3f21
The file was broken because I screwed up git so I fixed it and put th…
JaredM2028 38dc0ee
Testing correct and incorrect password reset submissions.
JaredM2028 6da78a4
Testing API error.
JaredM2028 52a3ec5
Refactored and added some extra coverage.
JaredM2028 0136a1c
Adding coverage for API server error message.
JaredM2028 7a50237
Replaced shared mockStore with makeMockStore.
JaredM2028 ecaf60b
Avoiding race conditions.
JaredM2028 243e42b
Merge pull request #12 from johnmweisz/vitest_reset_password
JaredM2028 db9306d
Merge branch 'expertiza:main' into development
johnmweisz a2fd150
remove validateOnChange
johnmweisz 24f8cce
remove validateOnChange for consistency
johnmweisz dea3a39
Merge pull request #18 from johnmweisz/jweisz/fix-submit-disabled
johnmweisz 881f8a6
rm docker-compose.yml diffs
johnmweisz 1cdc6ae
When viewing the login/forgot/reset pages the container doesn't have …
josev814 8b76b9a
Merge pull request #19 from johnmweisz/jvargas6/ui_form_padding
johnmweisz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,4 +7,3 @@ services: | |
| ports: | ||
| - "8080:80" | ||
| restart: unless-stopped | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://localhost:3002"; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import React from "react"; | ||
| import { Button, Col, Container } from "react-bootstrap"; | ||
| import { Form, Formik, FormikHelpers } from "formik"; | ||
| import FormInput from "../../components/Form/FormInput"; | ||
| import axios, { AxiosError } from "axios"; | ||
| import { alertActions } from "../../store/slices/alertSlice"; | ||
| import { useDispatch } from "react-redux"; | ||
| import { API_BASE_URL } from "../../constants/Api"; | ||
| import * as Yup from "yup"; | ||
|
|
||
| interface IForgotPasswordFormValues { | ||
| email: string; | ||
| } | ||
|
|
||
| const validationSchema = Yup.object({ | ||
| email: Yup.string().trim().email("Invalid email address").required("Required"), | ||
| }); | ||
|
|
||
| const ForgotPassword = () => { | ||
| const dispatch = useDispatch(); | ||
|
|
||
| const onSubmit = async ( | ||
| values: IForgotPasswordFormValues, | ||
| submitProps: FormikHelpers<IForgotPasswordFormValues> | ||
| ) => { | ||
| try { | ||
| await axios.post(`${API_BASE_URL}/password_resets`, { email: values.email }); | ||
| dispatch( | ||
| alertActions.showAlert({ | ||
| variant: "success", | ||
| message: "A link to reset your password has been sent to your e-mail address.", | ||
| }) | ||
| ); | ||
| } catch (error) { | ||
| let errorFallback = "An error occurred. Please try again."; | ||
| if (error instanceof AxiosError && error.response && error.response.data) { | ||
| const { error: errorMessage } = error.response.data; | ||
| errorFallback = errorMessage || errorFallback; | ||
| } | ||
| dispatch( | ||
| alertActions.showAlert({ | ||
| variant: "danger", | ||
| message: errorFallback, | ||
| }) | ||
| ); | ||
| } | ||
| submitProps.setSubmitting(false); | ||
| }; | ||
|
|
||
| return ( | ||
| <Container className="d-flex justify-content-center mt-xxl-5"> | ||
| <Col xs={12} md={6} lg={4}> | ||
| <h2 className="text-center">Forgotten Your Password?</h2> | ||
| <Formik | ||
| initialValues={{ email: "" }} | ||
| onSubmit={onSubmit} | ||
| validationSchema={validationSchema} | ||
| validateOnChange={false} | ||
| > | ||
| {(formik) => ( | ||
| <Form> | ||
| <p>Enter the email associated with your account</p> | ||
| <FormInput | ||
| controlId="forgot-password-email" | ||
| label="Email Address" | ||
| name="email" | ||
| type="email" | ||
| /> | ||
| <Button | ||
| style={{ width: "100%" }} | ||
| variant="primary" | ||
| type="submit" | ||
| disabled={!(formik.isValid && formik.dirty) || formik.isSubmitting} | ||
| > | ||
| Request Password Reset | ||
| </Button> | ||
| </Form> | ||
| )} | ||
| </Formik> | ||
| </Col> | ||
| </Container> | ||
| ); | ||
| }; | ||
|
|
||
| export default ForgotPassword; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| import React, { useEffect } from "react"; | ||
| import { Button, Col, Container } from "react-bootstrap"; | ||
| import { Form, Formik, FormikHelpers } from "formik"; | ||
| import FormInput from "../../components/Form/FormInput"; | ||
| import { useLocation, useNavigate } from "react-router-dom"; | ||
| import axios, { AxiosError } from "axios"; | ||
| import { alertActions } from "../../store/slices/alertSlice"; | ||
| import { useDispatch } from "react-redux"; | ||
| import { API_BASE_URL } from "../../constants/Api"; | ||
| import * as Yup from "yup"; | ||
|
|
||
| interface IResetPasswordFormValues { | ||
| password: string; | ||
| confirmPassword: string; | ||
| } | ||
|
|
||
| const validationSchema = Yup.object({ | ||
| password: Yup.string() | ||
| .min(6, "Password must be at least 6 characters") | ||
| .required("Required"), | ||
| confirmPassword: Yup.string() | ||
| .oneOf([Yup.ref("password")], "Passwords do not match") | ||
| .required("Required"), | ||
| }); | ||
|
|
||
| const ResetPassword = () => { | ||
| const location = useLocation(); | ||
| const navigate = useNavigate(); | ||
| const dispatch = useDispatch(); | ||
| const queryParams = new URLSearchParams(location.search); | ||
| const token = queryParams.get("token"); | ||
|
|
||
| // Ensure the token is present when the component mounts | ||
| useEffect(() => { | ||
| if (!token) { | ||
| dispatch( | ||
| alertActions.showAlert({ | ||
| variant: "danger", | ||
| message: "Invalid or missing token.", | ||
| }) | ||
| ); | ||
| navigate("/login"); | ||
| } | ||
| }, [token, dispatch, navigate]); | ||
|
|
||
| const onSubmit = async ( | ||
| values: IResetPasswordFormValues, | ||
| submitProps: FormikHelpers<IResetPasswordFormValues> | ||
| ) => { | ||
| try { | ||
| // Send password reset request to the backend | ||
| await axios.put(`${API_BASE_URL}/password_resets/${token}`, { | ||
|
johnmweisz marked this conversation as resolved.
|
||
| user: { password: values.password }, | ||
| }); | ||
| dispatch( | ||
| alertActions.showAlert({ | ||
| variant: "success", | ||
| message: "Password Successfully Updated", | ||
| }) | ||
| ); | ||
| navigate("/login"); | ||
| } catch (error) { | ||
| let errorFallback = "An error occurred. Please try again."; | ||
| if (error instanceof AxiosError && error.response && error.response.data) { | ||
| const { error: errorMessage } = error.response.data; | ||
| errorFallback = errorMessage || errorFallback; | ||
| } | ||
| dispatch( | ||
| alertActions.showAlert({ | ||
| variant: "danger", | ||
| message: errorFallback, | ||
| }) | ||
| ); | ||
| } | ||
| submitProps.setSubmitting(false); | ||
| }; | ||
|
|
||
| return ( | ||
| <Container className="d-flex justify-content-center mt-xxl-5"> | ||
| <Col xs={12} md={6} lg={4}> | ||
| <h2 className="text-center">Reset Your Password</h2> | ||
| <Formik | ||
| initialValues={{ password: "", confirmPassword: "" }} | ||
| onSubmit={onSubmit} | ||
| validationSchema={validationSchema} | ||
| validateOnChange={false} | ||
| > | ||
| {(formik) => ( | ||
| <Form> | ||
| <FormInput | ||
| controlId="reset-password" | ||
| label="Password" | ||
| name="password" | ||
| type="password" | ||
| /> | ||
| <FormInput | ||
| controlId="reset-confirm-password" | ||
| label="Confirm Password" | ||
| name="confirmPassword" | ||
| type="password" | ||
| /> | ||
| <Button | ||
| style={{ width: "100%", marginTop: "8px" }} | ||
| variant="primary" | ||
| type="submit" | ||
| disabled={!(formik.isValid && formik.dirty) || formik.isSubmitting} | ||
| > | ||
| Reset Password | ||
| </Button> | ||
| </Form> | ||
| )} | ||
| </Formik> | ||
| </Col> | ||
| </Container> | ||
| ); | ||
| }; | ||
|
|
||
| export default ResetPassword; | ||
159 changes: 159 additions & 0 deletions
159
src/pages/Authentication/__tests__/ForgotPassword.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| import React from "react"; | ||
| import { render, screen, waitFor } from "@testing-library/react"; | ||
| import userEvent from "@testing-library/user-event"; | ||
| import ForgotPassword from "../ForgotPassword"; | ||
| import { Provider } from "react-redux"; | ||
| import { configureStore } from "@reduxjs/toolkit"; | ||
| import alertReducer from "store/slices/alertSlice"; | ||
| import { vi } from "vitest"; | ||
| import axios from "axios"; | ||
| import { AxiosError } from "axios"; | ||
|
|
||
| vi.mock('axios'); | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| const makeMockStore = () => configureStore({ | ||
| reducer: { | ||
| alert: alertReducer, | ||
| }, | ||
| }); | ||
|
|
||
| const validEmail = '[email protected]'; | ||
| const submitText = /request password reset/i; | ||
|
|
||
| describe('Test Forgot Password Displays Correctly', () => { | ||
| it('Renders the component correctly', () => { | ||
| const store = makeMockStore(); | ||
| render( | ||
| <Provider store={store}> | ||
| <ForgotPassword /> | ||
| </Provider> | ||
| ); | ||
| expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(/forgotten your password\?/i); | ||
| expect(screen.getByText(/enter the email associated with your account/i)).toBeInTheDocument(); | ||
| expect(screen.getByRole('button', {name: submitText})).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('Renders email input field', () => { | ||
| const store = makeMockStore(); | ||
| render( | ||
| <Provider store={store}> | ||
| <ForgotPassword /> | ||
| </Provider> | ||
| ); | ||
| const emailInput = screen.getByRole('textbox', {name: /email address/i}); | ||
| expect(emailInput).toBeInTheDocument(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Test Forgot Password Form Validations', () => { | ||
| it('Does not submit form with empty email', async () => { | ||
| const user = userEvent.setup(); | ||
| const store = makeMockStore(); | ||
| render( | ||
| <Provider store={store}> | ||
| <ForgotPassword /> | ||
| </Provider> | ||
| ); | ||
|
|
||
| let emailInput = screen.getByRole('textbox'); | ||
| let submitButton = screen.getByRole('button', {name: submitText}); | ||
|
|
||
| await user.click(emailInput); | ||
| await user.tab(); | ||
| await user.click(submitButton); | ||
|
|
||
| expect(axios.post).not.toHaveBeenCalled(); | ||
|
|
||
| expect(screen.getByText(/required/i)).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('Does not submit form with invalid email', async () => { | ||
| const user = userEvent.setup(); | ||
| const store = makeMockStore(); | ||
| render( | ||
| <Provider store={store}> | ||
| <ForgotPassword /> | ||
| </Provider> | ||
| ); | ||
|
|
||
| let emailInput = screen.getByRole('textbox'); | ||
| let submitButton = screen.getByRole('button', {name: submitText}); | ||
|
|
||
| await user.type(emailInput, 'bademail'); | ||
| await user.tab(); | ||
|
|
||
| await waitFor(() => { | ||
| expect(screen.getByText(/invalid email address/i)).toBeInTheDocument(); | ||
| }); | ||
| expect(submitButton).toBeDisabled(); | ||
| expect(axios.post).not.toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Test Forgot Password Api Error', () => { | ||
| it('Handles API unavailable', async () => { | ||
| const user = userEvent.setup(); | ||
| (axios.post as any).mockRejectedValue( | ||
| new AxiosError("Network Error", 'ERR_NETWORK') | ||
| ); | ||
|
|
||
| const store = makeMockStore(); | ||
| render( | ||
| <Provider store={store}> | ||
| <ForgotPassword /> | ||
| </Provider> | ||
| ); | ||
|
|
||
| let emailInput = screen.getByRole('textbox'); | ||
| let submitButton = screen.getByRole('button', {name: submitText}); | ||
|
|
||
| await user.type(emailInput, validEmail); | ||
| await user.click(submitButton); | ||
|
|
||
| await waitFor(() => { | ||
| const state = store.getState(); | ||
| expect(state.alert.message).toBe('An error occurred. Please try again.'); | ||
| expect(state.alert.variant).toBe('danger'); | ||
| }); | ||
|
|
||
| expect(axios.post).toHaveBeenCalledWith( | ||
| expect.stringContaining('/password_resets'), {email: validEmail} | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Test Successful Password Reset Request', () => { | ||
| it('submit form successfully', async () => { | ||
| const user = userEvent.setup(); | ||
| (axios.post as any).mockResolvedValue({ | ||
| status: 200, | ||
| data: { message: 'If the email exists, a reset link has been sent.'}, | ||
| }); | ||
| const store = makeMockStore(); | ||
| render( | ||
| <Provider store={store}> | ||
| <ForgotPassword /> | ||
| </Provider> | ||
| ); | ||
|
|
||
| let emailInput = screen.getByRole('textbox'); | ||
| let submitButton = screen.getByRole('button', {name: submitText}); | ||
|
|
||
| await user.type(emailInput, validEmail); | ||
| await user.click(submitButton); | ||
|
|
||
| expect(axios.post).toHaveBeenCalledWith( | ||
| expect.stringContaining('/password_resets'), {email: validEmail} | ||
| ); | ||
|
|
||
| await waitFor(() => { | ||
| const state = store.getState(); | ||
| expect(state.alert.variant).toBe('success'); | ||
| expect(state.alert.message).toBe('A link to reset your password has been sent to your e-mail address.'); | ||
| }); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.