diff --git a/backend/src/main/java/heartbeat/service/pipeline/buildkite/CachePageService.java b/backend/src/main/java/heartbeat/service/pipeline/buildkite/CachePageService.java index 01e57c984..5a8d6f36e 100644 --- a/backend/src/main/java/heartbeat/service/pipeline/buildkite/CachePageService.java +++ b/backend/src/main/java/heartbeat/service/pipeline/buildkite/CachePageService.java @@ -13,11 +13,13 @@ import heartbeat.client.dto.pipeline.buildkite.BuildKiteBuildInfo; import heartbeat.client.dto.pipeline.buildkite.PageBuildKitePipelineInfoDTO; import heartbeat.client.dto.pipeline.buildkite.PageStepsInfoDto; +import heartbeat.exception.BaseException; import heartbeat.exception.InternalServerErrorException; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.cache.annotation.Cacheable; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; @@ -81,10 +83,13 @@ public PageOrganizationsInfoDTO getGitHubOrganizations(String token, int page, i int totalPage = parseTotalPage(allOrganizations.getHeaders().get(BUILD_KITE_LINK_HEADER)); return PageOrganizationsInfoDTO.builder().pageInfo(allOrganizations.getBody()).totalPage(totalPage).build(); } - catch (Exception e) { + catch (BaseException e) { log.info("Error to get paginated github organization info, page: {}, exception: {}", page, e); - throw new InternalServerErrorException( - String.format("Error to get paginated github organization info, page: %s, exception: %s", page, e)); + if (e.getStatus() == HttpStatus.INTERNAL_SERVER_ERROR.value()) { + throw new InternalServerErrorException(String + .format("Error to get paginated github organization info, page: %s, exception: %s", page, e)); + } + throw e; } } @@ -98,10 +103,13 @@ public PageReposInfoDTO getGitHubRepos(String token, String organization, int pa int totalPage = parseTotalPage(allRepos.getHeaders().get(BUILD_KITE_LINK_HEADER)); return PageReposInfoDTO.builder().pageInfo(allRepos.getBody()).totalPage(totalPage).build(); } - catch (Exception e) { + catch (BaseException e) { log.info("Error to get paginated github repo info, page: {}, exception: {}", page, e); - throw new InternalServerErrorException( - String.format("Error to get paginated github repo info, page: %s, exception: %s", page, e)); + if (e.getStatus() == HttpStatus.INTERNAL_SERVER_ERROR.value()) { + throw new InternalServerErrorException( + String.format("Error to get paginated github repo info, page: %s, exception: %s", page, e)); + } + throw e; } } @@ -116,10 +124,13 @@ public PageBranchesInfoDTO getGitHubBranches(String token, String organization, int totalPage = parseTotalPage(allRepos.getHeaders().get(BUILD_KITE_LINK_HEADER)); return PageBranchesInfoDTO.builder().pageInfo(allRepos.getBody()).totalPage(totalPage).build(); } - catch (Exception e) { + catch (BaseException e) { log.info("Error to get paginated github branch info, page: {}, exception: {}", page, e); - throw new InternalServerErrorException( - String.format("Error to get paginated github branch info, page: %s, exception: %s", page, e)); + if (e.getStatus() == HttpStatus.INTERNAL_SERVER_ERROR.value()) { + throw new InternalServerErrorException( + String.format("Error to get paginated github branch info, page: %s, exception: %s", page, e)); + } + throw e; } } @@ -135,10 +146,13 @@ public PagePullRequestInfoDTO getGitHubPullRequest(String token, String organiza int totalPage = parseTotalPage(allPullRequests.getHeaders().get(BUILD_KITE_LINK_HEADER)); return PagePullRequestInfoDTO.builder().pageInfo(allPullRequests.getBody()).totalPage(totalPage).build(); } - catch (Exception e) { + catch (BaseException e) { log.info("Error to get paginated github pull request info, page: {}, exception: {}", page, e); - throw new InternalServerErrorException( - String.format("Error to get paginated github pull request info, page: %s, exception: %s", page, e)); + if (e.getStatus() == HttpStatus.INTERNAL_SERVER_ERROR.value()) { + throw new InternalServerErrorException(String + .format("Error to get paginated github pull request info, page: %s, exception: %s", page, e)); + } + throw e; } } diff --git a/backend/src/test/java/heartbeat/service/pipeline/buildkite/CachePageServiceTest.java b/backend/src/test/java/heartbeat/service/pipeline/buildkite/CachePageServiceTest.java index b79a838ab..64f972114 100644 --- a/backend/src/test/java/heartbeat/service/pipeline/buildkite/CachePageServiceTest.java +++ b/backend/src/test/java/heartbeat/service/pipeline/buildkite/CachePageServiceTest.java @@ -16,9 +16,17 @@ import heartbeat.client.dto.pipeline.buildkite.BuildKiteJob; import heartbeat.client.dto.pipeline.buildkite.BuildKitePipelineDTO; import heartbeat.client.dto.pipeline.buildkite.PageStepsInfoDto; +import heartbeat.exception.BaseException; import heartbeat.exception.InternalServerErrorException; +import heartbeat.exception.NotFoundException; +import heartbeat.exception.PermissionDenyException; +import heartbeat.exception.RequestFailedException; +import heartbeat.exception.UnauthorizedException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -32,6 +40,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.hibernate.validator.internal.util.Contracts.assertNotNull; @@ -218,11 +227,27 @@ void shouldReturnPageOrganizationsInfoDtoWhenFetchPageOrganizationsInfoSuccessGi @Test void shouldThrowExceptionWhenFetchPageOrganizationsInfoThrow500() { - when(gitHubFeignClient.getAllOrganizations(MOCK_TOKEN, 100, 1)).thenThrow(RuntimeException.class); + when(gitHubFeignClient.getAllOrganizations(MOCK_TOKEN, 100, 1)).thenThrow(new RequestFailedException(500, "error")); InternalServerErrorException internalServerErrorException = assertThrows(InternalServerErrorException.class, () -> cachePageService.getGitHubOrganizations(MOCK_TOKEN, 1, 100)); + assertEquals(500, internalServerErrorException.getStatus()); - assertEquals("Error to get paginated github organization info, page: 1, exception: java.lang.RuntimeException", internalServerErrorException.getMessage()); + assertEquals("Error to get paginated github organization info, page: 1, exception: heartbeat.exception.RequestFailedException: Request failed with status statusCode 500, error: error", + internalServerErrorException.getMessage()); + } + + @ParameterizedTest + @MethodSource("baseExceptionProvider") + void shouldThrowExceptionWhenFetchPageOrganizationInfoThrow4xx(BaseException e, int errorCode) { + when(gitHubFeignClient.getAllOrganizations(MOCK_TOKEN, 100, 1)) + .thenThrow(e); + + BaseException baseException = assertThrows(BaseException.class, + () -> cachePageService.getGitHubOrganizations(MOCK_TOKEN, 1, 100)); + + assertEquals(errorCode, baseException.getStatus()); + assertEquals("error", + baseException.getMessage()); } @Test @@ -243,15 +268,31 @@ void shouldReturnPageReposInfoDtoWhenFetchPageReposInfoSuccessGivenExist() throw @Test void shouldThrowExceptionWhenFetchPageRepoInfoThrow500() { String organization = "test-org"; - when(gitHubFeignClient.getAllRepos(MOCK_TOKEN, organization, 100, 1)).thenThrow(RuntimeException.class); + when(gitHubFeignClient.getAllRepos(MOCK_TOKEN, organization, 100, 1)) + .thenThrow(new RequestFailedException(500, "error")); InternalServerErrorException internalServerErrorException = assertThrows(InternalServerErrorException.class, () -> cachePageService.getGitHubRepos(MOCK_TOKEN, organization, 1, 100)); + assertEquals(500, internalServerErrorException.getStatus()); - assertEquals("Error to get paginated github repo info, page: 1, exception: java.lang.RuntimeException", + assertEquals( + "Error to get paginated github repo info, page: 1, exception: heartbeat.exception.RequestFailedException: Request failed with status statusCode 500, error: error", internalServerErrorException.getMessage()); } + @ParameterizedTest + @MethodSource("baseExceptionProvider") + void shouldThrowExceptionWhenFetchPageRepoInfoThrow4xx(BaseException e, int errorCode) { + String organization = "test-org"; + when(gitHubFeignClient.getAllRepos(MOCK_TOKEN, organization, 100, 1)).thenThrow(e); + + BaseException baseException = assertThrows(BaseException.class, + () -> cachePageService.getGitHubRepos(MOCK_TOKEN, organization, 1, 100)); + + assertEquals(errorCode, baseException.getStatus()); + assertEquals("error", baseException.getMessage()); + } + @Test void shouldReturnPageBranchesInfoDtoWhenFetchPageBranchesInfoSuccessGivenExist() throws IOException { String organization = "test-org"; @@ -274,15 +315,31 @@ void shouldThrowExceptionWhenFetchPageBranchInfoThrow500() { String organization = "test-org"; String repo = "test-repo"; when(gitHubFeignClient.getAllBranches(MOCK_TOKEN, organization, repo, 100, 1)) - .thenThrow(RuntimeException.class); + .thenThrow(new RequestFailedException(500, "error")); InternalServerErrorException internalServerErrorException = assertThrows(InternalServerErrorException.class, () -> cachePageService.getGitHubBranches(MOCK_TOKEN, organization, repo, 1, 100)); + assertEquals(500, internalServerErrorException.getStatus()); - assertEquals("Error to get paginated github branch info, page: 1, exception: java.lang.RuntimeException", + assertEquals( + "Error to get paginated github branch info, page: 1, exception: heartbeat.exception.RequestFailedException: Request failed with status statusCode 500, error: error", internalServerErrorException.getMessage()); } + @ParameterizedTest + @MethodSource("baseExceptionProvider") + void shouldThrowExceptionWhenFetchPageBranchInfoThrow4xx(BaseException e, int errorCode) { + String organization = "test-org"; + String repo = "test-repo"; + when(gitHubFeignClient.getAllBranches(MOCK_TOKEN, organization, repo, 100, 1)).thenThrow(e); + + BaseException baseException = assertThrows(BaseException.class, + () -> cachePageService.getGitHubBranches(MOCK_TOKEN, organization, repo, 1, 100)); + + assertEquals(errorCode, baseException.getStatus()); + assertEquals("error", baseException.getMessage()); + } + @Test void shouldReturnPagePullRequestInfoDtoWhenFetchPullRequestInfoSuccessGivenExist() throws IOException { String organization = "test-org"; @@ -308,15 +365,31 @@ void shouldThrowExceptionWhenFetchPagePullRequestInfoThrow500() { String repo = "test-repo"; String branch = "test-branch"; when(gitHubFeignClient.getAllPullRequests(MOCK_TOKEN, organization, repo, 100, 1, branch, "all")) - .thenThrow(RuntimeException.class); + .thenThrow(new RequestFailedException(500, "error")); InternalServerErrorException internalServerErrorException = assertThrows(InternalServerErrorException.class, () -> cachePageService.getGitHubPullRequest(MOCK_TOKEN, organization, repo, branch, 1, 100)); assertEquals(500, internalServerErrorException.getStatus()); - assertEquals("Error to get paginated github pull request info, page: 1, exception: java.lang.RuntimeException", + assertEquals( + "Error to get paginated github pull request info, page: 1, exception: heartbeat.exception.RequestFailedException: Request failed with status statusCode 500, error: error", internalServerErrorException.getMessage()); } + @ParameterizedTest + @MethodSource("baseExceptionProvider") + void shouldThrowExceptionWhenFetchPagePullRequestInfoThrow4xx(BaseException e, int errorCode) { + String organization = "test-org"; + String repo = "test-repo"; + String branch = "test-branch"; + when(gitHubFeignClient.getAllPullRequests(MOCK_TOKEN, organization, repo, 100, 1, branch, "all")).thenThrow(e); + + BaseException baseException = assertThrows(BaseException.class, + () -> cachePageService.getGitHubPullRequest(MOCK_TOKEN, organization, repo, branch, 1, 100)); + + assertEquals(errorCode, baseException.getStatus()); + assertEquals("error", baseException.getMessage()); + } + private static ResponseEntity> getResponseEntity(HttpHeaders httpHeaders, String pathname) throws IOException { ObjectMapper mapper = new ObjectMapper(); @@ -337,4 +410,11 @@ private HttpHeaders buildGitHubHttpHeaders() { return buildHttpHeaders(GITHUB_TOTAL_PAGE_HEADER); } + static Stream baseExceptionProvider() { + return Stream.of(Arguments.of(new PermissionDenyException("error"), 403), + Arguments.of(new UnauthorizedException("error"), 401), Arguments.of(new NotFoundException("error"), 404) + + ); + } + } diff --git a/frontend/__tests__/containers/MetricsStep/SourceControlConfiguration/SourceControlConfiguration.test.tsx b/frontend/__tests__/containers/MetricsStep/SourceControlConfiguration/SourceControlConfiguration.test.tsx index 3c0184798..350dfce37 100644 --- a/frontend/__tests__/containers/MetricsStep/SourceControlConfiguration/SourceControlConfiguration.test.tsx +++ b/frontend/__tests__/containers/MetricsStep/SourceControlConfiguration/SourceControlConfiguration.test.tsx @@ -10,6 +10,7 @@ import { IUseGetSourceControlConfigurationCrewInterface } from '@src/hooks/useGe import { SourceControlConfiguration } from '@src/containers/MetricsStep/SouceControlConfiguration'; import { act, render, screen, waitFor, within } from '@testing-library/react'; import { LIST_OPEN, LOADING, REMOVE_BUTTON } from '@test/fixtures'; +import { MetricsDataFailStatus } from '@src/constants/commons'; import { setupStore } from '@test/utils/setupStoreUtil'; import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; @@ -35,16 +36,37 @@ const mockInitRepoEffectResponse = { isGetRepo: true, isLoading: false, getSourceControlRepoInfo: jest.fn(), + info: { + code: 200, + data: undefined, + errorTitle: '', + errorMessage: '', + }, + stepFailedStatus: MetricsDataFailStatus.NotFailed, }; const mockInitBranchEffectResponse = { isLoading: false, getSourceControlBranchInfo: jest.fn(), isGetBranch: true, + info: { + code: 200, + data: undefined, + errorTitle: '', + errorMessage: '', + }, + stepFailedStatus: MetricsDataFailStatus.NotFailed, }; const mockInitCrewEffectResponse = { isLoading: false, getSourceControlCrewInfo: jest.fn(), isGetAllCrews: true, + info: { + code: 200, + data: undefined, + errorTitle: '', + errorMessage: '', + }, + stepFailedStatus: MetricsDataFailStatus.NotFailed, }; let mockSourceControlSettings = mockInitSourceControlSettings; @@ -221,4 +243,18 @@ describe('SourceControlConfiguration', () => { expect(screen.getByText('Crew setting (optional)')).toBeInTheDocument(); }); + + it('should set error info when any request return error', () => { + mockOrganizationEffectResponse = { + ...mockOrganizationEffectResponse, + info: { + code: 404, + errorTitle: 'error title', + errorMessage: 'error message', + }, + }; + setup(); + + expect(screen.getByLabelText('Error UI for pipeline settings')).toBeInTheDocument(); + }); }); diff --git a/frontend/__tests__/containers/MetricsStep/SourceControlConfiguration/SourceControlMetricSelection.test.tsx b/frontend/__tests__/containers/MetricsStep/SourceControlConfiguration/SourceControlMetricSelection.test.tsx index 4b89988c6..4a3ea59c6 100644 --- a/frontend/__tests__/containers/MetricsStep/SourceControlConfiguration/SourceControlMetricSelection.test.tsx +++ b/frontend/__tests__/containers/MetricsStep/SourceControlConfiguration/SourceControlMetricSelection.test.tsx @@ -3,6 +3,7 @@ import { IUseGetSourceControlConfigurationBranchInterface } from '@src/hooks/use import { IUseGetSourceControlConfigurationRepoInterface } from '@src/hooks/useGetSourceControlConfigurationRepoEffect'; import { IUseGetSourceControlConfigurationCrewInterface } from '@src/hooks/useGetSourceControlConfigurationCrewEffect'; import { act, render, screen, within } from '@testing-library/react'; +import { MetricsDataFailStatus } from '@src/constants/commons'; import { setupStore } from '@test/utils/setupStoreUtil'; import userEvent from '@testing-library/user-event'; import { LIST_OPEN, LOADING } from '@test/fixtures'; @@ -12,16 +13,37 @@ const mockInitRepoEffectResponse = { isGetRepo: true, isLoading: false, getSourceControlRepoInfo: jest.fn(), + info: { + code: 200, + data: undefined, + errorTitle: '', + errorMessage: '', + }, + stepFailedStatus: MetricsDataFailStatus.NotFailed, }; const mockInitBranchEffectResponse = { isLoading: false, getSourceControlBranchInfo: jest.fn(), isGetBranch: true, + info: { + code: 200, + data: undefined, + errorTitle: '', + errorMessage: '', + }, + stepFailedStatus: MetricsDataFailStatus.NotFailed, }; const mockInitCrewEffectResponse = { isLoading: false, getSourceControlCrewInfo: jest.fn(), isGetAllCrews: true, + info: { + code: 200, + data: undefined, + errorTitle: '', + errorMessage: '', + }, + stepFailedStatus: MetricsDataFailStatus.NotFailed, }; let mockRepoEffectResponse: IUseGetSourceControlConfigurationRepoInterface = mockInitRepoEffectResponse; @@ -34,6 +56,11 @@ let mockSelectSourceControlRepos = mockInitSelectSourceControlRepos; const mockInitSelectSourceControlBranches = ['mockBranchName', 'mockBranchName1']; let mockSelectSourceControlBranches = mockInitSelectSourceControlBranches; +const myDispatch = jest.fn(); +jest.mock('@src/hooks', () => ({ + ...jest.requireActual('@src/hooks'), + useAppDispatch: () => myDispatch, +})); jest.mock('@src/hooks/useGetSourceControlConfigurationRepoEffect', () => { return { useGetSourceControlConfigurationRepoEffect: () => mockRepoEffectResponse, @@ -50,6 +77,10 @@ jest.mock('@src/hooks/useGetSourceControlConfigurationCrewEffect', () => { }; }); +jest.mock('@src/context/notification/NotificationSlice', () => ({ + ...jest.requireActual('@src/context/notification/NotificationSlice'), + addNotification: jest.fn(), +})); jest.mock('@src/context/Metrics/metricsSlice', () => ({ ...jest.requireActual('@src/context/Metrics/metricsSlice'), selectSourceControlConfigurationSettings: jest.fn().mockImplementation(() => [ @@ -78,7 +109,11 @@ describe('SourceControlMetricSelection', () => { mockSelectSourceControlBranches = mockInitSelectSourceControlBranches; mockSelectSourceControlRepos = mockInitSelectSourceControlRepos; }); + afterEach(() => { + jest.clearAllMocks(); + }); const onUpdateSourceControl = jest.fn(); + const handleUpdateErrorInfo = jest.fn(); const setup = (isDuplicated: boolean = false) => { const sourceControlSetting = { id: 0, @@ -96,6 +131,7 @@ describe('SourceControlMetricSelection', () => { isDuplicated={isDuplicated} setLoadingCompletedNumber={jest.fn()} totalSourceControlNumber={1} + handleUpdateErrorInfo={handleUpdateErrorInfo} /> , ); @@ -188,4 +224,43 @@ describe('SourceControlMetricSelection', () => { expect(getSourceControlBranchInfoFunction).toHaveBeenCalledTimes(1); expect(getSourceControlCrewInfoFunction).toHaveBeenCalledTimes(2); }); + + it('should add partial failed 4xx notification when any failed status is PartialFailed4xx', async () => { + mockCrewEffectResponse = { + ...mockCrewEffectResponse, + stepFailedStatus: MetricsDataFailStatus.PartialFailed4xx, + }; + setup(); + + expect(myDispatch).toHaveBeenCalledTimes(1); + }); + + it('should add partial failed 4xx notification when any failed status is PartialFailedNoCards', async () => { + mockCrewEffectResponse = { + ...mockCrewEffectResponse, + stepFailedStatus: MetricsDataFailStatus.PartialFailedNoCards, + }; + setup(); + + expect(myDispatch).toHaveBeenCalledTimes(1); + }); + + it('should set error info when any request return error', () => { + mockRepoEffectResponse = { + ...mockRepoEffectResponse, + info: { + code: 404, + errorTitle: 'error title', + errorMessage: 'error message', + }, + }; + setup(); + + expect(handleUpdateErrorInfo).toHaveBeenCalledTimes(1); + expect(handleUpdateErrorInfo).toBeCalledWith({ + code: 404, + errorTitle: 'error title', + errorMessage: 'error message', + }); + }); }); diff --git a/frontend/__tests__/hooks/useGetSourceControlConfigurationBranchEffect.test.tsx b/frontend/__tests__/hooks/useGetSourceControlConfigurationBranchEffect.test.tsx index f29375bf3..7750ea13e 100644 --- a/frontend/__tests__/hooks/useGetSourceControlConfigurationBranchEffect.test.tsx +++ b/frontend/__tests__/hooks/useGetSourceControlConfigurationBranchEffect.test.tsx @@ -2,6 +2,7 @@ import { useGetSourceControlConfigurationBranchEffect } from '@src/hooks/useGetS import { sourceControlClient } from '@src/clients/sourceControl/SourceControlClient'; import { MOCK_GITHUB_GET_BRANCHES_RESPONSE } from '@test/fixtures'; import { act, renderHook, waitFor } from '@testing-library/react'; +import { MetricsDataFailStatus } from '@src/constants/commons'; import { setupStore } from '@test/utils/setupStoreUtil'; import React, { ReactNode } from 'react'; import { Provider } from 'react-redux'; @@ -9,6 +10,10 @@ import { HttpStatusCode } from 'axios'; const mockDispatch = jest.fn(); const store = setupStore(); +jest.mock('@src/hooks', () => ({ + ...jest.requireActual('@src/hooks'), + useAppDispatch: () => jest.fn(), +})); jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), useDispatch: () => mockDispatch, @@ -27,31 +32,40 @@ const Wrapper = ({ children }: { children: ReactNode }) => { const clientSpy = jest.fn(); const mockRepo = jest.fn().mockImplementation(() => { clientSpy(); - return { - code: HttpStatusCode.Ok, - data: MOCK_GITHUB_GET_BRANCHES_RESPONSE, - errorTittle: '', - errorMessage: '', - }; -}); - -beforeEach(() => { - sourceControlClient.getBranch = mockRepo; - clientSpy.mockClear(); + return new Promise(() => { + return { + code: HttpStatusCode.Ok, + data: MOCK_GITHUB_GET_BRANCHES_RESPONSE, + errorTittle: '', + errorMessage: '', + }; + }); }); describe('use get source control configuration branch info side effect', () => { + beforeEach(() => { + sourceControlClient.getBranch = mockRepo; + clientSpy.mockClear(); + }); + it('should init data state when render hook', async () => { const { result } = renderHook(() => useGetSourceControlConfigurationBranchEffect(), { wrapper: Wrapper }); expect(result.current.isLoading).toBeFalsy(); expect(result.current.isGetBranch).toBeFalsy(); }); - it('should return success data and loading state when client goes happy path', async () => { + it('should return success data and loading state when client return 200', async () => { const { result } = renderHook(() => useGetSourceControlConfigurationBranchEffect(), { wrapper: Wrapper }); const mockOrganization = 'mockOrg'; const mockRepo = 'mockRepo'; - + sourceControlClient.getBranch = jest.fn().mockImplementation(() => { + return Promise.resolve({ + code: 200, + data: { + name: ['test-branch1', 'test-branch2'], + }, + }); + }); await act(async () => { result.current.getSourceControlBranchInfo(mockOrganization, mockRepo, 1); }); @@ -60,6 +74,64 @@ describe('use get source control configuration branch info side effect', () => { expect(result.current.isLoading).toBeFalsy(); expect(result.current.isGetBranch).toBeTruthy(); }); - expect(clientSpy).toBeCalled(); + + expect(sourceControlClient.getBranch).toBeCalled(); + }); + + it('should set error info when client dont return 200', async () => { + const { result } = renderHook(() => useGetSourceControlConfigurationBranchEffect(), { wrapper: Wrapper }); + const mockOrganization = 'mockOrg'; + const mockRepo = 'mockRepo'; + sourceControlClient.getBranch = jest.fn().mockImplementation(() => { + return Promise.resolve({ + code: 400, + }); + }); + await act(async () => { + result.current.getSourceControlBranchInfo(mockOrganization, mockRepo, 1); + }); + + expect(sourceControlClient.getBranch).toBeCalled(); + expect(result.current.info).toEqual({ + code: 400, + }); + }); + + it('should set error step failed status to PartialFailed4xx when getting branch response is failed and client return 400', async () => { + const { result } = renderHook(() => useGetSourceControlConfigurationBranchEffect(), { wrapper: Wrapper }); + const mockOrganization = 'mockOrg'; + const mockRepo = 'mockRepo'; + sourceControlClient.getBranch = jest.fn().mockImplementation(() => { + return Promise.reject({ + reason: { + code: 400, + }, + }); + }); + await act(async () => { + result.current.getSourceControlBranchInfo(mockOrganization, mockRepo, 1); + }); + + expect(sourceControlClient.getBranch).toBeCalled(); + expect(result.current.stepFailedStatus).toEqual(MetricsDataFailStatus.PartialFailed4xx); + }); + + it('should set error step failed status to PartialFailedTimeout when getting branch response is failed and client dont return 400', async () => { + const { result } = renderHook(() => useGetSourceControlConfigurationBranchEffect(), { wrapper: Wrapper }); + const mockOrganization = 'mockOrg'; + const mockRepo = 'mockRepo'; + sourceControlClient.getBranch = jest.fn().mockImplementation(() => { + return Promise.reject({ + reason: { + code: 404, + }, + }); + }); + await act(async () => { + result.current.getSourceControlBranchInfo(mockOrganization, mockRepo, 1); + }); + + expect(sourceControlClient.getBranch).toBeCalled(); + expect(result.current.stepFailedStatus).toEqual(MetricsDataFailStatus.PartialFailedTimeout); }); }); diff --git a/frontend/__tests__/hooks/useGetSourceControlConfigurationCrewEffect.test.tsx b/frontend/__tests__/hooks/useGetSourceControlConfigurationCrewEffect.test.tsx index 6d5b64e68..145b2bfec 100644 --- a/frontend/__tests__/hooks/useGetSourceControlConfigurationCrewEffect.test.tsx +++ b/frontend/__tests__/hooks/useGetSourceControlConfigurationCrewEffect.test.tsx @@ -2,6 +2,7 @@ import { useGetSourceControlConfigurationCrewEffect } from '@src/hooks/useGetSou import { sourceControlClient } from '@src/clients/sourceControl/SourceControlClient'; import { act, renderHook, waitFor } from '@testing-library/react'; import { MOCK_GITHUB_GET_CREWS_RESPONSE } from '@test/fixtures'; +import { MetricsDataFailStatus } from '@src/constants/commons'; import { DateRange } from '@src/context/config/configSlice'; import { setupStore } from '@test/utils/setupStoreUtil'; import React, { ReactNode } from 'react'; @@ -36,12 +37,12 @@ const mockRepo = jest.fn().mockImplementation(() => { }; }); -beforeEach(() => { - sourceControlClient.getCrew = mockRepo; - clientSpy.mockClear(); -}); - describe('use get source control configuration crew info side effect', () => { + beforeEach(() => { + sourceControlClient.getCrew = mockRepo; + clientSpy.mockClear(); + }); + it('should init data state when render hook', async () => { const { result } = renderHook(() => useGetSourceControlConfigurationCrewEffect(), { wrapper: Wrapper }); expect(result.current.isLoading).toBeFalsy(); @@ -74,4 +75,68 @@ describe('use get source control configuration crew info side effect', () => { }); expect(clientSpy).toHaveBeenCalledTimes(2); }); + + it('should set error step failed status to PartialFailed4xx when one of getting repo response is failed and code is 400', async () => { + sourceControlClient.getCrew = jest + .fn() + .mockImplementationOnce(() => { + return Promise.reject({ + code: 400, + }); + }) + .mockImplementationOnce(() => { + return Promise.resolve('success'); + }); + const { result } = renderHook(() => useGetSourceControlConfigurationCrewEffect(), { wrapper: Wrapper }); + const mockOrganization = 'mockOrg'; + const mockRepo = 'mockRepo'; + const mockBranch = 'mockBranch'; + const dateRanges: DateRange[] = [ + { + startDate: '2024-07-31T00:00:00.000+08:00', + endDate: '2024-08-02T23:59:59.999+08:00', + }, + { + startDate: '2024-07-15T00:00:00.000+08:00', + endDate: '2024-07-28T23:59:59.999+08:00', + }, + ]; + + await act(async () => { + result.current.getSourceControlCrewInfo(mockOrganization, mockRepo, mockBranch, dateRanges); + }); + + expect(result.current.stepFailedStatus).toEqual(MetricsDataFailStatus.PartialFailed4xx); + }); + + it('should set error step failed status to PartialFailedTimeout when one of getting repo responses is failed and code is not 400', async () => { + sourceControlClient.getCrew = jest + .fn() + .mockImplementationOnce(() => { + return Promise.reject('error'); + }) + .mockImplementationOnce(() => { + return Promise.resolve('success'); + }); + const { result } = renderHook(() => useGetSourceControlConfigurationCrewEffect(), { wrapper: Wrapper }); + const mockOrganization = 'mockOrg'; + const mockRepo = 'mockRepo'; + const mockBranch = 'mockBranch'; + const dateRanges: DateRange[] = [ + { + startDate: '2024-07-31T00:00:00.000+08:00', + endDate: '2024-08-02T23:59:59.999+08:00', + }, + { + startDate: '2024-07-15T00:00:00.000+08:00', + endDate: '2024-07-28T23:59:59.999+08:00', + }, + ]; + + await act(async () => { + result.current.getSourceControlCrewInfo(mockOrganization, mockRepo, mockBranch, dateRanges); + }); + + expect(result.current.stepFailedStatus).toEqual(MetricsDataFailStatus.PartialFailedTimeout); + }); }); diff --git a/frontend/__tests__/hooks/useGetSourceControlConfigurationRepoEffect.test.tsx b/frontend/__tests__/hooks/useGetSourceControlConfigurationRepoEffect.test.tsx index c5d35dcb6..2c6376778 100644 --- a/frontend/__tests__/hooks/useGetSourceControlConfigurationRepoEffect.test.tsx +++ b/frontend/__tests__/hooks/useGetSourceControlConfigurationRepoEffect.test.tsx @@ -1,6 +1,7 @@ import { useGetSourceControlConfigurationRepoEffect } from '@src/hooks/useGetSourceControlConfigurationRepoEffect'; import { sourceControlClient } from '@src/clients/sourceControl/SourceControlClient'; import { act, renderHook, waitFor } from '@testing-library/react'; +import { MetricsDataFailStatus } from '@src/constants/commons'; import { MOCK_GITHUB_GET_REPO_RESPONSE } from '@test/fixtures'; import { DateRange } from '@src/context/config/configSlice'; import { setupStore } from '@test/utils/setupStoreUtil'; @@ -36,12 +37,11 @@ const mockRepo = jest.fn().mockImplementation(() => { }; }); -beforeEach(() => { - sourceControlClient.getRepo = mockRepo; - clientSpy.mockClear(); -}); - describe('use get source control configuration repo info side effect', () => { + beforeEach(() => { + sourceControlClient.getRepo = mockRepo; + clientSpy.mockClear(); + }); it('should init data state when render hook', async () => { const { result } = renderHook(() => useGetSourceControlConfigurationRepoEffect(), { wrapper: Wrapper }); @@ -69,4 +69,64 @@ describe('use get source control configuration repo info side effect', () => { }); expect(clientSpy).toBeCalled(); }); + + it('should set error step failed status to PartialFailedTimeout when one of getting repo responses is failed and code is not 400', async () => { + sourceControlClient.getRepo = jest + .fn() + .mockImplementationOnce(() => { + return Promise.reject('error'); + }) + .mockImplementationOnce(() => { + return Promise.resolve('success'); + }); + const { result } = renderHook(() => useGetSourceControlConfigurationRepoEffect(), { wrapper: Wrapper }); + const mockOrganization = 'mockOrg'; + const mockDateRanges: DateRange[] = [ + { + startDate: 'startTime', + endDate: 'endTime', + }, + { + startDate: 'startTime', + endDate: 'endTime', + }, + ]; + + await act(async () => { + result.current.getSourceControlRepoInfo(mockOrganization, mockDateRanges, 1); + }); + + expect(result.current.stepFailedStatus).toEqual(MetricsDataFailStatus.PartialFailedTimeout); + }); + + it('should set error step failed status to PartialFailed4xx when one of getting repo response is failed and code is 400', async () => { + sourceControlClient.getRepo = jest + .fn() + .mockImplementationOnce(() => { + return Promise.reject({ + code: 400, + }); + }) + .mockImplementationOnce(() => { + return Promise.resolve('success'); + }); + const { result } = renderHook(() => useGetSourceControlConfigurationRepoEffect(), { wrapper: Wrapper }); + const mockOrganization = 'mockOrg'; + const mockDateRanges: DateRange[] = [ + { + startDate: 'startTime', + endDate: 'endTime', + }, + { + startDate: 'startTime', + endDate: 'endTime', + }, + ]; + + await act(async () => { + result.current.getSourceControlRepoInfo(mockOrganization, mockDateRanges, 1); + }); + + expect(result.current.stepFailedStatus).toEqual(MetricsDataFailStatus.PartialFailed4xx); + }); }); diff --git a/frontend/src/constants/resources.ts b/frontend/src/constants/resources.ts index 0014b9cbd..e574d409f 100644 --- a/frontend/src/constants/resources.ts +++ b/frontend/src/constants/resources.ts @@ -429,6 +429,10 @@ export const MESSAGE = { 'Failed to get partial Pipeline configuration, please go back to the previous page and change your pipeline token with correct access permission, or click "Next" button to go to Report page.', PIPELINE_STEP_REQUEST_PARTIAL_FAILED_OTHERS: 'Failed to get partial Pipeline configuration, you can click "Next" button to go to Report page.', + SOURCE_CONTROL_REQUEST_PARTIAL_FAILED_4XX: + 'Failed to get partial Source control configuration, please go back to the previous page and change your source control token with correct access permission, or click "Next" button to go to Report page.', + SOURCE_CONTROL_REQUEST_PARTIAL_FAILED_OTHERS: + 'Failed to get partial Source control configuration, you can click "Next" button to go to Report page.', DORA_CHART_LOADING_FAILED: 'Dora metrics loading timeout, Please click "Retry"!', SHARE_REPORT_EXPIRED: 'The report has expired. Please go home page and generate it again.', }; diff --git a/frontend/src/containers/MetricsStep/SouceControlConfiguration/SourceControlMetricSelection/index.tsx b/frontend/src/containers/MetricsStep/SouceControlConfiguration/SourceControlMetricSelection/index.tsx index f74ae428c..4c217fc46 100644 --- a/frontend/src/containers/MetricsStep/SouceControlConfiguration/SourceControlMetricSelection/index.tsx +++ b/frontend/src/containers/MetricsStep/SouceControlConfiguration/SourceControlMetricSelection/index.tsx @@ -5,10 +5,10 @@ import { WarningMessage, } from '@src/containers/MetricsStep/DeploymentFrequencySettings/PipelineMetricSelection/style'; import { + selectDateRange, + selectSourceControlBranches, selectSourceControlOrganizations, selectSourceControlRepos, - selectSourceControlBranches, - selectDateRange, } from '@src/context/config/configSlice'; import { selectSourceControlConfigurationSettings, @@ -19,9 +19,14 @@ import { useGetSourceControlConfigurationRepoEffect } from '@src/hooks/useGetSou import { useGetSourceControlConfigurationCrewEffect } from '@src/hooks/useGetSourceControlConfigurationCrewEffect'; import { SourceControlBranch } from '@src/containers/MetricsStep/SouceControlConfiguration/SourceControlBranch'; import { SingleSelection } from '@src/containers/MetricsStep/DeploymentFrequencySettings/SingleSelection'; +import { ErrorInfoType } from '@src/containers/MetricsStep/SouceControlConfiguration'; +import { addNotification } from '@src/context/notification/NotificationSlice'; +import { MetricsDataFailStatus } from '@src/constants/commons'; import { useAppDispatch, useAppSelector } from '@src/hooks'; +import { MESSAGE } from '@src/constants/resources'; import { Loading } from '@src/components/Loading'; import { useEffect, useRef } from 'react'; +import { HttpStatusCode } from 'axios'; import { store } from '@src/store'; interface SourceControlMetricSelectionProps { @@ -33,6 +38,7 @@ interface SourceControlMetricSelectionProps { }; isShowRemoveButton: boolean; onRemoveSourceControl: (id: number) => void; + handleUpdateErrorInfo: (errorInfo: ErrorInfoType) => void; onUpdateSourceControl: (id: number, label: string, value: string | StringConstructor[] | string[]) => void; isDuplicated: boolean; setLoadingCompletedNumber: React.Dispatch>; @@ -47,6 +53,7 @@ export const SourceControlMetricSelection = ({ isDuplicated, setLoadingCompletedNumber, totalSourceControlNumber, + handleUpdateErrorInfo, }: SourceControlMetricSelectionProps) => { const { id, organization, repo } = sourceControlSetting; const isInitialMount = useRef(true); @@ -54,16 +61,22 @@ export const SourceControlMetricSelection = ({ isLoading: repoIsLoading, getSourceControlRepoInfo, isGetRepo, + info: repoInfo, + stepFailedStatus: getRepoFailedStatus, } = useGetSourceControlConfigurationRepoEffect(); const { isLoading: branchIsLoading, getSourceControlBranchInfo, isGetBranch, + info: branchInfo, + stepFailedStatus: getBranchFailedStatus, } = useGetSourceControlConfigurationBranchEffect(); const { isLoading: crewIsLoading, getSourceControlCrewInfo, isGetAllCrews, + info: crewInfo, + stepFailedStatus: getCrewFailedStatus, } = useGetSourceControlConfigurationCrewEffect(); const storeContext = store.getState(); const dispatch = useAppDispatch(); @@ -113,6 +126,49 @@ export const SourceControlMetricSelection = ({ } }, [isGetAllCrews, setLoadingCompletedNumber, totalSourceControlNumber]); + useEffect(() => { + const errorInfoList: ErrorInfoType[] = [repoInfo, branchInfo, crewInfo].filter( + (it) => it.code !== HttpStatusCode.Ok, + ); + const errorInfo = errorInfoList.length === 0 ? crewInfo : errorInfoList[0]; + handleUpdateErrorInfo(errorInfo); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [repoInfo, branchInfo, crewInfo]); + + useEffect(() => { + const popup = () => { + if ( + getRepoFailedStatus === MetricsDataFailStatus.PartialFailed4xx || + getBranchFailedStatus === MetricsDataFailStatus.PartialFailed4xx || + getCrewFailedStatus === MetricsDataFailStatus.PartialFailed4xx + ) { + dispatch( + addNotification({ + type: 'warning', + message: MESSAGE.SOURCE_CONTROL_REQUEST_PARTIAL_FAILED_4XX, + }), + ); + } else if ( + getRepoFailedStatus === MetricsDataFailStatus.PartialFailedNoCards || + getRepoFailedStatus === MetricsDataFailStatus.PartialFailedTimeout || + getBranchFailedStatus === MetricsDataFailStatus.PartialFailedNoCards || + getBranchFailedStatus === MetricsDataFailStatus.PartialFailedTimeout || + getCrewFailedStatus === MetricsDataFailStatus.PartialFailedNoCards || + getCrewFailedStatus === MetricsDataFailStatus.PartialFailedTimeout + ) { + dispatch( + addNotification({ + type: 'warning', + message: MESSAGE.SOURCE_CONTROL_REQUEST_PARTIAL_FAILED_OTHERS, + }), + ); + } + }; + if (!isLoading) { + popup(); + } + }, [dispatch, getBranchFailedStatus, getCrewFailedStatus, getRepoFailedStatus, isLoading]); + const handleOnUpdateOrganization = (id: number, label: string, value: string | []): void => { onUpdateSourceControl(id, label, value); getSourceControlRepoInfo(value.toString(), dateRanges, id); diff --git a/frontend/src/containers/MetricsStep/SouceControlConfiguration/index.tsx b/frontend/src/containers/MetricsStep/SouceControlConfiguration/index.tsx index 6aa22b101..b4dac9450 100644 --- a/frontend/src/containers/MetricsStep/SouceControlConfiguration/index.tsx +++ b/frontend/src/containers/MetricsStep/SouceControlConfiguration/index.tsx @@ -4,6 +4,12 @@ import { selectSourceControlConfigurationSettings, updateSourceControlConfigurationSettings, } from '@src/context/Metrics/metricsSlice'; +import { + ISourceControlGetBranchResponseDTO, + ISourceControlGetCrewResponseDTO, + ISourceControlGetOrganizationResponseDTO, + ISourceControlGetRepoResponseDTO, +} from '@src/clients/sourceControl/dto/response'; import { useGetSourceControlConfigurationOrganizationEffect } from '@src/hooks/useGetSourceControlConfigurationOrganizationEffect'; import PresentationForErrorCases from '@src/components/Metrics/MetricsStep/DeploymentFrequencySettings/PresentationForErrorCases'; import { SourceControlMetricSelection } from '@src/containers/MetricsStep/SouceControlConfiguration/SourceControlMetricSelection'; @@ -17,11 +23,17 @@ import { getErrorDetail } from '@src/context/meta/metaSlice'; import { useAppDispatch, useAppSelector } from '@src/hooks'; import { Crews } from '@src/containers/MetricsStep/Crews'; import { Loading } from '@src/components/Loading'; +import { useEffect, useState } from 'react'; import { HttpStatusCode } from 'axios'; import { store } from '@src/store'; -import { useState } from 'react'; import dayjs from 'dayjs'; +export type ErrorInfoType = + | ISourceControlGetOrganizationResponseDTO + | ISourceControlGetRepoResponseDTO + | ISourceControlGetBranchResponseDTO + | ISourceControlGetCrewResponseDTO; + export const SourceControlConfiguration = () => { const dispatch = useAppDispatch(); const storeContext = store.getState(); @@ -30,6 +42,7 @@ export const SourceControlConfiguration = () => { const sourceControlConfigurationSettings = useAppSelector(selectSourceControlConfigurationSettings); const { getDuplicatedSourceControlIds } = useMetricsStepValidationCheckContext(); const [loadingCompletedNumber, setLoadingCompletedNumber] = useState(0); + const [errorInfo, setErrorInfo] = useState(info); const dateRanges = useAppSelector(selectDateRange); const realSourceControlConfigurationSettings = isFirstFetch ? [] : sourceControlConfigurationSettings; const totalSourceControlNumber = realSourceControlConfigurationSettings.length; @@ -63,6 +76,16 @@ export const SourceControlConfiguration = () => { const handleUpdateSourceControl = (id: number, label: string, value: string | StringConstructor[] | string[]) => { dispatch(updateSourceControlConfigurationSettings({ updateId: id, label: label.toLowerCase(), value })); }; + const handleUpdateErrorInfo = (newErrorInfo: ErrorInfoType) => { + const errorInfoList: ErrorInfoType[] = [newErrorInfo, info].filter((it) => it.code !== HttpStatusCode.Ok); + const errorInfo = errorInfoList.length === 0 ? info : errorInfoList[0]; + setErrorInfo(errorInfo); + }; + + useEffect(() => { + handleUpdateErrorInfo(errorInfo); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [info]); const shouldShowCrews = loadingCompletedNumber !== 0 && @@ -72,8 +95,8 @@ export const SourceControlConfiguration = () => { return ( <> {isLoading && } - {info?.code !== HttpStatusCode.Ok ? ( - + {errorInfo?.code !== HttpStatusCode.Ok ? ( + ) : ( <> @@ -90,6 +113,7 @@ export const SourceControlConfiguration = () => { isDuplicated={getDuplicatedSourceControlIds(realSourceControlConfigurationSettings).includes( sourceControlConfigurationSetting.id, )} + handleUpdateErrorInfo={handleUpdateErrorInfo} totalSourceControlNumber={totalSourceControlNumber} setLoadingCompletedNumber={setLoadingCompletedNumber} /> diff --git a/frontend/src/hooks/useGetSourceControlConfigurationBranchEffect.tsx b/frontend/src/hooks/useGetSourceControlConfigurationBranchEffect.tsx index c6a1f706a..eb757eeeb 100644 --- a/frontend/src/hooks/useGetSourceControlConfigurationBranchEffect.tsx +++ b/frontend/src/hooks/useGetSourceControlConfigurationBranchEffect.tsx @@ -3,8 +3,10 @@ import { updateSourceControlConfigurationSettingsFirstInto, } from '@src/context/Metrics/metricsSlice'; import { selectSourceControl, updateSourceControlVerifiedResponse } from '@src/context/config/configSlice'; +import { ISourceControlGetBranchResponseDTO } from '@src/clients/sourceControl/dto/response'; import { sourceControlClient } from '@src/clients/sourceControl/SourceControlClient'; import { useAppDispatch, useAppSelector } from '@src/hooks/index'; +import { MetricsDataFailStatus } from '@src/constants/commons'; import { SourceControlTypes } from '@src/constants/resources'; import { HttpStatusCode } from 'axios'; import { useState } from 'react'; @@ -13,13 +15,23 @@ export interface IUseGetSourceControlConfigurationBranchInterface { readonly isLoading: boolean; readonly getSourceControlBranchInfo: (organization: string, repo: string, id: number) => Promise; readonly isGetBranch: boolean; + readonly info: ISourceControlGetBranchResponseDTO; + readonly stepFailedStatus: MetricsDataFailStatus; } export const useGetSourceControlConfigurationBranchEffect = (): IUseGetSourceControlConfigurationBranchInterface => { + const defaultInfoStructure = { + code: 200, + errorTitle: '', + errorMessage: '', + }; + const dispatch = useAppDispatch(); const [isLoading, setIsLoading] = useState(false); const shouldGetSourceControlConfig = useAppSelector(selectShouldGetSourceControlConfig); const [isGetBranch, setIsGetBranch] = useState(!shouldGetSourceControlConfig); const restoredSourceControlInfo = useAppSelector(selectSourceControl); + const [stepFailedStatus, setStepFailedStatus] = useState(MetricsDataFailStatus.NotFailed); + const [info, setInfo] = useState(defaultInfoStructure); function getEnumKeyByEnumValue(enumValue: string): SourceControlTypes { return Object.entries(SourceControlTypes) @@ -36,31 +48,43 @@ export const useGetSourceControlConfigurationBranchEffect = (): IUseGetSourceCon }; setIsLoading(true); try { - const response = await sourceControlClient.getBranch(params); - if (response.code === HttpStatusCode.Ok) { - dispatch( - updateSourceControlVerifiedResponse({ - parents: [ - { - name: 'organization', - value: organization, - }, - { - name: 'repo', - value: repo, - }, - ], - names: response.data?.name.map((it) => it), - }), - ); - dispatch( - updateSourceControlConfigurationSettingsFirstInto({ - ...response.data, - id, - type: 'branches', - }), - ); - } + sourceControlClient.getBranch(params).then( + (response) => { + if (response.code === HttpStatusCode.Ok) { + dispatch( + updateSourceControlVerifiedResponse({ + parents: [ + { + name: 'organization', + value: organization, + }, + { + name: 'repo', + value: repo, + }, + ], + names: response.data?.name.map((it) => it), + }), + ); + dispatch( + updateSourceControlConfigurationSettingsFirstInto({ + ...response.data, + id, + type: 'branches', + }), + ); + } else { + setInfo(response); + } + }, + (e) => { + if ((e as PromiseRejectedResult).reason.code == 400) { + setStepFailedStatus(MetricsDataFailStatus.PartialFailed4xx); + } else { + setStepFailedStatus(MetricsDataFailStatus.PartialFailedTimeout); + } + }, + ); } finally { setIsLoading(false); setIsGetBranch(true); @@ -71,5 +95,7 @@ export const useGetSourceControlConfigurationBranchEffect = (): IUseGetSourceCon isLoading, getSourceControlBranchInfo, isGetBranch, + info, + stepFailedStatus, }; }; diff --git a/frontend/src/hooks/useGetSourceControlConfigurationCrewEffect.tsx b/frontend/src/hooks/useGetSourceControlConfigurationCrewEffect.tsx index bdb243616..3ec2e07f8 100644 --- a/frontend/src/hooks/useGetSourceControlConfigurationCrewEffect.tsx +++ b/frontend/src/hooks/useGetSourceControlConfigurationCrewEffect.tsx @@ -1,27 +1,41 @@ import { DateRange, selectSourceControl, updateSourceControlVerifiedResponse } from '@src/context/config/configSlice'; +import { ISourceControlGetCrewResponseDTO } from '@src/clients/sourceControl/dto/response'; import { selectShouldGetSourceControlConfig } from '@src/context/Metrics/metricsSlice'; import { sourceControlClient } from '@src/clients/sourceControl/SourceControlClient'; -import { FULFILLED, SourceControlTypes } from '@src/constants/resources'; +import { FULFILLED, REJECTED, SourceControlTypes } from '@src/constants/resources'; import { useAppDispatch, useAppSelector } from '@src/hooks/index'; +import { MetricsDataFailStatus } from '@src/constants/commons'; +import { HttpStatusCode } from 'axios'; import { useState } from 'react'; import dayjs from 'dayjs'; export interface IUseGetSourceControlConfigurationCrewInterface { readonly isLoading: boolean; readonly isGetAllCrews: boolean; + readonly info: ISourceControlGetCrewResponseDTO; readonly getSourceControlCrewInfo: ( organization: string, repo: string, branch: string, dateRanges: DateRange[], ) => Promise; + readonly stepFailedStatus: MetricsDataFailStatus; } + export const useGetSourceControlConfigurationCrewEffect = (): IUseGetSourceControlConfigurationCrewInterface => { + const defaultInfoStructure = { + code: 200, + errorTitle: '', + errorMessage: '', + }; + const dispatch = useAppDispatch(); const [isLoading, setIsLoading] = useState(false); const shouldGetSourceControlConfig = useAppSelector(selectShouldGetSourceControlConfig); const [isGetAllCrews, setIsGetAllCrews] = useState(!shouldGetSourceControlConfig); const restoredSourceControlInfo = useAppSelector(selectSourceControl); + const [stepFailedStatus, setStepFailedStatus] = useState(MetricsDataFailStatus.NotFailed); + const [info, setInfo] = useState(defaultInfoStructure); function getEnumKeyByEnumValue(enumValue: string): SourceControlTypes { return Object.entries(SourceControlTypes) @@ -51,45 +65,63 @@ export const useGetSourceControlConfigurationCrewEffect = (): IUseGetSourceContr }), ); + const hasRejected = allCrewsRes.some((crewInfo) => crewInfo.status === REJECTED); + const hasFulfilled = allCrewsRes.some((crewInfo) => crewInfo.status === FULFILLED); + if (!hasRejected) { + setStepFailedStatus(MetricsDataFailStatus.NotFailed); + } else if (hasRejected && hasFulfilled) { + const rejectedStep = allCrewsRes.find((stepInfo) => stepInfo.status === REJECTED); + if ((rejectedStep as PromiseRejectedResult).reason.code == 400) { + setStepFailedStatus(MetricsDataFailStatus.PartialFailed4xx); + } else { + setStepFailedStatus(MetricsDataFailStatus.PartialFailedTimeout); + } + } + allCrewsRes.forEach((response, index) => { if (response.status === FULFILLED) { - const startTime = dayjs(dateRanges[index].startDate).startOf('date').valueOf(); - const endTime = dayjs(dateRanges[index].endDate).startOf('date').valueOf(); - const parents = [ - { - name: 'organization', - value: organization, - }, - { - name: 'repo', - value: repo, - }, - { - name: 'branch', - value: branch, - }, - ]; - const savedTime = `${startTime}-${endTime}`; - dispatch( - updateSourceControlVerifiedResponse({ - parents: parents, - names: [savedTime], - }), - ); - dispatch( - updateSourceControlVerifiedResponse({ - parents: [ - ...parents, - { - name: 'time', - value: savedTime, - }, - ], - names: response.value.data?.crews.map((it) => it), - }), - ); + if (response.value.code !== HttpStatusCode.Ok) { + setInfo(response.value); + } else { + const startTime = dayjs(dateRanges[index].startDate).startOf('date').valueOf(); + const endTime = dayjs(dateRanges[index].endDate).startOf('date').valueOf(); + const parents = [ + { + name: 'organization', + value: organization, + }, + { + name: 'repo', + value: repo, + }, + { + name: 'branch', + value: branch, + }, + ]; + const savedTime = `${startTime}-${endTime}`; + dispatch( + updateSourceControlVerifiedResponse({ + parents: parents, + names: [savedTime], + }), + ); + dispatch( + updateSourceControlVerifiedResponse({ + parents: [ + ...parents, + { + name: 'time', + value: savedTime, + }, + ], + names: response.value.data?.crews.map((it) => it), + }), + ); + } } }); + setIsLoading(false); setIsGetAllCrews(true); }; @@ -98,5 +130,7 @@ export const useGetSourceControlConfigurationCrewEffect = (): IUseGetSourceContr isLoading, getSourceControlCrewInfo, isGetAllCrews, + info, + stepFailedStatus, }; }; diff --git a/frontend/src/hooks/useGetSourceControlConfigurationRepoEffect.tsx b/frontend/src/hooks/useGetSourceControlConfigurationRepoEffect.tsx index 19e77bb7f..092a43c9a 100644 --- a/frontend/src/hooks/useGetSourceControlConfigurationRepoEffect.tsx +++ b/frontend/src/hooks/useGetSourceControlConfigurationRepoEffect.tsx @@ -3,9 +3,12 @@ import { updateSourceControlConfigurationSettingsFirstInto, } from '@src/context/Metrics/metricsSlice'; import { DateRange, selectSourceControl, updateSourceControlVerifiedResponse } from '@src/context/config/configSlice'; +import { ISourceControlGetRepoResponseDTO } from '@src/clients/sourceControl/dto/response'; import { sourceControlClient } from '@src/clients/sourceControl/SourceControlClient'; -import { FULFILLED, SourceControlTypes } from '@src/constants/resources'; +import { FULFILLED, REJECTED, SourceControlTypes } from '@src/constants/resources'; import { useAppDispatch, useAppSelector } from '@src/hooks/index'; +import { MetricsDataFailStatus } from '@src/constants/commons'; +import { HttpStatusCode } from 'axios'; import { useState } from 'react'; import dayjs from 'dayjs'; @@ -13,13 +16,24 @@ export interface IUseGetSourceControlConfigurationRepoInterface { readonly isLoading: boolean; readonly getSourceControlRepoInfo: (value: string, dateRanges: DateRange[], id: number) => Promise; readonly isGetRepo: boolean; + readonly info: ISourceControlGetRepoResponseDTO; + readonly stepFailedStatus: MetricsDataFailStatus; } + export const useGetSourceControlConfigurationRepoEffect = (): IUseGetSourceControlConfigurationRepoInterface => { + const defaultInfoStructure = { + code: 200, + errorTitle: '', + errorMessage: '', + }; + const dispatch = useAppDispatch(); const [isLoading, setIsLoading] = useState(false); const shouldGetSourceControlConfig = useAppSelector(selectShouldGetSourceControlConfig); const [isGetRepo, setIsGetRepo] = useState(!shouldGetSourceControlConfig); + const [info, setInfo] = useState(defaultInfoStructure); const restoredSourceControlInfo = useAppSelector(selectSourceControl); + const [stepFailedStatus, setStepFailedStatus] = useState(MetricsDataFailStatus.NotFailed); function getEnumKeyByEnumValue(enumValue: string): SourceControlTypes { return Object.entries(SourceControlTypes) @@ -40,26 +54,45 @@ export const useGetSourceControlConfigurationRepoEffect = (): IUseGetSourceContr return sourceControlClient.getRepo(params); }), ); + + const hasRejected = allRepoRes.some((repoInfo) => repoInfo.status === REJECTED); + const hasFulfilled = allRepoRes.some((repoInfo) => repoInfo.status === FULFILLED); + + if (!hasRejected) { + setStepFailedStatus(MetricsDataFailStatus.NotFailed); + } else if (hasRejected && hasFulfilled) { + const rejectedStep = allRepoRes.find((repoInfo) => repoInfo.status === REJECTED); + if ((rejectedStep as PromiseRejectedResult).reason.code == 400) { + setStepFailedStatus(MetricsDataFailStatus.PartialFailed4xx); + } else { + setStepFailedStatus(MetricsDataFailStatus.PartialFailedTimeout); + } + } + allRepoRes.forEach((response) => { if (response.status === FULFILLED) { - dispatch( - updateSourceControlVerifiedResponse({ - parents: [ - { - name: 'organization', - value: organization, - }, - ], - names: response.value.data?.name.map((it) => it), - }), - ); - dispatch( - updateSourceControlConfigurationSettingsFirstInto({ - ...response.value.data, - id, - type: 'repo', - }), - ); + if (response.value.code !== HttpStatusCode.Ok) { + setInfo(response.value); + } else { + dispatch( + updateSourceControlVerifiedResponse({ + parents: [ + { + name: 'organization', + value: organization, + }, + ], + names: response.value.data?.name.map((it) => it), + }), + ); + dispatch( + updateSourceControlConfigurationSettingsFirstInto({ + ...response.value.data, + id, + type: 'repo', + }), + ); + } } }); setIsLoading(false); @@ -70,5 +103,7 @@ export const useGetSourceControlConfigurationRepoEffect = (): IUseGetSourceContr isLoading, getSourceControlRepoInfo, isGetRepo, + info, + stepFailedStatus, }; };