From 23c3b15983035fe9c6340a773b8ba7dea209f74a Mon Sep 17 00:00:00 2001 From: Anatoliy Sarakhman Date: Tue, 21 Nov 2023 20:07:01 +0200 Subject: [PATCH 1/4] rewrited mutation endpoints using RTK Query --- .../google-button/GoogleButton.jsx | 13 ++-- .../login-dialog/LoginDialog.jsx | 9 ++- .../signup-dialog/SignupDialog.jsx | 9 ++- .../CategoriesContainer.tsx | 24 ++----- src/pages/logout/Logout.tsx | 17 +++-- src/redux/apiSlice.ts | 10 +++ src/redux/reducer.ts | 66 ++--------------- src/redux/store.ts | 12 +++- src/services/auth-service.ts | 46 ++++++++---- src/services/resource-service.ts | 25 ++++--- src/services/setup-interceptors.ts | 8 +-- src/types/common/enums/common.enums.ts | 5 ++ .../user/user-interfaces/user.interfaces.ts | 2 +- .../login-dialog/LoginDialog.spec.jsx | 16 ++++- .../signup-dialog/SignupDialog.spec.jsx | 28 +++++--- tests/unit/context/snackbar-context.spec.jsx | 18 ++++- tests/unit/pages/logout/Logout.spec.jsx | 11 +++ tests/unit/redux/redux.spec.js | 70 ++----------------- 18 files changed, 186 insertions(+), 203 deletions(-) create mode 100644 src/redux/apiSlice.ts diff --git a/src/containers/guest-home-page/google-button/GoogleButton.jsx b/src/containers/guest-home-page/google-button/GoogleButton.jsx index 4e0324b40..0cbaed5be 100644 --- a/src/containers/guest-home-page/google-button/GoogleButton.jsx +++ b/src/containers/guest-home-page/google-button/GoogleButton.jsx @@ -2,7 +2,8 @@ import { useCallback, useEffect } from 'react' import { useDispatch } from 'react-redux' import { useHref } from 'react-router-dom' -import { googleAuth } from '~/redux/reducer' +import { setUser } from '~/redux/reducer' +import { useGoogleAuthMutation } from '~/services/auth-service' import { useModalContext } from '~/context/modal-context' import { useSnackBarContext } from '~/context/snackbar-context' import { scrollToHash } from '~/utils/hash-scroll' @@ -12,21 +13,23 @@ import { snackbarVariants } from '~/constants' import { styles } from '~/containers/guest-home-page/google-button/GoogleButton.styles' const GoogleButton = ({ role, route, buttonWidth, type }) => { + const ref = useHref(route) const dispatch = useDispatch() const mediaQuery = useBreakpoints().isLaptopAndAbove ? 'md' : 'xs' const { closeModal } = useModalContext() const { setAlert } = useSnackBarContext() - const ref = useHref(route) + const [googleAuth] = useGoogleAuthMutation() const handleCredentialResponse = useCallback( async (token) => { try { - await dispatch(googleAuth({ token, role })).unwrap() + const response = await googleAuth({ token, role }).unwrap() + dispatch(setUser(response.accessToken)) closeModal() } catch (e) { setAlert({ severity: snackbarVariants.error, - message: `errors.${e}` + message: `errors.${e.data.code}` }) if (e === 'USER_NOT_FOUND') { closeModal() @@ -34,7 +37,7 @@ const GoogleButton = ({ role, route, buttonWidth, type }) => { } } }, - [dispatch, role, closeModal, setAlert, ref] + [googleAuth, dispatch, role, closeModal, setAlert, ref] ) useEffect(() => { diff --git a/src/containers/guest-home-page/login-dialog/LoginDialog.jsx b/src/containers/guest-home-page/login-dialog/LoginDialog.jsx index 010daaac6..9321a267d 100644 --- a/src/containers/guest-home-page/login-dialog/LoginDialog.jsx +++ b/src/containers/guest-home-page/login-dialog/LoginDialog.jsx @@ -6,12 +6,13 @@ import { useDispatch } from 'react-redux' import GoogleLogin from '~/containers/guest-home-page/google-login/GoogleLogin' import LoginForm from '~/containers/guest-home-page/login-form/LoginForm' import useForm from '~/hooks/use-form' +import { setUser } from '~/redux/reducer' +import { useLoginMutation } from '~/services/auth-service' import { useModalContext } from '~/context/modal-context' import { useSnackBarContext } from '~/context/snackbar-context' import { email } from '~/utils/validations/login' import loginImg from '~/assets/img/login-dialog/login.svg' import { login, snackbarVariants } from '~/constants' -import { loginUser } from '~/redux/reducer' import styles from '~/containers/guest-home-page/login-dialog/LoginDialog.styles' @@ -20,17 +21,19 @@ const LoginDialog = () => { const { closeModal } = useModalContext() const { setAlert } = useSnackBarContext() const dispatch = useDispatch() + const [loginUser] = useLoginMutation() const { handleSubmit, handleInputChange, handleBlur, data, errors } = useForm( { onSubmit: async () => { try { - await dispatch(loginUser(data)).unwrap() + const response = await loginUser(data).unwrap() + dispatch(setUser(response.accessToken)) closeModal() } catch (e) { setAlert({ severity: snackbarVariants.error, - message: `errors.${e}` + message: `errors.${e.data.code}` }) } }, diff --git a/src/containers/guest-home-page/signup-dialog/SignupDialog.jsx b/src/containers/guest-home-page/signup-dialog/SignupDialog.jsx index bf89f009b..7b60edded 100644 --- a/src/containers/guest-home-page/signup-dialog/SignupDialog.jsx +++ b/src/containers/guest-home-page/signup-dialog/SignupDialog.jsx @@ -2,10 +2,10 @@ import { useEffect } from 'react' import Box from '@mui/material/Box' import Typography from '@mui/material/Typography' import { useTranslation } from 'react-i18next' -import { useDispatch } from 'react-redux' import useForm from '~/hooks/use-form' import useConfirm from '~/hooks/use-confirm' +import { useSignUpMutation } from '~/services/auth-service' import { useModalContext } from '~/context/modal-context' import { useSnackBarContext } from '~/context/snackbar-context' @@ -20,7 +20,6 @@ import { signup, snackbarVariants } from '~/constants' import GoogleLogin from '~/containers/guest-home-page/google-login/GoogleLogin' import SignupForm from '~/containers/guest-home-page/signup-form/SignupForm' import NotificationModal from '~/containers/guest-home-page/notification-modal/NotificationModal' -import { signupUser } from '~/redux/reducer' import student from '~/assets/img/signup-dialog/student.svg' import tutor from '~/assets/img/signup-dialog/tutor.svg' @@ -33,7 +32,7 @@ const SignupDialog = ({ type }) => { const { setNeedConfirmation } = useConfirm() const { openModal, closeModal } = useModalContext() const { setAlert } = useSnackBarContext() - const dispatch = useDispatch() + const [signUp] = useSignUpMutation() const signupImg = { student, tutor } @@ -41,7 +40,7 @@ const SignupDialog = ({ type }) => { useForm({ onSubmit: async () => { try { - await dispatch(signupUser({ ...data, role: type })).unwrap() + await signUp({ ...data, role: type }).unwrap() openModal( { component: ( @@ -59,7 +58,7 @@ const SignupDialog = ({ type }) => { } catch (e) { setAlert({ severity: snackbarVariants.error, - message: `errors.${e}` + message: `errors.${e.data.code}` }) } }, diff --git a/src/containers/my-resources/categories-container/CategoriesContainer.tsx b/src/containers/my-resources/categories-container/CategoriesContainer.tsx index e22c6f590..221a8b328 100644 --- a/src/containers/my-resources/categories-container/CategoriesContainer.tsx +++ b/src/containers/my-resources/categories-container/CategoriesContainer.tsx @@ -7,7 +7,10 @@ import Loader from '~/components/loader/Loader' import AppButton from '~/components/app-button/AppButton' import AddCategoriesModal from '~/containers/my-resources/add-categories-modal/AddCategoriesModal' import AddResourceWithInput from '~/containers/my-resources/add-resource-with-input/AddResourceWithInput' -import { ResourceService } from '~/services/resource-service' +import { + ResourceService, + useUpdateResourceCategoryMutation +} from '~/services/resource-service' import MyResourcesTable from '~/containers/my-resources/my-resources-table/MyResourcesTable' import useAxios from '~/hooks/use-axios' import useSort from '~/hooks/table/use-sort' @@ -29,7 +32,6 @@ import { GetResourcesCategoriesParams, ErrorResponse, ResourcesTabsEnum, - UpdateResourceCategory, CreateCategoriesParams } from '~/types' import { ajustColumns, getScreenBasedLimit } from '~/utils/helper-functions' @@ -45,6 +47,7 @@ const CategoriesContainer = () => { const { openModal, closeModal } = useModalContext() const { setAlert } = useSnackBarContext() const [selectedItemId, setSelectedItemId] = useState('') + const [updateResourceCategory] = useUpdateResourceCategoryMutation() const { sort } = sortOptions const itemsPerPage = getScreenBasedLimit(breakpoints, itemsLoadLimit) @@ -90,12 +93,6 @@ const CategoriesContainer = () => { [] ) - const updateCategory = useCallback( - (params?: UpdateResourceCategory) => - ResourceService.updateResourceCategory(params), - [] - ) - const deleteCategory = useCallback( (id?: string) => ResourceService.deleteResourceCategory(id ?? ''), [] @@ -127,14 +124,6 @@ const CategoriesContainer = () => { onResponse: onCategoryCreate }) - const { fetchData: updateData } = useAxios({ - service: updateCategory, - defaultResponse: null, - onResponseError, - onResponse: onCategoryUpdate, - fetchOnMount: false - }) - const onAdd = () => { openModal({ component: ( @@ -146,7 +135,8 @@ const CategoriesContainer = () => { }) } const onSave = async (name: string) => { - if (name) await updateData({ id: selectedItemId, name }) + if (name) await updateResourceCategory({ id: selectedItemId, name }) + onCategoryUpdate() setSelectedItemId('') } const onEdit = (id: string) => setSelectedItemId(id) diff --git a/src/pages/logout/Logout.tsx b/src/pages/logout/Logout.tsx index b053eab5b..0021c012f 100644 --- a/src/pages/logout/Logout.tsx +++ b/src/pages/logout/Logout.tsx @@ -1,18 +1,25 @@ -import { useEffect } from 'react' +import { useEffect, useCallback } from 'react' import { useNavigate } from 'react-router-dom' import { useAppDispatch } from '~/hooks/use-redux' -import { logoutUser } from '~/redux/reducer' +import { logout } from '~/redux/reducer' +import { useLogoutMutation } from '~/services/auth-service' import { guestRoutes } from '~/router/constants/guestRoutes' const Logout = () => { const dispatch = useAppDispatch() const navigate = useNavigate() + const [logoutUser] = useLogoutMutation() - useEffect(() => { - void dispatch(logoutUser()) + const onLogoutUser = useCallback(async () => { + await logoutUser() + dispatch(logout()) navigate(guestRoutes.home.route) - }, [dispatch, navigate]) + }, [logoutUser, dispatch, navigate]) + + useEffect(() => { + void onLogoutUser() + }, [onLogoutUser]) return null } diff --git a/src/redux/apiSlice.ts b/src/redux/apiSlice.ts new file mode 100644 index 000000000..33ad2bae4 --- /dev/null +++ b/src/redux/apiSlice.ts @@ -0,0 +1,10 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' + +export const appApi = createApi({ + baseQuery: fetchBaseQuery({ + baseUrl: import.meta.env.VITE_API_BASE_PATH, + credentials: 'include' + }), + reducerPath: 'appApi', + endpoints: () => ({}) +}) diff --git a/src/redux/reducer.ts b/src/redux/reducer.ts index 2c0c95e20..0b399a5bd 100644 --- a/src/redux/reducer.ts +++ b/src/redux/reducer.ts @@ -6,16 +6,9 @@ import { isFulfilled, isRejected } from '@reduxjs/toolkit' -import { AuthService } from '~/services/auth-service' +import { AuthService, authService } from '~/services/auth-service' import { AxiosError } from 'axios' -import { - AccessToken, - ErrorResponse, - GoogleAuthParams, - LoginParams, - SignupParams, - UserRole -} from '~/types' +import { AccessToken, ErrorResponse, UserRole } from '~/types' interface UserState { userId: string @@ -37,56 +30,7 @@ const initialState: UserState = { isFirstLogin: true } -export const loginUser = createAsyncThunk( - 'appMain/loginUser', - async (userData: LoginParams, { rejectWithValue, dispatch }) => { - try { - const { data } = await AuthService.login(userData) - dispatch(setUser(data.accessToken)) - } catch (e) { - const error = e as AxiosError - return rejectWithValue(error.response?.data.code) - } - } -) - -export const googleAuth = createAsyncThunk( - 'appMain/googleAuth', - async (userData: GoogleAuthParams, { rejectWithValue, dispatch }) => { - try { - const { data } = await AuthService.googleAuth(userData) - dispatch(setUser(data.accessToken)) - } catch (e) { - const error = e as AxiosError - return rejectWithValue(error.response?.data.code) - } - } -) - -export const signupUser = createAsyncThunk( - 'appMain/signupUser', - async (userData: SignupParams, { rejectWithValue }) => { - try { - await AuthService.signup(userData) - } catch (e) { - const error = e as AxiosError - return rejectWithValue(error.response?.data.code) - } - } -) - -export const logoutUser = createAsyncThunk( - 'appMain/logoutUser', - async (_, { rejectWithValue, dispatch }) => { - try { - await AuthService.logout() - dispatch(logout()) - } catch (e) { - const error = e as AxiosError - return rejectWithValue(error.response?.data.code) - } - } -) +const { logout: logoutEndpoint } = authService.endpoints export const checkAuth = createAsyncThunk( 'appMain/checkAuth', @@ -138,7 +82,7 @@ export const mainSlice = createSlice({ }, extraReducers: (builder) => { builder.addMatcher(isPending, (state, action) => { - if (isAnyOf(checkAuth.pending, logoutUser.pending)(action)) { + if (isAnyOf(checkAuth.pending, logoutEndpoint.matchPending)(action)) { state.loading = true } else { state.authLoading = true @@ -146,7 +90,7 @@ export const mainSlice = createSlice({ state.error = '' }) builder.addMatcher(isFulfilled, (state, action) => { - if (isAnyOf(checkAuth.fulfilled, logoutUser.fulfilled)(action)) { + if (isAnyOf(checkAuth.fulfilled, logoutEndpoint.matchFulfilled)(action)) { state.loading = false } else { state.authLoading = false diff --git a/src/redux/store.ts b/src/redux/store.ts index df8548c8e..b1e6c061e 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -1,13 +1,21 @@ import { configureStore } from '@reduxjs/toolkit' +import { setupListeners } from '@reduxjs/toolkit/query/react' import { ToolkitStore } from '@reduxjs/toolkit/dist/configureStore' + +import { appApi } from '~/redux/apiSlice' import appMainReducer from '~/redux/reducer' export const store = configureStore({ reducer: { - appMain: appMainReducer - } + appMain: appMainReducer, + [appApi.reducerPath]: appApi.reducer + }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware().concat(appApi.middleware) }) +setupListeners(store.dispatch) + export type RootState = ReturnType export type AppDispatch = typeof store.dispatch export interface Store extends ToolkitStore { diff --git a/src/services/auth-service.ts b/src/services/auth-service.ts index 1a16a1430..4e37682e9 100644 --- a/src/services/auth-service.ts +++ b/src/services/auth-service.ts @@ -1,30 +1,22 @@ import { AxiosResponse } from 'axios' + +import { appApi } from '~/redux/apiSlice' import { axiosClient } from '~/plugins/axiosClient' + import { createUrlPath } from '~/utils/helper-functions' import { URLs } from '~/constants/request' import { + ApiMethodEnum, GoogleAuthParams, LoginParams, LoginResponse, SignupParams, - SignupRespornse + SignupResponse } from '~/types' +const { POST } = ApiMethodEnum + export const AuthService = { - login: (userData: LoginParams): Promise> => { - return axiosClient.post(URLs.auth.login, userData) - }, - googleAuth: ( - userData: GoogleAuthParams - ): Promise> => { - return axiosClient.post(URLs.auth.googleAuth, userData) - }, - signup: (userData: SignupParams): Promise> => { - return axiosClient.post(URLs.auth.signup, userData) - }, - logout: (): Promise => { - return axiosClient.post(URLs.auth.logout) - }, refresh: (): Promise> => { return axiosClient.get(URLs.auth.refresh) }, @@ -43,3 +35,27 @@ export const AuthService = { return axiosClient.patch(confirmUrl, newPassword) } } + +export const authService = appApi.injectEndpoints({ + endpoints: (build) => ({ + signUp: build.mutation({ + query: (body) => ({ url: URLs.auth.signup, method: POST, body }) + }), + login: build.mutation({ + query: (body) => ({ url: URLs.auth.login, method: POST, body }) + }), + googleAuth: build.mutation({ + query: (body) => ({ url: URLs.auth.googleAuth, method: POST, body }) + }), + logout: build.mutation({ + query: () => ({ url: URLs.auth.logout, method: POST }) + }) + }) +}) + +export const { + useSignUpMutation, + useLoginMutation, + useGoogleAuthMutation, + useLogoutMutation +} = authService diff --git a/src/services/resource-service.ts b/src/services/resource-service.ts index fb5c39862..01c35f793 100644 --- a/src/services/resource-service.ts +++ b/src/services/resource-service.ts @@ -1,6 +1,7 @@ import { AxiosResponse } from 'axios' import { axiosClient } from '~/plugins/axiosClient' +import { appApi } from '~/redux/apiSlice' import { URLs } from '~/constants/request' import { @@ -20,7 +21,8 @@ import { UpdateQuestionParams, CreateQuizParams, Quiz, - UpdateQuizParams + UpdateQuizParams, + ApiMethodEnum } from '~/types' import { createUrlPath } from '~/utils/helper-functions' @@ -99,13 +101,6 @@ export const ResourceService = { getResourcesCategoriesNames: (): Promise< AxiosResponse > => axiosClient.get(URLs.resources.resourcesCategories.getNames), - updateResourceCategory: ( - params?: UpdateResourceCategory - ): Promise => - axiosClient.patch( - createUrlPath(URLs.resources.resourcesCategories.patch, params?.id), - params - ), createResourceCategory: async ( params?: CreateCategoriesParams ): Promise> => @@ -116,3 +111,17 @@ export const ResourceService = { createUrlPath(URLs.resources.resourcesCategories.delete, id) ) } + +export const resourceService = appApi.injectEndpoints({ + endpoints: (build) => ({ + updateResourceCategory: build.mutation({ + query: (params) => ({ + url: createUrlPath(URLs.resources.resourcesCategories.patch, params.id), + method: ApiMethodEnum.PATCH, + body: { ...params } + }) + }) + }) +}) + +export const { useUpdateResourceCategoryMutation } = resourceService diff --git a/src/services/setup-interceptors.ts b/src/services/setup-interceptors.ts index 6d1628a31..ba1a8f8cb 100644 --- a/src/services/setup-interceptors.ts +++ b/src/services/setup-interceptors.ts @@ -1,10 +1,10 @@ import { AxiosError, AxiosRequestConfig } from 'axios' + import { axiosClient } from '~/plugins/axiosClient' -import { logoutUser } from '~/redux/reducer' import i18n from '~/plugins/i18n' -import { Store } from '~/redux/store' +import { authService } from '~/services/auth-service' -export const setupInterceptors = (store: Store): void => { +export const setupInterceptors = (): void => { axiosClient.interceptors.request.use((config: AxiosRequestConfig) => { config.headers = { ...config.headers, @@ -23,7 +23,7 @@ export const setupInterceptors = (store: Store): void => { try { return await axiosClient.request(originalRequest) } catch (e) { - void store.dispatch(logoutUser()) + void authService.endpoints.logout.initiate() } } return Promise.reject(error) diff --git a/src/types/common/enums/common.enums.ts b/src/types/common/enums/common.enums.ts index 76e925016..5e83ff0bb 100644 --- a/src/types/common/enums/common.enums.ts +++ b/src/types/common/enums/common.enums.ts @@ -107,3 +107,8 @@ export enum OverlapEnum { export enum ColorEnum { Primary = 'primary' } + +export enum ApiMethodEnum { + POST = 'POST', + PATCH = 'PATCH' +} diff --git a/src/types/user/user-interfaces/user.interfaces.ts b/src/types/user/user-interfaces/user.interfaces.ts index 67a376094..e3b8813c2 100644 --- a/src/types/user/user-interfaces/user.interfaces.ts +++ b/src/types/user/user-interfaces/user.interfaces.ts @@ -78,7 +78,7 @@ export interface SignupParams { role: UserRole } -export interface SignupRespornse { +export interface SignupResponse { userId: string userEmail: string } diff --git a/tests/unit/containers/guest-home-page/login-dialog/LoginDialog.spec.jsx b/tests/unit/containers/guest-home-page/login-dialog/LoginDialog.spec.jsx index 980b8c693..1cbcece0d 100644 --- a/tests/unit/containers/guest-home-page/login-dialog/LoginDialog.spec.jsx +++ b/tests/unit/containers/guest-home-page/login-dialog/LoginDialog.spec.jsx @@ -2,9 +2,12 @@ import { screen, fireEvent, waitFor } from '@testing-library/react' import LoginDialog from '~/containers/guest-home-page/login-dialog/LoginDialog' import { renderWithProviders } from '~tests/test-utils' import { vi } from 'vitest' +import { accessToken } from '~tests/unit/redux/redux.variables' const mockDispatch = vi.fn() const mockSelector = vi.fn() +const unwrap = vi.fn().mockResolvedValue({ accessToken }) +const loginUser = vi.fn().mockReturnValue({ unwrap }) const mockState = { appMain: { authLoading: false } @@ -32,6 +35,14 @@ vi.mock('~/containers/guest-home-page/google-button/GoogleButton', () => ({ } })) +vi.mock('~/services/auth-service', async () => { + const actual = await vi.importActual('~/services/auth-service') + return { + ...actual, + useLoginMutation: () => [loginUser] + } +}) + describe('Login dialog test', () => { beforeEach(() => { renderWithProviders() @@ -82,6 +93,9 @@ describe('Login dialog test', () => { const button = screen.getByText('common.labels.login') fireEvent.click(button) - await waitFor(() => expect(mockDispatch).toHaveBeenCalledTimes(1)) + await waitFor(() => { + expect(mockDispatch).toHaveBeenCalledTimes(1) + expect(loginUser).toHaveBeenCalledTimes(1) + }) }) }) diff --git a/tests/unit/containers/guest-home-page/signup-dialog/SignupDialog.spec.jsx b/tests/unit/containers/guest-home-page/signup-dialog/SignupDialog.spec.jsx index 16cd11835..d61099cd5 100644 --- a/tests/unit/containers/guest-home-page/signup-dialog/SignupDialog.spec.jsx +++ b/tests/unit/containers/guest-home-page/signup-dialog/SignupDialog.spec.jsx @@ -1,13 +1,12 @@ +import { vi } from 'vitest' import { screen, fireEvent, waitFor } from '@testing-library/react' -import { student } from '~/constants' import SignupDialog from '~/containers/guest-home-page/signup-dialog/SignupDialog' - import { renderWithProviders } from '~tests/test-utils' +import { student } from '~/constants' -import { vi } from 'vitest' - -const mockDispatch = vi.fn() const mockSelector = vi.fn() +const unwrap = vi.fn().mockResolvedValue({}) +const signUp = vi.fn().mockReturnValue({ unwrap }) const mockState = { appMain: { authLoading: true } @@ -24,7 +23,6 @@ vi.mock('react-redux', async () => { const actual = await vi.importActual('react-redux') return { ...actual, - useDispatch: () => mockDispatch.mockReturnValue({ unwrap: () => '' }), useSelector: () => mockSelector.mockImplementation(mockState) } }) @@ -35,6 +33,14 @@ vi.mock('~/hooks/use-confirm', () => { } }) +vi.mock('~/services/auth-service', async () => { + const actual = await vi.importActual('~/services/auth-service') + return { + ...actual, + useSignUpMutation: () => [signUp] + } +}) + describe('Signup dialog test', () => { beforeEach(() => { renderWithProviders() @@ -75,17 +81,21 @@ describe('Signup dialog test', () => { expect(error).toBeInTheDocument() }) - it('should dispatch after button submit', async () => { + it('should call mutation after button submit', async () => { const inputFirstName = screen.getByLabelText(/common.labels.firstName/i) + fireEvent.change(inputFirstName, { target: { value: 'test' } }) const inputLastName = screen.getByLabelText(/common.labels.lastName/i) + fireEvent.change(inputLastName, { target: { value: 'test' } }) const inputEmail = screen.getByLabelText(/common.labels.email/i) + fireEvent.change(inputEmail, { target: { value: 'test@gmail.com' } }) const inputPassword = screen.getByLabelText(/common.labels.password/i) + fireEvent.change(inputPassword, { target: { value: '12345678a/A' } }) const inputConfirmPassword = screen.getByLabelText( @@ -94,13 +104,15 @@ describe('Signup dialog test', () => { fireEvent.change(inputConfirmPassword, { target: { value: '12345678a/A' } }) const checkbox = screen.getByRole('checkbox') + fireEvent.click(checkbox) const button = screen.getByText('common.labels.signup') + fireEvent.click(button) await waitFor(() => { - expect(mockDispatch).toHaveBeenCalledTimes(1) + expect(signUp).toHaveBeenCalledTimes(1) }) }) }) diff --git a/tests/unit/context/snackbar-context.spec.jsx b/tests/unit/context/snackbar-context.spec.jsx index 37af70f82..732dd80d1 100644 --- a/tests/unit/context/snackbar-context.spec.jsx +++ b/tests/unit/context/snackbar-context.spec.jsx @@ -1,10 +1,11 @@ import { screen, fireEvent, waitFor } from '@testing-library/react' -import { renderWithProviders, mockAxiosClient } from '~tests/test-utils' +import { renderWithProviders } from '~tests/test-utils' import LoginDialog from '~/containers/guest-home-page/login-dialog/LoginDialog' -import { URLs } from '~/constants/request' import { vi } from 'vitest' const preloadedState = { appMain: { loading: false, userRole: '', error: '' } } +const unwrap = vi.fn().mockRejectedValue({ data: { code: 'error' } }) +const loginUser = vi.fn().mockReturnValue({ unwrap }) vi.mock('~/containers/guest-home-page/google-button/GoogleButton', () => ({ __esModule: true, @@ -13,19 +14,30 @@ vi.mock('~/containers/guest-home-page/google-button/GoogleButton', () => ({ } })) +vi.mock('~/services/auth-service', async () => { + const actual = await vi.importActual('~/services/auth-service') + return { + ...actual, + useLoginMutation: () => [loginUser] + } +}) + describe('snackbar context', () => { beforeEach(async () => { renderWithProviders(, { preloadedState }) - mockAxiosClient.onPost(URLs.auth.login).reply(404, { code: 'error' }) const inputEmail = screen.getByLabelText(/common.labels.email/i) + fireEvent.change(inputEmail, { target: { value: 'test@gmail.com' } }) const inputPassword = screen.getByLabelText(/common.labels.password/i) + fireEvent.change(inputPassword, { target: { value: '12345678a/A' } }) const button = screen.getByText('common.labels.login') + fireEvent.click(button) + const snackbar = await screen.findByText('errors.error') await waitFor(() => expect(snackbar).toBeInTheDocument()) diff --git a/tests/unit/pages/logout/Logout.spec.jsx b/tests/unit/pages/logout/Logout.spec.jsx index b8638e4e7..577411b42 100644 --- a/tests/unit/pages/logout/Logout.spec.jsx +++ b/tests/unit/pages/logout/Logout.spec.jsx @@ -12,6 +12,16 @@ vi.mock('~/hooks/use-redux', () => ({ useAppDispatch: vi.fn() })) +const logout = vi.fn().mockReturnValue({}) + +vi.mock('~/services/auth-service', async () => { + const actual = await vi.importActual('~/services/auth-service') + return { + ...actual, + useLogoutMutation: () => [logout] + } +}) + const dispatch = vi.fn() useAppDispatch.mockReturnValue(dispatch) const navigate = vi.fn() @@ -21,6 +31,7 @@ describe('Logout', () => { it('dispatches logoutUser action and redirects to home route', async () => { render() await waitFor(() => { + expect(logout).toHaveBeenCalledTimes(1) expect(dispatch).toHaveBeenCalledTimes(1) expect(navigate).toHaveBeenCalledTimes(1) expect(navigate).toHaveBeenCalledWith(guestRoutes.home.route) diff --git a/tests/unit/redux/redux.spec.js b/tests/unit/redux/redux.spec.js index 015f166aa..be1a134fd 100644 --- a/tests/unit/redux/redux.spec.js +++ b/tests/unit/redux/redux.spec.js @@ -1,13 +1,7 @@ import { afterEach, vi } from 'vitest' import { store } from '~/redux/store' -import reducer, { - logout, - checkAuth, - loginUser, - logoutUser, - signupUser -} from '~/redux/reducer' +import reducer, { logout, checkAuth } from '~/redux/reducer' import { mockAxiosClient } from '~tests/test-utils' import { URLs } from '~/constants/request' @@ -15,20 +9,11 @@ import { errorMessage, initialState, accessToken, - loginUserData, - signupUserData, stateAfterSignup, stateAfterLogin, errorCode } from './redux.variables' -vi.mock('~/services/local-storage-service', () => ({ - __esModule: true, - getFromLocalStorage: () => true, - setToLocalStorage: () => true, - removeFromLocalStorage: () => true -})) - const error = new Error(errorMessage) error.code = errorCode @@ -38,36 +23,13 @@ describe('redux test', () => { expect(reducer(undefined, {})).toEqual(initialState) }) - it('should set an error to store after signup', async () => { - mockAxiosClient.onPost(URLs.auth.signup).reply(404, error) - await store.dispatch(signupUser(signupUserData)) - - expect(store.getState()).toEqual({ - appMain: { ...stateAfterSignup, error: errorCode } - }) - }) - - it('should set an error to store after login', async () => { - mockAxiosClient.onPost(URLs.auth.login).reply(404, error) - await store.dispatch(loginUser(loginUserData)) - - expect(store.getState()).toEqual({ - appMain: { ...stateAfterSignup, error: errorCode } - }) - }) - - it('should set user data to store after login', async () => { - mockAxiosClient.onPost(URLs.auth.login).reply(200, { accessToken }) - await store.dispatch(loginUser(loginUserData)) - - expect(store.getState()).toEqual({ appMain: stateAfterLogin }) - }) it('should set an error to store after checkAuth', async () => { mockAxiosClient.onGet(URLs.auth.refresh).reply(404, error) await store.dispatch(checkAuth()) - expect(store.getState()).toEqual({ - appMain: { ...stateAfterLogin, error: errorCode } + expect(store.getState().appMain).toEqual({ + ...stateAfterSignup, + error: errorCode }) }) @@ -75,29 +37,7 @@ describe('redux test', () => { mockAxiosClient.onGet(URLs.auth.refresh).reply(200, { accessToken }) await store.dispatch(checkAuth()) - expect(store.getState()).toEqual({ - appMain: { ...stateAfterLogin } - }) - }) - - it('should set an error to store after logout', async () => { - mockAxiosClient.onPost(URLs.auth.logout).reply(404, error) - await store.dispatch(logoutUser()) - - expect(store.getState()).toEqual({ - appMain: { ...stateAfterLogin, error: errorCode } - }) - }) - - it('should remove user data from store after logout', async () => { - mockAxiosClient - .onPost(URLs.auth.logout) - .reply(200, { count: 'deletedCount: 1' }) - await store.dispatch(logoutUser()) - - expect(store.getState()).toEqual({ - appMain: stateAfterSignup - }) + expect(store.getState().appMain).toEqual({ ...stateAfterLogin }) }) it('should clear user data from store', () => { From 66886e5c1f2e0a467e41a870c7f8388bb148a71f Mon Sep 17 00:00:00 2001 From: Anatoliy Sarakhman Date: Tue, 21 Nov 2023 20:30:34 +0200 Subject: [PATCH 2/4] fix --- src/services/setup-interceptors.ts | 2 +- .../signup-dialog/SignupDialog.spec.jsx | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/services/setup-interceptors.ts b/src/services/setup-interceptors.ts index ba1a8f8cb..bd9b98433 100644 --- a/src/services/setup-interceptors.ts +++ b/src/services/setup-interceptors.ts @@ -23,7 +23,7 @@ export const setupInterceptors = (): void => { try { return await axiosClient.request(originalRequest) } catch (e) { - void authService.endpoints.logout.initiate() + return authService.endpoints.logout.initiate() } } return Promise.reject(error) diff --git a/tests/unit/containers/guest-home-page/signup-dialog/SignupDialog.spec.jsx b/tests/unit/containers/guest-home-page/signup-dialog/SignupDialog.spec.jsx index d61099cd5..8352230ee 100644 --- a/tests/unit/containers/guest-home-page/signup-dialog/SignupDialog.spec.jsx +++ b/tests/unit/containers/guest-home-page/signup-dialog/SignupDialog.spec.jsx @@ -1,4 +1,4 @@ -import { vi } from 'vitest' +import { expect, vi } from 'vitest' import { screen, fireEvent, waitFor } from '@testing-library/react' import SignupDialog from '~/containers/guest-home-page/signup-dialog/SignupDialog' import { renderWithProviders } from '~tests/test-utils' @@ -86,23 +86,33 @@ describe('Signup dialog test', () => { fireEvent.change(inputFirstName, { target: { value: 'test' } }) + expect(inputFirstName.value).toBe('test') + const inputLastName = screen.getByLabelText(/common.labels.lastName/i) fireEvent.change(inputLastName, { target: { value: 'test' } }) + expect(inputFirstName.value).toBe('test') + const inputEmail = screen.getByLabelText(/common.labels.email/i) fireEvent.change(inputEmail, { target: { value: 'test@gmail.com' } }) + expect(inputEmail.value).toBe('test@gmail.com') + const inputPassword = screen.getByLabelText(/common.labels.password/i) fireEvent.change(inputPassword, { target: { value: '12345678a/A' } }) + expect(inputPassword.value).toBe('12345678a/A') + const inputConfirmPassword = screen.getByLabelText( /common.labels.confirmPassword/i ) fireEvent.change(inputConfirmPassword, { target: { value: '12345678a/A' } }) + expect(inputConfirmPassword.value).toBe('12345678a/A') + const checkbox = screen.getByRole('checkbox') fireEvent.click(checkbox) From 189ed30a0994f5b4750b3ac0a41535ac0c77229c Mon Sep 17 00:00:00 2001 From: Anatoliy Sarakhman Date: Thu, 23 Nov 2023 14:53:49 +0200 Subject: [PATCH 3/4] moved dispatch to api slice --- .../google-button/GoogleButton.jsx | 8 ++--- .../login-dialog/LoginDialog.jsx | 6 +--- src/pages/logout/Logout.tsx | 6 +--- src/services/auth-service.ts | 29 ++++++++++++++++--- 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/containers/guest-home-page/google-button/GoogleButton.jsx b/src/containers/guest-home-page/google-button/GoogleButton.jsx index 0cbaed5be..dff6c28c8 100644 --- a/src/containers/guest-home-page/google-button/GoogleButton.jsx +++ b/src/containers/guest-home-page/google-button/GoogleButton.jsx @@ -1,8 +1,6 @@ import { useCallback, useEffect } from 'react' -import { useDispatch } from 'react-redux' import { useHref } from 'react-router-dom' -import { setUser } from '~/redux/reducer' import { useGoogleAuthMutation } from '~/services/auth-service' import { useModalContext } from '~/context/modal-context' import { useSnackBarContext } from '~/context/snackbar-context' @@ -14,7 +12,6 @@ import { styles } from '~/containers/guest-home-page/google-button/GoogleButton. const GoogleButton = ({ role, route, buttonWidth, type }) => { const ref = useHref(route) - const dispatch = useDispatch() const mediaQuery = useBreakpoints().isLaptopAndAbove ? 'md' : 'xs' const { closeModal } = useModalContext() const { setAlert } = useSnackBarContext() @@ -23,8 +20,7 @@ const GoogleButton = ({ role, route, buttonWidth, type }) => { const handleCredentialResponse = useCallback( async (token) => { try { - const response = await googleAuth({ token, role }).unwrap() - dispatch(setUser(response.accessToken)) + await googleAuth({ token, role }).unwrap() closeModal() } catch (e) { setAlert({ @@ -37,7 +33,7 @@ const GoogleButton = ({ role, route, buttonWidth, type }) => { } } }, - [googleAuth, dispatch, role, closeModal, setAlert, ref] + [googleAuth, role, closeModal, setAlert, ref] ) useEffect(() => { diff --git a/src/containers/guest-home-page/login-dialog/LoginDialog.jsx b/src/containers/guest-home-page/login-dialog/LoginDialog.jsx index 9321a267d..897fd8126 100644 --- a/src/containers/guest-home-page/login-dialog/LoginDialog.jsx +++ b/src/containers/guest-home-page/login-dialog/LoginDialog.jsx @@ -1,12 +1,10 @@ import Box from '@mui/material/Box' import Typography from '@mui/material/Typography' import { useTranslation } from 'react-i18next' -import { useDispatch } from 'react-redux' import GoogleLogin from '~/containers/guest-home-page/google-login/GoogleLogin' import LoginForm from '~/containers/guest-home-page/login-form/LoginForm' import useForm from '~/hooks/use-form' -import { setUser } from '~/redux/reducer' import { useLoginMutation } from '~/services/auth-service' import { useModalContext } from '~/context/modal-context' import { useSnackBarContext } from '~/context/snackbar-context' @@ -20,15 +18,13 @@ const LoginDialog = () => { const { t } = useTranslation() const { closeModal } = useModalContext() const { setAlert } = useSnackBarContext() - const dispatch = useDispatch() const [loginUser] = useLoginMutation() const { handleSubmit, handleInputChange, handleBlur, data, errors } = useForm( { onSubmit: async () => { try { - const response = await loginUser(data).unwrap() - dispatch(setUser(response.accessToken)) + await loginUser(data).unwrap() closeModal() } catch (e) { setAlert({ diff --git a/src/pages/logout/Logout.tsx b/src/pages/logout/Logout.tsx index 0021c012f..836abc40e 100644 --- a/src/pages/logout/Logout.tsx +++ b/src/pages/logout/Logout.tsx @@ -1,21 +1,17 @@ import { useEffect, useCallback } from 'react' import { useNavigate } from 'react-router-dom' -import { useAppDispatch } from '~/hooks/use-redux' -import { logout } from '~/redux/reducer' import { useLogoutMutation } from '~/services/auth-service' import { guestRoutes } from '~/router/constants/guestRoutes' const Logout = () => { - const dispatch = useAppDispatch() const navigate = useNavigate() const [logoutUser] = useLogoutMutation() const onLogoutUser = useCallback(async () => { await logoutUser() - dispatch(logout()) navigate(guestRoutes.home.route) - }, [logoutUser, dispatch, navigate]) + }, [logoutUser, navigate]) useEffect(() => { void onLogoutUser() diff --git a/src/services/auth-service.ts b/src/services/auth-service.ts index 4e37682e9..bb4ebe294 100644 --- a/src/services/auth-service.ts +++ b/src/services/auth-service.ts @@ -1,6 +1,7 @@ import { AxiosResponse } from 'axios' import { appApi } from '~/redux/apiSlice' +import { logout, setUser } from '~/redux/reducer' import { axiosClient } from '~/plugins/axiosClient' import { createUrlPath } from '~/utils/helper-functions' @@ -41,14 +42,34 @@ export const authService = appApi.injectEndpoints({ signUp: build.mutation({ query: (body) => ({ url: URLs.auth.signup, method: POST, body }) }), - login: build.mutation({ - query: (body) => ({ url: URLs.auth.login, method: POST, body }) + login: build.mutation({ + query: (body) => ({ url: URLs.auth.login, method: POST, body }), + async onQueryStarted(_, { dispatch, queryFulfilled }) { + try { + const { data } = await queryFulfilled + dispatch(setUser(data.accessToken)) + } catch { + dispatch(logout()) + } + } }), googleAuth: build.mutation({ - query: (body) => ({ url: URLs.auth.googleAuth, method: POST, body }) + query: (body) => ({ url: URLs.auth.googleAuth, method: POST, body }), + async onQueryStarted(_, { dispatch, queryFulfilled }) { + try { + const { data } = await queryFulfilled + dispatch(setUser(data.accessToken)) + } catch { + dispatch(logout()) + } + } }), logout: build.mutation({ - query: () => ({ url: URLs.auth.logout, method: POST }) + query: () => ({ url: URLs.auth.logout, method: POST }), + async onQueryStarted(_, { dispatch, queryFulfilled }) { + await queryFulfilled + dispatch(logout()) + } }) }) }) From 7a940cb1f2289bc73a6fe3b96f01bb003c7e314a Mon Sep 17 00:00:00 2001 From: Anatoliy Sarakhman Date: Fri, 24 Nov 2023 14:26:14 +0200 Subject: [PATCH 4/4] fix tests --- .../google-button/GoogleButton.jsx | 2 +- src/redux/reducer.ts | 16 ++++++++++++---- .../login-dialog/LoginDialog.spec.jsx | 3 --- tests/unit/context/snackbar-context.spec.jsx | 10 +++++++--- tests/unit/pages/logout/Logout.spec.jsx | 8 -------- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/containers/guest-home-page/google-button/GoogleButton.jsx b/src/containers/guest-home-page/google-button/GoogleButton.jsx index dff6c28c8..92a249a63 100644 --- a/src/containers/guest-home-page/google-button/GoogleButton.jsx +++ b/src/containers/guest-home-page/google-button/GoogleButton.jsx @@ -27,7 +27,7 @@ const GoogleButton = ({ role, route, buttonWidth, type }) => { severity: snackbarVariants.error, message: `errors.${e.data.code}` }) - if (e === 'USER_NOT_FOUND') { + if (e.data.code === 'USER_NOT_FOUND') { closeModal() scrollToHash(ref) } diff --git a/src/redux/reducer.ts b/src/redux/reducer.ts index 0b399a5bd..1fb5015ec 100644 --- a/src/redux/reducer.ts +++ b/src/redux/reducer.ts @@ -30,8 +30,6 @@ const initialState: UserState = { isFirstLogin: true } -const { logout: logoutEndpoint } = authService.endpoints - export const checkAuth = createAsyncThunk( 'appMain/checkAuth', async (_, { rejectWithValue, dispatch }) => { @@ -82,7 +80,12 @@ export const mainSlice = createSlice({ }, extraReducers: (builder) => { builder.addMatcher(isPending, (state, action) => { - if (isAnyOf(checkAuth.pending, logoutEndpoint.matchPending)(action)) { + if ( + isAnyOf( + checkAuth.pending, + authService.endpoints.logout.matchPending + )(action) + ) { state.loading = true } else { state.authLoading = true @@ -90,7 +93,12 @@ export const mainSlice = createSlice({ state.error = '' }) builder.addMatcher(isFulfilled, (state, action) => { - if (isAnyOf(checkAuth.fulfilled, logoutEndpoint.matchFulfilled)(action)) { + if ( + isAnyOf( + checkAuth.fulfilled, + authService.endpoints.logout.matchFulfilled + )(action) + ) { state.loading = false } else { state.authLoading = false diff --git a/tests/unit/containers/guest-home-page/login-dialog/LoginDialog.spec.jsx b/tests/unit/containers/guest-home-page/login-dialog/LoginDialog.spec.jsx index 1cbcece0d..845729639 100644 --- a/tests/unit/containers/guest-home-page/login-dialog/LoginDialog.spec.jsx +++ b/tests/unit/containers/guest-home-page/login-dialog/LoginDialog.spec.jsx @@ -4,7 +4,6 @@ import { renderWithProviders } from '~tests/test-utils' import { vi } from 'vitest' import { accessToken } from '~tests/unit/redux/redux.variables' -const mockDispatch = vi.fn() const mockSelector = vi.fn() const unwrap = vi.fn().mockResolvedValue({ accessToken }) const loginUser = vi.fn().mockReturnValue({ unwrap }) @@ -17,7 +16,6 @@ vi.mock('react-redux', async () => { const actual = await vi.importActual('react-redux') return { ...actual, - useDispatch: () => mockDispatch.mockReturnValue({ unwrap: () => '' }), useSelector: () => mockSelector.mockReturnValue(mockState) } }) @@ -94,7 +92,6 @@ describe('Login dialog test', () => { fireEvent.click(button) await waitFor(() => { - expect(mockDispatch).toHaveBeenCalledTimes(1) expect(loginUser).toHaveBeenCalledTimes(1) }) }) diff --git a/tests/unit/context/snackbar-context.spec.jsx b/tests/unit/context/snackbar-context.spec.jsx index 732dd80d1..89255c2ad 100644 --- a/tests/unit/context/snackbar-context.spec.jsx +++ b/tests/unit/context/snackbar-context.spec.jsx @@ -3,7 +3,9 @@ import { renderWithProviders } from '~tests/test-utils' import LoginDialog from '~/containers/guest-home-page/login-dialog/LoginDialog' import { vi } from 'vitest' -const preloadedState = { appMain: { loading: false, userRole: '', error: '' } } +const preloadedState = { + appMain: { loading: false, authLoading: false, userRole: '', error: '' } +} const unwrap = vi.fn().mockRejectedValue({ data: { code: 'error' } }) const loginUser = vi.fn().mockReturnValue({ unwrap }) @@ -15,9 +17,11 @@ vi.mock('~/containers/guest-home-page/google-button/GoogleButton', () => ({ })) vi.mock('~/services/auth-service', async () => { - const actual = await vi.importActual('~/services/auth-service') return { - ...actual, + __esModule: true, + authService: { + endpoint: { matchFulfilled: vi.fn(), matchPending: vi.fn() } + }, useLoginMutation: () => [loginUser] } }) diff --git a/tests/unit/pages/logout/Logout.spec.jsx b/tests/unit/pages/logout/Logout.spec.jsx index 577411b42..db1d5e0b9 100644 --- a/tests/unit/pages/logout/Logout.spec.jsx +++ b/tests/unit/pages/logout/Logout.spec.jsx @@ -1,6 +1,5 @@ import { render, waitFor } from '@testing-library/react' import { useNavigate } from 'react-router-dom' -import { useAppDispatch } from '~/hooks/use-redux' import { guestRoutes } from '~/router/constants/guestRoutes' import Logout from '~/pages/logout/Logout' @@ -8,10 +7,6 @@ vi.mock('react-router-dom', () => ({ useNavigate: vi.fn() })) -vi.mock('~/hooks/use-redux', () => ({ - useAppDispatch: vi.fn() -})) - const logout = vi.fn().mockReturnValue({}) vi.mock('~/services/auth-service', async () => { @@ -22,8 +17,6 @@ vi.mock('~/services/auth-service', async () => { } }) -const dispatch = vi.fn() -useAppDispatch.mockReturnValue(dispatch) const navigate = vi.fn() useNavigate.mockReturnValue(navigate) @@ -32,7 +25,6 @@ describe('Logout', () => { render() await waitFor(() => { expect(logout).toHaveBeenCalledTimes(1) - expect(dispatch).toHaveBeenCalledTimes(1) expect(navigate).toHaveBeenCalledTimes(1) expect(navigate).toHaveBeenCalledWith(guestRoutes.home.route) })