Commit a7948458 authored by Fernando's avatar Fernando

Implement Policy tab for License Compliance

* Add license_policy_list feature flag

When feature flag is enabled, enable the following:

*Detected in project & policies tab in license compliance
* Policy tab has admin and non admin versions of policy table
* Admin policy tab mirrors Settings->CI/CD-> Licence Comp.
UI
* Refactor license policy admin to VueX module so it can
be resued
* Add JS unit tests
* Update QA selectors for existing Settings->CI/CD
License Compliance automation
parent 90fd52b0
import Vue from 'vue';
import Dashboard from 'ee/vue_shared/license_management/license_management.vue';
import LicenseManagement from 'ee/vue_shared/license_management/license_management.vue';
import createStore from 'ee/vue_shared/license_management/store/index';
import ProtectedEnvironmentCreate from 'ee/protected_environments/protected_environment_create';
import ProtectedEnvironmentEditList from 'ee/protected_environments/protected_environment_edit_list';
import showToast from '~/vue_shared/plugins/global_toast';
......@@ -10,11 +11,14 @@ document.addEventListener('DOMContentLoaded', () => {
const toasts = document.querySelectorAll('.js-toast-message');
if (el && el.dataset && el.dataset.apiUrl) {
const store = createStore();
store.dispatch('licenseManagement/setIsAdmin', Boolean(el.dataset.apiUrl));
// eslint-disable-next-line no-new
new Vue({
el,
store,
render(createElement) {
return createElement(Dashboard, {
return createElement(LicenseManagement, {
props: {
...el.dataset,
},
......
<script>
import { mapActions, mapState, mapGetters } from 'vuex';
import { GlEmptyState, GlLoadingIcon, GlLink, GlIcon } from '@gitlab/ui';
import { GlEmptyState, GlLoadingIcon, GlLink, GlIcon, GlTab, GlTabs, GlBadge } from '@gitlab/ui';
import { LICENSE_LIST } from '../store/constants';
import { LICENSE_MANAGEMENT } from 'ee/vue_shared/license_management/store/constants';
import PaginatedLicensesTable from './paginated_licenses_table.vue';
import PipelineInfo from './pipeline_info.vue';
import LicenseManagement from 'ee/vue_shared/license_management/license_management.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'ProjectLicensesApp',
......@@ -14,7 +17,12 @@ export default {
PaginatedLicensesTable,
PipelineInfo,
GlIcon,
GlTab,
GlTabs,
GlBadge,
LicenseManagement,
},
mixins: [glFeatureFlagsMixin()],
props: {
emptyStateSvgPath: {
type: String,
......@@ -24,13 +32,35 @@ export default {
type: String,
required: true,
},
readLicensePoliciesEndpoint: {
type: String,
required: true,
},
},
data() {
return {
tabIndex: 0,
};
},
computed: {
...mapState(LICENSE_LIST, ['initialized', 'reportInfo']),
...mapState(LICENSE_LIST, ['initialized', 'licenses', 'reportInfo', 'listTypes']),
...mapState(LICENSE_MANAGEMENT, ['managedLicenses']),
...mapGetters(LICENSE_LIST, ['isJobSetUp', 'isJobFailed']),
hasEmptyState() {
return Boolean(!this.isJobSetUp || this.isJobFailed);
},
hasLicensePolicyList() {
return Boolean(this.glFeatures.licensePolicyList);
},
licenseCount() {
return this.licenses.length;
},
policyCount() {
return this.managedLicenses.length;
},
isDetectedProjectTab() {
return this.tabIndex === 0;
},
},
created() {
this.fetchLicenses();
......@@ -65,7 +95,38 @@ export default {
</gl-link>
</h2>
<pipeline-info :path="reportInfo.jobPath" :timestamp="reportInfo.generatedAt" />
<paginated-licenses-table class="mt-3" />
<pipeline-info
v-if="isDetectedProjectTab"
:path="reportInfo.jobPath"
:timestamp="reportInfo.generatedAt"
/>
<template v-else>{{ s__('Licenses|Specified policies in this project') }}</template>
<!-- TODO: Remove feature flag -->
<template v-if="hasLicensePolicyList">
<gl-tabs v-model="tabIndex" content-class="pt-0">
<gl-tab>
<template #title>
{{ s__('Licenses|Detected in Project') }}
<gl-badge pill>{{ licenseCount }}</gl-badge>
</template>
<paginated-licenses-table />
</gl-tab>
<gl-tab>
<template #title>
{{ s__('Licenses|Policies') }}
<gl-badge pill>{{ policyCount }}</gl-badge>
</template>
<license-management :api-url="readLicensePoliciesEndpoint" />
</gl-tab>
</gl-tabs>
</template>
<template v-else>
<paginated-licenses-table class="mt-3" />
</template>
</div>
</template>
......@@ -5,9 +5,16 @@ import { LICENSE_LIST } from './store/constants';
export default () => {
const el = document.querySelector('#js-licenses-app');
const { endpoint, emptyStateSvgPath, documentationPath } = el.dataset;
const {
projectLicensesEndpoint,
emptyStateSvgPath,
documentationPath,
readLicensePoliciesEndpoint,
writeLicensePoliciesEndpoint,
} = el.dataset;
const store = createStore();
store.dispatch(`${LICENSE_LIST}/setLicensesEndpoint`, endpoint);
store.dispatch('licenseManagement/setIsAdmin', Boolean(writeLicensePoliciesEndpoint));
store.dispatch(`${LICENSE_LIST}/setLicensesEndpoint`, projectLicensesEndpoint);
return new Vue({
el,
......@@ -20,6 +27,7 @@ export default () => {
props: {
emptyStateSvgPath,
documentationPath,
readLicensePoliciesEndpoint,
},
});
},
......
......@@ -2,7 +2,9 @@ import Vue from 'vue';
import Vuex from 'vuex';
import listModule from './modules/list';
import { licenseManagementModule } from 'ee/vue_shared/license_management/store/index';
import { LICENSE_LIST } from './constants';
import { LICENSE_MANAGEMENT } from 'ee/vue_shared/license_management/store/constants';
Vue.use(Vuex);
......@@ -10,5 +12,6 @@ export default () =>
new Vuex.Store({
modules: {
[LICENSE_LIST]: listModule(),
[LICENSE_MANAGEMENT]: licenseManagementModule(),
},
});
......@@ -12,8 +12,8 @@ export default {
},
LICENSE_APPROVAL_STATUS,
approvalStatusOptions: [
{ value: LICENSE_APPROVAL_STATUS.APPROVED, label: s__('LicenseCompliance|Approve') },
{ value: LICENSE_APPROVAL_STATUS.BLACKLISTED, label: s__('LicenseCompliance|Blacklist') },
{ value: LICENSE_APPROVAL_STATUS.APPROVED, label: s__('LicenseCompliance|Allow') },
{ value: LICENSE_APPROVAL_STATUS.BLACKLISTED, label: s__('LicenseCompliance|Deny') },
],
props: {
managedLicenses: {
......
<script>
import { mapActions } from 'vuex';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { getIssueStatusFromLicenseStatus } from 'ee/vue_shared/license_management/store/utils';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
import { LICENSE_APPROVAL_STATUS } from '../constants';
import { LICENSE_MANAGEMENT } from 'ee/vue_shared/license_management/store/constants';
const visibleClass = 'visible';
const invisibleClass = 'invisible';
export default {
name: 'AdminLicenseManagementRow',
components: {
GlDropdown,
GlDropdownItem,
Icon,
IssueStatusIcon,
},
props: {
license: {
type: Object,
required: true,
validator: license =>
Boolean(license.name) &&
Object.values(LICENSE_APPROVAL_STATUS).includes(license.approvalStatus),
},
},
LICENSE_APPROVAL_STATUS,
[LICENSE_APPROVAL_STATUS.APPROVED]: s__('LicenseCompliance|Allowed'),
[LICENSE_APPROVAL_STATUS.BLACKLISTED]: s__('LicenseCompliance|Denied'),
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(LICENSE_MANAGEMENT, ['setLicenseInModal', 'approveLicense', 'blacklistLicense']),
},
};
</script>
<template>
<div data-qa-selector="admin_license_compliance_row">
<issue-status-icon :status="status" class="float-left append-right-default" />
<span class="js-license-name" data-qa-selector="license_name_content">{{ license.name }}</span>
<div class="float-right">
<div class="d-flex">
<gl-dropdown
:text="dropdownText"
toggle-class="d-flex justify-content-between align-items-center"
right
>
<gl-dropdown-item @click="approveLicense(license)">
<icon :class="approveIconClass" name="mobile-issue-close" />
{{ $options[$options.LICENSE_APPROVAL_STATUS.APPROVED] }}
</gl-dropdown-item>
<gl-dropdown-item @click="blacklistLicense(license)">
<icon :class="blacklistIconClass" name="mobile-issue-close" />
{{ $options[$options.LICENSE_APPROVAL_STATUS.BLACKLISTED] }}
</gl-dropdown-item>
</gl-dropdown>
<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>
</div>
</template>
......@@ -4,11 +4,13 @@ import { mapActions, mapState } from 'vuex';
import { s__, sprintf } from '~/locale';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { LICENSE_MANAGEMENT } from 'ee/vue_shared/license_management/store/constants';
export default {
name: 'LicenseDeleteConfirmationModal',
components: { GlModal: DeprecatedModal2 },
computed: {
...mapState(['currentLicenseInModal']),
...mapState(LICENSE_MANAGEMENT, ['currentLicenseInModal']),
confirmationText() {
const name = `<strong>${_.escape(this.currentLicenseInModal.name)}</strong>`;
......@@ -20,7 +22,7 @@ export default {
},
},
methods: {
...mapActions(['resetLicenseInModal', 'deleteLicense']),
...mapActions(LICENSE_MANAGEMENT, ['resetLicenseInModal', 'deleteLicense']),
},
};
</script>
......
......@@ -2,6 +2,7 @@
import { mapActions } from 'vuex';
import LicensePackages from './license_packages.vue';
import { LICENSE_MANAGEMENT } from 'ee/vue_shared/license_management/store/constants';
export default {
name: 'LicenseIssueBody',
......@@ -12,7 +13,7 @@ export default {
required: true,
},
},
methods: { ...mapActions(['setLicenseInModal']) },
methods: { ...mapActions(LICENSE_MANAGEMENT, ['setLicenseInModal']) },
};
</script>
......
<script>
import { mapActions } from 'vuex';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { getIssueStatusFromLicenseStatus } from 'ee/vue_shared/license_management/store/utils';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import {
getIssueStatusFromLicenseStatus,
getStatusTranslationsFromLicenseStatus,
} from 'ee/vue_shared/license_management/store/utils';
import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
import { LICENSE_APPROVAL_STATUS } from '../constants';
const visibleClass = 'visible';
const invisibleClass = 'invisible';
export default {
name: 'LicenseManagementRow',
components: {
GlDropdown,
GlDropdownItem,
Icon,
IssueStatusIcon,
},
props: {
license: {
type: Object,
required: true,
validator: license =>
Boolean(license.name) &&
Object.values(LICENSE_APPROVAL_STATUS).includes(license.approvalStatus),
required: false,
default: null,
},
},
LICENSE_APPROVAL_STATUS,
[LICENSE_APPROVAL_STATUS.APPROVED]: s__('LicenseCompliance|Approved'),
[LICENSE_APPROVAL_STATUS.BLACKLISTED]: s__('LicenseCompliance|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() {
iconStatus() {
return getIssueStatusFromLicenseStatus(this.license.approvalStatus);
},
dropdownText() {
return this.$options[this.license.approvalStatus];
textStatus() {
return getStatusTranslationsFromLicenseStatus(this.license.approvalStatus);
},
},
methods: {
...mapActions(['setLicenseInModal', 'approveLicense', 'blacklistLicense']),
},
};
</script>
<template>
<div data-qa-selector="license_compliance_row">
<issue-status-icon :status="status" class="float-left append-right-default" />
<span class="js-license-name" data-qa-selector="license_name_content">{{ license.name }}</span>
<div class="float-right">
<div class="d-flex">
<gl-dropdown
:text="dropdownText"
toggle-class="d-flex justify-content-between align-items-center"
right
>
<gl-dropdown-item @click="approveLicense(license)">
<icon :class="approveIconClass" name="mobile-issue-close" />
{{ $options[$options.LICENSE_APPROVAL_STATUS.APPROVED] }}
</gl-dropdown-item>
<gl-dropdown-item @click="blacklistLicense(license)">
<icon :class="blacklistIconClass" name="mobile-issue-close" />
{{ $options[$options.LICENSE_APPROVAL_STATUS.BLACKLISTED] }}
</gl-dropdown-item>
</gl-dropdown>
<button
class="btn btn-blank js-remove-button"
type="button"
data-toggle="modal"
data-target="#modal-license-delete-confirmation"
@click="setLicenseInModal(license)"
<div class="gl-responsive-table-row flex-md-column align-items-md-stretch p-0">
<div class="d-md-flex align-items-center js-license-row">
<!-- Name-->
<div class="table-section section-30 section-wrap pr-md-3">
<div class="table-mobile-header" role="rowheader">
{{ s__('Licenses|Name') }}
</div>
<div class="table-mobile-content name">
{{ license.name }}
</div>
</div>
<!-- Policy -->
<div class="table-section section-70 section-wrap pr-md-3">
<div class="table-mobile-header" role="rowheader">{{ s__('Licenses|Policy') }}</div>
<div
class="table-mobile-content text-capitalize d-flex align-items-center justify-content-end justify-content-md-start status"
>
<icon name="remove" />
</button>
<issue-status-icon :status="iconStatus" />
{{ textStatus }}
</div>
</div>
</div>
</div>
......
......@@ -5,20 +5,18 @@ import { s__ } from '~/locale';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import LicensePackages from './license_packages.vue';
import { LICENSE_APPROVAL_STATUS } from '../constants';
import { LICENSE_MANAGEMENT } from 'ee/vue_shared/license_management/store/constants';
export default {
name: 'LicenseSetApprovalStatusModal',
components: { SafeLink, LicensePackages, GlModal: DeprecatedModal2 },
computed: {
...mapState(['currentLicenseInModal', 'canManageLicenses']),
...mapState(LICENSE_MANAGEMENT, ['currentLicenseInModal', 'canManageLicenses']),
headerTitleText() {
if (!this.canManageLicenses) {
return s__('LicenseCompliance|License details');
}
if (this.canApprove) {
return s__('LicenseCompliance|Approve license?');
}
return s__('LicenseCompliance|Blacklist license?');
return s__('LicenseCompliance|License review');
},
canApprove() {
return (
......@@ -36,7 +34,11 @@ export default {
},
},
methods: {
...mapActions(['resetLicenseInModal', 'approveLicense', 'blacklistLicense']),
...mapActions(LICENSE_MANAGEMENT, [
'resetLicenseInModal',
'approveLicense',
'blacklistLicense',
]),
},
};
</script>
......@@ -97,7 +99,7 @@ export default {
data-qa-selector="blacklist_license_button"
@click="blacklistLicense(currentLicenseInModal)"
>
{{ s__('LicenseCompliance|Blacklist license') }}
{{ s__('LicenseCompliance|Deny') }}
</button>
<button
v-if="canApprove"
......@@ -107,7 +109,7 @@ export default {
data-qa-selector="approve_license_button"
@click="approveLicense(currentLicenseInModal)"
>
{{ s__('LicenseCompliance|Approve license') }}
{{ s__('LicenseCompliance|Allow') }}
</button>
</template>
</gl-modal>
......
......@@ -3,18 +3,19 @@ import { mapState, mapActions } from 'vuex';
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import AddLicenseForm from './components/add_license_form.vue';
import AdminLicenseManagementRow from './components/admin_license_management_row.vue';
import LicenseManagementRow from './components/license_management_row.vue';
import DeleteConfirmationModal from './components/delete_confirmation_modal.vue';
import PaginatedList from '~/vue_shared/components/paginated_list.vue';
import createStore from './store/index';
const store = createStore();
import { LICENSE_MANAGEMENT } from 'ee/vue_shared/license_management/store/constants';
export default {
name: 'LicenseManagement',
components: {
AddLicenseForm,
DeleteConfirmationModal,
AdminLicenseManagementRow,
LicenseManagementRow,
GlButton,
GlLoadingIcon,
......@@ -27,11 +28,16 @@ export default {
},
},
data() {
return { formIsOpen: false };
return {
formIsOpen: false,
tableHeaders: [
{ className: 'section-70', label: s__('Licenses|Policy') },
{ className: 'section-30', label: s__('Licenses|Name') },
],
};
},
store,
computed: {
...mapState(['managedLicenses', 'isLoadingManagedLicenses']),
...mapState(LICENSE_MANAGEMENT, ['managedLicenses', 'isLoadingManagedLicenses', 'isAdmin']),
},
mounted() {
this.setAPISettings({
......@@ -40,7 +46,11 @@ export default {
this.fetchManagedLicenses();
},
methods: {
...mapActions(['fetchManagedLicenses', 'setAPISettings', 'setLicenseApproval']),
...mapActions(LICENSE_MANAGEMENT, [
'fetchManagedLicenses',
'setAPISettings',
'setLicenseApproval',
]),
openAddLicenseForm() {
this.formIsOpen = true;
},
......@@ -59,17 +69,19 @@ export default {
<template>
<gl-loading-icon v-if="isLoadingManagedLicenses" />
<div v-else class="license-management">
<delete-confirmation-modal />
<delete-confirmation-modal v-if="isAdmin" />
<paginated-list
:list="managedLicenses"
:empty-search-message="$options.emptySearchMessage"
:empty-message="$options.emptyMessage"
:filterable="isAdmin"
filter="name"
data-qa-selector="license_compliance_list"
>
<template #header>
<gl-button
v-if="isAdmin"
class="js-open-form order-1"
:disabled="formIsOpen"
variant="success"
......@@ -78,9 +90,21 @@ export default {
>
{{ s__('LicenseCompliance|Add a license') }}
</gl-button>
<template v-else>
<div
v-for="header in tableHeaders"
:key="header.label"
class="table-section"
:class="header.className"
role="rowheader"
>
{{ header.label }}
</div>
</template>
</template>
<template #subheader>
<template v-if="isAdmin" #subheader>
<div v-if="formIsOpen" class="prepend-top-default append-bottom-default">
<add-license-form
:managed-licenses="managedLicenses"
......@@ -91,7 +115,8 @@ export default {
</template>
<template #default="{ listItem }">
<license-management-row :license="listItem" />
<admin-license-management-row v-if="isAdmin" :license="listItem" />
<license-management-row v-else :license="listItem" />
</template>
</paginated-list>
</div>
......
......@@ -7,6 +7,8 @@ import { componentNames } from 'ee/reports/components/issue_body';
import Icon from '~/vue_shared/components/icon.vue';
import ReportSection from '~/reports/components/report_section.vue';
import { LICENSE_MANAGEMENT } from 'ee/vue_shared/license_management/store/constants';
import createStore from './store';
const store = createStore();
......@@ -63,8 +65,8 @@ export default {
},
},
computed: {
...mapState(['loadLicenseReportError']),
...mapGetters([
...mapState(LICENSE_MANAGEMENT, ['loadLicenseReportError']),
...mapGetters(LICENSE_MANAGEMENT, [
'licenseReport',
'isLoading',
'licenseSummaryText',
......@@ -98,7 +100,7 @@ export default {
this.fetchParsedLicenseReport();
},
methods: {
...mapActions(['setAPISettings', 'fetchParsedLicenseReport']),
...mapActions(LICENSE_MANAGEMENT, ['setAPISettings', 'fetchParsedLicenseReport']),
},
};
</script>
......
......@@ -105,6 +105,11 @@ export const receiveSetLicenseApproval = ({ commit, dispatch, state }) => {
export const receiveSetLicenseApprovalError = ({ commit }, error) => {
commit(types.RECEIVE_SET_LICENSE_APPROVAL_ERROR, error);
};
export const setIsAdmin = ({ commit }, payload) => {
commit(types.SET_IS_ADMIN, payload);
};
export const setLicenseApproval = ({ dispatch, state }, payload) => {
const { apiUrlManageLicenses } = state;
const { license, newStatus } = payload;
......
/* eslint-disable import/prefer-default-export */
export const LICENSE_MANAGEMENT = 'licenseManagement';
......@@ -7,10 +7,17 @@ import mutations from './mutations';
Vue.use(Vuex);
export const licenseManagementModule = () => ({
namespaced: true,
state: createState(),
actions,
getters,
mutations,
});
export default () =>
new Vuex.Store({
state: createState(),
actions,
getters,
mutations,
modules: {
licenseManagement: licenseManagementModule(),
},
});
......@@ -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_IS_ADMIN = 'SET_IS_ADMIN';
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -15,7 +15,11 @@ export default {
[types.SET_API_SETTINGS](state, data) {
Object.assign(state, data);
},
[types.SET_IS_ADMIN](state, data) {
Object.assign(state, {
isAdmin: data,
});
},
[types.RECEIVE_MANAGED_LICENSES_SUCCESS](state, licenses = []) {
const managedLicenses = licenses.map(normalizeLicense).reverse();
......
......@@ -3,6 +3,7 @@ export default () => ({
licensesApiPath: null,
canManageLicenses: false,
currentLicenseInModal: null,
isAdmin: false,
isDeleting: false,
isLoadingLicenseReport: false,
isLoadingManagedLicenses: false,
......
import { LICENSE_APPROVAL_STATUS } from 'ee/vue_shared/license_management/constants';
import { n__, sprintf } from '~/locale';
import { s__, n__, sprintf } from '~/locale';
import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
/**
......@@ -18,6 +18,15 @@ export const normalizeLicense = license => {
};
};
export const getStatusTranslationsFromLicenseStatus = approvalStatus => {
if (approvalStatus === LICENSE_APPROVAL_STATUS.APPROVED) {
return s__('LicenseCompliance|Allowed');
} else if (approvalStatus === LICENSE_APPROVAL_STATUS.BLACKLISTED) {
return s__('LicenseCompliance|Denied');
}
return '';
};
export const getIssueStatusFromLicenseStatus = approvalStatus => {
if (approvalStatus === LICENSE_APPROVAL_STATUS.APPROVED) {
return STATUS_SUCCESS;
......
......@@ -4,6 +4,9 @@ module Projects
class LicensesController < Projects::ApplicationController
before_action :authorize_read_licenses!, only: [:index]
before_action :authorize_admin_software_license_policy!, only: [:create, :update]
before_action do
push_frontend_feature_flag(:license_policy_list)
end
def index
respond_to do |format|
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LicenseManagementRow allowed license renders the allowed status text with the status icon 1`] = `
<div
class="table-mobile-content text-capitalize d-flex align-items-center justify-content-end justify-content-md-start status"
>
<issue-status-icon-stub
status="success"
statusiconsize="24"
/>
Allowed
</div>
`;
exports[`LicenseManagementRow allowed license renders the license name 1`] = `
<div
class="table-mobile-content name"
>
MIT
</div>
`;
exports[`LicenseManagementRow denied license renders the denied status text with the status icon 1`] = `
<div
class="table-mobile-content text-capitalize d-flex align-items-center justify-content-end justify-content-md-start status"
>
<issue-status-icon-stub
status="failed"
statusiconsize="24"
/>
Denied
</div>
`;
exports[`LicenseManagementRow denied license renders the license name 1`] = `
<div
class="table-mobile-content name"
>
New BSD
</div>
`;
import { shallowMount } from '@vue/test-utils';
import LicenseManagementRow from 'ee/vue_shared/license_management/components/license_management_row.vue';
import { approvedLicense, blacklistedLicense } from 'ee_jest/license_management/mock_data';
let wrapper;
describe('LicenseManagementRow', () => {
afterEach(() => {
wrapper.destroy();
});
describe('allowed license', () => {
beforeEach(() => {
const props = { license: approvedLicense };
wrapper = shallowMount(LicenseManagementRow, {
propsData: {
...props,
},
});
});
it('renders the license name', () => {
expect(wrapper.find('.name').element).toMatchSnapshot();
});
it('renders the allowed status text with the status icon', () => {
expect(wrapper.find('.status').element).toMatchSnapshot();
});
});
describe('denied license', () => {
beforeEach(() => {
const props = { license: blacklistedLicense };
wrapper = shallowMount(LicenseManagementRow, {
propsData: {
...props,
},
});
});
it('renders the license name', () => {
expect(wrapper.find('.name').element).toMatchSnapshot();
});
it('renders the denied status text with the status icon', () => {
expect(wrapper.find('.status').element).toMatchSnapshot();
});
});
});
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import LicenseManagement from 'ee/vue_shared/license_management/license_management.vue';
import AdminLicenseManagementRow from 'ee/vue_shared/license_management/components/admin_license_management_row.vue';
import LicenseManagementRow from 'ee/vue_shared/license_management/components/license_management_row.vue';
import AddLicenseForm from 'ee/vue_shared/license_management/components/add_license_form.vue';
import DeleteConfirmationModal from 'ee/vue_shared/license_management/components/delete_confirmation_modal.vue';
import { TEST_HOST } from 'helpers/test_constants';
import { approvedLicense, blacklistedLicense } from './mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
Vue.use(Vuex);
let wrapper;
const apiUrl = `${TEST_HOST}/license_management`;
const managedLicenses = [approvedLicense, blacklistedLicense];
const PaginatedListMock = {
name: 'PaginatedList',
......@@ -24,111 +31,174 @@ const PaginatedListMock = {
const noop = () => {};
describe('LicenseManagement', () => {
const apiUrl = `${TEST_HOST}/license_management`;
const managedLicenses = [approvedLicense, blacklistedLicense];
let wrapper;
const createComponent = ({ state, props, actionMocks }) => {
const fakeStore = new Vuex.Store({
state: {
managedLicenses,
isLoadingManagedLicenses: true,
...state,
const createComponent = ({ state, props, actionMocks, isAdmin }) => {
const fakeStore = new Vuex.Store({
modules: {
licenseManagement: {
namespaced: true,
state: {
managedLicenses,
isLoadingManagedLicenses: true,
isAdmin,
...state,
},
actions: {
fetchManagedLicenses: noop,
setAPISettings: noop,
setLicenseApproval: noop,
...actionMocks,
},
},
actions: {
fetchManagedLicenses: noop,
setAPISettings: noop,
setLicenseApproval: noop,
...actionMocks,
},
});
},
});
wrapper = shallowMount(LicenseManagement, {
propsData: {
apiUrl,
...props,
},
stubs: {
LicenseManagementRow: true,
PaginatedList: PaginatedListMock,
},
store: fakeStore,
});
};
wrapper = shallowMount(LicenseManagement, {
propsData: {
apiUrl,
...props,
},
stubs: {
PaginatedList: PaginatedListMock,
},
store: fakeStore,
});
};
describe('License Management', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('when loading should render loading icon', () => {
createComponent({ state: { isLoadingManagedLicenses: true } });
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
describe('when not loading', () => {
beforeEach(() => {
createComponent({ state: { isLoadingManagedLicenses: false } });
});
it('should render the form if the form is open and disable the form button', () => {
wrapper.find(GlButton).vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(AddLicenseForm).exists()).toBe(true);
expect(wrapper.find(GlButton).attributes('disabled')).toBe('true');
describe('common functionality', () => {
describe.each`
desc | isAdmin
${'when admin'} | ${true}
${'when developer'} | ${false}
`('$desc', ({ isAdmin }) => {
it('when loading should render loading icon', () => {
createComponent({ state: { isLoadingManagedLicenses: true }, isAdmin });
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
});
it('should not render the form if the form is closed and have active button', () => {
expect(wrapper.find(AddLicenseForm).exists()).toBe(false);
expect(wrapper.find(GlButton).attributes('disabled')).not.toBe('true');
});
describe('when not loading', () => {
beforeEach(() => {
createComponent({ state: { isLoadingManagedLicenses: false }, isAdmin });
});
it('should render delete confirmation modal', () => {
expect(wrapper.find(DeleteConfirmationModal).exists()).toBe(true);
});
it('should render list of managed licenses', () => {
expect(wrapper.find({ name: 'PaginatedList' }).props('list')).toBe(managedLicenses);
});
});
it('should render list of managed licenses', () => {
expect(wrapper.find({ name: 'PaginatedList' }).props('list')).toBe(managedLicenses);
it('should set api settings after mount and init API calls', () => {
const setAPISettingsMock = jest.fn();
const fetchManagedLicensesMock = jest.fn();
createComponent({
state: { isLoadingManagedLicenses: false },
actionMocks: {
setAPISettings: setAPISettingsMock,
fetchManagedLicenses: fetchManagedLicensesMock,
},
isAdmin,
});
expect(setAPISettingsMock).toHaveBeenCalledWith(
expect.any(Object),
{
apiUrlManageLicenses: apiUrl,
},
undefined,
);
expect(fetchManagedLicensesMock).toHaveBeenCalledWith(
expect.any(Object),
undefined,
undefined,
);
});
});
});
it('should invoke `setLicenseAprroval` action on `addLicense` event on form', () => {
const setLicenseApprovalMock = jest.fn();
createComponent({
state: { isLoadingManagedLicenses: false },
actionMocks: { setLicenseApproval: setLicenseApprovalMock },
});
wrapper.find(GlButton).vm.$emit('click');
describe('permission based functionality', () => {
describe('when admin', () => {
it('should invoke `setLicenseAprroval` action on `addLicense` event on form only', () => {
const setLicenseApprovalMock = jest.fn();
createComponent({
state: { isLoadingManagedLicenses: false },
actionMocks: { setLicenseApproval: setLicenseApprovalMock },
isAdmin: true,
});
wrapper.find(GlButton).vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
wrapper.find(AddLicenseForm).vm.$emit('addLicense');
expect(setLicenseApprovalMock).toHaveBeenCalled();
});
});
return wrapper.vm.$nextTick().then(() => {
wrapper.find(AddLicenseForm).vm.$emit('addLicense');
expect(setLicenseApprovalMock).toHaveBeenCalled();
describe('when not loading', () => {
beforeEach(() => {
createComponent({ state: { isLoadingManagedLicenses: false }, isAdmin: true });
});
it('should render the form if the form is open and disable the form button', () => {
wrapper.find(GlButton).vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(AddLicenseForm).exists()).toBe(true);
expect(wrapper.find(GlButton).attributes('disabled')).toBe('true');
});
});
it('should not render the form if the form is closed and have active button', () => {
expect(wrapper.find(AddLicenseForm).exists()).toBe(false);
expect(wrapper.find(GlButton).attributes('disabled')).not.toBe('true');
});
it('should render delete confirmation modal', () => {
expect(wrapper.find(DeleteConfirmationModal).exists()).toBe(true);
});
it('renders the admin row', () => {
expect(wrapper.find(LicenseManagementRow).exists()).toBe(false);
expect(wrapper.find(AdminLicenseManagementRow).exists()).toBe(true);
});
});
});
});
describe('when developer', () => {
it('should not invoke `setLicenseAprroval` action or `addLicense` event on form', () => {
const setLicenseApprovalMock = jest.fn();
createComponent({
state: { isLoadingManagedLicenses: false },
actionMocks: { setLicenseApproval: setLicenseApprovalMock },
isAdmin: false,
});
expect(wrapper.find(GlButton).exists()).toBe(false);
expect(wrapper.find(AddLicenseForm).exists()).toBe(false);
expect(setLicenseApprovalMock).not.toHaveBeenCalled();
});
it('should set api settings after mount and init API calls', () => {
const setAPISettingsMock = jest.fn();
const fetchManagedLicensesMock = jest.fn();
describe('when not loading', () => {
beforeEach(() => {
createComponent({ state: { isLoadingManagedLicenses: false, isAdmin: false } });
});
createComponent({
state: { isLoadingManagedLicenses: false },
actionMocks: {
setAPISettings: setAPISettingsMock,
fetchManagedLicenses: fetchManagedLicensesMock,
},
});
it('should not render the form', () => {
expect(wrapper.find(AddLicenseForm).exists()).toBe(false);
expect(wrapper.find(GlButton).exists()).toBe(false);
});
expect(setAPISettingsMock).toHaveBeenCalledWith(
expect.any(Object),
{
apiUrlManageLicenses: apiUrl,
},
undefined,
);
it('should not render delete confirmation modal', () => {
expect(wrapper.find(DeleteConfirmationModal).exists()).toBe(false);
});
expect(fetchManagedLicensesMock).toHaveBeenCalledWith(expect.any(Object), undefined, undefined);
it('renders the read only row', () => {
expect(wrapper.find(LicenseManagementRow).exists()).toBe(true);
expect(wrapper.find(AdminLicenseManagementRow).exists()).toBe(false);
});
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { GlEmptyState, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import { REPORT_STATUS } from 'ee/project_licenses/store/modules/list/constants';
import ProjectLicensesApp from 'ee/project_licenses/components/app.vue';
import PaginatedLicensesTable from 'ee/project_licenses/components/paginated_licenses_table.vue';
import PipelineInfo from 'ee/project_licenses/components/pipeline_info.vue';
import LicenseManagement from 'ee/vue_shared/license_management/license_management.vue';
import * as getters from 'ee/project_licenses/store/modules/list/getters';
import { approvedLicense, blacklistedLicense } from 'ee_jest/license_management/mock_data';
Vue.use(Vuex);
let wrapper;
const readLicensePoliciesEndpoint = `${TEST_HOST}/license_management`;
const managedLicenses = [approvedLicense, blacklistedLicense];
const licenses = [{}, {}];
const emptyStateSvgPath = '/';
const documentationPath = '/';
const noop = () => {};
const createComponent = ({ state, props, options }) => {
const fakeStore = new Vuex.Store({
modules: {
licenseManagement: {
namespaced: true,
state: {
managedLicenses,
},
},
licenseList: {
namespaced: true,
state: {
licenses,
reportInfo: {
jobPath: '/',
generatedAt: '',
},
...state,
},
actions: {
fetchLicenses: noop,
},
getters,
},
},
});
wrapper = shallowMount(ProjectLicensesApp, {
propsData: {
emptyStateSvgPath,
documentationPath,
readLicensePoliciesEndpoint,
...props,
},
...options,
store: fakeStore,
});
};
describe('Project Licenses', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when loading', () => {
beforeEach(() => {
createComponent({
state: { initialized: false },
});
});
it('shows the loading component', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
it('does not show the empty state component', () => {
expect(wrapper.find(GlEmptyState).exists()).toBe(false);
});
it('does not show the list of detected in project licenses', () => {
expect(wrapper.find(PaginatedLicensesTable).exists()).toBe(false);
});
it('does not show the list of license policies', () => {
expect(wrapper.find(LicenseManagement).exists()).toBe(false);
});
it('does not render any tabs', () => {
expect(wrapper.find(GlTabs).exists()).toBe(false);
expect(wrapper.find(GlTab).exists()).toBe(false);
});
});
describe('when empty state', () => {
beforeEach(() => {
createComponent({
state: {
initialized: true,
reportInfo: {
jobPath: '/',
generatedAt: '',
status: REPORT_STATUS.jobNotSetUp,
},
},
});
});
it('shows the empty state component', () => {
expect(wrapper.find(GlEmptyState).exists()).toBe(true);
});
it('does not show the loading component', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
});
it('does not show the list of detected in project licenses', () => {
expect(wrapper.find(PaginatedLicensesTable).exists()).toBe(false);
});
it('does not show the list of license policies', () => {
expect(wrapper.find(LicenseManagement).exists()).toBe(false);
});
it('does not render any tabs', () => {
expect(wrapper.find(GlTabs).exists()).toBe(false);
expect(wrapper.find(GlTab).exists()).toBe(false);
});
});
describe('when licensePolicyList feature flag is enabled', () => {
beforeEach(() => {
createComponent({
state: {
initialized: true,
reportInfo: {
jobPath: '/',
generatedAt: '',
status: REPORT_STATUS.ok,
},
},
options: {
provide: {
glFeatures: { licensePolicyList: true },
},
},
});
});
it('renders a "Detected in project" tab and a "Policies" tab', () => {
expect(wrapper.find(GlTabs).exists()).toBe(true);
expect(wrapper.find(GlTab).exists()).toBe(true);
expect(wrapper.findAll(GlTab).length).toBe(2);
});
it('it renders the "Detected in project" table', () => {
expect(wrapper.find(PaginatedLicensesTable).exists()).toBe(true);
});
it('it renders the "Policies" table', () => {
expect(wrapper.find(LicenseManagement).exists()).toBe(true);
});
it('renders the pipeline info', () => {
expect(wrapper.find(PipelineInfo).exists()).toBe(true);
});
});
describe('when licensePolicyList feature flag is disabled', () => {
beforeEach(() => {
createComponent({
state: {
initialized: true,
reportInfo: {
jobPath: '/',
generatedAt: '',
status: REPORT_STATUS.ok,
},
},
options: {
provide: {
glFeatures: { licensePolicyList: false },
},
},
});
});
it('only renders the "Detected in project" table', () => {
expect(wrapper.find(PaginatedLicensesTable).exists()).toBe(true);
expect(wrapper.find(LicenseManagement).exists()).toBe(false);
});
it('renders no "Policies" table', () => {
expect(wrapper.find(GlTabs).exists()).toBe(false);
expect(wrapper.find(GlTab).exists()).toBe(false);
});
it('renders the pipeline info', () => {
expect(wrapper.find(PipelineInfo).exists()).toBe(true);
});
it('renders no tabs', () => {
expect(wrapper.find(GlTabs).exists()).toBe(false);
expect(wrapper.find(GlTab).exists()).toBe(false);
});
});
});
......@@ -98,9 +98,9 @@ describe('AddLicenseForm', () => {
const radioButtonParents = vm.$el.querySelectorAll('.form-check');
expect(radioButtonParents.length).toBe(2);
expect(radioButtonParents[0].innerText.trim()).toBe('Approve');
expect(radioButtonParents[0].innerText.trim()).toBe('Allow');
expect(radioButtonParents[0].querySelector('.form-check-input')).not.toBeNull();
expect(radioButtonParents[1].innerText.trim()).toBe('Blacklist');
expect(radioButtonParents[1].innerText.trim()).toBe('Deny');
expect(radioButtonParents[1].querySelector('.form-check-input')).not.toBeNull();
});
......
import Vue from 'vue';
import Vuex from 'vuex';
import LicenseManagementRow from 'ee/vue_shared/license_management/components/license_management_row.vue';
import AdminLicenseManagementRow from 'ee/vue_shared/license_management/components/admin_license_management_row.vue';
import { LICENSE_APPROVAL_STATUS } from 'ee/vue_shared/license_management/constants';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
......@@ -10,8 +10,8 @@ import { approvedLicense } from 'ee_spec/license_management/mock_data';
const visibleClass = 'visible';
const invisibleClass = 'invisible';
describe('LicenseManagementRow', () => {
const Component = Vue.extend(LicenseManagementRow);
describe('AdminLicenseManagementRow', () => {
const Component = Vue.extend(AdminLicenseManagementRow);
let vm;
let store;
......@@ -28,8 +28,13 @@ describe('LicenseManagementRow', () => {
};
store = new Vuex.Store({
state: {},
actions,
modules: {
licenseManagement: {
namespaced: true,
state: {},
actions,
},
},
});
const props = { license: approvedLicense };
......@@ -48,8 +53,8 @@ describe('LicenseManagementRow', () => {
});
describe('computed', () => {
it('dropdownText returns `Approved`', () => {
expect(vm.dropdownText).toBe('Approved');
it('dropdownText returns `Allowed`', () => {
expect(vm.dropdownText).toBe('Allowed');
});
it('isApproved returns `true`', () => {
......@@ -83,8 +88,8 @@ describe('LicenseManagementRow', () => {
});
describe('computed', () => {
it('dropdownText returns `Blacklisted`', () => {
expect(vm.dropdownText).toBe('Blacklisted');
it('dropdownText returns `Denied`', () => {
expect(vm.dropdownText).toBe('Denied');
});
it('isApproved returns `false`', () => {
......@@ -164,7 +169,7 @@ describe('LicenseManagementRow', () => {
expect(dropdownEl.innerText.trim()).toBe(vm.dropdownText);
});
it('renders the dropdown with `Approved` and `Blacklisted` options', () => {
it('renders the dropdown with `Allowed` and `Denied` options', () => {
const dropdownEl = vm.$el.querySelector('.dropdown');
expect(dropdownEl).not.toBeNull();
......@@ -172,12 +177,12 @@ describe('LicenseManagementRow', () => {
const firstOption = findNthDropdown(0);
expect(firstOption).not.toBeNull();
expect(firstOption.innerText.trim()).toBe('Approved');
expect(firstOption.innerText.trim()).toBe('Allowed');
const secondOption = findNthDropdown(1);
expect(secondOption).not.toBeNull();
expect(secondOption.innerText.trim()).toBe('Blacklisted');
expect(secondOption.innerText.trim()).toBe('Denied');
});
});
});
......@@ -8,7 +8,6 @@ import { approvedLicense } from 'ee_spec/license_management/mock_data';
describe('DeleteConfirmationModal', () => {
const Component = Vue.extend(DeleteConfirmationModal);
let vm;
let store;
let actions;
......@@ -20,10 +19,15 @@ describe('DeleteConfirmationModal', () => {
};
store = new Vuex.Store({
state: {
currentLicenseInModal: approvedLicense,
modules: {
licenseManagement: {
namespaced: true,
state: {
currentLicenseInModal: approvedLicense,
},
actions,
},
},
actions,
});
vm = mountComponentWithStore(Component, { store });
......@@ -47,9 +51,11 @@ describe('DeleteConfirmationModal', () => {
store.replaceState({
...store.state,
currentLicenseInModal: {
...approvedLicense,
name,
licenseManagement: {
currentLicenseInModal: {
...approvedLicense,
name,
},
},
});
......@@ -89,7 +95,7 @@ describe('DeleteConfirmationModal', () => {
expect(actions.deleteLicense).toHaveBeenCalledWith(
jasmine.any(Object),
store.state.currentLicenseInModal,
store.state.licenseManagement.currentLicenseInModal,
undefined,
);
});
......
......@@ -25,11 +25,11 @@ describe('LicenseIssueBody', () => {
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);
expect(store.state.licenseManagement.currentLicenseInModal).toBe(null);
linkEl.click();
expect(store.state.currentLicenseInModal).toBe(issue);
expect(store.state.licenseManagement.currentLicenseInModal).toBe(issue);
});
});
......
......@@ -22,11 +22,16 @@ describe('SetApprovalModal', () => {
};
store = new Vuex.Store({
state: {
currentLicenseInModal: licenseReport[0],
canManageLicenses: true,
modules: {
licenseManagement: {
namespaced: true,
state: {
currentLicenseInModal: licenseReport[0],
canManageLicenses: true,
},
actions,
},
},
actions,
});
vm = mountComponentWithStore(Component, { store });
......@@ -39,18 +44,20 @@ describe('SetApprovalModal', () => {
describe('for approved license', () => {
beforeEach(done => {
store.replaceState({
currentLicenseInModal: {
...licenseReport[0],
approvalStatus: LICENSE_APPROVAL_STATUS.APPROVED,
licenseManagement: {
currentLicenseInModal: {
...licenseReport[0],
approvalStatus: LICENSE_APPROVAL_STATUS.APPROVED,
},
canManageLicenses: true,
},
canManageLicenses: true,
});
Vue.nextTick(done);
});
describe('computed', () => {
it('headerTitleText returns `Blacklist license?`', () => {
expect(vm.headerTitleText).toBe('Blacklist license?');
it('headerTitleText returns `License review', () => {
expect(vm.headerTitleText).toBe('License review');
});
it('canApprove is false', () => {
......@@ -67,20 +74,20 @@ describe('SetApprovalModal', () => {
const headerEl = vm.$el.querySelector('.modal-title');
expect(headerEl).not.toBeNull();
expect(headerEl.innerText.trim()).toBe('Blacklist license?');
expect(headerEl.innerText.trim()).toBe('License review');
});
it('renders no Approve button in modal footer', () => {
it('renders no Allow button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-primary-action');
expect(footerButton).toBeNull();
});
it('renders Blacklist button in modal footer', () => {
it('renders Deny button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-secondary-action');
expect(footerButton).not.toBeNull();
expect(footerButton.innerText.trim()).toBe('Blacklist license');
expect(footerButton.innerText.trim()).toBe('Deny');
});
});
});
......@@ -88,18 +95,20 @@ describe('SetApprovalModal', () => {
describe('for unapproved license', () => {
beforeEach(done => {
store.replaceState({
currentLicenseInModal: {
...licenseReport[0],
approvalStatus: undefined,
licenseManagement: {
currentLicenseInModal: {
...licenseReport[0],
approvalStatus: undefined,
},
canManageLicenses: true,
},
canManageLicenses: true,
});
Vue.nextTick(done);
});
describe('computed', () => {
it('headerTitleText returns `Approve license?`', () => {
expect(vm.headerTitleText).toBe('Approve license?');
it('headerTitleText returns `License review`', () => {
expect(vm.headerTitleText).toBe('License review');
});
it('canApprove is true', () => {
......@@ -116,21 +125,21 @@ describe('SetApprovalModal', () => {
const headerEl = vm.$el.querySelector('.modal-title');
expect(headerEl).not.toBeNull();
expect(headerEl.innerText.trim()).toBe('Approve license?');
expect(headerEl.innerText.trim()).toBe('License review');
});
it('renders Approve button in modal footer', () => {
it('renders Allow button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-primary-action');
expect(footerButton).not.toBeNull();
expect(footerButton.innerText.trim()).toBe('Approve license');
expect(footerButton.innerText.trim()).toBe('Allow');
});
it('renders Blacklist button in modal footer', () => {
it('renders Deny button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-secondary-action');
expect(footerButton).not.toBeNull();
expect(footerButton.innerText.trim()).toBe('Blacklist license');
expect(footerButton.innerText.trim()).toBe('Deny');
});
});
});
......@@ -138,18 +147,20 @@ describe('SetApprovalModal', () => {
describe('for blacklisted license', () => {
beforeEach(done => {
store.replaceState({
currentLicenseInModal: {
...licenseReport[0],
approvalStatus: LICENSE_APPROVAL_STATUS.BLACKLISTED,
licenseManagement: {
currentLicenseInModal: {
...licenseReport[0],
approvalStatus: LICENSE_APPROVAL_STATUS.BLACKLISTED,
},
canManageLicenses: true,
},
canManageLicenses: true,
});
Vue.nextTick(done);
});
describe('computed', () => {
it('headerTitleText returns `Approve license?`', () => {
expect(vm.headerTitleText).toBe('Approve license?');
it('headerTitleText returns `License review`', () => {
expect(vm.headerTitleText).toBe('License review');
});
it('canApprove is true', () => {
......@@ -166,17 +177,17 @@ describe('SetApprovalModal', () => {
const headerEl = vm.$el.querySelector('.modal-title');
expect(headerEl).not.toBeNull();
expect(headerEl.innerText.trim()).toBe('Approve license?');
expect(headerEl.innerText.trim()).toBe('License review');
});
it('renders Approve button in modal footer', () => {
it('renders Allow button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-primary-action');
expect(footerButton).not.toBeNull();
expect(footerButton.innerText.trim()).toBe('Approve license');
expect(footerButton.innerText.trim()).toBe('Allow');
});
it('renders no Blacklist button in modal footer', () => {
it('renders no Deny button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-secondary-action');
expect(footerButton).toBeNull();
......@@ -187,11 +198,13 @@ describe('SetApprovalModal', () => {
describe('for user without the rights to manage licenses', () => {
beforeEach(done => {
store.replaceState({
currentLicenseInModal: {
...licenseReport[0],
approvalStatus: undefined,
licenseManagement: {
currentLicenseInModal: {
...licenseReport[0],
approvalStatus: undefined,
},
canManageLicenses: false,
},
canManageLicenses: false,
});
Vue.nextTick(done);
});
......@@ -284,7 +297,7 @@ describe('SetApprovalModal', () => {
expect(actions.approveLicense).toHaveBeenCalledWith(
jasmine.any(Object),
store.state.currentLicenseInModal,
store.state.licenseManagement.currentLicenseInModal,
undefined,
);
});
......@@ -297,7 +310,7 @@ describe('SetApprovalModal', () => {
expect(actions.blacklistLicense).toHaveBeenCalledWith(
jasmine.any(Object),
store.state.currentLicenseInModal,
store.state.licenseManagement.currentLicenseInModal,
undefined,
);
});
......@@ -309,10 +322,12 @@ describe('SetApprovalModal', () => {
const badURL = 'javascript:alert("")';
store.replaceState({
currentLicenseInModal: {
...licenseReport[0],
url: badURL,
approvalStatus: LICENSE_APPROVAL_STATUS.APPROVED,
licenseManagement: {
currentLicenseInModal: {
...licenseReport[0],
url: badURL,
approvalStatus: LICENSE_APPROVAL_STATUS.APPROVED,
},
},
});
Vue.nextTick()
......
......@@ -62,9 +62,14 @@ describe('License Report MR Widget', () => {
actions = defaultActions,
} = {}) => {
const store = new Vuex.Store({
state,
getters,
actions,
modules: {
licenseManagement: {
namespaced: true,
state,
getters,
actions,
},
},
});
return mountComponentWithStore(Component, { props, store });
};
......
......@@ -59,6 +59,20 @@ describe('License store actions', () => {
});
});
describe('setIsAdmin', () => {
it('commits SET_IS_ADMIN', done => {
testAction(
actions.setIsAdmin,
false,
state,
[{ type: mutationTypes.SET_IS_ADMIN, payload: false }],
[],
)
.then(done)
.catch(done.fail);
});
});
describe('resetLicenseInModal', () => {
it('commits RESET_LICENSE_IN_MODAL', done => {
testAction(
......
......@@ -14,9 +14,9 @@ describe('License store mutations', () => {
describe('SET_LICENSE_IN_MODAL', () => {
it('opens modal and sets passed license', () => {
store.commit(types.SET_LICENSE_IN_MODAL, approvedLicense);
store.commit(`licenseManagement/${types.SET_LICENSE_IN_MODAL}`, approvedLicense);
expect(store.state.currentLicenseInModal).toBe(approvedLicense);
expect(store.state.licenseManagement.currentLicenseInModal).toBe(approvedLicense);
});
});
......@@ -24,12 +24,14 @@ describe('License store mutations', () => {
it('closes modal and deletes licenseInApproval', () => {
store.replaceState({
...store.state,
currentLicenseInModal: approvedLicense,
licenseManagement: {
currentLicenseInModal: approvedLicense,
},
});
store.commit(types.RESET_LICENSE_IN_MODAL);
store.commit(`licenseManagement/${types.RESET_LICENSE_IN_MODAL}`);
expect(store.state.currentLicenseInModal).toBeNull();
expect(store.state.licenseManagement.currentLicenseInModal).toBeNull();
});
});
......@@ -37,9 +39,23 @@ describe('License store mutations', () => {
it('assigns data to the store', () => {
const data = { apiUrlManageLicenses: TEST_HOST };
store.commit(types.SET_API_SETTINGS, data);
store.commit(`licenseManagement/${types.SET_API_SETTINGS}`, data);
expect(store.state.apiUrlManageLicenses).toBe(TEST_HOST);
expect(store.state.licenseManagement.apiUrlManageLicenses).toBe(TEST_HOST);
});
});
describe('SET_IS_ADMIN', () => {
it('sets isAdmin to false', () => {
store.commit(`licenseManagement/${types.SET_IS_ADMIN}`, false);
expect(store.state.licenseManagement.isAdmin).toBe(false);
});
it('sets isAdmin to true', () => {
store.commit(`licenseManagement/${types.SET_IS_ADMIN}`, true);
expect(store.state.licenseManagement.isAdmin).toBe(true);
});
});
......@@ -47,12 +63,14 @@ describe('License store mutations', () => {
it('sets isDeleting to false and closes the modal', () => {
store.replaceState({
...store.state,
isDeleting: true,
licenseManagement: {
isDeleting: true,
},
});
store.commit(types.RECEIVE_DELETE_LICENSE);
store.commit(`licenseManagement/${types.RECEIVE_DELETE_LICENSE}`);
expect(store.state.isDeleting).toBe(false);
expect(store.state.licenseManagement.isDeleting).toBe(false);
});
});
......@@ -60,14 +78,16 @@ describe('License store mutations', () => {
it('sets isDeleting to false and closes the modal', () => {
store.replaceState({
...store.state,
isDeleting: true,
currentLicenseInModal: approvedLicense,
licenseManagement: {
isDeleting: true,
currentLicenseInModal: approvedLicense,
},
});
store.commit(types.RECEIVE_DELETE_LICENSE_ERROR);
store.commit(`licenseManagement/${types.RECEIVE_DELETE_LICENSE_ERROR}`);
expect(store.state.isDeleting).toBe(false);
expect(store.state.currentLicenseInModal).toBeNull();
expect(store.state.licenseManagement.isDeleting).toBe(false);
expect(store.state.licenseManagement.currentLicenseInModal).toBeNull();
});
});
......@@ -75,12 +95,14 @@ describe('License store mutations', () => {
it('sets isDeleting to true', () => {
store.replaceState({
...store.state,
isDeleting: false,
licenseManagement: {
isDeleting: false,
},
});
store.commit(types.REQUEST_DELETE_LICENSE);
store.commit(`licenseManagement/${types.REQUEST_DELETE_LICENSE}`);
expect(store.state.isDeleting).toBe(true);
expect(store.state.licenseManagement.isDeleting).toBe(true);
});
});
......@@ -88,12 +110,14 @@ describe('License store mutations', () => {
it('sets isSaving to false and closes the modal', () => {
store.replaceState({
...store.state,
isSaving: true,
licenseManagement: {
isSaving: true,
},
});
store.commit(types.RECEIVE_SET_LICENSE_APPROVAL);
store.commit(`licenseManagement/${types.RECEIVE_SET_LICENSE_APPROVAL}`);
expect(store.state.isSaving).toBe(false);
expect(store.state.licenseManagement.isSaving).toBe(false);
});
});
......@@ -101,14 +125,16 @@ describe('License store mutations', () => {
it('sets isSaving to false and closes the modal', () => {
store.replaceState({
...store.state,
isSaving: true,
currentLicenseInModal: approvedLicense,
licenseManagement: {
isSaving: true,
currentLicenseInModal: approvedLicense,
},
});
store.commit(types.RECEIVE_SET_LICENSE_APPROVAL_ERROR);
store.commit(`licenseManagement/${types.RECEIVE_SET_LICENSE_APPROVAL_ERROR}`);
expect(store.state.isSaving).toBe(false);
expect(store.state.currentLicenseInModal).toBeNull();
expect(store.state.licenseManagement.isSaving).toBe(false);
expect(store.state.licenseManagement.currentLicenseInModal).toBeNull();
});
});
......@@ -116,12 +142,14 @@ describe('License store mutations', () => {
it('sets isSaving to true', () => {
store.replaceState({
...store.state,
isSaving: false,
licenseManagement: {
isSaving: false,
},
});
store.commit(types.REQUEST_SET_LICENSE_APPROVAL);
store.commit(`licenseManagement/${types.REQUEST_SET_LICENSE_APPROVAL}`);
expect(store.state.isSaving).toBe(true);
expect(store.state.licenseManagement.isSaving).toBe(true);
});
});
......@@ -129,21 +157,23 @@ describe('License store mutations', () => {
it('sets isLoadingManagedLicenses and loadManagedLicensesError to false and saves managed licenses', () => {
store.replaceState({
...store.state,
managedLicenses: false,
isLoadingManagedLicenses: true,
loadManagedLicensesError: true,
licenseManagement: {
managedLicenses: false,
isLoadingManagedLicenses: true,
loadManagedLicensesError: true,
},
});
store.commit(types.RECEIVE_MANAGED_LICENSES_SUCCESS, [
store.commit(`licenseManagement/${types.RECEIVE_MANAGED_LICENSES_SUCCESS}`, [
{ name: 'Foo', approval_status: LICENSE_APPROVAL_STATUS.approved },
]);
expect(store.state.managedLicenses).toEqual([
expect(store.state.licenseManagement.managedLicenses).toEqual([
{ name: 'Foo', approvalStatus: LICENSE_APPROVAL_STATUS.approved },
]);
expect(store.state.isLoadingManagedLicenses).toBe(false);
expect(store.state.loadManagedLicensesError).toBe(false);
expect(store.state.licenseManagement.isLoadingManagedLicenses).toBe(false);
expect(store.state.licenseManagement.loadManagedLicensesError).toBe(false);
});
});
......@@ -152,14 +182,16 @@ describe('License store mutations', () => {
const error = new Error('test');
store.replaceState({
...store.state,
isLoadingManagedLicenses: true,
loadManagedLicensesError: false,
licenseManagement: {
isLoadingManagedLicenses: true,
loadManagedLicensesError: false,
},
});
store.commit(types.RECEIVE_MANAGED_LICENSES_ERROR, error);
store.commit(`licenseManagement/${types.RECEIVE_MANAGED_LICENSES_ERROR}`, error);
expect(store.state.isLoadingManagedLicenses).toBe(false);
expect(store.state.loadManagedLicensesError).toBe(error);
expect(store.state.licenseManagement.isLoadingManagedLicenses).toBe(false);
expect(store.state.licenseManagement.loadManagedLicensesError).toBe(error);
});
});
......@@ -167,12 +199,30 @@ describe('License store mutations', () => {
it('sets isLoadingManagedLicenses to true', () => {
store.replaceState({
...store.state,
isLoadingManagedLicenses: true,
licenseManagement: {
isLoadingManagedLicenses: true,
},
});
store.commit(types.REQUEST_MANAGED_LICENSES);
store.commit(`licenseManagement/${types.REQUEST_MANAGED_LICENSES}`);
expect(store.state.isLoadingManagedLicenses).toBe(true);
expect(store.state.licenseManagement.isLoadingManagedLicenses).toBe(true);
});
});
describe('REQUEST_PARSED_LICENSE_REPORT', () => {
beforeEach(() => {
store.replaceState({
...store.state,
licenseManagement: {
isLoadingLicenseReport: false,
},
});
store.commit(`licenseManagement/${types.REQUEST_PARSED_LICENSE_REPORT}`);
});
it('should initiate loading', () => {
expect(store.state.licenseManagement.isLoadingLicenseReport).toBe(true);
});
});
......@@ -181,47 +231,49 @@ describe('License store mutations', () => {
const existingLicenses = [];
beforeEach(() => {
store.state.isLoadingLicenseReport = true;
store.state.loadLicenseReportError = new Error('test');
store.commit(types.RECEIVE_PARSED_LICENSE_REPORT_SUCCESS, { newLicenses, existingLicenses });
store.replaceState({
...store.state,
licenseManagement: {
isLoadingLicenseReport: true,
loadLicenseReportError: new Error('test'),
},
});
store.commit(`licenseManagement/${types.RECEIVE_PARSED_LICENSE_REPORT_SUCCESS}`, {
newLicenses,
existingLicenses,
});
});
it('should set the new and existing reports', () => {
expect(store.state.newLicenses).toBe(newLicenses);
expect(store.state.existingLicenses).toBe(existingLicenses);
expect(store.state.licenseManagement.newLicenses).toBe(newLicenses);
expect(store.state.licenseManagement.existingLicenses).toBe(existingLicenses);
});
it('should cancel loading and clear any errors', () => {
expect(store.state.isLoadingLicenseReport).toBe(false);
expect(store.state.loadLicenseReportError).toBe(false);
expect(store.state.licenseManagement.isLoadingLicenseReport).toBe(false);
expect(store.state.licenseManagement.loadLicenseReportError).toBe(false);
});
});
describe('RECEIVE_PARSED_LICENSE_REPORT_ERROR', () => {
const error = new Error('test');
beforeEach(() => {
store.state.isLoadingLicenseReport = true;
store.state.loadLicenseReportError = false;
store.commit(types.RECEIVE_PARSED_LICENSE_REPORT_ERROR, error);
store.replaceState({
...store.state,
licenseManagement: {
isLoadingLicenseReport: true,
loadLicenseReportError: false,
},
});
store.commit(`licenseManagement/${types.RECEIVE_PARSED_LICENSE_REPORT_ERROR}`, error);
});
it('should set the error on the state', () => {
expect(store.state.loadLicenseReportError).toBe(error);
expect(store.state.licenseManagement.loadLicenseReportError).toBe(error);
});
it('should cancel loading', () => {
expect(store.state.isLoadingLicenseReport).toBe(false);
});
});
describe('REQUEST_PARSED_LICENSE_REPORT', () => {
beforeEach(() => {
store.state.isLoadingLicenseReport = false;
store.commit(types.REQUEST_PARSED_LICENSE_REPORT);
});
it('should initiate loading', () => {
expect(store.state.isLoadingLicenseReport).toBe(true);
expect(store.state.licenseManagement.isLoadingLicenseReport).toBe(false);
});
});
});
import {
normalizeLicense,
getPackagesString,
getStatusTranslationsFromLicenseStatus,
getIssueStatusFromLicenseStatus,
convertToOldReportFormat,
} from 'ee/vue_shared/license_management/store/utils';
......@@ -45,6 +46,24 @@ describe('utils', () => {
});
});
describe('getStatusTranslationsFromLicenseStatus', () => {
it('returns "Allowed" for allowed license status', () => {
expect(getStatusTranslationsFromLicenseStatus(LICENSE_APPROVAL_STATUS.APPROVED)).toBe(
'Allowed',
);
});
it('returns "Denied" status for denied license status', () => {
expect(getStatusTranslationsFromLicenseStatus(LICENSE_APPROVAL_STATUS.BLACKLISTED)).toBe(
'Denied',
);
});
it('returns "" for any other status', () => {
expect(getStatusTranslationsFromLicenseStatus()).toBe('');
});
});
describe('getIssueStatusFromLicenseStatus', () => {
it('returns SUCCESS status for approved license status', () => {
expect(getIssueStatusFromLicenseStatus(LICENSE_APPROVAL_STATUS.APPROVED)).toBe(
......
......@@ -11244,31 +11244,19 @@ msgstr ""
msgid "LicenseCompliance|Add licenses manually to approve or blacklist"
msgstr ""
msgid "LicenseCompliance|Approve"
msgid "LicenseCompliance|Allow"
msgstr ""
msgid "LicenseCompliance|Approve license"
msgid "LicenseCompliance|Allowed"
msgstr ""
msgid "LicenseCompliance|Approve license?"
msgstr ""
msgid "LicenseCompliance|Approved"
msgstr ""
msgid "LicenseCompliance|Blacklist"
msgstr ""
msgid "LicenseCompliance|Blacklist license"
msgstr ""
msgid "LicenseCompliance|Blacklist license?"
msgid "LicenseCompliance|Cancel"
msgstr ""
msgid "LicenseCompliance|Blacklisted"
msgid "LicenseCompliance|Denied"
msgstr ""
msgid "LicenseCompliance|Cancel"
msgid "LicenseCompliance|Deny"
msgstr ""
msgid "LicenseCompliance|Here you can approve or blacklist licenses for this project. Using %{ci} or %{license} will allow you to see if there are any unmanaged licenses and approve or blacklist them in merge request."
......@@ -11312,6 +11300,9 @@ msgstr ""
msgid "LicenseCompliance|License name"
msgstr ""
msgid "LicenseCompliance|License review"
msgstr ""
msgid "LicenseCompliance|Packages"
msgstr ""
......@@ -11357,6 +11348,9 @@ msgstr ""
msgid "Licenses|Components"
msgstr ""
msgid "Licenses|Detected in Project"
msgstr ""
msgid "Licenses|Displays licenses detected in the project, based on the %{linkStart}latest pipeline%{linkEnd} scan"
msgstr ""
......@@ -11372,6 +11366,15 @@ msgstr ""
msgid "Licenses|Name"
msgstr ""
msgid "Licenses|Policies"
msgstr ""
msgid "Licenses|Policy"
msgstr ""
msgid "Licenses|Specified policies in this project"
msgstr ""
msgid "Licenses|The license list details information about the licenses used within your project."
msgstr ""
......
......@@ -20,8 +20,8 @@ module QA::EE
element :license_compliance_list
end
view 'ee/app/assets/javascripts/vue_shared/license_management/components/license_management_row.vue' do
element :license_compliance_row
view 'ee/app/assets/javascripts/vue_shared/license_management/components/admin_license_management_row.vue' do
element :admin_license_compliance_row
element :license_name_content
end
......@@ -30,13 +30,13 @@ module QA::EE
end
def has_approved_license?(name)
within_element(:license_compliance_row, text: name) do
within_element(:admin_license_compliance_row, text: name) do
has_element?(:status_success_icon)
end
end
def has_denied_license?(name)
within_element(:license_compliance_row, text: name) do
within_element(:admin_license_compliance_row, text: name) do
has_element?(:status_failed_icon)
end
end
......
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