diff --git a/client/modules/User/actions.ts b/client/modules/User/actions.ts index b34c2bd1b4..53a1ca6964 100644 --- a/client/modules/User/actions.ts +++ b/client/modules/User/actions.ts @@ -25,7 +25,7 @@ import type { } from '../../../common/types'; import type { GetRootState, RootState } from '../../reducers'; -export function authError(error: Error) { +export function authError(error: Error | string) { return { type: ActionTypes.AUTH_ERROR, payload: error @@ -97,7 +97,10 @@ export function validateAndLoginUser(formProps: { }) .catch((error) => resolve({ - [FORM_ERROR]: error.response.data.message + [FORM_ERROR]: + error.response?.data?.message || + error.message || + 'Unknown error.' }) ); } @@ -127,8 +130,12 @@ export function validateAndSignUpUser(formValues: CreateUserRequestBody) { resolve(); }) .catch((error) => { - const { response } = error; - dispatch(authError(response.data.error)); + const message = + error.response?.data?.error || + error.response?.data?.message || + error.message || + 'Unknown error.'; + dispatch(authError(message)); resolve({ error }); }); }); @@ -188,7 +195,7 @@ export function resetProject(dispatch: Dispatch) { } export function logoutUser() { - return (dispatch: Dispatch) => { + return (dispatch: Dispatch) => apiClient .get('/logout') .then(() => { @@ -198,10 +205,13 @@ export function logoutUser() { resetProject(dispatch); }) .catch((error) => { - const { response } = error; - dispatch(authError(response.data.error)); + const message = + error.response?.data?.error || + error.response?.data?.message || + error.message || + 'Unknown error.'; + dispatch(authError(message)); }); - }; } /** @@ -479,8 +489,11 @@ export function unlinkService(service: string) { dispatch(authenticateUser(response.data)); }) .catch((error) => { - const { response } = error; - const message = response.message || response.data.error; + const message = + error.response?.message || + error.response?.data?.error || + error.message || + 'Unknown error.'; dispatch(authError(message)); }); }; @@ -500,8 +513,11 @@ export function setUserCookieConsent( }); }) .catch((error) => { - const { response } = error; - const message = response.message || response.data.error; + const message = + error.response?.message || + error.response?.data?.error || + error.message || + 'Unknown error.'; dispatch(authError(message)); }); }; diff --git a/client/modules/User/actions.unit.test.ts b/client/modules/User/actions.unit.test.ts new file mode 100644 index 0000000000..2879c0e801 --- /dev/null +++ b/client/modules/User/actions.unit.test.ts @@ -0,0 +1,129 @@ +// @ts-ignore +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { FORM_ERROR } from 'final-form'; +import * as UserActions from './actions'; +import * as ActionTypes from '../../constants'; +import { apiClient } from '../../utils/apiClient'; +import browserHistory from '../../browserHistory'; +import { initialTestState } from '../../testData/testReduxStore'; + +const mockStore = configureStore([thunk]); + +describe('User actions unit tests', () => { + let store: any; + + beforeEach(() => { + store = mockStore(initialTestState); + jest.clearAllMocks(); + }); + + afterEach(() => { + store.clearActions(); + }); + + describe('validateAndSignUpUser', () => { + const formValues = { + username: 'newuser', + email: 'newuser@example.com', + password: 'password123' + }; + + it('handles successful signup', async () => { + const mockUserData = { + id: 'u123', + username: 'newuser', + email: 'newuser@example.com' + }; + jest + .spyOn(apiClient, 'post') + .mockResolvedValueOnce({ data: mockUserData }); + const pushSpy = jest + .spyOn(browserHistory, 'push') + .mockImplementation(() => {}); + + const result = await store.dispatch( + UserActions.validateAndSignUpUser(formValues) + ); + + expect(result).toBeUndefined(); + expect(pushSpy).toHaveBeenCalledWith('/'); + const actions = store.getActions(); + expect(actions).toContainEqual( + UserActions.authenticateUser(mockUserData as any) + ); + expect(actions).toContainEqual( + expect.objectContaining({ type: ActionTypes.JUST_OPENED_PROJECT }) + ); + }); + + it('handles server validation/API error gracefully (with error.response)', async () => { + const apiError = { + response: { + data: { error: 'Username is in use' }, + status: 422 + } + }; + jest.spyOn(apiClient, 'post').mockRejectedValueOnce(apiError); + + const result = await store.dispatch( + UserActions.validateAndSignUpUser(formValues) + ); + + expect(result).toEqual({ error: apiError }); + expect(store.getActions()).toContainEqual({ + type: ActionTypes.AUTH_ERROR, + payload: 'Username is in use' + }); + }); + + it('handles network error where error.response is undefined without throwing or hanging', async () => { + const networkError = new Error('Network Error'); + jest.spyOn(apiClient, 'post').mockRejectedValueOnce(networkError); + + const result = await store.dispatch( + UserActions.validateAndSignUpUser(formValues) + ); + + expect(result).toEqual({ error: networkError }); + expect(store.getActions()).toContainEqual({ + type: ActionTypes.AUTH_ERROR, + payload: 'Network Error' + }); + }); + }); + + describe('validateAndLoginUser', () => { + const loginValues = { + email: 'user@example.com', + password: 'password123' + }; + + it('handles network error where error.response is undefined', async () => { + const networkError = new Error('Network Error'); + jest.spyOn(apiClient, 'post').mockRejectedValueOnce(networkError); + + const result = await store.dispatch( + UserActions.validateAndLoginUser(loginValues) + ); + + expect(result).toEqual({ + [FORM_ERROR]: 'Network Error' + }); + }); + }); + + describe('logoutUser', () => { + it('handles network error where error.response is undefined', async () => { + const networkError = new Error('Network Error'); + jest.spyOn(apiClient, 'get').mockRejectedValueOnce(networkError); + + await store.dispatch(UserActions.logoutUser()); + + expect(store.getActions()).toContainEqual({ + type: ActionTypes.AUTH_ERROR, + payload: 'Network Error' + }); + }); + }); +});