Commit 51eb87ee authored by Dave Pisek's avatar Dave Pisek

Show Jira issues count on vulnerabilities report

If the Jira vulnerabilities integration is enabled this change
will make sure that the issues-count badge on the Vulnerabilities
Report will display data from Jira issues.
parent f34aebd7
...@@ -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,8 +45,10 @@ export default { ...@@ -40,8 +45,10 @@ export default {
<template #title> <template #title>
{{ popoverTitle }} {{ popoverTitle }}
</template> </template>
<div>
<div v-for="{ issue } in issues" :key="issue.iid"> <div v-for="{ issue } in issues" :key="issue.iid">
<issue-link :issue="issue" /> <issue-link :issue="issue" :is-jira="isJira" />
</div>
</div> </div>
</gl-popover> </gl-popover>
</div> </div>
......
...@@ -16,7 +16,7 @@ export default { ...@@ -16,7 +16,7 @@ export default {
GlIntersectionObserver, GlIntersectionObserver,
VulnerabilityList, VulnerabilityList,
}, },
inject: ['projectFullPath'], inject: ['projectFullPath', 'hasJiraVulnerabilitiesIntegrationEnabled'],
props: { props: {
filters: { filters: {
type: Object, type: Object,
...@@ -42,6 +42,7 @@ export default { ...@@ -42,6 +42,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,7 @@ export default { ...@@ -52,7 +52,7 @@ export default {
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
inject: ['hasVulnerabilities'], inject: ['hasVulnerabilities', 'hasJiraVulnerabilitiesIntegrationEnabled'],
props: { props: {
filters: { filters: {
type: Object, type: Object,
...@@ -275,9 +275,20 @@ export default { ...@@ -275,9 +275,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 +451,11 @@ export default { ...@@ -440,7 +451,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,19 @@ export default { ...@@ -22,14 +34,19 @@ 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"
> >
<gl-icon <span class="gl-mr-3">
class="mr-1" <span
:class="{ cgreen: issue.state === $options.STATE_OPENED }" v-if="isJira"
:name="issue.state === $options.STATE_OPENED ? 'issue-open-m' : 'issue-close'" v-safe-html="$options.jiraLogo"
/> class="gl-min-h-6 gl-display-inline-flex gl-align-items-center"
data-testid="jira-logo"
></span>
<gl-icon v-else :class="{ cgreen: issue.state === $options.STATE_OPENED }" :name="iconName" />
</span>
#{{ issue.iid }} #{{ issue.iid }}
<gl-icon v-if="isJira" :size="12" name="external-link" class="gl-ml-1" />
</gl-link> </gl-link>
</template> </template>
...@@ -235,6 +235,7 @@ module EE ...@@ -235,6 +235,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'),
...@@ -243,6 +244,7 @@ module EE ...@@ -243,6 +244,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 }
......
...@@ -198,7 +198,7 @@ module EE ...@@ -198,7 +198,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,8 +21,9 @@ describe('Vulnerability list component', () => { ...@@ -20,8 +21,9 @@ describe('Vulnerability list component', () => {
let wrapper; let wrapper;
const createWrapper = ({ props = {}, listeners } = {}) => { const createWrapper = ({ props = {}, listeners, provide = {} } = {}) => {
return mount(VulnerabilityList, { return extendedWrapper(
mount(VulnerabilityList, {
propsData: { propsData: {
vulnerabilities: [], vulnerabilities: [],
...props, ...props,
...@@ -37,29 +39,34 @@ describe('Vulnerability list component', () => { ...@@ -37,29 +39,34 @@ describe('Vulnerability list component', () => {
notEnabledScannersHelpPath: '#', notEnabledScannersHelpPath: '#',
noPipelineRunScannersHelpPath: '#', noPipelineRunScannersHelpPath: '#',
hasVulnerabilities: true, 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(
shallowMount(IssueLink, {
propsData, propsData,
directives: { directives: {
GlTooltip: createMockDirective(), 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([true, false])('both internal and Jira issues', (isJira) => {
const issue = createIssue();
beforeEach(() => {
wrapper = createWrapper({ propsData: { issue, isJira } });
});
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('with internal issues', () => {
describe.each` describe.each`
state | icon state | icon
${'opened'} | ${'issue-open-m'} ${'opened'} | ${'issue-open-m'}
${'closed'} | ${'issue-close'} ${'closed'} | ${'issue-close'}
`('when issue link is mounted', ({ state }) => { `('with state "$state"', ({ state, icon }) => {
describe(`with state ${state}`, () => {
const issue = createIssue({ state });
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('with 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