diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-24 00:10:44 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-24 00:10:44 +0300 |
commit | 5e450e9022861d03048cc733c20585ad0891f5aa (patch) | |
tree | 6a5eb2f639fe66b3fa52008e2f99c31e1ce2d60b /spec | |
parent | c37dd28c4afd33fee46cff8ddfdada8a3f54564c (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
20 files changed, 460 insertions, 144 deletions
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb index 17db69e4699..065eb36375a 100644 --- a/spec/factories/groups.rb +++ b/spec/factories/groups.rb @@ -15,7 +15,7 @@ FactoryBot.define do raise "Don't set owner for groups, use `group.add_owner(user)` instead" end - create(:namespace_settings, namespace: group) + create(:namespace_settings, namespace: group) unless group.namespace_settings end trait :public do diff --git a/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js index 7e1d1acb62c..dba9c8be669 100644 --- a/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js +++ b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js @@ -1,10 +1,10 @@ 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'; +import alertFields from '../mocks/alert_fields.json'; +import parsedMapping from '../mocks/parsed_mapping.json'; describe('AlertMappingBuilder', () => { let wrapper; @@ -12,8 +12,8 @@ describe('AlertMappingBuilder', () => { function mountComponent() { wrapper = shallowMount(AlertMappingBuilder, { propsData: { - parsedPayload: parsedMapping.samplePayload.payloadAlerFields.nodes, - savedMapping: parsedMapping.storedMapping.nodes, + parsedPayload: parsedMapping.payloadAlerFields, + savedMapping: parsedMapping.payloadAttributeMappings, alertFields, }, }); @@ -33,6 +33,15 @@ describe('AlertMappingBuilder', () => { const findColumnInRow = (row, column) => wrapper.findAll('.gl-display-table-row').at(row).findAll('.gl-display-table-cell ').at(column); + const getDropdownContent = (dropdown, types) => { + const searchBox = dropdown.findComponent(GlSearchBoxByType); + const dropdownItems = dropdown.findAllComponents(GlDropdownItem); + const mappingOptions = parsedMapping.payloadAlerFields.filter(({ type }) => + types.includes(type), + ); + return { searchBox, dropdownItems, mappingOptions }; + }; + it('renders column captions', () => { expect(findColumnInRow(0, 0).text()).toContain(i18n.columns.gitlabKeyTitle); expect(findColumnInRow(0, 2).text()).toContain(i18n.columns.payloadKeyTitle); @@ -63,10 +72,7 @@ describe('AlertMappingBuilder', () => { 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)); + const { searchBox, dropdownItems, mappingOptions } = getDropdownContent(dropdown, types); expect(dropdown.exists()).toBe(true); expect(searchBox.exists()).toBe(true); @@ -80,11 +86,7 @@ describe('AlertMappingBuilder', () => { 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)); - + const { searchBox, dropdownItems, mappingOptions } = getDropdownContent(dropdown, types); expect(searchBox.exists()).toBe(Boolean(numberOfFallbacks)); expect(dropdownItems).toHaveLength(mappingOptions.length); } diff --git a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js index 02229b3d3da..511b3d2a059 100644 --- a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js +++ b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js @@ -11,7 +11,8 @@ 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 alertFields from '../mocks/alert_fields.json'; +import parsedMapping from '../mocks/parsed_mapping.json'; import { defaultAlertSettingsConfig } from './util'; describe('AlertsSettingsForm', () => { @@ -39,6 +40,9 @@ describe('AlertsSettingsForm', () => { multiIntegrations, }, mocks: { + $apollo: { + query: jest.fn(), + }, $toast: { show: mockToastShow, }, @@ -146,7 +150,7 @@ describe('AlertsSettingsForm', () => { enableIntegration(0, integrationName); - const sampleMapping = { field: 'test' }; + const sampleMapping = parsedMapping.payloadAttributeMappings; findMappingBuilder().vm.$emit('onMappingUpdate', sampleMapping); findForm().trigger('submit'); @@ -157,7 +161,7 @@ describe('AlertsSettingsForm', () => { name: integrationName, active: true, payloadAttributeMappings: sampleMapping, - payloadExample: null, + payloadExample: '{}', }, }, ]); @@ -275,34 +279,47 @@ describe('AlertsSettingsForm', () => { }); describe('Test payload section for HTTP integration', () => { + const validSamplePayload = JSON.stringify(alertFields); + const emptySamplePayload = '{}'; + beforeEach(() => { createComponent({ multipleHttpIntegrationsCustomMapping: true, - props: { + data: { currentIntegration: { type: typeSet.http, + payloadExample: validSamplePayload, + payloadAttributeMappings: [], }, - alertFields, + active: false, + resetPayloadAndMappingConfirmed: false, }, + props: { 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'; + active | resetPayloadAndMappingConfirmed | disabled + ${true} | ${true} | ${undefined} + ${false} | ${true} | ${'disabled'} + ${true} | ${false} | ${'disabled'} + ${false} | ${false} | ${'disabled'} + `('', ({ active, resetPayloadAndMappingConfirmed, disabled }) => { + const payloadResetMsg = resetPayloadAndMappingConfirmed + ? '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 }, + currentIntegration: { + type: typeSet.http, + payloadExample: validSamplePayload, + payloadAttributeMappings: [], + }, active, - resetSamplePayloadConfirmed, + resetPayloadAndMappingConfirmed, }); await wrapper.vm.$nextTick(); expect(findTestPayloadSection().find(GlFormTextarea).attributes('disabled')).toBe(disabled); @@ -311,20 +328,27 @@ describe('AlertsSettingsForm', () => { 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'; + resetPayloadAndMappingConfirmed | payloadExample | caption + ${false} | ${validSamplePayload} | ${'Edit payload'} + ${true} | ${emptySamplePayload} | ${'Submit payload'} + ${true} | ${validSamplePayload} | ${'Submit payload'} + ${false} | ${emptySamplePayload} | ${'Submit payload'} + `('', ({ resetPayloadAndMappingConfirmed, payloadExample, caption }) => { + const samplePayloadMsg = payloadExample ? 'was provided' : 'was not provided'; + const payloadResetMsg = resetPayloadAndMappingConfirmed + ? '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, + currentIntegration: { + payloadExample, + type: typeSet.http, + active: true, + payloadAttributeMappings: [], + }, + resetPayloadAndMappingConfirmed, }); await wrapper.vm.$nextTick(); expect(findActionBtn().text()).toBe(caption); @@ -333,16 +357,20 @@ describe('AlertsSettingsForm', () => { }); describe('Parsing payload', () => { - it('displays a toast message on successful parse', async () => { - jest.useFakeTimers(); + beforeEach(() => { wrapper.setData({ selectedIntegration: typeSet.http, - customMapping: { samplePayload: false }, + resetPayloadAndMappingConfirmed: true, }); - await wrapper.vm.$nextTick(); + }); + it('displays a toast message on successful parse', async () => { + jest.spyOn(wrapper.vm.$apollo, 'query').mockResolvedValue({ + data: { + project: { alertManagementPayloadFields: [] }, + }, + }); findActionBtn().vm.$emit('click'); - jest.advanceTimersByTime(1000); await waitForPromises(); @@ -350,6 +378,16 @@ describe('AlertsSettingsForm', () => { 'Sample payload has been parsed. You can now map the fields.', ); }); + + it('displays an error message under payload field on unsuccessful parse', async () => { + const errorMessage = 'Error parsing paylod'; + jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({ message: errorMessage }); + findActionBtn().vm.$emit('click'); + + await waitForPromises(); + + expect(findTestPayloadSection().find('.invalid-feedback').text()).toBe(errorMessage); + }); }); }); diff --git a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js index 80293597ab6..409805fdbf7 100644 --- a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js +++ b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js @@ -14,6 +14,8 @@ import createPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutat 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 updateCurrentHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/update_current_http_integration.mutation.graphql'; +import updateCurrentPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/update_current_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 getIntegrationsQuery from '~/alerts_settings/graphql/queries/get_integrations.query.graphql'; @@ -31,7 +33,8 @@ import { updateHttpVariables, createPrometheusVariables, updatePrometheusVariables, - ID, + HTTP_ID, + PROMETHEUS_ID, errorMsg, getIntegrationsQueryResponse, destroyIntegrationResponse, @@ -50,8 +53,30 @@ describe('AlertsSettingsWrapper', () => { let fakeApollo; let destroyIntegrationHandler; useMockIntersectionObserver(); + const httpMappingData = { + payloadExample: '{"test: : "field"}', + payloadAttributeMappings: [], + payloadAlertFields: [], + }; + const httpIntegrations = { + list: [ + { + id: mockIntegrations[0].id, + ...httpMappingData, + }, + { + id: mockIntegrations[1].id, + ...httpMappingData, + }, + { + id: mockIntegrations[2].id, + httpMappingData, + }, + ], + }; - const findLoader = () => wrapper.find(IntegrationsList).find(GlLoadingIcon); + const findLoader = () => wrapper.findComponent(IntegrationsList).findComponent(GlLoadingIcon); + const findIntegrationsList = () => wrapper.findComponent(IntegrationsList); const findIntegrations = () => wrapper.find(IntegrationsList).findAll('table tbody tr'); async function destroyHttpIntegration(localWrapper) { @@ -197,13 +222,13 @@ describe('AlertsSettingsWrapper', () => { }); wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', { type: typeSet.http, - variables: { id: ID }, + variables: { id: HTTP_ID }, }); expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ mutation: resetHttpTokenMutation, variables: { - id: ID, + id: HTTP_ID, }, }); }); @@ -232,7 +257,7 @@ describe('AlertsSettingsWrapper', () => { it('calls `$apollo.mutate` with `updatePrometheusIntegrationMutation`', () => { createComponent({ - data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] }, + data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[3] }, loading: false, }); @@ -261,13 +286,13 @@ describe('AlertsSettingsWrapper', () => { }); wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', { type: typeSet.prometheus, - variables: { id: ID }, + variables: { id: PROMETHEUS_ID }, }); expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ mutation: resetPrometheusTokenMutation, variables: { - id: ID, + id: PROMETHEUS_ID, }, }); }); @@ -328,6 +353,42 @@ describe('AlertsSettingsWrapper', () => { mock.restore(); }); }); + + it('calls `$apollo.mutate` with `updateCurrentHttpIntegrationMutation` on HTTP integration edit', () => { + createComponent({ + data: { + integrations: { list: mockIntegrations }, + currentIntegration: mockIntegrations[0], + httpIntegrations, + }, + loading: false, + }); + + jest.spyOn(wrapper.vm.$apollo, 'mutate'); + findIntegrationsList().vm.$emit('edit-integration', updateHttpVariables); + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: updateCurrentHttpIntegrationMutation, + variables: { ...mockIntegrations[0], ...httpMappingData }, + }); + }); + + it('calls `$apollo.mutate` with `updateCurrentPrometheusIntegrationMutation` on PROMETHEUS integration edit', () => { + createComponent({ + data: { + integrations: { list: mockIntegrations }, + currentIntegration: mockIntegrations[3], + httpIntegrations, + }, + loading: false, + }); + + jest.spyOn(wrapper.vm.$apollo, 'mutate'); + findIntegrationsList().vm.$emit('edit-integration', updatePrometheusVariables); + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: updateCurrentPrometheusIntegrationMutation, + variables: mockIntegrations[3], + }); + }); }); describe('with mocked Apollo client', () => { diff --git a/spec/frontend/alerts_settings/components/mocks/apollo_mock.js b/spec/frontend/alerts_settings/components/mocks/apollo_mock.js index e0eba1e8421..828580a436b 100644 --- a/spec/frontend/alerts_settings/components/mocks/apollo_mock.js +++ b/spec/frontend/alerts_settings/components/mocks/apollo_mock.js @@ -1,29 +1,34 @@ const projectPath = ''; -export const ID = 'gid://gitlab/AlertManagement::HttpIntegration/7'; +export const HTTP_ID = 'gid://gitlab/AlertManagement::HttpIntegration/7'; +export const PROMETHEUS_ID = 'gid://gitlab/PrometheusService/12'; export const errorMsg = 'Something went wrong'; export const createHttpVariables = { name: 'Test Pre', active: true, projectPath, + type: 'HTTP', }; export const updateHttpVariables = { name: 'Test Pre', active: true, - id: ID, + id: HTTP_ID, + type: 'HTTP', }; export const createPrometheusVariables = { apiUrl: 'https://test-pre.com', active: true, projectPath, + type: 'PROMETHEUS', }; export const updatePrometheusVariables = { apiUrl: 'https://test-pre.com', active: true, - id: ID, + id: PROMETHEUS_ID, + type: 'PROMETHEUS', }; export const getIntegrationsQueryResponse = { @@ -99,6 +104,9 @@ export const destroyIntegrationResponse = { 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json', token: '89eb01df471d990ff5162a1c640408cf', apiUrl: null, + payloadExample: '{"field": "value"}', + payloadAttributeMappings: [], + payloadAlertFields: [], }, }, }, @@ -117,6 +125,9 @@ export const destroyIntegrationResponseWithErrors = { 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json', token: '89eb01df471d990ff5162a1c640408cf', apiUrl: null, + payloadExample: '{"field": "value"}', + payloadAttributeMappings: [], + payloadAlertFields: [], }, }, }, diff --git a/spec/frontend/alerts_settings/mocks/alertFields.json b/spec/frontend/alerts_settings/mocks/alert_fields.json index ffe59dd0c05..ffe59dd0c05 100644 --- a/spec/frontend/alerts_settings/mocks/alertFields.json +++ b/spec/frontend/alerts_settings/mocks/alert_fields.json diff --git a/spec/frontend/alerts_settings/mocks/parsed_mapping.json b/spec/frontend/alerts_settings/mocks/parsed_mapping.json new file mode 100644 index 00000000000..e985671a923 --- /dev/null +++ b/spec/frontend/alerts_settings/mocks/parsed_mapping.json @@ -0,0 +1,122 @@ +{ + "payloadAlerFields": [ + { + "path": [ + "dashboardId" + ], + "label": "Dashboard Id", + "type": "string" + }, + { + "path": [ + "evalMatches" + ], + "label": "Eval Matches", + "type": "array" + }, + { + "path": [ + "createdAt" + ], + "label": "Created At", + "type": "datetime" + }, + { + "path": [ + "imageUrl" + ], + "label": "Image Url", + "type": "string" + }, + { + "path": [ + "message" + ], + "label": "Message", + "type": "string" + }, + { + "path": [ + "orgId" + ], + "label": "Org Id", + "type": "string" + }, + { + "path": [ + "panelId" + ], + "label": "Panel Id", + "type": "string" + }, + { + "path": [ + "ruleId" + ], + "label": "Rule Id", + "type": "string" + }, + { + "path": [ + "ruleName" + ], + "label": "Rule Name", + "type": "string" + }, + { + "path": [ + "ruleUrl" + ], + "label": "Rule Url", + "type": "string" + }, + { + "path": [ + "state" + ], + "label": "State", + "type": "string" + }, + { + "path": [ + "title" + ], + "label": "Title", + "type": "string" + }, + { + "path": [ + "tags", + "tag" + ], + "label": "Tags", + "type": "string" + } + ], + "payloadAttributeMappings": [ + { + "fieldName": "title", + "label": "Title", + "type": "STRING", + "path": ["title"] + }, + { + "fieldName": "description", + "label": "description", + "type": "STRING", + "path": ["description"] + }, + { + "fieldName": "hosts", + "label": "Host", + "type": "ARRAY", + "path": ["hosts", "host"] + }, + { + "fieldName": "startTime", + "label": "Created Atd", + "type": "STRING", + "path": ["time", "createdAt"] + } + ] +} diff --git a/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js b/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js index 8c1977ffebe..62b95c6078b 100644 --- a/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js +++ b/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js @@ -1,29 +1,25 @@ -import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json'; -import { - getMappingData, - getPayloadFields, - transformForSave, -} from '~/alerts_settings/utils/mapping_transformations'; -import alertFields from '../mocks/alertFields.json'; +import { getMappingData, transformForSave } from '~/alerts_settings/utils/mapping_transformations'; +import alertFields from '../mocks/alert_fields.json'; +import parsedMapping from '../mocks/parsed_mapping.json'; describe('Mapping Transformation Utilities', () => { const nameField = { label: 'Name', path: ['alert', 'name'], - type: 'string', + type: 'STRING', }; const dashboardField = { label: 'Dashboard Id', path: ['alert', 'dashboardId'], - type: 'string', + type: 'STRING', }; describe('getMappingData', () => { it('should return mapping data', () => { const result = getMappingData( alertFields, - getPayloadFields(parsedMapping.samplePayload.payloadAlerFields.nodes.slice(0, 3)), - parsedMapping.storedMapping.nodes.slice(0, 3), + parsedMapping.payloadAlerFields.slice(0, 3), + parsedMapping.payloadAttributeMappings.slice(0, 3), ); result.forEach((data, index) => { @@ -44,8 +40,8 @@ describe('Mapping Transformation Utilities', () => { const mockMappingData = [ { name: fieldName, - mapping: 'alert_name', - mappingFields: getPayloadFields([dashboardField, nameField]), + mapping: ['alert', 'name'], + mappingFields: [dashboardField, nameField], }, ]; const result = transformForSave(mockMappingData); @@ -61,21 +57,11 @@ describe('Mapping Transformation Utilities', () => { { name: fieldName, mapping: null, - mappingFields: getPayloadFields([nameField, dashboardField]), + mappingFields: [nameField, dashboardField], }, ]; const result = transformForSave(mockMappingData); expect(result).toEqual([]); }); }); - - describe('getPayloadFields', () => { - it('should add name field to each payload field', () => { - const result = getPayloadFields([nameField, dashboardField]); - expect(result).toEqual([ - { ...nameField, name: 'alert_name' }, - { ...dashboardField, name: 'alert_dashboardId' }, - ]); - }); - }); }); diff --git a/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap index 73a64875026..77095f7c611 100644 --- a/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap +++ b/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap @@ -102,7 +102,7 @@ exports[`packages_list_row renders 1`] = ` <gl-button-stub aria-label="Remove package" buttontextclasses="" - category="primary" + category="secondary" data-testid="action-delete" icon="remove" size="medium" diff --git a/spec/frontend/packages/shared/components/package_list_row_spec.js b/spec/frontend/packages/shared/components/package_list_row_spec.js index bd122167273..1c0ef7e3539 100644 --- a/spec/frontend/packages/shared/components/package_list_row_spec.js +++ b/spec/frontend/packages/shared/components/package_list_row_spec.js @@ -60,11 +60,9 @@ describe('packages_list_row', () => { }); describe('when is is group', () => { - beforeEach(() => { + it('has a package path component', () => { mountComponent({ isGroup: true }); - }); - it('has a package path component', () => { expect(findPackagePath().exists()).toBe(true); expect(findPackagePath().props()).toMatchObject({ path: 'foo/bar/baz' }); }); @@ -92,10 +90,22 @@ describe('packages_list_row', () => { }); }); - describe('delete event', () => { - beforeEach(() => mountComponent({ packageEntity: packageWithoutTags })); + describe('delete button', () => { + it('exists and has the correct props', () => { + mountComponent({ packageEntity: packageWithoutTags }); + + expect(findDeleteButton().exists()).toBe(true); + expect(findDeleteButton().attributes()).toMatchObject({ + icon: 'remove', + category: 'secondary', + variant: 'danger', + title: 'Remove package', + }); + }); it('emits the packageToDelete event when the delete button is clicked', async () => { + mountComponent({ packageEntity: packageWithoutTags }); + findDeleteButton().vm.$emit('click'); await wrapper.vm.$nextTick(); diff --git a/spec/frontend/pages/shared/wikis/wiki_alert_spec.js b/spec/frontend/pages/shared/wikis/wiki_alert_spec.js new file mode 100644 index 00000000000..6a18473b1a7 --- /dev/null +++ b/spec/frontend/pages/shared/wikis/wiki_alert_spec.js @@ -0,0 +1,40 @@ +import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import WikiAlert from '~/pages/shared/wikis/components/wiki_alert.vue'; + +describe('WikiAlert', () => { + let wrapper; + const ERROR = 'There is already a page with the same title in that path.'; + const ERROR_WITH_LINK = 'Before text %{wikiLinkStart}the page%{wikiLinkEnd} after text.'; + const PATH = '/test'; + + function createWrapper(propsData = {}, stubs = {}) { + wrapper = shallowMount(WikiAlert, { + propsData: { wikiPagePath: PATH, ...propsData }, + stubs, + }); + } + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const findGlAlert = () => wrapper.findComponent(GlAlert); + const findGlLink = () => wrapper.findComponent(GlLink); + const findGlSprintf = () => wrapper.findComponent(GlSprintf); + + describe('Wiki Alert', () => { + it('shows an alert when there is an error', () => { + createWrapper({ error: ERROR }); + expect(findGlAlert().exists()).toBe(true); + expect(findGlSprintf().exists()).toBe(true); + expect(findGlSprintf().attributes('message')).toBe(ERROR); + }); + + it('shows a the link to the help path', () => { + createWrapper({ error: ERROR_WITH_LINK }, { GlAlert, GlSprintf }); + expect(findGlLink().attributes('href')).toBe(PATH); + }); + }); +}); diff --git a/spec/frontend/pipelines/stage_spec.js b/spec/frontend/pipelines/stage_spec.js index bc4c5600d99..db15f6520ca 100644 --- a/spec/frontend/pipelines/stage_spec.js +++ b/spec/frontend/pipelines/stage_spec.js @@ -142,6 +142,8 @@ describe('Pipelines stage component', () => { beforeEach(() => { mock.onGet(dropdownPath).reply(200, stageReply); mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200); + + createComponent(); }); const clickCiAction = async () => { @@ -152,34 +154,22 @@ describe('Pipelines stage component', () => { await axios.waitForAll(); }; - describe('within pipeline table', () => { - beforeEach(() => { - createComponent({ type: 'PIPELINES_TABLE' }); - }); - - it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => { - await clickCiAction(); - - expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable'); - }); - }); + it('closes dropdown when job item action is clicked', async () => { + const hidden = jest.fn(); - describe('in MR widget', () => { - beforeEach(() => { - createComponent(); - }); + wrapper.vm.$root.$on('bv::dropdown::hide', hidden); - it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => { - const hidden = jest.fn(); + expect(hidden).toHaveBeenCalledTimes(0); - wrapper.vm.$root.$on('bv::dropdown::hide', hidden); + await clickCiAction(); - expect(hidden).toHaveBeenCalledTimes(0); + expect(hidden).toHaveBeenCalledTimes(1); + }); - await clickCiAction(); + it('emits `pipelineActionRequestComplete` when job item action is clicked', async () => { + await clickCiAction(); - expect(hidden).toHaveBeenCalledTimes(1); - }); + expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(1); }); }); }); diff --git a/spec/frontend/registry/explorer/components/delete_button_spec.js b/spec/frontend/registry/explorer/components/delete_button_spec.js index a557d9afacc..4597c42add9 100644 --- a/spec/frontend/registry/explorer/components/delete_button_spec.js +++ b/spec/frontend/registry/explorer/components/delete_button_spec.js @@ -58,6 +58,7 @@ describe('delete_button', () => { title: 'Foo title', variant: 'danger', disabled: 'true', + category: 'secondary', }); }); diff --git a/spec/lib/gitlab/background_migration/set_default_iteration_cadences_spec.rb b/spec/lib/gitlab/background_migration/set_default_iteration_cadences_spec.rb index 1f1877f5d2b..46c919f0854 100644 --- a/spec/lib/gitlab/background_migration/set_default_iteration_cadences_spec.rb +++ b/spec/lib/gitlab/background_migration/set_default_iteration_cadences_spec.rb @@ -58,12 +58,10 @@ RSpec.describe Gitlab::BackgroundMigration::SetDefaultIterationCadences, schema: context 'when an iteration cadence exists for a group' do let!(:group) { namespaces.create!(name: 'group', path: 'group') } - let!(:iterations_cadence_1) { iterations_cadences.create!(group_id: group.id, start_date: 5.days.ago, title: 'Cadence 1') } - let!(:iterations_cadence_2) { iterations_cadences.create!(group_id: group.id, start_date: 2.days.ago, title: 'Cadence 2') } + let!(:iterations_cadence_1) { iterations_cadences.create!(group_id: group.id, start_date: 2.days.ago, title: 'Cadence 1') } let!(:iteration_1) { iterations.create!(group_id: group.id, iid: 1, title: 'Iteration 1', start_date: 10.days.ago, due_date: 8.days.ago) } let!(:iteration_2) { iterations.create!(group_id: group.id, iterations_cadence_id: iterations_cadence_1.id, iid: 2, title: 'Iteration 2', start_date: 5.days.ago, due_date: 3.days.ago) } - let!(:iteration_3) { iterations.create!(group_id: group.id, iterations_cadence_id: iterations_cadence_2.id, iid: 3, title: 'Iteration 3', start_date: 2.days.ago, due_date: 1.day.ago) } subject { described_class.new.perform(group.id) } @@ -76,7 +74,6 @@ RSpec.describe Gitlab::BackgroundMigration::SetDefaultIterationCadences, schema: expect(iteration_1.reload.iterations_cadence_id).to eq(iterations_cadence_1.id) expect(iteration_2.reload.iterations_cadence_id).to eq(iterations_cadence_1.id) - expect(iteration_3.reload.iterations_cadence_id).to eq(iterations_cadence_2.id) end end end diff --git a/spec/migrations/migrate_delayed_project_removal_from_namespaces_to_namespace_settings_spec.rb b/spec/migrations/migrate_delayed_project_removal_from_namespaces_to_namespace_settings_spec.rb new file mode 100644 index 00000000000..28a8dcf0d4c --- /dev/null +++ b/spec/migrations/migrate_delayed_project_removal_from_namespaces_to_namespace_settings_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require Rails.root.join('db', 'post_migrate', '20210215095328_migrate_delayed_project_removal_from_namespaces_to_namespace_settings.rb') + +RSpec.describe MigrateDelayedProjectRemovalFromNamespacesToNamespaceSettings, :migration do + let(:namespaces) { table(:namespaces) } + let(:namespace_settings) { table(:namespace_settings) } + + let!(:namespace_wo_settings) { namespaces.create!(name: generate(:name), path: generate(:name), delayed_project_removal: true) } + let!(:namespace_wo_settings_delay_false) { namespaces.create!(name: generate(:name), path: generate(:name), delayed_project_removal: false) } + let!(:namespace_w_settings_delay_true) { namespaces.create!(name: generate(:name), path: generate(:name), delayed_project_removal: true) } + let!(:namespace_w_settings_delay_false) { namespaces.create!(name: generate(:name), path: generate(:name), delayed_project_removal: false) } + + let!(:namespace_settings_delay_true) { namespace_settings.create!(namespace_id: namespace_w_settings_delay_true.id, delayed_project_removal: false, created_at: DateTime.now, updated_at: DateTime.now) } + let!(:namespace_settings_delay_false) { namespace_settings.create!(namespace_id: namespace_w_settings_delay_false.id, delayed_project_removal: false, created_at: DateTime.now, updated_at: DateTime.now) } + + it 'migrates delayed_project_removal to namespace_settings' do + disable_migrations_output { migrate! } + + expect(namespace_settings.count).to eq(3) + + expect(namespace_settings.find_by(namespace_id: namespace_wo_settings.id).delayed_project_removal).to eq(true) + expect(namespace_settings.find_by(namespace_id: namespace_wo_settings_delay_false.id)).to be_nil + + expect(namespace_settings_delay_true.reload.delayed_project_removal).to eq(true) + expect(namespace_settings_delay_false.reload.delayed_project_removal).to eq(false) + end +end diff --git a/spec/migrations/reschedule_set_default_iteration_cadences_spec.rb b/spec/migrations/reschedule_set_default_iteration_cadences_spec.rb index 25c2b4efe8b..fb629c90d9f 100644 --- a/spec/migrations/reschedule_set_default_iteration_cadences_spec.rb +++ b/spec/migrations/reschedule_set_default_iteration_cadences_spec.rb @@ -16,13 +16,13 @@ RSpec.describe RescheduleSetDefaultIterationCadences do let(:group_7) { namespaces.create!(name: 'test_7', path: 'test_7') } let(:group_8) { namespaces.create!(name: 'test_8', path: 'test_8') } - let!(:iteration_1) { iterations.create!(iid: 1, title: 'iteration 1', group_id: group_1.id) } - let!(:iteration_2) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_3.id) } - let!(:iteration_3) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_4.id) } - let!(:iteration_4) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_5.id) } - let!(:iteration_5) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_6.id) } - let!(:iteration_6) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_7.id) } - let!(:iteration_7) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_8.id) } + let!(:iteration_1) { iterations.create!(iid: 1, title: 'iteration 1', group_id: group_1.id, start_date: 2.days.from_now, due_date: 3.days.from_now) } + let!(:iteration_2) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_3.id, start_date: 2.days.from_now, due_date: 3.days.from_now) } + let!(:iteration_3) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_4.id, start_date: 2.days.from_now, due_date: 3.days.from_now) } + let!(:iteration_4) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_5.id, start_date: 2.days.from_now, due_date: 3.days.from_now) } + let!(:iteration_5) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_6.id, start_date: 2.days.from_now, due_date: 3.days.from_now) } + let!(:iteration_6) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_7.id, start_date: 2.days.from_now, due_date: 3.days.from_now) } + let!(:iteration_7) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_8.id, start_date: 2.days.from_now, due_date: 3.days.from_now) } around do |example| freeze_time { Sidekiq::Testing.fake! { example.run } } diff --git a/spec/models/iteration_spec.rb b/spec/models/iteration_spec.rb index 7241a07a215..7c57f08b2bd 100644 --- a/spec/models/iteration_spec.rb +++ b/spec/models/iteration_spec.rb @@ -3,10 +3,11 @@ require 'spec_helper' RSpec.describe Iteration do - let_it_be(:project) { create(:project) } - let_it_be(:group) { create(:group) } let(:set_cadence) { nil } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + describe 'associations' do it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:group) } @@ -67,7 +68,7 @@ RSpec.describe Iteration do expect { iteration.save! }.to change { Iterations::Cadence.count }.by(1) end - it 'sets the newly created iterations_cadence to the reecord' do + it 'sets the newly created iterations_cadence to the record' do iteration.save! expect(iteration.iterations_cadence).to eq(Iterations::Cadence.last) @@ -148,7 +149,7 @@ RSpec.describe Iteration do context 'Validations' do subject { build(:iteration, group: group, start_date: start_date, due_date: due_date) } - describe '#not_belonging_to_project' do + describe 'when iteration belongs to project' do subject { build(:iteration, project: project, start_date: Time.current, due_date: 1.day.from_now) } it 'is invalid' do @@ -180,13 +181,13 @@ RSpec.describe Iteration do let(:due_date) { 6.days.from_now } shared_examples_for 'overlapping dates' do |skip_constraint_test: false| - context 'when start_date is in range' do + context 'when start_date overlaps' do let(:start_date) { 5.days.from_now } let(:due_date) { 3.weeks.from_now } it 'is not valid' do expect(subject).not_to be_valid - expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations') + expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations within this group') end unless skip_constraint_test @@ -197,13 +198,13 @@ RSpec.describe Iteration do end end - context 'when end_date is in range' do + context 'when due_date overlaps' do let(:start_date) { Time.current } let(:due_date) { 6.days.from_now } it 'is not valid' do expect(subject).not_to be_valid - expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations') + expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations within this group') end unless skip_constraint_test @@ -217,7 +218,7 @@ RSpec.describe Iteration do context 'when both overlap' do it 'is not valid' do expect(subject).not_to be_valid - expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations') + expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations within this group') end unless skip_constraint_test @@ -231,7 +232,7 @@ RSpec.describe Iteration do context 'group' do it_behaves_like 'overlapping dates' do - let(:constraint_name) { 'iteration_start_and_due_daterange_group_id_constraint' } + let(:constraint_name) { 'iteration_start_and_due_date_iterations_cadence_id_constraint' } end context 'different group' do @@ -249,11 +250,12 @@ RSpec.describe Iteration do subject { build(:iteration, group: subgroup, start_date: start_date, due_date: due_date) } - it_behaves_like 'overlapping dates', skip_constraint_test: true + it { is_expected.to be_valid } end end - context 'project' do + # Skipped. Pending https://gitlab.com/gitlab-org/gitlab/-/issues/299864 + xcontext 'project' do let_it_be(:existing_iteration) { create(:iteration, :skip_project_validation, project: project, start_date: 4.days.from_now, due_date: 1.week.from_now) } subject { build(:iteration, :skip_project_validation, project: project, start_date: start_date, due_date: due_date) } @@ -283,16 +285,16 @@ RSpec.describe Iteration do expect { subject.save! }.not_to raise_exception end end - end - context 'project in a group' do - let_it_be(:project) { create(:project, group: create(:group)) } - let_it_be(:existing_iteration) { create(:iteration, :skip_project_validation, project: project, start_date: 4.days.from_now, due_date: 1.week.from_now) } + context 'project in a group' do + let_it_be(:project) { create(:project, group: create(:group)) } + let_it_be(:existing_iteration) { create(:iteration, :skip_project_validation, project: project, start_date: 4.days.from_now, due_date: 1.week.from_now) } - subject { build(:iteration, :skip_project_validation, project: project, start_date: start_date, due_date: due_date) } + subject { build(:iteration, :skip_project_validation, project: project, start_date: start_date, due_date: due_date) } - it_behaves_like 'overlapping dates' do - let(:constraint_name) { 'iteration_start_and_due_daterange_project_id_constraint' } + it_behaves_like 'overlapping dates' do + let(:constraint_name) { 'iteration_start_and_due_daterange_project_id_constraint' } + end end end end @@ -310,19 +312,23 @@ RSpec.describe Iteration do let(:start_date) { 1.week.ago } let(:due_date) { 1.week.from_now } - it 'is not valid' do - expect(subject).not_to be_valid - expect(subject.errors[:start_date]).to include('cannot be in the past') - end + it { is_expected.to be_valid } end context 'when due_date is in the past' do + let(:start_date) { 2.weeks.ago } + let(:due_date) { 1.week.ago } + + it { is_expected.to be_valid } + end + + context 'when due_date is before start date' do let(:start_date) { Time.current } let(:due_date) { 1.week.ago } it 'is not valid' do expect(subject).not_to be_valid - expect(subject.errors[:due_date]).to include('cannot be in the past') + expect(subject.errors[:due_date]).to include('must be greater than start date') end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index ed0b9063e32..b3c3c6aaa41 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Namespace do include ProjectForksHelper include GitHelpers - let!(:namespace) { create(:namespace) } + let!(:namespace) { create(:namespace, :with_namespace_settings) } let(:gitlab_shell) { Gitlab::Shell.new } let(:repository_storage) { 'default' } @@ -116,6 +116,28 @@ RSpec.describe Namespace do it { is_expected.to include_module(Namespaces::Traversal::Recursive) } end + describe 'callbacks' do + describe 'before_save :ensure_delayed_project_removal_assigned_to_namespace_settings' do + it 'sets the matching value in namespace_settings' do + expect { namespace.update!(delayed_project_removal: true) }.to change { + namespace.namespace_settings.delayed_project_removal + }.from(false).to(true) + end + + context 'when the feature flag is disabled' do + before do + stub_feature_flags(migrate_delayed_project_removal: false) + end + + it 'does not set the matching value in namespace_settings' do + expect { namespace.update!(delayed_project_removal: true) }.not_to change { + namespace.namespace_settings.delayed_project_removal + } + end + end + end + end + describe '#visibility_level_field' do it { expect(namespace.visibility_level_field).to eq(:visibility_level) } end diff --git a/spec/support/shared_examples/graphql/mutation_shared_examples.rb b/spec/support/shared_examples/graphql/mutation_shared_examples.rb index 84ebd4852b9..51d52cbb901 100644 --- a/spec/support/shared_examples/graphql/mutation_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutation_shared_examples.rb @@ -48,6 +48,6 @@ RSpec.shared_examples 'a mutation that returns errors in the response' do |error it do post_graphql_mutation(mutation, current_user: current_user) - expect(mutation_response['errors']).to eq(errors) + expect(mutation_response['errors']).to match_array(errors) end end diff --git a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb index f91e4bd8cf7..68142e667a4 100644 --- a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb @@ -18,7 +18,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| context 'with a project' do it_behaves_like 'AtomicInternalId' do let(:internal_id_attribute) { :iid } - let(:instance) { build(timebox_type, *timebox_args, project: build(:project), group: nil) } + let(:instance) { build(timebox_type, *timebox_args, project: create(:project), group: nil) } let(:scope) { :project } let(:scope_attrs) { { project: instance.project } } let(:usage) { timebox_table_name } @@ -28,7 +28,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| context 'with a group' do it_behaves_like 'AtomicInternalId' do let(:internal_id_attribute) { :iid } - let(:instance) { build(timebox_type, *timebox_args, project: nil, group: build(:group)) } + let(:instance) { build(timebox_type, *timebox_args, project: nil, group: create(:group)) } let(:scope) { :group } let(:scope_attrs) { { namespace: instance.group } } let(:usage) { timebox_table_name } |