import { GlLink } from '@gitlab/ui'; import { getAllByRole, getByTestId } from '@testing-library/dom'; import { mount } from '@vue/test-utils'; import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue'; import VulnerabilityDetails from 'ee/vulnerabilities/components/vulnerability_details.vue'; import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants'; describe('Vulnerability Details', () => { let wrapper; const vulnerability = { severity: 'bad severity', confidence: 'high confidence', reportType: 'Some report type', description: 'vulnerability description', }; const createWrapper = (vulnerabilityOverrides) => { const propsData = { vulnerability: { ...vulnerability, ...vulnerabilityOverrides }, }; wrapper = mount(VulnerabilityDetails, { propsData }); }; const getById = (id) => wrapper.find(`[data-testid="${id}"]`); const getAllById = (id) => wrapper.findAll(`[data-testid="${id}"]`); const getText = (id) => getById(id).text(); afterEach(() => { wrapper.destroy(); }); it('shows the properties that should always be shown', () => { createWrapper(); expect(getText('description')).toBe(vulnerability.description); expect(wrapper.find(SeverityBadge).props('severity')).toBe(vulnerability.severity); expect(getText('reportType')).toBe(`Scan Type: ${vulnerability.reportType}`); expect(getById('title').exists()).toBe(false); expect(getById('image').exists()).toBe(false); expect(getById('os').exists()).toBe(false); expect(getById('file').exists()).toBe(false); expect(getById('class').exists()).toBe(false); expect(getById('method').exists()).toBe(false); expect(getById('evidence').exists()).toBe(false); expect(getById('scanner').exists()).toBe(false); expect(getAllById('link')).toHaveLength(0); expect(getAllById('identifier')).toHaveLength(0); }); it.each` reportType | expectedOutput ${'SAST'} | ${'SAST'} ${'DAST'} | ${'DAST'} ${'DEPENDENCY_SCANNING'} | ${'Dependency Scanning'} ${'CONTAINER_SCANNING'} | ${'Container Scanning'} ${'SECRET_DETECTION'} | ${'Secret Detection'} ${'COVERAGE_FUZZING'} | ${'Coverage Fuzzing'} ${'API_FUZZING'} | ${'API Fuzzing'} ${'CLUSTER_IMAGE_SCANNING'} | ${'Cluster Image Scanning'} `( 'displays "$expectedOutput" when report type is "$reportType"', ({ reportType, expectedOutput }) => { createWrapper({ reportType }); expect(getText('reportType')).toBe(`Scan Type: ${expectedOutput}`); }, ); it('shows the title if it exists', () => { createWrapper({ title: 'some title' }); expect(getText('title')).toBe('some title'); }); it('shows the location image if it exists', () => { createWrapper({ location: { image: 'some image' } }); expect(getText('image')).toBe(`Image: some image`); }); it('shows the operating system if it exists', () => { createWrapper({ location: { operatingSystem: 'linux' } }); expect(getText('namespace')).toBe(`Namespace: linux`); }); it('shows the vulnerability class if it exists', () => { createWrapper({ location: { file: 'file', class: 'class name' } }); expect(getText('class')).toBe(`Class: class name`); }); it('shows the vulnerability method if it exists', () => { createWrapper({ location: { file: 'file', method: 'method name' } }); expect(getText('method')).toBe(`Method: method name`); }); it('shows the evidence if it exists', () => { createWrapper({ evidence: 'some evidence' }); expect(getText('evidence')).toBe(`Evidence: some evidence`); }); it('shows the links if they exist', () => { createWrapper({ links: [{ url: '0' }, { url: '1' }, { url: '2' }] }); const links = getAllById('link'); expect(links).toHaveLength(3); links.wrappers.forEach((link, index) => { expect(link.attributes('target')).toBe('_blank'); expect(link.attributes('href')).toBe(index.toString()); expect(link.text()).toBe(index.toString()); }); }); it('shows the vulnerability identifiers if they exist', () => { const identifiersData = [ { name: '00', url: 'http://example.com/00' }, { name: '11', url: 'http://example.com/11' }, { name: '22', url: 'http://example.com/22' }, { name: '33' }, { name: '44' }, { name: '55' }, ]; createWrapper({ identifiers: identifiersData, }); const identifiers = getAllById('identifier'); expect(identifiers).toHaveLength(identifiersData.length); const checkIdentifier = ({ name, url }, index) => { const identifier = identifiers.at(index); expect(identifier.text()).toBe(name); if (url) { expect(identifier.is(GlLink)).toBe(true); expect(identifier.attributes()).toMatchObject({ target: '_blank', href: url, }); } else { expect(identifier.is(GlLink)).toBe(false); } }; identifiersData.forEach(checkIdentifier); }); it('shows the vulnerability assets if they exist', () => { const assetsData = [ { name: 'Postman Collection', url: 'http://example.com/postman' }, { name: 'HTTP Messages', url: 'http://example.com/http-messages' }, { name: 'Foo' }, { name: 'Bar' }, ]; createWrapper({ assets: assetsData, }); const assets = getAllById('asset'); expect(assets).toHaveLength(assetsData.length); const checkIdentifier = ({ name, url }, index) => { const asset = assets.at(index); expect(asset.text()).toBe(name); if (url) { expect(asset.is(GlLink)).toBe(true); expect(asset.attributes()).toMatchObject({ target: '_blank', href: url, }); } else { expect(asset.is(GlLink)).toBe(false); } }; assetsData.forEach(checkIdentifier); }); describe('file link', () => { const file = () => getById('file').find(GlLink); it('shows only the file name if there is no start line', () => { createWrapper({ location: { file: 'test.txt', blobPath: 'blob_path.txt' } }); expect(file().attributes('target')).toBe('_blank'); expect(file().attributes('href')).toBe('blob_path.txt'); expect(file().text()).toBe('test.txt'); }); it('shows the correct line number when there is a start line', () => { createWrapper({ location: { file: 'test.txt', startLine: 24, blobPath: 'blob.txt' } }); expect(file().attributes('target')).toBe('_blank'); expect(file().attributes('href')).toBe('blob.txt#L24'); expect(file().text()).toBe('test.txt:24'); }); it('shows the correct line numbers when there is a start and end line', () => { createWrapper({ location: { file: 'test.txt', startLine: 24, endLine: 27, blobPath: 'blob.txt' }, }); expect(file().attributes('target')).toBe('_blank'); expect(file().attributes('href')).toBe('blob.txt#L24-27'); expect(file().text()).toBe('test.txt:24-27'); }); it('shows only the start line when the end line is the same', () => { createWrapper({ location: { file: 'test.txt', startLine: 24, endLine: 24, blobPath: 'blob.txt' }, }); expect(file().attributes('target')).toBe('_blank'); expect(file().attributes('href')).toBe('blob.txt#L24'); expect(file().text()).toBe('test.txt:24'); }); }); describe('scanner', () => { const link = () => getById('scannerSafeLink'); const scannerText = () => getById('scanner').text(); it('shows the scanner name only but no link', () => { createWrapper({ scanner: { name: 'some tool' } }); expect(scannerText()).toBe('Tool: some tool'); expect(link().element instanceof HTMLSpanElement).toBe(true); }); it('shows the scanner name and version but no link', () => { createWrapper({ scanner: { name: 'some scanner', version: '1.2.3' } }); expect(scannerText()).toBe('Tool: some scanner (version 1.2.3)'); expect(link().element instanceof HTMLSpanElement).toBe(true); }); it('shows the scanner name only with a link', () => { createWrapper({ scanner: { name: 'some tool', url: '//link' } }); expect(scannerText()).toBe('Tool: some tool'); expect(link().attributes('href')).toBe('//link'); }); it('shows the scanner name and version with a link', () => { createWrapper({ scanner: { name: 'some tool', version: '1.2.3', url: '//link' } }); expect(scannerText()).toBe('Tool: some tool (version 1.2.3)'); expect(link().attributes('href')).toBe('//link'); }); }); describe('http data', () => { const TEST_HEADERS = [ { name: 'Name1', value: 'Value1' }, { name: 'Name2', value: 'Value2' }, ]; const EXPECT_REQUEST = { label: 'Sent request:', content: 'GET http://www.gitlab.com\nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]', isCode: true, }; const EXPECT_REQUEST_WITHOUT_BODY = { label: 'Sent request:', content: 'GET http://www.gitlab.com\nName1: Value1\nName2: Value2\n\n<Message body is not provided>', isCode: true, }; const EXPECT_REQUEST_WITH_EMPTY_STRING = { label: 'Sent request:', content: 'GET http://www.gitlab.com\nName1: Value1\nName2: Value2', isCode: true, }; const EXPECT_RESPONSE = { label: 'Actual response:', content: '500 INTERNAL SERVER ERROR\nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]', isCode: true, }; const EXPECT_RESPONSE_WITHOUT_REASON_PHRASE = { label: 'Actual response:', content: '500 \nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]', isCode: true, }; const EXPECT_RESPONSE_WITHOUT_BODY = { label: 'Actual response:', content: '500 INTERNAL SERVER ERROR\nName1: Value1\nName2: Value2\n\n<Message body is not provided>', isCode: true, }; const EXPECT_RESPONSE_WITH_EMPTY_STRING = { label: 'Actual response:', content: '500 INTERNAL SERVER ERROR\nName1: Value1\nName2: Value2', isCode: true, }; const EXPECT_RECORDED_RESPONSE = { label: 'Unmodified response:', content: '200 OK\nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]', isCode: true, }; const EXPECT_RECORDED_RESPONSE_WITHOUT_REASON_PHRASE = { label: 'Unmodified response:', content: '200 \nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]', isCode: true, }; const EXPECT_RECORDED_RESPONSE_WITHOUT_BODY = { label: 'Unmodified response:', content: '200 OK\nName1: Value1\nName2: Value2\n\n<Message body is not provided>', isCode: true, }; const EXPECT_RECORDED_RESPONSE_WITH_EMPTY_STRING = { label: 'Unmodified response:', content: '200 OK\nName1: Value1\nName2: Value2', isCode: true, }; const getTextContent = (el) => el.textContent.trim(); const getLabel = (el) => getTextContent(getByTestId(el, 'label')); const getContent = (el) => getTextContent(getByTestId(el, 'value')); const getSectionData = (testId) => { const section = getById(testId).element; if (!section) { return null; } return getAllByRole(section, 'listitem').map((li) => ({ label: getLabel(li), content: getContent(li), ...(li.querySelector('code') ? { isCode: true } : {}), })); }; it.each` request | expectedData ${null} | ${null} ${{}} | ${null} ${{ headers: TEST_HEADERS }} | ${null} ${{ method: 'GET' }} | ${null} ${{ method: 'GET', url: 'http://www.gitlab.com' }} | ${null} ${{ method: 'GET', url: 'http://www.gitlab.com', body: '[{"user_id":1,}]' }} | ${null} ${{ headers: TEST_HEADERS, method: 'GET', url: 'http://www.gitlab.com', body: '[{"user_id":1,}]' }} | ${[EXPECT_REQUEST]} ${{ headers: TEST_HEADERS, method: 'GET', url: 'http://www.gitlab.com', body: null }} | ${[EXPECT_REQUEST_WITHOUT_BODY]} ${{ headers: TEST_HEADERS, method: 'GET', url: 'http://www.gitlab.com', body: undefined }} | ${[EXPECT_REQUEST_WITHOUT_BODY]} ${{ headers: TEST_HEADERS, method: 'GET', url: 'http://www.gitlab.com', body: '' }} | ${[EXPECT_REQUEST_WITH_EMPTY_STRING]} `('shows request data for $request', ({ request, expectedData }) => { createWrapper({ request }); expect(getSectionData('request')).toEqual(expectedData); }); it.each` response | expectedData ${null} | ${null} ${{}} | ${null} ${{ headers: TEST_HEADERS }} | ${null} ${{ headers: TEST_HEADERS, body: '[{"user_id":1,}]' }} | ${null} ${{ headers: TEST_HEADERS, body: '[{"user_id":1,}]', statusCode: '500' }} | ${[EXPECT_RESPONSE_WITHOUT_REASON_PHRASE]} ${{ headers: TEST_HEADERS, body: '[{"user_id":1,}]', statusCode: '500', reasonPhrase: 'INTERNAL SERVER ERROR' }} | ${[EXPECT_RESPONSE]} ${{ headers: TEST_HEADERS, body: null, statusCode: '500', reasonPhrase: 'INTERNAL SERVER ERROR' }} | ${[EXPECT_RESPONSE_WITHOUT_BODY]} ${{ headers: TEST_HEADERS, body: undefined, statusCode: '500', reasonPhrase: 'INTERNAL SERVER ERROR' }} | ${[EXPECT_RESPONSE_WITHOUT_BODY]} ${{ headers: TEST_HEADERS, body: '', statusCode: '500', reasonPhrase: 'INTERNAL SERVER ERROR' }} | ${[EXPECT_RESPONSE_WITH_EMPTY_STRING]} `('shows response data for $response', ({ response, expectedData }) => { createWrapper({ response }); expect(getSectionData('response')).toEqual(expectedData); }); it.each` supportingMessages | expectedData ${null} | ${null} ${[]} | ${null} ${[{}]} | ${null} ${[{}, { response: {} }]} | ${null} ${[{}, { response: { headers: TEST_HEADERS } }]} | ${null} ${[{}, { response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]' } }]} | ${null} ${[{}, { response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]', status_code: '200' } }]} | ${null} ${[{}, { response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]', status_code: '200', reason_phrase: 'OK' } }]} | ${null} ${[{}, { name: SUPPORTING_MESSAGE_TYPES.RECORDED, response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]', statusCode: '200' } }]} | ${[EXPECT_RECORDED_RESPONSE_WITHOUT_REASON_PHRASE]} ${[{}, { name: SUPPORTING_MESSAGE_TYPES.RECORDED, response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]', statusCode: '200', reasonPhrase: 'OK' } }]} | ${[EXPECT_RECORDED_RESPONSE]} ${[{}, { name: SUPPORTING_MESSAGE_TYPES.RECORDED, response: { headers: TEST_HEADERS, body: null, statusCode: '200', reasonPhrase: 'OK' } }]} | ${[EXPECT_RECORDED_RESPONSE_WITHOUT_BODY]} ${[{}, { name: SUPPORTING_MESSAGE_TYPES.RECORDED, response: { headers: TEST_HEADERS, body: undefined, statusCode: '200', reasonPhrase: 'OK' } }]} | ${[EXPECT_RECORDED_RESPONSE_WITHOUT_BODY]} ${[{}, { name: SUPPORTING_MESSAGE_TYPES.RECORDED, response: { headers: TEST_HEADERS, body: '', statusCode: '200', reasonPhrase: 'OK' } }]} | ${[EXPECT_RECORDED_RESPONSE_WITH_EMPTY_STRING]} `('shows response data for $supporting_messages', ({ supportingMessages, expectedData }) => { createWrapper({ supportingMessages }); expect(getSectionData('recorded-response')).toEqual(expectedData); }); }); });