Skip to content
Snippets Groups Projects
Verified Commit 1dd084fe authored by Lorenz van Herwaarden's avatar Lorenz van Herwaarden :two: Committed by GitLab
Browse files

Remove pipeline filters and spec

1. Removes pipeline/filters.vue
2. Removes pipeline/filters_spec.js
3. Updates translations
parent 085f7d10
No related branches found
No related tags found
2 merge requests!170053Security patch upgrade alert: Only expose to admins 17-4,!168499Remove unused vuln pipeline components
Showing
with 0 additions and 1523 deletions
<script>
import jiraLogo from '@gitlab/svgs/dist/illustrations/logos/jira.svg?raw';
import {
GlButton,
GlFormCheckbox,
GlSkeletonLoader,
GlSprintf,
GlIcon,
GlLink,
GlTooltipDirective,
} from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapState } from 'vuex';
import { VULNERABILITY_MODAL_ID } from 'ee/vue_shared/security_reports/components/constants';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import convertReportType from 'ee/vue_shared/security_reports/store/utils/convert_report_type';
import getPrimaryIdentifier from 'ee/vue_shared/security_reports/store/utils/get_primary_identifier';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
getCreatedIssueForVulnerability,
getDismissalTransitionForVulnerability,
} from 'ee/vue_shared/security_reports/components/helpers';
import VulnerabilityActionButtons from './vulnerability_action_buttons.vue';
import VulnerabilityIssueLink from './vulnerability_issue_link.vue';
export default {
name: 'SecurityDashboardTableRow',
components: {
GlButton,
GlFormCheckbox,
GlSkeletonLoader,
GlSprintf,
GlIcon,
GlLink,
SeverityBadge,
VulnerabilityActionButtons,
VulnerabilityIssueLink,
},
directives: {
SafeHtml,
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
inject: ['canAdminVulnerability'],
props: {
vulnerability: {
type: Object,
required: false,
default: () => ({}),
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
...mapState('vulnerabilities', ['selectedVulnerabilities']),
vulnerabilityIdentifier() {
return getPrimaryIdentifier(this.vulnerability.identifiers, 'external_type');
},
vulnerabilityNamespace() {
const { location } = this.vulnerability;
return location && (location.image || location.file || location.path);
},
dismissalData() {
return getDismissalTransitionForVulnerability(this.vulnerability);
},
dismissalComment() {
return this.dismissalData?.comment;
},
issueData() {
return getCreatedIssueForVulnerability(this.vulnerability);
},
hasIssue() {
return Boolean(this.issueData || this.jiraIssueData);
},
hasJiraIssue() {
return Boolean(this.jiraIssueData);
},
hasGitLabIssue() {
return Boolean(this.issueData);
},
jiraIssueData() {
const jiraIssue = this.vulnerability.external_issue_links?.find(
(link) => link.external_issue_details?.external_tracker === 'jira',
);
if (!jiraIssue) {
return null;
}
const {
external_issue_details: {
web_url: webUrl,
references: { relative: title },
},
} = jiraIssue;
return {
webUrl,
title,
};
},
isJiraIssueCreationEnabled() {
return Boolean(this.vulnerability.create_jira_issue_url);
},
canDismissVulnerability() {
const path = this.vulnerability.create_vulnerability_feedback_dismissal_path;
return Boolean(path);
},
canCreateIssue() {
const {
create_vulnerability_feedback_issue_path: createGitLabIssuePath,
create_jira_issue_url: createJiraIssueUrl,
} = this.vulnerability;
if (createJiraIssueUrl && !this.hasJiraIssue) {
return true;
}
if (createGitLabIssuePath && !this.hasGitLabIssue) {
return true;
}
return false;
},
extraIdentifierCount() {
const { identifiers } = this.vulnerability;
if (!identifiers) {
return 0;
}
return identifiers.length - 1;
},
isSelected() {
return Boolean(this.selectedVulnerabilities[this.vulnerability.id]);
},
shouldShowExtraIdentifierCount() {
return this.extraIdentifierCount > 0;
},
useConvertReportType() {
return convertReportType(this.vulnerability.report_type);
},
vulnerabilityVendor() {
return this.vulnerability.scanner?.vendor;
},
},
methods: {
...mapActions('vulnerabilities', [
'setModalData',
'selectVulnerability',
'deselectVulnerability',
]),
toggleVulnerability() {
if (this.isSelected) {
return this.deselectVulnerability(this.vulnerability);
}
return this.selectVulnerability(this.vulnerability);
},
openModal(payload) {
this.setModalData(payload);
this.$root.$emit(BV_SHOW_MODAL, VULNERABILITY_MODAL_ID);
},
},
jiraLogo,
};
</script>
<template>
<div
class="gl-responsive-table-row p-2"
:class="{ dismissed: dismissalData, 'gl-bg-blue-50': isSelected }"
>
<div v-if="canAdminVulnerability" class="table-section section-5">
<gl-form-checkbox
:checked="isSelected"
:inline="true"
class="my-0 ml-1 mr-3"
data-testid="security-finding-checkbox"
:data-qa-finding-name="vulnerability.name"
@change="toggleVulnerability"
/>
</div>
<div class="table-section section-15">
<div class="table-mobile-header" role="rowheader">{{ s__('Reports|Severity') }}</div>
<div class="table-mobile-content">
<severity-badge
v-if="vulnerability.severity"
:severity="vulnerability.severity"
class="text-right text-md-left"
/>
</div>
</div>
<div class="table-section flex-grow-1">
<div class="table-mobile-header" role="rowheader">{{ s__('Reports|Vulnerability') }}</div>
<div
class="table-mobile-content gl-whitespace-normal"
data-testid="vulnerability-info-content"
>
<gl-skeleton-loader v-if="isLoading" :lines="2" />
<template v-else>
<gl-button
ref="vulnerability-title"
class="text-body gl-grid"
button-text-classes="gl-text-left gl-whitespace-normal! !gl-pr-4"
variant="link"
data-testid="security-finding-name-button"
:data-qa-status-description="vulnerability.name"
@click="openModal({ vulnerability })"
>{{ vulnerability.name }}</gl-button
>
<span v-if="dismissalData" data-testid="dismissal-label">
<gl-icon v-if="dismissalComment" name="comment" class="text-warning" />
<span class="text-uppercase">{{ s__('vulnerability|dismissed') }}</span>
</span>
<span
v-if="isJiraIssueCreationEnabled && hasJiraIssue"
class="gl-inline-flex gl-items-baseline"
>
<span
v-safe-html="$options.jiraLogo"
v-gl-tooltip
:title="s__('SecurityReports|Jira Issue Created')"
class="gl-mr-2 gl-self-end"
data-testid="jira-issue-icon"
>
</span>
<gl-link
:href="jiraIssueData.webUrl"
target="_blank"
class="vertical-align-middle"
data-testid="jira-issue-link"
>{{ jiraIssueData.title }}</gl-link
>
</span>
<vulnerability-issue-link
v-if="!isJiraIssueCreationEnabled && hasGitLabIssue"
class="text-nowrap"
:issue="issueData"
:project-name="vulnerability.project.name"
/>
<small v-if="vulnerabilityNamespace" class="gl-break-all gl-text-gray-500">
{{ vulnerabilityNamespace }}
</small>
</template>
</div>
</div>
<div class="table-section section-15 gl-whitespace-normal">
<div class="table-mobile-header" role="rowheader">{{ s__('Reports|Identifier') }}</div>
<div class="table-mobile-content">
<div class="gl-overflow-hidden gl-text-ellipsis" :title="vulnerabilityIdentifier">
{{ vulnerabilityIdentifier }}
</div>
<div v-if="shouldShowExtraIdentifierCount" class="gl-text-gray-300">
<gl-sprintf :message="__('+ %{count} more')">
<template #count>
{{ extraIdentifierCount }}
</template>
</gl-sprintf>
</div>
</div>
</div>
<div class="table-section section-15">
<div class="table-mobile-header" role="rowheader">{{ s__('Reports|Scanner') }}</div>
<div class="table-mobile-content">
<div class="text-capitalize">
{{ useConvertReportType }}
</div>
<div v-if="vulnerabilityVendor" class="gl-text-gray-300" data-testid="vulnerability-vendor">
{{ vulnerabilityVendor }}
</div>
</div>
</div>
<div class="table-section section-20">
<div class="table-mobile-header" role="rowheader">{{ s__('Reports|Actions') }}</div>
<div class="table-mobile-content action-buttons justify-content-end gl-flex">
<vulnerability-action-buttons
v-if="!isLoading"
:vulnerability="vulnerability"
:can-create-issue="canCreateIssue"
:can-dismiss-vulnerability="canDismissVulnerability"
:is-dismissed="Boolean(dismissalData)"
/>
</div>
</div>
</div>
</template>
<script>
import { GlButton, GlFormSelect } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters } from 'vuex';
import { __, n__ } from '~/locale';
const REASON_NONE = __('[No reason]');
const REASON_WONT_FIX = __("Won't fix / Accept risk");
const REASON_FALSE_POSITIVE = __('False positive');
export default {
name: 'SelectionSummary',
components: {
GlButton,
GlFormSelect,
},
data() {
return {
dismissalReason: null,
};
},
computed: {
...mapGetters('vulnerabilities', ['selectedVulnerabilitiesCount']),
canDismissVulnerability() {
return this.dismissalReason && this.selectedVulnerabilitiesCount > 0;
},
message() {
return n__(
'Dismiss %d selected vulnerability as',
'Dismiss %d selected vulnerabilities as',
this.selectedVulnerabilitiesCount,
);
},
},
methods: {
...mapActions('vulnerabilities', ['dismissSelectedVulnerabilities']),
handleDismiss() {
if (!this.canDismissVulnerability) {
return;
}
if (this.dismissalReason === REASON_NONE) {
this.dismissSelectedVulnerabilities();
} else {
this.dismissSelectedVulnerabilities({ comment: this.dismissalReason });
}
},
},
dismissalReasons: [
{ value: null, text: __('Select a reason') },
REASON_FALSE_POSITIVE,
REASON_WONT_FIX,
REASON_NONE,
],
};
</script>
<template>
<div class="card">
<form class="card-body gl-flex gl-items-center" @submit.prevent="handleDismiss">
<span data-testid="dismiss-message">{{ message }}</span>
<gl-form-select
v-model="dismissalReason"
class="mx-3 w-auto"
data-testid="finding-dismissal-reason"
:options="$options.dismissalReasons"
/>
<gl-button
type="submit"
data-testid="finding-dismiss-button"
:disabled="!canDismissVulnerability"
>
{{ __('Dismiss selected') }}
</gl-button>
</form>
</div>
</template>
<script>
import { GlTooltipDirective, GlButton } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapState } from 'vuex';
import { VULNERABILITY_MODAL_ID } from 'ee/vue_shared/security_reports/components/constants';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import securityReportFindingQuery from 'ee/security_dashboard/graphql/queries/security_report_finding.query.graphql';
import createJiraIssueMutation from 'ee/security_dashboard/graphql/mutations/finding_create_jira_issue.mutation.graphql';
import vulnerabilityExternalIssuesQuery from 'ee/security_dashboard/graphql/queries/vulnerability_external_issues.query.graphql';
import { s__ } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
export const i18n = {
moreInfo: s__('SecurityReports|More info'),
createIssue: s__('SecurityReports|Create issue'),
createJiraIssue: s__('SecurityReports|Create Jira issue'),
revertDismissVulnerability: s__('SecurityReports|Undo dismiss'),
dismissVulnerability: s__('SecurityReports|Dismiss vulnerability'),
};
export default {
i18n,
jiraIssuePollingInterval: 2000,
name: 'SecurityDashboardActionButtons',
components: {
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['projectFullPath', 'pipeline'],
props: {
vulnerability: {
type: Object,
required: true,
},
canCreateIssue: {
type: Boolean,
required: false,
default: false,
},
canDismissVulnerability: {
type: Boolean,
required: false,
default: false,
},
isDismissed: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
vulnerabilityId: null,
};
},
apollo: {
// eslint-disable-next-line @gitlab/vue-no-undef-apollo-properties
vulnerabilityExternalIssues: {
query: vulnerabilityExternalIssuesQuery,
manual: true,
variables() {
return {
vulnerabilityId: this.vulnerabilityId,
};
},
skip() {
return !this.vulnerabilityId;
},
result({
data: {
vulnerability: {
externalIssueLinks: { nodes },
},
},
}) {
const [firstExternalIssueLink = {}] = nodes;
const { externalIssue } = firstExternalIssueLink;
if (externalIssue?.webUrl) {
this.createJiraIssueSuccess({
externalIssue: {
web_url: externalIssue.webUrl,
external_tracker: 'jira',
references: {
relative: externalIssue.relativeReference,
},
},
vulnerability: this.vulnerability,
});
const openedInNewTab = true;
visitUrl(externalIssue.webUrl, openedInNewTab);
this.$apollo.queries.vulnerabilityExternalIssues.stopPolling();
}
},
error() {
this.receiveCreateIssueError();
},
},
},
computed: {
...mapState('vulnerabilities', ['isCreatingIssue', 'isDismissingVulnerability']),
isJiraVulnerabilityIssuesEnabled() {
return Boolean(this?.vulnerability?.create_jira_issue_url);
},
createIssueButtonLabel() {
const { $options } = this;
return this.isJiraVulnerabilityIssuesEnabled
? $options.i18n.createJiraIssue
: $options.i18n.createIssue;
},
},
methods: {
...mapActions('vulnerabilities', [
'setModalData',
'createIssue',
'createJiraIssueStart',
'createJiraIssueSuccess',
'dismissVulnerability',
'revertDismissVulnerability',
'receiveCreateIssueError',
]),
async fetchVulnerabilityId() {
try {
const result = await this.$apollo.query({
query: securityReportFindingQuery,
variables: {
findingUuid: this.vulnerability.uuid,
projectFullPath: this.projectFullPath,
pipelineIid: this.pipeline.iid,
},
});
return result.data.project.pipeline.securityReportFinding.vulnerability.id;
} catch {
// the error state is managed by Vuex
this.receiveCreateIssueError();
return null;
}
},
startCreateJiraIssueMutation() {
return this.$apollo.mutate({
mutation: createJiraIssueMutation,
variables: {
vulnerabilityId: this.vulnerabilityId,
},
});
},
async createJiraIssue() {
// the loading state is managed by Vuex
this.createJiraIssueStart();
// the REST endpoint does not include the vulnerability ID, so we need to query it first
this.vulnerabilityId = await this.fetchVulnerabilityId();
if (!this.vulnerabilityId) {
return;
}
try {
await this.startCreateJiraIssueMutation();
} catch {
this.receiveCreateIssueError();
}
// since the issue is created asynchronously, we need to poll the backend to check if it's ready
this.$apollo.queries.vulnerabilityExternalIssues.startPolling(
this.$options.jiraIssuePollingInterval,
);
},
handleCreateIssue() {
const { vulnerability } = this;
if (this.isJiraVulnerabilityIssuesEnabled) {
this.createJiraIssue({
vulnerability: this.vulnerability,
projectFullPath: this.projectFullPath,
pipelineIid: this.pipeline.iid,
});
} else {
this.createIssue({ vulnerability, flashError: true });
}
},
handleDismissVulnerability() {
const { vulnerability } = this;
this.dismissVulnerability({ vulnerability, flashError: true });
},
handleUndoDismiss() {
const { vulnerability } = this;
this.revertDismissVulnerability({ vulnerability, flashError: true });
},
openModal(payload) {
this.setModalData(payload);
this.$root.$emit(BV_SHOW_MODAL, VULNERABILITY_MODAL_ID);
},
},
};
</script>
<template>
<div>
<gl-button
key="more-info"
v-gl-tooltip
:aria-label="$options.i18n.moreInfo"
:title="$options.i18n.moreInfo"
class="js-more-info"
variant="default"
category="tertiary"
icon="information-o"
data-testid="more-info"
:data-qa-finding-name="vulnerability.name"
@click="openModal({ vulnerability })"
/>
<gl-button
v-if="canCreateIssue"
key="create-issue"
v-gl-tooltip
:aria-label="createIssueButtonLabel"
:loading="isCreatingIssue"
:title="createIssueButtonLabel"
class="js-create-issue gl-ml-3"
variant="default"
category="tertiary"
icon="issue-new"
data-testid="create-issue"
:data-qa-finding-name="vulnerability.name"
@click="handleCreateIssue"
/>
<template v-if="canDismissVulnerability">
<gl-button
v-if="isDismissed"
key="undo-dismiss"
v-gl-tooltip
:aria-label="$options.i18n.revertDismissVulnerability"
:loading="isDismissingVulnerability"
:title="$options.i18n.revertDismissVulnerability"
class="js-undo-dismiss gl-ml-3"
variant="default"
category="tertiary"
icon="redo"
data-testid="undo-dismiss"
:data-qa-finding-name="vulnerability.name"
@click="handleUndoDismiss"
/>
<gl-button
v-else
key="dismiss-vulnerability"
v-gl-tooltip
:aria-label="$options.i18n.dismissVulnerability"
:loading="isDismissingVulnerability"
:title="$options.i18n.dismissVulnerability"
class="js-dismiss-vulnerability gl-ml-3"
variant="default"
category="tertiary"
icon="cancel"
data-testid="dismiss-vulnerability"
:data-qa-finding-name="vulnerability.name"
@click="handleDismissVulnerability"
/>
</template>
</div>
</template>
<script>
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
export default {
name: 'VulnerabilityIssueLink',
components: {
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
issue: {
type: Object,
required: true,
},
projectName: {
type: String,
required: true,
},
},
computed: {
linkText() {
return `${this.projectName}#${this.issue.issue_iid}`;
},
},
};
</script>
<template>
<div class="gl-inline">
<gl-icon
v-gl-tooltip
name="issues"
class="vertical-align-middle gl-text-success"
:title="s__('SecurityReports|Issue Created')"
/>
<a :href="issue.issue_url" class="vertical-align-middle">{{ linkText }}</a>
</div>
</template>
import { GlFormCheckbox, GlIcon, GlSkeletonLoader } from '@gitlab/ui';
import { createWrapper, mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { cloneDeep } from 'lodash';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SecurityDashboardTableRow from 'ee/security_dashboard/components/pipeline/security_dashboard_table_row.vue';
import VulnerabilityActionButtons from 'ee/security_dashboard/components/pipeline/vulnerability_action_buttons.vue';
import { setupStore } from 'ee/security_dashboard/store';
import { VULNERABILITY_MODAL_ID } from 'ee/vue_shared/security_reports/components/constants';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import { trimText } from 'helpers/text_helper';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import VulnerabilityIssueLink from 'ee/security_dashboard/components/pipeline/vulnerability_issue_link.vue';
import { getCreatedIssueForVulnerability } from 'ee/vue_shared/security_reports/components/helpers';
import mockDataVulnerabilities, {
issueData,
} from '../../store/modules/vulnerabilities/data/mock_data_vulnerabilities';
Vue.use(Vuex);
describe('Security Dashboard Table Row', () => {
let wrapper;
let store;
const createComponent = (mountFunc, { props = {}, canAdminVulnerability = true } = {}) => {
wrapper = mountFunc(SecurityDashboardTableRow, {
store,
provide: { canAdminVulnerability, projectFullPath: 'group/project', pipeline: { iid: 1 } },
propsData: {
...props,
},
});
};
beforeEach(() => {
store = new Vuex.Store();
setupStore(store);
jest.spyOn(store, 'dispatch');
});
const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findContent = (i) => wrapper.findAll('.table-mobile-content').at(i);
const findVulnerabilityIssueLink = () => wrapper.findComponent(VulnerabilityIssueLink);
const hasSelectedClass = () => wrapper.classes('gl-bg-blue-50');
const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findSeverityBadge = () => wrapper.findComponent(SeverityBadge);
const findDismissalLabel = () => wrapper.findByTestId('dismissal-label');
const findDismissalCommentIcon = () => wrapper.findComponent(GlIcon);
describe('when loading', () => {
beforeEach(() => {
createComponent(shallowMount, { props: { isLoading: true } });
});
it('should display the skeleton loader', () => {
expect(findLoader().exists()).toBe(true);
});
it('should not render the severity', () => {
expect(findSeverityBadge().exists()).toBe(false);
});
it('should render a `` for the report type and scanner', () => {
expect(findContent(3).text()).toEqual('');
expect(wrapper.find('vulnerability-vendor').exists()).toBe(false);
});
it('should not render action buttons', () => {
expect(wrapper.findAll('.action-buttons button')).toHaveLength(0);
});
});
describe('when loaded', () => {
let vulnerability = mockDataVulnerabilities[0];
beforeEach(() => {
createComponent(mount, { props: { vulnerability } });
});
it('should not display the skeleton loader', () => {
expect(findLoader().exists()).toBe(false);
});
it('should render the severity', () => {
expect(findSeverityBadge().text().toLowerCase()).toBe(vulnerability.severity);
});
it('should render the identifier cell', () => {
const { identifiers } = vulnerability;
expect(findContent(2).text()).toContain(identifiers[0].name);
expect(trimText(findContent(2).text())).toContain(`${identifiers.length - 1} more`);
});
it('should render the report type', () => {
expect(findContent(3).text().toLowerCase()).toContain(
vulnerability.report_type.toLowerCase(),
);
});
it('should render the scanner vendor if the scanner does exist', () => {
expect(findContent(3).text()).toContain(vulnerability.scanner.vendor);
});
describe('the project name', () => {
it('should render the name', () => {
expect(findContent(1).text()).toContain(vulnerability.name);
});
it('should render the project namespace', () => {
expect(findContent(1).text()).toContain(vulnerability.location.file);
});
it('should fire the setModalData action and open the modal when clicked', () => {
const rootWrapper = createWrapper(wrapper.vm.$root);
jest.spyOn(store, 'dispatch').mockImplementation();
const el = wrapper.findComponent({ ref: 'vulnerability-title' });
el.trigger('click');
expect(store.dispatch).toHaveBeenCalledWith('vulnerabilities/setModalData', {
vulnerability,
});
expect(rootWrapper.emitted(BV_SHOW_MODAL)[0]).toContain(VULNERABILITY_MODAL_ID);
});
});
describe('Non-group Security Dashboard', () => {
beforeEach(() => {
// eslint-disable-next-line prefer-destructuring
vulnerability = mockDataVulnerabilities[7];
createComponent(shallowMount, { props: { vulnerability } });
});
it('should contain container image as the namespace', () => {
expect(findContent(1).text()).toContain(vulnerability.location.image);
});
});
});
describe('vulnerability dismissal', () => {
let vulnerability;
beforeEach(() => {
vulnerability = cloneDeep(mockDataVulnerabilities[0]);
});
it.each`
stateTransitions | isLabelShown | isIconShown
${[{ to_state: 'dismissed' }]} | ${true} | ${false}
${[{ to_state: 'dismissed', comment: 'comment' }]} | ${true} | ${true}
${[{}, {}, { to_state: 'dismissed' }]} | ${true} | ${false}
${[]} | ${false} | ${false}
${[{ to_state: 'detected' }]} | ${false} | ${false}
`(
'shows dismissal badge: $isLabelShown, shows dismissal comment icon: $isIconShown',
({ stateTransitions, isLabelShown, isIconShown }) => {
vulnerability.state_transitions = stateTransitions;
createComponent(shallowMountExtended, { props: { vulnerability } });
expect(findDismissalLabel().exists()).toBe(isLabelShown);
expect(findDismissalCommentIcon().exists()).toBe(isIconShown);
},
);
});
describe('with created issue', () => {
const vulnerability = mockDataVulnerabilities[3];
it('shows the vulnerability issue link with the expected props', () => {
createComponent(shallowMount, { props: { vulnerability } });
expect(findVulnerabilityIssueLink().props()).toMatchObject({
issue: getCreatedIssueForVulnerability(vulnerability),
projectName: vulnerability.project.name,
});
});
});
describe('with no created issue', () => {
const vulnerability = mockDataVulnerabilities[0];
beforeEach(() => {
createComponent(shallowMount, { props: { vulnerability } });
});
it('should not show the vulnerability issue link', () => {
expect(findVulnerabilityIssueLink().exists()).toBe(false);
});
it('should be unselected', () => {
expect(hasSelectedClass()).toBe(false);
expect(findCheckbox().attributes('checked')).toBe(undefined);
});
describe('when checked', () => {
beforeEach(() => {
findCheckbox().vm.$emit('change');
});
it('should be selected', () => {
expect(hasSelectedClass()).toBe(true);
expect(findCheckbox().attributes('checked')).toBe('true');
});
it('should update store', () => {
expect(store.dispatch).toHaveBeenCalledWith(
'vulnerabilities/selectVulnerability',
vulnerability,
);
});
describe('when unchecked', () => {
beforeEach(() => {
findCheckbox().vm.$emit('change');
});
it('should be unselected', () => {
expect(hasSelectedClass()).toBe(false);
expect(findCheckbox().attributes('checked')).toBe(undefined);
});
it('should update store', () => {
expect(store.dispatch).toHaveBeenCalledWith(
'vulnerabilities/deselectVulnerability',
vulnerability,
);
});
});
});
});
describe('with less than two identifiers', () => {
const vulnerability = mockDataVulnerabilities[1];
beforeEach(() => {
createComponent(shallowMount, { props: { vulnerability } });
});
it('should render the identifier cell', () => {
const { identifiers } = vulnerability;
expect(findContent(2).text()).toBe(identifiers[0].name);
});
});
describe.each`
createGitLabIssuePath | createJiraIssueUrl | canCreateIssue
${''} | ${''} | ${false}
${''} | ${'http://foo.bar'} | ${true}
${'/foo/bar'} | ${''} | ${true}
${'/foo/bar'} | ${'http://foo.bar'} | ${true}
`(
'with createGitLabIssuePath set to "$createGitLabIssuePath" and createJiraIssueUrl to "$createJiraIssueUrl"',
({ createGitLabIssuePath, createJiraIssueUrl, canCreateIssue }) => {
beforeEach(() => {
const vulnerability = mockDataVulnerabilities[1];
vulnerability.create_vulnerability_feedback_issue_path = createGitLabIssuePath;
vulnerability.create_jira_issue_url = createJiraIssueUrl;
createComponent(shallowMount, { props: { vulnerability } });
});
it(`should pass "canCreateIssue" as "${canCreateIssue}" to the action-buttons component`, () => {
expect(wrapper.findComponent(VulnerabilityActionButtons).props('canCreateIssue')).toBe(
canCreateIssue,
);
});
},
);
describe('with Jira issue-integration enabled', () => {
describe('with an existing GitLab issue', () => {
beforeEach(() => {
const vulnerability = {
...mockDataVulnerabilities[1],
issue_links: [issueData],
create_jira_issue_url: 'http://jira.example.com',
};
createComponent(shallowMountExtended, { props: { vulnerability } });
});
it('allows the creation of a Jira issue', () => {
expect(wrapper.findComponent(VulnerabilityActionButtons).props('canCreateIssue')).toBe(
true,
);
});
});
describe('with an existing Jira issue', () => {
const jiraIssueDetails = {
external_issue_details: {
external_tracker: 'jira',
web_url: 'http://jira.example.com/GTA-1',
references: {
relative: 'GTA#1',
},
},
};
beforeEach(() => {
const vulnerability = {
...mockDataVulnerabilities[1],
external_issue_links: [jiraIssueDetails],
create_jira_issue_url: 'http://jira.example.com',
};
createComponent(shallowMountExtended, { props: { vulnerability } });
});
it('does not allow the creation of an additional Jira issue', () => {
expect(wrapper.findComponent(VulnerabilityActionButtons).props('canCreateIssue')).toBe(
false,
);
});
it('renders a Jira logo with a tooltip to let the user know that there is an existing issue', () => {
expect(wrapper.findByTestId('jira-issue-icon').attributes('title')).toBe(
'Jira Issue Created',
);
});
it('renders a link to the Jira issue that opens in a new tab', () => {
const jiraIssueLink = wrapper.findByTestId('jira-issue-link');
expect(jiraIssueLink.props('href')).toBe(
jiraIssueDetails.external_issue_details.references.web_url,
);
expect(jiraIssueLink.attributes('target')).toBe('_blank');
});
});
});
describe('with a deleted Jira issue', () => {
const jiraIssueDetails = {
// when an attached Jira issue gets deleted the external_issue_details is set to null
external_issue_details: null,
};
beforeEach(() => {
const vulnerability = {
...mockDataVulnerabilities[1],
external_issue_links: [jiraIssueDetails],
create_jira_issue_url: 'http://jira.example.com',
};
createComponent(shallowMountExtended, { props: { vulnerability } });
});
it('allows the creation of a Jira issue', () => {
expect(wrapper.findComponent(VulnerabilityActionButtons).props('canCreateIssue')).toBe(true);
});
it('does not render any information about the deleted Jira issue', () => {
expect(wrapper.findByTestId('jira-issue-icon').exists()).toBe(false);
expect(wrapper.findByTestId('jira-issue-link').exists()).toBe(false);
});
});
describe('can admin vulnerability', () => {
it.each([true, false])(
'shows/hides the select all checkbox if the user can admin vulnerability = %s',
(canAdminVulnerability) => {
createComponent(shallowMount, { canAdminVulnerability });
expect(findCheckbox().exists()).toBe(canAdminVulnerability);
},
);
});
});
import { GlButton, GlFormSelect } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import SelectionSummary from 'ee/security_dashboard/components/pipeline/selection_summary_vuex.vue';
Vue.use(Vuex);
jest.mock('~/vue_shared/plugins/global_toast');
describe('Selection Summary', () => {
let store;
let wrapper;
const dismissSelectedVulnerabilitiesMock = jest.fn();
const setSelectedVulnerabilitiesCount = (count) => {
store.state.vulnerabilities.count = count;
};
const createComponent = () => {
store = new Vuex.Store({
modules: {
vulnerabilities: {
namespaced: true,
state: {
count: 0,
},
getters: {
selectedVulnerabilitiesCount: () => store.state.vulnerabilities.count,
},
actions: {
dismissSelectedVulnerabilities: dismissSelectedVulnerabilitiesMock,
},
},
},
});
wrapper = mount(SelectionSummary, { store });
};
beforeEach(() => {
createComponent();
});
const formSelect = () => wrapper.findComponent(GlFormSelect);
const dismissMessage = () => wrapper.find('[data-testid="dismiss-message"]');
const dismissButton = () => wrapper.findComponent(GlButton);
it('renders the form', () => {
expect(formSelect().exists()).toBe(true);
});
describe('dismiss message', () => {
it('renders when no vulnerabilities selected', () => {
expect(dismissMessage().text()).toBe('Dismiss 0 selected vulnerabilities as');
});
it('renders when 1 vulnerability selected', async () => {
setSelectedVulnerabilitiesCount(1);
await nextTick();
expect(dismissMessage().text()).toBe('Dismiss 1 selected vulnerability as');
});
it('renders when 2 vulnerabilities selected', async () => {
setSelectedVulnerabilitiesCount(2);
await nextTick();
expect(dismissMessage().text()).toBe('Dismiss 2 selected vulnerabilities as');
});
});
describe('dismiss button', () => {
it('should be disabled if an option is not selected', () => {
expect(dismissButton().exists()).toBe(true);
expect(dismissButton().props().disabled).toBe(true);
});
it('should be enabled if a vulnerability is selected and dismissal reason is selected', async () => {
expect(wrapper.vm.dismissalReason).toBe(null);
expect(wrapper.findAll('option')).toHaveLength(4);
setSelectedVulnerabilitiesCount(1);
const option = formSelect().findAll('option').at(1);
option.setSelected();
formSelect().trigger('change');
await nextTick();
expect(wrapper.vm.dismissalReason).toEqual(option.attributes('value'));
expect(dismissButton().props().disabled).toBe(false);
});
it('should call the dismissSelectedVulnerabilities action with the expected data', async () => {
setSelectedVulnerabilitiesCount(2);
const option = formSelect().findAll('option').at(1);
option.setSelected();
formSelect().trigger('change');
await nextTick();
dismissButton().trigger('submit');
await nextTick();
expect(dismissSelectedVulnerabilitiesMock).toHaveBeenCalledTimes(1);
expect(dismissSelectedVulnerabilitiesMock).toHaveBeenCalledWith(expect.anything(), {
comment: option.attributes('value'),
});
});
});
});
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import VueApollo from 'vue-apollo';
import { GlButton } from '@gitlab/ui';
import { createWrapper } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import VulnerabilityActionButtons, {
i18n,
} from 'ee/security_dashboard/components/pipeline/vulnerability_action_buttons.vue';
import { setupStore } from 'ee/security_dashboard/store';
import { VULNERABILITY_MODAL_ID } from 'ee/vue_shared/security_reports/components/constants';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { visitUrl } from '~/lib/utils/url_utility';
import {
getPipelineSecurityReportFindingResponse,
getVulnerabilityExternalIssuesQueryResponse,
} from 'ee_jest/security_dashboard/components/pipeline/mock_data';
import { vulnerabilityExternalIssueLinkCreateMockFactory } from 'ee_jest/vue_shared/security_reports/components/apollo_mocks';
import securityReportFindingQuery from 'ee/security_dashboard/graphql/queries/security_report_finding.query.graphql';
import vulnerabilityExternalIssuesQuery from 'ee/security_dashboard/graphql/queries/vulnerability_external_issues.query.graphql';
import createJiraIssueMutation from 'ee/security_dashboard/graphql/mutations/finding_create_jira_issue.mutation.graphql';
import { resetStore } from '../../helpers';
import mockDataVulnerabilities from '../../store/modules/vulnerabilities/data/mock_data_vulnerabilities';
Vue.use(Vuex);
Vue.use(VueApollo);
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
}));
describe('Security Dashboard Action Buttons', () => {
let store;
let wrapper;
let dispatchMock;
let mockApollo;
const wrapperFactory =
(mountFn) =>
({ ...options }) => {
mockApollo = createMockApollo([
[
securityReportFindingQuery,
jest.fn().mockResolvedValue(getPipelineSecurityReportFindingResponse()),
],
[
createJiraIssueMutation,
options.handlers?.createJiraIssueMutation ||
jest.fn().mockResolvedValue(vulnerabilityExternalIssueLinkCreateMockFactory()),
],
[
vulnerabilityExternalIssuesQuery,
jest.fn().mockResolvedValue(getVulnerabilityExternalIssuesQueryResponse()),
],
]);
wrapper = mountFn(VulnerabilityActionButtons, {
...options,
store,
apolloProvider: mockApollo,
provide: {
projectFullPath: 'group/project',
pipeline: {
iid: 22,
},
},
});
dispatchMock = jest.spyOn(store, 'dispatch').mockReturnValue(Promise.resolve());
};
const createShallowComponent = wrapperFactory(shallowMountExtended);
const createFullComponent = wrapperFactory(mountExtended);
const findAllButtons = () => wrapper.findAllComponents(GlButton);
const findMoreInfoButton = () => wrapper.findByTestId('more-info');
const findCreateIssueButton = () => wrapper.findByTestId('create-issue');
const findDismissVulnerabilityButton = () => wrapper.findByTestId('dismiss-vulnerability');
const findUndoDismissButton = () => wrapper.findByTestId('undo-dismiss');
beforeEach(() => {
store = new Vuex.Store();
setupStore(store);
});
afterEach(() => {
resetStore(store);
});
describe('with a fresh vulnerability', () => {
beforeEach(() => {
createFullComponent({
propsData: {
vulnerability: mockDataVulnerabilities[0],
canCreateIssue: true,
canDismissVulnerability: true,
},
});
});
it('should render three buttons in a button group', () => {
expect(findAllButtons()).toHaveLength(3);
});
describe('More Info Button', () => {
it('should render the More info button', () => {
expect(findMoreInfoButton().exists()).toBe(true);
});
it('should render the correct tooltip', () => {
expect(findMoreInfoButton().attributes('title')).toBe(i18n.moreInfo);
});
it('should emit an `setModalData` event and open the modal when clicked', async () => {
await findMoreInfoButton().trigger('click');
expect(dispatchMock).toHaveBeenCalledWith('vulnerabilities/setModalData', {
vulnerability: mockDataVulnerabilities[0],
});
expect(createWrapper(wrapper.vm.$root).emitted(BV_SHOW_MODAL)).toEqual([
[VULNERABILITY_MODAL_ID],
]);
});
});
describe('Create Issue Button', () => {
it('should render the create issue button', () => {
expect(findCreateIssueButton().exists()).toBe(true);
});
it('should render the correct tooltip', () => {
expect(findCreateIssueButton().attributes('title')).toBe(i18n.createIssue);
});
it('should emit an `createIssue` event when clicked', async () => {
await findCreateIssueButton().trigger('click');
expect(dispatchMock).toHaveBeenCalledWith('vulnerabilities/createIssue', {
vulnerability: mockDataVulnerabilities[0],
flashError: true,
});
});
describe('with Jira issues for vulnerabilities enabled', () => {
const propsData = {
vulnerability: mockDataVulnerabilities[8],
canCreateIssue: true,
};
it('should render the correct tooltip', () => {
createFullComponent({ propsData });
expect(findCreateIssueButton().attributes('title')).toBe(i18n.createJiraIssue);
});
it('should open a new window when the create-issue button is clicked', async () => {
createFullComponent({ propsData });
const createdJiraIssue =
getVulnerabilityExternalIssuesQueryResponse().data.vulnerability.externalIssueLinks
.nodes[0].externalIssue;
expect(visitUrl).not.toHaveBeenCalled();
await findCreateIssueButton().trigger('click');
// Vuex handles the loading state
expect(dispatchMock).toHaveBeenCalledWith(
'vulnerabilities/createJiraIssueStart',
undefined,
);
await waitForPromises();
expect(visitUrl).toHaveBeenCalledWith(
createdJiraIssue.webUrl,
true, // external link flag
);
// Vuex handles the success state, as we need to disable the loading state and add the new data to the store
expect(dispatchMock).toHaveBeenCalledWith('vulnerabilities/createJiraIssueSuccess', {
externalIssue: {
external_tracker: 'jira',
web_url: createdJiraIssue.webUrl,
references: {
relative: createdJiraIssue.relativeReference,
},
},
vulnerability: mockDataVulnerabilities[8],
});
});
it('should show an error message when the mutation fails', async () => {
createFullComponent({
propsData,
handlers: {
createJiraIssueMutation: jest.fn().mockRejectedValue(),
},
});
await findCreateIssueButton().trigger('click');
await waitForPromises();
expect(dispatchMock).toHaveBeenCalledWith(
'vulnerabilities/receiveCreateIssueError',
undefined,
);
});
});
});
describe('Dismiss Vulnerability Button', () => {
it('should render the dismiss vulnerability button', () => {
expect(findDismissVulnerabilityButton().exists()).toBe(true);
});
it('should render the correct tooltip', () => {
expect(findDismissVulnerabilityButton().attributes('title')).toBe(
i18n.dismissVulnerability,
);
});
it('should emit an `dismissVulnerability` event when clicked', async () => {
await findDismissVulnerabilityButton().trigger('click');
expect(dispatchMock).toHaveBeenCalledWith('vulnerabilities/dismissVulnerability', {
vulnerability: mockDataVulnerabilities[0],
flashError: true,
});
});
});
});
describe('with a vulnerability that has an issue', () => {
beforeEach(() => {
createShallowComponent({
propsData: {
vulnerability: mockDataVulnerabilities[3],
},
});
});
it('should only render one button', () => {
expect(findAllButtons()).toHaveLength(1);
});
it('should not render the create issue button', () => {
expect(findCreateIssueButton().exists()).toBe(false);
});
});
describe('with a vulnerability that has been dismissed', () => {
beforeEach(() => {
createShallowComponent({
propsData: {
vulnerability: mockDataVulnerabilities[2],
canDismissVulnerability: true,
isDismissed: true,
},
});
});
it('should render two buttons in a button group', () => {
expect(findAllButtons()).toHaveLength(2);
});
it('should not render the dismiss vulnerability button', () => {
expect(findDismissVulnerabilityButton().exists()).toBe(false);
});
it('should render the undo dismiss button', () => {
expect(findUndoDismissButton().exists()).toBe(true);
});
it('should render the correct tooltip', () => {
expect(findUndoDismissButton().attributes('title')).toBe(i18n.revertDismissVulnerability);
});
});
});
import { mount } from '@vue/test-utils';
import VulnerabilityIssueLink from 'ee/security_dashboard/components/pipeline/vulnerability_issue_link.vue';
describe('Vulnerability Issue Link component', () => {
let wrapper;
const createComponent = () => {
const issue = {
issue_iid: 1,
issue_url: 'https://gitlab.com',
};
const projectName = 'Project Name';
const propsData = { issue, projectName };
wrapper = mount(VulnerabilityIssueLink, { propsData });
};
beforeEach(() => {
createComponent();
});
it('should render the severity label', () => {
const props = wrapper.props();
expect(wrapper.text()).toContain(`${props.projectName}#${props.issue.issue_iid}`);
});
it('should link to the issue', () => {
const link = wrapper.find('a');
expect(link.attributes('href')).toMatch(wrapper.props().issue.issue_url);
});
});
......@@ -19949,20 +19949,12 @@ msgstr ""
msgid "Dismiss"
msgstr ""
 
msgid "Dismiss %d selected vulnerability as"
msgid_plural "Dismiss %d selected vulnerabilities as"
msgstr[0] ""
msgstr[1] ""
msgid "Dismiss Alert"
msgstr ""
 
msgid "Dismiss merge request promotion"
msgstr ""
 
msgid "Dismiss selected"
msgstr ""
msgid "Dismiss trial promotion"
msgstr ""
 
......@@ -23128,9 +23120,6 @@ msgstr ""
msgid "Failure"
msgstr ""
 
msgid "False positive"
msgstr ""
msgid "Fast timeout"
msgstr ""
 
......@@ -45915,9 +45904,6 @@ msgstr ""
msgid "Reports|Accessibility scanning results are being parsed"
msgstr ""
 
msgid "Reports|Actions"
msgstr ""
msgid "Reports|An error occurred while loading %{name} results"
msgstr ""
 
......@@ -45952,9 +45938,6 @@ msgstr ""
msgid "Reports|Head report parsing error:"
msgstr ""
 
msgid "Reports|Identifier"
msgstr ""
msgid "Reports|Metrics report scanning detected no new changes"
msgstr ""
 
......@@ -45970,12 +45953,6 @@ msgstr ""
msgid "Reports|New"
msgstr ""
 
msgid "Reports|Scanner"
msgstr ""
msgid "Reports|Severity"
msgstr ""
msgid "Reports|Test summary"
msgstr ""
 
......@@ -45988,9 +45965,6 @@ msgstr ""
msgid "Reports|Tool"
msgstr ""
 
msgid "Reports|Vulnerability"
msgstr ""
msgid "Reports|Vulnerability Name"
msgstr ""
 
......@@ -50363,9 +50337,6 @@ msgstr ""
msgid "SecurityReports|Create Jira issue"
msgstr ""
 
msgid "SecurityReports|Create issue"
msgstr ""
msgid "SecurityReports|Detection"
msgstr ""
 
......@@ -50465,15 +50436,9 @@ msgstr ""
msgid "SecurityReports|Issue"
msgstr ""
 
msgid "SecurityReports|Issue Created"
msgstr ""
msgid "SecurityReports|Issues created from a vulnerability cannot be removed."
msgstr ""
 
msgid "SecurityReports|Jira Issue Created"
msgstr ""
msgid "SecurityReports|Learn more about security configuration"
msgstr ""
 
......@@ -50507,9 +50472,6 @@ msgstr ""
msgid "SecurityReports|Monitored projects"
msgstr ""
 
msgid "SecurityReports|More info"
msgstr ""
msgid "SecurityReports|New feature: Grouping"
msgstr ""
 
......@@ -62098,9 +62060,6 @@ msgstr ""
msgid "Withdraw Access Request"
msgstr ""
 
msgid "Won't fix / Accept risk"
msgstr ""
msgid "Work Item type with id %{id} was not found"
msgstr ""
 
......@@ -64157,9 +64116,6 @@ msgstr ""
msgid "Zoom out"
msgstr ""
 
msgid "[No reason]"
msgstr ""
msgid "[REDACTED]"
msgstr ""
 
......@@ -66491,9 +66447,6 @@ msgstr[1] ""
msgid "vulnerability|Add a comment"
msgstr ""
 
msgid "vulnerability|dismissed"
msgstr ""
msgid "was set to auto-merge by"
msgstr ""
 
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment