Commit 83202c88 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch...

Merge branch '300143-jira-integration-fe-on-a-project-s-vulnerability-report-show-count-of-related-jira-issues' into 'master'

JIRA Integration - On a project's vulnerability report, show count of related Jira issues within a rows activity column

See merge request gitlab-org/gitlab!53321
parents 9b22eb48 627eee05
...@@ -15,6 +15,11 @@ export default { ...@@ -15,6 +15,11 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
isJira: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
numberOfIssues() { numberOfIssues() {
...@@ -40,9 +45,11 @@ export default { ...@@ -40,9 +45,11 @@ export default {
<template #title> <template #title>
{{ popoverTitle }} {{ popoverTitle }}
</template> </template>
<div v-for="{ issue } in issues" :key="issue.iid"> <ul class="gl-list-style-none gl-p-0 gl-m-0">
<issue-link :issue="issue" /> <li v-for="{ issue } in issues" :key="issue.iid">
</div> <issue-link :issue="issue" :is-jira="isJira" />
</li>
</ul>
</gl-popover> </gl-popover>
</div> </div>
</template> </template>
...@@ -16,7 +16,14 @@ export default { ...@@ -16,7 +16,14 @@ export default {
GlIntersectionObserver, GlIntersectionObserver,
VulnerabilityList, VulnerabilityList,
}, },
inject: ['projectFullPath'], inject: {
projectFullPath: {
default: '',
},
hasJiraVulnerabilitiesIntegrationEnabled: {
default: false,
},
},
props: { props: {
filters: { filters: {
type: Object, type: Object,
...@@ -42,6 +49,7 @@ export default { ...@@ -42,6 +49,7 @@ export default {
fullPath: this.projectFullPath, fullPath: this.projectFullPath,
first: VULNERABILITIES_PER_PAGE, first: VULNERABILITIES_PER_PAGE,
sort: this.sort, sort: this.sort,
includeExternalIssueLinks: this.hasJiraVulnerabilitiesIntegrationEnabled,
...this.filters, ...this.filters,
}; };
}, },
......
...@@ -52,7 +52,15 @@ export default { ...@@ -52,7 +52,15 @@ export default {
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
inject: ['hasVulnerabilities'], inject: {
hasVulnerabilities: {
default: false,
},
hasJiraVulnerabilitiesIntegrationEnabled: {
default: false,
},
},
props: { props: {
filters: { filters: {
type: Object, type: Object,
...@@ -275,9 +283,20 @@ export default { ...@@ -275,9 +283,20 @@ export default {
this.$set(this.selectedVulnerabilities, `${vulnerability.id}`, vulnerability); this.$set(this.selectedVulnerabilities, `${vulnerability.id}`, vulnerability);
} }
}, },
issues(item) { gitlabIssues(item) {
return item.issueLinks?.nodes || []; return item.issueLinks?.nodes || [];
}, },
externalIssues(item) {
return item.externalIssueLinks?.nodes || [];
},
jiraIssues(item) {
return this.externalIssues(item).filter(({ issue }) => issue?.externalTracker === 'jira');
},
badgeIssues(item) {
return this.hasJiraVulnerabilitiesIntegrationEnabled
? this.jiraIssues(item)
: this.gitlabIssues(item);
},
formatDate(item) { formatDate(item) {
return formatDate(item.detectedAt, 'yyyy-mm-dd'); return formatDate(item.detectedAt, 'yyyy-mm-dd');
}, },
...@@ -440,7 +459,11 @@ export default { ...@@ -440,7 +459,11 @@ export default {
<template #cell(activity)="{ item }"> <template #cell(activity)="{ item }">
<div class="gl-display-flex gl-justify-content-end"> <div class="gl-display-flex gl-justify-content-end">
<auto-fix-help-text v-if="item.hasSolutions" :merge-request="item.mergeRequest" /> <auto-fix-help-text v-if="item.hasSolutions" :merge-request="item.mergeRequest" />
<issues-badge v-if="issues(item).length > 0" :issues="issues(item)" /> <issues-badge
v-if="badgeIssues(item).length > 0"
:issues="badgeIssues(item)"
:is-jira="hasJiraVulnerabilitiesIntegrationEnabled"
/>
<remediated-badge v-if="item.resolvedOnDefaultBranch" class="gl-ml-3" /> <remediated-badge v-if="item.resolvedOnDefaultBranch" class="gl-ml-3" />
</div> </div>
</template> </template>
......
...@@ -37,6 +37,7 @@ export default (el, dashboardType) => { ...@@ -37,6 +37,7 @@ export default (el, dashboardType) => {
pipelinePath, pipelinePath,
pipelineSecurityBuildsFailedCount, pipelineSecurityBuildsFailedCount,
pipelineSecurityBuildsFailedPath, pipelineSecurityBuildsFailedPath,
hasJiraVulnerabilitiesIntegrationEnabled,
} = el.dataset; } = el.dataset;
if (isUnavailable) { if (isUnavailable) {
...@@ -61,6 +62,9 @@ export default (el, dashboardType) => { ...@@ -61,6 +62,9 @@ export default (el, dashboardType) => {
noPipelineRunScannersHelpPath, noPipelineRunScannersHelpPath,
hasVulnerabilities: parseBoolean(hasVulnerabilities), hasVulnerabilities: parseBoolean(hasVulnerabilities),
scanners: scanners ? JSON.parse(scanners) : [], scanners: scanners ? JSON.parse(scanners) : [],
hasJiraVulnerabilitiesIntegrationEnabled: parseBoolean(
hasJiraVulnerabilitiesIntegrationEnabled,
),
}; };
const props = { const props = {
......
...@@ -12,6 +12,7 @@ query project( ...@@ -12,6 +12,7 @@ query project(
$sort: VulnerabilitySort $sort: VulnerabilitySort
$hasIssues: Boolean $hasIssues: Boolean
$hasResolution: Boolean $hasResolution: Boolean
$includeExternalIssueLinks: Boolean = false
) { ) {
project(fullPath: $fullPath) { project(fullPath: $fullPath) {
vulnerabilities( vulnerabilities(
...@@ -27,6 +28,16 @@ query project( ...@@ -27,6 +28,16 @@ query project(
) { ) {
nodes { nodes {
...Vulnerability ...Vulnerability
externalIssueLinks @include(if: $includeExternalIssueLinks) {
nodes {
issue: externalIssue {
externalTracker
webUrl
title
iid: relativeReference
}
}
}
hasSolutions hasSolutions
mergeRequest { mergeRequest {
webUrl webUrl
......
<script> <script>
import { GlIcon, GlLink, GlTooltipDirective } from '@gitlab/ui'; import { GlIcon, GlLink, GlTooltipDirective, GlSafeHtmlDirective } from '@gitlab/ui';
import jiraLogo from '@gitlab/svgs/dist/illustrations/logos/jira.svg';
export default { export default {
components: { components: {
...@@ -8,13 +9,24 @@ export default { ...@@ -8,13 +9,24 @@ export default {
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
SafeHtml: GlSafeHtmlDirective,
}, },
props: { props: {
issue: { issue: {
type: Object, type: Object,
required: true, required: true,
}, },
isJira: {
type: Boolean,
required: false,
},
},
computed: {
iconName() {
return this.issue.state === this.$options.STATE_OPENED ? 'issue-open-m' : 'issue-close';
},
}, },
jiraLogo,
STATE_OPENED: 'opened', STATE_OPENED: 'opened',
}; };
</script> </script>
...@@ -22,14 +34,22 @@ export default { ...@@ -22,14 +34,22 @@ export default {
<gl-link <gl-link
v-gl-tooltip="issue.title" v-gl-tooltip="issue.title"
:href="issue.webUrl" :href="issue.webUrl"
:data-testid="`issue-link-${issue.iid}`" target="__blank"
class="d-inline-flex align-items-center gl-flex-shrink-0" class="gl-display-inline-flex gl-align-items-center gl-flex-shrink-0"
> >
<span
v-if="isJira"
v-safe-html="$options.jiraLogo"
class="gl-min-h-6 gl-mr-3 gl-display-inline-flex gl-align-items-center"
data-testid="jira-logo"
></span>
<gl-icon <gl-icon
class="mr-1" v-else
:class="{ cgreen: issue.state === $options.STATE_OPENED }" class="gl-mr-1"
:name="issue.state === $options.STATE_OPENED ? 'issue-open-m' : 'issue-close'" :class="{ 'gl-text-green-600': issue.state === $options.STATE_OPENED }"
:name="iconName"
/> />
#{{ issue.iid }} #{{ issue.iid }}
<gl-icon v-if="isJira" :size="12" name="external-link" class="gl-ml-1" />
</gl-link> </gl-link>
</template> </template>
...@@ -236,6 +236,7 @@ module EE ...@@ -236,6 +236,7 @@ module EE
if project.vulnerabilities.none? if project.vulnerabilities.none?
{ {
has_vulnerabilities: 'false', has_vulnerabilities: 'false',
has_jira_vulnerabilities_integration_enabled: project.configured_to_create_issues_from_vulnerabilities?.to_s,
empty_state_svg_path: image_path('illustrations/security-dashboard_empty.svg'), empty_state_svg_path: image_path('illustrations/security-dashboard_empty.svg'),
security_dashboard_help_path: help_page_path('user/application_security/security_dashboard/index'), security_dashboard_help_path: help_page_path('user/application_security/security_dashboard/index'),
no_vulnerabilities_svg_path: image_path('illustrations/issues.svg'), no_vulnerabilities_svg_path: image_path('illustrations/issues.svg'),
...@@ -244,6 +245,7 @@ module EE ...@@ -244,6 +245,7 @@ module EE
else else
{ {
has_vulnerabilities: 'true', has_vulnerabilities: 'true',
has_jira_vulnerabilities_integration_enabled: project.configured_to_create_issues_from_vulnerabilities?.to_s,
project: { id: project.id, name: project.name }, project: { id: project.id, name: project.name },
project_full_path: project.full_path, project_full_path: project.full_path,
vulnerabilities_export_endpoint: api_v4_security_projects_vulnerability_exports_path(id: project.id), vulnerabilities_export_endpoint: api_v4_security_projects_vulnerability_exports_path(id: project.id),
......
...@@ -37,7 +37,7 @@ module VulnerabilitiesHelper ...@@ -37,7 +37,7 @@ module VulnerabilitiesHelper
end end
def create_jira_issue_url_for(vulnerability) def create_jira_issue_url_for(vulnerability)
return unless vulnerability.project.jira_vulnerabilities_integration_enabled? return unless vulnerability.project.configured_to_create_issues_from_vulnerabilities?
decorated_vulnerability = vulnerability.present decorated_vulnerability = vulnerability.present
summary = _('Investigate vulnerability: %{title}') % { title: decorated_vulnerability.title } summary = _('Investigate vulnerability: %{title}') % { title: decorated_vulnerability.title }
......
...@@ -200,7 +200,7 @@ module EE ...@@ -200,7 +200,7 @@ module EE
delegate :auto_rollback_enabled, :auto_rollback_enabled=, :auto_rollback_enabled?, to: :ci_cd_settings delegate :auto_rollback_enabled, :auto_rollback_enabled=, :auto_rollback_enabled?, to: :ci_cd_settings
delegate :closest_gitlab_subscription, to: :namespace delegate :closest_gitlab_subscription, to: :namespace
delegate :jira_vulnerabilities_integration_enabled?, to: :jira_service, allow_nil: true delegate :jira_vulnerabilities_integration_enabled?, :configured_to_create_issues_from_vulnerabilities?, to: :jira_service, allow_nil: true
delegate :requirements_access_level, :security_and_compliance_access_level, to: :project_feature, allow_nil: true delegate :requirements_access_level, :security_and_compliance_access_level, to: :project_feature, allow_nil: true
delegate :pipeline_configuration_full_path, to: :compliance_management_framework, allow_nil: true delegate :pipeline_configuration_full_path, to: :compliance_management_framework, allow_nil: true
......
...@@ -24,7 +24,10 @@ describe('First Class Group Dashboard Vulnerabilities Component', () => { ...@@ -24,7 +24,10 @@ describe('First Class Group Dashboard Vulnerabilities Component', () => {
$apollo, $apollo,
fetchNextPage: () => {}, fetchNextPage: () => {},
}, },
provide: { hasVulnerabilities: true }, provide: {
hasVulnerabilities: true,
hasJiraVulnerabilitiesIntegrationEnabled: false,
},
}); });
}; };
......
...@@ -47,7 +47,10 @@ describe('First Class Instance Dashboard Vulnerabilities Component', () => { ...@@ -47,7 +47,10 @@ describe('First Class Instance Dashboard Vulnerabilities Component', () => {
fetchNextPage: () => {}, fetchNextPage: () => {},
}, },
data, data,
provide: { hasVulnerabilities: true }, provide: {
hasVulnerabilities: true,
hasJiraVulnerabilitiesIntegrationEnabled: false,
},
}); });
}; };
......
...@@ -5,12 +5,13 @@ import IssueLink from 'ee/vulnerabilities/components/issue_link.vue'; ...@@ -5,12 +5,13 @@ import IssueLink from 'ee/vulnerabilities/components/issue_link.vue';
describe('Remediated badge component', () => { describe('Remediated badge component', () => {
const issues = [{ issue: { iid: 41 } }, { issue: { iid: 591 } }]; const issues = [{ issue: { iid: 41 } }, { issue: { iid: 591 } }];
let wrapper; let wrapper;
const findIcon = () => wrapper.find(GlIcon); const findIcon = () => wrapper.findComponent(GlIcon);
const findBadge = () => wrapper.find(GlBadge); const findBadge = () => wrapper.findComponent(GlBadge);
const findIssueLink = () => wrapper.findAll(IssueLink); const findIssueLinks = () => wrapper.findAllComponents(IssueLink);
const findPopover = () => wrapper.find(GlPopover); const findPopover = () => wrapper.findComponent(GlPopover);
const createWrapper = ({ propsData }) => { const createWrapper = ({ propsData }) => {
return shallowMount(IssuesBadge, { propsData, stubs: { GlPopover, GlBadge } }); return shallowMount(IssuesBadge, { propsData, stubs: { GlPopover, GlBadge } });
...@@ -23,7 +24,7 @@ describe('Remediated badge component', () => { ...@@ -23,7 +24,7 @@ describe('Remediated badge component', () => {
describe('when there are multiple issues', () => { describe('when there are multiple issues', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createWrapper({ propsData: { issues } }); wrapper = createWrapper({ propsData: { issues, externalIssues: [] } });
}); });
it('displays the correct icon', () => { it('displays the correct icon', () => {
...@@ -36,7 +37,7 @@ describe('Remediated badge component', () => { ...@@ -36,7 +37,7 @@ describe('Remediated badge component', () => {
}); });
it('displays the issues', () => { it('displays the issues', () => {
expect(findIssueLink()).toHaveLength(issues.length); expect(findIssueLinks()).toHaveLength(issues.length);
}); });
it('displays the correct number of issues in the badge', () => { it('displays the correct number of issues in the badge', () => {
...@@ -50,7 +51,7 @@ describe('Remediated badge component', () => { ...@@ -50,7 +51,7 @@ describe('Remediated badge component', () => {
describe('when there are no issues', () => { describe('when there are no issues', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createWrapper({ propsData: { issues: [] } }); wrapper = createWrapper({ propsData: { issues: [], externalIssues: [] } });
}); });
it('displays the correct number of issues in the badge', () => { it('displays the correct number of issues in the badge', () => {
...@@ -61,4 +62,18 @@ describe('Remediated badge component', () => { ...@@ -61,4 +62,18 @@ describe('Remediated badge component', () => {
expect(findPopover().text()).toBe('0 Issues'); expect(findPopover().text()).toBe('0 Issues');
}); });
}); });
describe.each([true, false])('with "isJira" prop set to "%s"', (isJira) => {
beforeEach(() => {
wrapper = createWrapper({
propsData: { issues, isJira },
});
});
it('passes the correct prop to the issue link', () => {
findIssueLinks().wrappers.forEach((issueLink) => {
expect(issueLink.props('isJira')).toBe(isJira);
});
});
});
}); });
...@@ -37,6 +37,9 @@ export const generateVulnerabilities = () => [ ...@@ -37,6 +37,9 @@ export const generateVulnerabilities = () => [
issueLinks: { issueLinks: {
nodes: [{ issue: { iid: 15 } }], nodes: [{ issue: { iid: 15 } }],
}, },
externalIssueLinks: {
nodes: [{ issue: { iid: 15, externalTracker: 'jira' } }],
},
}, },
{ {
id: 'id_1', id: 'id_1',
......
...@@ -14,6 +14,7 @@ describe('Vulnerabilities app component', () => { ...@@ -14,6 +14,7 @@ describe('Vulnerabilities app component', () => {
wrapper = shallowMount(ProjectVulnerabilitiesApp, { wrapper = shallowMount(ProjectVulnerabilitiesApp, {
provide: { provide: {
projectFullPath: '#', projectFullPath: '#',
hasJiraVulnerabilitiesIntegrationEnabled: false,
}, },
propsData: { propsData: {
dashboardDocumentation: '#', dashboardDocumentation: '#',
......
...@@ -11,6 +11,7 @@ import VulnerabilityList, { ...@@ -11,6 +11,7 @@ import VulnerabilityList, {
SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY, SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY,
} from 'ee/security_dashboard/components/vulnerability_list.vue'; } from 'ee/security_dashboard/components/vulnerability_list.vue';
import RemediatedBadge from 'ee/vulnerabilities/components/remediated_badge.vue'; import RemediatedBadge from 'ee/vulnerabilities/components/remediated_badge.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
import { generateVulnerabilities, vulnerabilities } from './mock_data'; import { generateVulnerabilities, vulnerabilities } from './mock_data';
...@@ -20,46 +21,52 @@ describe('Vulnerability list component', () => { ...@@ -20,46 +21,52 @@ describe('Vulnerability list component', () => {
let wrapper; let wrapper;
const createWrapper = ({ props = {}, listeners } = {}) => { const createWrapper = ({ props = {}, listeners, provide = {} } = {}) => {
return mount(VulnerabilityList, { return extendedWrapper(
propsData: { mount(VulnerabilityList, {
vulnerabilities: [], propsData: {
...props, vulnerabilities: [],
}, ...props,
stubs: { },
GlPopover: true, stubs: {
}, GlPopover: true,
listeners, },
provide: () => ({ listeners,
noVulnerabilitiesSvgPath: '#', provide: () => ({
dashboardDocumentation: '#', noVulnerabilitiesSvgPath: '#',
emptyStateSvgPath: '#', dashboardDocumentation: '#',
notEnabledScannersHelpPath: '#', emptyStateSvgPath: '#',
noPipelineRunScannersHelpPath: '#', notEnabledScannersHelpPath: '#',
hasVulnerabilities: true, noPipelineRunScannersHelpPath: '#',
hasVulnerabilities: true,
hasJiraVulnerabilitiesIntegrationEnabled: false,
...provide,
}),
}), }),
}); );
}; };
const findTable = () => wrapper.find(GlTable); const findTable = () => wrapper.findComponent(GlTable);
const findSortableColumn = () => wrapper.find('[aria-sort="descending"]'); const findSortableColumn = () => wrapper.find('[aria-sort="descending"]');
const findCell = (label) => wrapper.find(`.js-${label}`); const findCell = (label) => wrapper.find(`.js-${label}`);
const findRows = () => wrapper.findAll('tbody tr'); const findRows = () => wrapper.findAll('tbody tr');
const findRow = (index = 0) => findRows().at(index); const findRow = (index = 0) => findRows().at(index);
const findRowById = (id) => wrapper.find(`tbody tr[data-pk="${id}"`); const findRowById = (id) => wrapper.find(`tbody tr[data-pk="${id}"`);
const findAutoFixBulbInRow = (row) => row.find('[data-testid="vulnerability-solutions-bulb"]'); const findAutoFixBulbInRow = (row) => row.find('[data-testid="vulnerability-solutions-bulb"]');
const findIssuesBadge = (index = 0) => wrapper.findAll(IssuesBadge).at(index); const findIssuesBadge = (index = 0) => wrapper.findAllComponents(IssuesBadge).at(index);
const findRemediatedBadge = () => wrapper.find(RemediatedBadge); const findRemediatedBadge = () => wrapper.findComponent(RemediatedBadge);
const findSecurityScannerAlert = () => wrapper.find(SecurityScannerAlert); const findSecurityScannerAlert = () => wrapper.findComponent(SecurityScannerAlert);
const findDismissalButton = () => findSecurityScannerAlert().find('button[aria-label="Dismiss"]'); const findDismissalButton = () => findSecurityScannerAlert().find('button[aria-label="Dismiss"]');
const findSelectionSummary = () => wrapper.find(SelectionSummary); const findSelectionSummary = () => wrapper.findComponent(SelectionSummary);
const findRowVulnerabilityCommentIcon = (row) => findRow(row).find(VulnerabilityCommentIcon); const findRowVulnerabilityCommentIcon = (row) =>
const findDataCell = (label) => wrapper.find(`[data-testid="${label}"]`); findRow(row).findComponent(VulnerabilityCommentIcon);
const findDataCell = (label) => wrapper.findByTestId(label);
const findDataCells = (label) => wrapper.findAll(`[data-testid="${label}"]`); const findDataCells = (label) => wrapper.findAll(`[data-testid="${label}"]`);
const findLocationTextWrapper = (cell) => cell.find(GlTruncate); const findLocationTextWrapper = (cell) => cell.find(GlTruncate);
const findFiltersProducedNoResults = () => wrapper.find(FiltersProducedNoResults); const findFiltersProducedNoResults = () => wrapper.findComponent(FiltersProducedNoResults);
const findDashboardHasNoVulnerabilities = () => wrapper.find(DashboardHasNoVulnerabilities); const findDashboardHasNoVulnerabilities = () =>
const findVendorNames = () => wrapper.find(`[data-testid="vulnerability-vendor"]`); wrapper.findComponent(DashboardHasNoVulnerabilities);
const findVendorNames = () => wrapper.findByTestId('vulnerability-vendor');
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -94,14 +101,6 @@ describe('Vulnerability list component', () => { ...@@ -94,14 +101,6 @@ describe('Vulnerability list component', () => {
expect(cell.text()).toBe(newVulnerabilities[0].title); expect(cell.text()).toBe(newVulnerabilities[0].title);
}); });
it('should display the issues badge for the first item', () => {
expect(findIssuesBadge(0).exists()).toBe(true);
});
it('should not display the issues badge for the second item', () => {
expect(() => findIssuesBadge(1)).toThrow();
});
it('should display the remediated badge', () => { it('should display the remediated badge', () => {
expect(findRemediatedBadge().exists()).toBe(true); expect(findRemediatedBadge().exists()).toBe(true);
}); });
...@@ -182,6 +181,30 @@ describe('Vulnerability list component', () => { ...@@ -182,6 +181,30 @@ describe('Vulnerability list component', () => {
expect(checkbox().element.checked).toBe(false); expect(checkbox().element.checked).toBe(false);
}); });
describe.each([true, false])(
'issues badge when "hasJiraVulnerabilitiesIntegrationEnabled" is set to "%s"',
(hasJiraVulnerabilitiesIntegrationEnabled) => {
beforeEach(() => {
wrapper = createWrapper({
props: { vulnerabilities: generateVulnerabilities() },
provide: { hasJiraVulnerabilitiesIntegrationEnabled },
});
});
it('should display the issues badge for the first item', () => {
expect(findIssuesBadge(0).exists()).toBe(true);
});
it('should not display the issues badge for the second item', () => {
expect(() => findIssuesBadge(1)).toThrow();
});
it('should render the badge as Jira issues', () => {
expect(findIssuesBadge(0).props('isJira')).toBe(hasJiraVulnerabilitiesIntegrationEnabled);
});
},
);
}); });
describe('when vulnerability selection is disabled', () => { describe('when vulnerability selection is disabled', () => {
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlIcon, GlLink } from '@gitlab/ui';
import IssueLink from 'ee/vulnerabilities/components/issue_link.vue'; import IssueLink from 'ee/vulnerabilities/components/issue_link.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { getBinding, createMockDirective } from 'helpers/vue_mock_directive'; import { getBinding, createMockDirective } from 'helpers/vue_mock_directive';
describe('IssueLink component', () => { describe('IssueLink component', () => {
...@@ -13,48 +15,71 @@ describe('IssueLink component', () => { ...@@ -13,48 +15,71 @@ describe('IssueLink component', () => {
}); });
const createWrapper = ({ propsData }) => { const createWrapper = ({ propsData }) => {
return shallowMount(IssueLink, { return extendedWrapper(
propsData, shallowMount(IssueLink, {
directives: { propsData,
GlTooltip: createMockDirective(), directives: {
}, GlTooltip: createMockDirective(),
}); },
}),
);
}; };
const findIssueLink = (id) => wrapper.find(`[data-testid="issue-link-${id}"]`); const findIssueLink = () => wrapper.findComponent(GlLink);
const findIssueWithState = (state) => const findIcon = () => wrapper.findComponent(GlIcon);
wrapper.find(state === 'opened' ? 'issue-open-m' : 'issue-close');
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
}); });
describe.each` describe.each([true, false])('internal and Jira issues with "isJira" set to "%s"', (isJira) => {
state | icon const issue = createIssue();
${'opened'} | ${'issue-open-m'}
${'closed'} | ${'issue-close'} beforeEach(() => {
`('when issue link is mounted', ({ state }) => { wrapper = createWrapper({ propsData: { issue, isJira } });
describe(`with state ${state}`, () => { });
const issue = createIssue({ state });
it('should contain a link to the issue', () => {
expect(findIssueLink().attributes('href')).toBe(issue.webUrl);
});
it('should contain the title', () => {
const tooltip = getBinding(findIssueLink().element, 'gl-tooltip');
expect(tooltip).toBeDefined();
expect(tooltip.value).toBe(issue.title);
});
});
describe('internal issues', () => {
describe.each`
state | icon
${'opened'} | ${'issue-open-m'}
${'closed'} | ${'issue-close'}
`('with state "$state"', ({ state, icon }) => {
beforeEach(() => { beforeEach(() => {
wrapper = createWrapper({ propsData: { issue } }); wrapper = createWrapper({ propsData: { issue: createIssue({ state }) } });
}); });
it('should contain the correct issue icon', () => { it('should contain the correct issue icon', () => {
expect(findIssueWithState(state)).toBeTruthy(); expect(findIcon().attributes('name')).toBe(icon);
}); });
});
});
it('should contain a link to the issue', () => { describe('Jira issues', () => {
expect(findIssueLink(issue.iid).attributes('href')).toBe(issue.webUrl); beforeEach(() => {
wrapper = createWrapper({
propsData: { issue: createIssue(), isJira: true },
}); });
});
it('should contain the title', () => { it('should contain a Jira logo icon', () => {
const tooltip = getBinding(findIssueLink(issue.iid).element, 'gl-tooltip'); expect(wrapper.findByTestId('jira-logo').exists()).toBe(true);
expect(tooltip).toBeDefined(); });
expect(tooltip.value).toBe(issue.title);
}); it('should contain an external-link icon', () => {
expect(findIcon().attributes('name')).toBe('external-link');
}); });
}); });
}); });
...@@ -119,11 +119,13 @@ RSpec.describe ProjectsHelper do ...@@ -119,11 +119,13 @@ RSpec.describe ProjectsHelper do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) } let_it_be(:project) { create(:project, :repository, group: group) }
let_it_be(:jira_service) { create(:jira_service, project: project, vulnerabilities_enabled: true, project_key: 'GV', vulnerabilities_issuetype: '10000') }
subject { helper.project_security_dashboard_config(project) } subject { helper.project_security_dashboard_config(project) }
before do before do
group.add_owner(user) group.add_owner(user)
stub_licensed_features(jira_vulnerabilities_integration: true)
allow(helper).to receive(:current_user).and_return(user) allow(helper).to receive(:current_user).and_return(user)
end end
...@@ -131,6 +133,7 @@ RSpec.describe ProjectsHelper do ...@@ -131,6 +133,7 @@ RSpec.describe ProjectsHelper do
let(:expected_value) do let(:expected_value) do
{ {
has_vulnerabilities: 'false', has_vulnerabilities: 'false',
has_jira_vulnerabilities_integration_enabled: 'true',
empty_state_svg_path: start_with('/assets/illustrations/security-dashboard_empty'), empty_state_svg_path: start_with('/assets/illustrations/security-dashboard_empty'),
security_dashboard_help_path: '/help/user/application_security/security_dashboard/index', security_dashboard_help_path: '/help/user/application_security/security_dashboard/index',
project_full_path: project.full_path, project_full_path: project.full_path,
...@@ -145,6 +148,7 @@ RSpec.describe ProjectsHelper do ...@@ -145,6 +148,7 @@ RSpec.describe ProjectsHelper do
let(:base_values) do let(:base_values) do
{ {
has_vulnerabilities: 'true', has_vulnerabilities: 'true',
has_jira_vulnerabilities_integration_enabled: 'true',
project: { id: project.id, name: project.name }, project: { id: project.id, name: project.name },
project_full_path: project.full_path, project_full_path: project.full_path,
vulnerabilities_export_endpoint: "/api/v4/security/projects/#{project.id}/vulnerability_exports", vulnerabilities_export_endpoint: "/api/v4/security/projects/#{project.id}/vulnerability_exports",
......
...@@ -176,6 +176,7 @@ RSpec.describe VulnerabilitiesHelper do ...@@ -176,6 +176,7 @@ RSpec.describe VulnerabilitiesHelper do
context 'with jira vulnerabilities integration enabled' do context 'with jira vulnerabilities integration enabled' do
before do before do
allow(project).to receive(:jira_vulnerabilities_integration_enabled?).and_return(true) allow(project).to receive(:jira_vulnerabilities_integration_enabled?).and_return(true)
allow(project).to receive(:configured_to_create_issues_from_vulnerabilities?).and_return(true)
end end
let(:expected_jira_issue_description) do let(:expected_jira_issue_description) do
...@@ -237,6 +238,7 @@ RSpec.describe VulnerabilitiesHelper do ...@@ -237,6 +238,7 @@ RSpec.describe VulnerabilitiesHelper do
context 'with jira vulnerabilities integration disabled' do context 'with jira vulnerabilities integration disabled' do
before do before do
allow(project).to receive(:jira_vulnerabilities_integration_enabled?).and_return(false) allow(project).to receive(:jira_vulnerabilities_integration_enabled?).and_return(false)
allow(project).to receive(:configured_to_create_issues_from_vulnerabilities?).and_return(false)
end end
it { expect(subject[:create_jira_issue_url]).to be_nil } it { expect(subject[:create_jira_issue_url]).to be_nil }
......
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