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