From fecceb9c7bed9df99478ddd6d993b68cd031041c Mon Sep 17 00:00:00 2001 From: Savas Vedova <svedova@gitlab.com> Date: Thu, 9 Mar 2023 12:21:44 +0000 Subject: [PATCH] Request finding data on modal click Fetch issue feedback when a finding is clicked and patch it to the existing element. --- .../graphql/queries/mr_widget_finding.graphql | 38 ++++++++++++++++++ .../extensions/security_reports/i18n.js | 3 ++ .../mr_widget_security_reports.vue | 37 ++++++++++++++++++ .../extensions/security_reports/mock_data.js | 39 +++++++++++++++++++ .../mr_widget_security_reports_spec.js | 37 ++++++++++++++++-- locale/gitlab.pot | 3 ++ 6 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 ee/app/assets/javascripts/security_dashboard/graphql/queries/mr_widget_finding.graphql diff --git a/ee/app/assets/javascripts/security_dashboard/graphql/queries/mr_widget_finding.graphql b/ee/app/assets/javascripts/security_dashboard/graphql/queries/mr_widget_finding.graphql new file mode 100644 index 0000000000000..d31fc03074108 --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/graphql/queries/mr_widget_finding.graphql @@ -0,0 +1,38 @@ +query pipelineFinding($fullPath: ID!, $pipelineId: ID!, $uuid: String!) { + project(fullPath: $fullPath) { + id + pipeline(iid: $pipelineId) { + id + securityReportFinding(uuid: $uuid) { + id: uuid + stateComment + dismissedAt + dismissedBy { + id + } + mergeRequest { + id + webUrl + } + issueLinks { + nodes { + id + linkType + issue { + id + iid + webUrl + createdAt + author { + id + name + username + webUrl + } + } + } + } + } + } + } +} diff --git a/ee/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/i18n.js b/ee/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/i18n.js index 1d0be98fdd70e..efddaf2ea06fd 100644 --- a/ee/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/i18n.js +++ b/ee/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/i18n.js @@ -30,6 +30,9 @@ export const i18n = { ), noNewVulnerabilities: s__('ciReport|%{scanner} detected no new potential vulnerabilities'), newVulnerabilities: s__('ciReport|%{scanner} detected %{number} new potential %{vulnStr}'), + findingLoadingError: s__( + 'ciReport|Something went wrong while fetching the finding. Please try again later.', + ), }; export const popovers = { diff --git a/ee/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue b/ee/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue index fb719f2975016..33c6b99580237 100644 --- a/ee/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue +++ b/ee/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue @@ -11,6 +11,7 @@ import { visitUrl } from '~/lib/utils/url_utility'; import { s__, sprintf } from '~/locale'; import FindingModal from 'ee/vue_shared/security_reports/components/modal.vue'; import { VULNERABILITY_MODAL_ID } from 'ee/vue_shared/security_reports/components/constants'; +import findingQuery from 'ee/security_dashboard/graphql/queries/mr_widget_finding.graphql'; import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants'; import { capitalizeFirstCharacter, convertToCamelCase } from '~/lib/utils/text_utility'; import { helpPagePath } from '~/helpers/help_page_helper'; @@ -56,6 +57,42 @@ export default { }, }; }, + + apollo: { + securityReportFinding: { + manual: true, + query: findingQuery, + variables() { + return { + fullPath: this.mr.sourceProjectFullPath, + pipelineId: this.modalData.vulnerability.found_by_pipeline?.iid, + uuid: this.modalData.vulnerability.uuid, + }; + }, + error() { + this.modalData.error = this.$options.i18n.findingLoadingError; + }, + result({ data }) { + const issue = data.project.pipeline.securityReportFinding.issueLinks.nodes.find( + (x) => x.linkType === 'CREATED', + )?.issue; + + if (issue) { + this.$set(this.modalData.vulnerability, 'hasIssue', true); + this.$set(this.modalData.vulnerability, 'issue_feedback', { + author: issue.author, + created_at: issue.createdAt, + issue_url: issue.webUrl, + issue_iid: issue.iid, + }); + } + }, + skip() { + return !this.modalData; + }, + }, + }, + computed: { helpPopovers() { return { diff --git a/ee/spec/frontend/vue_merge_request_widget/extensions/security_reports/mock_data.js b/ee/spec/frontend/vue_merge_request_widget/extensions/security_reports/mock_data.js index 96c5e6ed863b4..6850ad34732d6 100644 --- a/ee/spec/frontend/vue_merge_request_widget/extensions/security_reports/mock_data.js +++ b/ee/spec/frontend/vue_merge_request_widget/extensions/security_reports/mock_data.js @@ -441,3 +441,42 @@ export const emptyResponse = () => ({ added: [], fixed: [], }); + +export const findingQueryMockData = () => + jest.fn().mockResolvedValue({ + data: { + project: { + id: 'gid://gitlab/Project/11', + pipeline: { + id: 'gid://gitlab/Ci::Pipeline/13', + securityReportFinding: { + id: 'b58fadc9-e4af-5404-ba6b-e4ee14801c6f', + stateComment: null, + dismissedAt: null, + dismissedBy: null, + mergeRequest: null, + issueLinks: { + nodes: [ + { + id: 'git://gitlab/IssueLink/1', + linkType: 'CREATED', + issue: { + id: 'gid://gitlab/Issue/2', + iid: '2', + webUrl: 'http://gdk.test:3000/root/security-reports-v2/-/issues/2', + createdAt: '2023-03-07T10:50:09Z', + author: { + id: 'gid://gitlab/User/1', + name: 'Administrator', + username: 'root', + webUrl: 'http://gdk.test:3000/root', + }, + }, + }, + ], + }, + }, + }, + }, + }, + }); diff --git a/ee/spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js b/ee/spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js index 59aef50da7f35..0e8cb1952c6ca 100644 --- a/ee/spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js +++ b/ee/spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js @@ -1,12 +1,15 @@ import { GlBadge } from '@gitlab/ui'; -import { nextTick } from 'vue'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; import MockAdapter from 'axios-mock-adapter'; import waitForPromises from 'helpers/wait_for_promises'; import MRSecurityWidget from 'ee/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue'; import FindingModal from 'ee/vue_shared/security_reports/components/modal.vue'; import SummaryText from 'ee/vue_merge_request_widget/extensions/security_reports/summary_text.vue'; import SummaryHighlights from 'ee/vue_merge_request_widget/extensions/security_reports/summary_highlights.vue'; +import findingQuery from 'ee/security_dashboard/graphql/queries/mr_widget_finding.graphql'; import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; import Widget from '~/vue_merge_request_widget/components/widget/widget.vue'; import toast from '~/vue_shared/plugins/global_toast'; import download from '~/lib/utils/downloader'; @@ -15,11 +18,14 @@ import * as urlUtils from '~/lib/utils/url_utility'; import { BV_HIDE_MODAL } from '~/lib/utils/constants'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status'; +import { findingQueryMockData } from './mock_data'; jest.mock('~/vue_shared/components/user_callout_dismisser.vue', () => ({ render: () => {} })); jest.mock('~/vue_shared/plugins/global_toast'); jest.mock('~/lib/utils/downloader'); +Vue.use(VueApollo); + describe('MR Widget Security Reports', () => { let wrapper; let mockAxios; @@ -51,6 +57,7 @@ describe('MR Widget Security Reports', () => { const createComponent = ({ propsData, mountFn = shallowMountExtended } = {}) => { wrapper = mountFn(MRSecurityWidget, { + apolloProvider: createMockApollo([[findingQuery, findingQueryMockData()]]), propsData: { ...propsData, mr: { @@ -362,7 +369,17 @@ describe('MR Widget Security Reports', () => { const mockWithData = (props) => { Object.keys(reportEndpoints).forEach((key, i) => { mockAxios.onGet(reportEndpoints[key]).replyOnce(HTTP_STATUS_OK, { - added: [{ uuid: i, severity: 'critical', name: 'Password leak', ...props }], + added: [ + { + uuid: i.toString(), + severity: 'critical', + name: 'Password leak', + found_by_pipeline: { + iid: 1, + }, + ...props, + }, + ], }); }); }; @@ -395,15 +412,27 @@ describe('MR Widget Security Reports', () => { expect(modal.props('canCreateIssue')).toBe(false); expect(modal.props('isDismissingVulnerability')).toBe(false); - expect(modal.props('modal')).toEqual({ + + await waitForPromises(); + + expect(modal.props('modal')).toMatchObject({ title: 'Password leak', error: null, isShowingDeleteButtons: false, vulnerability: { - uuid: 0, + uuid: '0', severity: 'critical', name: 'Password leak', isDismissed: false, + issue_feedback: { + author: { + name: 'Administrator', + username: 'root', + }, + created_at: '2023-03-07T10:50:09Z', + issue_iid: '2', + issue_url: 'http://gdk.test:3000/root/security-reports-v2/-/issues/2', + }, }, }); }); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index baa0fc0311766..7676fd27faf03 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -50902,6 +50902,9 @@ msgstr "" msgid "ciReport|Solution" msgstr "" +msgid "ciReport|Something went wrong while fetching the finding. Please try again later." +msgstr "" + msgid "ciReport|Static Application Security Testing (SAST)" msgstr "" -- GitLab