From 5c4db7764d8c8e66e6e39697f776e7e2cc7f35fe Mon Sep 17 00:00:00 2001
From: Mateusz Winiarczyk <mateusz.winiarczyk@appunite.com>
Date: Wed, 20 Dec 2023 09:19:36 +0100
Subject: [PATCH] test(overlays): add test for login component and thunk

---
 .../LoginModal/LoginModal.component.test.tsx  | 61 +++++++++++++++++++
 .../Modal/LoginModal/LoginModal.component.tsx | 13 +++-
 .../UserOverlays/UserOverlays.component.tsx   |  5 --
 src/redux/root/init.thunks.ts                 |  4 ++
 src/redux/user/user.reducers.test.ts          |  5 +-
 src/redux/user/user.thunks.test.ts            | 53 ++++++++++++++++
 src/redux/user/user.thunks.ts                 |  5 +-
 7 files changed, 135 insertions(+), 11 deletions(-)
 create mode 100644 src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.test.tsx
 create mode 100644 src/redux/user/user.thunks.test.ts

diff --git a/src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.test.tsx b/src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.test.tsx
new file mode 100644
index 00000000..4b59aaa7
--- /dev/null
+++ b/src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.test.tsx
@@ -0,0 +1,61 @@
+import { render, screen, fireEvent } from '@testing-library/react';
+import { StoreType } from '@/redux/store';
+import {
+  InitialStoreState,
+  getReduxWrapperWithStore,
+} from '@/utils/testing/getReduxWrapperWithStore';
+import { act } from 'react-dom/test-utils';
+import { LoginModal } from './LoginModal.component';
+
+const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
+  const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
+
+  return (
+    render(
+      <Wrapper>
+        <LoginModal />
+      </Wrapper>,
+    ),
+    {
+      store,
+    }
+  );
+};
+
+test('renders LoginModal component', () => {
+  renderComponent();
+
+  const loginInput = screen.getByLabelText(/login/i);
+  const passwordInput = screen.getByLabelText(/password/i);
+  expect(loginInput).toBeInTheDocument();
+  expect(passwordInput).toBeInTheDocument();
+});
+
+test('handles input change correctly', () => {
+  renderComponent();
+
+  const loginInput: HTMLInputElement = screen.getByLabelText(/login/i);
+  const passwordInput: HTMLInputElement = screen.getByLabelText(/password/i);
+
+  fireEvent.change(loginInput, { target: { value: 'testuser' } });
+  fireEvent.change(passwordInput, { target: { value: 'testpassword' } });
+
+  expect(loginInput.value).toBe('testuser');
+  expect(passwordInput.value).toBe('testpassword');
+});
+
+test('submits form', () => {
+  renderComponent();
+
+  const loginInput = screen.getByLabelText(/login/i);
+  const passwordInput = screen.getByLabelText(/password/i);
+  const submitButton = screen.getByText(/submit/i);
+
+  fireEvent.change(loginInput, { target: { value: 'testuser' } });
+  fireEvent.change(passwordInput, { target: { value: 'testpassword' } });
+  act(() => {
+    submitButton.click();
+  });
+
+  expect(submitButton).toBeDisabled();
+});
diff --git a/src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.tsx b/src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.tsx
index 30a0f7f0..354b5337 100644
--- a/src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.tsx
+++ b/src/components/FunctionalArea/Modal/LoginModal/LoginModal.component.tsx
@@ -1,4 +1,6 @@
 import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
+import { useAppSelector } from '@/redux/hooks/useAppSelector';
+import { loadingUserSelector } from '@/redux/user/user.selectors';
 import { login } from '@/redux/user/user.thunks';
 import { Button } from '@/shared/Button';
 import Link from 'next/link';
@@ -6,6 +8,8 @@ import React from 'react';
 
 export const LoginModal: React.FC = () => {
   const dispatch = useAppDispatch();
+  const loadingUser = useAppSelector(loadingUserSelector);
+  const isPending = loadingUser === 'pending';
   const [credentials, setCredentials] = React.useState({ login: '', password: '' });
 
   const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
@@ -15,8 +19,7 @@ export const LoginModal: React.FC = () => {
 
   const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
     e.preventDefault();
-    const formBody = new URLSearchParams(credentials);
-    dispatch(login(formBody));
+    dispatch(login(credentials));
   };
 
   return (
@@ -51,7 +54,11 @@ export const LoginModal: React.FC = () => {
             Forgot password?
           </Link>
         </div>
-        <Button type="submit" className="w-full justify-center text-base font-medium">
+        <Button
+          type="submit"
+          className="w-full justify-center text-base font-medium"
+          disabled={isPending}
+        >
           Submit
         </Button>
       </form>
diff --git a/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlays.component.tsx b/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlays.component.tsx
index 24ff90f9..d594f016 100644
--- a/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlays.component.tsx
+++ b/src/components/Map/Drawer/OverlaysDrawer/UserOverlays/UserOverlays.component.tsx
@@ -2,17 +2,12 @@ import { useAppDispatch } from '@/redux/hooks/useAppDispatch';
 import { useAppSelector } from '@/redux/hooks/useAppSelector';
 import { openLoginModal } from '@/redux/modal/modal.slice';
 import { authenticatedUserSelector, loadingUserSelector } from '@/redux/user/user.selectors';
-import { getSessionValid } from '@/redux/user/user.thunks';
 import { Button } from '@/shared/Button';
-import { useEffect } from 'react';
 
 export const UserOverlays = (): JSX.Element => {
   const dispatch = useAppDispatch();
   const loadingUser = useAppSelector(loadingUserSelector);
   const authenticatedUser = useAppSelector(authenticatedUserSelector);
-  useEffect(() => {
-    dispatch(getSessionValid());
-  }, [dispatch]);
 
   const handleLoginClick = (): void => {
     dispatch(openLoginModal());
diff --git a/src/redux/root/init.thunks.ts b/src/redux/root/init.thunks.ts
index b2c3ef2a..6185d064 100644
--- a/src/redux/root/init.thunks.ts
+++ b/src/redux/root/init.thunks.ts
@@ -16,6 +16,7 @@ import {
 } from '../map/map.thunks';
 import { getSearchData } from '../search/search.thunks';
 import { setPerfectMatch } from '../search/search.slice';
+import { getSessionValid } from '../user/user.thunks';
 
 interface InitializeAppParams {
   queryData: QueryData;
@@ -42,6 +43,9 @@ export const fetchInitialAppData = createAsyncThunk<
   /** Create tabs for maps / submaps */
   dispatch(initOpenedMaps({ queryData }));
 
+  // Check if auth token is valid
+  dispatch(getSessionValid());
+
   /** Trigger search */
   if (queryData.searchValue) {
     dispatch(setPerfectMatch(queryData.perfectMatch));
diff --git a/src/redux/user/user.reducers.test.ts b/src/redux/user/user.reducers.test.ts
index 2784cc83..0c89c682 100644
--- a/src/redux/user/user.reducers.test.ts
+++ b/src/redux/user/user.reducers.test.ts
@@ -13,7 +13,10 @@ import userReducer from './user.slice';
 
 const mockedAxiosClient = mockNetworkResponse();
 
-const CREDENTIALS = new URLSearchParams();
+const CREDENTIALS = {
+  login: 'test',
+  password: 'password',
+};
 
 const INITIAL_STATE: UserState = {
   loading: 'idle',
diff --git a/src/redux/user/user.thunks.test.ts b/src/redux/user/user.thunks.test.ts
new file mode 100644
index 00000000..982274d9
--- /dev/null
+++ b/src/redux/user/user.thunks.test.ts
@@ -0,0 +1,53 @@
+import { mockNetworkResponse } from '@/utils/mockNetworkResponse';
+import { HttpStatusCode } from 'axios';
+import { loginFixture } from '@/models/fixtures/loginFixture';
+import {
+  ToolkitStoreWithSingleSlice,
+  createStoreInstanceUsingSliceReducer,
+} from '@/utils/createStoreInstanceUsingSliceReducer';
+import { apiPath } from '../apiPath';
+import { closeModal } from '../modal/modal.slice';
+import userReducer from './user.slice';
+import { UserState } from './user.types';
+import { login } from './user.thunks';
+
+const mockedAxiosClient = mockNetworkResponse();
+const CREDENTIALS = {
+  login: 'test',
+  password: 'password',
+};
+
+describe('login thunk', () => {
+  let store = {} as ToolkitStoreWithSingleSlice<UserState>;
+  beforeEach(() => {
+    store = createStoreInstanceUsingSliceReducer('user', userReducer);
+  });
+
+  it('dispatches closeModal action on successful login with valid data', async () => {
+    mockedAxiosClient.onPost(apiPath.postLogin()).reply(HttpStatusCode.Ok, loginFixture);
+
+    const mockDispatch = jest.fn(action => {
+      if (action.type === closeModal.type) {
+        expect(action).toEqual(closeModal());
+      }
+    });
+
+    store.dispatch = mockDispatch;
+
+    await store.dispatch(login(CREDENTIALS));
+  });
+
+  it('does not dispatch closeModal action on failed login with invalid data', async () => {
+    mockedAxiosClient.onPost(apiPath.postLogin()).reply(HttpStatusCode.NotFound, loginFixture);
+
+    const mockDispatch = jest.fn(action => {
+      if (action.type === closeModal.type) {
+        expect(action).not.toEqual(closeModal());
+      }
+    });
+
+    store.dispatch = mockDispatch;
+
+    await store.dispatch(login(CREDENTIALS));
+  });
+});
diff --git a/src/redux/user/user.thunks.ts b/src/redux/user/user.thunks.ts
index 13ade320..9a35ac49 100644
--- a/src/redux/user/user.thunks.ts
+++ b/src/redux/user/user.thunks.ts
@@ -8,8 +8,9 @@ import { closeModal } from '../modal/modal.slice';
 
 export const login = createAsyncThunk(
   'user/login',
-  async (credentials: URLSearchParams, { dispatch }) => {
-    const response = await axiosInstance.post(apiPath.postLogin(), credentials);
+  async (credentials: { login: string; password: string }, { dispatch }) => {
+    const searchParams = new URLSearchParams(credentials);
+    const response = await axiosInstance.post(apiPath.postLogin(), searchParams);
     const isDataValid = validateDataUsingZodSchema(response.data, loginSchema);
     dispatch(closeModal());
 
-- 
GitLab