diff --git a/ee/app/assets/javascripts/vue_shared/security_reports/components/merge_request_note_graphql.vue b/ee/app/assets/javascripts/vue_shared/security_reports/components/merge_request_note_graphql.vue new file mode 100644 index 0000000000000000000000000000000000000000..bc2d564e6537224ad03aca6ff212ccd4a63599ce --- /dev/null +++ b/ee/app/assets/javascripts/vue_shared/security_reports/components/merge_request_note_graphql.vue @@ -0,0 +1,56 @@ +<script> +import { GlSprintf, GlLink } from '@gitlab/ui'; +import EventItem from 'ee/vue_shared/security_reports/components/event_item.vue'; +import { __ } from '~/locale'; + +export default { + components: { + EventItem, + GlSprintf, + GlLink, + }, + props: { + mergeRequest: { + type: Object, + required: false, + default: undefined, + }, + project: { + type: Object, + required: false, + default: undefined, + }, + }, + computed: { + eventText() { + return this.project + ? __('Created merge request %{mergeRequestLink} at %{projectLink}') + : __('Created merge request %{mergeRequestLink}'); + }, + }, +}; +</script> + +<template> + <div v-if="mergeRequest" class="card gl-my-6"> + <event-item + :author="mergeRequest.author" + :created-at="mergeRequest.createdAt" + icon-name="merge-request" + class="card-body" + > + <gl-sprintf :message="eventText"> + <template #mergeRequestLink> + <gl-link :href="mergeRequest.webUrl" data-testid="merge-request-link"> + #{{ mergeRequest.iid }} + </gl-link> + </template> + <template v-if="project" #projectLink> + <gl-link :href="project.webUrl" data-testid="project-link"> + {{ project.nameWithNamespace }} + </gl-link> + </template> + </gl-sprintf> + </event-item> + </div> +</template> diff --git a/ee/spec/frontend/vue_shared/security_reports/components/merge_request_note_graphql_spec.js b/ee/spec/frontend/vue_shared/security_reports/components/merge_request_note_graphql_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..bab61b07dd567b546aa2ce9dc651939552fcd9d3 --- /dev/null +++ b/ee/spec/frontend/vue_shared/security_reports/components/merge_request_note_graphql_spec.js @@ -0,0 +1,119 @@ +import { GlSprintf } from '@gitlab/ui'; +import MergeRequestNote from 'ee/vue_shared/security_reports/components/merge_request_note_graphql.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import EventItem from 'ee/vue_shared/security_reports/components/event_item.vue'; + +const TEST_MERGE_REQUEST = { + iid: 2, + createdAt: '2022-10-16T22:42:02.975Z', + webUrl: 'http://gdk.test:3000/secure-ex/security-reports/-/merge_requests/2', + author: { + __typename: 'UserCore', + id: 'gid://gitlab/User/1', + username: 'admin', + name: 'Administrator', + webUrl: 'http://gdk.test:3000/root', + }, +}; + +const TEST_PROJECT = { + webUrl: 'http://gdk.test:3000/secure-ex/security-reports', + nameWithNamespace: 'Secure Ex / Security Reports', +}; + +describe('MergeRequestNoteGraphQL', () => { + let wrapper; + + const createWrapper = ({ propsData } = {}) => { + wrapper = shallowMountExtended(MergeRequestNote, { + stubs: { GlSprintf }, + propsData, + }); + }; + + const findEventItem = () => wrapper.findComponent(EventItem); + const findMergeRequestLink = () => wrapper.findByTestId('merge-request-link'); + const findProjectLink = () => wrapper.findByTestId('project-link'); + + afterEach(() => { + wrapper.destroy(); + }); + + it('should not render if no merge request', () => { + createWrapper({ propsData: { mergeRequest: null } }); + + expect(wrapper.html()).toBe(''); + }); + + describe('with no attached project', () => { + beforeEach(() => { + createWrapper({ propsData: { mergeRequest: TEST_MERGE_REQUEST } }); + }); + + it('should pass the author to the event item', () => { + expect(findEventItem().props('author')).toBe(TEST_MERGE_REQUEST.author); + }); + + it('should pass the created date to the event item', () => { + expect(findEventItem().props('createdAt')).toBe(TEST_MERGE_REQUEST.createdAt); + }); + + it('should return the event text with no project data', () => { + expect(wrapper.text()).toMatchInterpolatedText( + `Created merge request #${TEST_MERGE_REQUEST.iid}`, + ); + }); + }); + + describe('with an attached project', () => { + beforeEach(() => { + createWrapper({ propsData: { mergeRequest: TEST_MERGE_REQUEST, project: TEST_PROJECT } }); + }); + + it('should return the event text with project data', () => { + expect(wrapper.text()).toMatchInterpolatedText( + `Created merge request #${TEST_MERGE_REQUEST.iid} at ${TEST_PROJECT.nameWithNamespace}`, + ); + }); + }); + + describe('links', () => { + describe.each` + type | component | href | text + ${'merge request'} | ${findMergeRequestLink} | ${TEST_MERGE_REQUEST.webUrl} | ${`#${TEST_MERGE_REQUEST.iid}`} + ${'project'} | ${findProjectLink} | ${TEST_PROJECT.webUrl} | ${TEST_PROJECT.nameWithNamespace} + `('for $type', ({ type, component, href, text }) => { + beforeEach(() => { + createWrapper({ + propsData: { + mergeRequest: TEST_MERGE_REQUEST, + ...(type === 'project' && { project: TEST_PROJECT }), + }, + }); + }); + + it(`should render a link to the ${type}`, () => { + expect(component().attributes('href')).toBe(href); + }); + + it(`should render the ${type} path as the text`, () => { + expect(component().text()).toBe(text); + }); + }); + }); + + describe('with unsafe data', () => { + const unsafeProject = { + ...TEST_PROJECT, + nameWithNamespace: 'Foo <script>alert("XSS")</script>', + }; + + beforeEach(() => { + createWrapper({ propsData: { mergeRequest: TEST_MERGE_REQUEST, project: unsafeProject } }); + }); + + it('should escape the project name', () => { + expect(wrapper.text()).toContain(unsafeProject.nameWithNamespace); + }); + }); +});