Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/alerts_settings/components')
-rw-r--r--spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap406
-rw-r--r--spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js102
-rw-r--r--spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js118
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_form_spec.js385
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js379
-rw-r--r--spec/frontend/alerts_settings/components/mocks/apollo_mock.js123
-rw-r--r--spec/frontend/alerts_settings/components/mocks/integrations.json38
-rw-r--r--spec/frontend/alerts_settings/components/util.js24
8 files changed, 1575 insertions, 0 deletions
diff --git a/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap b/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap
new file mode 100644
index 00000000000..eb2b82a0211
--- /dev/null
+++ b/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap
@@ -0,0 +1,406 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AlertsSettingsForm with default values renders the initial template 1`] = `
+<form
+ class="gl-mt-6"
+>
+ <h5
+ class="gl-font-lg gl-my-5"
+ >
+ Add new integrations
+ </h5>
+
+ <div
+ class="form-group gl-form-group"
+ id="integration-type"
+ role="group"
+ >
+ <label
+ class="d-block col-form-label"
+ for="integration-type"
+ id="integration-type__BV_label_"
+ >
+ 1. Select integration type
+ </label>
+ <div
+ class="bv-no-focus-ring"
+ >
+ <select
+ class="gl-form-select mw-100 custom-select"
+ id="__BVID__8"
+ >
+ <option
+ value=""
+ >
+ Select integration type
+ </option>
+ <option
+ value="HTTP"
+ >
+ HTTP Endpoint
+ </option>
+ <option
+ value="PROMETHEUS"
+ >
+ External Prometheus
+ </option>
+ </select>
+
+ <!---->
+ <!---->
+ <!---->
+ <!---->
+ </div>
+ </div>
+
+ <transition-stub
+ class="gl-mt-3"
+ css="true"
+ enteractiveclass="collapsing"
+ enterclass=""
+ entertoclass="collapse show"
+ leaveactiveclass="collapsing"
+ leaveclass="collapse show"
+ leavetoclass="collapse"
+ >
+ <div
+ class="collapse"
+ id="__BVID__10"
+ style="display: none;"
+ >
+ <div>
+ <div
+ class="form-group gl-form-group"
+ id="name-integration"
+ role="group"
+ >
+ <label
+ class="d-block col-form-label"
+ for="name-integration"
+ id="name-integration__BV_label_"
+ >
+ 2. Name integration
+ </label>
+ <div
+ class="bv-no-focus-ring"
+ >
+ <input
+ class="gl-form-input form-control"
+ id="__BVID__15"
+ placeholder="Enter integration name"
+ type="text"
+ />
+ <!---->
+ <!---->
+ <!---->
+ </div>
+ </div>
+
+ <div
+ class="form-group gl-form-group"
+ id="integration-webhook"
+ role="group"
+ >
+ <label
+ class="d-block col-form-label"
+ for="integration-webhook"
+ id="integration-webhook__BV_label_"
+ >
+ 3. Set up webhook
+ </label>
+ <div
+ class="bv-no-focus-ring"
+ >
+ <span>
+ Utilize the URL and authorization key below to authorize an external service to send alerts to GitLab. Review your external service's documentation to learn where to add these details, and the
+ <a
+ class="gl-link gl-display-inline-block"
+ href="https://docs.gitlab.com/ee/operations/incident_management/alert_integrations.html"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ GitLab documentation
+ </a>
+ to learn more about configuring your endpoint.
+ </span>
+
+ <label
+ class="gl-display-flex gl-flex-direction-column gl-mb-0 gl-w-max-content gl-my-4 gl-font-weight-normal"
+ >
+ <span
+ class="gl-toggle-wrapper"
+ >
+ <span
+ class="gl-toggle-label"
+ data-testid="toggle-label"
+ >
+ Active
+ </span>
+
+ <!---->
+
+ <button
+ aria-label="Active"
+ class="gl-toggle"
+ role="switch"
+ type="button"
+ >
+ <span
+ class="toggle-icon"
+ >
+ <svg
+ aria-hidden="true"
+ class="gl-icon s16"
+ data-testid="close-icon"
+ >
+ <use
+ href="#close"
+ />
+ </svg>
+ </span>
+ </button>
+ </span>
+
+ <!---->
+ </label>
+
+ <!---->
+
+ <div
+ class="gl-my-4"
+ >
+ <span
+ class="gl-font-weight-bold"
+ >
+
+ Webhook URL
+
+ </span>
+
+ <div
+ id="url"
+ readonly="readonly"
+ >
+ <div
+ class="input-group"
+ role="group"
+ >
+ <!---->
+ <!---->
+
+ <input
+ class="gl-form-input form-control"
+ id="url"
+ readonly="readonly"
+ type="text"
+ />
+
+ <div
+ class="input-group-append"
+ >
+ <button
+ aria-label="Copy this value"
+ class="btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon"
+ data-clipboard-text=""
+ title="Copy"
+ type="button"
+ >
+ <!---->
+
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon gl-icon s16"
+ data-testid="copy-to-clipboard-icon"
+ >
+ <use
+ href="#copy-to-clipboard"
+ />
+ </svg>
+
+ <!---->
+ </button>
+ </div>
+ <!---->
+ </div>
+ </div>
+ </div>
+
+ <div
+ class="gl-my-4"
+ >
+ <span
+ class="gl-font-weight-bold"
+ >
+
+ Authorization key
+
+ </span>
+
+ <div
+ class="gl-mb-3"
+ id="authorization-key"
+ readonly="readonly"
+ >
+ <div
+ class="input-group"
+ role="group"
+ >
+ <!---->
+ <!---->
+
+ <input
+ class="gl-form-input form-control"
+ id="authorization-key"
+ readonly="readonly"
+ type="text"
+ />
+
+ <div
+ class="input-group-append"
+ >
+ <button
+ aria-label="Copy this value"
+ class="btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon"
+ data-clipboard-text=""
+ title="Copy"
+ type="button"
+ >
+ <!---->
+
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon gl-icon s16"
+ data-testid="copy-to-clipboard-icon"
+ >
+ <use
+ href="#copy-to-clipboard"
+ />
+ </svg>
+
+ <!---->
+ </button>
+ </div>
+ <!---->
+ </div>
+ </div>
+
+ <button
+ class="btn btn-default btn-md disabled gl-button"
+ disabled="disabled"
+ type="button"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Reset Key
+
+ </span>
+ </button>
+
+ <!---->
+ </div>
+ <!---->
+ <!---->
+ <!---->
+ </div>
+ </div>
+
+ <div
+ class="form-group gl-form-group"
+ id="test-integration"
+ role="group"
+ >
+ <label
+ class="d-block col-form-label"
+ for="test-integration"
+ id="test-integration__BV_label_"
+ >
+ 4. Sample alert payload (optional)
+ </label>
+ <div
+ class="bv-no-focus-ring"
+ >
+ <span>
+ Provide an example payload from the monitoring tool you intend to integrate with. This payload can be used to test the integration (optional).
+ </span>
+
+ <textarea
+ class="gl-form-input gl-form-textarea gl-my-3 form-control is-valid"
+ disabled="disabled"
+ id="test-payload"
+ placeholder="{ \\"events\\": [{ \\"application\\": \\"Name of application\\" }] }"
+ style="resize: none; overflow-y: scroll;"
+ wrap="soft"
+ />
+ <!---->
+ <!---->
+ <!---->
+ </div>
+ </div>
+
+ <!---->
+
+ <!---->
+ </div>
+
+ <div
+ class="gl-display-flex gl-justify-content-start gl-py-3"
+ >
+ <button
+ class="btn js-no-auto-disable btn-success btn-md gl-button"
+ data-testid="integration-form-submit"
+ type="submit"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+ Save integration
+
+ </span>
+ </button>
+
+ <button
+ class="btn gl-mx-3 js-no-auto-disable btn-success btn-md disabled gl-button btn-success-secondary"
+ data-testid="integration-test-and-submit"
+ disabled="disabled"
+ type="button"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+ Save and test payload
+ </span>
+ </button>
+
+ <button
+ class="btn js-no-auto-disable btn-default btn-md gl-button"
+ type="reset"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+ Cancel
+ </span>
+ </button>
+ </div>
+ </div>
+ </transition-stub>
+</form>
+`;
diff --git a/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js
new file mode 100644
index 00000000000..7e1d1acb62c
--- /dev/null
+++ b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js
@@ -0,0 +1,102 @@
+import { GlIcon, GlFormInput, GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import AlertMappingBuilder, { i18n } from '~/alerts_settings/components/alert_mapping_builder.vue';
+import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json';
+import * as transformationUtils from '~/alerts_settings/utils/mapping_transformations';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import alertFields from '../mocks/alertFields.json';
+
+describe('AlertMappingBuilder', () => {
+ let wrapper;
+
+ function mountComponent() {
+ wrapper = shallowMount(AlertMappingBuilder, {
+ propsData: {
+ parsedPayload: parsedMapping.samplePayload.payloadAlerFields.nodes,
+ savedMapping: parsedMapping.storedMapping.nodes,
+ alertFields,
+ },
+ });
+ }
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ const findColumnInRow = (row, column) =>
+ wrapper.findAll('.gl-display-table-row').at(row).findAll('.gl-display-table-cell ').at(column);
+
+ it('renders column captions', () => {
+ expect(findColumnInRow(0, 0).text()).toContain(i18n.columns.gitlabKeyTitle);
+ expect(findColumnInRow(0, 2).text()).toContain(i18n.columns.payloadKeyTitle);
+ expect(findColumnInRow(0, 3).text()).toContain(i18n.columns.fallbackKeyTitle);
+
+ const fallbackColumnIcon = findColumnInRow(0, 3).find(GlIcon);
+ expect(fallbackColumnIcon.exists()).toBe(true);
+ expect(fallbackColumnIcon.attributes('name')).toBe('question');
+ expect(fallbackColumnIcon.attributes('title')).toBe(i18n.fallbackTooltip);
+ });
+
+ it('renders disabled form input for each mapped field', () => {
+ alertFields.forEach((field, index) => {
+ const input = findColumnInRow(index + 1, 0).find(GlFormInput);
+ const types = field.types.map((t) => capitalizeFirstCharacter(t.toLowerCase())).join(' or ');
+ expect(input.attributes('value')).toBe(`${field.label} (${types})`);
+ expect(input.attributes('disabled')).toBe('');
+ });
+ });
+
+ it('renders right arrow next to each input', () => {
+ alertFields.forEach((field, index) => {
+ const arrow = findColumnInRow(index + 1, 1).find('.right-arrow');
+ expect(arrow.exists()).toBe(true);
+ });
+ });
+
+ it('renders mapping dropdown for each field', () => {
+ alertFields.forEach(({ types }, index) => {
+ const dropdown = findColumnInRow(index + 1, 2).find(GlDropdown);
+ const searchBox = dropdown.findComponent(GlSearchBoxByType);
+ const dropdownItems = dropdown.findAllComponents(GlDropdownItem);
+ const { nodes } = parsedMapping.samplePayload.payloadAlerFields;
+ const mappingOptions = nodes.filter(({ type }) => types.includes(type));
+
+ expect(dropdown.exists()).toBe(true);
+ expect(searchBox.exists()).toBe(true);
+ expect(dropdownItems).toHaveLength(mappingOptions.length);
+ });
+ });
+
+ it('renders fallback dropdown only for the fields that have fallback', () => {
+ alertFields.forEach(({ types, numberOfFallbacks }, index) => {
+ const dropdown = findColumnInRow(index + 1, 3).find(GlDropdown);
+ expect(dropdown.exists()).toBe(Boolean(numberOfFallbacks));
+
+ if (numberOfFallbacks) {
+ const searchBox = dropdown.findComponent(GlSearchBoxByType);
+ const dropdownItems = dropdown.findAllComponents(GlDropdownItem);
+ const { nodes } = parsedMapping.samplePayload.payloadAlerFields;
+ const mappingOptions = nodes.filter(({ type }) => types.includes(type));
+
+ expect(searchBox.exists()).toBe(Boolean(numberOfFallbacks));
+ expect(dropdownItems).toHaveLength(mappingOptions.length);
+ }
+ });
+ });
+
+ it('emits event with selected mapping', () => {
+ const mappingToSave = { fieldName: 'TITLE', mapping: 'PARSED_TITLE' };
+ jest.spyOn(transformationUtils, 'transformForSave').mockReturnValue(mappingToSave);
+ const dropdown = findColumnInRow(1, 2).find(GlDropdown);
+ const option = dropdown.find(GlDropdownItem);
+ option.vm.$emit('click');
+ expect(wrapper.emitted('onMappingUpdate')[0]).toEqual([mappingToSave]);
+ });
+});
diff --git a/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js b/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js
new file mode 100644
index 00000000000..c43d78a1cf3
--- /dev/null
+++ b/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js
@@ -0,0 +1,118 @@
+import { GlTable, GlIcon, GlButton } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
+import AlertIntegrationsList, {
+ i18n,
+} from '~/alerts_settings/components/alerts_integrations_list.vue';
+import { trackAlertIntegrationsViewsOptions } from '~/alerts_settings/constants';
+import Tracking from '~/tracking';
+
+const mockIntegrations = [
+ {
+ id: '1',
+ active: true,
+ name: 'Integration 1',
+ type: 'HTTP endpoint',
+ },
+ {
+ id: '2',
+ active: false,
+ name: 'Integration 2',
+ type: 'HTTP endpoint',
+ },
+];
+
+describe('AlertIntegrationsList', () => {
+ let wrapper;
+ const { trigger: triggerIntersection } = useMockIntersectionObserver();
+
+ function mountComponent({ data = {}, props = {} } = {}) {
+ wrapper = mount(AlertIntegrationsList, {
+ data() {
+ return { ...data };
+ },
+ propsData: {
+ integrations: mockIntegrations,
+ ...props,
+ },
+ stubs: {
+ GlIcon: true,
+ GlButton: true,
+ },
+ });
+ }
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ const findTableComponent = () => wrapper.find(GlTable);
+ const findTableComponentRows = () => wrapper.find(GlTable).findAll('table tbody tr');
+ const finsStatusCell = () => wrapper.findAll('[data-testid="integration-activated-status"]');
+
+ it('renders a table', () => {
+ expect(findTableComponent().exists()).toBe(true);
+ });
+
+ it('renders an empty state when no integrations provided', () => {
+ mountComponent({ props: { integrations: [] } });
+ expect(findTableComponent().text()).toContain(i18n.emptyState);
+ });
+
+ it('renders an an edit and delete button for each integration', () => {
+ expect(findTableComponent().findAll(GlButton).length).toBe(4);
+ });
+
+ it('renders an highlighted row when a current integration is selected to edit', () => {
+ mountComponent({ data: { currentIntegration: { id: '1' } } });
+ expect(findTableComponentRows().at(0).classes()).toContain('gl-bg-blue-50');
+ });
+
+ describe('integration status', () => {
+ it('enabled', () => {
+ const cell = finsStatusCell().at(0);
+ const activatedIcon = cell.find(GlIcon);
+ expect(cell.text()).toBe(i18n.status.enabled.name);
+ expect(activatedIcon.attributes('name')).toBe('check-circle-filled');
+ expect(activatedIcon.attributes('title')).toBe(i18n.status.enabled.tooltip);
+ });
+
+ it('disabled', () => {
+ const cell = finsStatusCell().at(1);
+ const notActivatedIcon = cell.find(GlIcon);
+ expect(cell.text()).toBe(i18n.status.disabled.name);
+ expect(notActivatedIcon.attributes('name')).toBe('warning-solid');
+ expect(notActivatedIcon.attributes('title')).toBe(i18n.status.disabled.tooltip);
+ });
+ });
+
+ describe('Snowplow tracking', () => {
+ beforeEach(() => {
+ mountComponent();
+ jest.spyOn(Tracking, 'event');
+ });
+
+ it('should NOT track alert list page views when list is collapsed', () => {
+ triggerIntersection(wrapper.vm.$el, { entry: { isIntersecting: false } });
+
+ expect(Tracking.event).not.toHaveBeenCalled();
+ });
+
+ it('should track alert list page views only once when list is expanded', () => {
+ triggerIntersection(wrapper.vm.$el, { entry: { isIntersecting: true } });
+ triggerIntersection(wrapper.vm.$el, { entry: { isIntersecting: true } });
+ triggerIntersection(wrapper.vm.$el, { entry: { isIntersecting: true } });
+
+ const { category, action } = trackAlertIntegrationsViewsOptions;
+ expect(Tracking.event).toHaveBeenCalledTimes(1);
+ expect(Tracking.event).toHaveBeenCalledWith(category, action);
+ });
+ });
+});
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
new file mode 100644
index 00000000000..02229b3d3da
--- /dev/null
+++ b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
@@ -0,0 +1,385 @@
+import {
+ GlForm,
+ GlFormSelect,
+ GlCollapse,
+ GlFormInput,
+ GlToggle,
+ GlFormTextarea,
+} from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import MappingBuilder from '~/alerts_settings/components/alert_mapping_builder.vue';
+import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue';
+import { typeSet } from '~/alerts_settings/constants';
+import alertFields from '../mocks/alertFields.json';
+import { defaultAlertSettingsConfig } from './util';
+
+describe('AlertsSettingsForm', () => {
+ let wrapper;
+ const mockToastShow = jest.fn();
+
+ const createComponent = ({
+ data = {},
+ props = {},
+ multipleHttpIntegrationsCustomMapping = false,
+ multiIntegrations = true,
+ } = {}) => {
+ wrapper = mount(AlertsSettingsForm, {
+ data() {
+ return { ...data };
+ },
+ propsData: {
+ loading: false,
+ canAddIntegration: true,
+ ...props,
+ },
+ provide: {
+ ...defaultAlertSettingsConfig,
+ glFeatures: { multipleHttpIntegrationsCustomMapping },
+ multiIntegrations,
+ },
+ mocks: {
+ $toast: {
+ show: mockToastShow,
+ },
+ },
+ });
+ };
+
+ const findForm = () => wrapper.find(GlForm);
+ const findSelect = () => wrapper.find(GlFormSelect);
+ const findFormSteps = () => wrapper.find(GlCollapse);
+ const findFormFields = () => wrapper.findAll(GlFormInput);
+ const findFormToggle = () => wrapper.find(GlToggle);
+ const findTestPayloadSection = () => wrapper.find(`[id = "test-integration"]`);
+ const findMappingBuilderSection = () => wrapper.find(`[id = "mapping-builder"]`);
+ const findMappingBuilder = () => wrapper.findComponent(MappingBuilder);
+ const findSubmitButton = () => wrapper.find(`[type = "submit"]`);
+ const findMultiSupportText = () =>
+ wrapper.find(`[data-testid="multi-integrations-not-supported"]`);
+ const findJsonTestSubmit = () => wrapper.find(`[data-testid="integration-test-and-submit"]`);
+ const findJsonTextArea = () => wrapper.find(`[id = "test-payload"]`);
+ const findActionBtn = () => wrapper.find(`[data-testid="payload-action-btn"]`);
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ const selectOptionAtIndex = async (index) => {
+ const options = findSelect().findAll('option');
+ await options.at(index).setSelected();
+ };
+
+ const enableIntegration = (index, value) => {
+ findFormFields().at(index).setValue(value);
+ findFormToggle().trigger('click');
+ };
+
+ describe('with default values', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the initial template', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('render the initial form with only an integration type dropdown', () => {
+ expect(findForm().exists()).toBe(true);
+ expect(findSelect().exists()).toBe(true);
+ expect(findMultiSupportText().exists()).toBe(false);
+ expect(findFormSteps().attributes('visible')).toBeUndefined();
+ });
+
+ it('shows the rest of the form when the dropdown is used', async () => {
+ await selectOptionAtIndex(1);
+
+ expect(findFormFields().at(0).isVisible()).toBe(true);
+ });
+
+ it('disables the dropdown and shows help text when multi integrations are not supported', async () => {
+ createComponent({ props: { canAddIntegration: false } });
+ expect(findSelect().attributes('disabled')).toBe('disabled');
+ expect(findMultiSupportText().exists()).toBe(true);
+ });
+
+ it('disabled the name input when the selected value is prometheus', async () => {
+ createComponent();
+ await selectOptionAtIndex(2);
+
+ expect(findFormFields().at(0).attributes('disabled')).toBe('disabled');
+ });
+ });
+
+ describe('submitting integration form', () => {
+ describe('HTTP', () => {
+ it('create', async () => {
+ createComponent();
+
+ const integrationName = 'Test integration';
+ await selectOptionAtIndex(1);
+ enableIntegration(0, integrationName);
+
+ const submitBtn = findSubmitButton();
+ expect(submitBtn.exists()).toBe(true);
+ expect(submitBtn.text()).toBe('Save integration');
+
+ findForm().trigger('submit');
+
+ expect(wrapper.emitted('create-new-integration')[0]).toEqual([
+ { type: typeSet.http, variables: { name: integrationName, active: true } },
+ ]);
+ });
+
+ it('create with custom mapping', async () => {
+ createComponent({
+ multipleHttpIntegrationsCustomMapping: true,
+ multiIntegrations: true,
+ props: { alertFields },
+ });
+
+ const integrationName = 'Test integration';
+ await selectOptionAtIndex(1);
+
+ enableIntegration(0, integrationName);
+
+ const sampleMapping = { field: 'test' };
+ findMappingBuilder().vm.$emit('onMappingUpdate', sampleMapping);
+ findForm().trigger('submit');
+
+ expect(wrapper.emitted('create-new-integration')[0]).toEqual([
+ {
+ type: typeSet.http,
+ variables: {
+ name: integrationName,
+ active: true,
+ payloadAttributeMappings: sampleMapping,
+ payloadExample: null,
+ },
+ },
+ ]);
+ });
+
+ it('update', () => {
+ createComponent({
+ data: {
+ selectedIntegration: typeSet.http,
+ currentIntegration: { id: '1', name: 'Test integration pre' },
+ },
+ props: {
+ loading: false,
+ },
+ });
+ const updatedIntegrationName = 'Test integration post';
+ enableIntegration(0, updatedIntegrationName);
+
+ const submitBtn = findSubmitButton();
+ expect(submitBtn.exists()).toBe(true);
+ expect(submitBtn.text()).toBe('Save integration');
+
+ findForm().trigger('submit');
+
+ expect(wrapper.emitted('update-integration')[0]).toEqual([
+ { type: typeSet.http, variables: { name: updatedIntegrationName, active: true } },
+ ]);
+ });
+ });
+
+ describe('PROMETHEUS', () => {
+ it('create', async () => {
+ createComponent();
+
+ await selectOptionAtIndex(2);
+
+ const apiUrl = 'https://test.com';
+ enableIntegration(1, apiUrl);
+
+ findFormToggle().trigger('click');
+
+ const submitBtn = findSubmitButton();
+ expect(submitBtn.exists()).toBe(true);
+ expect(submitBtn.text()).toBe('Save integration');
+
+ findForm().trigger('submit');
+
+ expect(wrapper.emitted('create-new-integration')[0]).toEqual([
+ { type: typeSet.prometheus, variables: { apiUrl, active: true } },
+ ]);
+ });
+
+ it('update', () => {
+ createComponent({
+ data: {
+ selectedIntegration: typeSet.prometheus,
+ currentIntegration: { id: '1', apiUrl: 'https://test-pre.com' },
+ },
+ props: {
+ loading: false,
+ },
+ });
+
+ const apiUrl = 'https://test-post.com';
+ enableIntegration(1, apiUrl);
+
+ const submitBtn = findSubmitButton();
+ expect(submitBtn.exists()).toBe(true);
+ expect(submitBtn.text()).toBe('Save integration');
+
+ findForm().trigger('submit');
+
+ expect(wrapper.emitted('update-integration')[0]).toEqual([
+ { type: typeSet.prometheus, variables: { apiUrl, active: true } },
+ ]);
+ });
+ });
+ });
+
+ describe('submitting the integration with a JSON test payload', () => {
+ beforeEach(() => {
+ createComponent({
+ data: {
+ selectedIntegration: typeSet.http,
+ currentIntegration: { id: '1', name: 'Test' },
+ active: true,
+ },
+ props: {
+ loading: false,
+ },
+ });
+ });
+
+ it('should not allow a user to test invalid JSON', async () => {
+ jest.useFakeTimers();
+ await findJsonTextArea().setValue('Invalid JSON');
+
+ jest.runAllTimers();
+ await wrapper.vm.$nextTick();
+
+ const jsonTestSubmit = findJsonTestSubmit();
+ expect(jsonTestSubmit.exists()).toBe(true);
+ expect(jsonTestSubmit.text()).toBe('Save and test payload');
+ expect(jsonTestSubmit.props('disabled')).toBe(true);
+ });
+
+ it('should allow for the form to be automatically saved if the test payload is successfully submitted', async () => {
+ jest.useFakeTimers();
+ await findJsonTextArea().setValue('{ "value": "value" }');
+
+ jest.runAllTimers();
+ await wrapper.vm.$nextTick();
+ expect(findJsonTestSubmit().props('disabled')).toBe(false);
+ });
+ });
+
+ describe('Test payload section for HTTP integration', () => {
+ beforeEach(() => {
+ createComponent({
+ multipleHttpIntegrationsCustomMapping: true,
+ props: {
+ currentIntegration: {
+ type: typeSet.http,
+ },
+ alertFields,
+ },
+ });
+ });
+
+ describe.each`
+ active | resetSamplePayloadConfirmed | disabled
+ ${true} | ${true} | ${undefined}
+ ${false} | ${true} | ${'disabled'}
+ ${true} | ${false} | ${'disabled'}
+ ${false} | ${false} | ${'disabled'}
+ `('', ({ active, resetSamplePayloadConfirmed, disabled }) => {
+ const payloadResetMsg = resetSamplePayloadConfirmed ? 'was confirmed' : 'was not confirmed';
+ const enabledState = disabled === 'disabled' ? 'disabled' : 'enabled';
+ const activeState = active ? 'active' : 'not active';
+
+ it(`textarea should be ${enabledState} when payload reset ${payloadResetMsg} and current integration is ${activeState}`, async () => {
+ wrapper.setData({
+ customMapping: { samplePayload: true },
+ active,
+ resetSamplePayloadConfirmed,
+ });
+ await wrapper.vm.$nextTick();
+ expect(findTestPayloadSection().find(GlFormTextarea).attributes('disabled')).toBe(disabled);
+ });
+ });
+
+ describe('action buttons for sample payload', () => {
+ describe.each`
+ resetSamplePayloadConfirmed | samplePayload | caption
+ ${false} | ${true} | ${'Edit payload'}
+ ${true} | ${false} | ${'Submit payload'}
+ ${true} | ${true} | ${'Submit payload'}
+ ${false} | ${false} | ${'Submit payload'}
+ `('', ({ resetSamplePayloadConfirmed, samplePayload, caption }) => {
+ const samplePayloadMsg = samplePayload ? 'was provided' : 'was not provided';
+ const payloadResetMsg = resetSamplePayloadConfirmed ? 'was confirmed' : 'was not confirmed';
+
+ it(`shows ${caption} button when sample payload ${samplePayloadMsg} and payload reset ${payloadResetMsg}`, async () => {
+ wrapper.setData({
+ selectedIntegration: typeSet.http,
+ customMapping: { samplePayload },
+ resetSamplePayloadConfirmed,
+ });
+ await wrapper.vm.$nextTick();
+ expect(findActionBtn().text()).toBe(caption);
+ });
+ });
+ });
+
+ describe('Parsing payload', () => {
+ it('displays a toast message on successful parse', async () => {
+ jest.useFakeTimers();
+ wrapper.setData({
+ selectedIntegration: typeSet.http,
+ customMapping: { samplePayload: false },
+ });
+ await wrapper.vm.$nextTick();
+
+ findActionBtn().vm.$emit('click');
+ jest.advanceTimersByTime(1000);
+
+ await waitForPromises();
+
+ expect(mockToastShow).toHaveBeenCalledWith(
+ 'Sample payload has been parsed. You can now map the fields.',
+ );
+ });
+ });
+ });
+
+ describe('Mapping builder section', () => {
+ describe.each`
+ alertFieldsProvided | multiIntegrations | featureFlag | integrationOption | visible
+ ${true} | ${true} | ${true} | ${1} | ${true}
+ ${true} | ${true} | ${true} | ${2} | ${false}
+ ${true} | ${true} | ${false} | ${1} | ${false}
+ ${true} | ${true} | ${false} | ${2} | ${false}
+ ${true} | ${false} | ${true} | ${1} | ${false}
+ ${false} | ${true} | ${true} | ${1} | ${false}
+ `('', ({ alertFieldsProvided, multiIntegrations, featureFlag, integrationOption, visible }) => {
+ const visibleMsg = visible ? 'is rendered' : 'is not rendered';
+ const featureFlagMsg = featureFlag ? 'is enabled' : 'is disabled';
+ const alertFieldsMsg = alertFieldsProvided ? 'are provided' : 'are not provided';
+ const integrationType = integrationOption === 1 ? typeSet.http : typeSet.prometheus;
+
+ it(`${visibleMsg} when multipleHttpIntegrationsCustomMapping feature flag ${featureFlagMsg} and integration type is ${integrationType} and alert fields ${alertFieldsMsg}`, async () => {
+ createComponent({
+ multipleHttpIntegrationsCustomMapping: featureFlag,
+ multiIntegrations,
+ props: {
+ alertFields: alertFieldsProvided ? alertFields : [],
+ },
+ });
+ await selectOptionAtIndex(integrationOption);
+
+ expect(findMappingBuilderSection().exists()).toBe(visible);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
new file mode 100644
index 00000000000..80293597ab6
--- /dev/null
+++ b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
@@ -0,0 +1,379 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { mount, createLocalVue } from '@vue/test-utils';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
+import waitForPromises from 'helpers/wait_for_promises';
+import IntegrationsList from '~/alerts_settings/components/alerts_integrations_list.vue';
+import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue';
+import AlertsSettingsWrapper from '~/alerts_settings/components/alerts_settings_wrapper.vue';
+import { typeSet } from '~/alerts_settings/constants';
+import createHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql';
+import createPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/create_prometheus_integration.mutation.graphql';
+import destroyHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql';
+import resetHttpTokenMutation from '~/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql';
+import resetPrometheusTokenMutation from '~/alerts_settings/graphql/mutations/reset_prometheus_token.mutation.graphql';
+import updateHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql';
+import updatePrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/update_prometheus_integration.mutation.graphql';
+import getIntegrationsQuery from '~/alerts_settings/graphql/queries/get_integrations.query.graphql';
+import {
+ ADD_INTEGRATION_ERROR,
+ RESET_INTEGRATION_TOKEN_ERROR,
+ UPDATE_INTEGRATION_ERROR,
+ INTEGRATION_PAYLOAD_TEST_ERROR,
+ DELETE_INTEGRATION_ERROR,
+} from '~/alerts_settings/utils/error_messages';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import {
+ createHttpVariables,
+ updateHttpVariables,
+ createPrometheusVariables,
+ updatePrometheusVariables,
+ ID,
+ errorMsg,
+ getIntegrationsQueryResponse,
+ destroyIntegrationResponse,
+ integrationToDestroy,
+ destroyIntegrationResponseWithErrors,
+} from './mocks/apollo_mock';
+import mockIntegrations from './mocks/integrations.json';
+import { defaultAlertSettingsConfig } from './util';
+
+jest.mock('~/flash');
+
+const localVue = createLocalVue();
+
+describe('AlertsSettingsWrapper', () => {
+ let wrapper;
+ let fakeApollo;
+ let destroyIntegrationHandler;
+ useMockIntersectionObserver();
+
+ const findLoader = () => wrapper.find(IntegrationsList).find(GlLoadingIcon);
+ const findIntegrations = () => wrapper.find(IntegrationsList).findAll('table tbody tr');
+
+ async function destroyHttpIntegration(localWrapper) {
+ await jest.runOnlyPendingTimers();
+ await localWrapper.vm.$nextTick();
+
+ localWrapper
+ .find(IntegrationsList)
+ .vm.$emit('delete-integration', { id: integrationToDestroy.id });
+ }
+
+ async function awaitApolloDomMock() {
+ await wrapper.vm.$nextTick(); // kick off the DOM update
+ await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises)
+ await wrapper.vm.$nextTick(); // kick off the DOM update for flash
+ }
+
+ const createComponent = ({ data = {}, provide = {}, loading = false } = {}) => {
+ wrapper = mount(AlertsSettingsWrapper, {
+ data() {
+ return { ...data };
+ },
+ provide: {
+ ...defaultAlertSettingsConfig,
+ ...provide,
+ },
+ mocks: {
+ $apollo: {
+ mutate: jest.fn(),
+ query: jest.fn(),
+ queries: {
+ integrations: {
+ loading,
+ },
+ },
+ },
+ },
+ });
+ };
+
+ function createComponentWithApollo({
+ destroyHandler = jest.fn().mockResolvedValue(destroyIntegrationResponse),
+ } = {}) {
+ localVue.use(VueApollo);
+ destroyIntegrationHandler = destroyHandler;
+
+ const requestHandlers = [
+ [getIntegrationsQuery, jest.fn().mockResolvedValue(getIntegrationsQueryResponse)],
+ [destroyHttpIntegrationMutation, destroyIntegrationHandler],
+ ];
+
+ fakeApollo = createMockApollo(requestHandlers);
+
+ wrapper = mount(AlertsSettingsWrapper, {
+ localVue,
+ apolloProvider: fakeApollo,
+ provide: {
+ ...defaultAlertSettingsConfig,
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('rendered via default permissions', () => {
+ it('renders the GraphQL alerts integrations list and new form', () => {
+ createComponent();
+ expect(wrapper.find(IntegrationsList).exists()).toBe(true);
+ expect(wrapper.find(AlertsSettingsForm).exists()).toBe(true);
+ });
+
+ it('uses a loading state inside the IntegrationsList table', () => {
+ createComponent({
+ data: { integrations: {} },
+ loading: true,
+ });
+ expect(wrapper.find(IntegrationsList).exists()).toBe(true);
+ expect(findLoader().exists()).toBe(true);
+ });
+
+ it('renders the IntegrationsList table using the API data', () => {
+ createComponent({
+ data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
+ loading: false,
+ });
+ expect(findLoader().exists()).toBe(false);
+ expect(findIntegrations()).toHaveLength(mockIntegrations.length);
+ });
+
+ it('calls `$apollo.mutate` with `createHttpIntegrationMutation`', () => {
+ createComponent({
+ data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
+ loading: false,
+ });
+
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
+ data: { createHttpIntegrationMutation: { integration: { id: '1' } } },
+ });
+ wrapper.find(AlertsSettingsForm).vm.$emit('create-new-integration', {
+ type: typeSet.http,
+ variables: createHttpVariables,
+ });
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: createHttpIntegrationMutation,
+ update: expect.anything(),
+ variables: createHttpVariables,
+ });
+ });
+
+ it('calls `$apollo.mutate` with `updateHttpIntegrationMutation`', () => {
+ createComponent({
+ data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
+ loading: false,
+ });
+
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
+ data: { updateHttpIntegrationMutation: { integration: { id: '1' } } },
+ });
+ wrapper.find(AlertsSettingsForm).vm.$emit('update-integration', {
+ type: typeSet.http,
+ variables: updateHttpVariables,
+ });
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: updateHttpIntegrationMutation,
+ variables: updateHttpVariables,
+ });
+ });
+
+ it('calls `$apollo.mutate` with `resetHttpTokenMutation`', () => {
+ createComponent({
+ data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
+ loading: false,
+ });
+
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
+ data: { resetHttpTokenMutation: { integration: { id: '1' } } },
+ });
+ wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', {
+ type: typeSet.http,
+ variables: { id: ID },
+ });
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: resetHttpTokenMutation,
+ variables: {
+ id: ID,
+ },
+ });
+ });
+
+ it('calls `$apollo.mutate` with `createPrometheusIntegrationMutation`', () => {
+ createComponent({
+ data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
+ loading: false,
+ });
+
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
+ data: { createPrometheusIntegrationMutation: { integration: { id: '2' } } },
+ });
+ wrapper.find(AlertsSettingsForm).vm.$emit('create-new-integration', {
+ type: typeSet.prometheus,
+ variables: createPrometheusVariables,
+ });
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: createPrometheusIntegrationMutation,
+ update: expect.anything(),
+ variables: createPrometheusVariables,
+ });
+ });
+
+ it('calls `$apollo.mutate` with `updatePrometheusIntegrationMutation`', () => {
+ createComponent({
+ data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
+ loading: false,
+ });
+
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
+ data: { updatePrometheusIntegrationMutation: { integration: { id: '2' } } },
+ });
+ wrapper.find(AlertsSettingsForm).vm.$emit('update-integration', {
+ type: typeSet.prometheus,
+ variables: updatePrometheusVariables,
+ });
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: updatePrometheusIntegrationMutation,
+ variables: updatePrometheusVariables,
+ });
+ });
+
+ it('calls `$apollo.mutate` with `resetPrometheusTokenMutation`', () => {
+ createComponent({
+ data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
+ loading: false,
+ });
+
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
+ data: { resetPrometheusTokenMutation: { integration: { id: '1' } } },
+ });
+ wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', {
+ type: typeSet.prometheus,
+ variables: { id: ID },
+ });
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: resetPrometheusTokenMutation,
+ variables: {
+ id: ID,
+ },
+ });
+ });
+
+ it('shows an error alert when integration creation fails ', async () => {
+ createComponent({
+ data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
+ loading: false,
+ });
+
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(ADD_INTEGRATION_ERROR);
+ wrapper.find(AlertsSettingsForm).vm.$emit('create-new-integration', {});
+
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalledWith({ message: ADD_INTEGRATION_ERROR });
+ });
+
+ it('shows an error alert when integration token reset fails ', async () => {
+ createComponent({
+ data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
+ loading: false,
+ });
+
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(RESET_INTEGRATION_TOKEN_ERROR);
+
+ wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', {});
+
+ await waitForPromises();
+ expect(createFlash).toHaveBeenCalledWith({ message: RESET_INTEGRATION_TOKEN_ERROR });
+ });
+
+ it('shows an error alert when integration update fails ', async () => {
+ createComponent({
+ data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
+ loading: false,
+ });
+
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(errorMsg);
+
+ wrapper.find(AlertsSettingsForm).vm.$emit('update-integration', {});
+
+ await waitForPromises();
+ expect(createFlash).toHaveBeenCalledWith({ message: UPDATE_INTEGRATION_ERROR });
+ });
+
+ it('shows an error alert when integration test payload fails ', async () => {
+ const mock = new AxiosMockAdapter(axios);
+ mock.onPost(/(.*)/).replyOnce(403);
+ createComponent({
+ data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
+ loading: false,
+ });
+
+ return wrapper.vm.validateAlertPayload({ endpoint: '', data: '', token: '' }).then(() => {
+ expect(createFlash).toHaveBeenCalledWith({ message: INTEGRATION_PAYLOAD_TEST_ERROR });
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ mock.restore();
+ });
+ });
+ });
+
+ describe('with mocked Apollo client', () => {
+ it('has a selection of integrations loaded via the getIntegrationsQuery', async () => {
+ createComponentWithApollo();
+
+ await jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+
+ expect(findIntegrations()).toHaveLength(4);
+ });
+
+ it('calls a mutation with correct parameters and destroys a integration', async () => {
+ createComponentWithApollo();
+
+ await destroyHttpIntegration(wrapper);
+
+ expect(destroyIntegrationHandler).toHaveBeenCalled();
+
+ await wrapper.vm.$nextTick();
+
+ expect(findIntegrations()).toHaveLength(3);
+ });
+
+ it('displays flash if mutation had a recoverable error', async () => {
+ createComponentWithApollo({
+ destroyHandler: jest.fn().mockResolvedValue(destroyIntegrationResponseWithErrors),
+ });
+
+ await destroyHttpIntegration(wrapper);
+ await awaitApolloDomMock();
+
+ expect(createFlash).toHaveBeenCalledWith({ message: 'Houston, we have a problem' });
+ });
+
+ it('displays flash if mutation had a non-recoverable error', async () => {
+ createComponentWithApollo({
+ destroyHandler: jest.fn().mockRejectedValue('Error'),
+ });
+
+ await destroyHttpIntegration(wrapper);
+ await awaitApolloDomMock();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: DELETE_INTEGRATION_ERROR,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/alerts_settings/components/mocks/apollo_mock.js b/spec/frontend/alerts_settings/components/mocks/apollo_mock.js
new file mode 100644
index 00000000000..e0eba1e8421
--- /dev/null
+++ b/spec/frontend/alerts_settings/components/mocks/apollo_mock.js
@@ -0,0 +1,123 @@
+const projectPath = '';
+export const ID = 'gid://gitlab/AlertManagement::HttpIntegration/7';
+export const errorMsg = 'Something went wrong';
+
+export const createHttpVariables = {
+ name: 'Test Pre',
+ active: true,
+ projectPath,
+};
+
+export const updateHttpVariables = {
+ name: 'Test Pre',
+ active: true,
+ id: ID,
+};
+
+export const createPrometheusVariables = {
+ apiUrl: 'https://test-pre.com',
+ active: true,
+ projectPath,
+};
+
+export const updatePrometheusVariables = {
+ apiUrl: 'https://test-pre.com',
+ active: true,
+ id: ID,
+};
+
+export const getIntegrationsQueryResponse = {
+ data: {
+ project: {
+ alertManagementIntegrations: {
+ nodes: [
+ {
+ id: '37',
+ type: 'HTTP',
+ active: true,
+ name: 'Test 5',
+ url:
+ 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json',
+ token: '89eb01df471d990ff5162a1c640408cf',
+ apiUrl: null,
+ },
+ {
+ id: '41',
+ type: 'HTTP',
+ active: true,
+ name: 'Test 9999',
+ url:
+ 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-9999/b78a566e1776cfc2.json',
+ token: 'f7579aa03844e07af3b1f0fca3f79f81',
+ apiUrl: null,
+ },
+ {
+ id: '40',
+ type: 'HTTP',
+ active: true,
+ name: 'Test 6',
+ url:
+ 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-6/3e828ae28a240222.json',
+ token: '6536102a607a5dd74fcdde921f2349ee',
+ apiUrl: null,
+ },
+ {
+ id: '12',
+ type: 'PROMETHEUS',
+ active: false,
+ name: 'Prometheus',
+ url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/prometheus/alerts/notify.json',
+ token: '256f687c6225aa5d6ee50c3d68120c4c',
+ apiUrl: 'https://localhost.ieeeesassadasasa',
+ },
+ ],
+ },
+ },
+ },
+};
+
+export const integrationToDestroy = {
+ id: '37',
+ type: 'HTTP',
+ active: true,
+ name: 'Test 5',
+ url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json',
+ token: '89eb01df471d990ff5162a1c640408cf',
+ apiUrl: null,
+};
+
+export const destroyIntegrationResponse = {
+ data: {
+ httpIntegrationDestroy: {
+ errors: [],
+ integration: {
+ id: '37',
+ type: 'HTTP',
+ active: true,
+ name: 'Test 5',
+ url:
+ 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json',
+ token: '89eb01df471d990ff5162a1c640408cf',
+ apiUrl: null,
+ },
+ },
+ },
+};
+
+export const destroyIntegrationResponseWithErrors = {
+ data: {
+ httpIntegrationDestroy: {
+ errors: ['Houston, we have a problem'],
+ integration: {
+ id: '37',
+ type: 'HTTP',
+ active: true,
+ name: 'Test 5',
+ url:
+ 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json',
+ token: '89eb01df471d990ff5162a1c640408cf',
+ apiUrl: null,
+ },
+ },
+ },
+};
diff --git a/spec/frontend/alerts_settings/components/mocks/integrations.json b/spec/frontend/alerts_settings/components/mocks/integrations.json
new file mode 100644
index 00000000000..b1284fc55a2
--- /dev/null
+++ b/spec/frontend/alerts_settings/components/mocks/integrations.json
@@ -0,0 +1,38 @@
+[
+ {
+ "id": "gid://gitlab/AlertManagement::HttpIntegration/7",
+ "type": "HTTP",
+ "active": true,
+ "name": "test",
+ "url": "http://192.168.1.152:3000/root/autodevops/alerts/notify/test/eddd36969b2d3d6a.json",
+ "token": "7eb24af194116411ec8d66b58c6b0d2e",
+ "apiUrl": null
+ },
+ {
+ "id": "gid://gitlab/AlertManagement::HttpIntegration/6",
+ "type": "HTTP",
+ "active": false,
+ "name": "test",
+ "url": "http://192.168.1.152:3000/root/autodevops/alerts/notify/test/abce123.json",
+ "token": "8639e0ce06c731b00ee3e8dcdfd14fe0",
+ "apiUrl": null
+ },
+ {
+ "id": "gid://gitlab/AlertManagement::HttpIntegration/5",
+ "type": "HTTP",
+ "active": false,
+ "name": "test",
+ "url": "http://192.168.1.152:3000/root/autodevops/alerts/notify/test/bcd64c85f918a2e2.json",
+ "token": "5c8101533d970a55d5c105f8abff2192",
+ "apiUrl": null
+ },
+ {
+ "id": "gid://gitlab/PrometheusService/12",
+ "type": "PROMETHEUS",
+ "active": true,
+ "name": "Prometheus",
+ "url": "http://192.168.1.152:3000/root/autodevops/prometheus/alerts/notify.json",
+ "token": "0b18c37caa8fe980799b349916fe5ddf",
+ "apiUrl": "https://another-url-2.com"
+ }
+]
diff --git a/spec/frontend/alerts_settings/components/util.js b/spec/frontend/alerts_settings/components/util.js
new file mode 100644
index 00000000000..5c07f22f1c9
--- /dev/null
+++ b/spec/frontend/alerts_settings/components/util.js
@@ -0,0 +1,24 @@
+const PROMETHEUS_URL = '/prometheus/alerts/notify.json';
+const GENERIC_URL = '/alerts/notify.json';
+const KEY = 'abcedfg123';
+const INVALID_URL = 'http://invalid';
+const ACTIVE = false;
+
+export const defaultAlertSettingsConfig = {
+ generic: {
+ authorizationKey: KEY,
+ formPath: INVALID_URL,
+ url: GENERIC_URL,
+ alertsSetupUrl: INVALID_URL,
+ alertsUsageUrl: INVALID_URL,
+ active: ACTIVE,
+ },
+ prometheus: {
+ authorizationKey: KEY,
+ prometheusFormPath: INVALID_URL,
+ url: PROMETHEUS_URL,
+ active: ACTIVE,
+ },
+ projectPath: '',
+ multiIntegrations: true,
+};