diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 54c276c36b11f22d24be21d67c8318e984cbf094..f51f00591f0ccde566fea35edac6645e4be5b054 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -147,6 +147,7 @@ export default { subscribedToVirtualScrollingEvents: false, autoScrolled: false, activeProject: undefined, + hasScannerError: false, }; }, apollo: { @@ -159,6 +160,10 @@ export default { skip() { const codeQualityBoolean = Boolean(this.endpointCodequality); + if (this.hasScannerError) { + return true; + } + return !this.sastReportsInInlineDiff || (!codeQualityBoolean && !this.sastReportAvailable); }, update(data) { @@ -170,13 +175,13 @@ export default { (sastReport?.status === FINDINGS_STATUS_PARSED || !this.sastReportAvailable) && (!codeQualityBoolean || codequalityReportsComparer.status === FINDINGS_STATUS_PARSED) ) { - this.getMRCodequalityAndSecurityReportStopPolling( - this.$apollo.queries.getMRCodequalityAndSecurityReports, - ); + this.$apollo.queries.getMRCodequalityAndSecurityReports.stopPolling(); } if (sastReport?.status === FINDINGS_STATUS_ERROR && this.sastReportAvailable) { this.fetchScannerFindingsError(); + + this.$apollo.queries.getMRCodequalityAndSecurityReports.stopPolling(); } if (codequalityReportsComparer?.report?.newErrors) { @@ -192,6 +197,7 @@ export default { }, error() { this.fetchScannerFindingsError(); + this.$apollo.queries.getMRCodequalityAndSecurityReports.stopPolling(); }, }, }, @@ -432,6 +438,7 @@ export default { this.setDrawer({}); }, fetchScannerFindingsError() { + this.hasScannerError = true; createAlert({ message: __('Something went wrong fetching the Scanner Findings. Please try again.'), }); @@ -445,9 +452,6 @@ export default { diffsEventHub.$on(EVT_MR_PREPARED, this.fetchData); diffsEventHub.$on(EVT_DISCUSSIONS_ASSIGNED, this.handleHash); }, - getMRCodequalityAndSecurityReportStopPolling(query) { - query.stopPolling(); - }, unsubscribeFromEvents() { diffsEventHub.$off(EVT_DISCUSSIONS_ASSIGNED, this.handleHash); diffsEventHub.$off(EVT_MR_PREPARED, this.fetchData); diff --git a/ee/spec/frontend/diffs/components/app_spec.js b/ee/spec/frontend/diffs/components/app_spec.js index 68f4b98c3a47cfd0861ae27f21e907e51fd387d4..932d263af7ec2aa6cca68e67859229d9dc039ced 100644 --- a/ee/spec/frontend/diffs/components/app_spec.js +++ b/ee/spec/frontend/diffs/components/app_spec.js @@ -12,8 +12,10 @@ import store from '~/mr_notes/stores'; import { codeQualityNewErrorsHandler, SASTParsedHandler, - SASTErrorAndParsedHandler, + SASTParsingAndParsedHandler, + SASTErrorHandler, codeQualityErrorAndParsed, + requestError, } from './mocks/queries'; const TEST_ENDPOINT = `${TEST_HOST}/diff/endpoint`; @@ -23,8 +25,6 @@ Vue.use(VueApollo); Vue.config.ignoredElements = ['copy-code']; describe('diffs/components/app', () => { - let wrapper; - let stopPollingSpy; let mockDispatch; let fakeApollo; @@ -68,7 +68,7 @@ describe('diffs/components/app', () => { fakeApollo = createMockApollo([[getMRCodequalityAndSecurityReports, queryHandler]]); - wrapper = shallowMount(App, { + shallowMount(App, { apolloProvider: fakeApollo, provide: { glFeatures: { @@ -125,8 +125,6 @@ describe('diffs/components/app', () => { }); it('stops polling when newErrors in response are defined', async () => { - stopPollingSpy = jest.spyOn(App.methods, 'getMRCodequalityAndSecurityReportStopPolling'); - createComponent( { shouldShow: true, @@ -136,13 +134,12 @@ describe('diffs/components/app', () => { { sastReportsInInlineDiff: true }, ); - const getMRCodequalityAndSecurityReportsQuery = - wrapper.vm.$apollo.queries.getMRCodequalityAndSecurityReports; - jest.spyOn(getMRCodequalityAndSecurityReportsQuery, 'stopPolling'); - await waitForPromises(); - expect(stopPollingSpy).toHaveBeenCalled(); + expect(codeQualityNewErrorsHandler).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(FINDINGS_POLL_INTERVAL); + + expect(codeQualityNewErrorsHandler).toHaveBeenCalledTimes(1); }); it('does not fetch code quality data when endpoint is blank', () => { @@ -177,19 +174,17 @@ describe('diffs/components/app', () => { { shouldShow: true, sastReportAvailable: true }, {}, { sastReportsInInlineDiff: true }, - SASTErrorAndParsedHandler, + SASTParsingAndParsedHandler, ); await waitForPromises(); - expect(SASTErrorAndParsedHandler).toHaveBeenCalledTimes(1); + expect(SASTParsingAndParsedHandler).toHaveBeenCalledTimes(1); jest.advanceTimersByTime(FINDINGS_POLL_INTERVAL); - expect(SASTErrorAndParsedHandler).toHaveBeenCalledTimes(2); + expect(SASTParsingAndParsedHandler).toHaveBeenCalledTimes(2); }); it('stops polling when sastReport status is PARSED', async () => { - stopPollingSpy = jest.spyOn(App.methods, 'getMRCodequalityAndSecurityReportStopPolling'); - createComponent( { shouldShow: true, @@ -200,13 +195,42 @@ describe('diffs/components/app', () => { SASTParsedHandler, ); - const getMRCodequalityAndSecurityReportsQuery = - wrapper.vm.$apollo.queries.getMRCodequalityAndSecurityReports; - jest.spyOn(getMRCodequalityAndSecurityReportsQuery, 'stopPolling'); + await waitForPromises(); + + expect(SASTParsedHandler).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(FINDINGS_POLL_INTERVAL); + + expect(SASTParsedHandler).toHaveBeenCalledTimes(1); + }); + + it('stops polling on request error', async () => { + createComponent( + { shouldShow: true, sastReportAvailable: true }, + {}, + { sastReportsInInlineDiff: true }, + requestError, + ); + await waitForPromises(); + + expect(requestError).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(FINDINGS_POLL_INTERVAL); + + expect(requestError).toHaveBeenCalledTimes(1); + }); + it('stops polling on response status error', async () => { + createComponent( + { shouldShow: true, sastReportAvailable: true }, + {}, + { sastReportsInInlineDiff: true }, + SASTErrorHandler, + ); await waitForPromises(); - expect(stopPollingSpy).toHaveBeenCalled(); + expect(SASTErrorHandler).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(FINDINGS_POLL_INTERVAL); + + expect(SASTErrorHandler).toHaveBeenCalledTimes(1); }); it('does not fetch SAST data when sastReportAvailable is false', () => { diff --git a/ee/spec/frontend/diffs/components/mocks/queries.js b/ee/spec/frontend/diffs/components/mocks/queries.js index a02544f8253a084c8ed87526184c326ac4393aeb..6f90719910554f04079d152b4fd3dca753fc14af 100644 --- a/ee/spec/frontend/diffs/components/mocks/queries.js +++ b/ee/spec/frontend/diffs/components/mocks/queries.js @@ -1,5 +1,8 @@ import { FINDINGS_STATUS_PARSED } from '~/diffs/components/app.vue'; +const mockError = new Error('mockedRequestError'); +export const requestError = jest.fn().mockRejectedValue(mockError); + export const codeQualityErrorAndParsed = jest .fn() .mockResolvedValueOnce({ @@ -85,7 +88,43 @@ export const codeQualityErrorAndParsed = jest }, }); -export const SASTErrorAndParsedHandler = jest +export const SASTErrorHandler = jest.fn().mockResolvedValueOnce({ + data: { + project: { + id: 'gid://gitlab/Project/20', + mergeRequest: { + id: 'gid://gitlab/MergeRequest/123', + title: 'Update file noise.rb', + project: { + id: 'testid', + nameWithNamespace: 'test/name', + fullPath: 'testPath', + }, + hasSecurityReports: false, + codequalityReportsComparer: { + status: 'PARSING', + report: { + status: 'FAILED', + newErrors: [], + resolvedErrors: [], + existingErrors: [], + summary: { + errored: 0, + resolved: 0, + total: 0, + }, + }, + }, + sastReport: { + status: 'ERROR', + report: null, + }, + }, + }, + }, +}); + +export const SASTParsingAndParsedHandler = jest .fn() .mockResolvedValueOnce({ data: { @@ -115,7 +154,7 @@ export const SASTErrorAndParsedHandler = jest }, }, sastReport: { - status: 'ERROR', + status: 'PARSING', report: null, }, },