Commit 646522cc authored by Fernando's avatar Fernando

Add MR widget support for license check

* Add vue component
* Add mapping and keys

Add missing stateKey

Add support for license check approval group

* Account for license check approval rule being enabled
* Show corrext messaging in the UI if the rule is enabled

Update POT files and translations

* Regenerate POT files

Add getters unit tests

* Refactor unit tests and add new ones

Run prettier and linter

* Fix pipeline errors

Add mr widget license compliance approval group action tests

* Add unit tests

Add mutation specs for approvals

* Add unit tests

Add specs for mr widget approval vue

* Add unit tests
parent d6f7cbd1
......@@ -15,6 +15,7 @@ export default () => {
apiUrl,
licenseManagementSettingsPath,
licensesApiPath,
approvalsApiPath,
} = licensesTab.dataset;
// eslint-disable-next-line no-new
......@@ -29,6 +30,7 @@ export default () => {
apiUrl,
licensesApiPath,
licenseManagementSettingsPath,
approvalsApiPath,
canManageLicenses: parseBoolean(canManageLicenses),
alwaysOpen: true,
reportSectionClass: 'split-report-section',
......
<script>
import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
export default {
name: 'MRWidgetPolicyViolation',
components: {
statusIcon,
},
};
</script>
<template>
<div class="mr-widget-body media">
<div class="space-children">
<status-icon status="warning" />
<button type="button" class="btn btn-success btn-sm" disabled="true">
{{ s__('mrWidget|Merge') }}
</button>
</div>
<div class="media-body">
<span class="bold">
{{ s__('mrWidget|You can only merge once the denied license is removed') }}
</span>
</div>
</div>
</template>
......@@ -12,6 +12,7 @@ import { n__, s__, __, sprintf } from '~/locale';
import CEWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import MrWidgetApprovals from './components/approvals/approvals.vue';
import MrWidgetGeoSecondaryNode from './components/states/mr_widget_secondary_geo_node.vue';
import MrWidgetPolicyViolation from './components/states/mr_widget_policy_violation.vue';
import MergeTrainHelperText from './components/merge_train_helper_text.vue';
import { MTWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants';
......@@ -21,6 +22,7 @@ export default {
MrWidgetLicenses,
MrWidgetApprovals,
MrWidgetGeoSecondaryNode,
MrWidgetPolicyViolation,
BlockingMergeRequestsReport,
GroupedSecurityReportsApp,
GroupedMetricsReportsApp,
......@@ -358,6 +360,7 @@ export default {
<mr-widget-licenses
v-if="shouldRenderLicenseReport"
:api-url="mr.licenseScanning.managed_licenses_path"
:approvals-api-path="mr.apiApprovalsPath"
:licenses-api-path="licensesApiPath"
:pipeline-path="mr.pipeline.path"
:can-manage-licenses="mr.licenseScanning.can_manage_licenses"
......
import CEGetStateKey from '~/vue_merge_request_widget/stores/get_state_key';
import { stateKey } from './state_maps';
export default function(data) {
if (this.isGeoSecondaryNode) {
return 'geoSecondaryNode';
}
if (data.policy_violation) {
return stateKey.policyViolation;
}
return CEGetStateKey.call(this, data);
}
import stateMaps from '~/vue_merge_request_widget/stores/state_maps';
stateMaps.stateToComponentMap.geoSecondaryNode = 'mr-widget-geo-secondary-node';
stateMaps.stateToComponentMap.policyViolation = 'mr-widget-policy-violation';
export const stateKey = {
policyViolation: 'policyViolation',
};
export default {
stateToComponentMap: stateMaps.stateToComponentMap,
......
......@@ -46,6 +46,11 @@ export default {
required: false,
default: '',
},
approvalsApiPath: {
type: String,
required: false,
default: '',
},
canManageLicenses: {
type: Boolean,
required: true,
......@@ -94,18 +99,31 @@ export default {
},
},
mounted() {
const { apiUrl, canManageLicenses, licensesApiPath } = this;
const { apiUrl, canManageLicenses, licensesApiPath, approvalsApiPath } = this;
this.setAPISettings({
apiUrlManageLicenses: apiUrl,
canManageLicenses,
licensesApiPath,
approvalsApiPath,
});
this.fetchParsedLicenseReport();
/*
If we render this widget from the "License" tab in the pipeline view,
then we don't fetch the approvals since we aren't in the Merge request context.
*/
if (approvalsApiPath) {
this.fetchLicenseCheckApprovalRule();
}
},
methods: {
...mapActions(LICENSE_MANAGEMENT, ['setAPISettings', 'fetchParsedLicenseReport']),
...mapActions(LICENSE_MANAGEMENT, [
'setAPISettings',
'fetchParsedLicenseReport',
'fetchLicenseCheckApprovalRule',
]),
},
};
</script>
......
......@@ -111,6 +111,37 @@ export const receiveSetLicenseApprovalError = ({ commit }, error) => {
commit(types.RECEIVE_SET_LICENSE_APPROVAL_ERROR, error);
};
export const fetchLicenseCheckApprovalRule = ({ dispatch, state }) => {
dispatch('requestLicenseCheckApprovalRule');
axios
.get(state.approvalsApiPath)
.then(({ data }) => {
const hasLicenseCheckApprovalRule = Boolean(
data.approval_rules_left.find(rule => {
return rule.name === 'License-Check';
}),
);
dispatch('receiveLicenseCheckApprovalRuleSuccess', { hasLicenseCheckApprovalRule });
})
.catch(error => {
dispatch('receiveLicenseCheckApprovalRuleError', error);
});
};
export const requestLicenseCheckApprovalRule = ({ commit }) => {
commit(types.REQUEST_LICENSE_CHECK_APPROVAL_RULE);
};
export const receiveLicenseCheckApprovalRuleSuccess = ({ commit }, rule) => {
commit(types.RECEIVE_LICENSE_CHECK_APPROVAL_RULE_SUCCESS, rule);
};
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);
};
......
......@@ -2,7 +2,10 @@ import { n__, s__, sprintf } from '~/locale';
import { addLicensesMatchingReportGroupStatus, reportGroupHasAtLeastOneLicense } from './utils';
import { LICENSE_APPROVAL_STATUS, REPORT_GROUPS } from '../constants';
export const isLoading = state => state.isLoadingManagedLicenses || state.isLoadingLicenseReport;
export const isLoading = state =>
state.isLoadingManagedLicenses ||
state.isLoadingLicenseReport ||
state.isLoadingLicenseCheckApprovalRule;
export const isLicenseBeingUpdated = state => (id = null) => state.pendingLicenses.includes(id);
......@@ -17,10 +20,19 @@ export const licenseReportGroups = state =>
reportGroupHasAtLeastOneLicense,
);
export const licenseSummaryText = (state, getters) => {
const hasReportItems = getters.licenseReport && getters.licenseReport.length;
const baseReportHasLicenses = state.existingLicenses.length;
export const hasReportItems = (_, getters) => {
return getters.licenseReport && getters.licenseReport.length;
};
export const baseReportHasLicenses = state => {
return state.existingLicenses.length;
};
export const licenseReportLength = (_, getters) => {
return getters.licenseReport.length;
};
export const licenseSummaryText = (state, getters) => {
if (getters.isLoading) {
return sprintf(s__('ciReport|Loading %{reportName} report'), {
reportName: s__('License Compliance'),
......@@ -33,43 +45,75 @@ export const licenseSummaryText = (state, getters) => {
});
}
if (hasReportItems) {
const licenseReportLength = getters.licenseReport.length;
if (!baseReportHasLicenses) {
return getters.reportContainsBlacklistedLicense
? n__(
'LicenseCompliance|License Compliance detected %d license and policy violation for the source branch only; approval required',
'LicenseCompliance|License Compliance detected %d licenses and policy violations for the source branch only; approval required',
licenseReportLength,
)
: n__(
'LicenseCompliance|License Compliance detected %d license for the source branch only',
'LicenseCompliance|License Compliance detected %d licenses for the source branch only',
licenseReportLength,
);
}
if (getters.hasReportItems) {
return state.hasLicenseCheckApprovalRule
? getters.summaryTextWithLicenseCheck
: getters.summaryTextWithoutLicenseCheck;
}
if (!getters.baseReportHasLicenses) {
return s__(
'LicenseCompliance|License Compliance detected no licenses for the source branch only',
);
}
return s__('LicenseCompliance|License Compliance detected no new licenses');
};
export const summaryTextWithLicenseCheck = (_, getters) => {
if (!getters.baseReportHasLicenses) {
return getters.reportContainsBlacklistedLicense
? n__(
'LicenseCompliance|License Compliance detected %d new license and policy violation; approval required',
'LicenseCompliance|License Compliance detected %d new licenses and policy violations; approval required',
licenseReportLength,
'LicenseCompliance|License Compliance detected %d license and policy violation for the source branch only; approval required',
'LicenseCompliance|License Compliance detected %d licenses and policy violations for the source branch only; approval required',
getters.licenseReportLength,
)
: n__(
'LicenseCompliance|License Compliance detected %d new license',
'LicenseCompliance|License Compliance detected %d new licenses',
licenseReportLength,
'LicenseCompliance|License Compliance detected %d license for the source branch only',
'LicenseCompliance|License Compliance detected %d licenses for the source branch only',
getters.licenseReportLength,
);
}
if (!baseReportHasLicenses) {
return s__(
'LicenseCompliance|License Compliance detected no licenses for the source branch only',
);
return getters.reportContainsBlacklistedLicense
? n__(
'LicenseCompliance|License Compliance detected %d new license and policy violation; approval required',
'LicenseCompliance|License Compliance detected %d new licenses and policy violations; approval required',
getters.licenseReportLength,
)
: n__(
'LicenseCompliance|License Compliance detected %d new license',
'LicenseCompliance|License Compliance detected %d new licenses',
getters.licenseReportLength,
);
};
export const summaryTextWithoutLicenseCheck = (_, getters) => {
if (!getters.baseReportHasLicenses) {
return getters.reportContainsBlacklistedLicense
? n__(
'LicenseCompliance|License Compliance detected %d license and policy violation for the source branch only',
'LicenseCompliance|License Compliance detected %d licenses and policy violations for the source branch only',
getters.licenseReportLength,
)
: n__(
'LicenseCompliance|License Compliance detected %d license for the source branch only',
'LicenseCompliance|License Compliance detected %d licenses for the source branch only',
getters.licenseReportLength,
);
}
return s__('LicenseCompliance|License Compliance detected no new licenses');
return getters.reportContainsBlacklistedLicense
? n__(
'LicenseCompliance|License Compliance detected %d new license and policy violation',
'LicenseCompliance|License Compliance detected %d new licenses and policy violations',
getters.licenseReportLength,
)
: n__(
'LicenseCompliance|License Compliance detected %d new license',
'LicenseCompliance|License Compliance detected %d new licenses',
getters.licenseReportLength,
);
};
export const reportContainsBlacklistedLicense = (_, getters) =>
......
......@@ -16,6 +16,11 @@ export const SET_LICENSE_IN_MODAL = 'SET_LICENSE_IN_MODAL';
export const SET_IS_ADMIN = 'SET_IS_ADMIN';
export const ADD_PENDING_LICENSE = 'ADD_PENDING_LICENSE';
export const REMOVE_PENDING_LICENSE = 'REMOVE_PENDING_LICENSE';
export const REQUEST_LICENSE_CHECK_APPROVAL_RULE = 'REQUEST_LICENSE_CHECK_APPROVAL_RULE';
export const RECEIVE_LICENSE_CHECK_APPROVAL_RULE_SUCCESS =
'RECEIVE_LICENSE_CHECK_APPROVAL_RULE_SUCCESS';
export const RECEIVE_LICENSE_CHECK_APPROVAL_RULE_ERROR =
'RECEIVE_LICENSE_CHECK_APPROVAL_RULE_ERROR';
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -96,6 +96,22 @@ export default {
currentLicenseInModal: null,
});
},
[types.REQUEST_LICENSE_CHECK_APPROVAL_RULE](state) {
Object.assign(state, {
isLoadingLicenseCheckApprovalRule: true,
});
},
[types.RECEIVE_LICENSE_CHECK_APPROVAL_RULE_SUCCESS](state, { hasLicenseCheckApprovalRule }) {
Object.assign(state, {
isLoadingLicenseCheckApprovalRule: false,
hasLicenseCheckApprovalRule,
});
},
[types.RECEIVE_LICENSE_CHECK_APPROVAL_RULE_ERROR](state) {
Object.assign(state, {
isLoadingLicenseCheckApprovalRule: false,
});
},
[types.ADD_PENDING_LICENSE](state, id) {
state.pendingLicenses.push(id);
},
......
export default () => ({
apiUrlManageLicenses: null,
approvalsApiPath: null,
licensesApiPath: null,
canManageLicenses: false,
currentLicenseInModal: null,
......@@ -14,4 +15,6 @@ export default () => ({
managedLicenses: [],
newLicenses: [],
existingLicenses: [],
hasLicenseCheckApprovalRule: false,
isLoadingLicenseCheckApprovalRule: false,
});
import { shallowMount } from '@vue/test-utils';
import MrWidgetPolicyViolation from 'ee/vue_merge_request_widget/components/states/mr_widget_policy_violation.vue';
describe('EE MrWidgetPolicyViolation', () => {
let wrapper;
const findButton = () => wrapper.find('button');
const createComponent = () => {
wrapper = shallowMount(MrWidgetPolicyViolation, {});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when shown', () => {
beforeEach(() => {
createComponent();
});
it('shows the disabled merge button', () => {
expect(wrapper.text()).toContain('Merge');
expect(findButton().attributes().disabled).toBe('disabled');
});
it('shows the disabled reason', () => {
expect(wrapper.text()).toContain('You can only merge once the denied license is removed');
});
});
});
......@@ -48,6 +48,7 @@ describe('License Report MR Widget', () => {
loadingText: 'LOADING',
errorText: 'ERROR',
licensesApiPath: `${TEST_HOST}/parsed_license_report.json`,
approvalsApiPath: `${TEST_HOST}/path/to/approvals`,
canManageLicenses: true,
licenseManagementSettingsPath: `${TEST_HOST}/lm_settings`,
fullReportPath: `${TEST_HOST}/path/to/the/full/report`,
......@@ -59,6 +60,7 @@ describe('License Report MR Widget', () => {
setAPISettings: () => {},
fetchManagedLicenses: () => {},
fetchParsedLicenseReport: () => {},
fetchLicenseCheckApprovalRule: () => {},
};
const mountComponent = ({
......@@ -341,6 +343,7 @@ describe('License Report MR Widget', () => {
const actions = {
setAPISettings: jest.fn(() => {}),
fetchParsedLicenseReport: jest.fn(() => {}),
fetchLicenseCheckApprovalRule: jest.fn(() => {}),
};
mountComponent({ actions });
......@@ -349,6 +352,7 @@ describe('License Report MR Widget', () => {
{
apiUrlManageLicenses: apiUrl,
licensesApiPath: defaultProps.licensesApiPath,
approvalsApiPath: defaultProps.approvalsApiPath,
canManageLicenses: true,
},
undefined,
......
......@@ -10,6 +10,7 @@ import axios from '~/lib/utils/axios_utils';
describe('License store actions', () => {
const apiUrlManageLicenses = `${TEST_HOST}/licenses/management`;
const approvalsApiPath = `${TEST_HOST}/approvalsApiPath`;
const licensesApiPath = `${TEST_HOST}/licensesApiPath`;
let axiosMock;
......@@ -26,6 +27,7 @@ describe('License store actions', () => {
state = {
...createState(),
apiUrlManageLicenses,
approvalsApiPath,
currentLicenseInModal: approvedLicense,
};
licenseId = approvedLicense.id;
......@@ -480,6 +482,122 @@ describe('License store actions', () => {
});
});
describe('fetchLicenseCheckApprovalRule ', () => {
it('dispatches request/receive with detected approval rule', done => {
const APPROVAL_RULE_RESPONSE = {
approval_rules_left: [{ name: 'License-Check' }],
};
axiosMock.onGet(approvalsApiPath).replyOnce(200, APPROVAL_RULE_RESPONSE);
testAction(
actions.fetchLicenseCheckApprovalRule,
null,
state,
[],
[
{ type: 'requestLicenseCheckApprovalRule' },
{
type: 'receiveLicenseCheckApprovalRuleSuccess',
payload: { hasLicenseCheckApprovalRule: true },
},
],
done,
);
});
it('dispatches request/receive without detected approval rule', done => {
const APPROVAL_RULE_RESPONSE = {
approval_rules_left: [{ name: 'Another Approval Rule' }],
};
axiosMock.onGet(approvalsApiPath).replyOnce(200, APPROVAL_RULE_RESPONSE);
testAction(
actions.fetchLicenseCheckApprovalRule,
null,
state,
[],
[
{ type: 'requestLicenseCheckApprovalRule' },
{
type: 'receiveLicenseCheckApprovalRuleSuccess',
payload: { hasLicenseCheckApprovalRule: false },
},
],
done,
);
});
it('dispatches request/receive on error', done => {
const error = new Error('Request failed with status code 500');
axiosMock.onGet(approvalsApiPath).replyOnce(500);
testAction(
actions.fetchLicenseCheckApprovalRule,
null,
state,
[],
[
{ type: 'requestLicenseCheckApprovalRule' },
{ type: 'receiveLicenseCheckApprovalRuleError', payload: error },
],
done,
);
});
});
describe('requestLicenseCheckApprovalRule', () => {
it('commits REQUEST_LICENSE_CHECK_APPROVAL_RULE', done => {
testAction(
actions.requestLicenseCheckApprovalRule,
null,
state,
[{ type: mutationTypes.REQUEST_LICENSE_CHECK_APPROVAL_RULE }],
[],
)
.then(done)
.catch(done.fail);
});
});
describe('receiveLicenseCheckApprovalRuleSuccess', () => {
it('commits REQUEST_LICENSE_CHECK_APPROVAL_RULE', done => {
const hasLicenseCheckApprovalRule = true;
testAction(
actions.receiveLicenseCheckApprovalRuleSuccess,
{ hasLicenseCheckApprovalRule },
state,
[
{
type: mutationTypes.RECEIVE_LICENSE_CHECK_APPROVAL_RULE_SUCCESS,
payload: { hasLicenseCheckApprovalRule },
},
],
[],
)
.then(done)
.catch(done.fail);
});
});
describe('receiveLicenseCheckApprovalRuleError', () => {
it('commits RECEIVE_LICENSE_CHECK_APPROVAL_RULE_ERROR', done => {
const error = new Error('Error');
testAction(
actions.receiveLicenseCheckApprovalRuleError,
error,
state,
[{ type: mutationTypes.RECEIVE_LICENSE_CHECK_APPROVAL_RULE_ERROR, payload: error }],
[],
)
.then(done)
.catch(done.fail);
});
});
describe('requestParsedLicenseReport', () => {
it(`should commit ${mutationTypes.REQUEST_PARSED_LICENSE_REPORT}`, done => {
testAction(
......
......@@ -153,6 +153,54 @@ describe('License store mutations', () => {
});
});
describe('REQUEST_LICENSE_CHECK_APPROVAL_RULE', () => {
it('sets isLoadingLicenseCheckApprovalRule to true', () => {
store.replaceState({
...store.state,
licenseManagement: {
isLoadingLicenseCheckApprovalRule: true,
},
});
store.commit(`licenseManagement/${types.REQUEST_LICENSE_CHECK_APPROVAL_RULE}`);
expect(store.state.licenseManagement.isLoadingLicenseCheckApprovalRule).toBe(true);
});
});
describe('RECEIVE_LICENSE_CHECK_APPROVAL_RULE_SUCCESS', () => {
it('sets isLoadingLicenseCheckApprovalRule to false and hasLicenseCheckApprovalRule to true', () => {
store.replaceState({
...store.state,
licenseManagement: {
isLoadingLicenseCheckApprovalRule: true,
},
});
store.commit(`licenseManagement/${types.RECEIVE_LICENSE_CHECK_APPROVAL_RULE_SUCCESS}`, {
hasLicenseCheckApprovalRule: true,
});
expect(store.state.licenseManagement.isLoadingLicenseCheckApprovalRule).toBe(false);
expect(store.state.licenseManagement.hasLicenseCheckApprovalRule).toBe(true);
});
});
describe('RECEIVE_LICENSE_CHECK_APPROVAL_RULE_ERROR', () => {
it('sets isLoadingLicenseCheckApprovalRule to false', () => {
store.replaceState({
...store.state,
licenseManagement: {
isLoadingLicenseCheckApprovalRule: true,
},
});
store.commit(`licenseManagement/${types.RECEIVE_LICENSE_CHECK_APPROVAL_RULE_ERROR}`);
expect(store.state.licenseManagement.isLoadingLicenseCheckApprovalRule).toBe(false);
});
});
describe('RECEIVE_MANAGED_LICENSES_SUCCESS', () => {
it('sets isLoadingManagedLicenses and loadManagedLicensesError to false and saves managed licenses', () => {
store.replaceState({
......
......@@ -13524,6 +13524,11 @@ msgstr ""
msgid "LicenseCompliance|License Approvals"
msgstr ""
msgid "LicenseCompliance|License Compliance detected %d license and policy violation for the source branch only"
msgid_plural "LicenseCompliance|License Compliance detected %d licenses and policy violations for the source branch only"
msgstr[0] ""
msgstr[1] ""
msgid "LicenseCompliance|License Compliance detected %d license and policy violation for the source branch only; approval required"
msgid_plural "LicenseCompliance|License Compliance detected %d licenses and policy violations for the source branch only; approval required"
msgstr[0] ""
......@@ -13539,6 +13544,11 @@ msgid_plural "LicenseCompliance|License Compliance detected %d new licenses"
msgstr[0] ""
msgstr[1] ""
msgid "LicenseCompliance|License Compliance detected %d new license and policy violation"
msgid_plural "LicenseCompliance|License Compliance detected %d new licenses and policy violations"
msgstr[0] ""
msgstr[1] ""
msgid "LicenseCompliance|License Compliance detected %d new license and policy violation; approval required"
msgid_plural "LicenseCompliance|License Compliance detected %d new licenses and policy violations; approval required"
msgstr[0] ""
......@@ -27961,6 +27971,9 @@ msgstr ""
msgid "mrWidget|You can merge this merge request manually using the"
msgstr ""
msgid "mrWidget|You can only merge once the denied license is removed"
msgstr ""
msgid "mrWidget|Your password"
msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment