Commit fca96c14 authored by Lukas Eipert's avatar Lukas Eipert Committed by Clement Ho

Add license management frontend

parent dbd5ba09
......@@ -75,31 +75,27 @@
margin: 0;
.license-item {
line-height: $gl-padding-24;
line-height: $gl-padding-32;
.license-dependencies {
color: $gl-text-color-tertiary;
.license-packages {
font-size: $label-font-size;
}
.btn-show-all-packages {
line-height: $gl-btn-line-height;
margin-bottom: 2px;
}
}
}
.report-block-list-icon {
display: flex;
&.failed {
&.failed svg {
color: $red-500;
}
&.success {
&.success svg {
color: $green-500;
}
&.neutral {
&.neutral svg {
color: $theme-gray-700;
}
......
......@@ -4,6 +4,8 @@ module Projects
before_action :authorize_admin_pipeline!
before_action :define_variables
prepend ::EE::Projects::Settings::CiCdController
def show
end
......
......@@ -11,12 +11,15 @@ by implicitly using [Auto License Management](../../../topics/autodevops/index.m
that is provided by [Auto DevOps](../../../topics/autodevops/index.md).
Going a step further, GitLab can show the licenses list right in the merge
request widget area.
request widget area, highlighting the presence of licenses you don't want to use, or new
ones that need a decision.
Licenses can be accepted or blacklisted in the project settings, or directly from the
merge request widget.
## Use cases
It helps you find licenses that you don't want to use in your project and see
which dependencies use them. For example, your application is using an external (open source)
It helps you find what licenses your project uses in its dependencies, and decide for each of then
whether to allow it or forbid it. For example, your application is using an external (open source)
library whose license is incompatible with yours.
## Supported languages and package managers
......@@ -54,9 +57,18 @@ management report will be shown properly.
The `license_management` job will search the application dependencies for licenses,
the resulting JSON file will be uploaded as an artifact, and
GitLab will then check this file and show the information inside the merge
request.
request. Blacklisted licenses will be clearly visible, as well as new licenses which
need a decision from you.
![License Management Widget](img/license_management.jpg)
![License Management Widget](img/license_management.png)
You can click on a license to be given the choice to approve it or blacklist it.
![License approval decision](img/license_management_decision.png)
The list of licenses and their status can also be managed from the project settings.
![License Management Settings](img/license_management_settings.png)
[ee-5483]: https://gitlab.com/gitlab-org/gitlab-ee/issues/5483
[ee]: https://about.gitlab.com/pricing/
......
import Vue from 'vue';
import Dashboard from 'ee/vue_shared/license_management/license_management.vue';
import '~/pages/projects/settings/ci_cd/show/index';
document.addEventListener('DOMContentLoaded', () => {
const el = document.getElementById('js-managed-licenses');
if (el && el.dataset && el.dataset.apiUrl) {
// eslint-disable-next-line no-new
new Vue({
el,
render(createElement) {
return createElement(Dashboard, {
props: {
...el.dataset,
},
});
},
});
}
});
......@@ -3,16 +3,18 @@ import ReportSection from '~/vue_shared/components/reports/report_section.vue';
import GroupedSecurityReportsApp from 'ee/vue_shared/security_reports/grouped_security_reports_app.vue';
import reportsMixin from 'ee/vue_shared/security_reports/mixins/reports_mixin';
import { componentNames } from 'ee/vue_shared/components/reports/issue_body';
import MrWidgetLicenses from 'ee/vue_shared/license_management/mr_widget_license_report.vue';
import { n__, s__, __, sprintf } from '~/locale';
import CEWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import WidgetApprovals from './components/approvals/mr_widget_approvals.vue';
import GeoSecondaryNode from './components/states/mr_widget_secondary_geo_node.vue';
import MrWidgetApprovals from './components/approvals/mr_widget_approvals.vue';
import MrWidgetGeoSecondaryNode from './components/states/mr_widget_secondary_geo_node.vue';
export default {
components: {
'mr-widget-approvals': WidgetApprovals,
'mr-widget-geo-secondary-node': GeoSecondaryNode,
MrWidgetLicenses,
MrWidgetApprovals,
MrWidgetGeoSecondaryNode,
GroupedSecurityReportsApp,
ReportSection,
},
......@@ -23,7 +25,6 @@ export default {
return {
isLoadingCodequality: false,
isLoadingPerformance: false,
isLoadingLicenseReport: false,
loadingCodequalityFailed: false,
loadingPerformanceFailed: false,
loadingLicenseReportFailed: false,
......@@ -58,10 +59,6 @@ export default {
(this.mr.performanceMetrics.neutral && this.mr.performanceMetrics.neutral.length > 0))
);
},
hasLicenseReportIssues() {
const { licenseReport } = this.mr;
return licenseReport && licenseReport.length > 0;
},
shouldRenderPerformance() {
const { performance } = this.mr;
return performance && performance.head_path && performance.base_path;
......@@ -124,18 +121,6 @@ export default {
return text.join('');
},
licenseReportText() {
const { licenseReport } = this.mr;
if (licenseReport.length > 0) {
return sprintf(s__('ciReport|License management detected %{licenseInfo}'), {
licenseInfo: n__('%d new license', '%d new licenses', licenseReport.length),
});
}
return s__('ciReport|License management detected no new licenses');
},
codequalityStatus() {
return this.checkReportStatus(this.isLoadingCodequality, this.loadingCodequalityFailed);
},
......@@ -143,10 +128,6 @@ export default {
performanceStatus() {
return this.checkReportStatus(this.isLoadingPerformance, this.loadingPerformanceFailed);
},
licenseReportStatus() {
return this.checkReportStatus(this.isLoadingLicenseReport, this.loadingLicenseReportFailed);
},
},
created() {
if (this.shouldRenderCodeQuality) {
......@@ -156,10 +137,6 @@ export default {
if (this.shouldRenderPerformance) {
this.fetchPerformance();
}
if (this.shouldRenderLicenseReport) {
this.fetchLicenseReport();
}
},
methods: {
fetchCodeQuality() {
......@@ -199,22 +176,6 @@ export default {
});
},
fetchLicenseReport() {
const { head_path, base_path } = this.mr.licenseManagement;
this.isLoadingLicenseReport = true;
Promise.all([this.service.fetchReport(head_path), this.service.fetchReport(base_path)])
.then(values => {
this.mr.parseLicenseReportMetrics(values[0], values[1]);
this.isLoadingLicenseReport = false;
})
.catch(() => {
this.isLoadingLicenseReport = false;
this.loadingLicenseReportFailed = true;
});
},
translateText(type) {
return {
error: sprintf(s__('ciReport|Failed to load %{reportName} report'), {
......@@ -230,7 +191,7 @@ export default {
</script>
<template>
<div class="mr-state-widget prepend-top-default">
<mr-widget-header :mr="mr" />
<mr-widget-header :mr="mr"/>
<mr-widget-pipeline
v-if="shouldRenderPipelines"
:pipeline="mr.pipeline"
......@@ -296,16 +257,13 @@ export default {
:can-create-issue="mr.canCreateIssue"
:can-create-feedback="mr.canCreateFeedback"
/>
<report-section
<mr-widget-licenses
v-if="shouldRenderLicenseReport"
:status="licenseReportStatus"
:loading-text="translateText('license management').loading"
:error-text="translateText('license management').error"
:success-text="licenseReportText"
:neutral-issues="mr.licenseReport"
:has-issues="hasLicenseReportIssues"
:component="$options.componentNames.LicenseIssueBody"
class="js-license-report-widget mr-widget-border-top"
:api-url="mr.licenseManagement.managed_licenses_path"
:can-manage-licenses="mr.licenseManagement.can_manage_licenses"
:base-path="mr.licenseManagement.base_path"
:head-path="mr.licenseManagement.head_path"
report-section-class="mr-widget-border-top"
/>
<div class="mr-section-container">
<div class="mr-widget-section">
......@@ -335,7 +293,7 @@ export default {
v-if="shouldRenderMergeHelp"
class="mr-widget-footer"
>
<mr-widget-merge-help />
<mr-widget-merge-help/>
</div>
</div>
</div>
......
......@@ -24,7 +24,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.initCodeclimate(data);
this.initPerformanceReport(data);
this.initLicenseReport(data);
this.licenseManagement = data.license_management;
}
setData(data) {
......@@ -70,11 +70,6 @@ export default class MergeRequestStore extends CEMergeRequestStore {
};
}
initLicenseReport(data) {
this.licenseManagement = data.license_management;
this.licenseReport = [];
}
compareCodeclimateMetrics(headIssues, baseIssues, headBlobPath, baseBlobPath) {
const parsedHeadIssues = MergeRequestStore.parseCodeclimateMetrics(headIssues, headBlobPath);
const parsedBaseIssues = MergeRequestStore.parseCodeclimateMetrics(baseIssues, baseBlobPath);
......@@ -135,44 +130,6 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.performanceMetrics = { improved, degraded, neutral };
}
parseLicenseReportMetrics(headMetrics, baseMetrics) {
const headLicenses = headMetrics.licenses;
const headDependencies = headMetrics.dependencies;
const baseLicenses = baseMetrics.licenses;
if (headLicenses.length > 0 && headDependencies.length > 0) {
const report = {};
const knownLicenses = baseLicenses.map(license => license.name);
const newLicenses = [];
headLicenses.forEach(license => {
if (knownLicenses.indexOf(license.name) === -1) {
report[license.name] = {
name: license.name,
count: license.count,
url: '',
packages: [],
};
newLicenses.push(license.name);
}
});
headDependencies.forEach(dependencyItem => {
const licenseName = dependencyItem.license.name;
if (newLicenses.indexOf(licenseName) > -1) {
if (!report[licenseName].url) {
report[licenseName].url = dependencyItem.license.url;
}
report[licenseName].packages.push(dependencyItem.dependency);
}
});
this.licenseReport = newLicenses.map(licenseName => report[licenseName]);
}
}
// normalize performance metrics by indexing on performance subject and metric name
static normalizePerformanceMetrics(performanceData) {
const indexedSubjects = {};
......
......@@ -4,7 +4,7 @@ import {
} from '~/vue_shared/components/reports/issue_body';
import PerformanceIssueBody from 'ee/vue_merge_request_widget/components/performance_issue_body.vue';
import CodequalityIssueBody from 'ee/vue_merge_request_widget/components/codequality_issue_body.vue';
import LicenseIssueBody from 'ee/vue_merge_request_widget/components/license_issue_body.vue';
import LicenseIssueBody from 'ee/vue_shared/license_management/components/license_issue_body.vue';
import SastIssueBody from 'ee/vue_shared/security_reports/components/sast_issue_body.vue';
import SastContainerIssueBody from 'ee/vue_shared/security_reports/components/sast_container_issue_body.vue';
import DastIssueBody from 'ee/vue_shared/security_reports/components/dast_issue_body.vue';
......
<script>
import _ from 'underscore';
import { s__, sprintf } from '~/locale';
import { mapActions, mapState } from 'vuex';
import GlModal from '~/vue_shared/components/gl_modal.vue';
export default {
name: 'LicenseDeleteConfirmationModal',
components: { GlModal },
computed: {
...mapState(['currentLicenseInModal']),
confirmationText() {
const name = `<strong>${_.escape(this.currentLicenseInModal.name)}</strong>`;
return sprintf(
s__('LicenseManagement|You are about to remove the license, %{name}, from this project.'),
{ name },
false,
);
},
},
methods: {
...mapActions(['resetLicenseInModal', 'deleteLicense']),
},
};
</script>
<template>
<gl-modal
id="modal-license-delete-confirmation"
:header-title-text="s__('LicenseManagement|Remove license?')"
:footer-primary-button-text="s__('LicenseManagement|Remove license')"
footer-primary-button-variant="danger"
@cancel="resetLicenseInModal"
@submit="deleteLicense(currentLicenseInModal)"
>
<span
v-if="currentLicenseInModal"
v-html="confirmationText"></span>
</gl-modal>
</template>
<script>
import { mapActions } from 'vuex';
import { s__ } from '~/locale/index';
import LicensePackages from './license_packages.vue';
import { LICENSE_APPROVAL_STATUS } from '../constants';
export default {
name: 'LicenseIssueBody',
components: { LicensePackages },
props: {
issue: {
type: Object,
required: true,
},
},
computed: {
status() {
switch (this.issue.approvalStatus) {
case LICENSE_APPROVAL_STATUS.APPROVED:
return s__('LicenseManagement|Approved');
case LICENSE_APPROVAL_STATUS.BLACKLISTED:
return s__('LicenseManagement|Blacklisted');
default:
return s__('LicenseManagement|Unapproved');
}
},
},
methods: { ...mapActions(['setLicenseInModal']) },
};
</script>
<template>
<div class="report-block-info license-item">
<span class="append-right-5">{{ status }}:</span>
<button
class="btn-blank btn-link append-right-5"
type="button"
data-toggle="modal"
data-target="#modal-set-license-approval"
@click="setLicenseInModal(issue)"
>
{{ issue.name }}
</button>
<license-packages
:packages="issue.packages"
class="text-secondary"
/>
</div>
</template>
<script>
import { mapActions } from 'vuex';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import IssueStatusIcon from '~/vue_shared/components/reports/issue_status_icon.vue';
import { getIssueStatusFromLicenseStatus } from 'ee/vue_shared/license_management/store/utils';
import { LICENSE_APPROVAL_STATUS } from '../constants';
const visibleClass = 'visible';
const invisibleClass = 'invisible';
export default {
name: 'LicenseManagementRow',
components: {
Icon,
IssueStatusIcon,
},
props: {
license: {
type: Object,
required: true,
validator: license =>
!!license.name && Object.values(LICENSE_APPROVAL_STATUS).includes(license.approvalStatus),
},
},
LICENSE_APPROVAL_STATUS,
[LICENSE_APPROVAL_STATUS.APPROVED]: s__('LicenseManagement|Approved'),
[LICENSE_APPROVAL_STATUS.BLACKLISTED]: s__('LicenseManagement|Blacklisted'),
computed: {
approveIconClass() {
return this.license.approvalStatus === LICENSE_APPROVAL_STATUS.APPROVED
? visibleClass
: invisibleClass;
},
blacklistIconClass() {
return this.license.approvalStatus === LICENSE_APPROVAL_STATUS.BLACKLISTED
? visibleClass
: invisibleClass;
},
status() {
return getIssueStatusFromLicenseStatus(this.license.approvalStatus);
},
dropdownText() {
return this.$options[this.license.approvalStatus];
},
},
methods: {
...mapActions(['setLicenseInModal', 'approveLicense', 'blacklistLicense']),
},
};
</script>
<template>
<li class="list-group-item">
<issue-status-icon
:status="status"
class="float-left append-right-default"
/>
<span class="js-license-name">{{ license.name }}</span>
<div class="float-right">
<div class="d-flex">
<div class="dropdown">
<button
class="btn btn-secondary dropdown-toggle"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
{{ dropdownText }}
<icon
class="float-right"
name="chevron-down"
/>
</button>
<div
class="dropdown-menu dropdown-menu-right"
>
<button
class="dropdown-item"
type="button"
@click="approveLicense(license)"
>
<icon
:class="approveIconClass"
name="mobile-issue-close"
/>
{{ $options[$options.LICENSE_APPROVAL_STATUS.APPROVED] }}
</button>
<button
class="dropdown-item"
type="button"
@click="blacklistLicense(license)"
>
<icon
:class="blacklistIconClass"
name="mobile-issue-close"
/>
{{ $options[$options.LICENSE_APPROVAL_STATUS.BLACKLISTED] }}
</button>
</div>
</div>
<button
class="btn btn-blank js-remove-button"
type="button"
data-toggle="modal"
data-target="#modal-license-delete-confirmation"
@click="setLicenseInModal(license)"
>
<icon name="remove"/>
</button>
</div>
</div>
</li>
</template>
<script>
import { s__, sprintf } from '~/locale';
import { getPackagesString } from '../store/utils';
export default {
name: 'LicenseIssueBody',
name: 'LicensePackages',
props: {
issue: {
type: Object,
packages: {
type: Array,
required: true,
},
},
......@@ -16,11 +17,11 @@ export default {
};
},
computed: {
packages() {
return this.getPackagesString(!this.showAllPackages);
packageString() {
return getPackagesString(this.packages, !this.showAllPackages, this.displayPackageCount);
},
remainingPackages() {
const { packages } = this.issue;
const { packages } = this;
if (packages.length > this.displayPackageCount) {
return sprintf(s__('ciReport|%{remainingPackagesCount} more'), {
remainingPackagesCount: packages.length - this.displayPackageCount,
......@@ -30,62 +31,22 @@ export default {
},
},
methods: {
getPackagesString(truncate) {
const { packages } = this.issue;
// When there is only 1 package name to show.
if (packages.length === 1) {
return packages[0].name;
}
// When packages count is higher than displayPackageCount
// and truncate is true.
if (truncate && packages.length > this.displayPackageCount) {
return sprintf(s__('ciReport|%{packagesString} and '), {
packagesString: packages
.slice(0, this.displayPackageCount)
.map(packageItem => packageItem.name)
.join(', '),
});
}
// Return all package names separated by comma with proper grammer
return sprintf(s__('ciReport|%{packagesString} and %{lastPackage}'), {
packagesString: packages
.slice(0, packages.length - 1)
.map(packageItem => packageItem.name)
.join(', '),
lastPackage: packages[packages.length - 1].name,
});
},
handleShowPackages() {
this.showAllPackages = true;
},
},
};
</script>
<template>
<p
class="prepend-left-4 append-bottom-0 report-block-info license-item"
>
<a
:href="issue.url"
target="_blank"
rel="noopener noreferrer nofollow"
>{{ issue.name }}</a>
<span
class="license-dependencies"
>
&nbsp;{{ packages }}
</span>
<div class="license-packages d-inline">
<div class="js-license-dependencies d-inline">{{ packageString }}</div>
<button
v-if="!showAllPackages"
v-if="!showAllPackages && remainingPackages"
type="button"
class="btn btn-link btn-show-all-packages"
class="btn-link btn-show-all-packages"
@click="handleShowPackages"
>
{{ remainingPackages }}
</button>
</p>
</div>
</template>
<script>
import { s__ } from '~/locale';
import { mapActions, mapState } from 'vuex';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import LicensePackages from './license_packages.vue';
import { LICENSE_APPROVAL_STATUS } from '../constants';
export default {
name: 'LicenseSetApprovalStatusModal',
components: { LicensePackages, GlModal },
computed: {
...mapState(['currentLicenseInModal', 'canManageLicenses']),
headerTitleText() {
if (!this.canManageLicenses) {
return s__('LicenseManagement|License details');
}
if (this.canApprove) {
return s__('LicenseManagement|Approve license?');
}
return s__('LicenseManagement|Blacklist license?');
},
canApprove() {
return (
this.canManageLicenses &&
this.currentLicenseInModal &&
this.currentLicenseInModal.approvalStatus !== LICENSE_APPROVAL_STATUS.APPROVED
);
},
canBlacklist() {
return (
this.canManageLicenses &&
this.currentLicenseInModal &&
this.currentLicenseInModal.approvalStatus !== LICENSE_APPROVAL_STATUS.BLACKLISTED
);
},
},
methods: {
...mapActions(['resetLicenseInModal', 'approveLicense', 'blacklistLicense']),
},
};
</script>
<template>
<gl-modal
id="modal-set-license-approval"
:header-title-text="headerTitleText"
modal-size="lg"
@cancel="resetLicenseInModal"
>
<slot v-if="currentLicenseInModal">
<div class="row prepend-top-10 append-bottom-10 js-license-name">
<label class="col-sm-2 text-right font-weight-bold">
{{ s__('LicenseManagement|License') }}:
</label>
<div class="col-sm-10 text-secondary">
{{ currentLicenseInModal.name }}
</div>
</div>
<div
v-if="currentLicenseInModal.url"
class="row prepend-top-10 append-bottom-10 js-license-url"
>
<label class="col-sm-2 text-right font-weight-bold">
{{ s__('LicenseManagement|URL') }}:
</label>
<div class="col-sm-10 text-secondary">
<a
:href="currentLicenseInModal.url"
target="_blank"
rel="noopener noreferrer nofollow"
>{{ currentLicenseInModal.url }}</a>
</div>
</div>
<div class="row prepend-top-10 append-bottom-10 js-license-packages">
<label class="col-sm-2 text-right font-weight-bold">
{{ s__('LicenseManagement|Packages') }}:
</label>
<license-packages
:packages="currentLicenseInModal.packages"
class="col-sm-10 text-secondary"
/>
</div>
</slot>
<template slot="footer">
<button
type="button"
class="btn js-modal-cancel-action"
data-dismiss="modal"
@click="resetLicenseInModal"
>
{{ s__('Modal|Cancel') }}
</button>
<button
v-if="canBlacklist"
class="btn btn-remove btn-inverted js-modal-secondary-action"
data-dismiss="modal"
@click="blacklistLicense(currentLicenseInModal)"
>
{{ s__('LicenseManagement|Blacklist license') }}
</button>
<button
v-if="canApprove"
type="button"
class="btn btn-success js-modal-primary-action"
data-dismiss="modal"
@click="approveLicense(currentLicenseInModal)"
>
{{ s__('LicenseManagement|Approve license') }}
</button>
</template>
</gl-modal>
</template>
// eslint-disable-next-line import/prefer-default-export
export const LICENSE_APPROVAL_STATUS = {
APPROVED: 'approved',
BLACKLISTED: 'blacklisted',
};
<script>
import { mapState, mapActions } from 'vuex';
import { s__ } from '~/locale';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import LicenseManagementRow from './components/license_management_row.vue';
import DeleteConfirmationModal from './components/delete_confirmation_modal.vue';
import createStore from './store/index';
const store = createStore();
export default {
name: 'LicenseManagement',
components: {
DeleteConfirmationModal,
LoadingIcon,
LicenseManagementRow,
},
props: {
apiUrl: {
type: String,
required: true,
},
},
store,
emptyMessage: s__(
'LicenseManagement|There are currently no approved or blacklisted licenses in this project.',
),
computed: {
...mapState(['managedLicenses', 'isLoadingManagedLicenses']),
},
mounted() {
this.setAPISettings({
apiUrlManageLicenses: this.apiUrl,
});
this.loadManagedLicenses();
},
methods: {
...mapActions(['setAPISettings', 'loadManagedLicenses']),
},
};
</script>
<template>
<div class="license-management">
<delete-confirmation-modal/>
<loading-icon v-if="isLoadingManagedLicenses"/>
<ul
v-if="managedLicenses.length"
class="list-group list-group-flush"
>
<license-management-row
v-for="license in managedLicenses"
:key="license.name"
:license="license"
/>
</ul>
<div
v-else
class="bs-callout bs-callout-warning"
>
{{ $options.emptyMessage }}
</div>
</div>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import ReportSection from '~/vue_shared/components/reports/report_section.vue';
import reportsMixin from 'ee/vue_shared/security_reports/mixins/reports_mixin';
import SetLicenseApprovalModal from 'ee/vue_shared/license_management/components/set_approval_status_modal.vue';
import { componentNames } from 'ee/vue_shared/components/reports/issue_body';
import createStore from './store';
const store = createStore();
export default {
name: 'MrWidgetLicenses',
componentNames,
store,
components: {
ReportSection,
SetLicenseApprovalModal,
},
mixins: [reportsMixin],
props: {
headPath: {
type: String,
required: true,
},
basePath: {
type: String,
required: false,
default: null,
},
apiUrl: {
type: String,
required: true,
},
canManageLicenses: {
type: Boolean,
required: true,
},
},
computed: {
...mapState(['loadLicenseReportError']),
...mapGetters(['licenseReport', 'isLoading', 'licenseSummaryText']),
hasLicenseReportIssues() {
const { licenseReport } = this;
return licenseReport && licenseReport.length > 0;
},
licenseReportStatus() {
return this.checkReportStatus(this.isLoading, this.loadLicenseReportError);
},
},
mounted() {
const { headPath, basePath, apiUrl, canManageLicenses } = this;
this.setAPISettings({
apiUrlManageLicenses: apiUrl,
headPath,
basePath,
canManageLicenses,
});
this.loadLicenseReport();
this.loadManagedLicenses();
},
methods: {
...mapActions(['setAPISettings', 'loadManagedLicenses', 'loadLicenseReport']),
},
};
</script>
<template>
<div>
<set-license-approval-modal/>
<report-section
:status="licenseReportStatus"
:success-text="licenseSummaryText"
:loading-text="licenseSummaryText"
:error-text="licenseSummaryText"
:neutral-issues="licenseReport"
:has-issues="hasLicenseReportIssues"
:component="$options.componentNames.LicenseIssueBody"
class="license-report-widget mr-widget-border-top"
/>
</div>
</template>
import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types';
import { LICENSE_APPROVAL_STATUS } from '../constants';
export const setAPISettings = ({ commit }, data) => {
commit(types.SET_API_SETTINGS, data);
};
export const setLicenseInModal = ({ commit }, license) => {
commit(types.SET_LICENSE_IN_MODAL, license);
};
export const resetLicenseInModal = ({ commit }) => {
commit(types.RESET_LICENSE_IN_MODAL);
};
export const requestDeleteLicense = ({ commit }) => {
commit(types.REQUEST_DELETE_LICENSE);
};
export const receiveDeleteLicense = ({ commit, dispatch }) => {
commit(types.RECEIVE_DELETE_LICENSE);
dispatch('loadManagedLicenses');
};
export const receiveDeleteLicenseError = ({ commit }, error) => {
commit(types.RECEIVE_DELETE_LICENSE_ERROR, error);
};
export const deleteLicense = ({ dispatch, state }) => {
const licenseId = state.currentLicenseInModal.id;
dispatch('requestDeleteLicense');
const endpoint = `${state.apiUrlManageLicenses}/${licenseId}`;
return axios
.delete(endpoint)
.then(() => {
dispatch('receiveDeleteLicense');
})
.catch(error => {
dispatch('receiveDeleteLicenseError', error);
});
};
export const requestLoadManagedLicenses = ({ commit }) => {
commit(types.REQUEST_LOAD_MANAGED_LICENSES);
};
export const receiveLoadManagedLicenses = ({ commit }, licenses) => {
commit(types.RECEIVE_LOAD_MANAGED_LICENSES, licenses);
};
export const receiveLoadManagedLicensesError = ({ commit }, error) => {
commit(types.RECEIVE_LOAD_MANAGED_LICENSES_ERROR, error);
};
export const loadManagedLicenses = ({ dispatch, state }) => {
dispatch('requestLoadManagedLicenses');
const { apiUrlManageLicenses } = state;
return axios
.get(apiUrlManageLicenses)
.then(({ data }) => {
dispatch('receiveLoadManagedLicenses', data);
})
.catch(error => {
dispatch('receiveLoadManagedLicensesError', error);
});
};
export const requestLoadLicenseReport = ({ commit }) => {
commit(types.REQUEST_LOAD_LICENSE_REPORT);
};
export const receiveLoadLicenseReport = ({ commit }, reports) => {
commit(types.RECEIVE_LOAD_LICENSE_REPORT, reports);
};
export const receiveLoadLicenseReportError = ({ commit }, error) => {
commit(types.RECEIVE_LOAD_LICENSE_REPORT_ERROR, error);
};
export const loadLicenseReport = ({ dispatch, state }) => {
dispatch('requestLoadLicenseReport');
const { headPath, basePath } = state;
const promises = [axios.get(headPath).then(({ data }) => data)];
if (basePath) {
promises.push(
axios
.get(basePath)
.then(({ data }) => data)
.catch(e => {
if (e.response.status === 404) {
return {};
}
throw e;
}),
);
}
return Promise.all(promises)
.then(([headReport, baseReport = {}]) => {
dispatch('receiveLoadLicenseReport', { headReport, baseReport });
})
.catch(error => {
dispatch('receiveLoadLicenseReportError', error);
});
};
export const requestSetLicenseApproval = ({ commit }) => {
commit(types.REQUEST_SET_LICENSE_APPROVAL);
};
export const receiveSetLicenseApproval = ({ commit, dispatch }) => {
commit(types.RECEIVE_SET_LICENSE_APPROVAL);
dispatch('loadManagedLicenses');
};
export const receiveSetLicenseApprovalError = ({ commit }, error) => {
commit(types.RECEIVE_SET_LICENSE_APPROVAL_ERROR, error);
};
export const setLicenseApproval = ({ dispatch, state }, payload) => {
const { apiUrlManageLicenses } = state;
const { license, newStatus } = payload;
const { id, name } = license;
dispatch('requestSetLicenseApproval');
let request;
/*
Licenses that have an ID, are already in the database.
So we need to send PATCH instead of POST.
*/
if (id) {
request = axios.patch(`${apiUrlManageLicenses}/${id}`, { approval_status: newStatus });
} else {
request = axios.post(apiUrlManageLicenses, { approval_status: newStatus, name });
}
return request
.then(() => {
dispatch('receiveSetLicenseApproval');
})
.catch(error => {
dispatch('receiveSetLicenseApprovalError', error);
});
};
export const approveLicense = ({ dispatch }, license) => {
const { approvalStatus } = license;
if (approvalStatus !== LICENSE_APPROVAL_STATUS.APPROVED) {
dispatch('setLicenseApproval', { license, newStatus: LICENSE_APPROVAL_STATUS.APPROVED });
}
};
export const blacklistLicense = ({ dispatch }, license) => {
const { approvalStatus } = license;
if (approvalStatus !== LICENSE_APPROVAL_STATUS.BLACKLISTED) {
dispatch('setLicenseApproval', { license, newStatus: LICENSE_APPROVAL_STATUS.BLACKLISTED });
}
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import { n__, s__, sprintf } from '~/locale';
import { parseLicenseReportMetrics } from './utils';
export const isLoading = state => state.isLoadingManagedLicenses || state.isLoadingLicenseReport;
export const licenseReport = state =>
parseLicenseReportMetrics(state.headReport, state.baseReport, state.managedLicenses);
export const licenseSummaryText = (state, getters) => {
if (getters.isLoading) {
return sprintf(s__('ciReport|Loading %{reportName} report'), {
reportName: s__('license management'),
});
}
if (state.loadLicenseReportError) {
return sprintf(s__('ciReport|Failed to load %{reportName} report'), {
reportName: s__('license management'),
});
}
if (getters.licenseReport && getters.licenseReport.length > 0) {
return n__(
'ciReport|License management detected %d new license',
'ciReport|License management detected %d new licenses',
getters.licenseReport.length,
);
}
return s__('ciReport|License management detected no new licenses');
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import Vue from 'vue';
import Vuex from 'vuex';
import createState from './state';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
Vue.use(Vuex);
export default () =>
new Vuex.Store({
state: createState(),
actions,
getters,
mutations,
});
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_MANAGED_LICENSES = 'RECEIVE_LOAD_MANAGED_LICENSES';
export const RECEIVE_LOAD_MANAGED_LICENSES_ERROR = 'RECEIVE_LOAD_MANAGED_LICENSES_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_MANAGED_LICENSES = 'REQUEST_LOAD_MANAGED_LICENSES';
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';
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import * as types from './mutation_types';
import { normalizeLicense, byLicenseNameComparator } from './utils';
export default {
[types.SET_LICENSE_IN_MODAL](state, license) {
Object.assign(state, {
currentLicenseInModal: license,
});
},
[types.RESET_LICENSE_IN_MODAL](state) {
Object.assign(state, {
currentLicenseInModal: null,
});
},
[types.SET_API_SETTINGS](state, data) {
Object.assign(state, data);
},
[types.RECEIVE_LOAD_MANAGED_LICENSES](state, licenses = []) {
const managedLicenses = licenses.map(normalizeLicense).sort(byLicenseNameComparator);
Object.assign(state, {
managedLicenses,
isLoadingManagedLicenses: false,
loadManagedLicensesError: false,
});
},
[types.RECEIVE_LOAD_MANAGED_LICENSES_ERROR](state, error) {
Object.assign(state, {
managedLicenses: [],
isLoadingManagedLicenses: false,
loadManagedLicensesError: error,
});
},
[types.REQUEST_LOAD_MANAGED_LICENSES](state) {
Object.assign(state, {
isLoadingManagedLicenses: true,
});
},
[types.RECEIVE_LOAD_LICENSE_REPORT](state, reports) {
const { headReport, baseReport } = reports;
Object.assign(state, {
headReport,
baseReport,
isLoadingLicenseReport: false,
loadLicenseReportError: false,
});
},
[types.RECEIVE_LOAD_LICENSE_REPORT_ERROR](state, error) {
Object.assign(state, {
managedLicenses: [],
isLoadingLicenseReport: false,
loadLicenseReportError: error,
});
},
[types.REQUEST_LOAD_LICENSE_REPORT](state) {
Object.assign(state, {
isLoadingLicenseReport: true,
});
},
[types.RECEIVE_DELETE_LICENSE](state) {
Object.assign(state, {
isDeleting: false,
currentLicenseInModal: null,
});
},
[types.RECEIVE_DELETE_LICENSE_ERROR](state) {
Object.assign(state, {
isDeleting: false,
currentLicenseInModal: null,
});
},
[types.REQUEST_DELETE_LICENSE](state) {
Object.assign(state, {
isDeleting: true,
});
},
[types.REQUEST_SET_LICENSE_APPROVAL](state) {
Object.assign(state, {
isSaving: true,
});
},
[types.RECEIVE_SET_LICENSE_APPROVAL](state) {
Object.assign(state, {
isSaving: false,
currentLicenseInModal: null,
});
},
[types.RECEIVE_SET_LICENSE_APPROVAL_ERROR](state) {
Object.assign(state, {
isSaving: false,
currentLicenseInModal: null,
});
},
};
export default () => ({
apiUrlManageLicenses: null,
headPath: null,
basePath: null,
managedLicenses: [],
headReport: null,
baseReport: null,
currentLicenseInModal: null,
isDeleting: false,
isLoadingManagedLicenses: false,
isLoadingLicenseReport: false,
isSaving: false,
loadManagedLicensesError: false,
loadLicenseReportError: false,
canManageLicenses: false,
});
import { n__, sprintf } from '~/locale';
import {
STATUS_FAILED,
STATUS_NEUTRAL,
STATUS_SUCCESS,
} from '~/vue_shared/components/reports/constants';
import { LICENSE_APPROVAL_STATUS } from 'ee/vue_shared/license_management/constants';
/**
*
* Converts the snake case in license objects to camel case
*
* @param license {Object} License Object
* @returns {Object}
*
*/
export const normalizeLicense = license => {
const { approval_status: approvalStatus, ...rest } = license;
return {
...rest,
approvalStatus,
};
};
/**
*
* Comparator function for sorting licenses by name
*
* @param a {Object} License Object a
* @param b {Object} License Object b
* @returns {number}
*
* @example
*
* arrayOfLicenses.sort(byLicenseNameComparator)
*
*/
export const byLicenseNameComparator = (a, b) => {
const x = (a.name || '').toLowerCase();
const y = (b.name || '').toLowerCase();
if (x === y) {
return 0;
}
return x > y ? 1 : -1;
};
export const getIssueStatusFromLicenseStatus = approvalStatus => {
if (approvalStatus === LICENSE_APPROVAL_STATUS.APPROVED) {
return STATUS_SUCCESS;
} else if (approvalStatus === LICENSE_APPROVAL_STATUS.BLACKLISTED) {
return STATUS_FAILED;
}
return STATUS_NEUTRAL;
};
const getLicenseStatusByName = (managedLicenses = [], licenseName) =>
managedLicenses.find(license => license.name === licenseName) || {};
const getDependenciesByLicenseName = (dependencies = [], licenseName) =>
dependencies.filter(dependencyItem => dependencyItem.license.name === licenseName);
/**
*
* Prepares a license report of the format:
*
* [
* {
* name: 'MIT',
* count: 1,
* url: 'https://spdx.org/MIT',
* packages: [{name: 'vue'}],
* approvalStatus: 'approved',
* id: 4,
* }
* ]
*
* @param headMetrics {Object}
* License scanning report on head. Contains all found licenses and dependencies.
* @param baseMetrics {Object}
* License scanning report on base. Contains all found licenses and dependencies.
* @param managedLicenses {Array} List of licenses currently managed. (Approval Status)
* @returns {Array}
*/
export const parseLicenseReportMetrics = (headMetrics, baseMetrics, managedLicenses) => {
if (!headMetrics && !baseMetrics) {
return [];
}
const headLicenses = headMetrics.licenses || [];
const headDependencies = headMetrics.dependencies || [];
const baseLicenses = baseMetrics.licenses || [];
const managedLicenseList = managedLicenses || [];
if (headLicenses.length > 0 && headDependencies.length > 0) {
const report = [];
const knownLicenses = baseLicenses.map(license => license.name);
headLicenses.forEach(license => {
const { name, count } = license;
if (!knownLicenses.includes(name)) {
const { id, approvalStatus } = getLicenseStatusByName(managedLicenseList, name);
const dependencies = getDependenciesByLicenseName(headDependencies, name);
const url =
(dependencies &&
dependencies[0] &&
dependencies[0].license &&
dependencies[0].license.url) ||
'';
report.push({
name,
count,
url,
packages: dependencies.map(dependencyItem => dependencyItem.dependency),
status: getIssueStatusFromLicenseStatus(approvalStatus),
approvalStatus,
id,
});
}
});
return report.sort(byLicenseNameComparator);
}
return [];
};
export const getPackagesString = (packages, truncate, maxPackages) => {
const translatedMessage = n__(
'ciReport|Used by %{packagesString}',
'ciReport|Used by %{packagesString}, and %{lastPackage}',
packages.length,
);
let packagesString;
let lastPackage = '';
if (packages.length === 1) {
// When there is only 1 package name to show.
packagesString = packages[0].name;
} else if (truncate && packages.length > maxPackages) {
// When packages count is higher than displayPackageCount
// and truncate is true.
packagesString = packages
.slice(0, maxPackages)
.map(packageItem => packageItem.name)
.join(', ');
} else {
// Return all package names separated by comma with proper grammar
packagesString = packages
.slice(0, packages.length - 1)
.map(packageItem => packageItem.name)
.join(', ');
lastPackage = packages[packages.length - 1].name;
}
return sprintf(translatedMessage, {
packagesString,
lastPackage,
});
};
......@@ -110,7 +110,7 @@
}
}
.grouped-security-reports {
.grouped-security-reports, .license-report-widget {
padding: 0;
> .media {
......
......@@ -140,3 +140,22 @@
border-bottom: none;
}
}
.license-management {
li.list-group-item {
line-height: $gl-padding-32;
}
.dropdown-toggle {
min-width: $gl-padding-8 * 15;
}
.dropdown-item svg {
vertical-align: sub;
}
.btn-blank {
padding: 6px 10px;
}
}
module EE
module Projects
module Settings
module CiCdController
include ::API::Helpers::RelatedResourcesHelpers
extend ::Gitlab::Utils::Override
# rubocop:disable Gitlab/ModuleWithInstanceVariables
override :show
def show
if project.feature_available?(:license_management)
@license_management_url = expose_url(api_v4_projects_managed_licenses_path(id: @project.id))
end
super
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
end
end
end
- return unless @project.feature_available?(:license_management)
- expanded = Rails.env.test?
%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
= s_('LicenseManagement|License Management')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
= s_('LicenseManagement|Manage approved and blacklisted licenses for this project.')
.settings-content
#js-managed-licenses{ data: { api_url: @license_management_url } }
= render_ce 'projects/settings/ci_cd/show'
= render 'projects/settings/ci_cd/managed_licenses'
---
title: Add license management frontend
merge_request: 6638
author:
type: added
import Vue from 'vue';
import Vuex from 'vuex';
import DeleteConfirmationModal from 'ee/vue_shared/license_management/components/delete_confirmation_modal.vue';
import { trimText } from 'spec/helpers/vue_component_helper';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { approvedLicense } from 'ee_spec/license_management/mock_data';
describe('DeleteConfirmationModal', () => {
const Component = Vue.extend(DeleteConfirmationModal);
let vm;
let store;
let actions;
beforeEach(() => {
actions = {
resetLicenseInModal: jasmine.createSpy('resetLicenseInModal'),
deleteLicense: jasmine.createSpy('deleteLicense'),
};
store = new Vuex.Store({
state: {
currentLicenseInModal: approvedLicense,
},
actions,
});
vm = mountComponentWithStore(Component, { store });
});
afterEach(() => {
vm.$destroy();
});
describe('computed', () => {
describe('confirmationText', () => {
it('returns information text with current license name in bold', () => {
expect(vm.confirmationText).toBe(
`You are about to remove the license, <strong>${
approvedLicense.name
}</strong>, from this project.`,
);
});
it('escapes the license name', done => {
const name = '<a href="#">BAD</a>';
const nameEscaped = '&lt;a href=&quot;#&quot;&gt;BAD&lt;/a&gt;';
store.replaceState({
...store.state,
currentLicenseInModal: {
...approvedLicense,
name,
},
});
Vue.nextTick()
.then(() => {
expect(vm.confirmationText).toBe(
`You are about to remove the license, <strong>${nameEscaped}</strong>, from this project.`,
);
})
.then(done)
.catch(done.fail);
});
});
});
describe('interaction', () => {
describe('triggering resetLicenseInModal on canceling', () => {
it('by clicking the cancel button', () => {
const linkEl = vm.$el.querySelector('.js-modal-cancel-action');
linkEl.click();
expect(actions.resetLicenseInModal).toHaveBeenCalled();
});
it('by clicking the X button', () => {
const linkEl = vm.$el.querySelector('.js-modal-close-action');
linkEl.click();
expect(actions.resetLicenseInModal).toHaveBeenCalled();
});
});
describe('triggering deleteLicense on canceling', () => {
it('by clicking the confirmation button', () => {
const linkEl = vm.$el.querySelector('.js-modal-primary-action');
linkEl.click();
expect(actions.deleteLicense).toHaveBeenCalledWith(
jasmine.any(Object),
store.state.currentLicenseInModal,
undefined,
);
});
});
});
describe('template', () => {
it('renders modal title', () => {
const headerEl = vm.$el.querySelector('.modal-title');
expect(headerEl).not.toBeNull();
expect(headerEl.innerText.trim()).toBe('Remove license?');
});
it('renders button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-primary-action');
expect(footerButton).not.toBeNull();
expect(footerButton.innerText.trim()).toBe('Remove license');
});
it('renders modal body', () => {
const modalBody = vm.$el.querySelector('.modal-body');
expect(modalBody).not.toBeNull();
expect(trimText(modalBody.innerText)).toBe(
`You are about to remove the license, ${approvedLicense.name}, from this project.`,
);
});
});
});
import Vue from 'vue';
import LicenseIssueBody from 'ee/vue_shared/license_management/components/license_issue_body.vue';
import { LICENSE_APPROVAL_STATUS } from 'ee/vue_shared/license_management/constants';
import { trimText } from 'spec/helpers/vue_component_helper';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import createStore from 'ee/vue_shared/license_management/store';
import { licenseReport } from 'ee_spec/license_management/mock_data';
describe('LicenseIssueBody', () => {
const issue = licenseReport[0];
const Component = Vue.extend(LicenseIssueBody);
let vm;
let store;
beforeEach(() => {
store = createStore();
vm = mountComponentWithStore(Component, { props: { issue }, store });
});
afterEach(() => {
vm.$destroy();
});
describe('computed', () => {
describe('status', () => {
it('returns correct status for Approved licenses', done => {
vm.issue = { ...vm.issue, approvalStatus: LICENSE_APPROVAL_STATUS.APPROVED };
Vue.nextTick()
.then(() => {
expect(vm.status).toBe('Approved');
})
.then(done)
.catch(done.fail);
});
it('returns correct status for Blacklisted licenses', done => {
vm.issue = { ...vm.issue, approvalStatus: LICENSE_APPROVAL_STATUS.BLACKLISTED };
Vue.nextTick()
.then(() => {
expect(vm.status).toBe('Blacklisted');
})
.then(done)
.catch(done.fail);
});
it('returns correct status for Unapproved licenses', done => {
vm.issue = { ...vm.issue, approvalStatus: undefined };
Vue.nextTick()
.then(() => {
expect(vm.status).toBe('Unapproved');
})
.then(done)
.catch(done.fail);
});
});
});
describe('interaction', () => {
it('clicking the button triggers openModal with the current license', () => {
const linkEl = vm.$el.querySelector('.license-item > .btn-link');
expect(store.state.currentLicenseInModal).toBe(null);
linkEl.click();
expect(store.state.currentLicenseInModal).toBe(issue);
});
});
describe('template', () => {
it('renders component container element with class `license-item`', () => {
expect(vm.$el.classList.contains('license-item')).toBe(true);
});
it('renders button to open modal', () => {
const linkEl = vm.$el.querySelector('.license-item > .btn-link');
expect(linkEl).not.toBeNull();
expect(linkEl.innerText.trim()).toBe(issue.name);
});
it('renders packages list', () => {
const packagesEl = vm.$el.querySelector('.license-packages');
expect(packagesEl).not.toBeNull();
expect(trimText(packagesEl.innerText)).toBe('Used by pg, puma, foo, and 2 more');
});
});
});
import Vue from 'vue';
import Vuex from 'vuex';
import LicenseManagementRow from 'ee/vue_shared/license_management/components/license_management_row.vue';
import { LICENSE_APPROVAL_STATUS } from 'ee/vue_shared/license_management/constants';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { approvedLicense } from 'ee_spec/license_management/mock_data';
const visibleClass = 'visible';
const invisibleClass = 'invisible';
describe('LicenseManagementRow', () => {
const Component = Vue.extend(LicenseManagementRow);
let vm;
let store;
let actions;
beforeEach(() => {
actions = {
setLicenseInModal: jasmine.createSpy('setLicenseInModal'),
approveLicense: jasmine.createSpy('approveLicense'),
blacklistLicense: jasmine.createSpy('blacklistLicense'),
};
store = new Vuex.Store({
state: {},
actions,
});
const props = { license: approvedLicense };
vm = mountComponentWithStore(Component, { props, store });
});
afterEach(() => {
vm.$destroy();
});
describe('approved license', () => {
beforeEach(done => {
vm.license = { ...approvedLicense, approvalStatus: LICENSE_APPROVAL_STATUS.APPROVED };
Vue.nextTick(done);
});
describe('computed', () => {
it('dropdownText returns `Approved`', () => {
expect(vm.dropdownText).toBe('Approved');
});
it('isApproved returns `true`', () => {
expect(vm.approveIconClass).toBe(visibleClass);
});
it('isBlacklisted returns `false`', () => {
expect(vm.blacklistIconClass).toBe(invisibleClass);
});
});
describe('template', () => {
it('first dropdown element should have a visible icon', () => {
const firstOption = vm.$el.querySelector('.dropdown-item:nth-child(1) svg');
expect(firstOption.classList).toContain(visibleClass);
});
it('second dropdown element should have no visible icon', () => {
const secondOption = vm.$el.querySelector('.dropdown-item:nth-child(2) svg');
expect(secondOption.classList).toContain(invisibleClass);
});
});
});
describe('blacklisted license', () => {
beforeEach(done => {
vm.license = { ...approvedLicense, approvalStatus: LICENSE_APPROVAL_STATUS.BLACKLISTED };
Vue.nextTick(done);
});
describe('computed', () => {
it('dropdownText returns `Blacklisted`', () => {
expect(vm.dropdownText).toBe('Blacklisted');
});
it('isApproved returns `false`', () => {
expect(vm.approveIconClass).toBe(invisibleClass);
});
it('isBlacklisted returns `true`', () => {
expect(vm.blacklistIconClass).toBe(visibleClass);
});
});
describe('template', () => {
it('first dropdown element should have no visible icon', () => {
const firstOption = vm.$el.querySelector('.dropdown-item:nth-child(1) svg');
expect(firstOption.classList).toContain(invisibleClass);
});
it('second dropdown element should have a visible icon', () => {
const secondOption = vm.$el.querySelector('.dropdown-item:nth-child(2) svg');
expect(secondOption.classList).toContain(visibleClass);
});
});
});
describe('interaction', () => {
it('triggering setLicenseInModal by clicking the cancel button', () => {
const linkEl = vm.$el.querySelector('.js-remove-button');
linkEl.click();
expect(actions.setLicenseInModal).toHaveBeenCalled();
});
it('triggering approveLicense by clicking the first dropdown option', () => {
const linkEl = vm.$el.querySelector('.dropdown-item:nth-child(1)');
linkEl.click();
expect(actions.approveLicense).toHaveBeenCalled();
});
it('triggering approveLicense blacklistLicense by clicking the second dropdown option', () => {
const linkEl = vm.$el.querySelector('.dropdown-item:nth-child(2)');
linkEl.click();
expect(actions.blacklistLicense).toHaveBeenCalled();
});
});
describe('template', () => {
it('renders component container element with class `list-group-item`', () => {
expect(vm.$el.classList.contains('list-group-item')).toBe(true);
});
it('renders status icon', () => {
const iconEl = vm.$el.querySelector('.report-block-list-icon');
expect(iconEl).not.toBeNull();
});
it('renders license name', () => {
const nameEl = vm.$el.querySelector('.js-license-name');
expect(nameEl.innerText.trim()).toBe(approvedLicense.name);
});
it('renders the removal button', () => {
const buttonEl = vm.$el.querySelector('.js-remove-button');
expect(buttonEl).not.toBeNull();
expect(buttonEl.querySelector('.ic-remove')).not.toBeNull();
});
it('renders computed property dropdownText into dropdown toggle', () => {
const dropdownEl = vm.$el.querySelector('.dropdown [data-toggle="dropdown"]');
expect(dropdownEl.innerText.trim()).toBe(vm.dropdownText);
});
it('renders the dropdown with `Approved` and `Blacklisted` options', () => {
const dropdownEl = vm.$el.querySelector('.dropdown');
expect(dropdownEl).not.toBeNull();
const firstOption = dropdownEl.querySelector('.dropdown-item:nth-child(1)');
expect(firstOption).not.toBeNull();
expect(firstOption.innerText.trim()).toBe('Approved');
const secondOption = dropdownEl.querySelector('.dropdown-item:nth-child(2)');
expect(secondOption).not.toBeNull();
expect(secondOption.innerText.trim()).toBe('Blacklisted');
});
});
});
import Vue from 'vue';
import LicensePackages from 'ee/vue_shared/license_management/components/license_packages.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { licenseReport } from 'ee_spec/license_management/mock_data';
const examplePackages = licenseReport[0].packages;
const createComponent = (packages = examplePackages) => {
const Component = Vue.extend(LicensePackages);
return mountComponent(Component, { packages });
};
describe('LicensePackages', () => {
let vm;
beforeEach(() => {
vm = createComponent();
});
afterEach(() => {
vm.$destroy();
});
describe('computed', () => {
describe('remainingPackages', () => {
it('returns string with count of packages when it exceeds `displayPackageCount` prop', () => {
expect(vm.remainingPackages).toBe('2 more');
});
it('returns empty string when count of packages does not exceed `displayPackageCount` prop', done => {
vm.displayPackageCount = examplePackages.length + 1;
Vue.nextTick()
.then(() => {
expect(vm.remainingPackages).toBe('');
})
.then(done)
.catch(done.fail);
});
});
});
describe('methods', () => {
describe('handleShowPackages', () => {
it('sets value of `showAllPackages` prop to true', () => {
vm.showAllPackages = false;
vm.handleShowPackages();
expect(vm.showAllPackages).toBe(true);
});
});
});
describe('template', () => {
it('renders packages list for a particular license', () => {
const packagesEl = vm.$el.querySelector('.js-license-dependencies');
expect(packagesEl).not.toBeNull();
expect(packagesEl.innerText.trim()).toBe('Used by pg, puma, foo, and');
});
it('renders more packages button element', () => {
const buttonEl = vm.$el.querySelector('.btn-show-all-packages');
expect(buttonEl).not.toBeNull();
expect(buttonEl.innerText.trim()).toBe('2 more');
});
});
});
import Vue from 'vue';
import Vuex from 'vuex';
import SetApprovalModal from 'ee/vue_shared/license_management/components/set_approval_status_modal.vue';
import { LICENSE_APPROVAL_STATUS } from 'ee/vue_shared/license_management/constants';
import { trimText } from 'spec/helpers/vue_component_helper';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { licenseReport } from 'ee_spec/license_management/mock_data';
describe('SetApprovalModal', () => {
const Component = Vue.extend(SetApprovalModal);
let vm;
let store;
let actions;
beforeEach(() => {
actions = {
resetLicenseInModal: jasmine.createSpy('resetLicenseInModal'),
approveLicense: jasmine.createSpy('approveLicense'),
blacklistLicense: jasmine.createSpy('blacklistLicense'),
};
store = new Vuex.Store({
state: {
currentLicenseInModal: licenseReport[0],
canManageLicenses: true,
},
actions,
});
vm = mountComponentWithStore(Component, { store });
});
afterEach(() => {
vm.$destroy();
});
describe('for approved license', () => {
beforeEach(done => {
store.replaceState({
currentLicenseInModal: {
...licenseReport[0],
approvalStatus: LICENSE_APPROVAL_STATUS.APPROVED,
},
canManageLicenses: true,
});
Vue.nextTick(done);
});
describe('computed', () => {
it('headerTitleText returns `Blacklist license?`', () => {
expect(vm.headerTitleText).toBe('Blacklist license?');
});
it('canApprove is false', () => {
expect(vm.canApprove).toBe(false);
});
it('canBlacklist is true', () => {
expect(vm.canBlacklist).toBe(true);
});
});
describe('template correctly', () => {
it('renders modal title', () => {
const headerEl = vm.$el.querySelector('.modal-title');
expect(headerEl).not.toBeNull();
expect(headerEl.innerText.trim()).toBe('Blacklist license?');
});
it('renders no Approve button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-primary-action');
expect(footerButton).toBeNull();
});
it('renders Blacklist button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-secondary-action');
expect(footerButton).not.toBeNull();
expect(footerButton.innerText.trim()).toBe('Blacklist license');
});
});
});
describe('for unapproved license', () => {
beforeEach(done => {
store.replaceState({
currentLicenseInModal: {
...licenseReport[0],
approvalStatus: undefined,
},
canManageLicenses: true,
});
Vue.nextTick(done);
});
describe('computed', () => {
it('headerTitleText returns `Approve license?`', () => {
expect(vm.headerTitleText).toBe('Approve license?');
});
it('canApprove is true', () => {
expect(vm.canApprove).toBe(true);
});
it('canBlacklist is true', () => {
expect(vm.canBlacklist).toBe(true);
});
});
describe('template', () => {
it('renders modal title', () => {
const headerEl = vm.$el.querySelector('.modal-title');
expect(headerEl).not.toBeNull();
expect(headerEl.innerText.trim()).toBe('Approve license?');
});
it('renders Approve button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-primary-action');
expect(footerButton).not.toBeNull();
expect(footerButton.innerText.trim()).toBe('Approve license');
});
it('renders Blacklist button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-secondary-action');
expect(footerButton).not.toBeNull();
expect(footerButton.innerText.trim()).toBe('Blacklist license');
});
});
});
describe('for blacklisted license', () => {
beforeEach(done => {
store.replaceState({
currentLicenseInModal: {
...licenseReport[0],
approvalStatus: LICENSE_APPROVAL_STATUS.BLACKLISTED,
},
canManageLicenses: true,
});
Vue.nextTick(done);
});
describe('computed', () => {
it('headerTitleText returns `Approve license?`', () => {
expect(vm.headerTitleText).toBe('Approve license?');
});
it('canApprove is true', () => {
expect(vm.canApprove).toBe(true);
});
it('canBlacklist is false', () => {
expect(vm.canBlacklist).toBe(false);
});
});
describe('template', () => {
it('renders modal title', () => {
const headerEl = vm.$el.querySelector('.modal-title');
expect(headerEl).not.toBeNull();
expect(headerEl.innerText.trim()).toBe('Approve license?');
});
it('renders Approve button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-primary-action');
expect(footerButton).not.toBeNull();
expect(footerButton.innerText.trim()).toBe('Approve license');
});
it('renders no Blacklist button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-secondary-action');
expect(footerButton).toBeNull();
});
});
});
describe('for user without the rights to manage licenses', () => {
beforeEach(done => {
store.replaceState({
currentLicenseInModal: {
...licenseReport[0],
approvalStatus: undefined,
},
canManageLicenses: false,
});
Vue.nextTick(done);
});
describe('computed', () => {
it('headerTitleText returns `License details`', () => {
expect(vm.headerTitleText).toBe('License details');
});
it('canApprove is false', () => {
expect(vm.canApprove).toBe(false);
});
it('canBlacklist is false', () => {
expect(vm.canBlacklist).toBe(false);
});
});
describe('template', () => {
it('renders modal title', () => {
const headerEl = vm.$el.querySelector('.modal-title');
expect(headerEl).not.toBeNull();
expect(headerEl.innerText.trim()).toBe('License details');
});
it('renders no Approve button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-primary-action');
expect(footerButton).toBeNull();
});
it('renders no Blacklist button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-secondary-action');
expect(footerButton).toBeNull();
});
});
});
describe('Modal Body', () => {
it('renders the license name', () => {
const licenseName = vm.$el.querySelector('.js-license-name');
expect(licenseName).not.toBeNull();
expect(trimText(licenseName.innerText)).toBe(`License: ${licenseReport[0].name}`);
});
it('renders the license url with link', () => {
const licenseName = vm.$el.querySelector('.js-license-url');
expect(licenseName).not.toBeNull();
expect(trimText(licenseName.innerText)).toBe(`URL: ${licenseReport[0].url}`);
const licenseLink = licenseName.querySelector('a');
expect(licenseLink.getAttribute('href')).toBe(licenseReport[0].url);
expect(trimText(licenseLink.innerText)).toBe(licenseReport[0].url);
});
it('renders the license url', () => {
const licenseName = vm.$el.querySelector('.js-license-packages');
expect(licenseName).not.toBeNull();
expect(trimText(licenseName.innerText)).toBe('Packages: Used by pg, puma, foo, and 2 more');
});
});
describe('interaction', () => {
describe('triggering resetLicenseInModal on canceling', () => {
it('by clicking the cancel button', () => {
const linkEl = vm.$el.querySelector('.js-modal-cancel-action');
linkEl.click();
expect(actions.resetLicenseInModal).toHaveBeenCalled();
});
it('triggering resetLicenseInModal by clicking the X button', () => {
const linkEl = vm.$el.querySelector('.js-modal-close-action');
linkEl.click();
expect(actions.resetLicenseInModal).toHaveBeenCalled();
});
});
describe('triggering approveLicense on approving', () => {
it('by clicking the confirmation button', () => {
const linkEl = vm.$el.querySelector('.js-modal-primary-action');
linkEl.click();
expect(actions.approveLicense).toHaveBeenCalledWith(
jasmine.any(Object),
store.state.currentLicenseInModal,
undefined,
);
});
});
describe('triggering blacklistLicense on blacklisting', () => {
it('by clicking the confirmation button', () => {
const linkEl = vm.$el.querySelector('.js-modal-secondary-action');
linkEl.click();
expect(actions.blacklistLicense).toHaveBeenCalledWith(
jasmine.any(Object),
store.state.currentLicenseInModal,
undefined,
);
});
});
});
});
import Vue from 'vue';
import Vuex from 'vuex';
import LicenseManagement from 'ee/vue_shared/license_management/license_management.vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { trimText } from 'spec/helpers/vue_component_helper';
import { TEST_HOST } from 'spec/test_constants';
import { approvedLicense, blacklistedLicense } from 'ee_spec/license_management/mock_data';
describe('LicenseManagement', () => {
const Component = Vue.extend(LicenseManagement);
const apiUrl = `${TEST_HOST}/license_management`;
let vm;
let store;
let actions;
beforeEach(() => {
actions = {
setAPISettings: jasmine.createSpy('setAPISettings').and.callFake(() => {}),
loadManagedLicenses: jasmine.createSpy('loadManagedLicenses').and.callFake(() => {}),
};
store = new Vuex.Store({
state: {
managedLicenses: [approvedLicense, blacklistedLicense],
currentLicenseInModal: approvedLicense,
isLoadingManagedLicenses: true,
},
actions,
});
vm = mountComponentWithStore(Component, { props: { apiUrl }, store });
});
afterEach(() => {
vm.$destroy();
});
it('should render loading icon', done => {
store.replaceState({ ...store.state, isLoadingManagedLicenses: true });
return Vue.nextTick().then(() => {
expect(vm.$el.querySelector('.loading-container')).not.toBeNull();
done();
});
});
it('should render callout if no licenses are managed', done => {
store.replaceState({ ...store.state, managedLicenses: [], isLoadingManagedLicenses: false });
return Vue.nextTick().then(() => {
const callout = vm.$el.querySelector('.bs-callout');
expect(callout).not.toBeNull();
expect(trimText(callout.innerText)).toBe(vm.$options.emptyMessage);
done();
});
});
it('should render delete confirmation modal', done => {
store.replaceState({ ...store.state });
return Vue.nextTick().then(() => {
expect(vm.$el.querySelector('#modal-license-delete-confirmation')).not.toBeNull();
done();
});
});
it('should render list of managed licenses', done => {
store.replaceState({ ...store.state, isLoadingManagedLicenses: false });
return Vue.nextTick().then(() => {
expect(vm.$el.querySelector('.list-group')).not.toBeNull();
expect(vm.$el.querySelector('.list-group .list-group-item')).not.toBeNull();
expect(vm.$el.querySelectorAll('.list-group .list-group-item').length).toBe(2);
done();
});
});
it('should set api settings after mount and init API calls', () =>
Vue.nextTick().then(() => {
expect(actions.setAPISettings).toHaveBeenCalledWith(
jasmine.any(Object),
{ apiUrlManageLicenses: apiUrl },
undefined,
);
expect(actions.loadManagedLicenses).toHaveBeenCalledWith(
jasmine.any(Object),
undefined,
undefined,
);
}));
});
import { LICENSE_APPROVAL_STATUS } from 'ee/vue_shared/license_management/constants';
export const approvedLicense = {
id: 5,
name: 'MIT',
approvalStatus: LICENSE_APPROVAL_STATUS.APPROVED,
};
export const blacklistedLicense = {
id: 6,
name: 'New BSD',
approvalStatus: LICENSE_APPROVAL_STATUS.BLACKLISTED,
};
export const licenseBaseIssues = {
licenses: [
{
count: 1,
name: 'MIT',
},
],
dependencies: [
{
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",
pathes: ['.'],
},
},
],
};
export const licenseHeadIssues = {
licenses: [
{
count: 3,
name: 'New BSD',
},
{
count: 1,
name: 'MIT',
},
],
dependencies: [
{
license: {
name: 'New BSD',
url: 'http://opensource.org/licenses/BSD-3-Clause',
},
dependency: {
name: 'pg',
url: 'https://bitbucket.org/ged/ruby-pg',
description:
'Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/]',
pathes: ['.'],
},
},
{
license: {
name: 'New BSD',
url: 'http://opensource.org/licenses/BSD-3-Clause',
},
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',
pathes: ['.'],
},
},
{
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',
pathes: ['.'],
},
},
{
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',
pathes: ['.'],
},
},
],
};
export const licenseReport = [
{
name: 'New BSD',
count: 5,
url: 'http://opensource.org/licenses/BSD-3-Clause',
packages: [
{
name: 'pg',
url: 'https://bitbucket.org/ged/ruby-pg',
description:
'Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/]',
pathes: ['.'],
},
{
name: 'puma',
url: 'http://puma.io',
description:
'Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications',
pathes: ['.'],
},
{
name: 'foo',
url: 'https://bitbucket.org/ged/ruby-pg',
description:
'Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/]',
pathes: ['.'],
},
{
name: 'bar',
url: 'http://puma.io',
description:
'Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications',
pathes: ['.'],
},
{
name: 'baz',
url: 'https://bitbucket.org/ged/ruby-pg',
description:
'Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/]',
pathes: ['.'],
},
],
},
];
export default () => {};
import Vue from 'vue';
import Vuex from 'vuex';
import LicenseManagement from 'ee/vue_shared/license_management/mr_widget_license_report.vue';
import { LOADING, ERROR, SUCCESS } from 'ee/vue_shared/security_reports/store/constants';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { TEST_HOST } from 'spec/test_constants';
import {
approvedLicense,
blacklistedLicense,
licenseReport as licenseReportMock,
} from 'ee_spec/license_management/mock_data';
describe('License Report MR Widget', () => {
const Component = Vue.extend(LicenseManagement);
const apiUrl = `${TEST_HOST}/license_management`;
let vm;
let store;
let actions;
let getters;
const props = {
loadingText: 'LOADING',
errorText: 'ERROR',
headPath: `${TEST_HOST}/head.json`,
basePath: `${TEST_HOST}/head.json`,
canManageLicenses: true,
apiUrl,
};
beforeEach(() => {
actions = {
setAPISettings: jasmine.createSpy('setAPISettings').and.callFake(() => {}),
loadManagedLicenses: jasmine.createSpy('loadManagedLicenses').and.callFake(() => {}),
loadLicenseReport: jasmine.createSpy('loadLicenseReport').and.callFake(() => {}),
};
getters = {
isLoading() {
return false;
},
licenseReport() {
return licenseReportMock;
},
licenseSummaryText() {
return 'FOO';
},
};
store = new Vuex.Store({
state: {
managedLicenses: [approvedLicense, blacklistedLicense],
currentLicenseInModal: licenseReportMock[0],
isLoadingManagedLicenses: true,
},
getters,
actions,
});
vm = mountComponentWithStore(Component, { props, store });
});
afterEach(() => {
vm.$destroy();
});
describe('computed', () => {
describe('hasLicenseReportIssues', () => {
it('should be false, if the report is empty', done => {
store.hotUpdate({
getters: {
...getters,
licenseReport() {
return [];
},
},
});
return Vue.nextTick().then(() => {
expect(vm.hasLicenseReportIssues).toBe(false);
done();
});
});
it('should be true, if the report is not empty', done =>
Vue.nextTick().then(() => {
expect(vm.hasLicenseReportIssues).toBe(true);
done();
}));
});
describe('licenseReportStatus', () => {
it('should be `LOADING`, if the report is loading', done => {
store.hotUpdate({
getters: {
...getters,
isLoading() {
return true;
},
},
});
return Vue.nextTick().then(() => {
expect(vm.licenseReportStatus).toBe(LOADING);
done();
});
});
it('should be `ERROR`, if the report is has an error', done => {
store.replaceState({ ...store.state, loadLicenseReportError: new Error('test') });
return Vue.nextTick().then(() => {
expect(vm.licenseReportStatus).toBe(ERROR);
done();
});
});
it('should be `SUCCESS`, if the report is successful', done =>
Vue.nextTick().then(() => {
expect(vm.licenseReportStatus).toBe(SUCCESS);
done();
}));
});
});
it('should render report section wrapper', done =>
Vue.nextTick().then(() => {
expect(vm.$el.querySelector('.license-report-widget')).not.toBeNull();
done();
}));
it('should render report widget section', done =>
Vue.nextTick().then(() => {
expect(vm.$el.querySelector('.report-block-container')).not.toBeNull();
done();
}));
it('should render set approval modal', done => {
store.replaceState({ ...store.state });
return Vue.nextTick().then(() => {
expect(vm.$el.querySelector('#modal-set-license-approval')).not.toBeNull();
done();
});
});
it('should init store after mount', () =>
Vue.nextTick().then(() => {
expect(actions.setAPISettings).toHaveBeenCalledWith(
jasmine.any(Object),
{
apiUrlManageLicenses: apiUrl,
headPath: props.headPath,
basePath: props.basePath,
canManageLicenses: true,
},
undefined,
);
expect(actions.loadManagedLicenses).toHaveBeenCalledWith(
jasmine.any(Object),
undefined,
undefined,
);
expect(actions.loadLicenseReport).toHaveBeenCalledWith(
jasmine.any(Object),
undefined,
undefined,
);
}));
});
This diff is collapsed.
import * as getters from 'ee/vue_shared/license_management/store/getters';
import { parseLicenseReportMetrics } from 'ee/vue_shared/license_management/store/utils';
import {
licenseHeadIssues,
licenseBaseIssues,
approvedLicense,
licenseReport as licenseReportMock,
} from 'ee_spec/license_management/mock_data';
describe('getters', () => {
describe('isLoading', () => {
it('is true if `isLoadingManagedLicenses` is true OR `isLoadingLicenseReport` is true', () => {
const state = {};
state.isLoadingManagedLicenses = true;
state.isLoadingLicenseReport = true;
expect(getters.isLoading(state)).toBe(true);
state.isLoadingManagedLicenses = false;
state.isLoadingLicenseReport = true;
expect(getters.isLoading(state)).toBe(true);
state.isLoadingManagedLicenses = true;
state.isLoadingLicenseReport = false;
expect(getters.isLoading(state)).toBe(true);
state.isLoadingManagedLicenses = false;
state.isLoadingLicenseReport = false;
expect(getters.isLoading(state)).toBe(false);
});
});
describe('licenseReport', () => {
it('returns empty array, if the reports are empty', () => {
const state = { headReport: {}, baseReport: {}, managedLicenses: [] };
expect(getters.licenseReport(state)).toEqual([]);
});
it('returns license report, if the license report is not loading', () => {
const state = {
headReport: licenseHeadIssues,
baseReport: licenseBaseIssues,
managedLicenses: [approvedLicense],
};
expect(getters.licenseReport(state)).toEqual(
parseLicenseReportMetrics(licenseHeadIssues, licenseBaseIssues, [approvedLicense]),
);
});
});
describe('licenseSummaryText', () => {
const state = {
loadLicenseReportError: null,
};
it('should be `Loading license management report` text if isLoading', () => {
const mockGetters = {};
mockGetters.isLoading = true;
expect(getters.licenseSummaryText(state, mockGetters)).toBe(
'Loading license management report',
);
});
it('should be `Failed to load license management report` text if an error has happened', () => {
const mockGetters = {};
expect(
getters.licenseSummaryText({ loadLicenseReportError: new Error('Test') }, mockGetters),
).toBe('Failed to load license management report');
});
it('should be `License management detected no new licenses`, if the report is empty', () => {
const mockGetters = { licenseReport: [] };
expect(getters.licenseSummaryText(state, mockGetters)).toBe(
'License management detected no new licenses',
);
});
it('should be `License management detected 1 new license`, if the report has one element', () => {
const mockGetters = { licenseReport: [licenseReportMock[0]] };
expect(getters.licenseSummaryText(state, mockGetters)).toBe(
'License management detected 1 new license',
);
});
it('should be `License management detected 2 new licenses`, if the report has two elements', () => {
const mockGetters = { licenseReport: [licenseReportMock[0], licenseReportMock[0]] };
expect(getters.licenseSummaryText(state, mockGetters)).toBe(
'License management detected 2 new licenses',
);
});
});
});
import createStore from 'ee/vue_shared/license_management/store';
import * as types from 'ee/vue_shared/license_management/store/mutation_types';
import { LICENSE_APPROVAL_STATUS } from 'ee/vue_shared/license_management/constants';
import { TEST_HOST } from 'spec/test_constants';
import {
approvedLicense,
licenseBaseIssues,
licenseHeadIssues,
} from 'ee_spec/license_management/mock_data';
describe('License store mutations', () => {
let store;
beforeEach(() => {
store = createStore();
});
describe('SET_LICENSE_IN_MODAL', () => {
it('opens modal and sets passed license', () => {
store.commit(types.SET_LICENSE_IN_MODAL, approvedLicense);
expect(store.state.currentLicenseInModal).toBe(approvedLicense);
});
});
describe('RESET_LICENSE_IN_MODAL', () => {
it('closes modal and deletes licenseInApproval', () => {
store.replaceState({
...store.state,
currentLicenseInModal: approvedLicense,
});
store.commit(types.RESET_LICENSE_IN_MODAL);
expect(store.state.currentLicenseInModal).toBeNull();
});
});
describe('SET_API_SETTINGS', () => {
it('assigns data to the store', () => {
const data = { apiUrlManageLicenses: TEST_HOST };
store.commit(types.SET_API_SETTINGS, data);
expect(store.state.apiUrlManageLicenses).toBe(TEST_HOST);
});
});
describe('RECEIVE_DELETE_LICENSE', () => {
it('sets isDeleting to false and closes the modal', () => {
store.replaceState({
...store.state,
isDeleting: true,
});
store.commit(types.RECEIVE_DELETE_LICENSE);
expect(store.state.isDeleting).toBe(false);
});
});
describe('RECEIVE_DELETE_LICENSE_ERROR', () => {
it('sets isDeleting to false and closes the modal', () => {
store.replaceState({
...store.state,
isDeleting: true,
currentLicenseInModal: approvedLicense,
});
store.commit(types.RECEIVE_DELETE_LICENSE_ERROR);
expect(store.state.isDeleting).toBe(false);
expect(store.state.currentLicenseInModal).toBeNull();
});
});
describe('REQUEST_DELETE_LICENSE', () => {
it('sets isDeleting to true', () => {
store.replaceState({
...store.state,
isDeleting: false,
});
store.commit(types.REQUEST_DELETE_LICENSE);
expect(store.state.isDeleting).toBe(true);
});
});
describe('RECEIVE_SET_LICENSE_APPROVAL', () => {
it('sets isSaving to false and closes the modal', () => {
store.replaceState({
...store.state,
isSaving: true,
});
store.commit(types.RECEIVE_SET_LICENSE_APPROVAL);
expect(store.state.isSaving).toBe(false);
});
});
describe('RECEIVE_SET_LICENSE_APPROVAL_ERROR', () => {
it('sets isSaving to false and closes the modal', () => {
store.replaceState({
...store.state,
isSaving: true,
currentLicenseInModal: approvedLicense,
});
store.commit(types.RECEIVE_SET_LICENSE_APPROVAL_ERROR);
expect(store.state.isSaving).toBe(false);
expect(store.state.currentLicenseInModal).toBeNull();
});
});
describe('REQUEST_SET_LICENSE_APPROVAL', () => {
it('sets isSaving to true', () => {
store.replaceState({
...store.state,
isSaving: false,
});
store.commit(types.REQUEST_SET_LICENSE_APPROVAL);
expect(store.state.isSaving).toBe(true);
});
});
describe('RECEIVE_LOAD_MANAGED_LICENSES', () => {
it('sets isLoadingManagedLicenses and loadManagedLicensesError to false and saves managed licenses', () => {
store.replaceState({
...store.state,
managedLicenses: false,
isLoadingManagedLicenses: true,
loadManagedLicensesError: true,
});
store.commit(types.RECEIVE_LOAD_MANAGED_LICENSES, [
{ name: 'Foo', approval_status: LICENSE_APPROVAL_STATUS.approved },
]);
expect(store.state.managedLicenses).toEqual([
{ name: 'Foo', approvalStatus: LICENSE_APPROVAL_STATUS.approved },
]);
expect(store.state.isLoadingManagedLicenses).toBe(false);
expect(store.state.loadManagedLicensesError).toBe(false);
});
});
describe('RECEIVE_LOAD_MANAGED_LICENSES_ERROR', () => {
it('sets isLoadingManagedLicenses to true and saves the error', () => {
const error = new Error('test');
store.replaceState({
...store.state,
isLoadingManagedLicenses: true,
loadManagedLicensesError: false,
});
store.commit(types.RECEIVE_LOAD_MANAGED_LICENSES_ERROR, error);
expect(store.state.isLoadingManagedLicenses).toBe(false);
expect(store.state.loadManagedLicensesError).toBe(error);
});
});
describe('REQUEST_LOAD_MANAGED_LICENSES', () => {
it('sets isLoadingManagedLicenses to true', () => {
store.replaceState({
...store.state,
isLoadingManagedLicenses: true,
});
store.commit(types.REQUEST_LOAD_MANAGED_LICENSES);
expect(store.state.isLoadingManagedLicenses).toBe(true);
});
});
describe('RECEIVE_LOAD_LICENSE_REPORT', () => {
it('sets isLoadingLicenseReport and loadLicenseReportError to false and saves report', () => {
store.replaceState({
...store.state,
headReport: false,
baseReport: false,
isLoadingLicenseReport: true,
loadLicenseReportError: true,
});
const payload = { headReport: licenseHeadIssues, baseReport: licenseBaseIssues };
store.commit(types.RECEIVE_LOAD_LICENSE_REPORT, payload);
expect(store.state.headReport).toEqual(licenseHeadIssues);
expect(store.state.baseReport).toEqual(licenseBaseIssues);
expect(store.state.isLoadingLicenseReport).toBe(false);
expect(store.state.loadLicenseReportError).toBe(false);
});
});
describe('RECEIVE_LOAD_LICENSE_REPORT_ERROR', () => {
it('sets isLoadingLicenseReport to true and saves the error', () => {
const error = new Error('test');
store.replaceState({
...store.state,
isLoadingLicenseReport: true,
loadLicenseReportError: false,
});
store.commit(types.RECEIVE_LOAD_LICENSE_REPORT_ERROR, error);
expect(store.state.isLoadingLicenseReport).toBe(false);
expect(store.state.loadLicenseReportError).toBe(error);
});
});
describe('REQUEST_LOAD_LICENSE_REPORT', () => {
it('sets isLoadingLicenseReport to true', () => {
store.replaceState({
...store.state,
isLoadingLicenseReport: true,
});
store.commit(types.REQUEST_LOAD_LICENSE_REPORT);
expect(store.state.isLoadingLicenseReport).toBe(true);
});
});
});
import {
parseLicenseReportMetrics,
byLicenseNameComparator,
normalizeLicense,
getPackagesString,
getIssueStatusFromLicenseStatus,
} 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 '~/vue_shared/components/reports/constants';
import {
approvedLicense,
blacklistedLicense,
licenseHeadIssues,
licenseBaseIssues,
licenseReport,
} from 'ee_spec/license_management/mock_data';
describe('utils', () => {
describe('parseLicenseReportMetrics', () => {
it('should return empty result, if no parameters are given', () => {
const result = parseLicenseReportMetrics();
expect(result).toEqual(jasmine.any(Array));
expect(result.length).toEqual(0);
});
it('should return empty result, if license head report is empty', () => {
const result = parseLicenseReportMetrics({ licenses: [] }, licenseBaseIssues);
expect(result).toEqual(jasmine.any(Array));
expect(result.length).toEqual(0);
});
it('should parse the received issues', () => {
const result = parseLicenseReportMetrics(licenseHeadIssues, licenseBaseIssues);
expect(result[0].name).toBe(licenseHeadIssues.licenses[0].name);
expect(result[0].url).toBe(licenseHeadIssues.dependencies[0].license.url);
});
it('should omit issues from base report', () => {
const knownLicenseName = licenseBaseIssues.licenses[0].name;
const result = parseLicenseReportMetrics(licenseHeadIssues, licenseBaseIssues);
expect(result.length).toBe(licenseHeadIssues.licenses.length - 1);
expect(result[0].packages.length).toBe(licenseHeadIssues.dependencies.length - 1);
result.forEach(license => {
expect(license.name).not.toBe(knownLicenseName);
});
});
it('should enrich the report with information from managed licenses report', () => {
const result = parseLicenseReportMetrics(licenseHeadIssues, {}, [
approvedLicense,
blacklistedLicense,
]);
expect(result.length).toBe(2);
expect(result[0].approvalStatus).toBe(approvedLicense.approvalStatus);
expect(result[0].id).toBe(approvedLicense.id);
expect(result[1].approvalStatus).toBe(blacklistedLicense.approvalStatus);
expect(result[1].id).toBe(blacklistedLicense.id);
});
});
describe('byLicenseNameComparator', () => {
it('should Array sorted by of licenses by name', () => {
const licenses = [
{ name: 'MIT' },
{ name: 'New BSD' },
{ name: 'BSD-3-Clause' },
{ name: null },
];
const result = licenses.sort(byLicenseNameComparator).map(({ name }) => name);
expect(result).toEqual([null, 'BSD-3-Clause', 'MIT', 'New BSD']);
});
});
describe('normalizeLicense', () => {
it('should convert `approval_status` to `approvalStatus`', () => {
const src = { name: 'Foo', approval_status: 'approved', id: 3 };
const result = normalizeLicense(src);
expect(result.approvalStatus).toBe(src.approval_status);
expect(result.approval_status).toBe(undefined);
expect(result.name).toBe(src.name);
expect(result.id).toBe(src.id);
});
});
describe('getPackagesString', () => {
const examplePackages = licenseReport[0].packages;
it('returns string containing name of package when packages contains only one item', () => {
expect(getPackagesString(examplePackages.slice(0, 1), true, 3)).toBe('Used by pg');
});
it('returns string with comma separated names of packages up to 3 when `truncate` param is true and packages count exceeds `displayPackageCount`', () => {
expect(getPackagesString(examplePackages, true, 3)).toBe('Used by pg, puma, foo, and ');
});
it('returns string with comma separated names of all the packages when `truncate` param is true and packages count does NOT exceed `displayPackageCount`', () => {
expect(getPackagesString(examplePackages.slice(0, 3), true, 3)).toBe(
'Used by pg, puma, and foo',
);
});
it('returns string with comma separated names of all the packages when `truncate` param is false irrespective of packages count', () => {
expect(getPackagesString(examplePackages, false, 3)).toBe(
'Used by pg, puma, foo, bar, and baz',
);
});
});
describe('getIssueStatusFromLicenseStatus', () => {
it('returns SUCCESS status for approved license status', () => {
expect(getIssueStatusFromLicenseStatus(LICENSE_APPROVAL_STATUS.APPROVED)).toBe(
STATUS_SUCCESS,
);
});
it('returns FAILED status for blacklisted licensens', () => {
expect(getIssueStatusFromLicenseStatus(LICENSE_APPROVAL_STATUS.BLACKLISTED)).toBe(
STATUS_FAILED,
);
});
it('returns NEUTRAL status for undefined', () => {
expect(getIssueStatusFromLicenseStatus()).toBe(STATUS_NEUTRAL);
});
});
});
......@@ -69,11 +69,6 @@ msgid_plural "%d metrics"
msgstr[0] ""
msgstr[1] ""
msgid "%d new license"
msgid_plural "%d new licenses"
msgstr[0] ""
msgstr[1] ""
msgid "%d staged change"
msgid_plural "%d staged changes"
msgstr[0] ""
......@@ -3881,6 +3876,54 @@ msgstr ""
msgid "License"
msgstr ""
msgid "LicenseManagement|Approve license"
msgstr ""
msgid "LicenseManagement|Approve license?"
msgstr ""
msgid "LicenseManagement|Approved"
msgstr ""
msgid "LicenseManagement|Blacklist license"
msgstr ""
msgid "LicenseManagement|Blacklist license?"
msgstr ""
msgid "LicenseManagement|Blacklisted"
msgstr ""
msgid "LicenseManagement|License"
msgstr ""
msgid "LicenseManagement|License Management"
msgstr ""
msgid "LicenseManagement|License details"
msgstr ""
msgid "LicenseManagement|Manage approved and blacklisted licenses for this project."
msgstr ""
msgid "LicenseManagement|Packages"
msgstr ""
msgid "LicenseManagement|Remove license"
msgstr ""
msgid "LicenseManagement|Remove license?"
msgstr ""
msgid "LicenseManagement|URL"
msgstr ""
msgid "LicenseManagement|Unapproved"
msgstr ""
msgid "LicenseManagement|You are about to remove the license, %{name}, from this project."
msgstr ""
msgid "LinkedIn"
msgstr ""
......@@ -7291,12 +7334,6 @@ msgstr ""
msgid "ciReport|%{namespace} is affected by %{vulnerability}."
msgstr ""
msgid "ciReport|%{packagesString} and "
msgstr ""
msgid "ciReport|%{packagesString} and %{lastPackage}"
msgstr ""
msgid "ciReport|%{remainingPackagesCount} more"
msgstr ""
......@@ -7390,9 +7427,6 @@ msgstr ""
msgid "ciReport|Learn more about whitelisting"
msgstr ""
msgid "ciReport|License management detected %{licenseInfo}"
msgstr ""
msgid "ciReport|License management detected no new licenses"
msgstr ""
......@@ -7552,6 +7586,9 @@ msgstr ""
msgid "latest version"
msgstr ""
msgid "license management"
msgstr ""
msgid "locked by %{path_lock_user_name} %{created_at}"
msgstr ""
......
import Vue from 'vue';
import LicenseIssueBody from 'ee/vue_merge_request_widget/components/license_issue_body.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { licenseReport } from '../mock_data';
const licenseReportIssue = licenseReport[0];
const createComponent = (issue = licenseReportIssue) => {
const Component = Vue.extend(LicenseIssueBody);
return mountComponent(Component, { issue });
};
describe('LicenseIssueBody', () => {
let vm;
beforeEach(() => {
vm = createComponent();
});
afterEach(() => {
vm.$destroy();
});
describe('computed', () => {
describe('remainingPackages', () => {
it('returns string with count of issue.packages when it exceeds `displayPackageCount` prop', () => {
expect(vm.remainingPackages).toBe('2 more');
});
it('returns empty string when count of issue.packages does not exceed `displayPackageCount` prop', (done) => {
vm.displayPackageCount = licenseReportIssue.packages.length + 1;
Vue.nextTick()
.then(() => {
expect(vm.remainingPackages).toBe('');
})
.then(done)
.catch(done.fail);
});
});
});
describe('methods', () => {
describe('getPackagesString', () => {
it('returns string containing name of package when issue.packages contains only one item', (done) => {
vm.issue = Object.assign({}, licenseReportIssue, {
// We need only 3 elements as it is same as
// default value of `displayPackageCount`
// which is 3.
packages: licenseReportIssue.packages.slice(0, 1),
});
Vue.nextTick()
.then(() => {
expect(vm.getPackagesString(true)).toBe('pg');
})
.then(done)
.catch(done.fail);
});
it('returns string with comma separated names of packages up to 3 when `truncate` param is true and issue.packages count exceeds `displayPackageCount`', () => {
expect(vm.getPackagesString(true)).toBe('pg, puma, foo and ');
});
it('returns string with comma separated names of all the packages when `truncate` param is true and issue.packages count does NOT exceed `displayPackageCount`', (done) => {
vm.issue = Object.assign({}, licenseReportIssue, {
// We need only 3 elements as it is same as
// default value of `displayPackageCount`
// which is 3.
packages: licenseReportIssue.packages.slice(0, 3),
});
Vue.nextTick()
.then(() => {
expect(vm.getPackagesString(true)).toBe('pg, puma and foo');
})
.then(done)
.catch(done.fail);
});
it('returns string with comma separated names of all the packages when `truncate` param is false irrespective of issue.packages count', () => {
expect(vm.getPackagesString(false)).toBe('pg, puma, foo, bar and baz');
});
});
describe('handleShowPackages', () => {
it('sets value of `showAllPackages` prop to true', () => {
vm.showAllPackages = false;
vm.handleShowPackages();
expect(vm.showAllPackages).toBe(true);
});
});
});
describe('template', () => {
it('renders component container element with class `license-item`', () => {
expect(vm.$el.classList.contains('license-item')).toBe(true);
});
it('renders license link element', () => {
const linkEl = vm.$el.querySelector('a');
expect(linkEl).not.toBeNull();
expect(linkEl.getAttribute('href')).toBe(licenseReportIssue.url);
expect(linkEl.innerText.trim()).toBe(licenseReportIssue.name);
});
it('renders packages list for a particular license', () => {
const packagesEl = vm.$el.querySelector('.license-dependencies');
expect(packagesEl).not.toBeNull();
expect(packagesEl.innerText.trim()).toBe('pg, puma, foo and');
});
it('renders more packages button element', () => {
const buttonEl = vm.$el.querySelector('.btn-show-all-packages');
expect(buttonEl).not.toBeNull();
expect(buttonEl.innerText.trim()).toBe('2 more');
});
});
});
......@@ -5,15 +5,10 @@ import mrWidgetOptions from 'ee/vue_merge_request_widget/mr_widget_options.vue';
import MRWidgetService from 'ee/vue_merge_request_widget/services/mr_widget_service';
import MRWidgetStore from 'ee/vue_merge_request_widget/stores/mr_widget_store';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { TEST_HOST } from 'spec/test_constants';
import state from 'ee/vue_shared/security_reports/store/state';
import mockData, {
baseIssues,
headIssues,
basePerformance,
headPerformance,
licenseBaseIssues,
licenseHeadIssues,
} from './mock_data';
import mockData, { baseIssues, headIssues, basePerformance, headPerformance } from './mock_data';
import {
sastIssues,
......@@ -664,102 +659,39 @@ describe('ee merge request widget options', () => {
});
describe('license management report', () => {
beforeEach(() => {
const headPath = `${TEST_HOST}/head.json`;
const basePath = `${TEST_HOST}/base.json`;
const licenseManagementApiUrl = `${TEST_HOST}/manage_license_api`;
it('should be rendered if license management data is set', () => {
gl.mrWidgetData = {
...mockData,
license_management: {
head_path: 'head.json',
base_path: 'base.json',
head_path: headPath,
base_path: basePath,
managed_licenses_path: licenseManagementApiUrl,
can_manage_licenses: false,
},
};
Component.mr = new MRWidgetStore(gl.mrWidgetData);
Component.service = new MRWidgetService({});
});
describe('when it is loading', () => {
it('should render loading indicator', () => {
mock.onGet('head.json').reply(200, licenseHeadIssues);
mock.onGet('base.json').reply(200, licenseBaseIssues);
vm = mountComponent(Component);
expect(
removeBreakLine(vm.$el.querySelector('.js-license-report-widget').textContent),
).toContain('Loading license management report');
});
});
describe('with successful request', () => {
beforeEach(() => {
mock.onGet('head.json').reply(200, licenseHeadIssues);
mock.onGet('base.json').reply(200, licenseBaseIssues);
vm = mountComponent(Component);
});
it('should render report overview', done => {
setTimeout(() => {
expect(
removeBreakLine(
vm.$el.querySelector('.js-license-report-widget .js-code-text').textContent,
),
).toEqual('License management detected 1 new license');
done();
}, 0);
});
it('should render report issues list in section body', done => {
setTimeout(() => {
const sectionBodyEl = vm.$el.querySelector(
'.js-license-report-widget .js-report-section-container',
);
expect(sectionBodyEl).not.toBeNull();
expect(sectionBodyEl.querySelectorAll('li.report-block-list-issue').length).toBe(
licenseHeadIssues.licenses.length - 1,
);
done();
}, 0);
});
});
describe('with empty successful request', () => {
beforeEach(() => {
mock.onGet('head.json').reply(200, licenseBaseIssues);
mock.onGet('base.json').reply(200, licenseBaseIssues);
vm = mountComponent(Component);
});
afterEach(() => {
mock.restore();
expect(vm.$el.querySelector('.license-report-widget')).not.toBeNull();
});
it('should render report overview', done => {
setTimeout(() => {
expect(
removeBreakLine(
vm.$el.querySelector('.js-license-report-widget .js-code-text').textContent,
),
).toEqual('License management detected no new licenses');
done();
}, 0);
});
});
it('should not be rendered if license management data is not set', () => {
gl.mrWidgetData = {
...mockData,
license_management: {},
};
describe('with failed request', () => {
beforeEach(() => {
mock.onGet('head.json').reply(500, {});
mock.onGet('base.json').reply(500, {});
Component.mr = new MRWidgetStore(gl.mrWidgetData);
Component.service = new MRWidgetService({});
vm = mountComponent(Component);
});
it('should render error indicator', done => {
setTimeout(() => {
expect(
removeBreakLine(
vm.$el.querySelector('.js-license-report-widget .js-code-text').textContent,
),
).toContain('Failed to load license management report');
done();
}, 0);
});
expect(vm.$el.querySelector('.license-report-widget')).toBeNull();
});
});
......
......@@ -400,141 +400,3 @@ export const codequalityParsedIssues = [
urlPath: 'foo/Gemfile.lock',
},
];
export const licenseBaseIssues = {
licenses: [
{
count: 1,
name: 'MIT',
},
],
dependencies: [
{
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',
pathes: [
'.',
],
},
},
],
};
export const licenseHeadIssues = {
licenses: [
{
count: 3,
name: 'New BSD',
},
{
count: 1,
name: 'MIT',
},
],
dependencies: [
{
license: {
name: 'New BSD',
url: 'http://opensource.org/licenses/BSD-3-Clause',
},
dependency: {
name: 'pg',
url: 'https://bitbucket.org/ged/ruby-pg',
description:
'Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/]',
pathes: ['.'],
},
},
{
license: {
name: 'New BSD',
url: 'http://opensource.org/licenses/BSD-3-Clause',
},
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',
pathes: ['.'],
},
},
{
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',
pathes: ['.'],
},
},
{
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',
pathes: [
'.',
],
},
},
],
};
export const licenseReport = [
{
name: 'New BSD',
count: 5,
url: 'http://opensource.org/licenses/BSD-3-Clause',
packages: [
{
name: 'pg',
url: 'https://bitbucket.org/ged/ruby-pg',
description:
'Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/]',
pathes: ['.'],
},
{
name: 'puma',
url: 'http://puma.io',
description:
'Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications',
pathes: ['.'],
},
{
name: 'foo',
url: 'https://bitbucket.org/ged/ruby-pg',
description:
'Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/]',
pathes: ['.'],
},
{
name: 'bar',
url: 'http://puma.io',
description:
'Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications',
pathes: ['.'],
},
{
name: 'baz',
url: 'https://bitbucket.org/ged/ruby-pg',
description:
'Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/]',
pathes: ['.'],
},
],
},
];
......@@ -367,7 +367,7 @@ describe('mrWidgetOptions', () => {
vm.mr.relatedLinks = {
assignToMe: null,
closing: `
<a class="close-related-link" href="#'>
<a class="close-related-link" href="#">
Close
</a>
`,
......
import MergeRequestStore from 'ee/vue_merge_request_widget/stores/mr_widget_store';
import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import mockData, {
headIssues,
baseIssues,
parsedBaseIssues,
parsedHeadIssues,
licenseBaseIssues,
licenseHeadIssues,
} from '../mock_data';
import mockData, { headIssues, baseIssues, parsedBaseIssues, parsedHeadIssues } from '../mock_data';
describe('MergeRequestStore', () => {
let store;
......@@ -97,26 +90,6 @@ describe('MergeRequestStore', () => {
});
});
describe('parseLicenseReportMetrics', () => {
it('should parse the received issues', () => {
store.parseLicenseReportMetrics(licenseHeadIssues, licenseBaseIssues);
expect(store.licenseReport[0].name).toBe(licenseHeadIssues.licenses[0].name);
expect(store.licenseReport[0].url).toBe(licenseHeadIssues.dependencies[0].license.url);
});
it('should ommit issues from base report', () => {
const knownLicenseName = licenseBaseIssues.licenses[0].name;
store.parseLicenseReportMetrics(licenseHeadIssues, licenseBaseIssues);
expect(store.licenseReport.length).toBe(licenseHeadIssues.licenses.length - 1);
expect(store.licenseReport[0].packages.length).toBe(
licenseHeadIssues.dependencies.length - 1,
);
store.licenseReport.forEach(license => {
expect(license.name).not.toBe(knownLicenseName);
});
});
});
describe('isNothingToMergeState', () => {
it('returns true when nothingToMerge', () => {
store.state = stateKey.nothingToMerge;
......
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