465 lines
14 KiB
JavaScript
465 lines
14 KiB
JavaScript
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 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;
|
|
|
|
const props = {
|
|
pipelineId: 123,
|
|
projectId: 456,
|
|
securityReportsDocsPath: '/docs',
|
|
discoverProjectSecurityPath: '/discoverProjectSecurityPath',
|
|
};
|
|
|
|
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 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([false, true])(
|
|
'given the coreSecurityMrWidgetCounts feature flag is %p',
|
|
coreSecurityMrWidgetCounts => {
|
|
const createComponentWithFlag = options =>
|
|
createComponent(
|
|
merge(
|
|
{
|
|
provide: {
|
|
glFeatures: {
|
|
coreSecurityMrWidgetCounts,
|
|
},
|
|
},
|
|
},
|
|
options,
|
|
),
|
|
);
|
|
|
|
describe.each(SecurityReportsApp.reportTypes)('given a report type %p', reportType => {
|
|
beforeEach(() => {
|
|
window.mrTabs = { tabShown: jest.fn() };
|
|
setupMockJobArtifact(reportType);
|
|
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 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('');
|
|
});
|
|
});
|
|
|
|
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,
|
|
);
|
|
});
|
|
});
|
|
|
|
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,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
beforeEach(() => {
|
|
mock = new MockAdapter(axios);
|
|
});
|
|
|
|
afterEach(() => {
|
|
mock.restore();
|
|
});
|
|
|
|
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('given coreSecurityMrWidgetDownloads feature flag is enabled', () => {
|
|
const createComponentWithFlagEnabled = options =>
|
|
createComponent(
|
|
merge(options, {
|
|
provide: {
|
|
glFeatures: {
|
|
coreSecurityMrWidgetDownloads: true,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
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);
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
|
|
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 coreSecurityMrWidgetCounts and coreSecurityMrWidgetDownloads feature flags are enabled', () => {
|
|
let mock;
|
|
|
|
beforeEach(() => {
|
|
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();
|
|
});
|
|
|
|
afterEach(() => {
|
|
mock.restore();
|
|
});
|
|
|
|
it('renders the download dropdown', () => {
|
|
expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
});
|