Commit becf081f authored by Miguel Rincon's avatar Miguel Rincon

Merge branch '292266-feature-flag-remove-core_security_mr_widget_downloads' into 'master'

Remove core_security_mr_widget_downloads feature flag [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!51639
parents 283dc43d 1642b1a5
<script> <script>
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import { GlLink, GlSprintf } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReportSection from '~/reports/components/report_section.vue'; import ReportSection from '~/reports/components/report_section.vue';
import { LOADING, ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/reports/constants'; import { ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/reports/constants';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import Api from '~/api';
import HelpIcon from './components/help_icon.vue'; import HelpIcon from './components/help_icon.vue';
import SecurityReportDownloadDropdown from './components/security_report_download_dropdown.vue'; import SecurityReportDownloadDropdown from './components/security_report_download_dropdown.vue';
import SecuritySummary from './components/security_summary.vue'; import SecuritySummary from './components/security_summary.vue';
...@@ -24,8 +21,6 @@ import { extractSecurityReportArtifacts } from './utils'; ...@@ -24,8 +21,6 @@ import { extractSecurityReportArtifacts } from './utils';
export default { export default {
store, store,
components: { components: {
GlLink,
GlSprintf,
ReportSection, ReportSection,
HelpIcon, HelpIcon,
SecurityReportDownloadDropdown, SecurityReportDownloadDropdown,
...@@ -101,9 +96,6 @@ export default { ...@@ -101,9 +96,6 @@ export default {
), ),
}; };
}, },
skip() {
return !this.canShowDownloads;
},
update(data) { update(data) {
return extractSecurityReportArtifacts(this.$options.reportTypes, data); return extractSecurityReportArtifacts(this.$options.reportTypes, data);
}, },
...@@ -124,9 +116,6 @@ export default { ...@@ -124,9 +116,6 @@ export default {
}, },
computed: { computed: {
...mapGetters(['groupedSummaryText', 'summaryStatus']), ...mapGetters(['groupedSummaryText', 'summaryStatus']),
canShowDownloads() {
return this.glFeatures.coreSecurityMrWidgetDownloads;
},
hasSecurityReports() { hasSecurityReports() {
return this.availableSecurityReports.length > 0; return this.availableSecurityReports.length > 0;
}, },
...@@ -139,23 +128,6 @@ export default { ...@@ -139,23 +128,6 @@ export default {
isLoadingReportArtifacts() { isLoadingReportArtifacts() {
return this.$apollo.queries.reportArtifacts.loading; return this.$apollo.queries.reportArtifacts.loading;
}, },
shouldShowDownloadGuidance() {
return !this.canShowDownloads && this.summaryStatus !== LOADING;
},
scansHaveRunMessage() {
return this.canShowDownloads
? this.$options.i18n.scansHaveRun
: this.$options.i18n.scansHaveRunWithDownloadGuidance;
},
},
created() {
if (!this.canShowDownloads) {
this.checkAvailableSecurityReports(this.$options.reportTypes)
.then((availableSecurityReports) => {
this.onCheckingAvailableSecurityReports(Array.from(availableSecurityReports));
})
.catch(this.showError);
}
}, },
methods: { methods: {
...mapActions(MODULE_SAST, { ...mapActions(MODULE_SAST, {
...@@ -166,36 +138,6 @@ export default { ...@@ -166,36 +138,6 @@ export default {
setSecretDetectionDiffEndpoint: 'setDiffEndpoint', setSecretDetectionDiffEndpoint: 'setDiffEndpoint',
fetchSecretDetectionDiff: 'fetchDiff', fetchSecretDetectionDiff: 'fetchDiff',
}), }),
async checkAvailableSecurityReports(reportTypes) {
const reportTypesSet = new Set(reportTypes);
const availableReportTypes = new Set();
let page = 1;
while (page) {
// eslint-disable-next-line no-await-in-loop
const { data: jobs, headers } = await Api.pipelineJobs(this.projectId, this.pipelineId, {
per_page: 100,
page,
});
jobs.forEach(({ artifacts = [] }) => {
artifacts.forEach(({ file_type }) => {
if (reportTypesSet.has(file_type)) {
availableReportTypes.add(file_type);
}
});
});
// If we've found artifacts for all the report types, stop looking!
if (availableReportTypes.size === reportTypesSet.size) {
return availableReportTypes;
}
page = parseIntPagination(normalizeHeaders(headers)).nextPage;
}
return availableReportTypes;
},
fetchCounts() { fetchCounts() {
if (!this.glFeatures.coreSecurityMrWidgetCounts) { if (!this.glFeatures.coreSecurityMrWidgetCounts) {
return; return;
...@@ -213,11 +155,6 @@ export default { ...@@ -213,11 +155,6 @@ export default {
this.canShowCounts = true; this.canShowCounts = true;
} }
}, },
activatePipelinesTab() {
if (window.mrTabs) {
window.mrTabs.tabShown('pipelines');
}
},
onCheckingAvailableSecurityReports(availableSecurityReports) { onCheckingAvailableSecurityReports(availableSecurityReports) {
this.availableSecurityReports = availableSecurityReports; this.availableSecurityReports = availableSecurityReports;
this.fetchCounts(); this.fetchCounts();
...@@ -236,12 +173,6 @@ export default { ...@@ -236,12 +173,6 @@ export default {
'SecurityReports|Failed to get security report information. Please reload the page or try again later.', 'SecurityReports|Failed to get security report information. Please reload the page or try again later.',
), ),
scansHaveRun: s__('SecurityReports|Security scans have run'), scansHaveRun: s__('SecurityReports|Security scans have run'),
scansHaveRunWithDownloadGuidance: s__(
'SecurityReports|Security scans have run. Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports',
),
downloadFromPipelineTab: s__(
'SecurityReports|Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports',
),
}, },
summarySlots: [SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR], summarySlots: [SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR],
}; };
...@@ -265,22 +196,7 @@ export default { ...@@ -265,22 +196,7 @@ export default {
</span> </span>
</template> </template>
<template v-if="shouldShowDownloadGuidance" #sub-heading> <template #action-buttons>
<span class="gl-font-sm">
<gl-sprintf :message="$options.i18n.downloadFromPipelineTab">
<template #link="{ content }">
<gl-link
class="gl-font-sm"
data-testid="show-pipelines"
@click="activatePipelinesTab"
>{{ content }}</gl-link
>
</template>
</gl-sprintf>
</span>
</template>
<template v-if="canShowDownloads" #action-buttons>
<security-report-download-dropdown <security-report-download-dropdown
:artifacts="reportArtifacts" :artifacts="reportArtifacts"
:loading="isLoadingReportArtifacts" :loading="isLoadingReportArtifacts"
...@@ -298,13 +214,7 @@ export default { ...@@ -298,13 +214,7 @@ export default {
data-testid="security-mr-widget" data-testid="security-mr-widget"
> >
<template #error> <template #error>
<gl-sprintf :message="scansHaveRunMessage"> {{ $options.i18n.scansHaveRun }}
<template #link="{ content }">
<gl-link data-testid="show-pipelines" @click="activatePipelinesTab">{{
content
}}</gl-link>
</template>
</gl-sprintf>
<help-icon <help-icon
:help-path="securityReportsDocsPath" :help-path="securityReportsDocsPath"
...@@ -312,7 +222,7 @@ export default { ...@@ -312,7 +222,7 @@ export default {
/> />
</template> </template>
<template v-if="canShowDownloads" #action-buttons> <template #action-buttons>
<security-report-download-dropdown <security-report-download-dropdown
:artifacts="reportArtifacts" :artifacts="reportArtifacts"
:loading="isLoadingReportArtifacts" :loading="isLoadingReportArtifacts"
......
...@@ -38,7 +38,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -38,7 +38,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:default_merge_ref_for_diffs, @project) push_frontend_feature_flag(:default_merge_ref_for_diffs, @project)
push_frontend_feature_flag(:core_security_mr_widget, @project, default_enabled: true) push_frontend_feature_flag(:core_security_mr_widget, @project, default_enabled: true)
push_frontend_feature_flag(:core_security_mr_widget_counts, @project) push_frontend_feature_flag(:core_security_mr_widget_counts, @project)
push_frontend_feature_flag(:core_security_mr_widget_downloads, @project, default_enabled: true)
push_frontend_feature_flag(:remove_resolve_note, @project, default_enabled: true) push_frontend_feature_flag(:remove_resolve_note, @project, default_enabled: true)
push_frontend_feature_flag(:diffs_gradual_load, @project, default_enabled: true) push_frontend_feature_flag(:diffs_gradual_load, @project, default_enabled: true)
push_frontend_feature_flag(:codequality_mr_diff, @project) push_frontend_feature_flag(:codequality_mr_diff, @project)
......
---
name: core_security_mr_widget_downloads
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48769
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/273418
milestone: '13.7'
type: development
group: group::static analysis
default_enabled: true
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import MrWidgetOptions from 'ee/vue_merge_request_widget/mr_widget_options.vue'; import MrWidgetOptions from 'ee/vue_merge_request_widget/mr_widget_options.vue';
import { import {
sastDiffSuccessMock, sastDiffSuccessMock,
...@@ -11,6 +12,8 @@ import { ...@@ -11,6 +12,8 @@ import {
coverageFuzzingDiffSuccessMock, coverageFuzzingDiffSuccessMock,
apiFuzzingDiffSuccessMock, apiFuzzingDiffSuccessMock,
} from 'ee_jest/vue_shared/security_reports/mock_data'; } from 'ee_jest/vue_shared/security_reports/mock_data';
import { securityReportDownloadPathsQueryResponse } from 'jest/vue_shared/security_reports/mock_data';
import createMockApollo from 'helpers/mock_apollo_helper';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
...@@ -18,12 +21,12 @@ import waitForPromises from 'helpers/wait_for_promises'; ...@@ -18,12 +21,12 @@ import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants'; import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
import securityReportDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_download_paths.query.graphql';
import mockData, { import mockData, {
baseBrowserPerformance, baseBrowserPerformance,
headBrowserPerformance, headBrowserPerformance,
baseLoadPerformance, baseLoadPerformance,
headLoadPerformance, headLoadPerformance,
pipelineJobs,
} from './mock_data'; } from './mock_data';
// Force Jest to transpile and cache // Force Jest to transpile and cache
...@@ -32,6 +35,8 @@ import _GroupedSecurityReportsApp from 'ee/vue_shared/security_reports/grouped_s ...@@ -32,6 +35,8 @@ import _GroupedSecurityReportsApp from 'ee/vue_shared/security_reports/grouped_s
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
import _Deployment from '~/vue_merge_request_widget/components/deployment/deployment.vue'; import _Deployment from '~/vue_merge_request_widget/components/deployment/deployment.vue';
Vue.use(VueApollo);
const SAST_SELECTOR = '.js-sast-widget'; const SAST_SELECTOR = '.js-sast-widget';
const DAST_SELECTOR = '.js-dast-widget'; const DAST_SELECTOR = '.js-dast-widget';
const DEPENDENCY_SCANNING_SELECTOR = '.js-dependency-scanning-widget'; const DEPENDENCY_SCANNING_SELECTOR = '.js-dependency-scanning-widget';
...@@ -999,8 +1004,6 @@ describe('ee merge request widget options', () => { ...@@ -999,8 +1004,6 @@ describe('ee merge request widget options', () => {
}); });
describe('CE security report', () => { describe('CE security report', () => {
const PIPELINE_JOBS_ENDPOINT = `/api/undefined/projects/${mockData.target_project_id}/pipelines/${mockData.pipeline.id}/jobs`;
describe.each` describe.each`
context | canReadVulnerabilities | hasPipeline | featureFlag | shouldRender context | canReadVulnerabilities | hasPipeline | featureFlag | shouldRender
${'user cannot read vulnerabilities'} | ${false} | ${true} | ${true} | ${true} ${'user cannot read vulnerabilities'} | ${false} | ${true} | ${true} | ${true}
...@@ -1017,8 +1020,15 @@ describe('ee merge request widget options', () => { ...@@ -1017,8 +1020,15 @@ describe('ee merge request widget options', () => {
gon.features = { coreSecurityMrWidget: featureFlag }; gon.features = { coreSecurityMrWidget: featureFlag };
mock.onGet(PIPELINE_JOBS_ENDPOINT).replyOnce(200, pipelineJobs); createComponent({
createComponent({ propsData: { mrData: gl.mrWidgetData } }); propsData: { mrData: gl.mrWidgetData },
apolloProvider: createMockApollo([
[
securityReportDownloadPathsQuery,
async () => ({ data: securityReportDownloadPathsQueryResponse }),
],
]),
});
return waitForPromises(); return waitForPromises();
}); });
......
...@@ -133,14 +133,3 @@ export const codequalityParsedIssues = [ ...@@ -133,14 +133,3 @@ export const codequalityParsedIssues = [
]; ];
export { mockStore }; export { mockStore };
// TODO: Remove as part of https://gitlab.com/gitlab-org/gitlab/-/issues/249544
export const pipelineJobs = [
{
artifacts: [
{
file_type: 'sast',
},
],
},
];
...@@ -25174,9 +25174,6 @@ msgstr "" ...@@ -25174,9 +25174,6 @@ msgstr ""
msgid "SecurityReports|Fuzzing artifacts" msgid "SecurityReports|Fuzzing artifacts"
msgstr "" msgstr ""
msgid "SecurityReports|Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports"
msgstr ""
msgid "SecurityReports|Hide dismissed" msgid "SecurityReports|Hide dismissed"
msgstr "" msgstr ""
...@@ -25240,9 +25237,6 @@ msgstr "" ...@@ -25240,9 +25237,6 @@ msgstr ""
msgid "SecurityReports|Security scans have run" msgid "SecurityReports|Security scans have run"
msgstr "" msgstr ""
msgid "SecurityReports|Security scans have run. Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports"
msgstr ""
msgid "SecurityReports|Select a project to add by using the project search field above." msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr "" msgstr ""
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import Api from '~/api'; import createMockApollo from 'helpers/mock_apollo_helper';
import { securityReportDownloadPathsQueryResponse } from 'jest/vue_shared/security_reports/mock_data';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue'; import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import eventHub from '~/vue_merge_request_widget/event_hub'; import eventHub from '~/vue_merge_request_widget/event_hub';
...@@ -12,11 +14,14 @@ import { stateKey } from '~/vue_merge_request_widget/stores/state_maps'; ...@@ -12,11 +14,14 @@ import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import mockData from './mock_data'; import mockData from './mock_data';
import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data'; import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data';
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants'; import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
import securityReportDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_download_paths.query.graphql';
jest.mock('~/smart_interval'); jest.mock('~/smart_interval');
jest.mock('~/lib/utils/favicon'); jest.mock('~/lib/utils/favicon');
Vue.use(VueApollo);
describe('MrWidgetOptions', () => { describe('MrWidgetOptions', () => {
let wrapper; let wrapper;
let mock; let mock;
...@@ -41,7 +46,7 @@ describe('MrWidgetOptions', () => { ...@@ -41,7 +46,7 @@ describe('MrWidgetOptions', () => {
gon.features = {}; gon.features = {};
}); });
const createComponent = (mrData = mockData) => { const createComponent = (mrData = mockData, options = {}) => {
if (wrapper) { if (wrapper) {
wrapper.destroy(); wrapper.destroy();
} }
...@@ -50,6 +55,7 @@ describe('MrWidgetOptions', () => { ...@@ -50,6 +55,7 @@ describe('MrWidgetOptions', () => {
propsData: { propsData: {
mrData: { ...mrData }, mrData: { ...mrData },
}, },
...options,
}); });
return axios.waitForAll(); return axios.waitForAll();
...@@ -815,36 +821,37 @@ describe('MrWidgetOptions', () => { ...@@ -815,36 +821,37 @@ describe('MrWidgetOptions', () => {
describe('security widget', () => { describe('security widget', () => {
describe.each` describe.each`
context | hasPipeline | reportType | isFlagEnabled | shouldRender context | hasPipeline | isFlagEnabled | shouldRender
${'security report and flag enabled'} | ${true} | ${'sast'} | ${true} | ${true} ${'has pipeline and flag enabled'} | ${true} | ${true} | ${true}
${'security report and flag disabled'} | ${true} | ${'sast'} | ${false} | ${false} ${'has pipeline and flag disabled'} | ${true} | ${false} | ${false}
${'no security report and flag enabled'} | ${true} | ${'foo'} | ${true} | ${false} ${'no pipeline and flag enabled'} | ${false} | ${true} | ${false}
${'no pipeline and flag enabled'} | ${false} | ${'sast'} | ${true} | ${false} `('given $context', ({ hasPipeline, isFlagEnabled, shouldRender }) => {
`('given $context', ({ hasPipeline, reportType, isFlagEnabled, shouldRender }) => {
beforeEach(() => { beforeEach(() => {
gon.features.coreSecurityMrWidget = isFlagEnabled; gon.features.coreSecurityMrWidget = isFlagEnabled;
if (hasPipeline) { const mrData = {
jest.spyOn(Api, 'pipelineJobs').mockResolvedValue({
data: [{ artifacts: [{ file_type: reportType }] }],
});
}
return createComponent({
...mockData, ...mockData,
...(hasPipeline ? {} : { pipeline: undefined }), ...(hasPipeline ? {} : { pipeline: null }),
};
// Override top-level mocked requests, which always use a fresh copy of
// mockData, which always includes the full pipeline object.
mock.onGet(mockData.merge_request_widget_path).reply(() => [200, mrData]);
mock.onGet(mockData.merge_request_cached_widget_path).reply(() => [200, mrData]);
return createComponent(mrData, {
apolloProvider: createMockApollo([
[
securityReportDownloadPathsQuery,
async () => ({ data: securityReportDownloadPathsQueryResponse }),
],
]),
}); });
}); });
if (shouldRender) { it(shouldRender ? 'renders' : 'does not render', () => {
it('renders', () => { expect(findSecurityMrWidget().exists()).toBe(shouldRender);
expect(findSecurityMrWidget().exists()).toBe(true); });
});
} else {
it('does not render', () => {
expect(findSecurityMrWidget().exists()).toBe(false);
});
}
}); });
}); });
......
...@@ -322,6 +322,23 @@ export const secretScanningDiffSuccessMock = { ...@@ -322,6 +322,23 @@ export const secretScanningDiffSuccessMock = {
head_report_created_at: '2020-01-10T10:00:00.000Z', head_report_created_at: '2020-01-10T10:00:00.000Z',
}; };
export const securityReportDownloadPathsQueryNoArtifactsResponse = {
project: {
mergeRequest: {
headPipeline: {
id: 'gid://gitlab/Ci::Pipeline/176',
jobs: {
nodes: [],
__typename: 'CiJobConnection',
},
__typename: 'Pipeline',
},
__typename: 'MergeRequest',
},
__typename: 'Project',
},
};
export const securityReportDownloadPathsQueryResponse = { export const securityReportDownloadPathsQueryResponse = {
project: { project: {
mergeRequest: { mergeRequest: {
......
...@@ -8,11 +8,11 @@ import { trimText } from 'helpers/text_helper'; ...@@ -8,11 +8,11 @@ import { trimText } from 'helpers/text_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { import {
expectedDownloadDropdownProps, expectedDownloadDropdownProps,
securityReportDownloadPathsQueryNoArtifactsResponse,
securityReportDownloadPathsQueryResponse, securityReportDownloadPathsQueryResponse,
sastDiffSuccessMock, sastDiffSuccessMock,
secretScanningDiffSuccessMock, secretScanningDiffSuccessMock,
} from 'jest/vue_shared/security_reports/mock_data'; } from 'jest/vue_shared/security_reports/mock_data';
import Api from '~/api';
import createFlash from '~/flash'; import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { import {
...@@ -60,6 +60,8 @@ describe('Security reports app', () => { ...@@ -60,6 +60,8 @@ describe('Security reports app', () => {
const pendingHandler = () => new Promise(() => {}); const pendingHandler = () => new Promise(() => {});
const successHandler = () => Promise.resolve({ data: securityReportDownloadPathsQueryResponse }); const successHandler = () => Promise.resolve({ data: securityReportDownloadPathsQueryResponse });
const successEmptyHandler = () =>
Promise.resolve({ data: securityReportDownloadPathsQueryNoArtifactsResponse });
const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] }); const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] });
const createMockApolloProvider = (handler) => { const createMockApolloProvider = (handler) => {
localVue.use(VueApollo); localVue.use(VueApollo);
...@@ -69,178 +71,85 @@ describe('Security reports app', () => { ...@@ -69,178 +71,85 @@ describe('Security reports app', () => {
return createMockApollo(requestHandlers); return createMockApollo(requestHandlers);
}; };
const anyParams = expect.any(Object);
const findDownloadDropdown = () => wrapper.find(SecurityReportDownloadDropdown); const findDownloadDropdown = () => wrapper.find(SecurityReportDownloadDropdown);
const findPipelinesTabAnchor = () => wrapper.find('[data-testid="show-pipelines"]');
const findHelpIconComponent = () => wrapper.find(HelpIcon); 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(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
delete window.mrTabs;
}); });
describe.each([false, true])( describe('given the artifacts query is loading', () => {
'given the coreSecurityMrWidgetCounts feature flag is %p', beforeEach(() => {
(coreSecurityMrWidgetCounts) => { createComponent({
const createComponentWithFlag = (options) => apolloProvider: createMockApolloProvider(pendingHandler),
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"', () => { // TODO: Remove this assertion as part of
beforeEach(() => { // https://gitlab.com/gitlab-org/gitlab/-/issues/273431
setupMockJobArtifact('foo'); it('initially renders nothing', () => {
createComponentWithFlag(); expect(wrapper.html()).toBe('');
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', () => { describe('given the artifacts query loads successfully', () => {
expect(wrapper.html()).toBe(''); beforeEach(() => {
}); createComponent({
apolloProvider: createMockApolloProvider(successHandler),
}); });
});
describe('security artifacts on last page of multi-page response', () => { it('renders the download dropdown', () => {
const numPages = 3; expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
});
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', () => { it('renders the expected message', () => {
expect(Api.pipelineJobs).toHaveBeenCalledTimes(numPages); expect(wrapper.text()).toContain(SecurityReportsApp.i18n.scansHaveRun);
}); });
it('renders the expected message', () => { it('renders a help link', () => {
expect(wrapper.text()).toMatchInterpolatedText( expect(findHelpIconComponent().props()).toEqual({
SecurityReportsApp.i18n.scansHaveRunWithDownloadGuidance, helpPath: props.securityReportsDocsPath,
); discoverProjectSecurityPath: props.discoverProjectSecurityPath,
});
}); });
});
});
describe('given an error from the API', () => { describe('given the artifacts query loads successfully with no artifacts', () => {
let error; beforeEach(() => {
createComponent({
beforeEach(() => { apolloProvider: createMockApolloProvider(successEmptyHandler),
error = new Error('an error'); });
jest.spyOn(Api, 'pipelineJobs').mockRejectedValue(error); });
createComponentWithFlag();
return wrapper.vm.$nextTick();
});
it('calls the pipelineJobs API correctly', () => { // TODO: Remove this assertion as part of
expect(Api.pipelineJobs).toHaveBeenCalledTimes(1); // https://gitlab.com/gitlab-org/gitlab/-/issues/273431
expect(Api.pipelineJobs).toHaveBeenCalledWith( it('initially renders nothing', () => {
props.projectId, expect(wrapper.html()).toBe('');
props.pipelineId, });
anyParams, });
);
});
it('renders nothing', () => { describe('given the artifacts query fails', () => {
expect(wrapper.html()).toBe(''); beforeEach(() => {
}); createComponent({
apolloProvider: createMockApolloProvider(failureHandler),
});
});
it('calls createFlash correctly', () => { it('calls createFlash correctly', () => {
expect(createFlash.mock.calls).toEqual([ expect(createFlash).toHaveBeenCalledWith({
[ message: SecurityReportsApp.i18n.apiError,
{ captureError: true,
message: SecurityReportsApp.i18n.apiError, error: expect.any(Error),
captureError: true,
error,
},
],
]);
});
}); });
}, });
);
// TODO: Remove this assertion as part of
// https://gitlab.com/gitlab-org/gitlab/-/issues/273431
it('renders nothing', () => {
expect(wrapper.html()).toBe('');
});
});
describe('given the coreSecurityMrWidgetCounts feature flag is enabled', () => { describe('given the coreSecurityMrWidgetCounts feature flag is enabled', () => {
let mock; let mock;
...@@ -253,6 +162,7 @@ describe('Security reports app', () => { ...@@ -253,6 +162,7 @@ describe('Security reports app', () => {
coreSecurityMrWidgetCounts: true, coreSecurityMrWidgetCounts: true,
}, },
}, },
apolloProvider: createMockApolloProvider(successHandler),
}), }),
); );
...@@ -274,11 +184,7 @@ describe('Security reports app', () => { ...@@ -274,11 +184,7 @@ describe('Security reports app', () => {
${REPORT_TYPE_SECRET_DETECTION} | ${'secretScanningComparisonPath'} | ${SECRET_SCANNING_COMPARISON_PATH} | ${secretScanningDiffSuccessMock} | ${SECRET_SCANNING_SUCCESS_MESSAGE} ${REPORT_TYPE_SECRET_DETECTION} | ${'secretScanningComparisonPath'} | ${SECRET_SCANNING_COMPARISON_PATH} | ${secretScanningDiffSuccessMock} | ${SECRET_SCANNING_SUCCESS_MESSAGE}
`( `(
'given a $pathProp and $reportType artifact', 'given a $pathProp and $reportType artifact',
({ reportType, pathProp, path, successResponse, successMessage }) => { ({ pathProp, path, successResponse, successMessage }) => {
beforeEach(() => {
setupMockJobArtifact(reportType);
});
describe('when loading', () => { describe('when loading', () => {
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios, { delayResponse: 1 }); mock = new MockAdapter(axios, { delayResponse: 1 });
...@@ -294,11 +200,11 @@ describe('Security reports app', () => { ...@@ -294,11 +200,11 @@ describe('Security reports app', () => {
}); });
it('should have loading message', () => { it('should have loading message', () => {
expect(wrapper.text()).toBe('Security scanning is loading'); expect(wrapper.text()).toContain('Security scanning is loading');
}); });
it('should not render the pipeline tab anchor', () => { it('renders the download dropdown', () => {
expect(findPipelinesTabAnchor().exists()).toBe(false); expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
}); });
}); });
...@@ -319,8 +225,8 @@ describe('Security reports app', () => { ...@@ -319,8 +225,8 @@ describe('Security reports app', () => {
expect(trimText(wrapper.text())).toContain(successMessage); expect(trimText(wrapper.text())).toContain(successMessage);
}); });
it('should render the pipeline tab anchor', () => { it('renders the download dropdown', () => {
expectPipelinesTabAnchor(); expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
}); });
}); });
...@@ -341,125 +247,25 @@ describe('Security reports app', () => { ...@@ -341,125 +247,25 @@ describe('Security reports app', () => {
expect(trimText(wrapper.text())).toContain('Loading resulted in an error'); expect(trimText(wrapper.text())).toContain('Loading resulted in an error');
}); });
it('should render the pipeline tab anchor', () => { it('renders the download dropdown', () => {
expectPipelinesTabAnchor(); expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
}); });
}); });
},
);
});
describe('given coreSecurityMrWidgetDownloads feature flag is enabled', () => { describe('when the comparison endpoint is not provided', () => {
const createComponentWithFlagEnabled = (options) => beforeEach(() => {
createComponent( mock.onGet(path).replyOnce(500);
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.html()).toBe('');
});
});
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', () => { createComponentWithFlagEnabled();
expect(findPipelinesTabAnchor().exists()).toBe(false);
});
});
describe('given the query fails', () => { return waitForPromises();
beforeEach(() => { });
createComponentWithFlagEnabled({
apolloProvider: createMockApolloProvider(failureHandler),
});
});
it('calls createFlash correctly', () => { it('renders the basic scansHaveRun message', () => {
expect(createFlash).toHaveBeenCalledWith({ expect(wrapper.text()).toContain(SecurityReportsApp.i18n.scansHaveRun);
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.html()).toBe('');
});
});
});
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);
});
}); });
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment