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 { ...@@ -33,17 +33,20 @@ export default {
issueName() { issueName() {
return `${this.severityLabel} - ${this.issue.name}`; return `${this.severityLabel} - ${this.issue.name}`;
}, },
issueSeverity() {
return this.issue.severity.toLowerCase();
},
isStatusSuccess() { isStatusSuccess() {
return this.status === STATUS_SUCCESS; return this.status === STATUS_SUCCESS;
}, },
severityClass() { severityClass() {
return SEVERITY_CLASSES[this.issue.severity] || SEVERITY_CLASSES.unknown; return SEVERITY_CLASSES[this.issueSeverity] || SEVERITY_CLASSES.unknown;
}, },
severityIcon() { severityIcon() {
return SEVERITY_ICONS[this.issue.severity] || SEVERITY_ICONS.unknown; return SEVERITY_ICONS[this.issueSeverity] || SEVERITY_ICONS.unknown;
}, },
severityLabel() { severityLabel() {
return this.$options.severityText[this.issue.severity] || this.$options.severityText.unknown; return this.$options.severityText[this.issueSeverity] || this.$options.severityText.unknown;
}, },
}, },
severityText: { severityText: {
......
export const parseCodeclimateMetrics = (issues = [], path = '') => { export const parseCodeclimateMetrics = (issues = [], blobPath = '') => {
return issues.map((issue) => { 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 = { const parsedIssue = {
name: issue.description, name: issue.description,
path: issue.file_path, path: issuePath,
urlPath: `${path}/${issue.file_path}#L${issue.line}`, urlPath: `${blobPath}/${issuePath}#L${issue.line}`,
...issue, ...issue,
}; };
if (issue?.location?.path) { if (issue?.location?.path) {
let parseCodeQualityUrl = `${path}/${issue.location.path}`; let parseCodeQualityUrl = `${blobPath}/${issue.location.path}`;
parsedIssue.path = issue.location.path; parsedIssue.path = issue.location.path;
if (issue?.location?.lines?.begin) { if (issue?.location?.lines?.begin) {
......
<script> <script>
import { GlPagination, GlSkeletonLoader } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { componentNames } from 'ee/reports/components/issue_body'; import { componentNames } from 'ee/reports/components/issue_body';
import reportsMixin from 'ee/vue_shared/security_reports/mixins/reports_mixin'; import reportsMixin from 'ee/vue_shared/security_reports/mixins/reports_mixin';
import { n__, s__, sprintf } from '~/locale'; import { n__, s__, sprintf } from '~/locale';
import ReportSection from '~/reports/components/report_section.vue'; import ReportSection from '~/reports/components/report_section.vue';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
components: { components: {
ReportSection, ReportSection,
PaginationLinks, PaginationLinks,
GlSkeletonLoader,
GlPagination,
}, },
mixins: [reportsMixin], mixins: [reportsMixin, glFeatureFlagsMixin()],
componentNames, componentNames,
computed: { computed: {
...mapState(['isLoadingCodequality', 'loadingCodequalityFailed', 'pageInfo']), ...mapState(['isLoadingCodequality', 'loadingCodequalityFailed', 'pageInfo']),
...mapGetters(['codequalityIssues', 'codequalityIssueTotal']), ...mapGetters(['codequalityIssues', 'codequalityIssueTotal']),
prevPage() {
return Math.max(this.pageInfo.currentPage - 1, 0);
},
nextPage() {
return this.pageInfo?.hasNextPage ? this.pageInfo.currentPage + 1 : null;
},
hasCodequalityIssues() { hasCodequalityIssues() {
return this.codequalityIssueTotal > 0; return this.codequalityIssueTotal > 0;
}, },
codequalityText() { codequalityText() {
const text = []; const text = [];
const { codequalityIssueTotal } = this; const count = this.codequalityIssueTotal;
if (codequalityIssueTotal === 0) { if (count === 0) {
return s__('ciReport|No code quality issues found'); return s__('ciReport|No code quality issues found');
} else if (codequalityIssueTotal > 0) { } else if (count > 0) {
return sprintf(s__('ciReport|Found %{issuesWithCount}'), { return sprintf(s__('ciReport|Found %{issuesWithCount}'), {
issuesWithCount: n__( issuesWithCount: n__('%d code quality issue', '%d code quality issues', count),
'%d code quality issue',
'%d code quality issues',
codequalityIssueTotal,
),
}); });
} }
...@@ -70,13 +76,27 @@ export default { ...@@ -70,13 +76,27 @@ export default {
:success-text="codequalityText" :success-text="codequalityText"
:unresolved-issues="codequalityIssues" :unresolved-issues="codequalityIssues"
:resolved-issues="[]" :resolved-issues="[]"
:has-issues="hasCodequalityIssues" :has-issues="hasCodequalityIssues && !isLoadingCodequality"
:component="$options.componentNames.CodequalityIssueBody" :component="$options.componentNames.CodequalityIssueBody"
class="codequality-report" class="codequality-report"
> >
<template v-if="hasCodequalityIssues" #sub-heading>{{ $options.i18n.subHeading }}</template> <template v-if="hasCodequalityIssues" #sub-heading>{{ $options.i18n.subHeading }}</template>
</report-section> </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 <pagination-links
v-else
:change="setPage" :change="setPage"
:page-info="pageInfo" :page-info="pageInfo"
class="d-flex justify-content-center gl-mt-3" 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'; ...@@ -4,10 +4,30 @@ import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { parseCodeclimateMetrics } from '~/reports/codequality_report/store/utils/codequality_parser'; 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 { VIEW_EVENT_NAME } from './constants';
import * as types from './mutation_types'; 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 }) => { export const requestReport = ({ commit }) => {
commit(types.REQUEST_REPORT); commit(types.REQUEST_REPORT);
...@@ -15,24 +35,49 @@ export const requestReport = ({ commit }) => { ...@@ -15,24 +35,49 @@ export const requestReport = ({ commit }) => {
Api.trackRedisHllUserEvent(VIEW_EVENT_NAME); Api.trackRedisHllUserEvent(VIEW_EVENT_NAME);
}; };
export const receiveReportSuccess = ({ state, commit }, data) => { 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); 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 receiveReportError = ({ commit }, error) => commit(types.RECEIVE_REPORT_ERROR, error);
export const fetchReport = ({ state, dispatch }) => { export const fetchReport = async ({ state, dispatch }) => {
try {
dispatch('requestReport'); 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,
};
axios await gqClient
.get(state.endpoint) .query({
query: getCodeQualityViolations,
variables,
})
.then(({ data }) => { .then(({ data }) => {
if (!state.blobPath) throw new Error(); dispatch('receiveReportSuccess', data.project?.pipeline?.codeQualityReports);
});
} else {
await axios.get(state.endpoint).then(({ data }) => {
dispatch('receiveReportSuccess', data); dispatch('receiveReportSuccess', data);
}) });
.catch((error) => { }
} catch (error) {
dispatch('receiveReportError', error); dispatch('receiveReportError', error);
createFlash({ createFlash({
message: s__('ciReport|There was an error fetching the codequality report.'), 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 VIEW_EVENT_NAME = 'i_testing_full_code_quality_report_total';
export const PAGE_SIZE = 25;
export const SEVERITY_SORT_ORDER = { export const SEVERITY_SORT_ORDER = {
unknown: 0, unknown: 0,
blocker: 1, blocker: 1,
......
export const codequalityIssues = (state) => { export const codequalityIssues = (state) => {
if (gon.features?.graphqlCodeQualityFullReport) {
return state.codequalityIssues;
}
const { page, perPage } = state.pageInfo; const { page, perPage } = state.pageInfo;
const start = (page - 1) * perPage; const start = (page - 1) * perPage;
return state.allCodequalityIssues.slice(start, start + 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 SET_PAGE = 'SET_PAGE';
export const REQUEST_REPORT = 'REQUEST_REPORT'; 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_SUCCESS = 'RECEIVE_REPORT_SUCCESS';
export const RECEIVE_REPORT_ERROR = 'RECEIVE_REPORT_ERROR'; export const RECEIVE_REPORT_ERROR = 'RECEIVE_REPORT_ERROR';
...@@ -2,16 +2,25 @@ import { SEVERITY_SORT_ORDER } from './constants'; ...@@ -2,16 +2,25 @@ import { SEVERITY_SORT_ORDER } from './constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
[types.SET_PAGE](state, page) { [types.SET_PAGE](state, pageInfo) {
Object.assign(state, { Object.assign(state, {
pageInfo: Object.assign(state.pageInfo, { pageInfo: Object.assign(state.pageInfo, pageInfo),
page,
}),
}); });
}, },
[types.REQUEST_REPORT](state) { [types.REQUEST_REPORT](state) {
Object.assign(state, { isLoadingCodequality: true }); 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) { [types.RECEIVE_REPORT_SUCCESS](state, allCodequalityIssues) {
Object.assign(state, { Object.assign(state, {
isLoadingCodequality: false, isLoadingCodequality: false,
...@@ -29,10 +38,12 @@ export default { ...@@ -29,10 +38,12 @@ export default {
Object.assign(state, { Object.assign(state, {
isLoadingCodequality: false, isLoadingCodequality: false,
allCodequalityIssues: [], allCodequalityIssues: [],
codequalityIssues: [],
loadingCodequalityFailed: true, loadingCodequalityFailed: true,
codeQualityError, codeQualityError,
pageInfo: Object.assign(state.pageInfo, { pageInfo: Object.assign(state.pageInfo, {
total: 0, total: 0,
count: 0,
}), }),
}); });
}, },
......
import { PAGE_SIZE } from './constants';
export default () => ({ export default () => ({
projectPath: null,
pipelineIid: null,
endpoint: '', endpoint: '',
allCodequalityIssues: [], allCodequalityIssues: [],
codequalityIssues: [],
isLoadingCodequality: false, isLoadingCodequality: false,
loadingCodequalityFailed: false, loadingCodequalityFailed: false,
codeQualityError: null, codeQualityError: null,
pageInfo: { pageInfo: {
page: 1, page: 1,
perPage: 25, perPage: PAGE_SIZE,
total: 0, 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 () => { ...@@ -11,8 +11,18 @@ export default () => {
if (tabsElement && codequalityTab) { if (tabsElement && codequalityTab) {
const fetchReportAction = 'fetchReport'; const fetchReportAction = 'fetchReport';
const { codequalityReportDownloadPath, blobPath } = codequalityTab.dataset; const {
const store = createStore({ endpoint: codequalityReportDownloadPath, blobPath }); codequalityReportDownloadPath,
blobPath,
projectPath,
pipelineIid,
} = codequalityTab.dataset;
const store = createStore({
endpoint: codequalityReportDownloadPath,
blobPath,
projectPath,
pipelineIid,
});
const isCodequalityTabActive = Boolean( const isCodequalityTabActive = Boolean(
document.querySelector('.pipelines-tabs > li > a.codequality-tab.active'), document.querySelector('.pipelines-tabs > li > a.codequality-tab.active'),
......
...@@ -10,6 +10,7 @@ module EE ...@@ -10,6 +10,7 @@ module EE
before_action :authorize_read_licenses!, only: [:licenses] before_action :authorize_read_licenses!, only: [:licenses]
before_action do before_action do
push_frontend_feature_flag(:pipeline_security_dashboard_graphql, project, type: :development, default_enabled: :yaml) 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 end
feature_category :license_compliance, [:licenses] feature_category :license_compliance, [:licenses]
......
...@@ -34,7 +34,9 @@ ...@@ -34,7 +34,9 @@
licenses_api_path: licenses_api_path, licenses_api_path: licenses_api_path,
can_manage_licenses: can?(current_user, :admin_software_license_policy, project).to_s } } 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-tab-codequality.tab-pane
#js-pipeline-codequality-report{ data: { codequality_report_download_path: codequality_report_download_path, #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 Vuex from 'vuex';
import CodequalityReportApp from 'ee/codequality_report/codequality_report.vue'; import CodequalityReportApp from 'ee/codequality_report/codequality_report.vue';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import { parsedIssues } from './mock_data'; import { parsedIssues } from './mock_data';
jest.mock('~/flash'); jest.mock('~/flash');
...@@ -12,10 +14,11 @@ describe('Codequality report app', () => { ...@@ -12,10 +14,11 @@ describe('Codequality report app', () => {
let wrapper; let wrapper;
let store; let store;
const createComponent = (state = {}, issues = [], glFeatures = {}) => { const createComponent = (state = {}, issues = [], glFeatures = {}, mountFn = mount) => {
store = new Vuex.Store({ store = new Vuex.Store({
state: { state: {
pageInfo: {}, pageInfo: {},
isLoadingCodequality: false,
...state, ...state,
}, },
getters: { getters: {
...@@ -24,7 +27,7 @@ describe('Codequality report app', () => { ...@@ -24,7 +27,7 @@ describe('Codequality report app', () => {
}, },
}); });
wrapper = mount(CodequalityReportApp, { wrapper = mountFn(CodequalityReportApp, {
localVue, localVue,
store, store,
provide: { provide: {
...@@ -36,6 +39,9 @@ describe('Codequality report app', () => { ...@@ -36,6 +39,9 @@ describe('Codequality report app', () => {
const findStatus = () => wrapper.find('.js-code-text'); const findStatus = () => wrapper.find('.js-code-text');
const findSuccessIcon = () => wrapper.find('.js-ci-status-icon-success'); const findSuccessIcon = () => wrapper.find('.js-ci-status-icon-success');
const findWarningIcon = () => wrapper.find('.js-ci-status-icon-warning'); 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(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -48,6 +54,7 @@ describe('Codequality report app', () => { ...@@ -48,6 +54,7 @@ describe('Codequality report app', () => {
it('shows a loading state', () => { it('shows a loading state', () => {
expect(findStatus().text()).toBe('Loading codeclimate report'); expect(findStatus().text()).toBe('Loading codeclimate report');
expect(findSkeletonLoader().exists()).toBe(true);
}); });
}); });
...@@ -88,6 +95,42 @@ describe('Codequality report app', () => { ...@@ -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', () => { describe('when there are no codequality issues', () => {
beforeEach(() => { beforeEach(() => {
createComponent({}, []); createComponent({}, []);
......
...@@ -180,3 +180,53 @@ export const parsedIssues = [ ...@@ -180,3 +180,53 @@ export const parsedIssues = [
'/root/test-codequality/blob/feature-branch/ee/spec/finders/geo/lfs_object_registry_finder_spec.rb#L512', '/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 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 * as actions from 'ee/codequality_report/store/actions';
import { VIEW_EVENT_NAME } from 'ee/codequality_report/store/constants'; import { VIEW_EVENT_NAME } from 'ee/codequality_report/store/constants';
import * as types from 'ee/codequality_report/store/mutation_types'; 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 { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import Api from '~/api'; import Api from '~/api';
import createFlash from '~/flash'; import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils'; 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('~/api.js');
jest.mock('~/flash'); jest.mock('~/flash');
...@@ -18,12 +26,14 @@ describe('Codequality report actions', () => { ...@@ -18,12 +26,14 @@ describe('Codequality report actions', () => {
const endpoint = `${TEST_HOST}/codequality_report.json`; const endpoint = `${TEST_HOST}/codequality_report.json`;
const defaultState = { const defaultState = {
...initialState(),
endpoint, endpoint,
}; };
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
state = defaultState; state = defaultState;
window.gon = { features: { graphqlCodeQualityFullReport: false } };
}); });
afterEach(() => { afterEach(() => {
...@@ -31,8 +41,59 @@ describe('Codequality report actions', () => { ...@@ -31,8 +41,59 @@ describe('Codequality report actions', () => {
}); });
describe('setPage', () => { describe('setPage', () => {
it('sets the page number', (done) => { it('sets the page number with feature flag disabled', (done) => {
testAction(actions.setPage, 12, state, [{ type: types.SET_PAGE, payload: 12 }], [], 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', () => { ...@@ -49,8 +110,8 @@ describe('Codequality report actions', () => {
}); });
describe('receiveReportSuccess', () => { describe('receiveReportSuccess', () => {
it('parses the list of issues from the report', (done) => { it('parses the list of issues from the report with feature flag disabled', (done) => {
testAction( return testAction(
actions.receiveReportSuccess, actions.receiveReportSuccess,
unparsedIssues, unparsedIssues,
{ blobPath: '/root/test-codequality/blob/feature-branch', ...state }, { blobPath: '/root/test-codequality/blob/feature-branch', ...state },
...@@ -59,6 +120,25 @@ describe('Codequality report actions', () => { ...@@ -59,6 +120,25 @@ describe('Codequality report actions', () => {
done, 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', () => { describe('receiveReportError', () => {
...@@ -75,12 +155,13 @@ describe('Codequality report actions', () => { ...@@ -75,12 +155,13 @@ describe('Codequality report actions', () => {
}); });
describe('fetchReport', () => { describe('fetchReport', () => {
describe('with graphql feature flag disabled', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(endpoint).replyOnce(200, unparsedIssues); mock.onGet(endpoint).replyOnce(200, unparsedIssues);
}); });
it('fetches the report', (done) => { it('fetches the report', (done) => {
testAction( return testAction(
actions.fetchReport, actions.fetchReport,
null, null,
{ blobPath: 'blah', ...state }, { blobPath: 'blah', ...state },
...@@ -96,7 +177,70 @@ describe('Codequality report actions', () => { ...@@ -96,7 +177,70 @@ describe('Codequality report actions', () => {
'error', 'error',
state, 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.',
});
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();
},
);
});
});
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) => {
testAction(
actions.fetchReport,
'error',
state,
[],
[{ type: 'requestReport' }, { type: 'receiveReportError', payload: expect.any(Error) }],
() => { () => {
expect(createFlash).toHaveBeenCalledWith({ expect(createFlash).toHaveBeenCalledWith({
message: 'There was an error fetching the codequality report.', message: 'There was an error fetching the codequality report.',
...@@ -112,7 +256,7 @@ describe('Codequality report actions', () => { ...@@ -112,7 +256,7 @@ describe('Codequality report actions', () => {
null, null,
state, state,
[], [],
[{ type: 'requestReport' }, { type: 'receiveReportError', payload: new Error() }], [{ type: 'requestReport' }, { type: 'receiveReportError', payload: expect.any(Error) }],
() => { () => {
expect(createFlash).toHaveBeenCalledWith({ expect(createFlash).toHaveBeenCalledWith({
message: 'There was an error fetching the codequality report.', message: 'There was an error fetching the codequality report.',
......
...@@ -15,7 +15,7 @@ describe('Codequality report mutations', () => { ...@@ -15,7 +15,7 @@ describe('Codequality report mutations', () => {
describe('set page', () => { describe('set page', () => {
it('should 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); expect(state.pageInfo.page).toBe(4);
}); });
}); });
...@@ -27,7 +27,17 @@ describe('Codequality report mutations', () => { ...@@ -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', () => { it('should set issue info and clear the loading flag', () => {
mutations[types.RECEIVE_REPORT_SUCCESS](state, parsedIssues); mutations[types.RECEIVE_REPORT_SUCCESS](state, parsedIssues);
...@@ -62,8 +72,10 @@ describe('Codequality report mutations', () => { ...@@ -62,8 +72,10 @@ describe('Codequality report mutations', () => {
expect(state.isLoadingCodequality).toBe(false); expect(state.isLoadingCodequality).toBe(false);
expect(state.loadingCodequalityFailed).toBe(true); expect(state.loadingCodequalityFailed).toBe(true);
expect(state.allCodequalityIssues).toEqual([]); expect(state.allCodequalityIssues).toEqual([]);
expect(state.codequalityIssues).toEqual([]);
expect(state.codeQualityError).toEqual(new Error()); expect(state.codeQualityError).toEqual(new Error());
expect(state.pageInfo.total).toBe(0); expect(state.pageInfo.total).toBe(0);
expect(state.pageInfo.count).toBe(0);
}); });
}); });
}); });
...@@ -38,6 +38,12 @@ describe('code quality issue body issue body', () => { ...@@ -38,6 +38,12 @@ describe('code quality issue body issue body', () => {
describe('severity rating', () => { describe('severity rating', () => {
it.each` it.each`
severity | iconClass | iconName 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'} ${'info'} | ${'text-primary-400'} | ${'severity-info'}
${'minor'} | ${'text-warning-200'} | ${'severity-low'} ${'minor'} | ${'text-warning-200'} | ${'severity-low'}
${'major'} | ${'text-warning-400'} | ${'severity-medium'} ${'major'} | ${'text-warning-400'} | ${'severity-medium'}
......
...@@ -25,6 +25,18 @@ describe('Codequality report store utils', () => { ...@@ -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', () => { describe('when an issue has a path but no line', () => {
const issue = { description: 'Insecure Dependency', location: { path: 'Gemfile.lock' } }; 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