From 0d0f9d75b17ad9c7b2757d781c23ca309833fddc Mon Sep 17 00:00:00 2001 From: PS01K Date: Fri, 11 Sep 2026 00:28:40 +0530 Subject: [PATCH] fix: handle network errors in saveProject (#4276) - Guard against undefined error.response in saveProject catch blocks using optional chaining - Pass response?.data safely to projectSaveFail - Add unit tests covering saveProject network errors and HTTP error responses --- client/modules/IDE/actions/project.js | 10 +- .../modules/IDE/actions/project.unit.test.js | 149 ++++++++++++++++++ 2 files changed, 154 insertions(+), 5 deletions(-) create mode 100644 client/modules/IDE/actions/project.unit.test.js diff --git a/client/modules/IDE/actions/project.js b/client/modules/IDE/actions/project.js index cf556f8532..bc2c09f25c 100644 --- a/client/modules/IDE/actions/project.js +++ b/client/modules/IDE/actions/project.js @@ -196,12 +196,12 @@ export function saveProject( dispatch(endSavingProject()); dispatch(setToastText('Toast.SketchFailedSave')); dispatch(showToast(1500)); - if (response.status === 403) { + if (response?.status === 403) { dispatch(showErrorModal('staleSession')); - } else if (response.status === 409) { + } else if (response?.status === 409) { dispatch(showErrorModal('staleProject')); } else { - dispatch(projectSaveFail(response.data)); + dispatch(projectSaveFail(response?.data)); } }); } @@ -246,10 +246,10 @@ export function saveProject( dispatch(endSavingProject()); dispatch(setToastText('Toast.SketchFailedSave')); dispatch(showToast(1500)); - if (response.status === 403) { + if (response?.status === 403) { dispatch(showErrorModal('staleSession')); } else { - dispatch(projectSaveFail(response.data)); + dispatch(projectSaveFail(response?.data)); } }); }; diff --git a/client/modules/IDE/actions/project.unit.test.js b/client/modules/IDE/actions/project.unit.test.js new file mode 100644 index 0000000000..a274bd5e58 --- /dev/null +++ b/client/modules/IDE/actions/project.unit.test.js @@ -0,0 +1,149 @@ +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import * as ProjectActions from './project'; +import * as ActionTypes from '../../../constants'; +import { apiClient } from '../../../utils/apiClient'; +import { initialTestState } from '../../../testData/testReduxStore'; + +const mockStore = configureStore([thunk]); + +describe('project actions saveProject unit tests', () => { + let store; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + store.clearActions(); + }); + + describe('updating existing project (PUT)', () => { + const existingProjectState = { + ...initialTestState, + project: { + ...initialTestState.project, + id: 'project123', + owner: { id: initialTestState.user.id } + } + }; + + it('handles network error when error.response is undefined without throwing TypeError', async () => { + store = mockStore(existingProjectState); + const networkError = new Error('Network Error'); + // error.response is undefined on network failure + jest.spyOn(apiClient, 'put').mockRejectedValueOnce(networkError); + + await store.dispatch(ProjectActions.saveProject()); + + const actions = store.getActions(); + expect(actions).toContainEqual(ProjectActions.startSavingProject()); + expect(actions).toContainEqual(ProjectActions.endSavingProject()); + expect(actions).toContainEqual( + expect.objectContaining({ + type: ActionTypes.SET_TOAST_TEXT, + text: 'Toast.SketchFailedSave' + }) + ); + expect(actions).toContainEqual( + expect.objectContaining({ + type: ActionTypes.SHOW_TOAST + }) + ); + expect(actions).toContainEqual(ProjectActions.projectSaveFail(undefined)); + }); + + it('handles 403 response with staleSession error modal', async () => { + store = mockStore(existingProjectState); + const error403 = { + response: { + status: 403, + data: { error: 'Forbidden' } + } + }; + jest.spyOn(apiClient, 'put').mockRejectedValueOnce(error403); + + await store.dispatch(ProjectActions.saveProject()); + + const actions = store.getActions(); + expect(actions).toContainEqual({ + type: ActionTypes.SHOW_ERROR_MODAL, + modalType: 'staleSession' + }); + }); + + it('handles 409 response with staleProject error modal', async () => { + store = mockStore(existingProjectState); + const error409 = { + response: { + status: 409, + data: { error: 'Conflict' } + } + }; + jest.spyOn(apiClient, 'put').mockRejectedValueOnce(error409); + + await store.dispatch(ProjectActions.saveProject()); + + const actions = store.getActions(); + expect(actions).toContainEqual({ + type: ActionTypes.SHOW_ERROR_MODAL, + modalType: 'staleProject' + }); + }); + }); + + describe('creating new project (POST)', () => { + const newProjectState = { + ...initialTestState, + project: { + ...initialTestState.project, + id: undefined, + owner: undefined + } + }; + + it('handles network error when error.response is undefined without throwing TypeError', async () => { + store = mockStore(newProjectState); + const networkError = new Error('Network Error'); + // error.response is undefined on network failure + jest.spyOn(apiClient, 'post').mockRejectedValueOnce(networkError); + + await store.dispatch(ProjectActions.saveProject()); + + const actions = store.getActions(); + expect(actions).toContainEqual(ProjectActions.startSavingProject()); + expect(actions).toContainEqual(ProjectActions.endSavingProject()); + expect(actions).toContainEqual( + expect.objectContaining({ + type: ActionTypes.SET_TOAST_TEXT, + text: 'Toast.SketchFailedSave' + }) + ); + expect(actions).toContainEqual( + expect.objectContaining({ + type: ActionTypes.SHOW_TOAST + }) + ); + expect(actions).toContainEqual(ProjectActions.projectSaveFail(undefined)); + }); + + it('handles 403 response when creating new project', async () => { + store = mockStore(newProjectState); + const error403 = { + response: { + status: 403, + data: { error: 'Forbidden' } + } + }; + jest.spyOn(apiClient, 'post').mockRejectedValueOnce(error403); + + await store.dispatch(ProjectActions.saveProject()); + + const actions = store.getActions(); + expect(actions).toContainEqual({ + type: ActionTypes.SHOW_ERROR_MODAL, + modalType: 'staleSession' + }); + }); + }); +});