From 8eea9f3d2b854603a0d371297e1de3d13f9526b8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Adrian=20Or=C5=82=C3=B3w?= <adrian.orlow@fishbrain.com>
Date: Tue, 24 Oct 2023 14:02:57 +0200
Subject: [PATCH] test: add tests for backgrounds, overlays, project

---
 .../BioEntitiesAccordion.component.test.tsx   |  8 +-
 src/models/fixtures/backgroundsFixture.ts     | 10 +++
 src/models/fixtures/overlaysFixture.ts        | 10 +++
 src/models/fixtures/projectFixture.ts         |  9 +++
 src/redux/apiPath.ts                          |  1 +
 .../backgrounds/backgrounds.reducers.test.ts  | 80 +++++++++++++++++++
 src/redux/backgrounds/backgrounds.reducers.ts |  7 ++
 src/redux/backgrounds/backgrounds.thunks.ts   |  2 +-
 src/redux/models/models.reducers.test.ts      |  8 +-
 src/redux/models/models.types.ts              |  2 +-
 src/redux/overlays/overlays.reducers.test.ts  | 79 ++++++++++++++++++
 src/redux/overlays/overlays.reducers.ts       |  7 ++
 src/redux/overlays/overlays.thunks.ts         |  2 +-
 src/redux/project/project.reducers.test.ts    | 77 ++++++++++++++++++
 src/redux/project/project.reducers.ts         | 13 ++-
 src/redux/project/project.thunks.ts           |  3 +-
 16 files changed, 303 insertions(+), 15 deletions(-)
 create mode 100644 src/models/fixtures/backgroundsFixture.ts
 create mode 100644 src/models/fixtures/overlaysFixture.ts
 create mode 100644 src/models/fixtures/projectFixture.ts
 create mode 100644 src/redux/backgrounds/backgrounds.reducers.test.ts
 create mode 100644 src/redux/overlays/overlays.reducers.test.ts
 create mode 100644 src/redux/project/project.reducers.test.ts

diff --git a/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx b/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx
index 3e62bb01..fc02472a 100644
--- a/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx
+++ b/src/components/Map/Drawer/SearchDrawerWrapper/GroupedSearchResults/BioEntitiesAccordion/BioEntitiesAccordion.component.test.tsx
@@ -1,12 +1,12 @@
+import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFixture';
+import { MODELS_MOCK } from '@/models/mocks/modelsMock';
 import { StoreType } from '@/redux/store';
+import { Accordion } from '@/shared/Accordion';
 import {
   InitialStoreState,
   getReduxWrapperWithStore,
 } from '@/utils/testing/getReduxWrapperWithStore';
 import { render, screen } from '@testing-library/react';
-import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFixture';
-import { Accordion } from '@/shared/Accordion';
-import { MODELS_MOCK } from '@/models/mocks/modelsMock';
 import { BioEntitiesAccordion } from './BioEntitiesAccordion.component';
 
 const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
@@ -35,7 +35,7 @@ describe('BioEntitiesAccordion - component', () => {
         error: { name: '', message: '' },
       },
       models: {
-        data: undefined,
+        data: [],
         loading: 'pending',
         error: { name: '', message: '' },
       },
diff --git a/src/models/fixtures/backgroundsFixture.ts b/src/models/fixtures/backgroundsFixture.ts
new file mode 100644
index 00000000..e06e5c15
--- /dev/null
+++ b/src/models/fixtures/backgroundsFixture.ts
@@ -0,0 +1,10 @@
+import { ZOD_SEED } from '@/constants';
+import { z } from 'zod';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { createFixture } from 'zod-fixture';
+import { mapBackground } from '../mapBackground';
+
+export const backgroundsFixture = createFixture(z.array(mapBackground), {
+  seed: ZOD_SEED,
+  array: { min: 2, max: 2 },
+});
diff --git a/src/models/fixtures/overlaysFixture.ts b/src/models/fixtures/overlaysFixture.ts
new file mode 100644
index 00000000..c0a26efd
--- /dev/null
+++ b/src/models/fixtures/overlaysFixture.ts
@@ -0,0 +1,10 @@
+import { ZOD_SEED } from '@/constants';
+import { z } from 'zod';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { createFixture } from 'zod-fixture';
+import { mapOverlay } from '../mapOverlay';
+
+export const overlaysFixture = createFixture(z.array(mapOverlay), {
+  seed: ZOD_SEED,
+  array: { min: 2, max: 2 },
+});
diff --git a/src/models/fixtures/projectFixture.ts b/src/models/fixtures/projectFixture.ts
new file mode 100644
index 00000000..99e01bb3
--- /dev/null
+++ b/src/models/fixtures/projectFixture.ts
@@ -0,0 +1,9 @@
+import { ZOD_SEED } from '@/constants';
+// eslint-disable-next-line import/no-extraneous-dependencies
+import { createFixture } from 'zod-fixture';
+import { projectSchema } from '../project';
+
+export const projectFixture = createFixture(projectSchema, {
+  seed: ZOD_SEED,
+  array: { min: 1, max: 1 },
+});
diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts
index 30395bcb..776993ed 100644
--- a/src/redux/apiPath.ts
+++ b/src/redux/apiPath.ts
@@ -16,4 +16,5 @@ export const apiPath = {
   ): string => `projects/${projectId}/overlays/?publicOverlay=${String(publicOverlay)}`,
   getAllBackgroundsByProjectIdQuery: (projectId: string): string =>
     `projects/${projectId}/backgrounds/`,
+  getProjectById: (projectId: string): string => `projects/${projectId}`,
 };
diff --git a/src/redux/backgrounds/backgrounds.reducers.test.ts b/src/redux/backgrounds/backgrounds.reducers.test.ts
new file mode 100644
index 00000000..9b99161a
--- /dev/null
+++ b/src/redux/backgrounds/backgrounds.reducers.test.ts
@@ -0,0 +1,80 @@
+import { PROJECT_ID } from '@/constants';
+import { backgroundsFixture } from '@/models/fixtures/backgroundsFixture';
+import {
+  ToolkitStoreWithSingleSlice,
+  createStoreInstanceUsingSliceReducer,
+} from '@/utils/createStoreInstanceUsingSliceReducer';
+import { mockNetworkResponse } from '@/utils/mockNetworkResponse';
+import { HttpStatusCode } from 'axios';
+import { apiPath } from '../apiPath';
+import backgroundsReducer from './backgrounds.slice';
+import { getAllBackgroundsByProjectId } from './backgrounds.thunks';
+import { BackgroundsState } from './backgrounds.types';
+
+const mockedAxiosClient = mockNetworkResponse();
+
+const INITIAL_STATE: BackgroundsState = {
+  data: [],
+  loading: 'idle',
+  error: { name: '', message: '' },
+};
+
+describe('backgrounds reducer', () => {
+  let store = {} as ToolkitStoreWithSingleSlice<BackgroundsState>;
+  beforeEach(() => {
+    store = createStoreInstanceUsingSliceReducer('backgrounds', backgroundsReducer);
+  });
+
+  it('should match initial state', () => {
+    const action = { type: 'unknown' };
+
+    expect(backgroundsReducer(undefined, action)).toEqual(INITIAL_STATE);
+  });
+  it('should update store after succesfull getAllBackgroundsByProjectId query', async () => {
+    mockedAxiosClient
+      .onGet(apiPath.getAllBackgroundsByProjectIdQuery(PROJECT_ID))
+      .reply(HttpStatusCode.Ok, backgroundsFixture);
+
+    const { type } = await store.dispatch(getAllBackgroundsByProjectId(PROJECT_ID));
+    const { data, loading, error } = store.getState().backgrounds;
+
+    expect(type).toBe('backgrounds/getAllBackgroundsByProjectId/fulfilled');
+    expect(loading).toEqual('succeeded');
+    expect(error).toEqual({ message: '', name: '' });
+    expect(data).toEqual(backgroundsFixture);
+  });
+
+  it('should update store after failed getAllBackgroundsByProjectId query', async () => {
+    mockedAxiosClient
+      .onGet(apiPath.getAllBackgroundsByProjectIdQuery(PROJECT_ID))
+      .reply(HttpStatusCode.NotFound, []);
+
+    const { type } = await store.dispatch(getAllBackgroundsByProjectId(PROJECT_ID));
+    const { data, loading, error } = store.getState().backgrounds;
+
+    expect(type).toBe('backgrounds/getAllBackgroundsByProjectId/rejected');
+    expect(loading).toEqual('failed');
+    expect(error).toEqual({ message: '', name: '' });
+    expect(data).toEqual([]);
+  });
+
+  it('should update store on loading getAllBackgroundsByProjectId query', async () => {
+    mockedAxiosClient
+      .onGet(apiPath.getAllBackgroundsByProjectIdQuery(PROJECT_ID))
+      .reply(HttpStatusCode.Ok, backgroundsFixture);
+
+    const actionPromise = store.dispatch(getAllBackgroundsByProjectId(PROJECT_ID));
+
+    const { data, loading } = store.getState().backgrounds;
+    expect(data).toEqual([]);
+    expect(loading).toEqual('pending');
+
+    actionPromise.then(() => {
+      const { data: dataPromiseFulfilled, loading: promiseFulfilled } =
+        store.getState().backgrounds;
+
+      expect(dataPromiseFulfilled).toEqual(backgroundsFixture);
+      expect(promiseFulfilled).toEqual('succeeded');
+    });
+  });
+});
diff --git a/src/redux/backgrounds/backgrounds.reducers.ts b/src/redux/backgrounds/backgrounds.reducers.ts
index 6c099f17..2f0868b4 100644
--- a/src/redux/backgrounds/backgrounds.reducers.ts
+++ b/src/redux/backgrounds/backgrounds.reducers.ts
@@ -5,8 +5,15 @@ import { BackgroundsState } from './backgrounds.types';
 export const getAllBackgroundsByProjectIdReducer = (
   builder: ActionReducerMapBuilder<BackgroundsState>,
 ): void => {
+  builder.addCase(getAllBackgroundsByProjectId.pending, state => {
+    state.loading = 'pending';
+  });
   builder.addCase(getAllBackgroundsByProjectId.fulfilled, (state, action) => {
     state.data = action.payload || [];
     state.loading = 'succeeded';
   });
+  builder.addCase(getAllBackgroundsByProjectId.rejected, state => {
+    state.loading = 'failed';
+    // TODO to discuss manage state of failure
+  });
 };
diff --git a/src/redux/backgrounds/backgrounds.thunks.ts b/src/redux/backgrounds/backgrounds.thunks.ts
index ee0b860f..3741a1c8 100644
--- a/src/redux/backgrounds/backgrounds.thunks.ts
+++ b/src/redux/backgrounds/backgrounds.thunks.ts
@@ -7,7 +7,7 @@ import { z } from 'zod';
 import { apiPath } from '../apiPath';
 
 export const getAllBackgroundsByProjectId = createAsyncThunk(
-  'models/getAllBackgroundsByProjectId',
+  'backgrounds/getAllBackgroundsByProjectId',
   async (projectId: string): Promise<MapBackground[]> => {
     const response = await axiosInstance.get<MapBackground[]>(
       apiPath.getAllBackgroundsByProjectIdQuery(projectId),
diff --git a/src/redux/models/models.reducers.test.ts b/src/redux/models/models.reducers.test.ts
index c130ae0e..1677afdf 100644
--- a/src/redux/models/models.reducers.test.ts
+++ b/src/redux/models/models.reducers.test.ts
@@ -1,13 +1,13 @@
-import { HttpStatusCode } from 'axios';
 import { modelsFixture } from '@/models/fixtures/modelsFixture';
-import { mockNetworkResponse } from '@/utils/mockNetworkResponse';
+import { apiPath } from '@/redux/apiPath';
 import {
   ToolkitStoreWithSingleSlice,
   createStoreInstanceUsingSliceReducer,
 } from '@/utils/createStoreInstanceUsingSliceReducer';
-import { apiPath } from '@/redux/apiPath';
-import { getModels } from './models.thunks';
+import { mockNetworkResponse } from '@/utils/mockNetworkResponse';
+import { HttpStatusCode } from 'axios';
 import modelsReducer from './models.slice';
+import { getModels } from './models.thunks';
 import { ModelsState } from './models.types';
 
 const mockedAxiosClient = mockNetworkResponse();
diff --git a/src/redux/models/models.types.ts b/src/redux/models/models.types.ts
index 06bd1892..6d27b9dc 100644
--- a/src/redux/models/models.types.ts
+++ b/src/redux/models/models.types.ts
@@ -1,4 +1,4 @@
 import { FetchDataState } from '@/types/fetchDataState';
 import { MapModel } from '@/types/models';
 
-export type ModelsState = FetchDataState<MapModel[] | []>;
+export type ModelsState = FetchDataState<MapModel[], []>;
diff --git a/src/redux/overlays/overlays.reducers.test.ts b/src/redux/overlays/overlays.reducers.test.ts
new file mode 100644
index 00000000..d0116134
--- /dev/null
+++ b/src/redux/overlays/overlays.reducers.test.ts
@@ -0,0 +1,79 @@
+import { PROJECT_ID } from '@/constants';
+import { overlaysFixture } from '@/models/fixtures/overlaysFixture';
+import {
+  ToolkitStoreWithSingleSlice,
+  createStoreInstanceUsingSliceReducer,
+} from '@/utils/createStoreInstanceUsingSliceReducer';
+import { mockNetworkResponse } from '@/utils/mockNetworkResponse';
+import { HttpStatusCode } from 'axios';
+import { apiPath } from '../apiPath';
+import overlaysReducer from './overlays.slice';
+import { getAllPublicOverlaysByProjectId } from './overlays.thunks';
+import { OverlaysState } from './overlays.types';
+
+const mockedAxiosClient = mockNetworkResponse();
+
+const INITIAL_STATE: OverlaysState = {
+  data: [],
+  loading: 'idle',
+  error: { name: '', message: '' },
+};
+
+describe('overlays reducer', () => {
+  let store = {} as ToolkitStoreWithSingleSlice<OverlaysState>;
+  beforeEach(() => {
+    store = createStoreInstanceUsingSliceReducer('overlays', overlaysReducer);
+  });
+
+  it('should match initial state', () => {
+    const action = { type: 'unknown' };
+
+    expect(overlaysReducer(undefined, action)).toEqual(INITIAL_STATE);
+  });
+  it('should update store after succesfull getAllPublicOverlaysByProjectId query', async () => {
+    mockedAxiosClient
+      .onGet(apiPath.getAllOverlaysByProjectIdQuery(PROJECT_ID, { publicOverlay: true }))
+      .reply(HttpStatusCode.Ok, overlaysFixture);
+
+    const { type } = await store.dispatch(getAllPublicOverlaysByProjectId(PROJECT_ID));
+    const { data, loading, error } = store.getState().overlays;
+
+    expect(type).toBe('overlays/getAllPublicOverlaysByProjectId/fulfilled');
+    expect(loading).toEqual('succeeded');
+    expect(error).toEqual({ message: '', name: '' });
+    expect(data).toEqual(overlaysFixture);
+  });
+
+  it('should update store after failed getAllPublicOverlaysByProjectId query', async () => {
+    mockedAxiosClient
+      .onGet(apiPath.getAllOverlaysByProjectIdQuery(PROJECT_ID, { publicOverlay: true }))
+      .reply(HttpStatusCode.NotFound, []);
+
+    const { type } = await store.dispatch(getAllPublicOverlaysByProjectId(PROJECT_ID));
+    const { data, loading, error } = store.getState().overlays;
+
+    expect(type).toBe('overlays/getAllPublicOverlaysByProjectId/rejected');
+    expect(loading).toEqual('failed');
+    expect(error).toEqual({ message: '', name: '' });
+    expect(data).toEqual([]);
+  });
+
+  it('should update store on loading getAllPublicOverlaysByProjectId query', async () => {
+    mockedAxiosClient
+      .onGet(apiPath.getAllOverlaysByProjectIdQuery(PROJECT_ID, { publicOverlay: true }))
+      .reply(HttpStatusCode.Ok, overlaysFixture);
+
+    const actionPromise = store.dispatch(getAllPublicOverlaysByProjectId(PROJECT_ID));
+
+    const { data, loading } = store.getState().overlays;
+    expect(data).toEqual([]);
+    expect(loading).toEqual('pending');
+
+    actionPromise.then(() => {
+      const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().overlays;
+
+      expect(dataPromiseFulfilled).toEqual(overlaysFixture);
+      expect(promiseFulfilled).toEqual('succeeded');
+    });
+  });
+});
diff --git a/src/redux/overlays/overlays.reducers.ts b/src/redux/overlays/overlays.reducers.ts
index 09863b89..99e493ea 100644
--- a/src/redux/overlays/overlays.reducers.ts
+++ b/src/redux/overlays/overlays.reducers.ts
@@ -5,8 +5,15 @@ import { OverlaysState } from './overlays.types';
 export const getAllPublicOverlaysByProjectIdReducer = (
   builder: ActionReducerMapBuilder<OverlaysState>,
 ): void => {
+  builder.addCase(getAllPublicOverlaysByProjectId.pending, state => {
+    state.loading = 'pending';
+  });
   builder.addCase(getAllPublicOverlaysByProjectId.fulfilled, (state, action) => {
     state.data = action.payload || [];
     state.loading = 'succeeded';
   });
+  builder.addCase(getAllPublicOverlaysByProjectId.rejected, state => {
+    state.loading = 'failed';
+    // TODO to discuss manage state of failure
+  });
 };
diff --git a/src/redux/overlays/overlays.thunks.ts b/src/redux/overlays/overlays.thunks.ts
index 3dc3c70e..6a019333 100644
--- a/src/redux/overlays/overlays.thunks.ts
+++ b/src/redux/overlays/overlays.thunks.ts
@@ -7,7 +7,7 @@ import { z } from 'zod';
 import { apiPath } from '../apiPath';
 
 export const getAllPublicOverlaysByProjectId = createAsyncThunk(
-  'models/getAllPublicOverlaysByProjectId',
+  'overlays/getAllPublicOverlaysByProjectId',
   async (projectId: string): Promise<MapOverlay[]> => {
     const response = await axiosInstance.get<MapOverlay[]>(
       apiPath.getAllOverlaysByProjectIdQuery(projectId, { publicOverlay: true }),
diff --git a/src/redux/project/project.reducers.test.ts b/src/redux/project/project.reducers.test.ts
new file mode 100644
index 00000000..28b9ef70
--- /dev/null
+++ b/src/redux/project/project.reducers.test.ts
@@ -0,0 +1,77 @@
+import { PROJECT_ID } from '@/constants';
+import { projectFixture } from '@/models/fixtures/projectFixture';
+import {
+  ToolkitStoreWithSingleSlice,
+  createStoreInstanceUsingSliceReducer,
+} from '@/utils/createStoreInstanceUsingSliceReducer';
+import { mockNetworkResponse } from '@/utils/mockNetworkResponse';
+import { HttpStatusCode } from 'axios';
+import { apiPath } from '../apiPath';
+import projectReducer from './project.slice';
+import { getProjectById } from './project.thunks';
+import { ProjectState } from './project.types';
+
+const mockedAxiosClient = mockNetworkResponse();
+
+const INITIAL_STATE: ProjectState = {
+  data: undefined,
+  loading: 'idle',
+  error: { name: '', message: '' },
+};
+
+describe('project reducer', () => {
+  let store = {} as ToolkitStoreWithSingleSlice<ProjectState>;
+  beforeEach(() => {
+    store = createStoreInstanceUsingSliceReducer('project', projectReducer);
+  });
+
+  it('should match initial state', () => {
+    const action = { type: 'unknown' };
+
+    expect(projectReducer(undefined, action)).toEqual(INITIAL_STATE);
+  });
+  it('should update store after succesfull getProjectById query', async () => {
+    mockedAxiosClient
+      .onGet(apiPath.getProjectById(PROJECT_ID))
+      .reply(HttpStatusCode.Ok, projectFixture);
+
+    const { type } = await store.dispatch(getProjectById(PROJECT_ID));
+    const { data, loading, error } = store.getState().project;
+
+    expect(type).toBe('project/getProjectById/fulfilled');
+    expect(loading).toEqual('succeeded');
+    expect(error).toEqual({ message: '', name: '' });
+    expect(data).toEqual(projectFixture);
+  });
+
+  it('should update store after failed getProjectById query', async () => {
+    mockedAxiosClient.onGet(apiPath.getProjectById(PROJECT_ID)).reply(HttpStatusCode.NotFound, []);
+
+    const { type } = await store.dispatch(getProjectById(PROJECT_ID));
+    const { data, loading, error } = store.getState().project;
+
+    expect(type).toBe('project/getProjectById/rejected');
+    expect(loading).toEqual('failed');
+    expect(error).toEqual({ message: '', name: '' });
+    expect(data).toEqual(undefined);
+  });
+
+  it('should update store on loading getProjectById query', async () => {
+    mockedAxiosClient
+      .onGet(apiPath.getProjectById(PROJECT_ID))
+      .reply(HttpStatusCode.Ok, projectFixture);
+
+    const actionPromise = store.dispatch(getProjectById(PROJECT_ID));
+
+    const { data, loading } = store.getState().project;
+    expect(data).toEqual(undefined);
+    expect(loading).toEqual('pending');
+
+    actionPromise.then(() => {
+      const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().project;
+
+      expect(dataPromiseFulfilled).toEqual(projectFixture);
+      expect(promiseFulfilled).toEqual('succeeded');
+    });
+  });
+});
diff --git a/src/redux/project/project.reducers.ts b/src/redux/project/project.reducers.ts
index aee885ae..435d55e8 100644
--- a/src/redux/project/project.reducers.ts
+++ b/src/redux/project/project.reducers.ts
@@ -1,10 +1,17 @@
-import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
-import { ProjectState } from '@/redux/project/project.types';
 import { getProjectById } from '@/redux/project/project.thunks';
+import { ProjectState } from '@/redux/project/project.types';
+import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
 
 export const getProjectByIdReducer = (builder: ActionReducerMapBuilder<ProjectState>): void => {
+  builder.addCase(getProjectById.pending, state => {
+    state.loading = 'pending';
+  });
   builder.addCase(getProjectById.fulfilled, (state, action) => {
-    state.data = action.payload;
+    state.data = action.payload || undefined;
     state.loading = 'succeeded';
   });
+  builder.addCase(getProjectById.rejected, state => {
+    state.loading = 'failed';
+    // TODO to discuss manage state of failure
+  });
 };
diff --git a/src/redux/project/project.thunks.ts b/src/redux/project/project.thunks.ts
index d99f5d55..f3d9fbe2 100644
--- a/src/redux/project/project.thunks.ts
+++ b/src/redux/project/project.thunks.ts
@@ -3,11 +3,12 @@ import { axiosInstance } from '@/services/api/utils/axiosInstance';
 import { Project } from '@/types/models';
 import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema';
 import { createAsyncThunk } from '@reduxjs/toolkit';
+import { apiPath } from '../apiPath';
 
 export const getProjectById = createAsyncThunk(
   'project/getProjectById',
   async (id: string): Promise<Project | undefined> => {
-    const response = await axiosInstance.get<Project>(`projects/${id}`);
+    const response = await axiosInstance.get<Project>(apiPath.getProjectById(id));
 
     const isDataValid = validateDataUsingZodSchema(response.data, projectSchema);
 
-- 
GitLab