diff options
Diffstat (limited to 'spec/frontend/vue_shared/security_reports/security_reports_app_spec.js')
-rw-r--r-- | spec/frontend/vue_shared/security_reports/security_reports_app_spec.js | 497 |
1 files changed, 400 insertions, 97 deletions
diff --git a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js index ab87d80b291..c440081a0c4 100644 --- a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js +++ b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js @@ -1,162 +1,465 @@ -import { mount } from '@vue/test-utils'; +import { mount, createLocalVue } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import { merge } from 'lodash'; +import VueApollo from 'vue-apollo'; +import Vuex from 'vuex'; +import createMockApollo from 'jest/helpers/mock_apollo_helper'; +import { trimText } from 'helpers/text_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { + expectedDownloadDropdownProps, + securityReportDownloadPathsQueryResponse, + sastDiffSuccessMock, + secretScanningDiffSuccessMock, +} from 'jest/vue_shared/security_reports/mock_data'; import Api from '~/api'; -import Flash from '~/flash'; +import createFlash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; +import { + REPORT_TYPE_SAST, + REPORT_TYPE_SECRET_DETECTION, +} from '~/vue_shared/security_reports/constants'; +import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue'; +import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue'; import SecurityReportsApp from '~/vue_shared/security_reports/security_reports_app.vue'; +import securityReportDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_download_paths.query.graphql'; jest.mock('~/flash'); +const localVue = createLocalVue(); +localVue.use(Vuex); + +const SAST_COMPARISON_PATH = '/sast.json'; +const SECRET_SCANNING_COMPARISON_PATH = '/secret_detection.json'; + describe('Security reports app', () => { let wrapper; - let mrTabsMock; const props = { pipelineId: 123, projectId: 456, securityReportsDocsPath: '/docs', + discoverProjectSecurityPath: '/discoverProjectSecurityPath', }; - const createComponent = () => { - wrapper = mount(SecurityReportsApp, { - propsData: { ...props }, - }); + const createComponent = options => { + wrapper = mount( + SecurityReportsApp, + merge( + { + localVue, + propsData: { ...props }, + stubs: { + HelpIcon: true, + }, + }, + options, + ), + ); + }; + + const pendingHandler = () => new Promise(() => {}); + const successHandler = () => Promise.resolve({ data: securityReportDownloadPathsQueryResponse }); + const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] }); + const createMockApolloProvider = handler => { + localVue.use(VueApollo); + + const requestHandlers = [[securityReportDownloadPathsQuery, handler]]; + + return createMockApollo(requestHandlers); }; const anyParams = expect.any(Object); + const findDownloadDropdown = () => wrapper.find(SecurityReportDownloadDropdown); const findPipelinesTabAnchor = () => wrapper.find('[data-testid="show-pipelines"]'); - const findHelpLink = () => wrapper.find('[data-testid="help"]'); - const setupMrTabsMock = () => { - mrTabsMock = { tabShown: jest.fn() }; - window.mrTabs = mrTabsMock; - }; + const findHelpIconComponent = () => wrapper.find(HelpIcon); const setupMockJobArtifact = reportType => { jest .spyOn(Api, 'pipelineJobs') .mockResolvedValue({ data: [{ artifacts: [{ file_type: reportType }] }] }); }; + const expectPipelinesTabAnchor = () => { + const mrTabsMock = { tabShown: jest.fn() }; + window.mrTabs = mrTabsMock; + findPipelinesTabAnchor().trigger('click'); + expect(mrTabsMock.tabShown.mock.calls).toEqual([['pipelines']]); + }; afterEach(() => { wrapper.destroy(); delete window.mrTabs; }); - describe.each(SecurityReportsApp.reportTypes)('given a report type %p', reportType => { - beforeEach(() => { - window.mrTabs = { tabShown: jest.fn() }; - setupMockJobArtifact(reportType); - createComponent(); - return wrapper.vm.$nextTick(); - }); + describe.each([false, true])( + 'given the coreSecurityMrWidgetCounts feature flag is %p', + coreSecurityMrWidgetCounts => { + const createComponentWithFlag = options => + createComponent( + merge( + { + provide: { + glFeatures: { + coreSecurityMrWidgetCounts, + }, + }, + }, + options, + ), + ); - it('calls the pipelineJobs API correctly', () => { - expect(Api.pipelineJobs).toHaveBeenCalledTimes(1); - expect(Api.pipelineJobs).toHaveBeenCalledWith(props.projectId, props.pipelineId, anyParams); - }); + describe.each(SecurityReportsApp.reportTypes)('given a report type %p', reportType => { + beforeEach(() => { + window.mrTabs = { tabShown: jest.fn() }; + setupMockJobArtifact(reportType); + createComponentWithFlag(); + return wrapper.vm.$nextTick(); + }); - it('renders the expected message', () => { - expect(wrapper.text()).toMatchInterpolatedText(SecurityReportsApp.i18n.scansHaveRun); - }); + it('calls the pipelineJobs API correctly', () => { + expect(Api.pipelineJobs).toHaveBeenCalledTimes(1); + expect(Api.pipelineJobs).toHaveBeenCalledWith( + props.projectId, + props.pipelineId, + anyParams, + ); + }); - describe('clicking the anchor to the pipelines tab', () => { - beforeEach(() => { - setupMrTabsMock(); - findPipelinesTabAnchor().trigger('click'); + it('renders the expected message', () => { + expect(wrapper.text()).toMatchInterpolatedText( + SecurityReportsApp.i18n.scansHaveRunWithDownloadGuidance, + ); + }); + + describe('clicking the anchor to the pipelines tab', () => { + it('calls the mrTabs.tabShown global', () => { + expectPipelinesTabAnchor(); + }); + }); + + it('renders a help link', () => { + expect(findHelpIconComponent().props()).toEqual({ + helpPath: props.securityReportsDocsPath, + discoverProjectSecurityPath: props.discoverProjectSecurityPath, + }); + }); + }); + + describe('given a report type "foo"', () => { + beforeEach(() => { + setupMockJobArtifact('foo'); + createComponentWithFlag(); + return wrapper.vm.$nextTick(); + }); + + it('calls the pipelineJobs API correctly', () => { + expect(Api.pipelineJobs).toHaveBeenCalledTimes(1); + expect(Api.pipelineJobs).toHaveBeenCalledWith( + props.projectId, + props.pipelineId, + anyParams, + ); + }); + + it('renders nothing', () => { + expect(wrapper.html()).toBe(''); + }); }); - it('calls the mrTabs.tabShown global', () => { - expect(mrTabsMock.tabShown.mock.calls).toEqual([['pipelines']]); + describe('security artifacts on last page of multi-page response', () => { + const numPages = 3; + + beforeEach(() => { + jest + .spyOn(Api, 'pipelineJobs') + .mockImplementation(async (projectId, pipelineId, { page }) => { + const requestedPage = parseInt(page, 10); + if (requestedPage < numPages) { + return { + // Some jobs with no relevant artifacts + data: [{}, {}], + headers: { 'x-next-page': String(requestedPage + 1) }, + }; + } else if (requestedPage === numPages) { + return { + data: [{ artifacts: [{ file_type: SecurityReportsApp.reportTypes[0] }] }], + }; + } + + throw new Error('Test failed due to request of non-existent jobs page'); + }); + + createComponentWithFlag(); + return wrapper.vm.$nextTick(); + }); + + it('fetches all pages', () => { + expect(Api.pipelineJobs).toHaveBeenCalledTimes(numPages); + }); + + it('renders the expected message', () => { + expect(wrapper.text()).toMatchInterpolatedText( + SecurityReportsApp.i18n.scansHaveRunWithDownloadGuidance, + ); + }); }); - }); - it('renders a help link', () => { - expect(findHelpLink().attributes()).toMatchObject({ - href: props.securityReportsDocsPath, + describe('given an error from the API', () => { + let error; + + beforeEach(() => { + error = new Error('an error'); + jest.spyOn(Api, 'pipelineJobs').mockRejectedValue(error); + createComponentWithFlag(); + return wrapper.vm.$nextTick(); + }); + + it('calls the pipelineJobs API correctly', () => { + expect(Api.pipelineJobs).toHaveBeenCalledTimes(1); + expect(Api.pipelineJobs).toHaveBeenCalledWith( + props.projectId, + props.pipelineId, + anyParams, + ); + }); + + it('renders nothing', () => { + expect(wrapper.html()).toBe(''); + }); + + it('calls createFlash correctly', () => { + expect(createFlash.mock.calls).toEqual([ + [ + { + message: SecurityReportsApp.i18n.apiError, + captureError: true, + error, + }, + ], + ]); + }); }); - }); - }); + }, + ); + + describe('given the coreSecurityMrWidgetCounts feature flag is enabled', () => { + let mock; + + const createComponentWithFlagEnabled = options => + createComponent( + merge(options, { + provide: { + glFeatures: { + coreSecurityMrWidgetCounts: true, + }, + }, + }), + ); - describe('given a report type "foo"', () => { beforeEach(() => { - setupMockJobArtifact('foo'); - createComponent(); - return wrapper.vm.$nextTick(); + mock = new MockAdapter(axios); }); - it('calls the pipelineJobs API correctly', () => { - expect(Api.pipelineJobs).toHaveBeenCalledTimes(1); - expect(Api.pipelineJobs).toHaveBeenCalledWith(props.projectId, props.pipelineId, anyParams); + afterEach(() => { + mock.restore(); }); - it('renders nothing', () => { - expect(wrapper.html()).toBe(''); - }); + const SAST_SUCCESS_MESSAGE = + 'Security scanning detected 1 potential vulnerability 1 Critical 0 High and 0 Others'; + const SECRET_SCANNING_SUCCESS_MESSAGE = + 'Security scanning detected 2 potential vulnerabilities 1 Critical 1 High and 0 Others'; + describe.each` + reportType | pathProp | path | successResponse | successMessage + ${REPORT_TYPE_SAST} | ${'sastComparisonPath'} | ${SAST_COMPARISON_PATH} | ${sastDiffSuccessMock} | ${SAST_SUCCESS_MESSAGE} + ${REPORT_TYPE_SECRET_DETECTION} | ${'secretScanningComparisonPath'} | ${SECRET_SCANNING_COMPARISON_PATH} | ${secretScanningDiffSuccessMock} | ${SECRET_SCANNING_SUCCESS_MESSAGE} + `( + 'given a $pathProp and $reportType artifact', + ({ reportType, pathProp, path, successResponse, successMessage }) => { + beforeEach(() => { + setupMockJobArtifact(reportType); + }); + + describe('when loading', () => { + beforeEach(() => { + mock = new MockAdapter(axios, { delayResponse: 1 }); + mock.onGet(path).replyOnce(200, successResponse); + + createComponentWithFlagEnabled({ + propsData: { + [pathProp]: path, + }, + }); + + return waitForPromises(); + }); + + it('should have loading message', () => { + expect(wrapper.text()).toBe('Security scanning is loading'); + }); + + it('should not render the pipeline tab anchor', () => { + expect(findPipelinesTabAnchor().exists()).toBe(false); + }); + }); + + describe('when successfully loaded', () => { + beforeEach(() => { + mock.onGet(path).replyOnce(200, successResponse); + + createComponentWithFlagEnabled({ + propsData: { + [pathProp]: path, + }, + }); + + return waitForPromises(); + }); + + it('should show counts', () => { + expect(trimText(wrapper.text())).toContain(successMessage); + }); + + it('should render the pipeline tab anchor', () => { + expectPipelinesTabAnchor(); + }); + }); + + describe('when an error occurs', () => { + beforeEach(() => { + mock.onGet(path).replyOnce(500); + + createComponentWithFlagEnabled({ + propsData: { + [pathProp]: path, + }, + }); + + return waitForPromises(); + }); + + it('should show error message', () => { + expect(trimText(wrapper.text())).toContain('Loading resulted in an error'); + }); + + it('should render the pipeline tab anchor', () => { + expectPipelinesTabAnchor(); + }); + }); + }, + ); }); - describe('security artifacts on last page of multi-page response', () => { - const numPages = 3; + describe('given coreSecurityMrWidgetDownloads feature flag is enabled', () => { + const createComponentWithFlagEnabled = options => + createComponent( + merge(options, { + provide: { + glFeatures: { + coreSecurityMrWidgetDownloads: true, + }, + }, + }), + ); - beforeEach(() => { - jest - .spyOn(Api, 'pipelineJobs') - .mockImplementation(async (projectId, pipelineId, { page }) => { - const requestedPage = parseInt(page, 10); - if (requestedPage < numPages) { - return { - // Some jobs with no relevant artifacts - data: [{}, {}], - headers: { 'x-next-page': String(requestedPage + 1) }, - }; - } else if (requestedPage === numPages) { - return { - data: [{ artifacts: [{ file_type: SecurityReportsApp.reportTypes[0] }] }], - }; - } - - throw new Error('Test failed due to request of non-existent jobs page'); - }); - - createComponent(); - return wrapper.vm.$nextTick(); + describe('given the query is loading', () => { + beforeEach(() => { + createComponentWithFlagEnabled({ + apolloProvider: createMockApolloProvider(pendingHandler), + }); + }); + + // TODO: Remove this assertion as part of + // https://gitlab.com/gitlab-org/gitlab/-/issues/273431 + it('initially renders nothing', () => { + expect(wrapper.isEmpty()).toBe(true); + }); }); - it('fetches all pages', () => { - expect(Api.pipelineJobs).toHaveBeenCalledTimes(numPages); + describe('given the query loads successfully', () => { + beforeEach(() => { + createComponentWithFlagEnabled({ + apolloProvider: createMockApolloProvider(successHandler), + }); + }); + + it('renders the download dropdown', () => { + expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps); + }); + + it('renders the expected message', () => { + const text = wrapper.text(); + expect(text).not.toContain(SecurityReportsApp.i18n.scansHaveRunWithDownloadGuidance); + expect(text).toContain(SecurityReportsApp.i18n.scansHaveRun); + }); + + it('should not render the pipeline tab anchor', () => { + expect(findPipelinesTabAnchor().exists()).toBe(false); + }); }); - it('renders the expected message', () => { - expect(wrapper.text()).toMatchInterpolatedText(SecurityReportsApp.i18n.scansHaveRun); + describe('given the query fails', () => { + beforeEach(() => { + createComponentWithFlagEnabled({ + apolloProvider: createMockApolloProvider(failureHandler), + }); + }); + + it('calls createFlash correctly', () => { + expect(createFlash).toHaveBeenCalledWith({ + message: SecurityReportsApp.i18n.apiError, + captureError: true, + error: expect.any(Error), + }); + }); + + // TODO: Remove this assertion as part of + // https://gitlab.com/gitlab-org/gitlab/-/issues/273431 + it('renders nothing', () => { + expect(wrapper.isEmpty()).toBe(true); + }); }); }); - describe('given an error from the API', () => { - let error; + describe('given coreSecurityMrWidgetCounts and coreSecurityMrWidgetDownloads feature flags are enabled', () => { + let mock; beforeEach(() => { - error = new Error('an error'); - jest.spyOn(Api, 'pipelineJobs').mockRejectedValue(error); - createComponent(); - return wrapper.vm.$nextTick(); + mock = new MockAdapter(axios); + mock.onGet(SAST_COMPARISON_PATH).replyOnce(200, sastDiffSuccessMock); + mock.onGet(SECRET_SCANNING_COMPARISON_PATH).replyOnce(200, secretScanningDiffSuccessMock); + createComponent({ + propsData: { + sastComparisonPath: SAST_COMPARISON_PATH, + secretScanningComparisonPath: SECRET_SCANNING_COMPARISON_PATH, + }, + provide: { + glFeatures: { + coreSecurityMrWidgetCounts: true, + coreSecurityMrWidgetDownloads: true, + }, + }, + apolloProvider: createMockApolloProvider(successHandler), + }); + + return waitForPromises(); }); - it('calls the pipelineJobs API correctly', () => { - expect(Api.pipelineJobs).toHaveBeenCalledTimes(1); - expect(Api.pipelineJobs).toHaveBeenCalledWith(props.projectId, props.pipelineId, anyParams); + afterEach(() => { + mock.restore(); }); - it('renders nothing', () => { - expect(wrapper.html()).toBe(''); + it('renders the download dropdown', () => { + expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps); }); - it('calls Flash correctly', () => { - expect(Flash.mock.calls).toEqual([ - [ - { - message: SecurityReportsApp.i18n.apiError, - captureError: true, - error, - }, - ], - ]); + it('renders the expected counts message', () => { + expect(trimText(wrapper.text())).toContain( + 'Security scanning detected 3 potential vulnerabilities 2 Critical 1 High and 0 Others', + ); + }); + + it('should not render the pipeline tab anchor', () => { + expect(findPipelinesTabAnchor().exists()).toBe(false); }); }); }); |