summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-11-03 18:09:22 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-11-03 18:09:22 +0000
commit62baa95f25f1cc56b100d2b64b0a3906f47dcfe1 (patch)
tree0bee30bc13c3cb7444f1d89d2647719718a31d76 /spec
parentff8eb438401fc82b883fc4ae69626f0035b69236 (diff)
downloadgitlab-ce-62baa95f25f1cc56b100d2b64b0a3906f47dcfe1.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/projects/settings/repository_controller_spec.rb10
-rw-r--r--spec/frontend/groups/components/app_spec.js124
-rw-r--r--spec/frontend/groups/components/item_actions_spec.js89
-rw-r--r--spec/frontend/monitoring/components/charts/column_spec.js3
-rw-r--r--spec/frontend/monitoring/components/charts/stacked_column_spec.js12
-rw-r--r--spec/frontend/projects/pipelines/charts/components/app_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/members/table/members_table_spec.js73
-rw-r--r--spec/frontend/vue_shared/components/members/utils_spec.js97
-rw-r--r--spec/graphql/mutations/alert_management/http_integration/destroy_spec.rb58
-rw-r--r--spec/graphql/resolvers/users_resolver_spec.rb12
-rw-r--r--spec/helpers/search_helper_spec.rb31
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/mailers/emails/projects_spec.rb36
-rw-r--r--spec/requests/api/files_spec.rb55
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/http_integration/destroy_spec.rb55
-rw-r--r--spec/services/alert_management/http_integrations/destroy_service_spec.rb71
-rw-r--r--spec/services/alert_management/process_prometheus_alert_service_spec.rb56
-rw-r--r--spec/services/notification_service_spec.rb18
-rw-r--r--spec/services/projects/alerting/notify_service_spec.rb6
-rw-r--r--spec/services/projects/cleanup_service_spec.rb85
-rw-r--r--spec/workers/repository_cleanup_worker_spec.rb10
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>&lt;h1&gt;foo&lt;/h1&gt;</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&#39;t find any projects matching <code>&lt;h1&gt;foo&lt;/h1&gt;</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&#39;t find any projects matching <code>&lt;h1&gt;foo&lt;/h1&gt;</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&#39;t find any projects matching <code>&lt;h1&gt;foo&lt;/h1&gt;</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