diff --git a/.eslintrc.cjs b/.eslintrc.cjs index e89d9dc..1979ef5 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -69,6 +69,7 @@ module.exports = { "no-unsafe-optional-chaining": "off", "react/no-unescaped-entities": "off", '@typescript-eslint/no-explicit-any': 0, + "react/prop-types": "off", "react-refresh/only-export-components": [ "warn", { allowConstantExport: true }, diff --git a/src/__test__/passwordUpdate.test.tsx b/src/__test__/passwordUpdate.test.tsx new file mode 100644 index 0000000..ec8155b --- /dev/null +++ b/src/__test__/passwordUpdate.test.tsx @@ -0,0 +1,122 @@ +import "@testing-library/jest-dom"; +import { + render, + screen, + fireEvent, + act, + waitFor, +} from "@testing-library/react"; +import { Provider } from "react-redux"; +import { BrowserRouter as Router } from "react-router-dom"; +import configureStore from "redux-mock-store"; +import { thunk } from "redux-thunk"; +import { ToastContainer } from "react-toastify"; + +import UpdatePasswordmod from "../components/password/updateModal"; +// import { updatePassword } from "../redux/api/updatePasswordApiSlice"; +// import updatePasswordApiSlice from "../redux/api/updatePasswordApiSlice"; + +jest.mock("react-toastify", () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + }, + ToastContainer: () =>
, +})); + +jest.mock("react-redux", () => ({ + ...jest.requireActual("react-redux"), + useDispatch: () => jest.fn(), +})); + +const middlewares = [thunk]; +// @ts-ignore +const mockStore = configureStore(middlewares); +const setPasswordModal = jest.fn(); +// @ts-ignore +const renderComponent = (store) => render( + + + + + + , +); + +describe("Update Password Modal", () => { + let store; + beforeEach(() => { + store = mockStore({ + updatePassword: { + loading: false, + }, + }); + jest.clearAllMocks(); + }); + + it("update Password Modal renders correctly", () => { + renderComponent(store); + expect(screen.getByPlaceholderText("Old Password")).toBeInTheDocument(); + expect(screen.getByPlaceholderText("New Password")).toBeInTheDocument(); + expect(screen.getByPlaceholderText("Confirm Password")).toBeInTheDocument(); + }); + + it("handles input and form submission", async () => { + // const mockUpdatePassword = jest.fn(); + // (useDispatch as unknown as jest.Mock).mockReturnValue(mockUpdatePassword); + const mockDispatch = jest.fn(); + jest.mock("react-redux", () => ({ + useDispatch: () => mockDispatch, + })); + + renderComponent(store); + const currentPasswordInput = screen.getByPlaceholderText("Old Password"); + const newPasswordInput = screen.getByPlaceholderText("New Password"); + const confirmNewPasswordInput = screen.getByPlaceholderText("Confirm Password"); + const updateButton = screen.getByRole("button", { name: /Save Changes/i }); + + await act(() => { + fireEvent.change(currentPasswordInput, { target: { value: "Test@123" } }); + fireEvent.change(newPasswordInput, { target: { value: "NewTest@123" } }); + fireEvent.change(confirmNewPasswordInput, { + target: { value: "NewTest@123" }, + }); + }); + + await act(() => { + fireEvent.click(updateButton); + console.log("updateButton", updateButton.textContent); + }); + + await waitFor(() => { + expect(mockDispatch).toHaveBeenCalledTimes(0); + expect(updateButton).toHaveTextContent("Save Changes"); + expect(setPasswordModal).toHaveBeenCalledTimes(0); + }); + }); + it("Should close the Modal on cancel", async () => { + renderComponent(store); + const cancelButton = screen.getByRole("button", { name: /Cancel/i }); + await act(() => { + fireEvent.click(cancelButton); + }); + await waitFor(() => { + expect(setPasswordModal).toHaveBeenCalledTimes(1); + }); + }); + + it("Should show PassWord and hide Password", async () => { + renderComponent(store); + const passwordInput = screen.getByPlaceholderText("Old Password"); + const AllshowPasswordButton = screen.getAllByRole("button", { + name: /Show/i, + }); + const showPasswordButton = AllshowPasswordButton[0]; + await act(() => { + fireEvent.click(showPasswordButton); + }); + await waitFor(() => { + expect(passwordInput).toHaveAttribute("type", "text"); + }); + }); +}); diff --git a/src/__test__/updatePasswordApiSlice.test.tsx b/src/__test__/updatePasswordApiSlice.test.tsx new file mode 100644 index 0000000..793405c --- /dev/null +++ b/src/__test__/updatePasswordApiSlice.test.tsx @@ -0,0 +1,56 @@ +import { configureStore } from "@reduxjs/toolkit"; +import axios from "axios"; + +import updatePasswordApiSlice, { + updatePassword, +} from "../redux/api/updatePasswordApiSlice"; + +jest.mock("axios"); + +describe("updatePasswordApiSlice", () => { + let store; + + beforeEach(() => { + store = configureStore({ + reducer: { + updatePassword: updatePasswordApiSlice, + }, + }); + }); + + it("handles successful password update", async () => { + const mockResponse = { data: { message: "Password updated successfully" } }; + // @ts-ignore + axios.put.mockResolvedValueOnce(mockResponse); + + await store.dispatch( + updatePassword({ + oldPassword: "Test@123", + newPassword: "NewTest@123", + confirmPassword: "NewTest@123", + }), + ); + + const state = store.getState(); + expect(state.updatePassword.loading).toBe(false); + expect(state.updatePassword.error).toBe(null); + }); + + it("handles failed password update", async () => { + const mockError = { response: { data: { message: "Update failed" } } }; + // @ts-ignore + axios.put.mockRejectedValueOnce(mockError); + + await store.dispatch( + updatePassword({ + oldPassword: "Test@123", + newPassword: "NewTest@123", + confirmPassword: "NewTest@123", + }), + ); + + const state = store.getState(); + expect(state.updatePassword.loading).toBe(false); + expect(state.updatePassword.error).toBe(null); + }); +}); diff --git a/src/components/common/auth/Button.tsx b/src/components/common/auth/Button.tsx index 59d634b..94f64ed 100644 --- a/src/components/common/auth/Button.tsx +++ b/src/components/common/auth/Button.tsx @@ -5,6 +5,8 @@ interface ButtonProps { disabled?: boolean; dataTestId?: string; backgroundColor?: string; + className?: string; + onClick?: () => void; } const Button: React.FC = ({ @@ -12,12 +14,15 @@ const Button: React.FC = ({ disabled, dataTestId, backgroundColor, + className, + onClick, }) => ( diff --git a/src/components/common/auth/InputField.tsx b/src/components/common/auth/InputField.tsx index 1d00197..7a9ebc0 100644 --- a/src/components/common/auth/InputField.tsx +++ b/src/components/common/auth/InputField.tsx @@ -8,6 +8,7 @@ interface InputFieldProps { // eslint-disable-next-line @typescript-eslint/no-explicit-any register: UseFormRegister; error: string | undefined; + className?: string; } const InputField: React.FC = ({ @@ -16,10 +17,11 @@ const InputField: React.FC = ({ placeholder, register, error, + className, }) => (
= ({ + id, + placeholder, + register, + error, +}) => { + const [showPassword, setShowPassword] = useState(false); + + return ( +
+ + + {error &&

{error.message}

} +
+ ); +}; + +export default PasswordInput; diff --git a/src/components/password/updateModal.tsx b/src/components/password/updateModal.tsx new file mode 100644 index 0000000..9ed4748 --- /dev/null +++ b/src/components/password/updateModal.tsx @@ -0,0 +1,102 @@ +import React, { useState } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { toast, ToastContainer } from "react-toastify"; +import { AxiosError } from "axios"; +import { useDispatch } from "react-redux"; + +import updatePasswordSchema from "../../schemas/updatePasswordSchema"; +import { updatePassword } from "../../redux/api/updatePasswordApiSlice"; +import PasswordInput from "../common/auth/password"; + +interface UpdatePasswordProps { + setPasswordModal: (isOpen: boolean) => void; +} + +const UpdatePasswordmod: React.FC = ({ + setPasswordModal, +}) => { + const dispatch = useDispatch(); + const [loading, setLoading] = useState(false); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: yupResolver(updatePasswordSchema), + }); + + const onSubmit = async (data: { + oldPassword: string; + newPassword: string; + confirmPassword: string; + }) => { + try { + setLoading(true); + // @ts-ignore + const response = await dispatch(updatePassword(data)).unwrap(); + setLoading(false); + toast.success(response.message); + setTimeout(() => { + setPasswordModal(false); + }, 3000); + } catch (err) { + setLoading(false); + const error = err as AxiosError; + toast.error(error.message); + } + }; + + return ( +
+
+ +

+ Update Password +

+
+ + + + +
+ +
+
+ + +
+ +
+
+ ); +}; + +export default UpdatePasswordmod; diff --git a/src/pages/passwordUpdatePage.tsx b/src/pages/passwordUpdatePage.tsx new file mode 100644 index 0000000..f24412f --- /dev/null +++ b/src/pages/passwordUpdatePage.tsx @@ -0,0 +1,25 @@ +import { useState } from "react"; + +import UpdatePasswordmod from "../components/password/updateModal"; + +const UpdatePasswordPage = () => { + const [PasswordModal, setPasswordModal] = useState(false); + return ( +
+ + {PasswordModal && ( + + )} +
+ ); +}; + +export default UpdatePasswordPage; diff --git a/src/redux/api/updatePasswordApiSlice.ts b/src/redux/api/updatePasswordApiSlice.ts new file mode 100644 index 0000000..4afe4e1 --- /dev/null +++ b/src/redux/api/updatePasswordApiSlice.ts @@ -0,0 +1,69 @@ +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; +import { AxiosError } from "axios"; + +import axios from "./api"; + +interface UpdatePasswordState { + loading: boolean; + error: string | null; +} + +const initialState: UpdatePasswordState = { + loading: false, + error: null, +}; + +interface UpdatePasswordPayload { + oldPassword: string; + newPassword: string; + confirmPassword: string; +} + +interface UpdatePasswordResponse { + message: string; +} + +interface UpdatePasswordError { + message: string; +} +const token = localStorage.getItem("accessToken"); +export const updatePassword = createAsyncThunk< +UpdatePasswordResponse, +UpdatePasswordPayload, +{ rejectValue: UpdatePasswordError } +>("updatePassword", async (payload, { rejectWithValue }) => { + try { + const response = await axios.put("/users/passwordupdate", payload, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + return response.data; + } catch (error) { + const axiosError = error as AxiosError; + return rejectWithValue(axiosError.response?.data as UpdatePasswordError); + } +}); + +const updatePasswordApiSlice = createSlice({ + name: "updatePassword", + initialState, + reducers: {}, + extraReducers: (builder) => { + builder + .addCase(updatePassword.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(updatePassword.fulfilled, (state) => { + state.loading = false; + state.error = null; + }) + .addCase(updatePassword.rejected, (state, { payload }) => { + state.loading = false; + state.error = payload?.message || null; + }); + }, +}); + +export default updatePasswordApiSlice.reducer; diff --git a/src/redux/store.ts b/src/redux/store.ts index 3105127..5eaf138 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -6,6 +6,7 @@ import otpVerificationReucer from "./api/otpApiSclice"; import getLinkReducer from "./reducers/getLinkSlice"; import resetReducer from "./reducers/resetPasswordSlice"; import categoriesReducer from "./reducers/categoriesSlice"; +import updatePasswordApiSlice from "./api/updatePasswordApiSlice"; const store = configureStore({ reducer: { @@ -15,6 +16,7 @@ const store = configureStore({ getLink: getLinkReducer, reset: resetReducer, categories: categoriesReducer, + updatePassword: updatePasswordApiSlice, }, }); diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx index 0e477f9..a768e5d 100644 --- a/src/routes/AppRoutes.tsx +++ b/src/routes/AppRoutes.tsx @@ -11,6 +11,7 @@ import GetLinkPage from "../pages/GetLinkPage"; import ResetPassword from "../pages/ResetPassword"; import SellerDashboard from "../dashboard/sellers/Index"; import AddProduct from "../dashboard/sellers/AddProduct"; +import UpdatePasswordPage from "../pages/passwordUpdatePage"; const AppRoutes = () => ( @@ -26,6 +27,7 @@ const AppRoutes = () => ( } /> } /> } /> + } /> ); diff --git a/src/schemas/updatePasswordSchema.ts b/src/schemas/updatePasswordSchema.ts new file mode 100644 index 0000000..6bfd0b1 --- /dev/null +++ b/src/schemas/updatePasswordSchema.ts @@ -0,0 +1,24 @@ +import { object, string, ref } from "yup"; + +const updatePasswordSchema = object({ + oldPassword: string().required("Old password is required"), + newPassword: string() + .required("New password is required") + .min(8, "Password must be at least 8 characters long") + .matches(/[a-z]/, "Password must contain at least one lowercase letter") + .matches(/[A-Z]/, "Password must contain at least one uppercase letter") + .matches(/[0-9]/, "Password must contain at least one number") + .matches( + /[!@#$%^&*(),.?":{}|<>]/, + "Password must contain at least one special character", + ) + .notOneOf( + [ref("oldPassword")], + "New password must be different from old password", + ), + confirmPassword: string() + .required("Confirm password is required") + .oneOf([ref("newPassword")], "Confirm password must match new password"), +}); + +export default updatePasswordSchema;