diff options
Diffstat (limited to 'spec/frontend/ci/reports/codequality_report')
6 files changed, 616 insertions, 0 deletions
diff --git a/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js b/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js new file mode 100644 index 00000000000..5ca4b25da9b --- /dev/null +++ b/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js @@ -0,0 +1,102 @@ +import { GlIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import component from '~/ci/reports/codequality_report/components/codequality_issue_body.vue'; +import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/ci/reports/constants'; + +describe('code quality issue body issue body', () => { + let wrapper; + + const findSeverityIcon = () => wrapper.findByTestId('codequality-severity-icon'); + const findGlIcon = () => wrapper.findComponent(GlIcon); + + const codequalityIssue = { + name: + 'rubygem-rest-client: session fixation vulnerability via Set-Cookie headers in 30x redirection responses', + path: 'Gemfile.lock', + severity: 'normal', + type: 'Issue', + urlPath: '/Gemfile.lock#L22', + }; + + const createComponent = (initialStatus, issue = codequalityIssue) => { + wrapper = extendedWrapper( + shallowMount(component, { + propsData: { + issue, + status: initialStatus, + }, + }), + ); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('severity rating', () => { + it.each` + severity | iconClass | iconName + ${'INFO'} | ${'text-primary-400'} | ${'severity-info'} + ${'MINOR'} | ${'text-warning-200'} | ${'severity-low'} + ${'CRITICAL'} | ${'text-danger-600'} | ${'severity-high'} + ${'BLOCKER'} | ${'text-danger-800'} | ${'severity-critical'} + ${'UNKNOWN'} | ${'text-secondary-400'} | ${'severity-unknown'} + ${'INVALID'} | ${'text-secondary-400'} | ${'severity-unknown'} + ${'info'} | ${'text-primary-400'} | ${'severity-info'} + ${'minor'} | ${'text-warning-200'} | ${'severity-low'} + ${'major'} | ${'text-warning-400'} | ${'severity-medium'} + ${'critical'} | ${'text-danger-600'} | ${'severity-high'} + ${'blocker'} | ${'text-danger-800'} | ${'severity-critical'} + ${'unknown'} | ${'text-secondary-400'} | ${'severity-unknown'} + ${'invalid'} | ${'text-secondary-400'} | ${'severity-unknown'} + ${undefined} | ${'text-secondary-400'} | ${'severity-unknown'} + `( + 'renders correct icon for "$severity" severity rating', + ({ severity, iconClass, iconName }) => { + createComponent(STATUS_FAILED, { + ...codequalityIssue, + severity, + }); + const icon = findGlIcon(); + + expect(findSeverityIcon().classes()).toContain(iconClass); + expect(icon.exists()).toBe(true); + expect(icon.props('name')).toBe(iconName); + }, + ); + }); + + describe('with success', () => { + it('renders fixed label', () => { + createComponent(STATUS_SUCCESS); + + expect(wrapper.text()).toContain('Fixed'); + }); + }); + + describe('without success', () => { + it('does not render fixed label', () => { + createComponent(STATUS_FAILED); + + expect(wrapper.text()).not.toContain('Fixed'); + }); + }); + + describe('name', () => { + it('renders name', () => { + createComponent(STATUS_NEUTRAL); + + expect(wrapper.text()).toContain(codequalityIssue.name); + }); + }); + + describe('path', () => { + it('renders the report-link path using the correct code quality issue', () => { + createComponent(STATUS_NEUTRAL); + + expect(wrapper.find('report-link-stub').props('issue')).toBe(codequalityIssue); + }); + }); +}); diff --git a/spec/frontend/ci/reports/codequality_report/mock_data.js b/spec/frontend/ci/reports/codequality_report/mock_data.js new file mode 100644 index 00000000000..2c994116db6 --- /dev/null +++ b/spec/frontend/ci/reports/codequality_report/mock_data.js @@ -0,0 +1,49 @@ +export const reportIssues = { + status: 'failed', + new_errors: [ + { + description: + 'Method `long_if` has a Cognitive Complexity of 10 (exceeds 5 allowed). Consider refactoring.', + severity: 'minor', + file_path: 'codequality.rb', + line: 5, + }, + ], + resolved_errors: [ + { + description: 'Insecure Dependency', + severity: 'major', + file_path: 'lib/six.rb', + line: 22, + }, + ], + existing_errors: [], + summary: { total: 3, resolved: 0, errored: 3 }, +}; + +export const parsedReportIssues = { + newIssues: [ + { + description: + 'Method `long_if` has a Cognitive Complexity of 10 (exceeds 5 allowed). Consider refactoring.', + file_path: 'codequality.rb', + line: 5, + name: + 'Method `long_if` has a Cognitive Complexity of 10 (exceeds 5 allowed). Consider refactoring.', + path: 'codequality.rb', + severity: 'minor', + urlPath: 'null/codequality.rb#L5', + }, + ], + resolvedIssues: [ + { + description: 'Insecure Dependency', + file_path: 'lib/six.rb', + line: 22, + name: 'Insecure Dependency', + path: 'lib/six.rb', + severity: 'major', + urlPath: 'null/lib/six.rb#L22', + }, + ], +}; diff --git a/spec/frontend/ci/reports/codequality_report/store/actions_spec.js b/spec/frontend/ci/reports/codequality_report/store/actions_spec.js new file mode 100644 index 00000000000..88628210793 --- /dev/null +++ b/spec/frontend/ci/reports/codequality_report/store/actions_spec.js @@ -0,0 +1,185 @@ +import MockAdapter from 'axios-mock-adapter'; +import testAction from 'helpers/vuex_action_helper'; +import { TEST_HOST } from 'spec/test_constants'; +import axios from '~/lib/utils/axios_utils'; +import createStore from '~/ci/reports/codequality_report/store'; +import * as actions from '~/ci/reports/codequality_report/store/actions'; +import * as types from '~/ci/reports/codequality_report/store/mutation_types'; +import { STATUS_NOT_FOUND } from '~/ci/reports/constants'; +import { reportIssues, parsedReportIssues } from '../mock_data'; + +const pollInterval = 123; +const pollIntervalHeader = { + 'Poll-Interval': pollInterval, +}; + +describe('Codequality Reports actions', () => { + let localState; + let localStore; + + beforeEach(() => { + localStore = createStore(); + localState = localStore.state; + }); + + describe('setPaths', () => { + it('should commit SET_PATHS mutation', () => { + const paths = { + baseBlobPath: 'baseBlobPath', + headBlobPath: 'headBlobPath', + reportsPath: 'reportsPath', + }; + + return testAction( + actions.setPaths, + paths, + localState, + [{ type: types.SET_PATHS, payload: paths }], + [], + ); + }); + }); + + describe('fetchReports', () => { + const endpoint = `${TEST_HOST}/codequality_reports.json`; + let mock; + + beforeEach(() => { + localState.reportsPath = endpoint; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('on success', () => { + it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', () => { + mock.onGet(endpoint).reply(200, reportIssues); + + return testAction( + actions.fetchReports, + null, + localState, + [{ type: types.REQUEST_REPORTS }], + [ + { + payload: parsedReportIssues, + type: 'receiveReportsSuccess', + }, + ], + ); + }); + }); + + describe('on error', () => { + it('commits REQUEST_REPORTS and dispatches receiveReportsError', () => { + mock.onGet(endpoint).reply(500); + + return testAction( + actions.fetchReports, + null, + localState, + [{ type: types.REQUEST_REPORTS }], + [{ type: 'receiveReportsError', payload: expect.any(Error) }], + ); + }); + }); + + describe('when base report is not found', () => { + it('commits REQUEST_REPORTS and dispatches receiveReportsError', () => { + const data = { status: STATUS_NOT_FOUND }; + mock.onGet(`${TEST_HOST}/codequality_reports.json`).reply(200, data); + + return testAction( + actions.fetchReports, + null, + localState, + [{ type: types.REQUEST_REPORTS }], + [{ type: 'receiveReportsError', payload: data }], + ); + }); + }); + + describe('while waiting for report results', () => { + it('continues polling until it receives data', () => { + mock + .onGet(endpoint) + .replyOnce(204, undefined, pollIntervalHeader) + .onGet(endpoint) + .reply(200, reportIssues); + + return Promise.all([ + testAction( + actions.fetchReports, + null, + localState, + [{ type: types.REQUEST_REPORTS }], + [ + { + payload: parsedReportIssues, + type: 'receiveReportsSuccess', + }, + ], + ), + axios + // wait for initial NO_CONTENT response to be fulfilled + .waitForAll() + .then(() => { + jest.advanceTimersByTime(pollInterval); + }), + ]); + }); + + it('continues polling until it receives an error', () => { + mock + .onGet(endpoint) + .replyOnce(204, undefined, pollIntervalHeader) + .onGet(endpoint) + .reply(500); + + return Promise.all([ + testAction( + actions.fetchReports, + null, + localState, + [{ type: types.REQUEST_REPORTS }], + [{ type: 'receiveReportsError', payload: expect.any(Error) }], + ), + axios + // wait for initial NO_CONTENT response to be fulfilled + .waitForAll() + .then(() => { + jest.advanceTimersByTime(pollInterval); + }), + ]); + }); + }); + }); + + describe('receiveReportsSuccess', () => { + it('commits RECEIVE_REPORTS_SUCCESS', () => { + const data = { issues: [] }; + + return testAction( + actions.receiveReportsSuccess, + data, + localState, + [{ type: types.RECEIVE_REPORTS_SUCCESS, payload: data }], + [], + ); + }); + }); + + describe('receiveReportsError', () => { + it('commits RECEIVE_REPORTS_ERROR', () => { + return testAction( + actions.receiveReportsError, + null, + localState, + [{ type: types.RECEIVE_REPORTS_ERROR, payload: null }], + [], + ); + }); + }); +}); diff --git a/spec/frontend/ci/reports/codequality_report/store/getters_spec.js b/spec/frontend/ci/reports/codequality_report/store/getters_spec.js new file mode 100644 index 00000000000..f4505204f67 --- /dev/null +++ b/spec/frontend/ci/reports/codequality_report/store/getters_spec.js @@ -0,0 +1,94 @@ +import createStore from '~/ci/reports/codequality_report/store'; +import * as getters from '~/ci/reports/codequality_report/store/getters'; +import { LOADING, ERROR, SUCCESS, STATUS_NOT_FOUND } from '~/ci/reports/constants'; + +describe('Codequality reports store getters', () => { + let localState; + let localStore; + + beforeEach(() => { + localStore = createStore(); + localState = localStore.state; + }); + + describe('hasCodequalityIssues', () => { + describe('when there are issues', () => { + it('returns true', () => { + localState.newIssues = [{ reason: 'repetitive code' }]; + localState.resolvedIssues = []; + + expect(getters.hasCodequalityIssues(localState)).toEqual(true); + + localState.newIssues = []; + localState.resolvedIssues = [{ reason: 'repetitive code' }]; + + expect(getters.hasCodequalityIssues(localState)).toEqual(true); + }); + }); + + describe('when there are no issues', () => { + it('returns false when there are no issues', () => { + expect(getters.hasCodequalityIssues(localState)).toEqual(false); + }); + }); + }); + + describe('codequalityStatus', () => { + describe('when loading', () => { + it('returns loading status', () => { + localState.isLoading = true; + + expect(getters.codequalityStatus(localState)).toEqual(LOADING); + }); + }); + + describe('on error', () => { + it('returns error status', () => { + localState.hasError = true; + + expect(getters.codequalityStatus(localState)).toEqual(ERROR); + }); + }); + + describe('when successfully loaded', () => { + it('returns error status', () => { + expect(getters.codequalityStatus(localState)).toEqual(SUCCESS); + }); + }); + }); + + describe('codequalityText', () => { + it.each` + resolvedIssues | newIssues | expectedText + ${0} | ${0} | ${'No changes to code quality'} + ${0} | ${1} | ${'Code quality degraded due to 1 new issue'} + ${2} | ${0} | ${'Code quality improved due to 2 resolved issues'} + ${1} | ${2} | ${'Code quality scanning detected 3 changes in merged results'} + `( + 'returns a summary containing $resolvedIssues resolved issues and $newIssues new issues', + ({ newIssues, resolvedIssues, expectedText }) => { + localState.newIssues = new Array(newIssues).fill({ reason: 'Repetitive code' }); + localState.resolvedIssues = new Array(resolvedIssues).fill({ reason: 'Repetitive code' }); + + expect(getters.codequalityText(localState)).toEqual(expectedText); + }, + ); + }); + + describe('codequalityPopover', () => { + describe('when base report is not available', () => { + it('returns a popover with a documentation link', () => { + localState.status = STATUS_NOT_FOUND; + localState.helpPath = 'codequality_help.html'; + + expect(getters.codequalityPopover(localState).title).toEqual( + 'Base pipeline codequality artifact not found', + ); + expect(getters.codequalityPopover(localState).content).toContain( + 'Learn more about codequality reports', + 'href="codequality_help.html"', + ); + }); + }); + }); +}); diff --git a/spec/frontend/ci/reports/codequality_report/store/mutations_spec.js b/spec/frontend/ci/reports/codequality_report/store/mutations_spec.js new file mode 100644 index 00000000000..22ff86b1040 --- /dev/null +++ b/spec/frontend/ci/reports/codequality_report/store/mutations_spec.js @@ -0,0 +1,100 @@ +import createStore from '~/ci/reports/codequality_report/store'; +import mutations from '~/ci/reports/codequality_report/store/mutations'; +import { STATUS_NOT_FOUND } from '~/ci/reports/constants'; + +describe('Codequality Reports mutations', () => { + let localState; + let localStore; + + beforeEach(() => { + localStore = createStore(); + localState = localStore.state; + }); + + describe('SET_PATHS', () => { + it('sets paths to given values', () => { + const baseBlobPath = 'base/blob/path/'; + const headBlobPath = 'head/blob/path/'; + const reportsPath = 'reports.json'; + const helpPath = 'help.html'; + + mutations.SET_PATHS(localState, { + baseBlobPath, + headBlobPath, + reportsPath, + helpPath, + }); + + expect(localState.baseBlobPath).toEqual(baseBlobPath); + expect(localState.headBlobPath).toEqual(headBlobPath); + expect(localState.reportsPath).toEqual(reportsPath); + expect(localState.helpPath).toEqual(helpPath); + }); + }); + + describe('REQUEST_REPORTS', () => { + it('sets isLoading to true', () => { + mutations.REQUEST_REPORTS(localState); + + expect(localState.isLoading).toEqual(true); + }); + }); + + describe('RECEIVE_REPORTS_SUCCESS', () => { + it('sets isLoading to false', () => { + mutations.RECEIVE_REPORTS_SUCCESS(localState, {}); + + expect(localState.isLoading).toEqual(false); + }); + + it('sets hasError to false', () => { + mutations.RECEIVE_REPORTS_SUCCESS(localState, {}); + + expect(localState.hasError).toEqual(false); + }); + + it('clears status and statusReason', () => { + mutations.RECEIVE_REPORTS_SUCCESS(localState, {}); + + expect(localState.status).toEqual(''); + expect(localState.statusReason).toEqual(''); + }); + + it('sets newIssues and resolvedIssues from response data', () => { + const data = { newIssues: [{ id: 1 }], resolvedIssues: [{ id: 2 }] }; + mutations.RECEIVE_REPORTS_SUCCESS(localState, data); + + expect(localState.newIssues).toEqual(data.newIssues); + expect(localState.resolvedIssues).toEqual(data.resolvedIssues); + }); + }); + + describe('RECEIVE_REPORTS_ERROR', () => { + it('sets isLoading to false', () => { + mutations.RECEIVE_REPORTS_ERROR(localState); + + expect(localState.isLoading).toEqual(false); + }); + + it('sets hasError to true', () => { + mutations.RECEIVE_REPORTS_ERROR(localState); + + expect(localState.hasError).toEqual(true); + }); + + it('sets status based on error object', () => { + const error = { status: STATUS_NOT_FOUND }; + mutations.RECEIVE_REPORTS_ERROR(localState, error); + + expect(localState.status).toEqual(error.status); + }); + + it('sets statusReason to string from error response data', () => { + const data = { status_reason: 'This merge request does not have codequality reports' }; + const error = { response: { data } }; + mutations.RECEIVE_REPORTS_ERROR(localState, error); + + expect(localState.statusReason).toEqual(data.status_reason); + }); + }); +}); diff --git a/spec/frontend/ci/reports/codequality_report/store/utils/codequality_parser_spec.js b/spec/frontend/ci/reports/codequality_report/store/utils/codequality_parser_spec.js new file mode 100644 index 00000000000..f7d82d2b662 --- /dev/null +++ b/spec/frontend/ci/reports/codequality_report/store/utils/codequality_parser_spec.js @@ -0,0 +1,86 @@ +import { reportIssues, parsedReportIssues } from 'jest/ci/reports/codequality_report/mock_data'; +import { parseCodeclimateMetrics } from '~/ci/reports/codequality_report/store/utils/codequality_parser'; + +describe('Codequality report store utils', () => { + let result; + + describe('parseCodeclimateMetrics', () => { + it('should parse the issues from backend codequality diff', () => { + [result] = parseCodeclimateMetrics(reportIssues.new_errors, 'path'); + + expect(result.name).toEqual(parsedReportIssues.newIssues[0].name); + expect(result.path).toEqual(parsedReportIssues.newIssues[0].path); + expect(result.line).toEqual(parsedReportIssues.newIssues[0].line); + }); + + describe('when an issue has no location or path', () => { + const issue = { description: 'Insecure Dependency' }; + + beforeEach(() => { + [result] = parseCodeclimateMetrics([issue], 'path'); + }); + + it('is parsed', () => { + expect(result.name).toEqual(issue.description); + }); + }); + + describe('when an issue has a non-nested path', () => { + const issue = { description: 'Insecure Dependency', path: 'Gemfile.lock' }; + + beforeEach(() => { + [result] = parseCodeclimateMetrics([issue], 'path'); + }); + + it('is parsed', () => { + expect(result.name).toEqual(issue.description); + }); + }); + + describe('when an issue has a path but no line', () => { + const issue = { description: 'Insecure Dependency', location: { path: 'Gemfile.lock' } }; + + beforeEach(() => { + [result] = parseCodeclimateMetrics([issue], 'path'); + }); + + it('is parsed', () => { + expect(result.name).toEqual(issue.description); + expect(result.path).toEqual(issue.location.path); + expect(result.urlPath).toEqual(`path/${issue.location.path}`); + }); + }); + + describe('when an issue has a line nested in positions', () => { + const issue = { + description: 'Insecure Dependency', + location: { + path: 'Gemfile.lock', + positions: { begin: { line: 84 } }, + }, + }; + + beforeEach(() => { + [result] = parseCodeclimateMetrics([issue], 'path'); + }); + + it('is parsed', () => { + expect(result.name).toEqual(issue.description); + expect(result.path).toEqual(issue.location.path); + expect(result.urlPath).toEqual( + `path/${issue.location.path}#L${issue.location.positions.begin.line}`, + ); + }); + }); + + describe('with an empty issue array', () => { + beforeEach(() => { + result = parseCodeclimateMetrics([], 'path'); + }); + + it('returns an empty array', () => { + expect(result).toEqual([]); + }); + }); + }); +}); |