Skip to content

Commit

Permalink
Merge pull request #6 from atlp-rwanda/ft-seller-auth-2f-#187419122
Browse files Browse the repository at this point in the history
#187419122 Seller Authentication and Two factor authentication for seller
  • Loading branch information
teerenzo authored and gisubizo Jovan committed Jun 24, 2024
2 parents 88b567f + bea1c3b commit ff03571
Show file tree
Hide file tree
Showing 25 changed files with 1,350 additions and 1,009 deletions.
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ module.exports = {
"no-undef": "off",
"@typescript-eslint/ban-ts-comment": "off",
"react/no-unescaped-entities": "off",
"react/prop-types": "off",
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ jobs:
uses: codecov/[email protected]
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Coveralls GitHub Action
uses: coverallsapp/[email protected]

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ dist-ssr

# Environment variables
.env
coverage
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The front-end of Eagle E-commerce utilizes React for a modern, user-friendly int
[![Maintainability](https://api.codeclimate.com/v1/badges/81fa30232b27b1482f4f/maintainability)](https://codeclimate.com/github/atlp-rwanda/eagles-ec-fe/maintainability)
![Github Actions](https://github.com/atlp-rwanda/eagles-ec-fe/actions/workflows/deploy.yml/badge.svg)
[![codecov](https://codecov.io/gh/atlp-rwanda/eagles-ec-fe/graph/badge.svg?token=MZAXZNVDXC)](https://codecov.io/gh/atlp-rwanda/eagles-ec-fe)
[![Coverage Status](https://coveralls.io/repos/github/atlp-rwanda/eagles-ec-fe/badge.svg?branch=dev)](https://coveralls.io/github/atlp-rwanda/eagles-ec-fe?branch=dev)

## Tech Stack

Expand Down
1,452 changes: 453 additions & 999 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 12 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,25 @@
"prettier": "prettier . --write"
},
"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@hookform/resolvers": "^3.6.0",
"@reduxjs/toolkit": "^2.2.5",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/jest-dom": "^6.4.6",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"axios": "^1.7.2",
"axios-mock-adapter": "^1.22.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"expect-puppeteer": "^10.0.0",
"jest-environment-jsdom": "^29.7.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"jest-fetch-mock": "^3.0.3",
"jest-mock-extended": "^3.0.7",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.51.5",
"react-icons": "^5.2.1",
"react-modal": "^3.16.1",
"react-redux": "^9.1.2",
"react-router-dom": "^6.23.1",
"react-toastify": "^10.0.5",
Expand All @@ -43,11 +50,13 @@
"@types/jest": "^29.5.12",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@types/react-modal": "^3.16.3",
"@types/react-redux": "^7.1.33",
"@types/react-router-dom": "^5.3.3",
"@types/redux": "^3.6.0",
"@types/redux-mock-store": "^1.0.6",
"@types/redux-thunk": "^2.1.0",
"@types/testing-library__react": "^10.2.0",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitejs/plugin-react-swc": "^3.5.0",
Expand Down
30 changes: 30 additions & 0 deletions src/__test__/otpApiSclice.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { verifyOtp, otpVerificationApiSlice } from "../redux/api/otpApiSclice";

const { reducer } = otpVerificationApiSlice;
describe("otpVerification slice", () => {
it("handles pending state on verifyOtp.pending", () => {
// @ts-ignore
const initialState = reducer(undefined, { type: verifyOtp.pending });
expect(initialState.loading).toBeTruthy();
});

it("handles fulfilled state and data on verifyOtp.fulfilled", () => {
const mockData = { message: "Success" };
const initialState = reducer(undefined, {
// @ts-ignore
type: verifyOtp.fulfilled,
payload: mockData,
});
expect(initialState.success).toBeTruthy();
});

it("handles rejected state and error on verifyOtp.rejected", () => {
const error = { message: "Error" };
const initialState = reducer(undefined, {
// @ts-ignore
type: verifyOtp.rejected,
payload: error,
});
expect(initialState.error).toBe("Error");
});
});
160 changes: 160 additions & 0 deletions src/__test__/otpVerfication.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import "@testing-library/jest-dom";
import {
render, screen, fireEvent, waitFor,
} from "@testing-library/react";
import { Provider } from "react-redux";
import { BrowserRouter } from "react-router-dom";
import configureStore from "redux-mock-store";
import { thunk } from "redux-thunk";
import { toast } from "react-toastify";

import OtpVerificationForm from "../pages/otpVerfication";

jest.mock("react-toastify", () => ({
toast: {
// @ts-ignore
success: jest.fn(),
error: jest.fn(),
},
ToastContainer: () => <div />,
}));

jest.mock("../redux/api/otpApiSclice", () => ({
verifyOtp: jest.fn(),
}));

const middlewares = [thunk];
// @ts-ignores
const mockStore = configureStore(middlewares);
// @ts-ignore
const renderComponent = (store) => render(
<Provider store={store}>
<BrowserRouter>
<OtpVerificationForm />
</BrowserRouter>
</Provider>,
);

describe("OtpVerification", () => {
// @ts-ignore
let store;
beforeEach(() => {
store = mockStore({
otpVerification: {
loading: false,
},
});
localStorage.clear();
jest.clearAllMocks();
});

it("renders correctly", () => {
renderComponent(store);
expect(screen.getByText("verify your identity")).toBeInTheDocument();
expect(
screen.getByText(
"Protecting your account is our priority. Please confirm your identity by providing the code sent to your email address",
),
).toBeInTheDocument();
});
it("navigates to login on cancel", () => {
renderComponent(store);
fireEvent.click(screen.getByText("Cancel"));
expect(window.location.pathname).toBe("/login");
});

it("focuses on the next input on entering a digit", () => {
renderComponent(store);
const inputs = screen.getAllByRole("textbox");
inputs.forEach((input, index) => {
fireEvent.change(input, { target: { value: "1" } });
if (index < inputs.length - 1) {
expect(document.activeElement).toBe(inputs[index + 1]);
}
});
});
it("handles paste event", () => {
renderComponent(store);
const inputs = screen.getAllByRole("textbox");
fireEvent.paste(inputs[0], {
clipboardData: {
getData: () => "123456",
},
});
inputs.forEach((input, index) => {
// @ts-ignore
expect(input.value).toBe(String(index + 1));
});
});

it("handles backspace correctly", () => {
renderComponent(store);
const inputs = screen.getAllByRole("textbox");
inputs.forEach((input, index) => {
fireEvent.change(input, { target: { value: String(index + 1) } });
});
fireEvent.keyDown(inputs[5], { key: "Backspace", code: "Backspace" });
expect(document.activeElement).toBe(inputs[5]);
});

it("should handle submit with success", async () => {
const { getByText } = renderComponent(store);
const form = getByText("Ver");
fireEvent.submit(form);
expect(toast.success).toHaveBeenCalledTimes(0);
});

it("should handle submit with error", async () => {
const { getByText } = renderComponent(store);
const form = getByText("Ver");
fireEvent.submit(form);
await waitFor(() => expect(toast.error).toHaveBeenCalledTimes(0));
});

it("should handle backspace key press", () => {
const { getAllByRole } = renderComponent(store);
const inputs = getAllByRole("textbox");
fireEvent.keyDown(inputs[0], { key: "Backspace" });
// @ts-ignore
expect(inputs[0].value).toBe("");
});

it("should handle paste with same length", () => {
const { getAllByRole } = renderComponent(store);
const inputs = getAllByRole("textbox");
const pasteValue = "123456";
fireEvent.paste(inputs[0], {
clipboardData: { getData: () => pasteValue },
});
// @ts-ignore
inputs.forEach((input, index) => expect(input.value).toBe(pasteValue[index]));
});
it("should handle paste with shorter length", () => {
const { getAllByRole } = renderComponent(store);
const inputs = getAllByRole("textbox");
const pasteValue = "123";
fireEvent.paste(inputs[0], {
clipboardData: { getData: () => pasteValue },
});
inputs.forEach((input, index) => {
if (index < pasteValue.length) {
// @ts-ignore
expect(input.value).toBe(pasteValue[index]);
} else {
// @ts-ignore
expect(input.value).toBe("");
}
});
});

it("should render form elements", () => {
const { getByText } = renderComponent(store);

expect(
getByText(
"Protecting your account is our priority. Please confirm your identity by providing the code sent to your email address",
),
).toBeInTheDocument();
expect(getByText("Cancel")).toBeInTheDocument();
});
});
32 changes: 32 additions & 0 deletions src/__test__/updatePassword.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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 { ToastContainer } from "react-toastify";

import UpdatePasswordmod from "../components/password/UpdateModal";
import store from "../redux/store";

test("test update password", () => {
const setPasswordModal = jest.fn();
render(
<Provider store={store}>
<Router>
<UpdatePasswordmod setPasswordModal={setPasswordModal} />
<ToastContainer />
</Router>
</Provider>,
);

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 });

userEvent.type(currentPasswordInput, "Test@123");
userEvent.type(newPasswordInput, "NewTest@123");
userEvent.type(confirmNewPasswordInput, "NewTest@123");

userEvent.click(updateButton);
});
14 changes: 11 additions & 3 deletions src/components/common/auth/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,25 @@ interface ButtonProps {
text: string;
disabled?: boolean;
dataTestId?: string;
className?: string;
onClick?: () => void;
}

const Button: React.FC<ButtonProps> = ({ text, disabled, dataTestId }) => (
const Button: React.FC<ButtonProps> = ({
text,
disabled,
dataTestId,
className,
onClick,
}) => (
<button
type="submit"
className="bg-[#161616] text-white py-3 my-4 text-normal md:text-lg rounded-sm"
className={`bg-[#161616] text-white py-3 my-4 text-normal md:text-lg rounded-sm ${className}`}
disabled={disabled}
data-testid={dataTestId}
onClick={onClick}
>
{text}
</button>
);

export default Button;
4 changes: 3 additions & 1 deletion src/components/common/auth/InputField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface InputFieldProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
register: UseFormRegister<any>;
error: string | undefined;
className?: string;
}

const InputField: React.FC<InputFieldProps> = ({
Expand All @@ -16,10 +17,11 @@ const InputField: React.FC<InputFieldProps> = ({
placeholder,
register,
error,
className,
}) => (
<div className="py-4">
<input
className="w-full border-b bg-transparent font-normal text-normal py-1 border-transparent border-b-gray-500 outline-none"
className={`w-full border-b bg-transparent font-normal text-normal py-1 border-transparent border-b-gray-500 outline-none ${className}`}
type={type}
placeholder={placeholder}
{...register(name)}
Expand Down
40 changes: 40 additions & 0 deletions src/components/common/auth/Password.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React, { useState } from "react";
import { FieldError, UseFormRegisterReturn } from "react-hook-form";

interface PasswordInputProps {
id: string;
placeholder: string;
register: UseFormRegisterReturn;
error: FieldError | undefined;
}

const PasswordInput: React.FC<PasswordInputProps> = ({
id,
placeholder,
register,
error,
}) => {
const [showPassword, setShowPassword] = useState(false);

return (
<div className="mb-4 relative">
<input
type={showPassword ? "text" : "password"}
id={id}
className={`w-full p-2 border-b ${error ? "border-red-500" : "border-gray-300"}`}
placeholder={placeholder}
{...register}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-2 top-1/2 transform -translate-y-1/2"
>
{showPassword ? "Hide Password" : "Show Password"}
</button>
{error && <p className="text-red-500">{error.message}</p>}
</div>
);
};

export default PasswordInput;
Loading

0 comments on commit ff03571

Please sign in to comment.