Skip to content

Commit a39218d

Browse files
committed
fix: handle network errors in validateAndSignUpUser (#4285)
- Guard against undefined error.response in validateAndSignUpUser by safely extracting message via optional chaining - Update authError type definition to accept string payloads - Guard against similar missing error.response crashes in validateAndLoginUser, logoutUser, unlinkService, and setUserCookieConsent - Return promise from logoutUser thunk - Add unit tests covering signup, login, and logout network error handling
1 parent 19778f5 commit a39218d

2 files changed

Lines changed: 157 additions & 12 deletions

File tree

client/modules/User/actions.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import type {
2525
} from '../../../common/types';
2626
import type { GetRootState, RootState } from '../../reducers';
2727

28-
export function authError(error: Error) {
28+
export function authError(error: Error | string) {
2929
return {
3030
type: ActionTypes.AUTH_ERROR,
3131
payload: error
@@ -97,7 +97,10 @@ export function validateAndLoginUser(formProps: {
9797
})
9898
.catch((error) =>
9999
resolve({
100-
[FORM_ERROR]: error.response.data.message
100+
[FORM_ERROR]:
101+
error.response?.data?.message ||
102+
error.message ||
103+
'Unknown error.'
101104
})
102105
);
103106
}
@@ -127,8 +130,12 @@ export function validateAndSignUpUser(formValues: CreateUserRequestBody) {
127130
resolve();
128131
})
129132
.catch((error) => {
130-
const { response } = error;
131-
dispatch(authError(response.data.error));
133+
const message =
134+
error.response?.data?.error ||
135+
error.response?.data?.message ||
136+
error.message ||
137+
'Unknown error.';
138+
dispatch(authError(message));
132139
resolve({ error });
133140
});
134141
});
@@ -188,7 +195,7 @@ export function resetProject(dispatch: Dispatch) {
188195
}
189196

190197
export function logoutUser() {
191-
return (dispatch: Dispatch) => {
198+
return (dispatch: Dispatch) =>
192199
apiClient
193200
.get('/logout')
194201
.then(() => {
@@ -198,10 +205,13 @@ export function logoutUser() {
198205
resetProject(dispatch);
199206
})
200207
.catch((error) => {
201-
const { response } = error;
202-
dispatch(authError(response.data.error));
208+
const message =
209+
error.response?.data?.error ||
210+
error.response?.data?.message ||
211+
error.message ||
212+
'Unknown error.';
213+
dispatch(authError(message));
203214
});
204-
};
205215
}
206216

207217
/**
@@ -479,8 +489,11 @@ export function unlinkService(service: string) {
479489
dispatch(authenticateUser(response.data));
480490
})
481491
.catch((error) => {
482-
const { response } = error;
483-
const message = response.message || response.data.error;
492+
const message =
493+
error.response?.message ||
494+
error.response?.data?.error ||
495+
error.message ||
496+
'Unknown error.';
484497
dispatch(authError(message));
485498
});
486499
};
@@ -500,8 +513,11 @@ export function setUserCookieConsent(
500513
});
501514
})
502515
.catch((error) => {
503-
const { response } = error;
504-
const message = response.message || response.data.error;
516+
const message =
517+
error.response?.message ||
518+
error.response?.data?.error ||
519+
error.message ||
520+
'Unknown error.';
505521
dispatch(authError(message));
506522
});
507523
};
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// @ts-ignore
2+
import configureStore from 'redux-mock-store';
3+
import thunk from 'redux-thunk';
4+
import { FORM_ERROR } from 'final-form';
5+
import * as UserActions from './actions';
6+
import * as ActionTypes from '../../constants';
7+
import { apiClient } from '../../utils/apiClient';
8+
import browserHistory from '../../browserHistory';
9+
import { initialTestState } from '../../testData/testReduxStore';
10+
11+
const mockStore = configureStore([thunk]);
12+
13+
describe('User actions unit tests', () => {
14+
let store: any;
15+
16+
beforeEach(() => {
17+
store = mockStore(initialTestState);
18+
jest.clearAllMocks();
19+
});
20+
21+
afterEach(() => {
22+
store.clearActions();
23+
});
24+
25+
describe('validateAndSignUpUser', () => {
26+
const formValues = {
27+
username: 'newuser',
28+
email: 'newuser@example.com',
29+
password: 'password123'
30+
};
31+
32+
it('handles successful signup', async () => {
33+
const mockUserData = {
34+
id: 'u123',
35+
username: 'newuser',
36+
email: 'newuser@example.com'
37+
};
38+
jest
39+
.spyOn(apiClient, 'post')
40+
.mockResolvedValueOnce({ data: mockUserData });
41+
const pushSpy = jest
42+
.spyOn(browserHistory, 'push')
43+
.mockImplementation(() => {});
44+
45+
const result = await store.dispatch(
46+
UserActions.validateAndSignUpUser(formValues)
47+
);
48+
49+
expect(result).toBeUndefined();
50+
expect(pushSpy).toHaveBeenCalledWith('/');
51+
const actions = store.getActions();
52+
expect(actions).toContainEqual(
53+
UserActions.authenticateUser(mockUserData as any)
54+
);
55+
expect(actions).toContainEqual(
56+
expect.objectContaining({ type: ActionTypes.JUST_OPENED_PROJECT })
57+
);
58+
});
59+
60+
it('handles server validation/API error gracefully (with error.response)', async () => {
61+
const apiError = {
62+
response: {
63+
data: { error: 'Username is in use' },
64+
status: 422
65+
}
66+
};
67+
jest.spyOn(apiClient, 'post').mockRejectedValueOnce(apiError);
68+
69+
const result = await store.dispatch(
70+
UserActions.validateAndSignUpUser(formValues)
71+
);
72+
73+
expect(result).toEqual({ error: apiError });
74+
expect(store.getActions()).toContainEqual({
75+
type: ActionTypes.AUTH_ERROR,
76+
payload: 'Username is in use'
77+
});
78+
});
79+
80+
it('handles network error where error.response is undefined without throwing or hanging', async () => {
81+
const networkError = new Error('Network Error');
82+
jest.spyOn(apiClient, 'post').mockRejectedValueOnce(networkError);
83+
84+
const result = await store.dispatch(
85+
UserActions.validateAndSignUpUser(formValues)
86+
);
87+
88+
expect(result).toEqual({ error: networkError });
89+
expect(store.getActions()).toContainEqual({
90+
type: ActionTypes.AUTH_ERROR,
91+
payload: 'Network Error'
92+
});
93+
});
94+
});
95+
96+
describe('validateAndLoginUser', () => {
97+
const loginValues = {
98+
email: 'user@example.com',
99+
password: 'password123'
100+
};
101+
102+
it('handles network error where error.response is undefined', async () => {
103+
const networkError = new Error('Network Error');
104+
jest.spyOn(apiClient, 'post').mockRejectedValueOnce(networkError);
105+
106+
const result = await store.dispatch(
107+
UserActions.validateAndLoginUser(loginValues)
108+
);
109+
110+
expect(result).toEqual({
111+
[FORM_ERROR]: 'Network Error'
112+
});
113+
});
114+
});
115+
116+
describe('logoutUser', () => {
117+
it('handles network error where error.response is undefined', async () => {
118+
const networkError = new Error('Network Error');
119+
jest.spyOn(apiClient, 'get').mockRejectedValueOnce(networkError);
120+
121+
await store.dispatch(UserActions.logoutUser());
122+
123+
expect(store.getActions()).toContainEqual({
124+
type: ActionTypes.AUTH_ERROR,
125+
payload: 'Network Error'
126+
});
127+
});
128+
});
129+
});

0 commit comments

Comments
 (0)