Commit 25ad8786 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch 'alert-endpoint-trigger-test' into 'master'

Alert endpoint trigger test

See merge request gitlab-org/gitlab!35970
parents bc18b58b baf38c4e
......@@ -5,18 +5,21 @@ import {
GlForm,
GlFormGroup,
GlFormInput,
GlFormInputGroup,
GlFormTextarea,
GlLink,
GlModal,
GlModalDirective,
GlSprintf,
GlFormSelect,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ToggleButton from '~/vue_shared/components/toggle_button.vue';
import csrf from '~/lib/utils/csrf';
import service from '../services';
import { i18n, serviceOptions } from '../constants';
import { i18n, serviceOptions, JSON_VALIDATE_DELAY } from '../constants';
export default {
i18n,
......@@ -27,7 +30,9 @@ export default {
GlForm,
GlFormGroup,
GlFormInput,
GlFormInputGroup,
GlFormSelect,
GlFormTextarea,
GlLink,
GlModal,
GlSprintf,
......@@ -73,6 +78,11 @@ export default {
feedbackMessage: null,
isFeedbackDismissed: false,
},
testAlert: {
json: null,
error: null,
},
canSaveForm: false,
};
},
computed: {
......@@ -109,12 +119,32 @@ export default {
showFeedbackMsg() {
return this.feedback.feedbackMessage && !this.isFeedbackDismissed;
},
showAlertSave() {
return (
this.feedback.feedbackMessage === this.$options.i18n.testAlertFailed &&
!this.isFeedbackDismissed
);
},
prometheusInfo() {
return !this.isGeneric ? this.$options.i18n.prometheusInfo : '';
},
prometheusFeatureEnabled() {
return !this.isGeneric && this.glFeatures.alertIntegrationsDropdown;
},
jsonIsValid() {
return this.testAlert.error === null;
},
canTestAlert() {
return this.selectedService.active && this.testAlert.json !== null;
},
canSaveConfig() {
return !this.loading && this.canSaveForm;
},
},
watch: {
'testAlert.json': debounce(function debouncedJsonValidate() {
this.validateJson();
}, JSON_VALIDATE_DELAY),
},
created() {
if (this.glFeatures.alertIntegrationsDropdown) {
......@@ -126,6 +156,9 @@ export default {
}
},
methods: {
clearJson() {
this.testAlert.json = null;
},
dismissFeedback() {
this.feedback = { ...this.feedback, feedbackMessage: null };
this.isFeedbackDismissed = false;
......@@ -135,6 +168,7 @@ export default {
.updateGenericKey({ endpoint: this.generic.formPath, params: { service: { token: '' } } })
.then(({ data: { token } }) => {
this.authorizationKey.generic = token;
this.setFeedback({ feedbackMessage: this.$options.i18n.authKeyRest, variant: 'success' });
})
.catch(() => {
this.setFeedback({ feedbackMessage: this.$options.i18n.errorKeyMsg, variant: 'danger' });
......@@ -145,11 +179,24 @@ export default {
.updatePrometheusKey({ endpoint: this.prometheus.prometheusResetKeyPath })
.then(({ data: { token } }) => {
this.authorizationKey.prometheus = token;
this.setFeedback({ feedbackMessage: this.$options.i18n.authKeyRest, variant: 'success' });
})
.catch(() => {
this.setFeedback({ feedbackMessage: this.$options.i18n.errorKeyMsg, variant: 'danger' });
});
},
toggleService(value) {
this.canSaveForm = true;
if (!this.glFeatures.alertIntegrationsDropdown) {
this.toggleActivated(value);
}
if (this.isGeneric) {
this.activated.generic = value;
} else {
this.activated.prometheus = value;
}
},
toggleActivated(value) {
return this.isGeneric
? this.toggleGenericActivated(value)
......@@ -164,15 +211,14 @@ export default {
})
.then(() => {
this.activated.generic = value;
if (value) {
this.toggleSuccess(value);
})
.catch(() => {
this.setFeedback({
feedbackMessage: this.$options.i18n.endPointActivated,
variant: 'success',
feedbackMessage: this.$options.i18n.errorMsg,
variant: 'danger',
});
}
})
.catch(() => {})
.finally(() => {
this.loading = false;
});
......@@ -191,16 +237,60 @@ export default {
})
.then(() => {
this.activated.prometheus = value;
this.toggleSuccess(value);
})
.catch(() => {
this.setFeedback({
feedbackMessage: this.$options.i18n.errorApiUrlMsg,
variant: 'danger',
});
})
.finally(() => {
this.loading = false;
});
},
toggleSuccess(value) {
if (value) {
this.setFeedback({
feedbackMessage: this.$options.i18n.endPointActivated,
variant: 'success',
variant: 'info',
});
} else {
this.setFeedback({
feedbackMessage: this.$options.i18n.changesSaved,
variant: 'info',
});
}
},
setFeedback({ feedbackMessage, variant }) {
this.feedback = { feedbackMessage, variant };
},
validateJson() {
this.testAlert.error = null;
try {
JSON.parse(this.testAlert.json);
} catch (e) {
this.testAlert.error = JSON.stringify(e.message);
}
},
validateTestAlert() {
this.loading = true;
this.validateJson();
return service
.updateTestAlert({
endpoint: this.selectedService.url,
data: this.testAlert.json,
authKey: this.selectedService.authKey,
})
.then(() => {
this.setFeedback({
feedbackMessage: this.$options.i18n.testAlertSuccess,
variant: 'success',
});
})
.catch(() => {
this.setFeedback({
feedbackMessage: this.$options.i18n.errorApiUrlMsg,
feedbackMessage: this.$options.i18n.testAlertFailed,
variant: 'danger',
});
})
......@@ -208,16 +298,12 @@ export default {
this.loading = false;
});
},
setFeedback({ feedbackMessage, variant }) {
this.feedback = { feedbackMessage, variant };
onSubmit() {
this.toggleActivated(this.selectedService.active);
},
onSubmit(evt) {
// TODO: Add form submit as part of https://gitlab.com/gitlab-org/gitlab/-/issues/215356
evt.preventDefault();
},
onReset(evt) {
// TODO: Add form reset as part of https://gitlab.com/gitlab-org/gitlab/-/issues/215356
evt.preventDefault();
onReset() {
this.testAlert.json = null;
this.dismissFeedback();
},
},
};
......@@ -227,6 +313,15 @@ export default {
<div>
<gl-alert v-if="showFeedbackMsg" :variant="feedback.variant" @dismiss="dismissFeedback">
{{ feedback.feedbackMessage }}
<gl-button
v-if="showAlertSave"
variant="danger"
category="primary"
class="gl-display-block gl-mt-3"
@click="toggleActivated(selectedService.active)"
>
{{ __('Save anyway') }}
</gl-button>
</gl-alert>
<div data-testid="alert-settings-description" class="gl-mt-5">
<p v-for="section in sections" :key="section.text">
......@@ -237,7 +332,7 @@ export default {
</gl-sprintf>
</p>
</div>
<gl-form @submit="onSubmit" @reset="onReset">
<gl-form @submit.prevent="onSubmit" @reset.prevent="onReset">
<gl-form-group
v-if="glFeatures.alertIntegrationsDropdown"
:label="$options.i18n.integrationsLabel"
......@@ -248,6 +343,7 @@ export default {
v-model="selectedEndpoint"
:options="options"
data-testid="alert-settings-select"
@change="clearJson"
/>
<span class="gl-text-gray-400">
<gl-sprintf :message="$options.i18n.integrationsInfo">
......@@ -272,7 +368,7 @@ export default {
:disabled-input="loading"
:is-loading="loading"
:value="selectedService.active"
@change="toggleActivated"
@change="toggleService"
/>
</gl-form-group>
<gl-form-group
......@@ -293,12 +389,15 @@ export default {
</span>
</gl-form-group>
<gl-form-group :label="$options.i18n.urlLabel" label-for="url" label-class="label-bold">
<div class="input-group">
<gl-form-input id="url" :readonly="true" :value="selectedService.url" />
<span class="input-group-append">
<clipboard-button :text="selectedService.url" :title="$options.i18n.copyToClipboard" />
</span>
</div>
<gl-form-input-group id="url" :readonly="true" :value="selectedService.url">
<template #append>
<clipboard-button
:text="selectedService.url"
:title="$options.i18n.copyToClipboard"
class="gl-m-0!"
/>
</template>
</gl-form-input-group>
<span class="gl-text-gray-400">
{{ prometheusInfo }}
</span>
......@@ -308,15 +407,20 @@ export default {
label-for="authorization-key"
label-class="label-bold"
>
<div class="input-group">
<gl-form-input id="authorization-key" :readonly="true" :value="selectedService.authKey" />
<span class="input-group-append">
<gl-form-input-group
id="authorization-key"
class="gl-mb-2"
:readonly="true"
:value="selectedService.authKey"
>
<template #append>
<clipboard-button
:text="selectedService.authKey"
:title="$options.i18n.copyToClipboard"
class="gl-m-0!"
/>
</span>
</div>
</template>
</gl-form-input-group>
<gl-button v-gl-modal.authKeyModal class="gl-mt-3">{{ $options.i18n.resetKey }}</gl-button>
<gl-modal
modal-id="authKeyModal"
......@@ -328,11 +432,32 @@ export default {
{{ $options.i18n.restKeyInfo }}
</gl-modal>
</gl-form-group>
<gl-form-group
v-if="glFeatures.alertIntegrationsDropdown"
:label="$options.i18n.alertJson"
label-for="alert-json"
label-class="label-bold"
:invalid-feedback="testAlert.error"
>
<gl-form-textarea
id="alert-json"
v-model.trim="testAlert.json"
:disabled="!selectedService.active"
:state="jsonIsValid"
:placeholder="$options.i18n.alertJsonPlaceholder"
rows="6"
max-rows="10"
/>
</gl-form-group>
<gl-button :disabled="!canTestAlert" @click="validateTestAlert">{{
$options.i18n.testAlertInfo
}}</gl-button>
<div
class="footer-block row-content-block gl-display-flex gl-justify-content-space-between d-none"
v-if="glFeatures.alertIntegrationsDropdown"
class="footer-block row-content-block gl-display-flex gl-justify-content-space-between"
>
<gl-button type="submit" variant="success" category="primary">
{{ __('Save and test changes') }}
<gl-button type="submit" variant="success" category="primary" :disabled="!canSaveConfig">
{{ __('Save changes') }}
</gl-button>
<gl-button type="reset" variant="default" category="primary">
{{ __('Cancel') }}
......
......@@ -21,6 +21,7 @@ export const i18n = {
'AlertSettings|Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in.',
),
endPointActivated: s__('AlertSettings|Alerts endpoint successfully activated.'),
changesSaved: s__('AlertSettings|Your changes were successfully updated.'),
prometheusInfo: s__('AlertSettings|Add URL and auth key to your Prometheus config file'),
integrationsInfo: s__(
'AlertSettings|Learn more about our %{linkStart}upcoming integrations%{linkEnd}',
......@@ -32,10 +33,20 @@ export const i18n = {
authKeyLabel: s__('AlertSettings|Authorization key'),
urlLabel: s__('AlertSettings|Webhook URL'),
activeLabel: s__('AlertSettings|Active'),
apiBaseUrlHelpText: s__(' AlertSettings|URL cannot be blank and must start with http or https'),
apiBaseUrlHelpText: s__('AlertSettings|URL cannot be blank and must start with http or https'),
testAlertInfo: s__('AlertSettings|Test alert payload'),
alertJson: s__('AlertSettings|Alert test payload'),
alertJsonPlaceholder: s__('AlertSettings|Enter test alert JSON....'),
testAlertFailed: s__('AlertSettings|Test failed. Do you still want to save your changes anyway?'),
testAlertSuccess: s__(
'AlertSettings|Test alert sent successfully. If you have made other changes, please save them now.',
),
authKeyRest: s__('AlertSettings|Authorization key has been successfully reset'),
};
export const serviceOptions = [
{ value: 'generic', text: s__('AlertSettings|Generic') },
{ value: 'prometheus', text: s__('AlertSettings|External Prometheus') },
];
export const JSON_VALIDATE_DELAY = 250;
/* eslint-disable @gitlab/require-i18n-strings */
import axios from '~/lib/utils/axios_utils';
export default {
......@@ -24,4 +25,12 @@ export default {
},
});
},
updateTestAlert({ endpoint, data, authKey }) {
return axios.post(endpoint, data, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authKey}`,
},
});
},
};
......@@ -17,7 +17,7 @@ module OperationsHelper
def alerts_settings_data
{
'prometheus_activated' => prometheus_service.activated?.to_s,
'prometheus_activated' => prometheus_service.manual_configuration?.to_s,
'activated' => alerts_service.activated?.to_s,
'prometheus_form_path' => scoped_integration_path(prometheus_service),
'form_path' => scoped_integration_path(alerts_service),
......
......@@ -16,9 +16,6 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
msgid " AlertSettings|URL cannot be blank and must start with http or https"
msgstr ""
msgid " %{start} to %{end}"
msgstr ""
......@@ -2086,15 +2083,24 @@ msgstr ""
msgid "AlertSettings|Add URL and auth key to your Prometheus config file"
msgstr ""
msgid "AlertSettings|Alert test payload"
msgstr ""
msgid "AlertSettings|Alerts endpoint successfully activated."
msgstr ""
msgid "AlertSettings|Authorization key"
msgstr ""
msgid "AlertSettings|Authorization key has been successfully reset"
msgstr ""
msgid "AlertSettings|Copy"
msgstr ""
msgid "AlertSettings|Enter test alert JSON...."
msgstr ""
msgid "AlertSettings|External Prometheus"
msgstr ""
......@@ -2119,6 +2125,15 @@ msgstr ""
msgid "AlertSettings|Review your external service's documentation to learn where to provide this information to your external service, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint."
msgstr ""
msgid "AlertSettings|Test alert payload"
msgstr ""
msgid "AlertSettings|Test alert sent successfully. If you have made other changes, please save them now."
msgstr ""
msgid "AlertSettings|Test failed. Do you still want to save your changes anyway?"
msgstr ""
msgid "AlertSettings|There was an error updating the the alert settings. Please refresh the page to try again."
msgstr ""
......@@ -2128,6 +2143,9 @@ msgstr ""
msgid "AlertSettings|There was an error while trying to reset the key. Please refresh the page to try again."
msgstr ""
msgid "AlertSettings|URL cannot be blank and must start with http or https"
msgstr ""
msgid "AlertSettings|Webhook URL"
msgstr ""
......@@ -2137,6 +2155,9 @@ msgstr ""
msgid "AlertSettings|You must provide this URL and authorization key to authorize an external service to send alerts to GitLab. You can provide this URL and key to multiple services. After configuring an external service, alerts from your service will display on the GitLab %{linkStart}Alerts%{linkEnd} page."
msgstr ""
msgid "AlertSettings|Your changes were successfully updated."
msgstr ""
msgid "AlertSettings|http://prometheus.example.com/"
msgstr ""
......@@ -19939,9 +19960,6 @@ msgstr ""
msgid "Save Changes"
msgstr ""
msgid "Save and test changes"
msgstr ""
msgid "Save anyway"
msgstr ""
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AlertsSettingsForm prometheus is active renders a valid "select" 1`] = `"<gl-form-select-stub options=\\"[object Object],[object Object]\\" data-testid=\\"alert-settings-select\\" value=\\"prometheus\\"></gl-form-select-stub>"`;
exports[`AlertsSettingsForm with default values renders the initial template 1`] = `
"<div>
<!---->
......@@ -20,29 +18,20 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`]
</gl-form-group-stub>
<!---->
<gl-form-group-stub label=\\"Webhook URL\\" label-for=\\"url\\" label-class=\\"label-bold\\">
<div class=\\"input-group\\">
<gl-form-input-stub id=\\"url\\" readonly=\\"true\\" value=\\"/alerts/notify.json\\"></gl-form-input-stub> <span class=\\"input-group-append\\"><clipboard-button-stub text=\\"/alerts/notify.json\\" title=\\"Copy\\" tooltipplacement=\\"top\\" cssclass=\\"btn-default\\"></clipboard-button-stub></span>
</div> <span class=\\"gl-text-gray-400\\">
<gl-form-input-group-stub value=\\"/alerts/notify.json\\" predefinedoptions=\\"[object Object]\\" id=\\"url\\" readonly=\\"true\\"></gl-form-input-group-stub> <span class=\\"gl-text-gray-400\\">
</span>
</gl-form-group-stub>
<gl-form-group-stub label=\\"Authorization key\\" label-for=\\"authorization-key\\" label-class=\\"label-bold\\">
<div class=\\"input-group\\">
<gl-form-input-stub id=\\"authorization-key\\" readonly=\\"true\\" value=\\"abcedfg123\\"></gl-form-input-stub> <span class=\\"input-group-append\\"><clipboard-button-stub text=\\"abcedfg123\\" title=\\"Copy\\" tooltipplacement=\\"top\\" cssclass=\\"btn-default\\"></clipboard-button-stub></span>
</div>
<gl-form-input-group-stub value=\\"abcedfg123\\" predefinedoptions=\\"[object Object]\\" id=\\"authorization-key\\" readonly=\\"true\\" class=\\"gl-mb-2\\"></gl-form-input-group-stub>
<gl-button-stub category=\\"tertiary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" class=\\"gl-mt-3\\" role=\\"button\\" tabindex=\\"0\\">Reset key</gl-button-stub>
<gl-modal-stub modalid=\\"authKeyModal\\" titletag=\\"h4\\" modalclass=\\"\\" size=\\"md\\" title=\\"Reset key\\" ok-title=\\"Reset key\\" ok-variant=\\"danger\\">
Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in.
</gl-modal-stub>
</gl-form-group-stub>
<div class=\\"footer-block row-content-block gl-display-flex gl-justify-content-space-between d-none\\">
<gl-button-stub category=\\"primary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" type=\\"submit\\">
Save and test changes
</gl-button-stub>
<gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" type=\\"reset\\">
Cancel
</gl-button-stub>
</div>
<!---->
<gl-button-stub category=\\"tertiary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\">Test alert payload</gl-button-stub>
<!---->
</gl-form-stub>
</div>"
`;
......@@ -36,8 +36,12 @@ describe('AlertsSettingsForm', () => {
props = defaultProps,
{ methods } = {},
alertIntegrationsDropdown = false,
data,
) => {
wrapper = shallowMount(AlertsSettingsForm, {
data() {
return { ...data };
},
propsData: {
...defaultProps,
...props,
......@@ -52,6 +56,7 @@ describe('AlertsSettingsForm', () => {
};
const findSelect = () => wrapper.find('[data-testid="alert-settings-select"]');
const findJsonInput = () => wrapper.find('#alert-json');
const findUrl = () => wrapper.find('#url');
const findAuthorizationKey = () => wrapper.find('#authorization-key');
const findApiUrl = () => wrapper.find('#api-url');
......@@ -115,13 +120,13 @@ describe('AlertsSettingsForm', () => {
describe('activate toggle', () => {
it('triggers toggleActivated method', () => {
const toggleActivated = jest.fn();
const methods = { toggleActivated };
const toggleService = jest.fn();
const methods = { toggleService };
createComponent(defaultProps, { methods });
wrapper.find(ToggleButton).vm.$emit('change', true);
expect(toggleActivated).toHaveBeenCalled();
expect(toggleService).toHaveBeenCalled();
});
describe('error is encountered', () => {
......@@ -149,7 +154,7 @@ describe('AlertsSettingsForm', () => {
});
it('renders a valid "select"', () => {
expect(findSelect().html()).toMatchSnapshot();
expect(findSelect().exists()).toBe(true);
});
it('shows the API URL input', () => {
......@@ -160,9 +165,53 @@ describe('AlertsSettingsForm', () => {
expect(findUrl().exists()).toBe(true);
expect(findUrl().attributes('value')).toBe(PROMETHEUS_URL);
});
});
describe('trigger test alert', () => {
beforeEach(() => {
createComponent({ generic: { ...defaultProps.generic, initialActivated: true } }, {}, true);
});
it('should enable the JSON input', () => {
expect(findJsonInput().exists()).toBe(true);
expect(findJsonInput().props('value')).toBe(null);
});
it('should validate JSON input', () => {
createComponent({ generic: { ...defaultProps.generic } }, {}, true, {
testAlertJson: '{ "value": "test" }',
});
findJsonInput().vm.$emit('change');
return wrapper.vm.$nextTick().then(() => {
expect(findJsonInput().attributes('state')).toBe('true');
});
});
describe('alert service is toggled', () => {
it('should show a info alert if successful', () => {
const formPath = 'some/path';
const toggleService = true;
mockAxios.onPut(formPath).replyOnce(200);
createComponent({ generic: { ...defaultProps.generic, formPath } });
return wrapper.vm.toggleGenericActivated(toggleService).then(() => {
expect(wrapper.find(GlAlert).attributes('variant')).toBe('info');
});
});
it('should not show a footer block', () => {
expect(wrapper.find('.footer-block').classes('d-none')).toBe(true);
it('should show a error alert if failed', () => {
const formPath = 'some/path';
const toggleService = true;
mockAxios.onPut(formPath).replyOnce(404);
createComponent({ generic: { ...defaultProps.generic, formPath } });
return wrapper.vm.toggleGenericActivated(toggleService).then(() => {
expect(wrapper.find(GlAlert).attributes('variant')).toBe('danger');
});
});
});
});
});
......@@ -45,7 +45,9 @@ RSpec.describe OperationsHelper do
end
context 'with external Prometheus configured' do
let_it_be(:prometheus_service, reload: true) { create(:prometheus_service, project: project) }
let_it_be(:prometheus_service, reload: true) do
create(:prometheus_service, project: project)
end
context 'with external Prometheus enabled' do
it 'returns the correct values' do
......@@ -57,11 +59,7 @@ RSpec.describe OperationsHelper do
end
context 'with external Prometheus disabled' do
before do
# Prometheus services uses manual_configuration as an alias for active, beware
prometheus_service.update!(manual_configuration: false)
end
shared_examples 'Prometheus is disabled' do
it 'returns the correct values' do
expect(subject).to include(
'prometheus_activated' => 'false',
......@@ -70,6 +68,25 @@ RSpec.describe OperationsHelper do
end
end
let(:cluster_managed) { false }
before do
allow(prometheus_service)
.to receive(:prometheus_available?)
.and_return(cluster_managed)
prometheus_service.update!(manual_configuration: false)
end
include_examples 'Prometheus is disabled'
context 'when cluster managed' do
let(:cluster_managed) { true }
include_examples 'Prometheus is disabled'
end
end
context 'with project alert setting' do
let_it_be(:project_alerting_setting) { create(:project_alerting_setting, project: project) }
......
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