Commit 6176f4e8 authored by Miguel Rincon's avatar Miguel Rincon

Merge branch '328257-full-codequality-report-graphql' into 'master'

Use GraphQL data source for pipeline's code quality report

See merge request gitlab-org/gitlab!69917
parents 4ea31e32 21f69dcf
......@@ -33,17 +33,20 @@ export default {
issueName() {
return `${this.severityLabel} - ${this.issue.name}`;
},
issueSeverity() {
return this.issue.severity.toLowerCase();
},
isStatusSuccess() {
return this.status === STATUS_SUCCESS;
},
severityClass() {
return SEVERITY_CLASSES[this.issue.severity] || SEVERITY_CLASSES.unknown;
return SEVERITY_CLASSES[this.issueSeverity] || SEVERITY_CLASSES.unknown;
},
severityIcon() {
return SEVERITY_ICONS[this.issue.severity] || SEVERITY_ICONS.unknown;
return SEVERITY_ICONS[this.issueSeverity] || SEVERITY_ICONS.unknown;
},
severityLabel() {
return this.$options.severityText[this.issue.severity] || this.$options.severityText.unknown;
return this.$options.severityText[this.issueSeverity] || this.$options.severityText.unknown;
},
},
severityText: {
......
export const parseCodeclimateMetrics = (issues = [], path = '') => {
export const parseCodeclimateMetrics = (issues = [], blobPath = '') => {
return issues.map((issue) => {
// the `file_path` attribute from the artifact is returned as `file` by GraphQL
const issuePath = issue.file_path || issue.path;
const parsedIssue = {
name: issue.description,
path: issue.file_path,
urlPath: `${path}/${issue.file_path}#L${issue.line}`,
path: issuePath,
urlPath: `${blobPath}/${issuePath}#L${issue.line}`,
...issue,
};
if (issue?.location?.path) {
let parseCodeQualityUrl = `${path}/${issue.location.path}`;
let parseCodeQualityUrl = `${blobPath}/${issue.location.path}`;
parsedIssue.path = issue.location.path;
if (issue?.location?.lines?.begin) {
......
<script>
import { GlPagination, GlSkeletonLoader } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
import { componentNames } from 'ee/reports/components/issue_body';
import reportsMixin from 'ee/vue_shared/security_reports/mixins/reports_mixin';
import { n__, s__, sprintf } from '~/locale';
import ReportSection from '~/reports/components/report_section.vue';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
ReportSection,
PaginationLinks,
GlSkeletonLoader,
GlPagination,
},
mixins: [reportsMixin],
mixins: [reportsMixin, glFeatureFlagsMixin()],
componentNames,
computed: {
...mapState(['isLoadingCodequality', 'loadingCodequalityFailed', 'pageInfo']),
...mapGetters(['codequalityIssues', 'codequalityIssueTotal']),
prevPage() {
return Math.max(this.pageInfo.currentPage - 1, 0);
},
nextPage() {
return this.pageInfo?.hasNextPage ? this.pageInfo.currentPage + 1 : null;
},
hasCodequalityIssues() {
return this.codequalityIssueTotal > 0;
},
codequalityText() {
const text = [];
const { codequalityIssueTotal } = this;
const count = this.codequalityIssueTotal;
if (codequalityIssueTotal === 0) {
if (count === 0) {
return s__('ciReport|No code quality issues found');
} else if (codequalityIssueTotal > 0) {
} else if (count > 0) {
return sprintf(s__('ciReport|Found %{issuesWithCount}'), {
issuesWithCount: n__(
'%d code quality issue',
'%d code quality issues',
codequalityIssueTotal,
),
issuesWithCount: n__('%d code quality issue', '%d code quality issues', count),
});
}
......@@ -70,13 +76,27 @@ export default {
:success-text="codequalityText"
:unresolved-issues="codequalityIssues"
:resolved-issues="[]"
:has-issues="hasCodequalityIssues"
:has-issues="hasCodequalityIssues && !isLoadingCodequality"
:component="$options.componentNames.CodequalityIssueBody"
class="codequality-report"
>
<template v-if="hasCodequalityIssues" #sub-heading>{{ $options.i18n.subHeading }}</template>
</report-section>
<div v-if="isLoadingCodequality" class="gl-p-4">
<gl-skeleton-loader :lines="50" />
</div>
<gl-pagination
v-if="glFeatures.graphqlCodeQualityFullReport"
:disabled="isLoadingCodequality"
:value="pageInfo.currentPage"
:prev-page="prevPage"
:next-page="nextPage"
align="center"
class="gl-mt-3"
@input="setPage"
/>
<pagination-links
v-else
:change="setPage"
:page-info="pageInfo"
class="d-flex justify-content-center gl-mt-3"
......
query getCodeQualityViolations($projectPath: ID!, $iid: ID!, $first: Int, $after: String) {
project(fullPath: $projectPath) {
pipeline(iid: $iid) {
codeQualityReports(first: $first, after: $after) {
count
edges {
node {
line
description
path
fingerprint
severity
}
}
pageInfo {
startCursor
endCursor
hasNextPage
}
}
}
}
}
......@@ -4,10 +4,30 @@ import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
import { parseCodeclimateMetrics } from '~/reports/codequality_report/store/utils/codequality_parser';
import getCodeQualityViolations from '../graphql/queries/get_code_quality_violations.query.graphql';
import { VIEW_EVENT_NAME } from './constants';
import * as types from './mutation_types';
import { gqClient } from './utils';
export const setPage = ({ commit }, page) => commit(types.SET_PAGE, page);
export const setPage = ({ state, commit, dispatch }, page) => {
if (gon.features?.graphqlCodeQualityFullReport) {
const { currentPage, startCursor, endCursor } = state.pageInfo;
if (page > currentPage) {
commit(types.SET_PAGE, {
after: endCursor,
currentPage: page,
});
} else {
commit(types.SET_PAGE, {
after: startCursor,
currentPage: page,
});
}
return dispatch('fetchReport');
}
return commit(types.SET_PAGE, { page });
};
export const requestReport = ({ commit }) => {
commit(types.REQUEST_REPORT);
......@@ -15,24 +35,49 @@ export const requestReport = ({ commit }) => {
Api.trackRedisHllUserEvent(VIEW_EVENT_NAME);
};
export const receiveReportSuccess = ({ state, commit }, data) => {
if (gon.features?.graphqlCodeQualityFullReport) {
const parsedIssues = parseCodeclimateMetrics(
data.edges.map((edge) => edge.node),
state.blobPath,
);
return commit(types.RECEIVE_REPORT_SUCCESS_GRAPHQL, { data, parsedIssues });
}
const parsedIssues = parseCodeclimateMetrics(data, state.blobPath);
commit(types.RECEIVE_REPORT_SUCCESS, parsedIssues);
return commit(types.RECEIVE_REPORT_SUCCESS, parsedIssues);
};
export const receiveReportError = ({ commit }, error) => commit(types.RECEIVE_REPORT_ERROR, error);
export const fetchReport = ({ state, dispatch }) => {
dispatch('requestReport');
axios
.get(state.endpoint)
.then(({ data }) => {
if (!state.blobPath) throw new Error();
dispatch('receiveReportSuccess', data);
})
.catch((error) => {
dispatch('receiveReportError', error);
createFlash({
message: s__('ciReport|There was an error fetching the codequality report.'),
export const fetchReport = async ({ state, dispatch }) => {
try {
dispatch('requestReport');
if (!state.blobPath) throw new Error();
if (gon.features?.graphqlCodeQualityFullReport) {
const { projectPath, pipelineIid, pageInfo } = state;
const variables = {
projectPath,
iid: pipelineIid,
first: pageInfo.first,
after: pageInfo.after,
};
await gqClient
.query({
query: getCodeQualityViolations,
variables,
})
.then(({ data }) => {
dispatch('receiveReportSuccess', data.project?.pipeline?.codeQualityReports);
});
} else {
await axios.get(state.endpoint).then(({ data }) => {
dispatch('receiveReportSuccess', data);
});
}
} catch (error) {
dispatch('receiveReportError', error);
createFlash({
message: s__('ciReport|There was an error fetching the codequality report.'),
});
}
};
export const VIEW_EVENT_NAME = 'i_testing_full_code_quality_report_total';
export const PAGE_SIZE = 25;
export const SEVERITY_SORT_ORDER = {
unknown: 0,
blocker: 1,
......
export const codequalityIssues = (state) => {
if (gon.features?.graphqlCodeQualityFullReport) {
return state.codequalityIssues;
}
const { page, perPage } = state.pageInfo;
const start = (page - 1) * perPage;
return state.allCodequalityIssues.slice(start, start + perPage);
};
export const codequalityIssueTotal = (state) => state.allCodequalityIssues.length;
export const codequalityIssueTotal = (state) => {
if (gon.features?.graphqlCodeQualityFullReport) {
return state.pageInfo.count;
}
return state.allCodequalityIssues.length;
};
export const SET_PAGE = 'SET_PAGE';
export const REQUEST_REPORT = 'REQUEST_REPORT';
export const RECEIVE_REPORT_SUCCESS_GRAPHQL = 'RECEIVE_REPORT_SUCCESS_GRAPHQL';
export const RECEIVE_REPORT_SUCCESS = 'RECEIVE_REPORT_SUCCESS';
export const RECEIVE_REPORT_ERROR = 'RECEIVE_REPORT_ERROR';
......@@ -2,16 +2,25 @@ import { SEVERITY_SORT_ORDER } from './constants';
import * as types from './mutation_types';
export default {
[types.SET_PAGE](state, page) {
[types.SET_PAGE](state, pageInfo) {
Object.assign(state, {
pageInfo: Object.assign(state.pageInfo, {
page,
}),
pageInfo: Object.assign(state.pageInfo, pageInfo),
});
},
[types.REQUEST_REPORT](state) {
Object.assign(state, { isLoadingCodequality: true });
},
[types.RECEIVE_REPORT_SUCCESS_GRAPHQL](state, { data, parsedIssues }) {
Object.assign(state, {
isLoadingCodequality: false,
codequalityIssues: parsedIssues,
loadingCodequalityFailed: false,
pageInfo: Object.assign(state.pageInfo, {
count: data.count,
...data.pageInfo,
}),
});
},
[types.RECEIVE_REPORT_SUCCESS](state, allCodequalityIssues) {
Object.assign(state, {
isLoadingCodequality: false,
......@@ -29,10 +38,12 @@ export default {
Object.assign(state, {
isLoadingCodequality: false,
allCodequalityIssues: [],
codequalityIssues: [],
loadingCodequalityFailed: true,
codeQualityError,
pageInfo: Object.assign(state.pageInfo, {
total: 0,
count: 0,
}),
});
},
......
import { PAGE_SIZE } from './constants';
export default () => ({
projectPath: null,
pipelineIid: null,
endpoint: '',
allCodequalityIssues: [],
codequalityIssues: [],
isLoadingCodequality: false,
loadingCodequalityFailed: false,
codeQualityError: null,
pageInfo: {
page: 1,
perPage: 25,
perPage: PAGE_SIZE,
total: 0,
count: 0,
currentPage: 1,
startCursor: '',
endCursor: '',
first: PAGE_SIZE,
after: '',
hasNextPage: false,
},
});
import createGqClient from '~/lib/graphql';
export const gqClient = createGqClient();
......@@ -11,8 +11,18 @@ export default () => {
if (tabsElement && codequalityTab) {
const fetchReportAction = 'fetchReport';
const { codequalityReportDownloadPath, blobPath } = codequalityTab.dataset;
const store = createStore({ endpoint: codequalityReportDownloadPath, blobPath });
const {
codequalityReportDownloadPath,
blobPath,
projectPath,
pipelineIid,
} = codequalityTab.dataset;
const store = createStore({
endpoint: codequalityReportDownloadPath,
blobPath,
projectPath,
pipelineIid,
});
const isCodequalityTabActive = Boolean(
document.querySelector('.pipelines-tabs > li > a.codequality-tab.active'),
......
......@@ -10,6 +10,7 @@ module EE
before_action :authorize_read_licenses!, only: [:licenses]
before_action do
push_frontend_feature_flag(:pipeline_security_dashboard_graphql, project, type: :development, default_enabled: :yaml)
push_frontend_feature_flag(:graphql_code_quality_full_report, project, type: :development, default_enabled: :yaml)
end
feature_category :license_compliance, [:licenses]
......
......@@ -34,7 +34,9 @@
licenses_api_path: licenses_api_path,
can_manage_licenses: can?(current_user, :admin_software_license_policy, project).to_s } }
- if codequality_report_download_path
- if codequality_report_download_path || pipeline.can_generate_codequality_reports?
#js-tab-codequality.tab-pane
#js-pipeline-codequality-report{ data: { codequality_report_download_path: codequality_report_download_path,
blob_path: project_blob_path(project, pipeline.commit) } }
blob_path: project_blob_path(project, pipeline.commit),
project_path: project.full_path,
pipeline_iid: pipeline.iid } }
---
name: graphql_code_quality_full_report
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69917
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340525
milestone: '14.4'
type: development
group: group::testing
default_enabled: false
import { mount, createLocalVue } from '@vue/test-utils';
import { GlPagination, GlSkeletonLoader } from '@gitlab/ui';
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import CodequalityReportApp from 'ee/codequality_report/codequality_report.vue';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import { parsedIssues } from './mock_data';
jest.mock('~/flash');
......@@ -12,10 +14,11 @@ describe('Codequality report app', () => {
let wrapper;
let store;
const createComponent = (state = {}, issues = [], glFeatures = {}) => {
const createComponent = (state = {}, issues = [], glFeatures = {}, mountFn = mount) => {
store = new Vuex.Store({
state: {
pageInfo: {},
isLoadingCodequality: false,
...state,
},
getters: {
......@@ -24,7 +27,7 @@ describe('Codequality report app', () => {
},
});
wrapper = mount(CodequalityReportApp, {
wrapper = mountFn(CodequalityReportApp, {
localVue,
store,
provide: {
......@@ -36,6 +39,9 @@ describe('Codequality report app', () => {
const findStatus = () => wrapper.find('.js-code-text');
const findSuccessIcon = () => wrapper.find('.js-ci-status-icon-success');
const findWarningIcon = () => wrapper.find('.js-ci-status-icon-warning');
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findOldPagination = () => wrapper.findComponent(PaginationLinks);
const findPagination = () => wrapper.findComponent(GlPagination);
afterEach(() => {
wrapper.destroy();
......@@ -48,6 +54,7 @@ describe('Codequality report app', () => {
it('shows a loading state', () => {
expect(findStatus().text()).toBe('Loading codeclimate report');
expect(findSkeletonLoader().exists()).toBe(true);
});
});
......@@ -88,6 +95,42 @@ describe('Codequality report app', () => {
});
});
describe('with graphql feature flag disabled', () => {
beforeEach(() => {
createComponent(
{},
parsedIssues,
{
graphqlCodeQualityFullReport: false,
},
shallowMount,
);
});
it('renders the old pagination component', () => {
expect(findOldPagination().exists()).toBe(true);
expect(findPagination().exists()).toBe(false);
});
});
describe('with graphql feature flag enabled', () => {
beforeEach(() => {
createComponent(
{},
parsedIssues,
{
graphqlCodeQualityFullReport: true,
},
shallowMount,
);
});
it('renders the pagination component', () => {
expect(findOldPagination().exists()).toBe(false);
expect(findPagination().exists()).toBe(true);
});
});
describe('when there are no codequality issues', () => {
beforeEach(() => {
createComponent({}, []);
......
......@@ -180,3 +180,53 @@ export const parsedIssues = [
'/root/test-codequality/blob/feature-branch/ee/spec/finders/geo/lfs_object_registry_finder_spec.rb#L512',
},
];
export const mockGraphqlResponse = {
data: {
project: {
pipeline: {
codeQualityReports: {
count: 3,
edges: [
{
node: {
line: 33,
description:
'Function `addToImport` has 54 lines of code (exceeds 25 allowed). Consider refactoring.',
path: 'app/assets/javascripts/importer_status.js',
fingerprint: 'f5c4a1a17a8903f8c6dd885142d0e5a7',
severity: 'MAJOR',
},
},
{
node: {
line: 170,
description: 'Avoid too many `return` statements within this function.',
path: 'app/assets/javascripts/ide/stores/utils.js',
fingerprint: '75e7e39f5b8ea0aadff2470a9b44ca68',
severity: 'MINOR',
},
},
{
node: {
line: 44,
description: 'Similar blocks of code found in 3 locations. Consider refactoring.',
path: 'ee/spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb',
fingerprint: '99c054a8b1a7270b193b0a03a6c69cfc',
severity: 'INFO',
},
},
],
},
},
},
},
};
export const mockGraphqlPagination = {
hasNextPage: true,
hasPreviousPage: true,
startCursor: 'abc123',
endCursor: 'abc124',
page: 1,
};
import MockAdapter from 'axios-mock-adapter';
import getCodeQualityViolations from 'ee/codequality_report/graphql/queries/get_code_quality_violations.query.graphql';
import * as actions from 'ee/codequality_report/store/actions';
import { VIEW_EVENT_NAME } from 'ee/codequality_report/store/constants';
import * as types from 'ee/codequality_report/store/mutation_types';
import initialState from 'ee/codequality_report/store/state';
import { gqClient } from 'ee/codequality_report/store/utils';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { unparsedIssues, parsedIssues } from '../mock_data';
import {
unparsedIssues,
parsedIssues,
mockGraphqlResponse,
mockGraphqlPagination,
} from '../mock_data';
jest.mock('~/api.js');
jest.mock('~/flash');
......@@ -18,12 +26,14 @@ describe('Codequality report actions', () => {
const endpoint = `${TEST_HOST}/codequality_report.json`;
const defaultState = {
...initialState(),
endpoint,
};
beforeEach(() => {
mock = new MockAdapter(axios);
state = defaultState;
window.gon = { features: { graphqlCodeQualityFullReport: false } };
});
afterEach(() => {
......@@ -31,8 +41,59 @@ describe('Codequality report actions', () => {
});
describe('setPage', () => {
it('sets the page number', (done) => {
testAction(actions.setPage, 12, state, [{ type: types.SET_PAGE, payload: 12 }], [], done);
it('sets the page number with feature flag disabled', (done) => {
return testAction(
actions.setPage,
12,
state,
[{ type: types.SET_PAGE, payload: { page: 12 } }],
[],
done,
);
});
describe('with the feature flag enabled', () => {
let mockPageInfo;
beforeEach(() => {
window.gon = { features: { graphqlCodeQualityFullReport: true } };
mockPageInfo = {
...mockGraphqlPagination,
currentPage: 11,
};
});
it('sets the next page number', (done) => {
return testAction(
actions.setPage,
12,
{ ...state, pageInfo: mockPageInfo },
[
{
type: types.SET_PAGE,
payload: { after: mockGraphqlPagination.endCursor, currentPage: 12 },
},
],
[{ type: 'fetchReport' }],
done,
);
});
it('sets the previous page number', (done) => {
return testAction(
actions.setPage,
10,
{ ...state, pageInfo: mockPageInfo },
[
{
type: types.SET_PAGE,
payload: { after: mockGraphqlPagination.startCursor, currentPage: 10 },
},
],
[{ type: 'fetchReport' }],
done,
);
});
});
});
......@@ -49,8 +110,8 @@ describe('Codequality report actions', () => {
});
describe('receiveReportSuccess', () => {
it('parses the list of issues from the report', (done) => {
testAction(
it('parses the list of issues from the report with feature flag disabled', (done) => {
return testAction(
actions.receiveReportSuccess,
unparsedIssues,
{ blobPath: '/root/test-codequality/blob/feature-branch', ...state },
......@@ -59,6 +120,25 @@ describe('Codequality report actions', () => {
done,
);
});
it('parses the list of issues from the report with feature flag enabled', (done) => {
window.gon = { features: { graphqlCodeQualityFullReport: true } };
const data = {
edges: unparsedIssues.map((issue) => {
return { node: issue };
}),
};
return testAction(
actions.receiveReportSuccess,
data,
{ blobPath: '/root/test-codequality/blob/feature-branch', ...state },
[{ type: types.RECEIVE_REPORT_SUCCESS_GRAPHQL, payload: { data, parsedIssues } }],
[],
done,
);
});
});
describe('receiveReportError', () => {
......@@ -75,19 +155,83 @@ describe('Codequality report actions', () => {
});
describe('fetchReport', () => {
beforeEach(() => {
mock.onGet(endpoint).replyOnce(200, unparsedIssues);
describe('with graphql feature flag disabled', () => {
beforeEach(() => {
mock.onGet(endpoint).replyOnce(200, unparsedIssues);
});
it('fetches the report', (done) => {
return testAction(
actions.fetchReport,
null,
{ blobPath: 'blah', ...state },
[],
[{ type: 'requestReport' }, { type: 'receiveReportSuccess', payload: unparsedIssues }],
done,
);
});
it('shows a flash message when there is an error', (done) => {
testAction(
actions.fetchReport,
'error',
state,
[],
[{ type: 'requestReport' }, { type: 'receiveReportError', payload: expect.any(Error) }],
() => {
expect(createFlash).toHaveBeenCalledWith({
message: 'There was an error fetching the codequality report.',
});
done();
},
);
});
it('shows an error when blob path is missing', (done) => {
testAction(
actions.fetchReport,
null,
state,
[],
[{ type: 'requestReport' }, { type: 'receiveReportError', payload: expect.any(Error) }],
() => {
expect(createFlash).toHaveBeenCalledWith({
message: 'There was an error fetching the codequality report.',
});
done();
},
);
});
});
it('fetches the report', (done) => {
testAction(
actions.fetchReport,
null,
{ blobPath: 'blah', ...state },
[],
[{ type: 'requestReport' }, { type: 'receiveReportSuccess', payload: unparsedIssues }],
done,
);
describe('with graphql feature flag enabled', () => {
beforeEach(() => {
jest.spyOn(gqClient, 'query').mockResolvedValue(mockGraphqlResponse);
state.paginationData = mockGraphqlPagination;
window.gon = { features: { graphqlCodeQualityFullReport: true } };
});
it('fetches the report', () => {
return testAction(
actions.fetchReport,
null,
{ blobPath: 'blah', ...state },
[],
[
{ type: 'requestReport' },
{
type: 'receiveReportSuccess',
payload: mockGraphqlResponse.data.project.pipeline.codeQualityReports,
},
],
() => {
expect(gqClient.query).toHaveBeenCalledWith({
query: getCodeQualityViolations,
variables: { after: '', first: 25, iid: null, projectPath: null },
});
},
);
});
});
it('shows a flash message when there is an error', (done) => {
......@@ -96,7 +240,7 @@ describe('Codequality report actions', () => {
'error',
state,
[],
[{ type: 'requestReport' }, { type: 'receiveReportError', payload: new Error() }],
[{ type: 'requestReport' }, { type: 'receiveReportError', payload: expect.any(Error) }],
() => {
expect(createFlash).toHaveBeenCalledWith({
message: 'There was an error fetching the codequality report.',
......@@ -112,7 +256,7 @@ describe('Codequality report actions', () => {
null,
state,
[],
[{ type: 'requestReport' }, { type: 'receiveReportError', payload: new Error() }],
[{ type: 'requestReport' }, { type: 'receiveReportError', payload: expect.any(Error) }],
() => {
expect(createFlash).toHaveBeenCalledWith({
message: 'There was an error fetching the codequality report.',
......
......@@ -15,7 +15,7 @@ describe('Codequality report mutations', () => {
describe('set page', () => {
it('should set page', () => {
mutations[types.SET_PAGE](state, 4);
mutations[types.SET_PAGE](state, { page: 4 });
expect(state.pageInfo.page).toBe(4);
});
});
......@@ -27,7 +27,17 @@ describe('Codequality report mutations', () => {
});
});
describe('receive report success', () => {
describe('receive report success with graphql', () => {
it('should set issue info and clear the loading flag', () => {
mutations[types.RECEIVE_REPORT_SUCCESS_GRAPHQL](state, { data: { count: 42 }, parsedIssues });
expect(state.isLoadingCodequality).toBe(false);
expect(state.codequalityIssues).toBe(parsedIssues);
expect(state.pageInfo.count).toBe(42);
});
});
describe('receive report success without graphql', () => {
it('should set issue info and clear the loading flag', () => {
mutations[types.RECEIVE_REPORT_SUCCESS](state, parsedIssues);
......@@ -62,8 +72,10 @@ describe('Codequality report mutations', () => {
expect(state.isLoadingCodequality).toBe(false);
expect(state.loadingCodequalityFailed).toBe(true);
expect(state.allCodequalityIssues).toEqual([]);
expect(state.codequalityIssues).toEqual([]);
expect(state.codeQualityError).toEqual(new Error());
expect(state.pageInfo.total).toBe(0);
expect(state.pageInfo.count).toBe(0);
});
});
});
......@@ -38,6 +38,12 @@ describe('code quality issue body issue body', () => {
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'}
......
......@@ -25,6 +25,18 @@ describe('Codequality report store utils', () => {
});
});
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' } };
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment