Commit 592020ca authored by David O'Regan's avatar David O'Regan

Merge branch...

Merge branch '289810-jira-integration-fe-add-option-to-enable-jira-issue-creation-from-vulnerabilities' into 'master'

Add option to enable Jira issue creation from vulnerabilities

See merge request gitlab-org/gitlab!50350
parents 1318891b 30ed4601
......@@ -8,6 +8,7 @@ import {
GlButton,
GlCard,
} from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../event_hub';
export default {
......@@ -20,18 +21,36 @@ export default {
GlLink,
GlButton,
GlCard,
JiraIssueCreationVulnerabilities: () =>
import('ee_component/integrations/edit/components/jira_issue_creation_vulnerabilities.vue'),
},
mixins: [glFeatureFlagsMixin()],
props: {
showJiraIssuesIntegration: {
type: Boolean,
required: false,
default: false,
},
showJiraVulnerabilitiesIntegration: {
type: Boolean,
required: false,
default: false,
},
initialEnableJiraIssues: {
type: Boolean,
required: false,
default: null,
},
initialEnableJiraVulnerabilities: {
type: Boolean,
required: false,
default: false,
},
initialVulnerabilitiesIssuetype: {
type: String,
required: false,
default: '',
},
initialProjectKey: {
type: String,
required: false,
......@@ -45,12 +64,12 @@ export default {
upgradePlanPath: {
type: String,
required: false,
default: null,
default: '',
},
editProjectPath: {
type: String,
required: false,
default: null,
default: '',
},
},
data() {
......@@ -64,6 +83,13 @@ export default {
validProjectKey() {
return !this.enableJiraIssues || Boolean(this.projectKey) || !this.validated;
},
showJiraVulnerabilitiesOptions() {
return (
this.enableJiraIssues &&
this.showJiraVulnerabilitiesIntegration &&
this.glFeatures.jiraForVulnerabilities
);
},
},
created() {
eventHub.$on('validateForm', this.validateForm);
......@@ -75,6 +101,9 @@ export default {
validateForm() {
this.validated = true;
},
getJiraIssueTypes() {
eventHub.$emit('getJiraIssueTypes');
},
},
};
</script>
......@@ -105,6 +134,14 @@ export default {
}}
</template>
</gl-form-checkbox>
<jira-issue-creation-vulnerabilities
v-if="showJiraVulnerabilitiesOptions"
:project-key="projectKey"
:initial-is-enabled="initialEnableJiraVulnerabilities"
:initial-issue-type-id="initialVulnerabilitiesIssuetype"
data-testid="jira-for-vulnerabilities"
@request-get-issue-types="getJiraIssueTypes"
/>
</template>
<gl-card v-else class="gl-mt-7">
<strong>{{ __('This is a Premium feature') }}</strong>
......
......@@ -27,6 +27,7 @@ function parseDatasetToProps(data) {
cancelPath,
testPath,
resetPath,
vulnerabilitiesIssuetype,
...booleanAttributes
} = data;
const {
......@@ -38,7 +39,9 @@ function parseDatasetToProps(data) {
mergeRequestEvents,
enableComments,
showJiraIssuesIntegration,
showJiraVulnerabilitiesIntegration,
enableJiraIssues,
enableJiraVulnerabilities,
gitlabIssuesEnabled,
} = parseBooleanInData(booleanAttributes);
......@@ -59,7 +62,10 @@ function parseDatasetToProps(data) {
},
jiraIssuesProps: {
showJiraIssuesIntegration,
showJiraVulnerabilitiesIntegration,
initialEnableJiraIssues: enableJiraIssues,
initialEnableJiraVulnerabilities: enableJiraVulnerabilities,
initialVulnerabilitiesIssuetype: vulnerabilitiesIssuetype,
initialProjectKey: projectKey,
gitlabIssuesEnabled,
upgradePlanPath,
......
......@@ -26,3 +26,18 @@ export const fetchResetIntegration = ({ dispatch, getters }) => {
.then(() => dispatch('receiveResetIntegrationSuccess'))
.catch(() => dispatch('receiveResetIntegrationError'));
};
export const requestJiraIssueTypes = ({ commit }) => {
commit(types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE, '');
commit(types.SET_IS_LOADING_JIRA_ISSUE_TYPES, true);
};
export const receiveJiraIssueTypesSuccess = ({ commit }, issueTypes = []) => {
commit(types.SET_IS_LOADING_JIRA_ISSUE_TYPES, false);
commit(types.SET_JIRA_ISSUE_TYPES, issueTypes);
};
export const receiveJiraIssueTypesError = ({ commit }, errorMessage) => {
commit(types.SET_IS_LOADING_JIRA_ISSUE_TYPES, false);
commit(types.SET_JIRA_ISSUE_TYPES, []);
commit(types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE, errorMessage);
};
......@@ -3,5 +3,9 @@ export const SET_IS_SAVING = 'SET_IS_SAVING';
export const SET_IS_TESTING = 'SET_IS_TESTING';
export const SET_IS_RESETTING = 'SET_IS_RESETTING';
export const SET_IS_LOADING_JIRA_ISSUE_TYPES = 'SET_IS_LOADING_JIRA_ISSUE_TYPES';
export const SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE = 'SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE';
export const SET_JIRA_ISSUE_TYPES = 'SET_JIRA_ISSUE_TYPES';
export const REQUEST_RESET_INTEGRATION = 'REQUEST_RESET_INTEGRATION';
export const RECEIVE_RESET_INTEGRATION_ERROR = 'RECEIVE_RESET_INTEGRATION_ERROR';
......@@ -19,4 +19,13 @@ export default {
[types.RECEIVE_RESET_INTEGRATION_ERROR](state) {
state.isResetting = false;
},
[types.SET_JIRA_ISSUE_TYPES](state, jiraIssueTypes) {
state.jiraIssueTypes = jiraIssueTypes;
},
[types.SET_IS_LOADING_JIRA_ISSUE_TYPES](state, isLoadingJiraIssueTypes) {
state.isLoadingJiraIssueTypes = isLoadingJiraIssueTypes;
},
[types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE](state, errorMessage) {
state.loadingJiraIssueTypesErrorMessage = errorMessage;
},
};
......@@ -8,5 +8,8 @@ export default ({ defaultState = null, customState = {} } = {}) => {
isSaving: false,
isTesting: false,
isResetting: false,
isLoadingJiraIssueTypes: false,
loadingJiraIssueTypesErrorMessage: '',
jiraIssueTypes: [],
};
};
......@@ -33,6 +33,12 @@ export default class IntegrationSettingsForm {
eventHub.$on('saveIntegration', () => {
this.saveIntegration();
});
eventHub.$on('getJiraIssueTypes', () => {
// eslint-disable-next-line no-jquery/no-serialize
this.getJiraIssueTypes(this.$form.serialize());
});
eventHub.$emit('formInitialized');
}
saveIntegration() {
......@@ -79,16 +85,59 @@ export default class IntegrationSettingsForm {
}
}
/**
* Get a list of Jira issue types for the currently configured project
*
* @param {string} formData - URL encoded string containing the form data
*
* @return {Promise}
*/
getJiraIssueTypes(formData) {
const {
$store: { dispatch },
} = this.vue;
dispatch('requestJiraIssueTypes');
return this.fetchTestSettings(formData)
.then(
({
data: {
issuetypes,
error,
message = s__('Integrations|Connection failed. Please check your settings.'),
},
}) => {
if (error || !issuetypes?.length) {
eventHub.$emit('validateForm');
throw new Error(message);
}
dispatch('receiveJiraIssueTypesSuccess', issuetypes);
},
)
.catch(({ message = __('Something went wrong on our end.') }) => {
dispatch('receiveJiraIssueTypesError', message);
});
}
/**
* Send request to the test endpoint which checks if the current config is valid
*/
fetchTestSettings(formData) {
return axios.put(this.testEndPoint, formData);
}
/**
* Test Integration config
*/
testSettings(formData) {
return axios
.put(this.testEndPoint, formData)
return this.fetchTestSettings(formData)
.then(({ data }) => {
if (data.error) {
toast(`${data.message} ${data.service_response}`);
} else {
this.vue.$store.dispatch('receiveJiraIssueTypesSuccess', data.issuetypes);
toast(s__('Integrations|Connection successful.'));
}
})
......
<script>
import { mapState } from 'vuex';
import {
GlAlert,
GlButton,
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlFormCheckbox,
GlFormGroup,
GlIcon,
GlTooltipDirective,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import { defaultJiraIssueTypeId } from '../constants';
export const i18n = {
checkbox: {
label: s__('JiraService|Enable Jira issues creation from vulnerabilities'),
description: s__(
'JiraService|Issues created from vulnerabilities in this project will be Jira issues, even if GitLab issues are enabled.',
),
},
issueTypeSelect: {
label: s__('JiraService|Jira issue type'),
description: s__('JiraService|Define the type of Jira issue to create from a vulnerability.'),
defaultText: s__('JiraService|Select issue type'),
},
fetchIssueTypesButtonLabel: s__('JiraService|Fetch issue types for this Jira project'),
fetchIssueTypesErrorMessage: s__('JiraService|An error occured while fetching issue list'),
projectKeyWarnings: {
missing: s__('JiraService|Project key is required to generate issue types'),
changed: s__('JiraService|Project key changed, refresh list'),
},
};
export default {
i18n,
components: {
GlAlert,
GlButton,
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlFormCheckbox,
GlFormGroup,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
projectKey: {
type: String,
required: false,
default: '',
},
initialIssueTypeId: {
type: String,
required: false,
default: defaultJiraIssueTypeId,
},
initialIsEnabled: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
isLoadingErrorAlertDimissed: false,
projectKeyForCurrentIssues: '',
isJiraVulnerabilitiesEnabled: this.initialIsEnabled,
selectedJiraIssueType: null,
};
},
computed: {
...mapState([
'isTesting',
'jiraIssueTypes',
'isLoadingJiraIssueTypes',
'loadingJiraIssueTypesErrorMessage',
]),
initialJiraIssueType() {
return this.jiraIssueTypes?.find(({ id }) => id === this.initialIssueTypeId) || {};
},
checkedIssueType() {
return this.selectedJiraIssueType || this.initialJiraIssueType;
},
hasProjectKeyChanged() {
return this.projectKeyForCurrentIssues && this.projectKey !== this.projectKeyForCurrentIssues;
},
shouldShowLoadingErrorAlert() {
return !this.isLoadingErrorAlertDimissed && this.loadingJiraIssueTypesErrorMessage;
},
projectKeyWarning() {
const {
$options: {
i18n: { projectKeyWarnings },
},
} = this;
if (!this.projectKey) {
return projectKeyWarnings.missing;
}
if (this.hasProjectKeyChanged) {
return projectKeyWarnings.changed;
}
return '';
},
},
created() {
if (this.initialIsEnabled) {
this.$emit('request-get-issue-types');
}
},
methods: {
handleLoadJiraIssueTypesClick() {
this.$emit('request-get-issue-types');
this.projectKeyForCurrentIssues = this.projectKey;
this.isLoadingErrorAlertDimissed = false;
},
},
};
</script>
<template>
<div>
<gl-form-checkbox
v-model="isJiraVulnerabilitiesEnabled"
data-testid="enable-jira-vulnerabilities"
>
{{ $options.i18n.checkbox.label }}
<template #help>
{{ $options.i18n.checkbox.description }}
</template>
</gl-form-checkbox>
<input
name="service[vulnerabilities_enabled]"
type="hidden"
:value="isJiraVulnerabilitiesEnabled"
/>
<gl-form-group
v-if="isJiraVulnerabilitiesEnabled"
:label="$options.i18n.issueTypeSelect.label"
class="gl-mt-4 gl-pl-1 gl-ml-5"
data-testid="issue-type-section"
>
<p>{{ $options.i18n.issueTypeSelect.description }}</p>
<gl-alert
v-if="shouldShowLoadingErrorAlert"
class="gl-mb-5"
variant="danger"
:title="$options.i18n.fetchIssueTypesErrorMessage"
@dismiss="isLoadingErrorAlertDimissed = true"
>
{{ loadingJiraIssueTypesErrorMessage }}
</gl-alert>
<div class="row gl-display-flex gl-align-items-center">
<gl-button-group class="col-md-5 gl-mr-3">
<input
name="service[vulnerabilities_issuetype]"
type="hidden"
:value="checkedIssueType.id || initialIssueTypeId"
/>
<gl-dropdown
class="gl-w-full"
:disabled="!jiraIssueTypes.length"
:loading="isLoadingJiraIssueTypes || isTesting"
:text="checkedIssueType.name || $options.i18n.issueTypeSelect.defaultText"
>
<gl-dropdown-item
v-for="jiraIssueType in jiraIssueTypes"
:key="jiraIssueType.id"
:is-checked="checkedIssueType.id === jiraIssueType.id"
is-check-item
@click="selectedJiraIssueType = jiraIssueType"
>
{{ jiraIssueType.name }}
</gl-dropdown-item>
</gl-dropdown>
<gl-button
v-gl-tooltip.hover
:title="$options.i18n.fetchIssueTypesButtonLabel"
:disabled="!projectKey"
icon="retry"
data-testid="fetch-issue-types"
@click="handleLoadJiraIssueTypesClick"
/>
</gl-button-group>
<p v-if="projectKeyWarning" class="gl-my-0">
<gl-icon name="warning" class="gl-text-orange-500" />
{{ projectKeyWarning }}
</p>
</div>
</gl-form-group>
</div>
</template>
// default value that is used when initially fetching available issues
// see https://gitlab.com/gitlab-org/gitlab/-/issues/289810#note_460366303
export const defaultJiraIssueTypeId = '10000';
import { nextTick } from 'vue';
import { mount, shallowMount } from '@vue/test-utils';
import { within } from '@testing-library/dom';
import { GlAlert, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import JiraIssueCreationVulnerabilities, {
i18n,
} from 'ee/integrations/edit/components/jira_issue_creation_vulnerabilities.vue';
import { createStore } from '~/integrations/edit/store';
describe('JiraIssuesFields', () => {
let store;
let wrapper;
const defaultProps = {
initialIssueTypeId: '10000',
};
const TEST_JIRA_ISSUE_TYPES = [
{ id: '1', name: 'issue', description: 'issue' },
{ id: '2', name: 'bug', description: 'bug' },
{ id: '3', name: 'epic', description: 'epic' },
];
const createComponent = (mountFn) => ({ props } = {}) => {
return extendedWrapper(
mountFn(JiraIssueCreationVulnerabilities, {
store,
propsData: { ...defaultProps, ...props },
}),
);
};
const createShallowComponent = createComponent(shallowMount);
const createFullComponent = createComponent(mount);
const withinComponent = () => within(wrapper.element);
const findHiddenInput = (name) => wrapper.find(`input[name="service[${name}]"]`);
const findEnableJiraVulnerabilities = () => wrapper.findByTestId('enable-jira-vulnerabilities');
const findIssueTypeSection = () => wrapper.findByTestId('issue-type-section');
const findIssueTypeDropdown = () => wrapper.findComponent(GlDropdown);
const findAllIssueDropdownItems = () => findIssueTypeDropdown().findAll(GlDropdownItem);
const findFetchIssueTypeButton = () => wrapper.findByTestId('fetch-issue-types');
const findFetchErrorAlert = () => wrapper.findComponent(GlAlert);
const setEnableJiraVulnerabilitiesChecked = (isChecked) =>
findEnableJiraVulnerabilities().vm.$emit('input', isChecked);
beforeEach(() => {
store = createStore();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('content', () => {
beforeEach(() => {
wrapper = createFullComponent();
});
it('contains a heading', () => {
expect(withinComponent().getByText(i18n.checkbox.label)).not.toBe(null);
});
it('contains a more detailed description', () => {
expect(withinComponent().getByText(i18n.checkbox.description)).not.toBe(null);
});
describe('when Jira issue creation is enabled', () => {
beforeEach(async () => {
await findEnableJiraVulnerabilities().setChecked();
});
it('shows a label for the issue-type selector', () => {
expect(withinComponent().getByText(i18n.issueTypeSelect.label)).not.toBe(null);
});
it('shows a reason why the issue type is needed', () => {
expect(withinComponent().getByText(i18n.issueTypeSelect.description)).not.toBe(null);
});
});
});
describe('"Enable Jira issues creation from vulnerabilities" checkbox', () => {
beforeEach(() => {
wrapper = createShallowComponent();
});
it.each([true, false])(
'toggles the hidden "vulnerabilities_enabled" input value',
async (isChecked) => {
await setEnableJiraVulnerabilitiesChecked(isChecked);
expect(findHiddenInput('vulnerabilities_enabled').attributes('value')).toBe(`${isChecked}`);
},
);
it.each([true, false])('toggles the Jira issue-type selection section', async (isChecked) => {
await setEnableJiraVulnerabilitiesChecked(isChecked);
expect(findIssueTypeSection().exists()).toBe(isChecked);
});
});
describe('Jira issue type dropdown', () => {
describe('with no Jira issues fetched', () => {
beforeEach(async () => {
wrapper = createShallowComponent();
await setEnableJiraVulnerabilitiesChecked(true);
});
it('receives the correct props', () => {
expect(findIssueTypeDropdown().props()).toMatchObject({
disabled: true,
loading: false,
text: i18n.issueTypeSelect.defaultText,
});
});
it('does not contain any dropdown-items', () => {
expect(findAllIssueDropdownItems()).toHaveLength(0);
});
});
describe('with Jira issues fetching in progress', () => {
beforeEach(async () => {
store.state.isLoadingJiraIssueTypes = true;
wrapper = createShallowComponent();
await setEnableJiraVulnerabilitiesChecked(true);
});
it('receives the correct props', () => {
expect(findIssueTypeDropdown().props()).toMatchObject({
disabled: true,
loading: true,
});
});
});
describe('with Jira issues fetched', () => {
beforeEach(async () => {
store.state.jiraIssueTypes = TEST_JIRA_ISSUE_TYPES;
wrapper = createShallowComponent({ props: { projectKey: 'TES' } });
await setEnableJiraVulnerabilitiesChecked(true);
});
it('receives the correct props', () => {
expect(findIssueTypeDropdown().props()).toMatchObject({
disabled: false,
loading: false,
});
});
it('contains a dropdown-item for each issue type', () => {
expect(findAllIssueDropdownItems()).toHaveLength(TEST_JIRA_ISSUE_TYPES.length);
});
it.each(TEST_JIRA_ISSUE_TYPES)('shows the selected issue name', async (issue) => {
const issueIndex = TEST_JIRA_ISSUE_TYPES.indexOf(issue);
findAllIssueDropdownItems().at(issueIndex).vm.$emit('click');
await nextTick();
expect(findIssueTypeDropdown().props('text')).toBe(issue.name);
});
});
describe('with Jira issue fetch failure', () => {
beforeEach(async () => {
store.state.loadingJiraIssueTypesErrorMessage = 'something went wrong';
wrapper = createShallowComponent();
await setEnableJiraVulnerabilitiesChecked(true);
});
it('shows an error message', () => {
expect(findFetchErrorAlert().exists()).toBe(true);
});
});
});
describe('fetch Jira issue types button', () => {
beforeEach(async () => {
wrapper = createShallowComponent({ props: { projectKey: null } });
await setEnableJiraVulnerabilitiesChecked(true);
});
it('has a help text', () => {
expect(findFetchIssueTypeButton().attributes('title')).toBe(i18n.fetchIssueTypesButtonLabel);
});
it('emits "fetch-issues-clicked" when clicked', async () => {
expect(wrapper.emitted('request-get-issue-types')).toBe(undefined);
await findFetchIssueTypeButton().vm.$emit('click');
expect(wrapper.emitted('request-get-issue-types')).toHaveLength(1);
});
});
describe('Jira project key prop', () => {
describe('with no Jira project key', () => {
beforeEach(async () => {
wrapper = createShallowComponent({ props: { projectKey: null } });
await setEnableJiraVulnerabilitiesChecked(true);
});
it('shows a warning message telling the user to enter a valid project key', () => {
expect(withinComponent().getByText(i18n.projectKeyWarnings.missing)).not.toBe(null);
});
});
describe('with fetched issue types and Jira project key changing', () => {
beforeEach(async () => {
wrapper = createShallowComponent({ props: { projectKey: 'INITIAL' } });
await setEnableJiraVulnerabilitiesChecked(true);
findFetchIssueTypeButton().vm.$emit('click');
wrapper.setProps({ projectKey: 'CHANGED' });
});
it('shows a warning message telling the user to refetch the issues list', () => {
expect(withinComponent().getByText(i18n.projectKeyWarnings.changed)).not.toBe(null);
});
});
});
});
......@@ -16091,15 +16091,27 @@ msgstr ""
msgid "JiraService|%{user_link} mentioned this issue in %{entity_link} of %{project_link}%{branch}:{quote}%{entity_message}{quote}"
msgstr ""
msgid "JiraService|An error occured while fetching issue list"
msgstr ""
msgid "JiraService|Define the type of Jira issue to create from a vulnerability."
msgstr ""
msgid "JiraService|Displaying Jira issues while leaving the GitLab issue functionality enabled might be confusing. Consider %{linkStart}disabling GitLab issues%{linkEnd} if they won’t otherwise be used."
msgstr ""
msgid "JiraService|Enable Jira issues"
msgstr ""
msgid "JiraService|Enable Jira issues creation from vulnerabilities"
msgstr ""
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
msgid "JiraService|Fetch issue types for this Jira project"
msgstr ""
msgid "JiraService|For example, 12, 24"
msgstr ""
......@@ -16109,6 +16121,9 @@ msgstr ""
msgid "JiraService|Issue List"
msgstr ""
msgid "JiraService|Issues created from vulnerabilities in this project will be Jira issues, even if GitLab issues are enabled."
msgstr ""
msgid "JiraService|Jira API URL"
msgstr ""
......@@ -16124,6 +16139,9 @@ msgstr ""
msgid "JiraService|Jira issue tracker"
msgstr ""
msgid "JiraService|Jira issue type"
msgstr ""
msgid "JiraService|Jira project key"
msgstr ""
......@@ -16136,6 +16154,15 @@ msgstr ""
msgid "JiraService|Password or API token"
msgstr ""
msgid "JiraService|Project key changed, refresh list"
msgstr ""
msgid "JiraService|Project key is required to generate issue types"
msgstr ""
msgid "JiraService|Select issue type"
msgstr ""
msgid "JiraService|Set transition IDs for Jira workflow transitions. %{link_start}Learn more%{link_end}"
msgstr ""
......
......@@ -3,18 +3,22 @@ import { mount } from '@vue/test-utils';
import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
import eventHub from '~/integrations/edit/event_hub';
describe('JiraIssuesFields', () => {
let wrapper;
const defaultProps = {
showJiraIssuesIntegration: true,
editProjectPath: '/edit',
showJiraIssuesIntegration: true,
showJiraVulnerabilitiesIntegration: true,
};
const createComponent = (props) => {
const createComponent = ({ props, ...options } = {}) => {
wrapper = mount(JiraIssuesFields, {
propsData: { ...defaultProps, ...props },
stubs: ['jira-issue-creation-vulnerabilities'],
...options,
});
};
......@@ -28,11 +32,14 @@ describe('JiraIssuesFields', () => {
const findEnableCheckbox = () => wrapper.find(GlFormCheckbox);
const findProjectKey = () => wrapper.find(GlFormInput);
const expectedBannerText = 'This is a Premium feature';
const findJiraForVulnerabilities = () => wrapper.find('[data-testid="jira-for-vulnerabilities"]');
const setEnableCheckbox = async (isEnabled = true) =>
findEnableCheckbox().vm.$emit('input', isEnabled);
describe('template', () => {
describe('upgrade banner for non-Premium user', () => {
beforeEach(() => {
createComponent({ initialProjectKey: '', showJiraIssuesIntegration: false });
createComponent({ props: { initialProjectKey: '', showJiraIssuesIntegration: false } });
});
it('shows upgrade banner', () => {
......@@ -47,7 +54,7 @@ describe('JiraIssuesFields', () => {
describe('Enable Jira issues checkbox', () => {
beforeEach(() => {
createComponent({ initialProjectKey: '' });
createComponent({ props: { initialProjectKey: '' } });
});
it('does not show upgrade banner', () => {
......@@ -69,20 +76,16 @@ describe('JiraIssuesFields', () => {
});
describe('on enable issues', () => {
it('enables project_key input', () => {
findEnableCheckbox().vm.$emit('input', true);
it('enables project_key input', async () => {
await setEnableCheckbox(true);
return wrapper.vm.$nextTick().then(() => {
expect(findProjectKey().attributes('disabled')).toBeUndefined();
});
expect(findProjectKey().attributes('disabled')).toBeUndefined();
});
it('requires project_key input', () => {
findEnableCheckbox().vm.$emit('input', true);
it('requires project_key input', async () => {
await setEnableCheckbox(true);
return wrapper.vm.$nextTick().then(() => {
expect(findProjectKey().attributes('required')).toBe('required');
});
expect(findProjectKey().attributes('required')).toBe('required');
});
});
});
......@@ -103,10 +106,46 @@ describe('JiraIssuesFields', () => {
});
it('does not contain warning when GitLab issues is disabled', () => {
createComponent({ gitlabIssuesEnabled: false });
createComponent({ props: { gitlabIssuesEnabled: false } });
expect(wrapper.text()).not.toContain(expectedText);
});
});
describe('Vulnerabilities creation', () => {
beforeEach(() => {
createComponent({ provide: { glFeatures: { jiraForVulnerabilities: true } } });
});
it.each([true, false])(
'shows the jira-vulnerabilities component correctly when jira issues enables is set to "%s"',
async (hasJiraIssuesEnabled) => {
await setEnableCheckbox(hasJiraIssuesEnabled);
expect(findJiraForVulnerabilities().exists()).toBe(hasJiraIssuesEnabled);
},
);
it('emits "getJiraIssueTypes" to the eventHub when the jira-vulnerabilities component requests to fetch issue types', async () => {
const eventHubEmitSpy = jest.spyOn(eventHub, '$emit');
await setEnableCheckbox(true);
await findJiraForVulnerabilities().vm.$emit('request-get-issue-types');
expect(eventHubEmitSpy).toHaveBeenCalledWith('getJiraIssueTypes');
});
describe('with "jiraForVulnerabilities" feature flag disabled', () => {
beforeEach(async () => {
createComponent({
provide: { glFeatures: { jiraForVulnerabilities: false } },
});
});
it('does not show section', () => {
expect(findJiraForVulnerabilities().exists()).toBe(false);
});
});
});
});
});
......@@ -9,6 +9,9 @@ import {
requestResetIntegration,
receiveResetIntegrationSuccess,
receiveResetIntegrationError,
requestJiraIssueTypes,
receiveJiraIssueTypesSuccess,
receiveJiraIssueTypesError,
} from '~/integrations/edit/store/actions';
import * as types from '~/integrations/edit/store/mutation_types';
......@@ -70,4 +73,34 @@ describe('Integration form store actions', () => {
]);
});
});
describe('requestJiraIssueTypes', () => {
it('should commit SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE and SET_IS_LOADING_JIRA_ISSUE_TYPES mutations', () => {
return testAction(requestJiraIssueTypes, null, state, [
{ type: types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE, payload: '' },
{ type: types.SET_IS_LOADING_JIRA_ISSUE_TYPES, payload: true },
]);
});
});
describe('receiveJiraIssueTypesSuccess', () => {
it('should commit SET_IS_LOADING_JIRA_ISSUE_TYPES and SET_JIRA_ISSUE_TYPES mutations', () => {
const issueTypes = ['issue', 'epic'];
return testAction(receiveJiraIssueTypesSuccess, issueTypes, state, [
{ type: types.SET_IS_LOADING_JIRA_ISSUE_TYPES, payload: false },
{ type: types.SET_JIRA_ISSUE_TYPES, payload: issueTypes },
]);
});
});
describe('receiveJiraIssueTypesError', () => {
it('should commit SET_IS_LOADING_JIRA_ISSUE_TYPES, SET_JIRA_ISSUE_TYPES and SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE mutations', () => {
const errorMessage = 'something went wrong';
return testAction(receiveJiraIssueTypesError, errorMessage, state, [
{ type: types.SET_IS_LOADING_JIRA_ISSUE_TYPES, payload: false },
{ type: types.SET_JIRA_ISSUE_TYPES, payload: [] },
{ type: types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE, payload: errorMessage },
]);
});
});
});
......@@ -56,4 +56,30 @@ describe('Integration form store mutations', () => {
expect(state.isResetting).toBe(false);
});
});
describe(`${types.SET_JIRA_ISSUE_TYPES}`, () => {
it('sets jiraIssueTypes', () => {
const jiraIssueTypes = ['issue', 'epic'];
mutations[types.SET_JIRA_ISSUE_TYPES](state, jiraIssueTypes);
expect(state.jiraIssueTypes).toBe(jiraIssueTypes);
});
});
describe(`${types.SET_IS_LOADING_JIRA_ISSUE_TYPES}`, () => {
it.each([true, false])('sets isLoadingJiraIssueTypes to "%s"', (isLoading) => {
mutations[types.SET_IS_LOADING_JIRA_ISSUE_TYPES](state, isLoading);
expect(state.isLoadingJiraIssueTypes).toBe(isLoading);
});
});
describe(`${types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE}`, () => {
it('sets loadingJiraIssueTypesErrorMessage', () => {
const errorMessage = 'something went wrong';
mutations[types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE](state, errorMessage);
expect(state.loadingJiraIssueTypesErrorMessage).toBe(errorMessage);
});
});
});
......@@ -9,6 +9,9 @@ describe('Integration form state factory', () => {
isTesting: false,
isResetting: false,
override: false,
isLoadingJiraIssueTypes: false,
jiraIssueTypes: [],
loadingJiraIssueTypesErrorMessage: '',
});
});
......
......@@ -132,4 +132,83 @@ describe('IntegrationSettingsForm', () => {
expect(dispatchSpy).toHaveBeenCalledWith('setIsTesting', false);
});
});
describe('getJiraIssueTypes', () => {
let integrationSettingsForm;
let formData;
let mock;
beforeEach(() => {
mock = new MockAdaptor(axios);
jest.spyOn(axios, 'put');
integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
integrationSettingsForm.init();
// eslint-disable-next-line no-jquery/no-serialize
formData = integrationSettingsForm.$form.serialize();
});
afterEach(() => {
mock.restore();
});
it('should always dispatch `requestJiraIssueTypes`', async () => {
const dispatchSpy = jest.fn();
mock.onPut(integrationSettingsForm.testEndPoint).networkError();
integrationSettingsForm.vue.$store = { dispatch: dispatchSpy };
await integrationSettingsForm.getJiraIssueTypes();
expect(dispatchSpy).toHaveBeenCalledWith('requestJiraIssueTypes');
});
it('should make an ajax request with provided `formData`', async () => {
await integrationSettingsForm.getJiraIssueTypes(formData);
expect(axios.put).toHaveBeenCalledWith(integrationSettingsForm.testEndPoint, formData);
});
it('should dispatch `receiveJiraIssueTypesSuccess` with the correct payload if ajax request is successful', async () => {
const mockData = ['ISSUE', 'EPIC'];
const dispatchSpy = jest.fn();
mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
error: false,
issuetypes: mockData,
});
integrationSettingsForm.vue.$store = { dispatch: dispatchSpy };
await integrationSettingsForm.getJiraIssueTypes(formData);
expect(dispatchSpy).toHaveBeenCalledWith('receiveJiraIssueTypesSuccess', mockData);
});
it.each(['something went wrong', undefined])(
'should dispatch "receiveJiraIssueTypesError" with a message if the backend responds with error',
async (responseErrorMessage) => {
const defaultErrorMessage = 'Connection failed. Please check your settings.';
const expectedErrorMessage = responseErrorMessage || defaultErrorMessage;
const dispatchSpy = jest.fn();
mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
error: true,
message: responseErrorMessage,
});
integrationSettingsForm.vue.$store = { dispatch: dispatchSpy };
await integrationSettingsForm.getJiraIssueTypes(formData);
expect(dispatchSpy).toHaveBeenCalledWith(
'receiveJiraIssueTypesError',
expectedErrorMessage,
);
},
);
});
});
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