Skip to content

Commit

Permalink
feat(reset-password): Implement reset password using email
Browse files Browse the repository at this point in the history
   - User should be allowed to reset a password
   - User should see email before reset a password
   - User should click on link in email and go to reset password

[Deliver #187419121]
  • Loading branch information
yvanddniyo committed Jun 24, 2024
1 parent 577e395 commit c342239
Show file tree
Hide file tree
Showing 20 changed files with 907 additions and 301 deletions.
577 changes: 295 additions & 282 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
"dependencies": {
"@hookform/resolvers": "^3.6.0",
"@reduxjs/toolkit": "^2.2.5",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"axios": "^1.7.2",
Expand All @@ -35,6 +34,7 @@
"redux-mock-store": "^1.5.4",
"redux-thunk": "^3.1.0",
"vite-plugin-environment": "^1.1.3",
"@testing-library/jest-dom": "^6.4.6",
"yup": "^1.4.0"
},
"devDependencies": {
Expand Down
4 changes: 3 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import * as React from "react";
import "./App.css";
import "react-toastify/dist/ReactToastify.css";
import { ToastContainer } from "react-toastify";

import AppRoutes from "./routes/AppRoutes";
import 'react-toastify/dist/ReactToastify.css';

const App: React.FC = () => (
<main>
<AppRoutes />
<ToastContainer />
</main>
);

Expand Down
60 changes: 60 additions & 0 deletions src/__test__/getLink.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import { Provider } from "react-redux";
import { BrowserRouter as Router } from "react-router-dom";

import store from "../redux/store";
import GetLinkPage from "../pages/GetLinkPage";
import { getLink } from "../redux/reducers/getLinkSlice";

test("should render registration page correctly", async () => {
render(
<Provider store={store}>
<Router>
<GetLinkPage />
</Router>
</Provider>,
);

expect(screen.getByText("eagles", { exact: false })).toBeInTheDocument();
expect(screen.getByText(/Get a link to reset password/i)).toBeInTheDocument();
const title = screen.getAllByText("Get a link to reset password");
const email = screen.getAllByPlaceholderText("Email");
const description = screen.getAllByText(
"To reset your password provide registered email below. Before proceed make sure you provide valid email.",
);

expect(title).toBeTruthy();
expect(email).toBeTruthy();
expect(description).toBeTruthy();
});

test("should handle initial state", () => {
expect(store.getState().getLink).toEqual({
isLoading: false,
data: [],
error: null,
});
});

test("should handle getLink.pending", () => {
// @ts-ignore
store.dispatch(getLink.pending());
expect(store.getState().getLink).toEqual({
isLoading: true,
data: [],
error: null,
});
});

test("should handle getLink.fulfilled", () => {
const mockData = { message: "Reset link sent successfully" };
store.dispatch(
getLink.fulfilled(mockData, "", { email: "[email protected]" }),
);
expect(store.getState().getLink).toEqual({
isLoading: false,
data: mockData,
error: null,
});
});
73 changes: 64 additions & 9 deletions src/__test__/registerUser.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
// @ts-nocheck
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import { Provider } from "react-redux";
import { BrowserRouter as Router } from "react-router-dom";
import userEvent from "@testing-library/user-event";
import { AnyAction } from "redux";

import store from "../redux/store";
import RegisterUser from "../pages/RegisterUser";
import { createUser } from "../redux/reducers/registerSlice";

jest.mock("../redux/api/api");

test("should render registration page correctly", async () => {
render(
Expand All @@ -14,13 +20,62 @@ test("should render registration page correctly", async () => {
</Router>
</Provider>,
);
const name = screen.getAllByPlaceholderText("Name");
const username = screen.getAllByPlaceholderText("Username");
const email = screen.getAllByPlaceholderText("Email");
const password = screen.getAllByPlaceholderText("Password");

expect(name).toBeTruthy();
expect(username).toBeTruthy();
expect(email).toBeTruthy();
expect(password).toBeTruthy();

const name = screen.getByPlaceholderText("Name");
const username = screen.getByPlaceholderText("Username");
const email = screen.getByPlaceholderText("Email");
const password = screen.getByPlaceholderText("Password");

expect(name).toBeInTheDocument();
expect(username).toBeInTheDocument();
expect(email).toBeInTheDocument();
expect(password).toBeInTheDocument();

const googleButton = screen.getByRole("button", {
name: /Sign up with Google/i,
});
expect(googleButton).toBeInTheDocument();

const loginLink = screen.getByRole("link", { name: /Login/i });
expect(loginLink).toBeInTheDocument();
expect(loginLink.getAttribute("href")).toBe("/login");

userEvent.click(loginLink);
});

it("should handle initial state", () => {
expect(store.getState().reset).toEqual({
isLoading: false,
data: [],
error: null,
});
});

it("should handle registerUser.pending", () => {
// @ts-ignore
store.dispatch(createUser.pending(""));
expect(store.getState().reset).toEqual({
isLoading: false,
data: [],
error: null,
});
});

it("should handle createUser.fulfilled", () => {
const mockData = { message: "Account created successfully!" };
store.dispatch(createUser.fulfilled(mockData, "", {} as AnyAction));
expect(store.getState().reset).toEqual({
isLoading: false,
data: [],
error: null,
});
});

it("should handle createUser.rejected", () => {
store.dispatch(createUser.rejected(null, "", {} as AnyAction));
expect(store.getState().reset).toEqual({
isLoading: false,
data: [],
error: null,
});
});
85 changes: 85 additions & 0 deletions src/__test__/resetPassword.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// @ts-nocheck
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";
import { Provider } from "react-redux";
import { BrowserRouter as Router } from "react-router-dom";
import { AnyAction } from "@reduxjs/toolkit";

import store from "../redux/store";
import ResetPassword from "../pages/ResetPassword";
import { resetPassword } from "../redux/reducers/resetPasswordSlice";

test("should render reset password page correctly", async () => {
render(
<Provider store={store}>
<Router>
<ResetPassword />
</Router>
</Provider>,
);

expect(screen.getByText("eagles", { exact: false })).toBeInTheDocument();
expect(
screen.getByText("Reset your password", { exact: false }),
).toBeInTheDocument();
const description = screen.getAllByText(
"Before you write your password consider if your password is strong enough that can not be guessed or cracked by anyone.",
);
const newPassword = screen.getAllByPlaceholderText("New Password");
const confirmPassword = screen.getAllByPlaceholderText("Confirm password");

expect(description).toBeTruthy();
expect(newPassword).toBeTruthy();
expect(confirmPassword).toBeTruthy();
});

test("the link is not disabled", () => {
const { getByText } = render(
<Provider store={store}>
<Router>
<ResetPassword />
</Router>
</Provider>,
);
const linkElement = getByText("Reset");
expect(linkElement).not.toBeDisabled();
expect(linkElement).toBeEnabled();
expect(linkElement).toBeVisible();
});

it("should handle initial state", () => {
expect(store.getState().reset).toEqual({
isLoading: false,
data: [],
error: null,
});
});

it("should handle resetPassword.pending", () => {
// @ts-ignore
store.dispatch(resetPassword.pending(""));
expect(store.getState().reset).toEqual({
isLoading: true,
data: [],
error: null,
});
});

it("should handle resetPassword.fulfilled", () => {
const mockData = { message: "Password reset successfully" };
store.dispatch(resetPassword.fulfilled(mockData, "", {} as AnyAction));
expect(store.getState().reset).toEqual({
isLoading: false,
data: mockData,
error: null,
});
});

it("should handle resetPassword.rejected", () => {
store.dispatch(resetPassword.rejected(null, "", {} as AnyAction));
expect(store.getState().reset).toEqual({
isLoading: false,
data: [],
error: undefined,
});
});
10 changes: 8 additions & 2 deletions src/components/common/auth/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@ interface ButtonProps {
text: string;
disabled?: boolean;
dataTestId?: string;
backgroundColor?: string;
}

const Button: React.FC<ButtonProps> = ({ text, disabled, dataTestId }) => (
const Button: React.FC<ButtonProps> = ({
text,
disabled,
dataTestId,
backgroundColor,
}) => (
<button
type="submit"
className="bg-[#161616] text-white py-3 my-4 text-normal md:text-lg rounded-sm"
className={`${backgroundColor} text-white py-3 px-12 my-4 text-normal md:text-lg rounded-sm cursor-pointer`}
disabled={disabled}
data-testid={dataTestId}
>
Expand Down
12 changes: 12 additions & 0 deletions src/components/common/auth/Logo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { FaCircle } from "react-icons/fa";

const Logo = () => (
<div className="text-black font-bold text-[30px] flex items-center gap-1 p-5">
<h1 className="font-medium text-[36px]">
<span className="font-[550] text-heading"> eagles</span>
</h1>
<FaCircle className="text-sm text-[#DB4444] mt-3" />
</div>
);

export default Logo;
10 changes: 10 additions & 0 deletions src/components/common/auth/createSlice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createSlice } from "@reduxjs/toolkit";
// @ts-ignore
const createCustomSlice = (name, initialState, extraReducers) => createSlice({
name,
initialState,
reducers: {},
extraReducers,
});

export default createCustomSlice;
77 changes: 77 additions & 0 deletions src/pages/GetLinkPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { yupResolver } from "@hookform/resolvers/yup";
import { SubmitHandler, useForm } from "react-hook-form";
import { useDispatch, useSelector } from "react-redux";
import { toast } from "react-toastify";
import { AxiosError } from "axios";

import InputField from "../components/common/auth/InputField";
import { emailSchema } from "../schemas/PasswordResetSchema";
import Button from "../components/common/auth/Button";
import { RegisterError, emailType } from "../../type";
import { RootState } from "../redux/store";
import { getLink } from "../redux/reducers/getLinkSlice";
import Logo from "../components/common/auth/Logo";

const GetLinkPage = () => {
const dispatch = useDispatch();
const loading = useSelector((state: RootState) => state.getLink.isLoading);
const {
register,
reset,
handleSubmit,
formState: { errors },
} = useForm<emailType>({
resolver: yupResolver(emailSchema),
});

const onSubmit: SubmitHandler<emailType> = async (data: emailType) => {
try {
// @ts-ignore
await dispatch(getLink(data)).unwrap();
console.log(data);
toast.success("Email sent successfully check your inbox!");
console.log(data);
reset();
} catch (err) {
const error = err as AxiosError<RegisterError>;
console.log(error);
toast.error(`${error.message}`);
}
};
return (
<div className="w-full h-screen overflow-y-hidden">
<Logo />
<div className=" w-[75%] md:w-1/2 flex mx-auto flex-col mt-36">
<h1 className="text-[#EB5757] text-[15px] sm:text-2xl md:text-[38px] text-center">
Get a link to reset password
</h1>
<p className="text-[13px] md:text-[16px] text-center pt-6">
To reset your password provide registered email below. Before proceed
make sure you provide valid email.
</p>
<form
onSubmit={handleSubmit(onSubmit)}
className="w-[100%] md:w-[70%] mx-auto mt-8"
>
<InputField
name="email"
type="email"
placeholder="Email"
register={register}
error={errors.email?.message}
/>
<div className="flex justify-center">
<Button
text={loading ? "Loading..." : "Send Link"}
backgroundColor="bg-[#EB5757]"
disabled={loading}
data-testid="sent-link-btn"
/>
</div>
</form>
</div>
</div>
);
};

export default GetLinkPage;
Loading

0 comments on commit c342239

Please sign in to comment.