diff options
Diffstat (limited to 'spec/frontend/alerts_settings')
10 files changed, 1540 insertions, 0 deletions
diff --git a/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_new_spec.js.snap b/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_new_spec.js.snap new file mode 100644 index 00000000000..e2ef7483316 --- /dev/null +++ b/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_new_spec.js.snap @@ -0,0 +1,97 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AlertsSettingsFormNew 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 id=\\"integration-type\\" role=\\"group\\" class=\\"form-group gl-form-group\\"><label id=\\"integration-type__BV_label_\\" for=\\"integration-type\\" class=\\"d-block col-form-label\\">1. Select integration type</label> + <div class=\\"bv-no-focus-ring\\"><select class=\\"gl-form-select custom-select\\" id=\\"__BVID__8\\"> + <option value=\\"\\">Select integration type</option> + <option value=\\"HTTP\\">HTTP Endpoint</option> + <option value=\\"PROMETHEUS\\">External Prometheus</option> + <option value=\\"OPSGENIE\\">Opsgenie</option> + </select> + <!----> + <!----> + <!----> + <!----> + </div> + </div> + <div class=\\"gl-mt-3 collapse\\" style=\\"display: none;\\" id=\\"__BVID__10\\"> + <div> + <div id=\\"name-integration\\" role=\\"group\\" class=\\"form-group gl-form-group\\"><label id=\\"name-integration__BV_label_\\" for=\\"name-integration\\" class=\\"d-block col-form-label\\">2. Name integration</label> + <div class=\\"bv-no-focus-ring\\"><input type=\\"text\\" placeholder=\\"Enter integration name\\" class=\\"gl-form-input form-control\\" id=\\"__BVID__15\\"> + <!----> + <!----> + <!----> + </div> + </div> + <div id=\\"integration-webhook\\" role=\\"group\\" class=\\"form-group gl-form-group\\"><label id=\\"integration-webhook__BV_label_\\" for=\\"integration-webhook\\" class=\\"d-block col-form-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 rel=\\"noopener noreferrer\\" target=\\"_blank\\" href=\\"https://docs.gitlab.com/ee/operations/incident_management/alert_integrations.html\\" class=\\"gl-link gl-display-inline-block\\">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\\"> + <div class=\\"gl-toggle-wrapper\\"><span class=\\"gl-toggle-label\\">Active</span> + <!----> <button aria-label=\\"Active\\" type=\\"button\\" class=\\"gl-toggle\\"><span class=\\"toggle-icon\\"><svg data-testid=\\"close-icon\\" class=\\"gl-icon s16\\"><use href=\\"#close\\"></use></svg></span></button></div> + <!----> + </label> + <!----> + <div class=\\"gl-my-4\\"><span class=\\"gl-font-weight-bold\\"> + Webhook URL + </span> + <div id=\\"url\\" readonly=\\"readonly\\"> + <div role=\\"group\\" class=\\"input-group\\"> + <!----> + <!----> <input id=\\"url\\" type=\\"text\\" readonly=\\"readonly\\" class=\\"gl-form-input form-control\\"> + <div class=\\"input-group-append\\"><button title=\\"Copy\\" data-clipboard-text=\\"\\" aria-label=\\"Copy this value\\" type=\\"button\\" class=\\"btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon\\"> + <!----> <svg data-testid=\\"copy-to-clipboard-icon\\" class=\\"gl-button-icon gl-icon s16\\"> + <use href=\\"#copy-to-clipboard\\"></use> + </svg> + <!----></button></div> + <!----> + </div> + </div> + </div> + <div class=\\"gl-my-4\\"><span class=\\"gl-font-weight-bold\\"> + Authorization key + </span> + <div id=\\"authorization-key\\" readonly=\\"readonly\\" class=\\"gl-mb-3\\"> + <div role=\\"group\\" class=\\"input-group\\"> + <!----> + <!----> <input id=\\"authorization-key\\" type=\\"text\\" readonly=\\"readonly\\" class=\\"gl-form-input form-control\\"> + <div class=\\"input-group-append\\"><button title=\\"Copy\\" data-clipboard-text=\\"\\" aria-label=\\"Copy this value\\" type=\\"button\\" class=\\"btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon\\"> + <!----> <svg data-testid=\\"copy-to-clipboard-icon\\" class=\\"gl-button-icon gl-icon s16\\"> + <use href=\\"#copy-to-clipboard\\"></use> + </svg> + <!----></button></div> + <!----> + </div> + </div> <button type=\\"button\\" disabled=\\"disabled\\" class=\\"btn btn-default btn-md disabled gl-button\\"> + <!----> + <!----> <span class=\\"gl-button-text\\"> + Reset Key + </span></button> + <!----> + </div> + <!----> + <!----> + <!----> + </div> + </div> + <div id=\\"test-integration\\" role=\\"group\\" class=\\"form-group gl-form-group\\"><label id=\\"test-integration__BV_label_\\" for=\\"test-integration\\" class=\\"d-block col-form-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 id=\\"test-payload\\" disabled=\\"disabled\\" placeholder=\\"{ "events": [{ "application": "Name of application" }] }\\" wrap=\\"soft\\" class=\\"gl-form-input gl-form-textarea gl-my-3 form-control is-valid\\" style=\\"resize: none; overflow-y: scroll;\\"></textarea> + <!----> + <!----> + <!----> + </div> + </div> + <!----> + <!----> + </div> + <div class=\\"gl-display-flex gl-justify-content-start gl-py-3\\"><button data-testid=\\"integration-form-submit\\" type=\\"submit\\" class=\\"btn js-no-auto-disable btn-success btn-md gl-button\\"> + <!----> + <!----> <span class=\\"gl-button-text\\">Save integration + </span></button> <button data-testid=\\"integration-test-and-submit\\" type=\\"button\\" class=\\"btn gl-mx-3 js-no-auto-disable btn-success btn-md gl-button btn-success-secondary\\"> + <!----> + <!----> <span class=\\"gl-button-text\\">Save and test payload</span></button> <button type=\\"reset\\" class=\\"btn js-no-auto-disable btn-default btn-md gl-button\\"> + <!----> + <!----> <span class=\\"gl-button-text\\">Cancel</span></button></div> + </div> +</form>" +`; diff --git a/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_old_spec.js.snap b/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_old_spec.js.snap new file mode 100644 index 00000000000..9306bf24baf --- /dev/null +++ b/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_old_spec.js.snap @@ -0,0 +1,47 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AlertsSettingsFormOld with default values renders the initial template 1`] = ` +"<gl-form-stub> + <h5 class=\\"gl-font-lg gl-my-5\\"></h5> + <!----> + <div data-testid=\\"alert-settings-description\\"> + <p> + <gl-sprintf-stub message=\\"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.\\"></gl-sprintf-stub> + </p> + <p> + <gl-sprintf-stub message=\\"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.\\"></gl-sprintf-stub> + </p> + </div> + <gl-form-group-stub label-for=\\"integration-type\\" label=\\"Integration\\"> + <gl-form-select-stub id=\\"integration-type\\" options=\\"[object Object],[object Object],[object Object]\\" data-testid=\\"alert-settings-select\\" value=\\"HTTP\\"></gl-form-select-stub> <span class=\\"gl-text-gray-500\\"><gl-sprintf-stub message=\\"Learn more about our our upcoming %{linkStart}integrations%{linkEnd}\\"></gl-sprintf-stub></span> + </gl-form-group-stub> + <gl-form-group-stub label=\\"Active\\" label-for=\\"active\\"> + <toggle-button-stub id=\\"active\\"></toggle-button-stub> + </gl-form-group-stub> + <!----> + <gl-form-group-stub label=\\"Webhook URL\\" label-for=\\"url\\"> + <gl-form-input-group-stub value=\\"/alerts/notify.json\\" predefinedoptions=\\"[object Object]\\" id=\\"url\\" readonly=\\"\\"></gl-form-input-group-stub> <span class=\\"gl-text-gray-500\\"> + + </span> + </gl-form-group-stub> + <gl-form-group-stub label-for=\\"authorization-key\\"> + <gl-form-input-group-stub value=\\"\\" predefinedoptions=\\"[object Object]\\" id=\\"authorization-key\\" readonly=\\"\\" class=\\"gl-mb-2\\"></gl-form-input-group-stub> + <gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\" class=\\"gl-mt-3\\" role=\\"button\\" tabindex=\\"0\\">Reset key</gl-button-stub> + <gl-modal-stub modalid=\\"tokenModal\\" 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> + <gl-form-group-stub label=\\"Alert test payload\\" label-for=\\"alert-json\\"> + <gl-form-textarea-stub noresize=\\"true\\" id=\\"alert-json\\" disabled=\\"true\\" state=\\"true\\" placeholder=\\"Enter test alert JSON....\\" rows=\\"6\\" max-rows=\\"10\\"></gl-form-textarea-stub> + </gl-form-group-stub> + <gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\">Test alert payload</gl-button-stub> + <div class=\\"footer-block row-content-block gl-display-flex gl-justify-content-space-between\\"> + <gl-button-stub category=\\"primary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\"> + Save changes + </gl-button-stub> + <gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\"> + Cancel + </gl-button-stub> + </div> +</gl-form-stub>" +`; diff --git a/spec/frontend/alerts_settings/alert_mapping_builder_spec.js b/spec/frontend/alerts_settings/alert_mapping_builder_spec.js new file mode 100644 index 00000000000..12536c27dfe --- /dev/null +++ b/spec/frontend/alerts_settings/alert_mapping_builder_spec.js @@ -0,0 +1,97 @@ +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 gitlabFields from '~/alerts_settings/components/mocks/gitlabFields.json'; +import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json'; + +describe('AlertMappingBuilder', () => { + let wrapper; + + function mountComponent() { + wrapper = shallowMount(AlertMappingBuilder, { + propsData: { + payloadFields: parsedMapping.samplePayload.payloadAlerFields.nodes, + mapping: parsedMapping.storedMapping.nodes, + }, + }); + } + + 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', () => { + gitlabFields.forEach((field, index) => { + const input = findColumnInRow(index + 1, 0).find(GlFormInput); + expect(input.attributes('value')).toBe(`${field.label} (${field.type.join(' or ')})`); + expect(input.attributes('disabled')).toBe(''); + }); + }); + + it('renders right arrow next to each input', () => { + gitlabFields.forEach((field, index) => { + const arrow = findColumnInRow(index + 1, 1).find('.right-arrow'); + expect(arrow.exists()).toBe(true); + }); + }); + + it('renders mapping dropdown for each field', () => { + gitlabFields.forEach(({ compatibleTypes }, index) => { + const dropdown = findColumnInRow(index + 1, 2).find(GlDropdown); + const searchBox = dropdown.find(GlSearchBoxByType); + const dropdownItems = dropdown.findAll(GlDropdownItem); + const { nodes } = parsedMapping.samplePayload.payloadAlerFields; + const numberOfMappingOptions = nodes.filter(({ type }) => + type.some(t => compatibleTypes.includes(t)), + ); + + expect(dropdown.exists()).toBe(true); + expect(searchBox.exists()).toBe(true); + expect(dropdownItems).toHaveLength(numberOfMappingOptions.length); + }); + }); + + it('renders fallback dropdown only for the fields that have fallback', () => { + gitlabFields.forEach(({ compatibleTypes, numberOfFallbacks }, index) => { + const dropdown = findColumnInRow(index + 1, 3).find(GlDropdown); + expect(dropdown.exists()).toBe(Boolean(numberOfFallbacks)); + + if (numberOfFallbacks) { + const searchBox = dropdown.find(GlSearchBoxByType); + const dropdownItems = dropdown.findAll(GlDropdownItem); + const { nodes } = parsedMapping.samplePayload.payloadAlerFields; + const numberOfMappingOptions = nodes.filter(({ type }) => + type.some(t => compatibleTypes.includes(t)), + ); + + expect(searchBox.exists()).toBe(Boolean(numberOfFallbacks)); + expect(dropdownItems).toHaveLength(numberOfMappingOptions.length); + } + }); + }); +}); diff --git a/spec/frontend/alerts_settings/alerts_integrations_list_spec.js b/spec/frontend/alerts_settings/alerts_integrations_list_spec.js new file mode 100644 index 00000000000..90bb38f0c2b --- /dev/null +++ b/spec/frontend/alerts_settings/alerts_integrations_list_spec.js @@ -0,0 +1,125 @@ +import { GlTable, GlIcon, GlButton } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import { useMockIntersectionObserver } from 'helpers/mock_dom_observer'; +import Tracking from '~/tracking'; +import AlertIntegrationsList, { + i18n, +} from '~/alerts_settings/components/alerts_integrations_list.vue'; +import { trackAlertIntegrationsViewsOptions } from '~/alerts_settings/constants'; + +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, + }, + provide: { + glFeatures: { httpIntegrationsList: true }, + }, + 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/alerts_settings_form_new_spec.js b/spec/frontend/alerts_settings/alerts_settings_form_new_spec.js new file mode 100644 index 00000000000..fbd482b1906 --- /dev/null +++ b/spec/frontend/alerts_settings/alerts_settings_form_new_spec.js @@ -0,0 +1,364 @@ +import { mount } from '@vue/test-utils'; +import { + GlForm, + GlFormSelect, + GlCollapse, + GlFormInput, + GlToggle, + GlFormTextarea, +} from '@gitlab/ui'; +import waitForPromises from 'helpers/wait_for_promises'; +import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form_new.vue'; +import { defaultAlertSettingsConfig } from './util'; +import { typeSet } from '~/alerts_settings/constants'; + +describe('AlertsSettingsFormNew', () => { + let wrapper; + const mockToastShow = jest.fn(); + + const createComponent = ({ + data = {}, + props = {}, + multipleHttpIntegrationsCustomMapping = false, + } = {}) => { + wrapper = mount(AlertsSettingsForm, { + data() { + return { ...data }; + }, + propsData: { + loading: false, + canAddIntegration: true, + canManageOpsgenie: true, + ...props, + }, + provide: { + glFeatures: { multipleHttpIntegrationsCustomMapping }, + ...defaultAlertSettingsConfig, + }, + 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 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; + } + }); + + describe('with default values', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders the initial template', () => { + expect(wrapper.html()).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 () => { + const options = findSelect().findAll('option'); + await options.at(1).setSelected(); + + await wrapper.vm.$nextTick(); + + expect( + findFormFields() + .at(0) + .isVisible(), + ).toBe(true); + }); + + it('disabled 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); + }); + }); + + describe('submitting integration form', () => { + it('allows for create-new-integration with the correct form values for HTTP', async () => { + createComponent({}); + + const options = findSelect().findAll('option'); + await options.at(1).setSelected(); + + await findFormFields() + .at(0) + .setValue('Test integration'); + await findFormToggle().trigger('click'); + + await wrapper.vm.$nextTick(); + + expect(findSubmitButton().exists()).toBe(true); + expect(findSubmitButton().text()).toBe('Save integration'); + + findForm().trigger('submit'); + + await wrapper.vm.$nextTick(); + + expect(wrapper.emitted('create-new-integration')).toBeTruthy(); + expect(wrapper.emitted('create-new-integration')[0]).toEqual([ + { type: typeSet.http, variables: { name: 'Test integration', active: true } }, + ]); + }); + + it('allows for create-new-integration with the correct form values for PROMETHEUS', async () => { + createComponent({}); + + const options = findSelect().findAll('option'); + await options.at(2).setSelected(); + + await findFormFields() + .at(0) + .setValue('Test integration'); + await findFormFields() + .at(1) + .setValue('https://test.com'); + await findFormToggle().trigger('click'); + + await wrapper.vm.$nextTick(); + + expect(findSubmitButton().exists()).toBe(true); + expect(findSubmitButton().text()).toBe('Save integration'); + + findForm().trigger('submit'); + + await wrapper.vm.$nextTick(); + + expect(wrapper.emitted('create-new-integration')).toBeTruthy(); + expect(wrapper.emitted('create-new-integration')[0]).toEqual([ + { type: typeSet.prometheus, variables: { apiUrl: 'https://test.com', active: true } }, + ]); + }); + + it('allows for update-integration with the correct form values for HTTP', async () => { + createComponent({ + data: { + selectedIntegration: typeSet.http, + currentIntegration: { id: '1', name: 'Test integration pre' }, + }, + props: { + loading: false, + }, + }); + + await findFormFields() + .at(0) + .setValue('Test integration post'); + await findFormToggle().trigger('click'); + + await wrapper.vm.$nextTick(); + + expect(findSubmitButton().exists()).toBe(true); + expect(findSubmitButton().text()).toBe('Save integration'); + + findForm().trigger('submit'); + + await wrapper.vm.$nextTick(); + + expect(wrapper.emitted('update-integration')).toBeTruthy(); + expect(wrapper.emitted('update-integration')[0]).toEqual([ + { type: typeSet.http, variables: { name: 'Test integration post', active: true } }, + ]); + }); + + it('allows for update-integration with the correct form values for PROMETHEUS', async () => { + createComponent({ + data: { + selectedIntegration: typeSet.prometheus, + currentIntegration: { id: '1', apiUrl: 'https://test-pre.com' }, + }, + props: { + loading: false, + }, + }); + + await findFormFields() + .at(0) + .setValue('Test integration'); + await findFormFields() + .at(1) + .setValue('https://test-post.com'); + await findFormToggle().trigger('click'); + + await wrapper.vm.$nextTick(); + + expect(findSubmitButton().exists()).toBe(true); + expect(findSubmitButton().text()).toBe('Save integration'); + + findForm().trigger('submit'); + + await wrapper.vm.$nextTick(); + + expect(wrapper.emitted('update-integration')).toBeTruthy(); + expect(wrapper.emitted('update-integration')[0]).toEqual([ + { type: typeSet.prometheus, variables: { apiUrl: 'https://test-post.com', 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(); + + expect(findJsonTestSubmit().exists()).toBe(true); + expect(findJsonTestSubmit().text()).toBe('Save and test payload'); + expect(findJsonTestSubmit().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, + }, + }, + }); + }); + + 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` + featureFlag | integrationOption | visible + ${true} | ${1} | ${true} + ${true} | ${2} | ${false} + ${false} | ${1} | ${false} + ${false} | ${2} | ${false} + `('', ({ featureFlag, integrationOption, visible }) => { + const visibleMsg = visible ? 'is rendered' : 'is not rendered'; + const featureFlagMsg = featureFlag ? 'is enabled' : 'is disabled'; + const integrationType = integrationOption === 1 ? typeSet.http : typeSet.prometheus; + + it(`${visibleMsg} when multipleHttpIntegrationsCustomMapping feature flag ${featureFlagMsg} and integration type is ${integrationType}`, async () => { + createComponent({ multipleHttpIntegrationsCustomMapping: featureFlag }); + const options = findSelect().findAll('option'); + options.at(integrationOption).setSelected(); + await wrapper.vm.$nextTick(); + expect(findMappingBuilderSection().exists()).toBe(visible); + }); + }); + }); +}); diff --git a/spec/frontend/alerts_settings/alerts_settings_form_old_spec.js b/spec/frontend/alerts_settings/alerts_settings_form_old_spec.js new file mode 100644 index 00000000000..3d0dfb44d63 --- /dev/null +++ b/spec/frontend/alerts_settings/alerts_settings_form_old_spec.js @@ -0,0 +1,204 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlModal, GlAlert } from '@gitlab/ui'; +import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form_old.vue'; +import ToggleButton from '~/vue_shared/components/toggle_button.vue'; +import { i18n } from '~/alerts_settings/constants'; +import service from '~/alerts_settings/services'; +import { defaultAlertSettingsConfig } from './util'; + +jest.mock('~/alerts_settings/services'); + +describe('AlertsSettingsFormOld', () => { + let wrapper; + + const createComponent = ({ methods } = {}, data) => { + wrapper = shallowMount(AlertsSettingsForm, { + data() { + return { ...data }; + }, + provide: { + ...defaultAlertSettingsConfig, + }, + methods, + }); + }; + + 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'); + + beforeEach(() => { + setFixtures(` + <div> + <span class="js-service-active-status fa fa-circle" data-value="true"></span> + <span class="js-service-active-status fa fa-power-off" data-value="false"></span> + </div>`); + }); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + wrapper = null; + } + }); + + describe('with default values', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders the initial template', () => { + expect(wrapper.html()).toMatchSnapshot(); + }); + }); + + describe('reset key', () => { + it('triggers resetKey method', () => { + const resetKey = jest.fn(); + const methods = { resetKey }; + createComponent({ methods }); + + wrapper.find(GlModal).vm.$emit('ok'); + + expect(resetKey).toHaveBeenCalled(); + }); + + it('updates the authorization key on success', () => { + createComponent( + {}, + { + token: 'newToken', + }, + ); + + expect(findAuthorizationKey().attributes('value')).toBe('newToken'); + }); + + it('shows a alert message on error', () => { + service.updateGenericKey.mockRejectedValueOnce({}); + + createComponent(); + + return wrapper.vm.resetKey().then(() => { + expect(wrapper.find(GlAlert).exists()).toBe(true); + }); + }); + }); + + describe('activate toggle', () => { + it('triggers toggleActivated method', () => { + const toggleService = jest.fn(); + const methods = { toggleService }; + createComponent({ methods }); + + wrapper.find(ToggleButton).vm.$emit('change', true); + expect(toggleService).toHaveBeenCalled(); + }); + + describe('error is encountered', () => { + it('restores previous value', () => { + service.updateGenericKey.mockRejectedValueOnce({}); + createComponent(); + return wrapper.vm.resetKey().then(() => { + expect(wrapper.find(ToggleButton).props('value')).toBe(false); + }); + }); + }); + }); + + describe('prometheus is active', () => { + beforeEach(() => { + createComponent( + {}, + { + selectedIntegration: 'PROMETHEUS', + }, + ); + }); + + it('renders a valid "select"', () => { + expect(findSelect().exists()).toBe(true); + }); + + it('shows the API URL input', () => { + expect(findApiUrl().exists()).toBe(true); + }); + + it('shows the correct default API URL', () => { + expect(findUrl().attributes('value')).toBe(defaultAlertSettingsConfig.prometheus.url); + }); + }); + + describe('Opsgenie is active', () => { + beforeEach(() => { + createComponent( + {}, + { + selectedIntegration: 'OPSGENIE', + }, + ); + }); + + it('shows a input for the Opsgenie target URL', () => { + expect(findApiUrl().exists()).toBe(true); + }); + }); + + describe('trigger test alert', () => { + beforeEach(() => { + createComponent({}); + }); + + it('should enable the JSON input', () => { + expect(findJsonInput().exists()).toBe(true); + expect(findJsonInput().props('value')).toBe(null); + }); + + it('should validate JSON input', async () => { + createComponent(true, { + testAlertJson: '{ "value": "test" }', + }); + + findJsonInput().vm.$emit('change'); + + await wrapper.vm.$nextTick(); + + expect(findJsonInput().attributes('state')).toBe('true'); + }); + + describe('alert service is toggled', () => { + describe('error handling', () => { + const toggleService = true; + + it('should show generic error', async () => { + service.updateGenericActive.mockRejectedValueOnce({}); + + createComponent(); + + await wrapper.vm.toggleActivated(toggleService); + expect(wrapper.vm.active).toBe(false); + expect(wrapper.find(GlAlert).attributes('variant')).toBe('danger'); + expect(wrapper.find(GlAlert).text()).toBe(i18n.errorMsg); + }); + + it('should show first field specific error when available', async () => { + const err1 = "can't be blank"; + const err2 = 'is not a valid URL'; + const key = 'api_url'; + service.updateGenericActive.mockRejectedValueOnce({ + response: { data: { errors: { [key]: [err1, err2] } } }, + }); + + createComponent(); + + await wrapper.vm.toggleActivated(toggleService); + + expect(wrapper.find(GlAlert).text()).toContain(i18n.errorMsg); + expect(wrapper.find(GlAlert).text()).toContain(`${key} ${err1}`); + }); + }); + }); + }); +}); diff --git a/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js new file mode 100644 index 00000000000..7384cf9a095 --- /dev/null +++ b/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js @@ -0,0 +1,415 @@ +import VueApollo from 'vue-apollo'; +import { mount, createLocalVue } from '@vue/test-utils'; +import createMockApollo from 'jest/helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { GlLoadingIcon, GlAlert } from '@gitlab/ui'; +import { useMockIntersectionObserver } from 'helpers/mock_dom_observer'; +import AlertsSettingsWrapper from '~/alerts_settings/components/alerts_settings_wrapper.vue'; +import AlertsSettingsFormOld from '~/alerts_settings/components/alerts_settings_form_old.vue'; +import AlertsSettingsFormNew from '~/alerts_settings/components/alerts_settings_form_new.vue'; +import IntegrationsList from '~/alerts_settings/components/alerts_integrations_list.vue'; +import getIntegrationsQuery from '~/alerts_settings/graphql/queries/get_integrations.query.graphql'; +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 updateHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql'; +import updatePrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/update_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 { typeSet } from '~/alerts_settings/constants'; +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 { defaultAlertSettingsConfig } from './util'; +import mockIntegrations from './mocks/integrations.json'; +import { + createHttpVariables, + updateHttpVariables, + createPrometheusVariables, + updatePrometheusVariables, + ID, + errorMsg, + getIntegrationsQueryResponse, + destroyIntegrationResponse, + integrationToDestroy, + destroyIntegrationResponseWithErrors, +} from './mocks/apollo_mock'; + +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, + glFeatures: { httpIntegrationsList: false }, + ...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, + glFeatures: { httpIntegrationsList: true }, + }, + }); + } + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + wrapper = null; + } + }); + + describe('with httpIntegrationsList feature flag disabled', () => { + it('renders data driven alerts integrations list and old form by default', () => { + createComponent(); + expect(wrapper.find(IntegrationsList).exists()).toBe(true); + expect(wrapper.find(AlertsSettingsFormOld).exists()).toBe(true); + expect(wrapper.find(AlertsSettingsFormNew).exists()).toBe(false); + }); + }); + + describe('with httpIntegrationsList feature flag enabled', () => { + it('renders the GraphQL alerts integrations list and new form', () => { + createComponent({ provide: { glFeatures: { httpIntegrationsList: true } } }); + expect(wrapper.find(IntegrationsList).exists()).toBe(true); + expect(wrapper.find(AlertsSettingsFormOld).exists()).toBe(false); + expect(wrapper.find(AlertsSettingsFormNew).exists()).toBe(true); + }); + + it('uses a loading state inside the IntegrationsList table', () => { + createComponent({ + data: { integrations: {} }, + provide: { glFeatures: { httpIntegrationsList: true } }, + 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] }, + provide: { glFeatures: { httpIntegrationsList: true } }, + 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] }, + provide: { glFeatures: { httpIntegrationsList: true } }, + loading: false, + }); + + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ + data: { createHttpIntegrationMutation: { integration: { id: '1' } } }, + }); + wrapper.find(AlertsSettingsFormNew).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] }, + provide: { glFeatures: { httpIntegrationsList: true } }, + loading: false, + }); + + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ + data: { updateHttpIntegrationMutation: { integration: { id: '1' } } }, + }); + wrapper.find(AlertsSettingsFormNew).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] }, + provide: { glFeatures: { httpIntegrationsList: true } }, + loading: false, + }); + + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ + data: { resetHttpTokenMutation: { integration: { id: '1' } } }, + }); + wrapper.find(AlertsSettingsFormNew).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] }, + provide: { glFeatures: { httpIntegrationsList: true } }, + loading: false, + }); + + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ + data: { createPrometheusIntegrationMutation: { integration: { id: '2' } } }, + }); + wrapper.find(AlertsSettingsFormNew).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] }, + provide: { glFeatures: { httpIntegrationsList: true } }, + loading: false, + }); + + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ + data: { updatePrometheusIntegrationMutation: { integration: { id: '2' } } }, + }); + wrapper.find(AlertsSettingsFormNew).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] }, + provide: { glFeatures: { httpIntegrationsList: true } }, + loading: false, + }); + + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ + data: { resetPrometheusTokenMutation: { integration: { id: '1' } } }, + }); + wrapper.find(AlertsSettingsFormNew).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] }, + provide: { glFeatures: { httpIntegrationsList: true } }, + loading: false, + }); + + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(ADD_INTEGRATION_ERROR); + wrapper.find(AlertsSettingsFormNew).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] }, + provide: { glFeatures: { httpIntegrationsList: true } }, + loading: false, + }); + + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(RESET_INTEGRATION_TOKEN_ERROR); + + wrapper.find(AlertsSettingsFormNew).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] }, + provide: { glFeatures: { httpIntegrationsList: true } }, + loading: false, + }); + + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(errorMsg); + + wrapper.find(AlertsSettingsFormNew).vm.$emit('update-integration', {}); + + await waitForPromises(); + expect(createFlash).toHaveBeenCalledWith({ message: UPDATE_INTEGRATION_ERROR }); + }); + + it('shows an error alert when integration test payload fails ', async () => { + createComponent({ + data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] }, + provide: { glFeatures: { httpIntegrationsList: true } }, + loading: false, + }); + + wrapper.find(AlertsSettingsFormNew).vm.$emit('test-payload-failure'); + + await waitForPromises(); + expect(createFlash).toHaveBeenCalledWith({ message: INTEGRATION_PAYLOAD_TEST_ERROR }); + expect(createFlash).toHaveBeenCalledTimes(1); + }); + }); + + 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, + }); + }); + }); + + // TODO: Will be removed in 13.7 as part of: https://gitlab.com/gitlab-org/gitlab/-/issues/273657 + describe('Opsgenie integration', () => { + it.each([true, false])('it shows/hides the alert when opsgenie is %s', active => { + createComponent({ + data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] }, + provide: { glFeatures: { httpIntegrationsList: true }, opsgenie: { active } }, + loading: false, + }); + + expect(wrapper.find(GlAlert).exists()).toBe(active); + }); + }); +}); diff --git a/spec/frontend/alerts_settings/mocks/apollo_mock.js b/spec/frontend/alerts_settings/mocks/apollo_mock.js new file mode 100644 index 00000000000..e0eba1e8421 --- /dev/null +++ b/spec/frontend/alerts_settings/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/mocks/integrations.json b/spec/frontend/alerts_settings/mocks/integrations.json new file mode 100644 index 00000000000..b1284fc55a2 --- /dev/null +++ b/spec/frontend/alerts_settings/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/util.js b/spec/frontend/alerts_settings/util.js new file mode 100644 index 00000000000..f9f9b69791e --- /dev/null +++ b/spec/frontend/alerts_settings/util.js @@ -0,0 +1,30 @@ +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, + }, + opsgenie: { + opsgenieMvcIsAvailable: true, + formPath: INVALID_URL, + active: ACTIVE, + opsgenieMvcTargetUrl: GENERIC_URL, + }, + projectPath: '', + multiIntegrations: true, +}; |