Commit 331c5d85 authored by Phil Hughes's avatar Phil Hughes

Merge branch '37999-spdx-dropdown' into 'master'

Pass SPDX licenses from backend to frontend

Closes #37999

See merge request gitlab-org/gitlab!36926
parents 71dd8e65 a5d2c222
......@@ -17,6 +17,7 @@ export default () => {
settingsPath,
approvalsDocumentationPath,
lockedApprovalsRuleName,
softwareLicenses,
} = el.dataset;
const storeSettings = {
......@@ -33,6 +34,8 @@ export default () => {
store.dispatch('licenseManagement/setAPISettings', {
apiUrlManageLicenses: readLicensePoliciesEndpoint,
});
store.dispatch('licenseManagement/setKnownLicenses', JSON.parse(softwareLicenses));
store.dispatch(`${LICENSE_LIST}/setLicensesEndpoint`, projectLicensesEndpoint);
return new Vue({
......
......@@ -33,6 +33,10 @@ export default {
required: false,
default: () => [],
},
knownLicenses: {
type: Array,
required: true,
},
loading: {
type: Boolean,
required: false,
......@@ -78,6 +82,7 @@ export default {
<add-license-form-dropdown
id="js-license-dropdown"
v-model="licenseName"
:known-licenses="knownLicenses"
:placeholder="s__('LicenseCompliance|License name')"
/>
<div class="invalid-feedback" :class="{ 'd-block': isInvalidLicense }">
......
......@@ -2,7 +2,6 @@
/* eslint-disable no-unused-vars */
import $ from 'jquery';
import select2 from 'select2/select2';
import { KNOWN_LICENSES } from '../constants';
export default {
name: 'AddLicenseFormDropdown',
......@@ -17,6 +16,10 @@ export default {
required: false,
default: '',
},
knownLicenses: {
type: Array,
required: true,
},
},
mounted() {
$(this.$refs.dropdownInput)
......@@ -26,7 +29,10 @@ export default {
placeholder: this.placeholder,
createSearchChoice: term => ({ id: term, text: term }),
createSearchChoicePosition: 'bottom',
data: KNOWN_LICENSES.map(license => ({ id: license, text: license })),
data: this.knownLicenses.map(license => ({
id: license,
text: license,
})),
})
.on('change', e => {
this.$emit('input', e.target.value);
......
......@@ -24,36 +24,6 @@ export const LICENSE_APPROVAL_ACTION = {
DENY: 'deny',
};
/* eslint-disable @gitlab/require-i18n-strings */
export const KNOWN_LICENSES = [
'AGPL-1.0',
'AGPL-3.0',
'Apache 2.0',
'Artistic-2.0',
'BSD',
'CC0 1.0 Universal',
'CDDL-1.0',
'CDDL-1.1',
'EPL-1.0',
'EPL-2.0',
'GPLv2',
'GPLv3',
'ISC',
'LGPL',
'LGPL-2.1',
'MIT',
'Mozilla Public License 2.0',
'MS-PL',
'MS-RL',
'New BSD',
'Python Software Foundation License',
'ruby',
'Simplified BSD',
'WTFPL',
'Zlib',
];
/* eslint-enable @gitlab/require-i18n-strings */
export const REPORT_GROUPS = [
{
name: s__('LicenseManagement|Denied'),
......
......@@ -32,7 +32,12 @@ export default {
};
},
computed: {
...mapState(LICENSE_MANAGEMENT, ['managedLicenses', 'isLoadingManagedLicenses', 'isAdmin']),
...mapState(LICENSE_MANAGEMENT, [
'managedLicenses',
'isLoadingManagedLicenses',
'isAdmin',
'knownLicenses',
]),
...mapGetters(LICENSE_MANAGEMENT, [
'isLicenseBeingUpdated',
'hasPendingLicenses',
......@@ -143,6 +148,7 @@ export default {
<div v-if="formIsOpen" class="gl-mt-3 gl-mb-3">
<add-license-form
:managed-licenses="managedLicenses"
:known-licenses="knownLicenses"
:loading="isAddingNewLicense"
@addLicense="setLicenseApproval"
@closeForm="closeAddLicenseForm"
......
......@@ -12,6 +12,15 @@ export const setAPISettings = ({ commit }, data) => {
export const setLicenseInModal = ({ commit }, license) => {
commit(types.SET_LICENSE_IN_MODAL, license);
};
export const setIsAdmin = ({ commit }, payload) => {
commit(types.SET_IS_ADMIN, payload);
};
export const setKnownLicenses = ({ commit }, licenses) => {
commit(types.SET_KNOWN_LICENSES, licenses);
};
export const resetLicenseInModal = ({ commit }) => {
commit(types.RESET_LICENSE_IN_MODAL);
};
......@@ -153,10 +162,6 @@ export const receiveLicenseCheckApprovalRuleError = ({ commit }, error) => {
commit(types.RECEIVE_LICENSE_CHECK_APPROVAL_RULE_ERROR, error);
};
export const setIsAdmin = ({ commit }, payload) => {
commit(types.SET_IS_ADMIN, payload);
};
export const addPendingLicense = ({ state, commit }, id = null) => {
if (!state.pendingLicenses.includes(id)) {
commit(types.ADD_PENDING_LICENSE, id);
......
......@@ -13,6 +13,7 @@ export const REQUEST_SET_LICENSE_APPROVAL = 'REQUEST_SET_LICENSE_APPROVAL';
export const RESET_LICENSE_IN_MODAL = 'RESET_LICENSE_IN_MODAL';
export const SET_API_SETTINGS = 'SET_API_SETTINGS';
export const SET_LICENSE_IN_MODAL = 'SET_LICENSE_IN_MODAL';
export const SET_KNOWN_LICENSES = 'SET_KNOWN_LICENSES';
export const SET_IS_ADMIN = 'SET_IS_ADMIN';
export const ADD_PENDING_LICENSE = 'ADD_PENDING_LICENSE';
export const REMOVE_PENDING_LICENSE = 'REMOVE_PENDING_LICENSE';
......
......@@ -20,6 +20,11 @@ export default {
isAdmin: data,
});
},
[types.SET_KNOWN_LICENSES](state, data) {
Object.assign(state, {
knownLicenses: data,
});
},
[types.RECEIVE_MANAGED_LICENSES_SUCCESS](state, licenses = []) {
const managedLicenses = licenses.map(normalizeLicense).reverse();
......
......@@ -17,4 +17,5 @@ export default () => ({
existingLicenses: [],
hasLicenseCheckApprovalRule: false,
isLoadingLicenseCheckApprovalRule: false,
knownLicenses: [],
});
---
title: Show up to date SPDX licenses for license compliance
merge_request: 36926
author:
type: added
import Vue from 'vue';
import $ from 'jquery';
import Dropdown from 'ee/vue_shared/license_compliance/components/add_license_form_dropdown.vue';
import { KNOWN_LICENSES } from 'ee/vue_shared/license_compliance/constants';
import mountComponent from 'helpers/vue_mount_component_helper';
import { shallowMount } from '@vue/test-utils';
describe('AddLicenseFormDropdown', () => {
const Component = Vue.extend(Dropdown);
let vm;
let vm;
let wrapper;
const KNOWN_LICENSES = ['AGPL-1.0', 'AGPL-3.0', 'Apache 2.0', 'BSD'];
const createComponent = (props = {}) => {
wrapper = shallowMount(Dropdown, { propsData: { knownLicenses: KNOWN_LICENSES, ...props } });
vm = wrapper.vm;
};
describe('AddLicenseFormDropdown', () => {
afterEach(() => {
vm.$destroy();
vm = undefined;
wrapper.destroy();
});
it('emits `input` invent on change', () => {
vm = mountComponent(Component);
createComponent();
jest.spyOn(vm, '$emit').mockImplementation(() => {});
$(vm.$el)
......@@ -25,7 +32,7 @@ describe('AddLicenseFormDropdown', () => {
it('sets the placeholder appropriately', () => {
const placeholder = 'Select a license';
vm = mountComponent(Component, { placeholder });
createComponent({ placeholder });
const dropdownContainer = $(vm.$el).select2('container')[0];
......@@ -34,13 +41,13 @@ describe('AddLicenseFormDropdown', () => {
it('sets the initial value correctly', () => {
const value = 'AWESOME_LICENSE';
vm = mountComponent(Component, { value });
createComponent({ value });
expect(vm.$el.value).toContain(value);
});
it('shows all pre-defined licenses', done => {
vm = mountComponent(Component);
it('shows all defined licenses', done => {
createComponent();
const element = $(vm.$el);
......
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import { shallowMount, mount } from '@vue/test-utils';
import LicenseIssueBody from 'ee/vue_shared/license_compliance/components/add_license_form.vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import { LICENSE_APPROVAL_STATUS } from 'ee/vue_shared/license_compliance/constants';
describe('AddLicenseForm', () => {
const Component = Vue.extend(LicenseIssueBody);
let vm;
const KNOWN_LICENSES = [{ name: 'BSD' }, { name: 'Apache' }];
let wrapper;
let vm;
const findSubmitButton = () => vm.$el.querySelector('.js-submit');
const findCancelButton = () => vm.$el.querySelector('.js-cancel');
const createComponent = (props = {}, mountFn = shallowMount) => {
wrapper = mountFn(LicenseIssueBody, { propsData: { knownLicenses: KNOWN_LICENSES, ...props } });
vm = wrapper.vm;
};
describe('AddLicenseForm', () => {
const findSubmitButton = () => wrapper.find('.js-submit');
const findCancelButton = () => wrapper.find('.js-cancel');
beforeEach(() => {
vm = mountComponent(Component);
createComponent();
});
afterEach(() => {
vm.$destroy();
vm = undefined;
wrapper.destroy();
});
describe('interaction', () => {
it('clicking the Submit button submits the data and closes the form', done => {
it('clicking the Submit button submits the data and closes the form', async () => {
const name = 'LICENSE_TEST';
createComponent({}, mount);
jest.spyOn(vm, '$emit').mockImplementation(() => {});
vm.approvalStatus = LICENSE_APPROVAL_STATUS.ALLOWED;
vm.licenseName = name;
wrapper.setData({ approvalStatus: LICENSE_APPROVAL_STATUS.ALLOWED, licenseName: name });
Vue.nextTick(() => {
const linkEl = findSubmitButton();
linkEl.click();
await Vue.nextTick();
expect(vm.$emit).toHaveBeenCalledWith('addLicense', {
newStatus: LICENSE_APPROVAL_STATUS.ALLOWED,
license: { name },
});
const linkEl = findSubmitButton();
linkEl.trigger('click');
done();
expect(vm.$emit).toHaveBeenCalledWith('addLicense', {
newStatus: LICENSE_APPROVAL_STATUS.ALLOWED,
license: { name },
});
});
it('clicking the Cancel button closes the form', () => {
createComponent({}, mount);
const linkEl = findCancelButton();
jest.spyOn(vm, '$emit').mockImplementation(() => {});
linkEl.click();
linkEl.trigger('click');
expect(vm.$emit).toHaveBeenCalledWith('closeForm');
});
......@@ -51,23 +58,20 @@ describe('AddLicenseForm', () => {
describe('computed', () => {
describe('submitDisabled', () => {
it('is true if the approvalStatus is empty', () => {
vm.licenseName = 'FOO';
vm.approvalStatus = '';
wrapper.setData({ licenseName: 'FOO', approvalStatus: '' });
expect(vm.submitDisabled).toBe(true);
});
it('is true if the licenseName is empty', () => {
vm.licenseName = '';
vm.approvalStatus = LICENSE_APPROVAL_STATUS.ALLOWED;
wrapper.setData({ licenseName: '', approvalStatus: LICENSE_APPROVAL_STATUS.ALLOWED });
expect(vm.submitDisabled).toBe(true);
});
it('is true if the entered license is duplicated', () => {
vm = mountComponent(Component, { managedLicenses: [{ name: 'FOO' }] });
vm.licenseName = 'FOO';
vm.approvalStatus = LICENSE_APPROVAL_STATUS.ALLOWED;
createComponent({ managedLicenses: [{ name: 'FOO' }] });
wrapper.setData({ licenseName: 'FOO', approvalStatus: LICENSE_APPROVAL_STATUS.ALLOWED });
expect(vm.submitDisabled).toBe(true);
});
......@@ -75,15 +79,15 @@ describe('AddLicenseForm', () => {
describe('isInvalidLicense', () => {
it('is true if the entered license is duplicated', () => {
vm = mountComponent(Component, { managedLicenses: [{ name: 'FOO' }] });
vm.licenseName = 'FOO';
createComponent({ managedLicenses: [{ name: 'FOO' }] });
wrapper.setData({ licenseName: 'FOO' });
expect(vm.isInvalidLicense).toBe(true);
});
it('is false if the entered license is unique', () => {
vm = mountComponent(Component, { managedLicenses: [{ name: 'FOO' }] });
vm.licenseName = 'FOO2';
createComponent({ managedLicenses: [{ name: 'FOO' }] });
wrapper.setData({ licenseName: 'FOO2' });
expect(vm.isInvalidLicense).toBe(false);
});
......@@ -92,98 +96,99 @@ describe('AddLicenseForm', () => {
describe('template', () => {
it('renders the license select dropdown', () => {
const dropdownElement = vm.$el.querySelector('#js-license-dropdown');
const dropdownElement = wrapper.find('#js-license-dropdown');
expect(dropdownElement).not.toBeNull();
expect(dropdownElement.exists()).toBe(true);
});
it('renders the license approval radio buttons dropdown', () => {
const radioButtonParents = vm.$el.querySelectorAll('.form-check');
const radioButtonParents = wrapper.findAll('.form-check');
expect(radioButtonParents).toHaveLength(2);
expect(radioButtonParents[0].innerText.trim()).toBe('Allow');
expect(radioButtonParents[0].querySelector('.form-check-input')).not.toBeNull();
expect(radioButtonParents[1].innerText.trim()).toBe('Deny');
expect(radioButtonParents[1].querySelector('.form-check-input')).not.toBeNull();
expect(radioButtonParents.at(0).text()).toBe('Allow');
expect(
radioButtonParents
.at(0)
.find('.form-check-input')
.exists(),
).toBe(true);
expect(radioButtonParents.at(1).text()).toBe('Deny');
expect(
radioButtonParents
.at(1)
.find('.form-check-input')
.exists(),
).toBe(true);
});
it('renders error text, if there is a duplicate license', done => {
vm = mountComponent(Component, { managedLicenses: [{ name: 'FOO' }] });
vm.licenseName = 'FOO';
Vue.nextTick(() => {
const feedbackElement = vm.$el.querySelector('.invalid-feedback');
expect(feedbackElement).not.toBeNull();
expect(feedbackElement.classList).toContain('d-block');
expect(feedbackElement.innerText.trim()).toBe(
'This license already exists in this project.',
);
done();
});
it('renders error text, if there is a duplicate license', async () => {
createComponent({ managedLicenses: [{ name: 'FOO' }] });
wrapper.setData({ licenseName: 'FOO' });
await Vue.nextTick();
const feedbackElement = wrapper.find('.invalid-feedback');
expect(feedbackElement.exists()).toBe(true);
expect(feedbackElement.classes()).toContain('d-block');
expect(feedbackElement.text()).toBe('This license already exists in this project.');
});
it('shows radio button descriptions, if licenseComplianceDeniesMr feature flag is enabled', done => {
const wrapper = shallowMount(LicenseIssueBody, {
it('shows radio button descriptions, if licenseComplianceDeniesMr feature flag is enabled', async () => {
wrapper = shallowMount(LicenseIssueBody, {
propsData: {
managedLicenses: [{ name: 'FOO' }],
knownLicenses: KNOWN_LICENSES,
},
provide: {
glFeatures: { licenseComplianceDeniesMr: true },
},
});
Vue.nextTick(() => {
const descriptionElement = wrapper.findAll('.text-secondary');
await Vue.nextTick();
expect(descriptionElement.at(0).text()).toBe(
'Acceptable license to be used in the project',
);
const descriptionElement = wrapper.findAll('.text-secondary');
expect(descriptionElement.at(1).text()).toBe(
'Disallow merge request if detected and will instruct developer to remove',
);
expect(descriptionElement.at(0).text()).toBe('Acceptable license to be used in the project');
done();
});
expect(descriptionElement.at(1).text()).toBe(
'Disallow merge request if detected and will instruct developer to remove',
);
});
it('does not show radio button descriptions, if licenseComplianceDeniesMr feature flag is disabled', done => {
vm = mountComponent(Component, { managedLicenses: [{ name: 'FOO' }] });
vm.licenseName = 'FOO';
Vue.nextTick(() => {
const formCheckElements = vm.$el.querySelectorAll('.form-check');
it('does not show radio button descriptions, if licenseComplianceDeniesMr feature flag is disabled', () => {
createComponent({ managedLicenses: [{ name: 'FOO' }] });
wrapper.setData({ licenseName: 'FOO' });
return Vue.nextTick().then(() => {
const formCheckElements = wrapper.findAll('.form-check');
expect(formCheckElements[0]).toMatchSnapshot();
expect(formCheckElements[1]).toMatchSnapshot();
done();
expect(formCheckElements.at(0).element).toMatchSnapshot();
expect(formCheckElements.at(1).element).toMatchSnapshot();
});
});
it('disables submit, if the form is invalid', done => {
vm.licenseName = '';
Vue.nextTick(() => {
expect(vm.submitDisabled).toBe(true);
it('disables submit, if the form is invalid', async () => {
wrapper.setData({ licenseName: '' });
await Vue.nextTick();
const submitButton = findSubmitButton();
expect(vm.submitDisabled).toBe(true);
expect(submitButton).not.toBeNull();
expect(submitButton.disabled).toBe(true);
done();
});
const submitButton = findSubmitButton();
expect(submitButton.exists()).toBe(true);
expect(submitButton.props().disabled).toBe(true);
});
it('disables submit and cancel while a new license is being added', done => {
vm.loading = true;
Vue.nextTick(() => {
const submitButton = findSubmitButton();
const cancelButton = findCancelButton();
expect(submitButton).not.toBeNull();
expect(submitButton.disabled).toBe(true);
expect(cancelButton).not.toBeNull();
expect(cancelButton.disabled).toBe(true);
done();
});
it('disables submit and cancel while a new license is being added', async () => {
wrapper.setProps({ loading: true });
await Vue.nextTick();
const submitButton = findSubmitButton();
const cancelButton = findCancelButton();
expect(submitButton.exists()).toBe(true);
expect(submitButton.props().disabled).toBe(true);
expect(cancelButton.exists()).toBe(true);
expect(cancelButton.props().disabled).toBe(true);
});
});
});
......@@ -47,6 +47,7 @@ const createComponent = ({ state, getters, props, actionMocks, isAdmin, options,
managedLicenses,
isLoadingManagedLicenses: true,
isAdmin,
knownLicenses: [],
...state,
},
actions: {
......
......@@ -60,6 +60,21 @@ describe('License store actions', () => {
});
});
describe('setKnownLicenses', () => {
it('commits SET_KNOWN_LICENSES', done => {
const payload = [{ name: 'BSD' }, { name: 'Apache' }];
testAction(
actions.setKnownLicenses,
payload,
state,
[{ type: mutationTypes.SET_KNOWN_LICENSES, payload }],
[],
)
.then(done)
.catch(done.fail);
});
});
describe('setLicenseInModal', () => {
it('commits SET_LICENSE_IN_MODAL with license', done => {
testAction(
......
......@@ -20,6 +20,15 @@ describe('License store mutations', () => {
});
});
describe('SET_KNOWN_LICENSES', () => {
it('assigns knownLicenses to the store', () => {
const licenses = [{ name: 'BSD' }, { name: 'Apache' }];
store.commit(`licenseManagement/${types.SET_KNOWN_LICENSES}`, licenses);
expect(store.state.licenseManagement.knownLicenses).toBe(licenses);
});
});
describe('RESET_LICENSE_IN_MODAL', () => {
it('closes modal and deletes licenseInApproval', () => {
store.replaceState({
......
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