diff options
Diffstat (limited to 'spec')
21 files changed, 693 insertions, 211 deletions
diff --git a/spec/controllers/projects/settings/repository_controller_spec.rb b/spec/controllers/projects/settings/repository_controller_spec.rb index d93f23ae142..394f1ff28f2 100644 --- a/spec/controllers/projects/settings/repository_controller_spec.rb +++ b/spec/controllers/projects/settings/repository_controller_spec.rb @@ -23,13 +23,15 @@ RSpec.describe Projects::Settings::RepositoryController do describe 'PUT cleanup' do let(:object_map) { fixture_file_upload('spec/fixtures/bfg_object_map.txt') } - it 'enqueues a RepositoryCleanupWorker' do - allow(RepositoryCleanupWorker).to receive(:perform_async) + it 'enqueues a project cleanup' do + expect(Projects::CleanupService) + .to receive(:enqueue) + .with(project, user, anything) + .and_return(status: :success) - put :cleanup, params: { namespace_id: project.namespace, project_id: project, project: { object_map: object_map } } + put :cleanup, params: { namespace_id: project.namespace, project_id: project, project: { bfg_object_map: object_map } } expect(response).to redirect_to project_settings_repository_path(project) - expect(RepositoryCleanupWorker).to have_received(:perform_async).once end end diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js index 5d34bc48ed5..691f8896d74 100644 --- a/spec/frontend/groups/components/app_spec.js +++ b/spec/frontend/groups/components/app_spec.js @@ -2,6 +2,8 @@ import '~/flash'; import $ from 'jquery'; import Vue from 'vue'; import AxiosMockAdapter from 'axios-mock-adapter'; +import { GlModal, GlLoadingIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import waitForPromises from 'helpers/wait_for_promises'; import axios from '~/lib/utils/axios_utils'; import appComponent from '~/groups/components/app.vue'; @@ -23,47 +25,51 @@ import { mockPageInfo, } from '../mock_data'; -const createComponent = (hideProjects = false) => { - const Component = Vue.extend(appComponent); - const store = new GroupsStore(false); - const service = new GroupsService(mockEndpoint); - - store.state.pageInfo = mockPageInfo; - - return new Component({ - propsData: { - store, - service, - hideProjects, - }, - }); +const $toast = { + show: jest.fn(), }; describe('AppComponent', () => { + let wrapper; let vm; let mock; let getGroupsSpy; + const store = new GroupsStore(false); + const service = new GroupsService(mockEndpoint); + + const createShallowComponent = (hideProjects = false) => { + store.state.pageInfo = mockPageInfo; + wrapper = shallowMount(appComponent, { + propsData: { + store, + service, + hideProjects, + }, + mocks: { + $toast, + }, + }); + vm = wrapper.vm; + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + beforeEach(() => { mock = new AxiosMockAdapter(axios); mock.onGet('/dashboard/groups.json').reply(200, mockGroups); Vue.component('group-folder', groupFolderComponent); Vue.component('group-item', groupItemComponent); - vm = createComponent(); + createShallowComponent(); getGroupsSpy = jest.spyOn(vm.service, 'getGroups'); return vm.$nextTick(); }); describe('computed', () => { - beforeEach(() => { - vm.$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - describe('groups', () => { it('should return list of groups from store', () => { jest.spyOn(vm.store, 'getGroups').mockImplementation(() => {}); @@ -88,14 +94,6 @@ describe('AppComponent', () => { }); describe('methods', () => { - beforeEach(() => { - vm.$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - describe('fetchGroups', () => { it('should call `getGroups` with all the params provided', () => { return vm @@ -284,29 +282,15 @@ describe('AppComponent', () => { it('updates props which show modal confirmation dialog', () => { const group = { ...mockParentGroupItem }; - expect(vm.showModal).toBe(false); expect(vm.groupLeaveConfirmationMessage).toBe(''); vm.showLeaveGroupModal(group, mockParentGroupItem); - expect(vm.showModal).toBe(true); expect(vm.groupLeaveConfirmationMessage).toBe( `Are you sure you want to leave the "${group.fullName}" group?`, ); }); }); - describe('hideLeaveGroupModal', () => { - it('hides modal confirmation which is shown before leaving the group', () => { - const group = { ...mockParentGroupItem }; - vm.showLeaveGroupModal(group, mockParentGroupItem); - - expect(vm.showModal).toBe(true); - vm.hideLeaveGroupModal(); - - expect(vm.showModal).toBe(false); - }); - }); - describe('leaveGroup', () => { let groupItem; let childGroupItem; @@ -324,18 +308,16 @@ describe('AppComponent', () => { const notice = `You left the "${childGroupItem.fullName}" group.`; jest.spyOn(vm.service, 'leaveGroup').mockResolvedValue({ data: { notice } }); jest.spyOn(vm.store, 'removeGroup'); - jest.spyOn(window, 'Flash').mockImplementation(() => {}); jest.spyOn($, 'scrollTo').mockImplementation(() => {}); vm.leaveGroup(); - expect(vm.showModal).toBe(false); expect(vm.targetGroup.isBeingRemoved).toBe(true); expect(vm.service.leaveGroup).toHaveBeenCalledWith(vm.targetGroup.leavePath); return waitForPromises().then(() => { expect($.scrollTo).toHaveBeenCalledWith(0); expect(vm.store.removeGroup).toHaveBeenCalledWith(vm.targetGroup, vm.targetParentGroup); - expect(window.Flash).toHaveBeenCalledWith(notice, 'notice'); + expect($toast.show).toHaveBeenCalledWith(notice); }); }); @@ -417,8 +399,7 @@ describe('AppComponent', () => { it('should bind event listeners on eventHub', () => { jest.spyOn(eventHub, '$on').mockImplementation(() => {}); - const newVm = createComponent(); - newVm.$mount(); + createShallowComponent(); return vm.$nextTick().then(() => { expect(eventHub.$on).toHaveBeenCalledWith('fetchPage', expect.any(Function)); @@ -426,25 +407,20 @@ describe('AppComponent', () => { expect(eventHub.$on).toHaveBeenCalledWith('showLeaveGroupModal', expect.any(Function)); expect(eventHub.$on).toHaveBeenCalledWith('updatePagination', expect.any(Function)); expect(eventHub.$on).toHaveBeenCalledWith('updateGroups', expect.any(Function)); - newVm.$destroy(); }); }); it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`', () => { - const newVm = createComponent(); - newVm.$mount(); + createShallowComponent(); return vm.$nextTick().then(() => { - expect(newVm.searchEmptyMessage).toBe('No groups or projects matched your search'); - newVm.$destroy(); + expect(vm.searchEmptyMessage).toBe('No groups or projects matched your search'); }); }); it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `true`', () => { - const newVm = createComponent(true); - newVm.$mount(); + createShallowComponent(true); return vm.$nextTick().then(() => { - expect(newVm.searchEmptyMessage).toBe('No groups matched your search'); - newVm.$destroy(); + expect(vm.searchEmptyMessage).toBe('No groups matched your search'); }); }); }); @@ -453,9 +429,8 @@ describe('AppComponent', () => { it('should unbind event listeners on eventHub', () => { jest.spyOn(eventHub, '$off').mockImplementation(() => {}); - const newVm = createComponent(); - newVm.$mount(); - newVm.$destroy(); + createShallowComponent(); + wrapper.destroy(); return vm.$nextTick().then(() => { expect(eventHub.$off).toHaveBeenCalledWith('fetchPage', expect.any(Function)); @@ -468,19 +443,10 @@ describe('AppComponent', () => { }); describe('template', () => { - beforeEach(() => { - vm.$mount(); - }); - - afterEach(() => { - vm.$destroy(); - }); - it('should render loading icon', () => { vm.isLoading = true; return vm.$nextTick().then(() => { - expect(vm.$el.querySelector('.loading-animation')).toBeDefined(); - expect(vm.$el.querySelector('span').getAttribute('aria-label')).toBe('Loading groups'); + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); }); }); @@ -493,15 +459,13 @@ describe('AppComponent', () => { }); it('renders modal confirmation dialog', () => { - vm.groupLeaveConfirmationMessage = 'Are you sure you want to leave the "foo" group?'; - vm.showModal = true; - return vm.$nextTick().then(() => { - const modalDialogEl = vm.$el.querySelector('.modal'); + createShallowComponent(); - expect(modalDialogEl).not.toBe(null); - expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?'); - expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave'); - }); + const findGlModal = wrapper.find(GlModal); + + expect(findGlModal.exists()).toBe(true); + expect(findGlModal.attributes('title')).toBe('Are you sure?'); + expect(findGlModal.props('actionPrimary').text).toBe('Leave group'); }); }); }); diff --git a/spec/frontend/groups/components/item_actions_spec.js b/spec/frontend/groups/components/item_actions_spec.js index 88a7d4ce2d4..9adbc9abe13 100644 --- a/spec/frontend/groups/components/item_actions_spec.js +++ b/spec/frontend/groups/components/item_actions_spec.js @@ -1,5 +1,4 @@ import { shallowMount } from '@vue/test-utils'; -import { GlIcon } from '@gitlab/ui'; import ItemActions from '~/groups/components/item_actions.vue'; import eventHub from '~/groups/event_hub'; import { mockParentGroupItem, mockChildren } from '../mock_data'; @@ -20,18 +19,25 @@ describe('ItemActions', () => { }; afterEach(() => { - if (wrapper) { - wrapper.destroy(); - wrapper = null; - } + wrapper.destroy(); + wrapper = null; }); const findEditGroupBtn = () => wrapper.find('[data-testid="edit-group-btn"]'); - const findEditGroupIcon = () => findEditGroupBtn().find(GlIcon); const findLeaveGroupBtn = () => wrapper.find('[data-testid="leave-group-btn"]'); - const findLeaveGroupIcon = () => findLeaveGroupBtn().find(GlIcon); describe('template', () => { + let group; + + beforeEach(() => { + group = { + ...mockParentGroupItem, + canEdit: true, + canLeave: true, + }; + createComponent({ group }); + }); + it('renders component template correctly', () => { createComponent(); @@ -39,49 +45,46 @@ describe('ItemActions', () => { }); it('renders "Edit group" button with correct attribute values', () => { - const group = { - ...mockParentGroupItem, - canEdit: true, - }; - - createComponent({ group }); - - expect(findEditGroupBtn().exists()).toBe(true); - expect(findEditGroupBtn().classes()).toContain('no-expand'); - expect(findEditGroupBtn().attributes('href')).toBe(group.editPath); - expect(findEditGroupBtn().attributes('aria-label')).toBe('Edit group'); - expect(findEditGroupBtn().attributes('title')).toBe('Edit group'); - expect(findEditGroupIcon().exists()).toBe(true); - expect(findEditGroupIcon().props('name')).toBe('settings'); + const button = findEditGroupBtn(); + expect(button.exists()).toBe(true); + expect(button.props('icon')).toBe('pencil'); + expect(button.attributes('aria-label')).toBe('Edit group'); }); - describe('`canLeave` is true', () => { - const group = { - ...mockParentGroupItem, - canLeave: true, - }; + it('renders "Leave this group" button with correct attribute values', () => { + const button = findLeaveGroupBtn(); + expect(button.exists()).toBe(true); + expect(button.props('icon')).toBe('leave'); + expect(button.attributes('aria-label')).toBe('Leave this group'); + }); - beforeEach(() => { - createComponent({ group }); - }); + it('emits `showLeaveGroupModal` event in the event hub', () => { + jest.spyOn(eventHub, '$emit'); + findLeaveGroupBtn().vm.$emit('click', { stopPropagation: () => {} }); - it('renders "Leave this group" button with correct attribute values', () => { - expect(findLeaveGroupBtn().exists()).toBe(true); - expect(findLeaveGroupBtn().classes()).toContain('no-expand'); - expect(findLeaveGroupBtn().attributes('href')).toBe(group.leavePath); - expect(findLeaveGroupBtn().attributes('aria-label')).toBe('Leave this group'); - expect(findLeaveGroupBtn().attributes('title')).toBe('Leave this group'); - expect(findLeaveGroupIcon().exists()).toBe(true); - expect(findLeaveGroupIcon().props('name')).toBe('leave'); - }); + expect(eventHub.$emit).toHaveBeenCalledWith('showLeaveGroupModal', group, parentGroup); + }); + }); - it('emits event on "Leave this group" button click', () => { - jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + it('does not render leave button if group can not be left', () => { + createComponent({ + group: { + ...mockParentGroupItem, + canLeave: false, + }, + }); - findLeaveGroupBtn().trigger('click'); + expect(findLeaveGroupBtn().exists()).toBe(false); + }); - expect(eventHub.$emit).toHaveBeenCalledWith('showLeaveGroupModal', group, parentGroup); - }); + it('does not render edit button if group can not be edited', () => { + createComponent({ + group: { + ...mockParentGroupItem, + canEdit: false, + }, }); + + expect(findEditGroupBtn().exists()).toBe(false); }); }); diff --git a/spec/frontend/monitoring/components/charts/column_spec.js b/spec/frontend/monitoring/components/charts/column_spec.js index 16e2080c000..fbcff33d692 100644 --- a/spec/frontend/monitoring/components/charts/column_spec.js +++ b/spec/frontend/monitoring/components/charts/column_spec.js @@ -30,6 +30,7 @@ describe('Column component', () => { }, metrics: [ { + label: 'Mock data', result: [ { metric: {}, @@ -96,7 +97,7 @@ describe('Column component', () => { describe('wrapped components', () => { describe('GitLab UI column chart', () => { it('receives data properties needed for proper chart render', () => { - expect(chartProps('data').values).toEqual(dataValues); + expect(chartProps('bars')).toEqual([{ name: 'Mock data', data: dataValues }]); }); it('passes the y axis name correctly', () => { diff --git a/spec/frontend/monitoring/components/charts/stacked_column_spec.js b/spec/frontend/monitoring/components/charts/stacked_column_spec.js index 24a2af87eb8..2032258730a 100644 --- a/spec/frontend/monitoring/components/charts/stacked_column_spec.js +++ b/spec/frontend/monitoring/components/charts/stacked_column_spec.js @@ -44,19 +44,19 @@ describe('Stacked column chart component', () => { }); it('data should match the graphData y value for each series', () => { - const data = findChart().props('data'); + const data = findChart().props('bars'); data.forEach((series, index) => { const { values } = stackedColumnMockedData.metrics[index].result[0]; - expect(series).toEqual(values.map(value => value[1])); + expect(series.data).toEqual(values.map(value => value[1])); }); }); - it('series names should be the same as the graphData metrics labels', () => { - const seriesNames = findChart().props('seriesNames'); + it('data should be the same length as the graphData metrics labels', () => { + const barDataProp = findChart().props('bars'); - expect(seriesNames).toHaveLength(stackedColumnMockedData.metrics.length); - seriesNames.forEach((name, index) => { + expect(barDataProp).toHaveLength(stackedColumnMockedData.metrics.length); + barDataProp.forEach(({ name }, index) => { expect(stackedColumnMockedData.metrics[index].label).toBe(name); }); }); diff --git a/spec/frontend/projects/pipelines/charts/components/app_spec.js b/spec/frontend/projects/pipelines/charts/components/app_spec.js index 883f2bec5f7..0dd3407dbbc 100644 --- a/spec/frontend/projects/pipelines/charts/components/app_spec.js +++ b/spec/frontend/projects/pipelines/charts/components/app_spec.js @@ -45,7 +45,7 @@ describe('ProjectsPipelinesChartsApp', () => { expect(chart.exists()).toBeTruthy(); expect(chart.props('yAxisTitle')).toBe('Minutes'); expect(chart.props('xAxisTitle')).toBe('Commit'); - expect(chart.props('data')).toBe(wrapper.vm.timesChartTransformedData); + expect(chart.props('bars')).toBe(wrapper.vm.timesChartTransformedData); expect(chart.props('option')).toBe(wrapper.vm.$options.timesChartOptions); }); }); diff --git a/spec/frontend/vue_shared/components/members/table/members_table_spec.js b/spec/frontend/vue_shared/components/members/table/members_table_spec.js index 90a36c29518..39234e230dc 100644 --- a/spec/frontend/vue_shared/components/members/table/members_table_spec.js +++ b/spec/frontend/vue_shared/components/members/table/members_table_spec.js @@ -3,6 +3,7 @@ import Vuex from 'vuex'; import { getByText as getByTextHelper, getByTestId as getByTestIdHelper, + within, } from '@testing-library/dom'; import { GlBadge } from '@gitlab/ui'; import MembersTable from '~/vue_shared/components/members/table/members_table.vue'; @@ -28,6 +29,7 @@ describe('MemberList', () => { members: [], tableFields: [], sourceId: 1, + currentUserId: 1, ...state, }, }); @@ -62,12 +64,16 @@ describe('MemberList', () => { }); describe('fields', () => { - const memberCanUpdate = { + const directMember = { ...memberMock, - canUpdate: true, source: { ...memberMock.source, id: 1 }, }; + const memberCanUpdate = { + ...directMember, + canUpdate: true, + }; + it.each` field | label | member | expectedComponent ${'account'} | ${'Account'} | ${memberMock} | ${MemberAvatar} @@ -96,19 +102,60 @@ describe('MemberList', () => { } }); - it('renders "Actions" field for screen readers', () => { - createComponent({ members: [memberMock], tableFields: ['actions'] }); + describe('"Actions" field', () => { + it('renders "Actions" field for screen readers', () => { + createComponent({ members: [memberCanUpdate], tableFields: ['actions'] }); - const actionField = getByTestId('col-actions'); + const actionField = getByTestId('col-actions'); - expect(actionField.exists()).toBe(true); - expect(actionField.classes('gl-sr-only')).toBe(true); - expect( - wrapper - .find(`[data-label="Actions"][role="cell"]`) - .find(MemberActionButtons) - .exists(), - ).toBe(true); + expect(actionField.exists()).toBe(true); + expect(actionField.classes('gl-sr-only')).toBe(true); + expect( + wrapper + .find(`[data-label="Actions"][role="cell"]`) + .find(MemberActionButtons) + .exists(), + ).toBe(true); + }); + + describe('when user is not logged in', () => { + it('does not render the "Actions" field', () => { + createComponent({ currentUserId: null, tableFields: ['actions'] }); + + expect(within(wrapper.element).queryByTestId('col-actions')).toBe(null); + }); + }); + + const memberCanRemove = { + ...directMember, + canRemove: true, + }; + + describe.each` + permission | members + ${'canUpdate'} | ${[memberCanUpdate]} + ${'canRemove'} | ${[memberCanRemove]} + ${'canResend'} | ${[invite]} + `('when one of the members has $permission permissions', ({ members }) => { + it('renders the "Actions" field', () => { + createComponent({ members, tableFields: ['actions'] }); + + expect(getByTestId('col-actions').exists()).toBe(true); + }); + }); + + describe.each` + permission | members + ${'canUpdate'} | ${[memberMock]} + ${'canRemove'} | ${[memberMock]} + ${'canResend'} | ${[{ ...invite, invite: { ...invite.invite, canResend: false } }]} + `('when none of the members have $permission permissions', ({ members }) => { + it('does not render the "Actions" field', () => { + createComponent({ members, tableFields: ['actions'] }); + + expect(within(wrapper.element).queryByTestId('col-actions')).toBe(null); + }); + }); }); }); diff --git a/spec/frontend/vue_shared/components/members/utils_spec.js b/spec/frontend/vue_shared/components/members/utils_spec.js index f183abc08d6..3f2b2097133 100644 --- a/spec/frontend/vue_shared/components/members/utils_spec.js +++ b/spec/frontend/vue_shared/components/members/utils_spec.js @@ -1,5 +1,19 @@ -import { generateBadges } from '~/vue_shared/components/members/utils'; -import { member as memberMock } from './mock_data'; +import { + generateBadges, + isGroup, + isDirectMember, + isCurrentUser, + canRemove, + canResend, + canUpdate, + canOverride, +} from '~/vue_shared/components/members/utils'; +import { member as memberMock, group, invite } from './mock_data'; + +const DIRECT_MEMBER_ID = 178; +const INHERITED_MEMBER_ID = 179; +const IS_CURRENT_USER_ID = 123; +const IS_NOT_CURRENT_USER_ID = 124; describe('Members Utils', () => { describe('generateBadges', () => { @@ -26,4 +40,83 @@ describe('Members Utils', () => { expect(generateBadges(member, true)).toContainEqual(expect.objectContaining(expected)); }); }); + + describe('isGroup', () => { + test.each` + member | expected + ${group} | ${true} + ${memberMock} | ${false} + `('returns $expected', ({ member, expected }) => { + expect(isGroup(member)).toBe(expected); + }); + }); + + describe('isDirectMember', () => { + test.each` + sourceId | expected + ${DIRECT_MEMBER_ID} | ${true} + ${INHERITED_MEMBER_ID} | ${false} + `('returns $expected', ({ sourceId, expected }) => { + expect(isDirectMember(memberMock, sourceId)).toBe(expected); + }); + }); + + describe('isCurrentUser', () => { + test.each` + currentUserId | expected + ${IS_CURRENT_USER_ID} | ${true} + ${IS_NOT_CURRENT_USER_ID} | ${false} + `('returns $expected', ({ currentUserId, expected }) => { + expect(isCurrentUser(memberMock, currentUserId)).toBe(expected); + }); + }); + + describe('canRemove', () => { + const memberCanRemove = { + ...memberMock, + canRemove: true, + }; + + test.each` + member | sourceId | expected + ${memberCanRemove} | ${DIRECT_MEMBER_ID} | ${true} + ${memberCanRemove} | ${INHERITED_MEMBER_ID} | ${false} + ${memberMock} | ${INHERITED_MEMBER_ID} | ${false} + `('returns $expected', ({ member, sourceId, expected }) => { + expect(canRemove(member, sourceId)).toBe(expected); + }); + }); + + describe('canResend', () => { + test.each` + member | expected + ${invite} | ${true} + ${{ ...invite, invite: { ...invite.invite, canResend: false } }} | ${false} + `('returns $expected', ({ member, sourceId, expected }) => { + expect(canResend(member, sourceId)).toBe(expected); + }); + }); + + describe('canUpdate', () => { + const memberCanUpdate = { + ...memberMock, + canUpdate: true, + }; + + test.each` + member | currentUserId | sourceId | expected + ${memberCanUpdate} | ${IS_NOT_CURRENT_USER_ID} | ${DIRECT_MEMBER_ID} | ${true} + ${memberCanUpdate} | ${IS_CURRENT_USER_ID} | ${DIRECT_MEMBER_ID} | ${false} + ${memberCanUpdate} | ${IS_CURRENT_USER_ID} | ${INHERITED_MEMBER_ID} | ${false} + ${memberMock} | ${IS_NOT_CURRENT_USER_ID} | ${DIRECT_MEMBER_ID} | ${false} + `('returns $expected', ({ member, currentUserId, sourceId, expected }) => { + expect(canUpdate(member, currentUserId, sourceId)).toBe(expected); + }); + }); + + describe('canOverride', () => { + it('returns `false`', () => { + expect(canOverride(memberMock)).toBe(false); + }); + }); }); diff --git a/spec/graphql/mutations/alert_management/http_integration/destroy_spec.rb b/spec/graphql/mutations/alert_management/http_integration/destroy_spec.rb new file mode 100644 index 00000000000..f74f9186743 --- /dev/null +++ b/spec/graphql/mutations/alert_management/http_integration/destroy_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::AlertManagement::HttpIntegration::Destroy do + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project) } + let(:integration) { create(:alert_management_http_integration, project: project) } + let(:args) { { id: GitlabSchema.id_from_object(integration) } } + + specify { expect(described_class).to require_graphql_authorizations(:admin_operations) } + + describe '#resolve' do + subject(:resolve) { mutation_for(project, current_user).resolve(args) } + + context 'user has access to project' do + before do + project.add_maintainer(current_user) + end + + context 'when HttpIntegrations::DestroyService responds with success' do + it 'returns the integration with no errors' do + expect(resolve).to eq( + integration: integration, + errors: [] + ) + end + end + + context 'when HttpIntegrations::DestroyService responds with an error' do + before do + allow_any_instance_of(::AlertManagement::HttpIntegrations::DestroyService) + .to receive(:execute) + .and_return(ServiceResponse.error(payload: { integration: nil }, message: 'An error has occurred')) + end + + it 'returns errors' do + expect(resolve).to eq( + integration: nil, + errors: ['An error has occurred'] + ) + end + end + end + + context 'when resource is not accessible to the user' do + it 'raises an error if the resource is not accessible to the user' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + end + + private + + def mutation_for(project, user) + described_class.new(object: project, context: { current_user: user }, field: nil) + end +end diff --git a/spec/graphql/resolvers/users_resolver_spec.rb b/spec/graphql/resolvers/users_resolver_spec.rb index e3d595e0790..a18a6dbfa80 100644 --- a/spec/graphql/resolvers/users_resolver_spec.rb +++ b/spec/graphql/resolvers/users_resolver_spec.rb @@ -5,8 +5,8 @@ require 'spec_helper' RSpec.describe Resolvers::UsersResolver do include GraphqlHelpers - let_it_be(:user1) { create(:user) } - let_it_be(:user2) { create(:user) } + let_it_be(:user1) { create(:user, name: "SomePerson") } + let_it_be(:user2) { create(:user, username: "someone123784") } describe '#resolve' do it 'raises an error when read_users_list is not authorized' do @@ -43,6 +43,14 @@ RSpec.describe Resolvers::UsersResolver do ).to contain_exactly(user1, user2) end end + + context 'when a search term is passed' do + it 'returns all users who match', :aggregate_failures do + expect(resolve_users(search: "some")).to contain_exactly(user1, user2) + expect(resolve_users(search: "123784")).to contain_exactly(user2) + expect(resolve_users(search: "someperson")).to contain_exactly(user1) + end + end end def resolve_users(args = {}) diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index 983d1f6ef41..c31ef1aba28 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -236,11 +236,34 @@ RSpec.describe SearchHelper do end describe 'search_entries_empty_message' do - it 'returns the formatted entry message' do - message = search_entries_empty_message('projects', '<h1>foo</h1>') + let!(:group) { build(:group) } + let!(:project) { build(:project, group: group) } - expect(message).to eq("We couldn't find any projects matching <code><h1>foo</h1></code>") - expect(message).to be_html_safe + context 'global search' do + let(:message) { search_entries_empty_message('projects', '<h1>foo</h1>', nil, nil) } + + it 'returns the formatted entry message' do + expect(message).to eq("We couldn't find any projects matching <code><h1>foo</h1></code>") + expect(message).to be_html_safe + end + end + + context 'group search' do + let(:message) { search_entries_empty_message('projects', '<h1>foo</h1>', group, nil) } + + it 'returns the formatted entry message' do + expect(message).to start_with('We couldn't find any projects matching <code><h1>foo</h1></code> in group <a') + expect(message).to be_html_safe + end + end + + context 'project search' do + let(:message) { search_entries_empty_message('projects', '<h1>foo</h1>', group, project) } + + it 'returns the formatted entry message' do + expect(message).to start_with('We couldn't find any projects matching <code><h1>foo</h1></code> in project <a') + expect(message).to be_html_safe + end end end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index f1d7b684016..bbf6ae64cad 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -683,6 +683,7 @@ ProjectCiCdSetting: ProjectSetting: - allow_merge_on_skipped_pipeline - has_confluence +- has_vulnerabilities ProtectedEnvironment: - id - project_id diff --git a/spec/mailers/emails/projects_spec.rb b/spec/mailers/emails/projects_spec.rb index aa5947bf68e..6c23625d4a3 100644 --- a/spec/mailers/emails/projects_spec.rb +++ b/spec/mailers/emails/projects_spec.rb @@ -32,19 +32,13 @@ RSpec.describe Emails::Projects do describe '#prometheus_alert_fired_email' do let(:default_title) { Gitlab::AlertManagement::Payload::Generic::DEFAULT_TITLE } let(:payload) { { 'startsAt' => Time.now.rfc3339 } } - let(:alert_attributes) { build(:alert_management_alert, :from_payload, payload: payload, project: project).attributes } + let(:alert) { create(:alert_management_alert, :from_payload, payload: payload, project: project) } subject do - Notify.prometheus_alert_fired_email(project.id, user.id, alert_attributes) + Notify.prometheus_alert_fired_email(project, user, alert) end - context 'missing required attributes' do - let(:alert_attributes) { build(:alert_management_alert, :prometheus, :from_payload, payload: payload, project: project).attributes } - - it_behaves_like 'no email' - end - - context 'with minimum required attributes' do + context 'with empty payload' do let(:payload) { {} } it_behaves_like 'an email sent from GitLab' @@ -58,6 +52,7 @@ RSpec.describe Emails::Projects do it 'has expected content' do is_expected.to have_body_text('An alert has been triggered') is_expected.to have_body_text(project.full_path) + is_expected.to have_body_text(alert.details_url) is_expected.not_to have_body_text('Description:') is_expected.not_to have_body_text('Environment:') is_expected.not_to have_body_text('Metric:') @@ -78,6 +73,7 @@ RSpec.describe Emails::Projects do it 'has expected content' do is_expected.to have_body_text('An alert has been triggered') is_expected.to have_body_text(project.full_path) + is_expected.to have_body_text(alert.details_url) is_expected.to have_body_text('Description:') is_expected.to have_body_text('alert description') is_expected.not_to have_body_text('Environment:') @@ -101,6 +97,7 @@ RSpec.describe Emails::Projects do it 'has expected content' do is_expected.to have_body_text('An alert has been triggered') is_expected.to have_body_text(project.full_path) + is_expected.to have_body_text(alert.details_url) is_expected.to have_body_text('Environment:') is_expected.to have_body_text(environment.name) is_expected.not_to have_body_text('Description:') @@ -112,7 +109,7 @@ RSpec.describe Emails::Projects do let_it_be(:prometheus_alert) { create(:prometheus_alert, project: project) } let_it_be(:environment) { prometheus_alert.environment } - let(:alert_attributes) { build(:alert_management_alert, :prometheus, :from_payload, payload: payload, project: project).attributes } + let(:alert) { create(:alert_management_alert, :prometheus, :from_payload, payload: payload, project: project) } let(:title) { "#{prometheus_alert.title} #{prometheus_alert.computed_operator} #{prometheus_alert.threshold}" } let(:metrics_url) { metrics_project_environment_url(project, environment) } @@ -135,6 +132,7 @@ RSpec.describe Emails::Projects do it 'has expected content' do is_expected.to have_body_text('An alert has been triggered') is_expected.to have_body_text(project.full_path) + is_expected.to have_body_text(alert.details_url) is_expected.to have_body_text('Environment:') is_expected.to have_body_text(environment.name) is_expected.to have_body_text('Metric:') @@ -143,5 +141,23 @@ RSpec.describe Emails::Projects do is_expected.not_to have_body_text('Description:') end end + + context 'resolved' do + let_it_be(:alert) { create(:alert_management_alert, :resolved, project: project) } + + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' + + it 'has expected subject' do + is_expected.to have_subject("#{project.name} | Alert: #{alert.title}") + end + + it 'has expected content' do + is_expected.to have_body_text('An alert has been resolved') + is_expected.to have_body_text(project.full_path) + is_expected.to have_body_text(alert.details_url) + end + end end end diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index f77f127ddc8..df6456ab55d 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -73,18 +73,20 @@ RSpec.describe API::Files do describe "HEAD /projects/:id/repository/files/:file_path" do shared_examples_for 'repository files' do + let(:options) { {} } + it 'returns 400 when file path is invalid' do - head api(route(rouge_file_path), current_user), params: params + head api(route(rouge_file_path), current_user, **options), params: params expect(response).to have_gitlab_http_status(:bad_request) end it_behaves_like 'when path is absolute' do - subject { head api(route(absolute_path), current_user), params: params } + subject { head api(route(absolute_path), current_user, **options), params: params } end it 'returns file attributes in headers' do - head api(route(file_path), current_user), params: params + head api(route(file_path), current_user, **options), params: params expect(response).to have_gitlab_http_status(:ok) expect(response.headers['X-Gitlab-File-Path']).to eq(CGI.unescape(file_path)) @@ -98,7 +100,7 @@ RSpec.describe API::Files do file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee" params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" - head api(route(file_path), current_user), params: params + head api(route(file_path), current_user, **options), params: params expect(response).to have_gitlab_http_status(:ok) expect(response.headers['X-Gitlab-File-Name']).to eq('commit.js.coffee') @@ -107,7 +109,7 @@ RSpec.describe API::Files do context 'when mandatory params are not given' do it "responds with a 400 status" do - head api(route("any%2Ffile"), current_user) + head api(route("any%2Ffile"), current_user, **options) expect(response).to have_gitlab_http_status(:bad_request) end @@ -117,7 +119,7 @@ RSpec.describe API::Files do it "responds with a 404 status" do params[:ref] = 'master' - head api(route('app%2Fmodels%2Fapplication%2Erb'), current_user), params: params + head api(route('app%2Fmodels%2Fapplication%2Erb'), current_user, **options), params: params expect(response).to have_gitlab_http_status(:not_found) end @@ -127,7 +129,7 @@ RSpec.describe API::Files do include_context 'disabled repository' it "responds with a 403 status" do - head api(route(file_path), current_user), params: params + head api(route(file_path), current_user, **options), params: params expect(response).to have_gitlab_http_status(:forbidden) end @@ -154,8 +156,8 @@ RSpec.describe API::Files do context 'when PATs are used' do it_behaves_like 'repository files' do let(:token) { create(:personal_access_token, scopes: ['read_repository'], user: user) } - let(:current_user) { user } - let(:api_user) { { personal_access_token: token } } + let(:current_user) { nil } + let(:options) { { personal_access_token: token } } end end @@ -174,21 +176,21 @@ RSpec.describe API::Files do describe "GET /projects/:id/repository/files/:file_path" do shared_examples_for 'repository files' do - let(:api_user) { current_user } + let(:options) { {} } it 'returns 400 for invalid file path' do - get api(route(rouge_file_path), api_user), params: params + get api(route(rouge_file_path), api_user, **options), params: params expect(response).to have_gitlab_http_status(:bad_request) expect(json_response['error']).to eq(invalid_file_message) end it_behaves_like 'when path is absolute' do - subject { get api(route(absolute_path), api_user), params: params } + subject { get api(route(absolute_path), api_user, **options), params: params } end it 'returns file attributes as json' do - get api(route(file_path), api_user), params: params + get api(route(file_path), api_user, **options), params: params expect(response).to have_gitlab_http_status(:ok) expect(json_response['file_path']).to eq(CGI.unescape(file_path)) @@ -201,10 +203,10 @@ RSpec.describe API::Files do it 'returns json when file has txt extension' do file_path = "bar%2Fbranch-test.txt" - get api(route(file_path), api_user), params: params + get api(route(file_path), api_user, **options), params: params expect(response).to have_gitlab_http_status(:ok) - expect(response.content_type).to eq('application/json') + expect(response.media_type).to eq('application/json') end context 'with filename with pathspec characters' do @@ -218,7 +220,7 @@ RSpec.describe API::Files do it 'returns JSON wth commit SHA' do params[:ref] = 'master' - get api(route(file_path), api_user), params: params + get api(route(file_path), api_user, **options), params: params expect(response).to have_gitlab_http_status(:ok) expect(json_response['file_path']).to eq(file_path) @@ -232,7 +234,7 @@ RSpec.describe API::Files do file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee" params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" - get api(route(file_path), api_user), params: params + get api(route(file_path), api_user, **options), params: params expect(response).to have_gitlab_http_status(:ok) expect(json_response['file_name']).to eq('commit.js.coffee') @@ -244,7 +246,7 @@ RSpec.describe API::Files do url = route(file_path) + "/raw" expect(Gitlab::Workhorse).to receive(:send_git_blob) - get api(url, api_user), params: params + get api(url, api_user, **options), params: params expect(response).to have_gitlab_http_status(:ok) expect(headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" @@ -253,7 +255,7 @@ RSpec.describe API::Files do it 'returns blame file info' do url = route(file_path) + '/blame' - get api(url, api_user), params: params + get api(url, api_user, **options), params: params expect(response).to have_gitlab_http_status(:ok) end @@ -261,14 +263,14 @@ RSpec.describe API::Files do it 'sets inline content disposition by default' do url = route(file_path) + "/raw" - get api(url, api_user), params: params + get api(url, api_user, **options), params: params expect(headers['Content-Disposition']).to eq(%q(inline; filename="popen.rb"; filename*=UTF-8''popen.rb)) end context 'when mandatory params are not given' do it_behaves_like '400 response' do - let(:request) { get api(route("any%2Ffile"), current_user) } + let(:request) { get api(route("any%2Ffile"), current_user, **options) } end end @@ -276,7 +278,7 @@ RSpec.describe API::Files do let(:params) { { ref: 'master' } } it_behaves_like '404 response' do - let(:request) { get api(route('app%2Fmodels%2Fapplication%2Erb'), api_user), params: params } + let(:request) { get api(route('app%2Fmodels%2Fapplication%2Erb'), api_user, **options), params: params } let(:message) { '404 File Not Found' } end end @@ -285,7 +287,7 @@ RSpec.describe API::Files do include_context 'disabled repository' it_behaves_like '403 response' do - let(:request) { get api(route(file_path), api_user), params: params } + let(:request) { get api(route(file_path), api_user, **options), params: params } end end end @@ -294,6 +296,7 @@ RSpec.describe API::Files do it_behaves_like 'repository files' do let(:project) { create(:project, :public, :repository) } let(:current_user) { nil } + let(:api_user) { nil } end end @@ -301,7 +304,8 @@ RSpec.describe API::Files do it_behaves_like 'repository files' do let(:token) { create(:personal_access_token, scopes: ['read_repository'], user: user) } let(:current_user) { user } - let(:api_user) { { personal_access_token: token } } + let(:api_user) { nil } + let(:options) { { personal_access_token: token } } end end @@ -315,6 +319,7 @@ RSpec.describe API::Files do context 'when authenticated', 'as a developer' do it_behaves_like 'repository files' do let(:current_user) { user } + let(:api_user) { user } end end @@ -687,7 +692,7 @@ RSpec.describe API::Files do post api(route("new_file_with_author%2Etxt"), user), params: params expect(response).to have_gitlab_http_status(:created) - expect(response.content_type).to eq('application/json') + expect(response.media_type).to eq('application/json') last_commit = project.repository.commit.raw expect(last_commit.author_email).to eq(author_email) expect(last_commit.author_name).to eq(author_name) diff --git a/spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb b/spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb new file mode 100644 index 00000000000..1ecb5c76b57 --- /dev/null +++ b/spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Removing an HTTP Integration' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:integration) { create(:alert_management_http_integration, project: project) } + + let(:mutation) do + variables = { + id: GitlabSchema.id_from_object(integration).to_s + } + graphql_mutation(:http_integration_destroy, variables) do + <<~QL + clientMutationId + errors + integration { + id + type + name + active + token + url + apiUrl + } + QL + end + end + + let(:mutation_response) { graphql_mutation_response(:http_integration_destroy) } + + before do + project.add_maintainer(user) + end + + it 'removes the integration' do + post_graphql_mutation(mutation, current_user: user) + + integration_response = mutation_response['integration'] + + expect(response).to have_gitlab_http_status(:success) + expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s) + expect(integration_response['type']).to eq('HTTP') + expect(integration_response['name']).to eq(integration.name) + expect(integration_response['active']).to eq(integration.active) + expect(integration_response['token']).to eq(integration.token) + expect(integration_response['url']).to eq(integration.url) + expect(integration_response['apiUrl']).to eq(nil) + + expect { integration.reload }.to raise_error ActiveRecord::RecordNotFound + end +end diff --git a/spec/services/alert_management/http_integrations/destroy_service_spec.rb b/spec/services/alert_management/http_integrations/destroy_service_spec.rb new file mode 100644 index 00000000000..4e64ad9145b --- /dev/null +++ b/spec/services/alert_management/http_integrations/destroy_service_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe AlertManagement::HttpIntegrations::DestroyService do + let_it_be(:user_with_permissions) { create(:user) } + let_it_be(:user_without_permissions) { create(:user) } + let_it_be(:project) { create(:project) } + + let!(:integration) { create(:alert_management_http_integration, project: project) } + let(:current_user) { user_with_permissions } + let(:params) { {} } + let(:service) { described_class.new(integration, current_user) } + + before_all do + project.add_maintainer(user_with_permissions) + end + + describe '#execute' do + shared_examples 'error response' do |message| + it 'has an informative message' do + expect(response).to be_error + expect(response.message).to eq(message) + end + end + + subject(:response) { service.execute } + + context 'when the current_user is anonymous' do + let(:current_user) { nil } + + it_behaves_like 'error response', 'You have insufficient permissions to remove this HTTP integration' + end + + context 'when current_user does not have permission to create integrations' do + let(:current_user) { user_without_permissions } + + it_behaves_like 'error response', 'You have insufficient permissions to remove this HTTP integration' + end + + context 'when feature flag is not enabled' do + before do + stub_feature_flags(multiple_http_integrations: false) + end + + it_behaves_like 'error response', 'Removing integrations is not supported for this project' + end + + context 'when an error occurs during removal' do + before do + allow(integration).to receive(:destroy).and_return(false) + integration.errors.add(:name, 'cannot be removed') + end + + it_behaves_like 'error response', 'Name cannot be removed' + end + + it 'successfully returns the integration' do + expect(response).to be_success + + integration_result = response.payload[:integration] + expect(integration_result).to be_a(::AlertManagement::HttpIntegration) + expect(integration_result.name).to eq(integration.name) + expect(integration_result.active).to eq(integration.active) + expect(integration_result.token).to eq(integration.token) + expect(integration_result.endpoint_identifier).to eq(integration.endpoint_identifier) + + expect { integration.reload }.to raise_error ActiveRecord::RecordNotFound + end + end +end diff --git a/spec/services/alert_management/process_prometheus_alert_service_spec.rb b/spec/services/alert_management/process_prometheus_alert_service_spec.rb index ae0b8d6d7ac..2f920de7fc7 100644 --- a/spec/services/alert_management/process_prometheus_alert_service_spec.rb +++ b/spec/services/alert_management/process_prometheus_alert_service_spec.rb @@ -11,9 +11,16 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do describe '#execute' do let(:service) { described_class.new(project, nil, payload) } - let(:incident_management_setting) { double(auto_close_incident?: auto_close_incident, create_issue?: create_issue) } let(:auto_close_incident) { true } let(:create_issue) { true } + let(:send_email) { true } + let(:incident_management_setting) do + double( + auto_close_incident?: auto_close_incident, + create_issue?: create_issue, + send_email?: send_email + ) + end before do allow(service) @@ -55,6 +62,7 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do it_behaves_like 'adds an alert management alert event' it_behaves_like 'processes incident issues' + it_behaves_like 'Alert Notification Service sends notification email' context 'existing alert is resolved' do let!(:alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: fingerprint) } @@ -92,28 +100,48 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do end end - context 'when auto-alert creation is disabled' do + context 'when auto-creation of issues is disabled' do let(:create_issue) { false } it_behaves_like 'does not process incident issues' end + + context 'when emails are disabled' do + let(:send_email) { false } + + it 'does not send notification' do + expect(NotificationService).not_to receive(:new) + + expect(subject).to be_success + end + end end context 'when alert does not exist' do context 'when alert can be created' do it_behaves_like 'creates an alert management alert' + it_behaves_like 'Alert Notification Service sends notification email' + it_behaves_like 'processes incident issues' it 'creates a system note corresponding to alert creation' do expect { subject }.to change(Note, :count).by(1) end - it_behaves_like 'processes incident issues' - context 'when auto-alert creation is disabled' do let(:create_issue) { false } it_behaves_like 'does not process incident issues' end + + context 'when emails are disabled' do + let(:send_email) { false } + + it 'does not send notification' do + expect(NotificationService).not_to receive(:new) + + expect(subject).to be_success + end + end end context 'when alert cannot be created' do @@ -125,6 +153,9 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do allow(service).to receive_message_chain(:alert, :errors).and_return(errors) end + it_behaves_like 'Alert Notification Service sends no notifications', http_status: :bad_request + it_behaves_like 'does not process incident issues due to error', http_status: :bad_request + it 'writes a warning to the log' do expect(Gitlab::AppLogger).to receive(:warn).with( message: 'Unable to create AlertManagement::Alert', @@ -134,8 +165,6 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do execute end - - it_behaves_like 'does not process incident issues' end it { is_expected.to be_success } @@ -148,6 +177,9 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do context 'when auto_resolve_incident set to true' do context 'when status can be changed' do + it_behaves_like 'Alert Notification Service sends notification email' + it_behaves_like 'does not process incident issues' + it 'resolves an existing alert' do expect { execute }.to change { alert.reload.resolved? }.to(true) end @@ -185,6 +217,8 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do execute end + + it_behaves_like 'Alert Notification Service sends notification email' end it { is_expected.to be_success } @@ -197,6 +231,16 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do expect { execute }.not_to change { alert.reload.resolved? } end end + + context 'when emails are disabled' do + let(:send_email) { false } + + it 'does not send notification' do + expect(NotificationService).not_to receive(:new) + + expect(subject).to be_success + end + end end context 'environment given' do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 99f52e01031..e29cbd80a57 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -3100,26 +3100,26 @@ RSpec.describe NotificationService, :mailer do end describe '#prometheus_alerts_fired' do - let!(:project) { create(:project) } - let!(:master) { create(:user) } - let!(:developer) { create(:user) } - let(:alert_attributes) { build(:alert_management_alert, project: project).attributes } + let_it_be(:project) { create(:project) } + let_it_be(:master) { create(:user) } + let_it_be(:developer) { create(:user) } + let_it_be(:alert) { create(:alert_management_alert, project: project) } before do project.add_maintainer(master) end it 'sends the email to owners and masters' do - expect(Notify).to receive(:prometheus_alert_fired_email).with(project.id, master.id, alert_attributes).and_call_original - expect(Notify).to receive(:prometheus_alert_fired_email).with(project.id, project.owner.id, alert_attributes).and_call_original - expect(Notify).not_to receive(:prometheus_alert_fired_email).with(project.id, developer.id, alert_attributes) + expect(Notify).to receive(:prometheus_alert_fired_email).with(project, master, alert).and_call_original + expect(Notify).to receive(:prometheus_alert_fired_email).with(project, project.owner, alert).and_call_original + expect(Notify).not_to receive(:prometheus_alert_fired_email).with(project, developer, alert) - subject.prometheus_alerts_fired(project, [alert_attributes]) + subject.prometheus_alerts_fired(project, [alert]) end it_behaves_like 'project emails are disabled' do let(:notification_target) { project } - let(:notification_trigger) { subject.prometheus_alerts_fired(project, [alert_attributes]) } + let(:notification_trigger) { subject.prometheus_alerts_fired(project, [alert]) } around do |example| perform_enqueued_jobs { example.run } diff --git a/spec/services/projects/alerting/notify_service_spec.rb b/spec/services/projects/alerting/notify_service_spec.rb index 95aa7cc3ea4..786280e8250 100644 --- a/spec/services/projects/alerting/notify_service_spec.rb +++ b/spec/services/projects/alerting/notify_service_spec.rb @@ -129,6 +129,12 @@ RSpec.describe Projects::Alerting::NotifyService do it { expect { subject }.to change { issue.reload.state }.from('opened').to('closed') } it { expect { subject }.to change(ResourceStateEvent, :count).by(1) } end + + context 'with issue enabled' do + let(:issue_enabled) { true } + + it_behaves_like 'does not process incident issues' + end end end diff --git a/spec/services/projects/cleanup_service_spec.rb b/spec/services/projects/cleanup_service_spec.rb index 7c28b729e84..eeb1b2e3e6c 100644 --- a/spec/services/projects/cleanup_service_spec.rb +++ b/spec/services/projects/cleanup_service_spec.rb @@ -3,14 +3,84 @@ require 'spec_helper' RSpec.describe Projects::CleanupService do - let(:project) { create(:project, :repository, bfg_object_map: fixture_file_upload('spec/fixtures/bfg_object_map.txt')) } - let(:object_map) { project.bfg_object_map } + subject(:service) { described_class.new(project) } - let(:cleaner) { service.__send__(:repository_cleaner) } + describe '.enqueue' do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } - subject(:service) { described_class.new(project) } + let(:object_map_file) { fixture_file_upload('spec/fixtures/bfg_object_map.txt') } + + subject(:enqueue) { described_class.enqueue(project, user, object_map_file) } + + it 'makes the repository read-only' do + expect { enqueue } + .to change(project, :repository_read_only?) + .from(false) + .to(true) + end + + it 'sets the bfg_object_map of the project' do + enqueue + + expect(project.bfg_object_map.read).to eq(object_map_file.read) + end + + it 'enqueues a RepositoryCleanupWorker' do + enqueue + + expect(RepositoryCleanupWorker.jobs.count).to eq(1) + end + + it 'returns success' do + expect(enqueue[:status]).to eq(:success) + end + + it 'returns an error if making the repository read-only fails' do + project.set_repository_read_only! + + expect(enqueue[:status]).to eq(:error) + end + + it 'returns an error if updating the project fails' do + expect_next_instance_of(Projects::UpdateService) do |service| + expect(service).to receive(:execute).and_return(status: :error) + end + + expect(enqueue[:status]).to eq(:error) + expect(project.reload.repository_read_only?).to be_falsy + end + end + + describe '.cleanup_after' do + let(:project) { create(:project, :repository, bfg_object_map: fixture_file_upload('spec/fixtures/bfg_object_map.txt')) } + + subject(:cleanup_after) { described_class.cleanup_after(project) } + + before do + project.set_repository_read_only! + end + + it 'sets the repository read-write' do + expect { cleanup_after }.to change(project, :repository_read_only?).from(true).to(false) + end + + it 'removes the BFG object map' do + cleanup_after + + expect(project.bfg_object_map).not_to be_exist + end + end describe '#execute' do + let(:project) { create(:project, :repository, bfg_object_map: fixture_file_upload('spec/fixtures/bfg_object_map.txt')) } + let(:object_map) { project.bfg_object_map } + let(:cleaner) { service.__send__(:repository_cleaner) } + + before do + project.set_repository_read_only! + end + it 'runs the apply_bfg_object_map_stream gitaly RPC' do expect(cleaner).to receive(:apply_bfg_object_map_stream).with(kind_of(IO)) @@ -37,6 +107,13 @@ RSpec.describe Projects::CleanupService do expect(object_map.exists?).to be_falsy end + it 'makes the repository read-write again' do + expect { service.execute } + .to change(project, :repository_read_only?) + .from(true) + .to(false) + end + context 'with a tainted merge request diff' do let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:diff) { merge_request.merge_request_diff } diff --git a/spec/workers/repository_cleanup_worker_spec.rb b/spec/workers/repository_cleanup_worker_spec.rb index f5887d08bd2..2b700b944d2 100644 --- a/spec/workers/repository_cleanup_worker_spec.rb +++ b/spec/workers/repository_cleanup_worker_spec.rb @@ -40,6 +40,8 @@ RSpec.describe RepositoryCleanupWorker do describe '#sidekiq_retries_exhausted' do let(:job) { { 'args' => [project.id, user.id], 'error_message' => 'Error' } } + subject(:sidekiq_retries_exhausted) { described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new) } + it 'does not send a failure notification for a RecordNotFound error' do expect(NotificationService).not_to receive(:new) @@ -51,7 +53,13 @@ RSpec.describe RepositoryCleanupWorker do expect(service).to receive(:repository_cleanup_failure).with(project, user, 'Error') end - described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new) + sidekiq_retries_exhausted + end + + it 'cleans up the attempt' do + expect(Projects::CleanupService).to receive(:cleanup_after).with(project) + + sidekiq_retries_exhausted end end end |