Commit 7c26ba61 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '13083-js-diffing-v2-reports' into 'master'

Update the frontend diffing code to support v2 license scan reports

See merge request gitlab-org/gitlab!18105
parents 2687b526 86118dee
import V2Report from './v2_report';
const DEFAULT_VERSION = '1';
export default class ReportMapper {
constructor() {
this.mappers = {
'1': report => report,
'2': report => new V2Report(report).toV1Schema(),
};
}
mapFrom(reportArtifact) {
const majorVersion = ReportMapper.majorVersionFor(reportArtifact);
return this.mapperFor(majorVersion)(reportArtifact);
}
mapperFor(majorVersion) {
return this.mappers[majorVersion];
}
static majorVersionFor(report) {
if (report && report.version) {
const [majorVersion] = report.version.split('.');
return majorVersion;
}
return DEFAULT_VERSION;
}
}
import { n__, sprintf } from '~/locale'; import { n__, sprintf } from '~/locale';
import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants'; import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
import { LICENSE_APPROVAL_STATUS } from 'ee/vue_shared/license_management/constants'; import { LICENSE_APPROVAL_STATUS } from 'ee/vue_shared/license_management/constants';
import ReportMapper from 'ee/vue_shared/license_management/report_mapper';
const toLowerCase = name => name.toLowerCase(); const toLowerCase = name => name.toLowerCase();
/** /**
...@@ -85,10 +86,13 @@ export const parseLicenseReportMetrics = (headMetrics, baseMetrics, managedLicen ...@@ -85,10 +86,13 @@ export const parseLicenseReportMetrics = (headMetrics, baseMetrics, managedLicen
if (!headMetrics && !baseMetrics) { if (!headMetrics && !baseMetrics) {
return []; return [];
} }
const reportMapper = new ReportMapper();
const headReport = reportMapper.mapFrom(headMetrics);
const baseReport = reportMapper.mapFrom(baseMetrics);
const headLicenses = headMetrics.licenses || []; const headLicenses = headReport.licenses || [];
const headDependencies = headMetrics.dependencies || []; const headDependencies = headReport.dependencies || [];
const baseLicenses = baseMetrics.licenses || []; const baseLicenses = baseReport.licenses || [];
const managedLicenseList = managedLicenses || []; const managedLicenseList = managedLicenses || [];
if (!headLicenses.length && !headDependencies.length) return []; if (!headLicenses.length && !headDependencies.length) return [];
......
import { byLicenseNameComparator } from './store/utils';
export default class V2Report {
constructor(report) {
this.report = report;
this.licenseMap = V2Report.createLicenseMap(report.licenses);
this.licenses = report.licenses.sort(byLicenseNameComparator).map(V2Report.mapFromLicense);
}
toV1Schema() {
return {
licenses: this.licenses,
dependencies: this.report.dependencies.map(v2Dependency =>
this.mapFromDependency(v2Dependency),
),
};
}
combine(licenses, visitor) {
const reducer = (memo, licenseId) => {
const license = this.licenseMap[licenseId];
visitor(license);
if (memo) return { name: `${memo.name}, ${license.name}`, url: '' };
return { name: license.name, url: license.url };
};
return licenses.reduce(reducer, null);
}
incrementCountFor(licenseName) {
const matchingLicense = this.licenses.find(license => license.name === licenseName);
if (matchingLicense) matchingLicense.count += 1;
}
mapFromDependency({ name, description, url, licenses }) {
const combinedLicense = this.combine(licenses, license => {
this.incrementCountFor(license.name);
});
return {
license: combinedLicense,
dependency: { name, url, description },
};
}
static mapFromLicense({ name, url = '', count = 0 }) {
return { name, url, count };
}
static createLicenseMap(licenses) {
const identityMap = {};
licenses.forEach(item => {
identityMap[item.id] = {
name: item.name,
url: item.url,
};
});
return identityMap;
}
}
---
title: Update the frontend diffing code to support v2 license scan reports
merge_request: 18105
author:
type: changed
import { LICENSE_APPROVAL_STATUS } from 'ee/vue_shared/license_management/constants'; import { LICENSE_APPROVAL_STATUS } from 'ee/vue_shared/license_management/constants';
const urlFor = ({ scheme = 'https', host = 'www.example.org', path = '/' }) =>
`${scheme}://${host}${path}`;
const licenseUrlFor = name =>
urlFor({ host: 'opensource.org', path: `/licenses/${name.split(' ')[0]}` });
const dependencyUrlFor = name => urlFor({ path: `/${name}` });
const normalizeV1License = ({ name, url = licenseUrlFor(name) }) => ({ name, url });
const V1 = {
normalizeLicenseSummary: ({ name, url = licenseUrlFor(name), count = 0 }) => ({
name,
url,
count,
}),
normalizeDependency: ({
name,
url = dependencyUrlFor(name),
description = name.toUpperCase(),
license = {},
}) => ({ dependency: { name, url, description }, license: normalizeV1License(license) }),
};
const V2 = {
normalizeLicenseSummary: ({ id, name, url = licenseUrlFor(id) }) => ({ id, name, url }),
normalizeDependency: ({
name,
url = dependencyUrlFor(name),
description = name.toUpperCase(),
licenses = [],
}) => ({ name, url, licenses, description }),
};
export class Builder {
static for(version, template = { licenses: [], dependencies: [] }) {
return new Builder(version, template);
}
static forV1() {
return this.for(V1);
}
static forV2() {
return this.for(V2, { version: '2.0', licenses: [], dependencies: [] });
}
constructor(version, template = { licenses: [], dependencies: [] }) {
this.report = template;
this.version = version;
}
addLicense(license) {
this.report.licenses.push(this.version.normalizeLicenseSummary(license));
return this;
}
addDependency(dependency) {
this.report.dependencies.push(this.version.normalizeDependency(dependency));
return this;
}
build(override = {}) {
return Object.assign(this.report, override);
}
}
export const approvedLicense = { export const approvedLicense = {
id: 5, id: 5,
name: 'MIT', name: 'MIT',
...@@ -12,94 +74,46 @@ export const blacklistedLicense = { ...@@ -12,94 +74,46 @@ export const blacklistedLicense = {
approvalStatus: LICENSE_APPROVAL_STATUS.BLACKLISTED, approvalStatus: LICENSE_APPROVAL_STATUS.BLACKLISTED,
}; };
export const licenseBaseIssues = { export const licenseBaseIssues = Builder.forV1()
licenses: [ .addLicense({ name: 'MIT', count: 1 })
{ .addDependency({
count: 1, name: 'bundler',
name: 'MIT', url: 'http://bundler.io',
}, description: "The best way to manage your application's dependencies",
], license: { name: 'MIT', url: 'http://opensource.org/licenses/mit-license' },
dependencies: [ })
{ .build();
license: {
name: 'MIT',
url: 'http://opensource.org/licenses/mit-license',
},
dependency: {
name: 'bundler',
url: 'http://bundler.io',
description: "The best way to manage your application's dependencies",
paths: ['.'],
},
},
],
};
export const licenseHeadIssues = { export const licenseHeadIssues = Builder.forV1()
licenses: [ .addLicense({ name: 'New BSD', count: 3 })
{ .addLicense({ name: 'MIT', count: 1 })
count: 3, .addDependency({
name: 'New BSD', name: 'pg',
}, url: 'https://bitbucket.org/ged/ruby-pg',
{ description: 'Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/]',
count: 1, license: { name: 'New BSD', url: 'http://opensource.org/licenses/BSD-3-Clause' },
name: 'MIT', })
}, .addDependency({
], name: 'puma',
dependencies: [ url: 'http://puma.io',
{ description:
license: { 'Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications',
name: 'New BSD', license: { name: 'New BSD', url: 'http://opensource.org/licenses/BSD-3-Clause' },
url: 'http://opensource.org/licenses/BSD-3-Clause', })
}, .addDependency({
dependency: { name: 'foo',
name: 'pg', url: 'http://foo.io',
url: 'https://bitbucket.org/ged/ruby-pg', description:
description: 'Foo is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications',
'Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/]', license: { name: 'New BSD', url: 'http://opensource.org/licenses/BSD-3-Clause' },
paths: ['.'], })
}, .addDependency({
}, name: 'execjs',
{ url: 'https://github.com/rails/execjs',
license: { description: 'Run JavaScript code from Ruby',
name: 'New BSD', license: { name: 'MIT', url: 'http://opensource.org/licenses/mit-license' },
url: 'http://opensource.org/licenses/BSD-3-Clause', })
}, .build();
dependency: {
name: 'puma',
url: 'http://puma.io',
description:
'Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications',
paths: ['.'],
},
},
{
license: {
name: 'New BSD',
url: 'http://opensource.org/licenses/BSD-3-Clause',
},
dependency: {
name: 'foo',
url: 'http://foo.io',
description:
'Foo is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications',
paths: ['.'],
},
},
{
license: {
name: 'MIT',
url: 'http://opensource.org/licenses/mit-license',
},
dependency: {
name: 'execjs',
url: 'https://github.com/rails/execjs',
description: 'Run JavaScript code from Ruby',
paths: ['.'],
},
},
],
};
export const licenseReport = [ export const licenseReport = [
{ {
......
import { Builder } from '../../license_management/mock_data';
describe('build', () => {
it('creates a v1 report', () => {
const result = Builder.forV1()
.addLicense({ name: 'MIT License' })
.addDependency({ name: 'rails', license: { name: 'MIT License' } })
.build();
expect(result).toMatchObject({
licenses: [{ name: 'MIT License', url: 'https://opensource.org/licenses/MIT', count: 0 }],
dependencies: [
{
license: { name: 'MIT License', url: 'https://opensource.org/licenses/MIT' },
dependency: { name: 'rails', description: 'RAILS', url: 'https://www.example.org/rails' },
},
],
});
});
it('creates a v2 report', () => {
const result = Builder.forV2()
.addLicense({ id: 'MIT', name: 'MIT License' })
.addDependency({ name: 'rails', licenses: ['MIT'] })
.build();
expect(result).toMatchObject({
version: '2.0',
licenses: [{ id: 'MIT', name: 'MIT License', url: 'https://opensource.org/licenses/MIT' }],
dependencies: [{ name: 'rails', description: 'RAILS', licenses: ['MIT'] }],
});
});
});
import ReportMapper from 'ee/vue_shared/license_management/report_mapper';
import { Builder } from '../../license_management/mock_data';
describe('mapFrom', () => {
let subject = null;
beforeEach(() => {
subject = new ReportMapper(true);
});
it('converts a v2 schema report to v1', () => {
const report = Builder.forV2()
.addLicense({ id: 'MIT', name: 'MIT License' })
.addLicense({ id: 'BSD', name: 'BSD License' })
.addDependency({ name: 'x', licenses: ['MIT'] })
.addDependency({ name: 'y', licenses: ['BSD'] })
.addDependency({ name: 'z', licenses: ['BSD', 'MIT'] })
.build();
const result = subject.mapFrom(report);
expect(result).toMatchObject(
Builder.forV1()
.addLicense({ name: 'BSD License', count: 2 })
.addLicense({ name: 'MIT License', count: 2 })
.addDependency({ name: 'x', license: { name: 'MIT License' } })
.addDependency({ name: 'y', license: { name: 'BSD License' } })
.addDependency({ name: 'z', license: { name: 'BSD License, MIT License', url: '' } })
.build(),
);
});
it('returns a v1 schema report', () => {
const report = Builder.forV1().build();
expect(subject.mapFrom(report)).toBe(report);
});
it('returns a v1.1 schema report', () => {
const report = Builder.forV1().build({ version: '1.1' });
expect(subject.mapFrom(report)).toBe(report);
});
it('ignores undefined versions', () => {
const report = {};
expect(subject.mapFrom(report)).toBe(report);
});
it('ignores undefined reports', () => {
const report = undefined;
expect(subject.mapFrom(report)).toBe(report);
});
it('ignores null reports', () => {
const report = null;
expect(subject.mapFrom(report)).toBe(report);
});
});
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
import { LICENSE_APPROVAL_STATUS } from 'ee/vue_shared/license_management/constants'; import { LICENSE_APPROVAL_STATUS } from 'ee/vue_shared/license_management/constants';
import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants'; import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
import { import {
Builder,
approvedLicense, approvedLicense,
blacklistedLicense, blacklistedLicense,
licenseHeadIssues, licenseHeadIssues,
...@@ -63,6 +64,71 @@ describe('utils', () => { ...@@ -63,6 +64,71 @@ describe('utils', () => {
expect(result[1].id).toBe(blacklistedLicense.id); expect(result[1].id).toBe(blacklistedLicense.id);
}); });
it('compares a v2 report with a v2 report', () => {
const policies = [{ id: 100, name: 'BSD License', approvalStatus: 'blacklisted' }];
const baseReport = Builder.forV2()
.addLicense({ id: 'MIT', name: 'MIT License' })
.addDependency({ name: 'x', licenses: ['MIT'] })
.build();
const headReport = Builder.forV2()
.addLicense({ id: 'MIT', name: 'MIT License' })
.addLicense({ id: 'BSD', name: 'BSD License' })
.addDependency({ name: 'x', licenses: ['MIT'] })
.addDependency({ name: 'y', licenses: ['BSD'] })
.addDependency({ name: 'z', licenses: ['BSD', 'MIT'] })
.build();
const result = parseLicenseReportMetrics(headReport, baseReport, policies);
expect(result.length).toBe(1);
expect(result[0]).toEqual(
jasmine.objectContaining({
id: 100,
approvalStatus: 'blacklisted',
count: 2,
status: 'failed',
name: 'BSD License',
url: 'https://opensource.org/licenses/BSD',
packages: [{ name: 'y', url: 'https://www.example.org/y', description: 'Y' }],
}),
);
});
it('compares a v1 report with a v2 report', () => {
const policies = [{ id: 101, name: 'BSD License', approvalStatus: 'blacklisted' }];
const baseReport = Builder.forV1()
.addLicense({ name: 'MIT License' })
.addDependency({
name: 'x',
license: { name: 'MIT License', url: 'https://opensource.org/licenses/MIT' },
})
.build();
const headReport = Builder.forV2()
.addLicense({ id: 'MIT', name: 'MIT License' })
.addLicense({ id: 'BSD', name: 'BSD License' })
.addDependency({ name: 'x', licenses: ['MIT'] })
.addDependency({ name: 'y', licenses: ['BSD'] })
.addDependency({ name: 'z', licenses: ['BSD', 'MIT'] })
.build();
const result = parseLicenseReportMetrics(headReport, baseReport, policies);
expect(result.length).toBe(1);
expect(result[0]).toEqual(
jasmine.objectContaining({
id: 101,
approvalStatus: 'blacklisted',
count: 2,
status: 'failed',
name: 'BSD License',
url: 'https://opensource.org/licenses/BSD',
packages: [{ name: 'y', url: 'https://www.example.org/y', description: 'Y' }],
}),
);
});
it('matches using a case insensitive match on license name', () => { it('matches using a case insensitive match on license name', () => {
const headReport = { licenses: [{ count: 1, name: 'BSD' }], dependencies: [] }; const headReport = { licenses: [{ count: 1, name: 'BSD' }], dependencies: [] };
const baseReport = { licenses: [{ count: 1, name: 'bsd' }], dependencies: [] }; const baseReport = { licenses: [{ count: 1, name: 'bsd' }], dependencies: [] };
......
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