Commit 6d29ff33 authored by Jannik Lehmann's avatar Jannik Lehmann Committed by Phil Hughes

License compliance MR widget extension

* Add component and unit tests
parent 4bfa8a0e
import { s__, n__, __, sprintf } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
import { parseDependencies } from './utils';
// TODO: Clean up both status versions as part of https://gitlab.com/gitlab-org/gitlab/-/issues/356206
const APPROVAL_STATUS_TO_ICON = {
allowed: EXTENSION_ICONS.success,
approved: EXTENSION_ICONS.success,
denied: EXTENSION_ICONS.failed,
blacklisted: EXTENSION_ICONS.failed,
unclassified: EXTENSION_ICONS.notice,
};
export default {
name: 'WidgetLicenseCompliance',
i18n: {
label: s__('ciReport|License Compliance'),
loading: s__('ciReport|License Compliance test metrics results are being parsed'),
error: s__('ciReport|License Compliance failed loading results'),
},
expandEvent: 'i_testing_license_compliance_widget_total',
props: ['licenseCompliance'],
computed: {
summary() {
if (
this.collapsedData[0].new_licenses.length > 0 &&
this.collapsedData[0].removed_licenses.length > 0
) {
const newLicenses = n__(
'%d new license',
'%d new licenses',
this.collapsedData[0].new_licenses.length,
);
const removedLicenses = n__(
'%d removed license',
'%d removed licenses',
this.collapsedData[0].removed_licenses.length,
);
return sprintf(__(`License Compliance detected ${newLicenses} and ${removedLicenses}`));
} else if (this.collapsedData[0].new_licenses.length > 0) {
return n__(
'LicenseCompliance|License Compliance detected %d new license',
'LicenseCompliance|License Compliance detected %d new licenses',
this.collapsedData[0].new_licenses.length,
);
} else if (this.collapsedData[0].removed_licenses.length > 0) {
return n__(
'LicenseCompliance|License Compliance detected %d removed license',
'LicenseCompliance|License Compliance detected %d removed licenses',
this.collapsedData[0].removed_licenses.length,
);
}
return s__('LicenseCompliance|License Compliance detected no new licenses');
},
statusIcon() {
if (this.collapsedData[0].new_licenses.length === 0) {
return EXTENSION_ICONS.success;
}
return EXTENSION_ICONS.warning;
},
},
methods: {
fetchCollapsedData() {
const { license_scanning_comparison_path } = this.licenseCompliance;
return Promise.all([this.fetchReport(license_scanning_comparison_path)]).then(
(values) => values,
);
},
fetchFullData() {
const { license_scanning_comparison_path } = this.licenseCompliance;
return Promise.all([this.fetchReport(license_scanning_comparison_path)]).then((values) => {
let newLicenses = values[0].new_licenses;
newLicenses = newLicenses.map((e) => ({
status: e.classification.approval_status,
icon: {
name: APPROVAL_STATUS_TO_ICON[e.classification.approval_status],
},
link: {
href: e.url,
text: e.name,
},
supportingText: `${s__('License Compliance| Used by')} ${parseDependencies(
e.dependencies,
)}`,
}));
const groupedLicenses = newLicenses.reduce(
(licenses, license) => ({
...licenses,
[license.status]: [...(licenses[license.status] || []), license],
}),
{},
);
// TODO: Clean up both status versions as part of https://gitlab.com/gitlab-org/gitlab/-/issues/356206
const licenseSections = [
...(groupedLicenses.denied?.length > 0 || groupedLicenses.blacklisted?.length > 0
? [
{
header: s__('LicenseCompliance|Denied'),
text: s__(
"LicenseCompliance|Out-of-compliance with the project's policies and should be removed",
),
children: groupedLicenses.denied || groupedLicenses.blacklisted,
},
]
: []),
...(groupedLicenses.unclassified?.length > 0
? [
{
header: s__('LicenseCompliance|Uncategorized'),
text: s__('LicenseCompliance|No policy matches this license'),
children: groupedLicenses.unclassified,
},
]
: []),
...(groupedLicenses.allowed?.length > 0 || groupedLicenses.approved?.length > 0
? [
{
header: s__('LicenseCompliance|Allowed'),
text: s__('LicenseCompliance|Acceptable for use in this project'),
children: groupedLicenses.allowed || groupedLicenses.approved,
},
]
: []),
];
return licenseSections;
});
},
fetchReport(endpoint) {
return axios.get(endpoint).then((res) => res.data);
},
},
};
export const parseDependencies = (dependencies) => {
return dependencies
.map((dependency) => {
return dependency.name;
})
.join(', ');
};
...@@ -13,6 +13,7 @@ import loadPerformanceExtension from './extensions/load_performance'; ...@@ -13,6 +13,7 @@ import loadPerformanceExtension from './extensions/load_performance';
import browserPerformanceExtension from './extensions/browser_performance'; import browserPerformanceExtension from './extensions/browser_performance';
import statusChecksExtension from './extensions/status_checks'; import statusChecksExtension from './extensions/status_checks';
import metricsExtension from './extensions/metrics'; import metricsExtension from './extensions/metrics';
import licenseComplianceExtension from './extensions/license_compliance';
export default { export default {
components: { components: {
...@@ -53,7 +54,7 @@ export default { ...@@ -53,7 +54,7 @@ export default {
}, },
computed: { computed: {
shouldRenderLicenseReport() { shouldRenderLicenseReport() {
return this.mr.enabledReports?.licenseScanning; return this.mr?.enabledReports?.licenseScanning;
}, },
hasBrowserPerformanceMetrics() { hasBrowserPerformanceMetrics() {
return ( return (
...@@ -211,8 +212,18 @@ export default { ...@@ -211,8 +212,18 @@ export default {
this.registerMetrics(); this.registerMetrics();
} }
}, },
shouldRenderLicenseReport(newVal) {
if (newVal) {
this.registerLicenseCompliance();
}
},
}, },
methods: { methods: {
registerLicenseCompliance() {
if (this.shouldShowExtension) {
registerExtension(licenseComplianceExtension);
}
},
registerLoadPerformance() { registerLoadPerformance() {
if (this.shouldShowExtension) { if (this.shouldShowExtension) {
registerExtension(loadPerformanceExtension); registerExtension(loadPerformanceExtension);
...@@ -464,7 +475,7 @@ export default { ...@@ -464,7 +475,7 @@ export default {
</mr-widget-enable-feature-prompt> </mr-widget-enable-feature-prompt>
<mr-widget-licenses <mr-widget-licenses
v-if="shouldRenderLicenseReport" v-if="shouldRenderLicenseReport && !shouldShowExtension"
:api-url="mr.licenseScanning.managed_licenses_path" :api-url="mr.licenseScanning.managed_licenses_path"
:approvals-api-path="mr.apiApprovalsPath" :approvals-api-path="mr.apiApprovalsPath"
:licenses-api-path="licensesApiPath" :licenses-api-path="licensesApiPath"
......
...@@ -26,10 +26,12 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -26,10 +26,12 @@ export default class MergeRequestStore extends CEMergeRequestStore {
data.create_vulnerability_feedback_dismissal_path; data.create_vulnerability_feedback_dismissal_path;
this.visualReviewAppAvailable = Boolean(data.visual_review_app_available); this.visualReviewAppAvailable = Boolean(data.visual_review_app_available);
this.appUrl = gon && gon.gitlab_url; this.appUrl = gon && gon.gitlab_url;
this.licenseScanning = data.license_scanning;
this.initBrowserPerformanceReport(data); this.initBrowserPerformanceReport(data);
this.initLoadPerformanceReport(data); this.initLoadPerformanceReport(data);
this.licenseScanning = data.license_scanning; this.initLicenseComplianceReport(data);
this.metricsReportsPath = data.metrics_reports_path; this.metricsReportsPath = data.metrics_reports_path;
this.enabledReports = convertObjectPropsToCamelCase(data.enabled_reports); this.enabledReports = convertObjectPropsToCamelCase(data.enabled_reports);
...@@ -119,6 +121,13 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -119,6 +121,13 @@ export default class MergeRequestStore extends CEMergeRequestStore {
}; };
} }
initLicenseComplianceReport({ license_scanning_comparison_path, api_approvals_path }) {
this.licenseCompliance = {
license_scanning_comparison_path,
api_approvals_path,
};
}
compareBrowserPerformanceMetrics(headMetrics, baseMetrics) { compareBrowserPerformanceMetrics(headMetrics, baseMetrics) {
const headMetricsIndexed = MergeRequestStore.normalizeBrowserPerformanceMetrics(headMetrics); const headMetricsIndexed = MergeRequestStore.normalizeBrowserPerformanceMetrics(headMetrics);
const baseMetricsIndexed = MergeRequestStore.normalizeBrowserPerformanceMetrics(baseMetrics); const baseMetricsIndexed = MergeRequestStore.normalizeBrowserPerformanceMetrics(baseMetrics);
......
import MockAdapter from 'axios-mock-adapter';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
import licenseComplianceExtension from 'ee/vue_merge_request_widget/extensions/license_compliance';
import httpStatusCodes from '~/lib/utils/http_status';
import {
licenseComplianceSuccess,
licenseComplianceRemovedLicenses,
licenseComplianceNewAndRemovedLicenses,
licenseComplianceEmpty,
} from './mock_data';
describe('License Compliance extension', () => {
let wrapper;
let mock;
registerExtension(licenseComplianceExtension);
const endpoint = '/group-name/project-name/-/merge_requests/78/license_scanning_reports';
const mockApi = (statusCode, data) => {
mock.onGet(endpoint).reply(statusCode, data);
};
const findToggleCollapsedButton = () => wrapper.findByTestId('toggle-button');
const findAllExtensionListItems = () => wrapper.findAllByTestId('extension-list-item');
const createComponent = () => {
wrapper = mountExtended(extensionsContainer, {
propsData: {
mr: {
licenseCompliance: {
license_scanning_comparison_path: endpoint,
api_approvals_path: endpoint,
},
},
},
});
};
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
mock.restore();
});
describe('summary', () => {
it('displays loading text', () => {
mockApi(httpStatusCodes.OK, licenseComplianceSuccess);
createComponent();
expect(wrapper.text()).toBe('License Compliance test metrics results are being parsed');
});
it('displays failed loading text', async () => {
mockApi(httpStatusCodes.INTERNAL_SERVER_ERROR);
createComponent();
await waitForPromises();
expect(wrapper.text()).toBe('License Compliance failed loading results');
});
it('displays no licenses', async () => {
mockApi(httpStatusCodes.OK, licenseComplianceEmpty);
createComponent();
await waitForPromises();
expect(wrapper.text()).toBe('License Compliance detected no new licenses');
});
it('displays new licenses count', async () => {
mockApi(httpStatusCodes.OK, licenseComplianceSuccess);
createComponent();
await waitForPromises();
expect(wrapper.text()).toBe('License Compliance detected 3 new licenses');
});
it('displays removed licenses count', async () => {
mockApi(httpStatusCodes.OK, licenseComplianceRemovedLicenses);
createComponent();
await waitForPromises();
expect(wrapper.text()).toBe('License Compliance detected 3 removed licenses');
});
it('displays new and removed licenses count', async () => {
mockApi(httpStatusCodes.OK, licenseComplianceNewAndRemovedLicenses);
createComponent();
await waitForPromises();
expect(wrapper.text()).toBe(
'License Compliance detected 3 new licenses and 3 removed licenses',
);
});
});
describe('expanded data', () => {
describe('with new licesnes', () => {
beforeEach(async () => {
mockApi(httpStatusCodes.OK, licenseComplianceSuccess);
createComponent();
await waitForPromises();
findToggleCollapsedButton().trigger('click');
await waitForPromises();
});
it('displays denied licenses', async () => {
expect(findAllExtensionListItems().at(0).element).toMatchSnapshot();
});
it('displays uncategorized licenses', async () => {
expect(findAllExtensionListItems().at(1).element).toMatchSnapshot();
});
it('displays allowed licenses', async () => {
expect(findAllExtensionListItems().at(2).element).toMatchSnapshot();
});
});
});
});
export const licenses = [
{
name: 'Academic Free License v2.1',
dependencies: [
{
name: 'json-schema',
version: '0.4.0',
package_manager: 'npm',
blob_path: 'package.json',
},
],
url: 'http://opensource.linux-mirror.org/licenses/afl-2.1.txt',
classification: {
id: null,
name: 'Academic Free License v2.1',
approval_status: 'unclassified',
},
count: 1,
},
{
name: 'Apache License 2.0',
dependencies: [
{
name: 'websocket-driver',
version: '0.7.4',
package_manager: 'npm',
blob_path: 'package.json',
},
{
name: 'websocket-extensions',
version: '0.1.4',
package_manager: 'npm',
blob_path: 'package.json',
},
{
name: 'xml-name-validator',
version: '3.0.0',
package_manager: 'npm',
blob_path: 'package.json',
},
],
url: 'https://opensource.org/licenses/Apache-2.0',
classification: {
id: null,
name: 'Apache License 2.0',
approval_status: 'blacklisted',
},
count: 3,
},
{
name: 'ISC License',
dependencies: [
{
name: 'abbrev',
version: '1.1.1',
package_manager: 'npm',
blob_path: 'package.json',
},
{
name: 'anymatch',
version: '2.0.0',
package_manager: 'npm',
blob_path: 'package.json',
},
],
url: 'https://opensource.org/licenses/ISC',
classification: {
id: 4,
name: 'ISC License',
approval_status: 'approved',
},
count: 2,
},
];
export const licenseComplianceSuccess = {
new_licenses: licenses,
existing_licenses: [],
removed_licenses: [],
};
export const licenseComplianceNewAndRemovedLicenses = {
new_licenses: licenses,
existing_licenses: [],
removed_licenses: licenses,
};
export const licenseComplianceRemovedLicenses = {
new_licenses: [],
existing_licenses: [],
removed_licenses: licenses,
};
export const licenseComplianceEmpty = {
new_licenses: [],
existing_licenses: [],
removed_licenses: [],
};
import { parseDependencies } from 'ee/vue_merge_request_widget/extensions/license_compliance/utils';
import { licenses } from './mock_data';
describe('parseDependencies', () => {
it('generates a string', () => {
expect(parseDependencies(licenses[1].dependencies)).toBe(
'websocket-driver, websocket-extensions, xml-name-validator',
);
});
});
...@@ -335,6 +335,11 @@ msgid_plural "%d more comments" ...@@ -335,6 +335,11 @@ msgid_plural "%d more comments"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%d new license"
msgid_plural "%d new licenses"
msgstr[0] ""
msgstr[1] ""
msgid "%d open issue" msgid "%d open issue"
msgid_plural "%d open issues" msgid_plural "%d open issues"
msgstr[0] "" msgstr[0] ""
...@@ -370,6 +375,11 @@ msgid_plural "%d projects selected" ...@@ -370,6 +375,11 @@ msgid_plural "%d projects selected"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%d removed license"
msgid_plural "%d removed licenses"
msgstr[0] ""
msgstr[1] ""
msgid "%d second" msgid "%d second"
msgid_plural "%d seconds" msgid_plural "%d seconds"
msgstr[0] "" msgstr[0] ""
...@@ -22163,6 +22173,9 @@ msgstr "" ...@@ -22163,6 +22173,9 @@ msgstr ""
msgid "License Compliance" msgid "License Compliance"
msgstr "" msgstr ""
msgid "License Compliance| Used by"
msgstr ""
msgid "License compliance" msgid "License compliance"
msgstr "" msgstr ""
...@@ -22181,6 +22194,9 @@ msgstr "" ...@@ -22181,6 +22194,9 @@ msgstr ""
msgid "LicenseCompliance|%{docLinkStart}License Approvals%{docLinkEnd} are inactive" msgid "LicenseCompliance|%{docLinkStart}License Approvals%{docLinkEnd} are inactive"
msgstr "" msgstr ""
msgid "LicenseCompliance|Acceptable for use in this project"
msgstr ""
msgid "LicenseCompliance|Acceptable license to be used in the project" msgid "LicenseCompliance|Acceptable license to be used in the project"
msgstr "" msgstr ""
...@@ -22241,6 +22257,11 @@ msgid_plural "LicenseCompliance|License Compliance detected %d new licenses and ...@@ -22241,6 +22257,11 @@ msgid_plural "LicenseCompliance|License Compliance detected %d new licenses and
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "LicenseCompliance|License Compliance detected %d removed license"
msgid_plural "LicenseCompliance|License Compliance detected %d removed licenses"
msgstr[0] ""
msgstr[1] ""
msgid "LicenseCompliance|License Compliance detected no licenses for the source branch only" msgid "LicenseCompliance|License Compliance detected no licenses for the source branch only"
msgstr "" msgstr ""
...@@ -22250,6 +22271,12 @@ msgstr "" ...@@ -22250,6 +22271,12 @@ msgstr ""
msgid "LicenseCompliance|License name" msgid "LicenseCompliance|License name"
msgstr "" msgstr ""
msgid "LicenseCompliance|No policy matches this license"
msgstr ""
msgid "LicenseCompliance|Out-of-compliance with the project's policies and should be removed"
msgstr ""
msgid "LicenseCompliance|Remove license" msgid "LicenseCompliance|Remove license"
msgstr "" msgstr ""
...@@ -22265,6 +22292,9 @@ msgstr "" ...@@ -22265,6 +22292,9 @@ msgstr ""
msgid "LicenseCompliance|This license already exists in this project." msgid "LicenseCompliance|This license already exists in this project."
msgstr "" msgstr ""
msgid "LicenseCompliance|Uncategorized"
msgstr ""
msgid "LicenseCompliance|You are about to remove the license, %{name}, from this project." msgid "LicenseCompliance|You are about to remove the license, %{name}, from this project."
msgstr "" msgstr ""
...@@ -43843,6 +43873,15 @@ msgstr "" ...@@ -43843,6 +43873,15 @@ msgstr ""
msgid "ciReport|Investigate this vulnerability by creating an issue" msgid "ciReport|Investigate this vulnerability by creating an issue"
msgstr "" msgstr ""
msgid "ciReport|License Compliance"
msgstr ""
msgid "ciReport|License Compliance failed loading results"
msgstr ""
msgid "ciReport|License Compliance test metrics results are being parsed"
msgstr ""
msgid "ciReport|Load Performance" msgid "ciReport|Load Performance"
msgstr "" msgstr ""
......
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