Commit bfbc4f78 authored by Sam Beckham's avatar Sam Beckham Committed by Fatih Acet

Moves the license compliance reports to the BE

- Adds actions and mutations to fetch the new endpoint
- Conditionally fetches the old or new reports dependant on a feature
flag
- Tells the LC getter to fetch the new report if it's there, and the old
one if not.
- Adds a bit of a hack to morph the new data in to the old format
(temporarily)
- Pulls in the proper polling endpoints
- Adds tests for all the above
parent 73ee3034
---
title: Moves the license compliance reports to the Backend
merge_request: 17905
author:
type: other
......@@ -15,6 +15,7 @@ export default () => {
canManageLicenses,
apiUrl,
licenseManagementSettingsPath,
licensesApiPath,
} = licensesTab.dataset;
// eslint-disable-next-line no-new
......@@ -27,6 +28,7 @@ export default () => {
return createElement('license-report-app', {
props: {
apiUrl,
licensesApiPath,
licenseManagementSettingsPath,
headPath: licenseHeadPath,
canManageLicenses: parseBoolean(canManageLicenses),
......
......@@ -145,6 +145,9 @@ export default {
!this.mr.autoMergeEnabled
);
},
licensesApiPath() {
return (gl && gl.mrWidgetData && gl.mrWidgetData.license_management_comparison_path) || null;
},
},
created() {
if (this.shouldRenderCodeQuality) {
......@@ -297,6 +300,7 @@ export default {
<mr-widget-licenses
v-if="shouldRenderLicenseReport"
:api-url="mr.licenseManagement.managed_licenses_path"
:licenses-api-path="licensesApiPath"
:pipeline-path="mr.pipeline.path"
:can-manage-licenses="mr.licenseManagement.can_manage_licenses"
:full-report-path="mr.licenseManagement.license_management_full_report_path"
......
......@@ -44,6 +44,11 @@ export default {
type: String,
required: true,
},
licensesApiPath: {
type: String,
required: false,
default: '',
},
canManageLicenses: {
type: Boolean,
required: true,
......@@ -79,20 +84,30 @@ export default {
},
},
mounted() {
const { headPath, basePath, apiUrl, canManageLicenses } = this;
const { headPath, basePath, apiUrl, canManageLicenses, licensesApiPath } = this;
this.setAPISettings({
apiUrlManageLicenses: apiUrl,
headPath,
basePath,
canManageLicenses,
licensesApiPath,
});
this.loadLicenseReport();
this.loadManagedLicenses();
if (gon.features && gon.features.parsedLicenseReport) {
this.loadParsedLicenseReport();
} else {
this.loadLicenseReport();
this.loadManagedLicenses();
}
},
methods: {
...mapActions(['setAPISettings', 'loadManagedLicenses', 'loadLicenseReport']),
...mapActions([
'setAPISettings',
'loadManagedLicenses',
'loadLicenseReport',
'loadParsedLicenseReport',
]),
},
};
</script>
......
import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types';
import { LICENSE_APPROVAL_STATUS } from '../constants';
import { convertToOldReportFormat } from './utils';
import { pollUntilComplete } from '../../security_reports/store/utils';
export const setAPISettings = ({ commit }, data) => {
commit(types.SET_API_SETTINGS, data);
......@@ -61,6 +63,29 @@ export const loadManagedLicenses = ({ dispatch, state }) => {
});
};
export const requestLoadParsedLicenseReport = ({ commit }) => {
commit(types.REQUEST_LOAD_PARSED_LICENSE_REPORT);
};
export const receiveLoadParsedLicenseReport = ({ commit }, reports) => {
commit(types.RECEIVE_LOAD_PARSED_LICENSE_REPORT, reports);
};
export const receiveLoadParsedLicenseReportError = ({ commit }, error) => {
commit(types.RECEIVE_LOAD_PARSED_LICENSE_REPORT_ERROR, error);
};
export const loadParsedLicenseReport = ({ dispatch, state }) => {
dispatch('requestLoadParsedLicenseReport');
pollUntilComplete(state.licensesApiPath)
.then(({ data }) => {
const newLicenses = (data.new_licenses || data).map(convertToOldReportFormat);
const existingLicenses = (data.existing_licenses || []).map(convertToOldReportFormat);
dispatch('receiveLoadParsedLicenseReport', { newLicenses, existingLicenses });
})
.catch(() => {
dispatch('receiveLoadLicenseReportError');
});
};
export const requestLoadLicenseReport = ({ commit }) => {
commit(types.REQUEST_LOAD_LICENSE_REPORT);
};
......
......@@ -4,12 +4,15 @@ import { parseLicenseReportMetrics } from './utils';
export const isLoading = state => state.isLoadingManagedLicenses || state.isLoadingLicenseReport;
export const licenseReport = state =>
parseLicenseReportMetrics(state.headReport, state.baseReport, state.managedLicenses);
gon.features && gon.features.parsedLicenseReport
? state.newLicenses
: parseLicenseReportMetrics(state.headReport, state.baseReport, state.managedLicenses);
export const licenseSummaryText = (state, getters) => {
const hasReportItems = getters.licenseReport && getters.licenseReport.length;
const baseReportHasLicenses =
state.baseReport && state.baseReport.licenses && state.baseReport.licenses.length;
state.existingLicenses.length ||
(state.baseReport && state.baseReport.licenses && state.baseReport.licenses.length);
if (getters.isLoading) {
return sprintf(s__('ciReport|Loading %{reportName} report'), {
......
export const SET_API_SETTINGS = 'SET_API_SETTINGS';
export const RECEIVE_DELETE_LICENSE = 'RECEIVE_DELETE_LICENSE';
export const RECEIVE_DELETE_LICENSE_ERROR = 'RECEIVE_DELETE_LICENSE_ERROR';
export const RECEIVE_LOAD_LICENSE_REPORT = 'RECEIVE_LOAD_LICENSE_REPORT';
export const RECEIVE_LOAD_LICENSE_REPORT_ERROR = 'RECEIVE_LOAD_LICENSE_REPORT_ERROR';
export const RECEIVE_LOAD_MANAGED_LICENSES = 'RECEIVE_LOAD_MANAGED_LICENSES';
export const RECEIVE_LOAD_MANAGED_LICENSES_ERROR = 'RECEIVE_LOAD_MANAGED_LICENSES_ERROR';
export const RECEIVE_LOAD_PARSED_LICENSE_REPORT = 'RECEIVE_LOAD_PARSED_LICENSE_REPORT';
export const RECEIVE_LOAD_PARSED_LICENSE_REPORT_ERROR = 'RECEIVE_LOAD_PARSED_LICENSE_REPORT_ERROR';
export const RECEIVE_SET_LICENSE_APPROVAL = 'RECEIVE_SET_LICENSE_APPROVAL';
export const RECEIVE_SET_LICENSE_APPROVAL_ERROR = 'RECEIVE_SET_LICENSE_APPROVAL_ERROR';
export const RECEIVE_LOAD_LICENSE_REPORT = 'RECEIVE_LOAD_LICENSE_REPORT';
export const RECEIVE_LOAD_LICENSE_REPORT_ERROR = 'RECEIVE_LOAD_LICENSE_REPORT_ERROR';
export const REQUEST_DELETE_LICENSE = 'REQUEST_DELETE_LICENSE';
export const REQUEST_LOAD_LICENSE_REPORT = 'REQUEST_LOAD_LICENSE_REPORT';
export const REQUEST_LOAD_MANAGED_LICENSES = 'REQUEST_LOAD_MANAGED_LICENSES';
export const REQUEST_LOAD_PARSED_LICENSE_REPORT = 'REQUEST_LOAD_PARSED_LICENSE_REPORT';
export const REQUEST_SET_LICENSE_APPROVAL = 'REQUEST_SET_LICENSE_APPROVAL';
export const REQUEST_LOAD_LICENSE_REPORT = 'REQUEST_LOAD_LICENSE_REPORT';
export const SET_LICENSE_IN_MODAL = 'SET_LICENSE_IN_MODAL';
export const RESET_LICENSE_IN_MODAL = 'RESET_LICENSE_IN_MODAL';
export const SET_API_SETTINGS = 'SET_API_SETTINGS';
export const SET_LICENSE_IN_MODAL = 'SET_LICENSE_IN_MODAL';
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -38,6 +38,26 @@ export default {
});
},
[types.RECEIVE_LOAD_PARSED_LICENSE_REPORT](state, { newLicenses, existingLicenses }) {
Object.assign(state, {
newLicenses,
existingLicenses,
isLoadingLicenseReport: false,
loadLicenseReportError: false,
});
},
[types.RECEIVE_LOAD_PARSED_LICENSE_REPORT_ERROR](state, error) {
Object.assign(state, {
isLoadingLicenseReport: false,
loadLicenseReportError: error,
});
},
[types.REQUEST_LOAD_PARSED_LICENSE_REPORT](state) {
Object.assign(state, {
isLoadingLicenseReport: true,
});
},
[types.RECEIVE_LOAD_LICENSE_REPORT](state, reports) {
const { headReport, baseReport } = reports;
......
export default () => ({
apiUrlManageLicenses: null,
headPath: null,
licensesApiPath: null,
basePath: null,
managedLicenses: [],
headReport: null,
baseReport: null,
canManageLicenses: false,
currentLicenseInModal: null,
headPath: null,
headReport: null,
isDeleting: false,
isLoadingManagedLicenses: false,
isLoadingLicenseReport: false,
isLoadingManagedLicenses: false,
isSaving: false,
loadManagedLicensesError: false,
loadLicenseReportError: false,
canManageLicenses: false,
loadManagedLicensesError: false,
managedLicenses: [],
newLicenses: [],
existingLicenses: [],
});
......@@ -154,3 +154,26 @@ export const getPackagesString = (packages, truncate, maxPackages) => {
lastPackage,
});
};
/**
* This converts the newer licence format into the old one so we can use it with our older components.
*
* NOTE: This helper is temporary and can be removed once we flip the `parsedLicenseReport` feature flag
* The below issue is for tracking its removal:
* https://gitlab.com/gitlab-org/gitlab/issues/33878
*
* @param {Object} license The license in the newer format that needs converting
* @returns {Object} The converted license;
*/
export const convertToOldReportFormat = license => {
const approvalStatus = license.classification.approval_status;
return {
...license,
approvalStatus,
id: license.classification.id,
packages: license.dependencies,
status: getIssueStatusFromLicenseStatus(approvalStatus),
};
};
......@@ -457,7 +457,11 @@ export const pollUntilComplete = endpoint =>
const eTagPoll = new Poll({
resource: {
getReports(url) {
return axios.get(url);
return axios.get(url, {
headers: {
'Content-Type': 'application/json',
},
});
},
},
data: endpoint,
......
......@@ -601,4 +601,200 @@ describe('License store actions', () => {
.catch(done.fail);
});
});
describe('requestLoadParsedLicenseReport', () => {
it(`should commit ${mutationTypes.REQUEST_LOAD_PARSED_LICENSE_REPORT}`, done => {
testAction(
actions.requestLoadParsedLicenseReport,
null,
state,
[{ type: mutationTypes.REQUEST_LOAD_PARSED_LICENSE_REPORT }],
[],
)
.then(done)
.catch(done.fail);
});
});
describe('receiveLoadParsedLicenseReport', () => {
it(`should commit ${mutationTypes.RECEIVE_LOAD_PARSED_LICENSE_REPORT} with the correct payload`, done => {
const payload = { newLicenses: [{ name: 'foo' }] };
testAction(
actions.receiveLoadParsedLicenseReport,
payload,
state,
[{ type: mutationTypes.RECEIVE_LOAD_PARSED_LICENSE_REPORT, payload }],
[],
)
.then(done)
.catch(done.fail);
});
});
describe('receiveLoadParsedLicenseReportError', () => {
it(`should commit ${mutationTypes.RECEIVE_LOAD_PARSED_LICENSE_REPORT_ERROR}`, done => {
const payload = new Error('Test');
testAction(
actions.receiveLoadParsedLicenseReportError,
payload,
state,
[{ type: mutationTypes.RECEIVE_LOAD_PARSED_LICENSE_REPORT_ERROR, payload }],
[],
)
.then(done)
.catch(done.fail);
});
});
describe('loadParsedLicenseReport', () => {
const licensesApiPath = `${TEST_HOST}/licensesApiPath`;
let licensesApiMock;
let rawLicenseReport;
beforeEach(() => {
licensesApiMock = axiosMock.onGet(licensesApiPath);
state = {
...createState(),
licensesApiPath,
};
});
describe('pipeline reports', () => {
beforeEach(() => {
rawLicenseReport = [
{
name: 'MIT',
classification: { id: 2, approval_status: 'blacklisted', name: 'MIT' },
dependencies: [{ name: 'vue' }],
count: 1,
url: 'http://opensource.org/licenses/mit-license',
},
];
});
it('should fetch, parse, and dispatch the new licenses on a successful request', done => {
licensesApiMock.replyOnce(() => [200, rawLicenseReport]);
const parsedLicenses = {
existingLicenses: [],
newLicenses: [
{
...rawLicenseReport[0],
id: 2,
approvalStatus: 'blacklisted',
packages: [{ name: 'vue' }],
status: 'failed',
},
],
};
testAction(
actions.loadParsedLicenseReport,
null,
state,
[],
[
{ type: 'requestLoadParsedLicenseReport' },
{ type: 'receiveLoadParsedLicenseReport', payload: parsedLicenses },
],
)
.then(done)
.catch(done.fail);
});
it('should send an error on an unsuccesful request', done => {
licensesApiMock.replyOnce(400);
testAction(
actions.loadParsedLicenseReport,
null,
state,
[],
[{ type: 'requestLoadParsedLicenseReport' }, { type: 'receiveLoadLicenseReportError' }],
)
.then(done)
.catch(done.fail);
});
});
describe('MR widget reports', () => {
beforeEach(() => {
rawLicenseReport = {
new_licenses: [
{
name: 'Apache 2.0',
classification: { id: 1, approval_status: 'approved', name: 'Apache 2.0' },
dependencies: [{ name: 'echarts' }],
count: 1,
url: 'http://www.apache.org/licenses/LICENSE-2.0.txt',
},
{
name: 'New BSD',
classification: { id: 3, approval_status: 'unclassified', name: 'New BSD' },
dependencies: [{ name: 'zrender' }],
count: 1,
url: 'http://opensource.org/licenses/BSD-3-Clause',
},
],
existing_licenses: [
{
name: 'MIT',
classification: { id: 2, approval_status: 'blacklisted', name: 'MIT' },
dependencies: [{ name: 'vue' }],
count: 1,
url: 'http://opensource.org/licenses/mit-license',
},
],
removed_licenses: [],
};
});
it('should fetch, parse, and dispatch the new licenses on a successful request', done => {
licensesApiMock.replyOnce(() => [200, rawLicenseReport]);
const parsedLicenses = {
existingLicenses: [
{
...rawLicenseReport.existing_licenses[0],
id: 2,
approvalStatus: 'blacklisted',
packages: [{ name: 'vue' }],
status: 'failed',
},
],
newLicenses: [
{
...rawLicenseReport.new_licenses[0],
id: 1,
approvalStatus: 'approved',
packages: [{ name: 'echarts' }],
status: 'success',
},
{
...rawLicenseReport.new_licenses[1],
id: 3,
approvalStatus: 'unclassified',
packages: [{ name: 'zrender' }],
status: 'neutral',
},
],
};
testAction(
actions.loadParsedLicenseReport,
null,
state,
[],
[
{ type: 'requestLoadParsedLicenseReport' },
{ type: 'receiveLoadParsedLicenseReport', payload: parsedLicenses },
],
)
.then(done)
.catch(done.fail);
});
});
});
});
import createState from 'ee/vue_shared/license_management/store/state';
import * as getters from 'ee/vue_shared/license_management/store/getters';
import { parseLicenseReportMetrics } from 'ee/vue_shared/license_management/store/utils';
......@@ -9,9 +10,11 @@ import {
} from 'ee_spec/license_management/mock_data';
describe('getters', () => {
let state;
describe('isLoading', () => {
it('is true if `isLoadingManagedLicenses` is true OR `isLoadingLicenseReport` is true', () => {
const state = {};
state = createState();
state.isLoadingManagedLicenses = true;
state.isLoadingLicenseReport = true;
......@@ -32,32 +35,61 @@ describe('getters', () => {
});
describe('licenseReport', () => {
it('returns empty array, if the reports are empty', () => {
const state = { headReport: {}, baseReport: {}, managedLicenses: [] };
describe('with parsedLicenseReport set to false', () => {
beforeAll(() => {
gon.features = gon.features || {};
gon.features.parsedLicenseReport = false;
});
it('returns empty array, if the reports are empty', () => {
state = { ...createState(), headReport: {}, baseReport: {}, managedLicenses: [] };
expect(getters.licenseReport(state)).toEqual([]);
});
expect(getters.licenseReport(state)).toEqual([]);
it('returns license report, if the license report is not loading', () => {
state = {
...createState(),
headReport: licenseHeadIssues,
baseReport: licenseBaseIssues,
managedLicenses: [approvedLicense],
};
expect(getters.licenseReport(state)).toEqual(
parseLicenseReportMetrics(licenseHeadIssues, licenseBaseIssues, [approvedLicense]),
);
});
});
it('returns license report, if the license report is not loading', () => {
const state = {
headReport: licenseHeadIssues,
baseReport: licenseBaseIssues,
managedLicenses: [approvedLicense],
};
describe('with parsedLicenseReport set to true', () => {
beforeAll(() => {
gon.features = gon.features || {};
gon.features.parsedLicenseReport = true;
});
expect(getters.licenseReport(state)).toEqual(
parseLicenseReportMetrics(licenseHeadIssues, licenseBaseIssues, [approvedLicense]),
);
afterAll(() => {
gon.features.parsedLicenseReport = false;
});
it('should return the new licenses from the state', () => {
const newLicenses = { test: 'foo' };
state = { ...createState(), newLicenses };
expect(getters.licenseReport(state)).toBe(newLicenses);
});
});
});
describe('licenseSummaryText', () => {
describe('when licenses exist on both the HEAD and the BASE', () => {
const state = {
loadLicenseReportError: null,
headReport: licenseHeadIssues,
baseReport: licenseBaseIssues,
};
beforeEach(() => {
state = {
...createState(),
loadLicenseReportError: null,
headReport: licenseHeadIssues,
baseReport: licenseBaseIssues,
};
});
it('should be `Loading License Compliance report` text if isLoading', () => {
const mockGetters = {};
......@@ -70,10 +102,11 @@ describe('getters', () => {
it('should be `Failed to load License Compliance report` text if an error has happened', () => {
const mockGetters = {};
state.loadLicenseReportError = new Error('Test');
expect(
getters.licenseSummaryText({ loadLicenseReportError: new Error('Test') }, mockGetters),
).toBe('Failed to load License Compliance report');
expect(getters.licenseSummaryText(state, mockGetters)).toBe(
'Failed to load License Compliance report',
);
});
it('should be `License Compliance detected no new licenses`, if the report is empty', () => {
......@@ -102,7 +135,9 @@ describe('getters', () => {
});
describe('when there are no licences on the BASE', () => {
const state = { baseReport: {} };
beforeEach(() => {
state = { ...createState(), baseReport: {} };
});
it('should be `License Compliance detected no licenses for the source branch only` with no new licences', () => {
const mockGetters = { licenseReport: [] };
......
......@@ -229,4 +229,53 @@ describe('License store mutations', () => {
expect(store.state.isLoadingLicenseReport).toBe(true);
});
});
describe('RECEIVE_LOAD_PARSED_LICENSE_REPORT', () => {
const newLicenses = [];
const existingLicenses = [];
beforeEach(() => {
store.state.isLoadingLicenseReport = true;
store.state.loadLicenseReportError = new Error('test');
store.commit(types.RECEIVE_LOAD_PARSED_LICENSE_REPORT, { newLicenses, existingLicenses });
});
it('should set the new and existing reports', () => {
expect(store.state.newLicenses).toBe(newLicenses);
expect(store.state.existingLicenses).toBe(existingLicenses);
});
it('should cancel loading and clear any errors', () => {
expect(store.state.isLoadingLicenseReport).toBe(false);
expect(store.state.loadLicenseReportError).toBe(false);
});
});
describe('RECEIVE_LOAD_PARSED_LICENSE_REPORT_ERROR', () => {
const error = new Error('test');
beforeEach(() => {
store.state.isLoadingLicenseReport = true;
store.state.loadLicenseReportError = false;
store.commit(types.RECEIVE_LOAD_PARSED_LICENSE_REPORT_ERROR, error);
});
it('should set the error on the state', () => {
expect(store.state.loadLicenseReportError).toBe(error);
});
it('should cancel loading', () => {
expect(store.state.isLoadingLicenseReport).toBe(false);
});
});
describe('REQUEST_LOAD_PARSED_LICENSE_REPORT', () => {
beforeEach(() => {
store.state.isLoadingLicenseReport = false;
store.commit(types.REQUEST_LOAD_PARSED_LICENSE_REPORT);
});
it('should initiate loading', () => {
expect(store.state.isLoadingLicenseReport).toBe(true);
});
});
});
......@@ -4,6 +4,7 @@ import {
normalizeLicense,
getPackagesString,
getIssueStatusFromLicenseStatus,
convertToOldReportFormat,
} from 'ee/vue_shared/license_management/store/utils';
import { LICENSE_APPROVAL_STATUS } from 'ee/vue_shared/license_management/constants';
import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
......@@ -164,4 +165,40 @@ describe('utils', () => {
expect(getIssueStatusFromLicenseStatus()).toBe(STATUS_NEUTRAL);
});
});
describe('convertToOldReportFormat', () => {
const rawLicense = {
name: 'license',
classification: {
id: 1,
approval_status: LICENSE_APPROVAL_STATUS.APPROVED,
},
dependencies: [{ id: 1 }, { id: 2 }, { id: 3 }],
};
let parsedLicense;
beforeEach(() => {
parsedLicense = convertToOldReportFormat(rawLicense);
});
it('should get the approval status', () => {
expect(parsedLicense.approvalStatus).toEqual(rawLicense.classification.approval_status);
});
it('should get the packages', () => {
expect(parsedLicense.packages).toEqual(rawLicense.dependencies);
});
it('should get the id', () => {
expect(parsedLicense.id).toEqual(rawLicense.classification.id);
});
it('should get the status', () => {
expect(parsedLicense.status).toEqual(STATUS_SUCCESS);
});
it('should retain the license name', () => {
expect(parsedLicense.name).toEqual(rawLicense.name);
});
});
});
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