Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 28 additions & 12 deletions client/modules/User/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.'
})
);
}
Expand Down Expand Up @@ -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 });
});
});
Expand Down Expand Up @@ -188,7 +195,7 @@ export function resetProject(dispatch: Dispatch) {
}

export function logoutUser() {
return (dispatch: Dispatch) => {
return (dispatch: Dispatch) =>
apiClient
.get('/logout')
.then(() => {
Expand All @@ -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));
});
};
}

/**
Expand Down Expand Up @@ -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));
});
};
Expand All @@ -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));
});
};
Expand Down
129 changes: 129 additions & 0 deletions client/modules/User/actions.unit.test.ts
Original file line number Diff line number Diff line change
@@ -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'
});
});
});
});