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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-08-03 15:09:25 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-08-03 15:09:25 +0300
commitaeee636c18f82107ec7a489f33c944c65ad5f34e (patch)
tree2c30286279e096c9114e9a41a3ed07a83293c059 /spec
parent3d8459c18b7a20d9142359bb9334b467e774eb36 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/frontend/boards/cache_updates_spec.js37
-rw-r--r--spec/frontend/boards/components/board_add_new_column_spec.js65
-rw-r--r--spec/frontend/boards/components/board_app_spec.js25
-rw-r--r--spec/frontend/boards/components/board_content_spec.js64
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js56
-rw-r--r--spec/frontend/boards/components/board_settings_sidebar_spec.js33
-rw-r--r--spec/frontend/boards/components/board_top_bar_spec.js40
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js34
-rw-r--r--spec/frontend/jobs/components/log/line_header_spec.js29
-rw-r--r--spec/frontend/jobs/components/log/line_spec.js47
-rw-r--r--spec/frontend/repository/components/blob_button_group_spec.js4
-rw-r--r--spec/frontend/repository/components/blob_viewers/index_spec.js11
-rw-r--r--spec/frontend/repository/components/delete_blob_modal_spec.js44
-rw-r--r--spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js14
-rw-r--r--spec/lib/gitlab/database/click_house_client_spec.rb7
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/batched_background_migration_failed_jobs_metric_spec.rb40
-rw-r--r--spec/lib/peek/views/click_house_spec.rb37
-rw-r--r--spec/models/project_spec.rb26
-rw-r--r--spec/services/web_hook_service_spec.rb27
-rw-r--r--spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb18
-rw-r--r--spec/workers/users/deactivate_dormant_users_worker_spec.rb55
22 files changed, 649 insertions, 78 deletions
diff --git a/spec/frontend/boards/cache_updates_spec.js b/spec/frontend/boards/cache_updates_spec.js
new file mode 100644
index 00000000000..bc661f20451
--- /dev/null
+++ b/spec/frontend/boards/cache_updates_spec.js
@@ -0,0 +1,37 @@
+import * as Sentry from '@sentry/browser';
+import { setError } from '~/boards/graphql/cache_updates';
+import { defaultClient } from '~/graphql_shared/issuable_client';
+import setErrorMutation from '~/boards/graphql/client/set_error.mutation.graphql';
+
+describe('setError', () => {
+ let sentryCaptureExceptionSpy;
+ const errorMessage = 'Error';
+ const error = new Error(errorMessage);
+
+ beforeEach(() => {
+ jest.spyOn(defaultClient, 'mutate').mockResolvedValue();
+ sentryCaptureExceptionSpy = jest.spyOn(Sentry, 'captureException');
+ });
+
+ it('calls setErrorMutation and capture Sentry error', () => {
+ setError({ message: errorMessage, error });
+
+ expect(defaultClient.mutate).toHaveBeenCalledWith({
+ mutation: setErrorMutation,
+ variables: { error: errorMessage },
+ });
+
+ expect(sentryCaptureExceptionSpy).toHaveBeenCalledWith(error);
+ });
+
+ it('does not capture Sentry error when captureError is false', () => {
+ setError({ message: errorMessage, error, captureError: false });
+
+ expect(defaultClient.mutate).toHaveBeenCalledWith({
+ mutation: setErrorMutation,
+ variables: { error: errorMessage },
+ });
+
+ expect(sentryCaptureExceptionSpy).not.toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/boards/components/board_add_new_column_spec.js b/spec/frontend/boards/components/board_add_new_column_spec.js
index 8d6cc9373af..af76ad7bcac 100644
--- a/spec/frontend/boards/components/board_add_new_column_spec.js
+++ b/spec/frontend/boards/components/board_add_new_column_spec.js
@@ -3,12 +3,14 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import BoardAddNewColumn from '~/boards/components/board_add_new_column.vue';
import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue';
import defaultState from '~/boards/stores/state';
import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql';
import boardLabelsQuery from '~/boards/graphql/board_labels.query.graphql';
+import * as cacheUpdates from '~/boards/graphql/cache_updates';
import {
mockLabelList,
createBoardListResponse,
@@ -21,13 +23,14 @@ Vue.use(VueApollo);
describe('BoardAddNewColumn', () => {
let wrapper;
+ let mockApollo;
const createBoardListQueryHandler = jest.fn().mockResolvedValue(createBoardListResponse);
const labelsQueryHandler = jest.fn().mockResolvedValue(labelsQueryResponse);
- const mockApollo = createMockApollo([
- [boardLabelsQuery, labelsQueryHandler],
- [createBoardListMutation, createBoardListQueryHandler],
- ]);
+ const errorMessage = 'Failed to create list';
+ const createBoardListQueryHandlerFailure = jest.fn().mockRejectedValue(new Error(errorMessage));
+ const errorMessageLabels = 'Failed to fetch labels';
+ const labelsQueryHandlerFailure = jest.fn().mockRejectedValue(new Error(errorMessageLabels));
const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
const findAddNewColumnForm = () => wrapper.findComponent(BoardAddNewColumnForm);
@@ -53,7 +56,14 @@ describe('BoardAddNewColumn', () => {
actions = {},
provide = {},
lists = {},
+ labelsHandler = labelsQueryHandler,
+ createHandler = createBoardListQueryHandler,
} = {}) => {
+ mockApollo = createMockApollo([
+ [boardLabelsQuery, labelsHandler],
+ [createBoardListMutation, createHandler],
+ ]);
+
wrapper = shallowMountExtended(BoardAddNewColumn, {
apolloProvider: mockApollo,
propsData: {
@@ -111,6 +121,10 @@ describe('BoardAddNewColumn', () => {
mockApollo.clients.defaultClient.cache.writeQuery = jest.fn();
};
+ beforeEach(() => {
+ cacheUpdates.setError = jest.fn();
+ });
+
describe('Add list button', () => {
it('calls addList', async () => {
const getListByLabelId = jest.fn().mockReturnValue(null);
@@ -208,11 +222,52 @@ describe('BoardAddNewColumn', () => {
findAddNewColumnForm().vm.$emit('add-list');
- await nextTick();
+ await waitForPromises();
expect(wrapper.emitted('highlight-list')).toEqual([[mockLabelList.id]]);
expect(createBoardListQueryHandler).not.toHaveBeenCalledWith();
});
});
+
+ describe('when fetch labels query fails', () => {
+ beforeEach(() => {
+ mountComponent({
+ provide: { isApolloBoard: true },
+ labelsHandler: labelsQueryHandlerFailure,
+ });
+ });
+
+ it('sets error', async () => {
+ findDropdown().vm.$emit('show');
+
+ await waitForPromises();
+ expect(cacheUpdates.setError).toHaveBeenCalled();
+ });
+ });
+
+ describe('when create list mutation fails', () => {
+ beforeEach(() => {
+ mountComponent({
+ selectedId: mockLabelList.label.id,
+ provide: { isApolloBoard: true },
+ createHandler: createBoardListQueryHandlerFailure,
+ });
+ });
+
+ it('sets error', async () => {
+ findDropdown().vm.$emit('show');
+
+ await nextTick();
+ expect(labelsQueryHandler).toHaveBeenCalled();
+
+ selectLabel(mockLabelList.label.id);
+
+ findAddNewColumnForm().vm.$emit('add-list');
+
+ await waitForPromises();
+
+ expect(cacheUpdates.setError).toHaveBeenCalled();
+ });
+ });
});
});
diff --git a/spec/frontend/boards/components/board_app_spec.js b/spec/frontend/boards/components/board_app_spec.js
index e7624437ac5..77ec260745c 100644
--- a/spec/frontend/boards/components/board_app_spec.js
+++ b/spec/frontend/boards/components/board_app_spec.js
@@ -9,13 +9,17 @@ import BoardApp from '~/boards/components/board_app.vue';
import eventHub from '~/boards/eventhub';
import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
+import * as cacheUpdates from '~/boards/graphql/cache_updates';
import { rawIssue, boardListsQueryResponse } from '../mock_data';
describe('BoardApp', () => {
let wrapper;
let store;
+ let mockApollo;
+
+ const errorMessage = 'Failed to fetch lists';
const boardListQueryHandler = jest.fn().mockResolvedValue(boardListsQueryResponse);
- const mockApollo = createMockApollo([[boardListsQuery, boardListQueryHandler]]);
+ const boardListQueryHandlerFailure = jest.fn().mockRejectedValue(new Error(errorMessage));
Vue.use(Vuex);
Vue.use(VueApollo);
@@ -33,7 +37,12 @@ describe('BoardApp', () => {
});
};
- const createComponent = ({ isApolloBoard = false, issue = rawIssue } = {}) => {
+ const createComponent = ({
+ isApolloBoard = false,
+ issue = rawIssue,
+ handler = boardListQueryHandler,
+ } = {}) => {
+ mockApollo = createMockApollo([[boardListsQuery, handler]]);
mockApollo.clients.defaultClient.cache.writeQuery({
query: activeBoardItemQuery,
data: {
@@ -57,6 +66,10 @@ describe('BoardApp', () => {
});
};
+ beforeEach(() => {
+ cacheUpdates.setError = jest.fn();
+ });
+
afterEach(() => {
store = null;
});
@@ -104,5 +117,13 @@ describe('BoardApp', () => {
expect(eventHub.$on).toHaveBeenCalledWith('updateBoard', wrapper.vm.refetchLists);
});
+
+ it('sets error on fetch lists failure', async () => {
+ createComponent({ isApolloBoard: true, handler: boardListQueryHandlerFailure });
+
+ await waitForPromises();
+
+ expect(cacheUpdates.setError).toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index 0a2a78479fb..38daef28149 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -1,7 +1,7 @@
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Draggable from 'vuedraggable';
import Vuex from 'vuex';
@@ -10,6 +10,7 @@ import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
import getters from 'ee_else_ce/boards/stores/getters';
+import * as cacheUpdates from '~/boards/graphql/cache_updates';
import BoardColumn from '~/boards/components/board_column.vue';
import BoardContent from '~/boards/components/board_content.vue';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
@@ -36,6 +37,8 @@ describe('BoardContent', () => {
let mockApollo;
const updateListHandler = jest.fn().mockResolvedValue(updateBoardListResponse);
+ const errorMessage = 'Failed to update list';
+ const updateListHandlerFailure = jest.fn().mockRejectedValue(new Error(errorMessage));
const defaultState = {
isShowingEpicsSwimlanes: false,
@@ -60,8 +63,9 @@ describe('BoardContent', () => {
issuableType = 'issue',
isIssueBoard = true,
isEpicBoard = false,
+ handler = updateListHandler,
} = {}) => {
- mockApollo = createMockApollo([[updateBoardListMutation, updateListHandler]]);
+ mockApollo = createMockApollo([[updateBoardListMutation, handler]]);
const listQueryVariables = { isProject: true };
mockApollo.clients.defaultClient.writeQuery({
@@ -107,6 +111,11 @@ describe('BoardContent', () => {
const findBoardColumns = () => wrapper.findAllComponents(BoardColumn);
const findBoardAddNewColumn = () => wrapper.findComponent(BoardAddNewColumn);
const findDraggable = () => wrapper.findComponent(Draggable);
+ const findError = () => wrapper.findComponent(GlAlert);
+
+ beforeEach(() => {
+ cacheUpdates.setError = jest.fn();
+ });
describe('default', () => {
beforeEach(() => {
@@ -123,7 +132,7 @@ describe('BoardContent', () => {
it('does not display EpicsSwimlanes component', () => {
expect(wrapper.findComponent(EpicsSwimlanes).exists()).toBe(false);
- expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
+ expect(findError().exists()).toBe(false);
});
it('sets delay and delayOnTouchOnly attributes on board list', () => {
@@ -169,6 +178,18 @@ describe('BoardContent', () => {
});
describe('when Apollo boards FF is on', () => {
+ const moveList = () => {
+ const movableListsOrder = [mockLists[0].id, mockLists[1].id];
+
+ findDraggable().vm.$emit('end', {
+ item: { dataset: { listId: mockLists[0].id, draggableItemType: DraggableItemTypes.list } },
+ newIndex: 1,
+ to: {
+ children: movableListsOrder.map((listId) => ({ dataset: { listId } })),
+ },
+ });
+ };
+
beforeEach(async () => {
createComponent({ isApolloBoard: true });
await waitForPromises();
@@ -183,19 +204,38 @@ describe('BoardContent', () => {
});
it('reorders lists', async () => {
- const movableListsOrder = [mockLists[0].id, mockLists[1].id];
-
- findDraggable().vm.$emit('end', {
- item: { dataset: { listId: mockLists[0].id, draggableItemType: DraggableItemTypes.list } },
- newIndex: 1,
- to: {
- children: movableListsOrder.map((listId) => ({ dataset: { listId } })),
- },
- });
+ moveList();
await waitForPromises();
expect(updateListHandler).toHaveBeenCalled();
});
+
+ it('sets error on reorder lists failure', async () => {
+ createComponent({ isApolloBoard: true, handler: updateListHandlerFailure });
+
+ moveList();
+ await waitForPromises();
+
+ expect(cacheUpdates.setError).toHaveBeenCalled();
+ });
+
+ describe('when error is passed', () => {
+ beforeEach(async () => {
+ createComponent({ isApolloBoard: true, props: { apolloError: 'Error' } });
+ await waitForPromises();
+ });
+
+ it('displays error banner', () => {
+ expect(findError().exists()).toBe(true);
+ });
+
+ it('dismisses error', async () => {
+ findError().vm.$emit('dismiss');
+ await nextTick();
+
+ expect(cacheUpdates.setError).toHaveBeenCalledWith({ message: null, captureError: false });
+ });
+ });
});
describe('when "add column" form is visible', () => {
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index 0c9e1b4646e..7c2ebede320 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -4,6 +4,7 @@ import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import {
boardListQueryResponse,
mockLabelList,
@@ -12,6 +13,7 @@ import {
import BoardListHeader from '~/boards/components/board_list_header.vue';
import updateBoardListMutation from '~/boards/graphql/board_list_update.mutation.graphql';
import { ListType } from '~/boards/constants';
+import * as cacheUpdates from '~/boards/graphql/cache_updates';
import listQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
Vue.use(VueApollo);
@@ -25,7 +27,11 @@ describe('Board List Header Component', () => {
const updateListSpy = jest.fn();
const toggleListCollapsedSpy = jest.fn();
const mockClientToggleListCollapsedResolver = jest.fn();
- const updateListHandler = jest.fn().mockResolvedValue(updateBoardListResponse);
+ const updateListHandlerSuccess = jest.fn().mockResolvedValue(updateBoardListResponse);
+
+ beforeEach(() => {
+ cacheUpdates.setError = jest.fn();
+ });
afterEach(() => {
fakeApollo = null;
@@ -39,6 +45,7 @@ describe('Board List Header Component', () => {
withLocalStorage = true,
currentUserId = 1,
listQueryHandler = jest.fn().mockResolvedValue(boardListQueryResponse()),
+ updateListHandler = updateListHandlerSuccess,
injectedProps = {},
} = {}) => {
const boardId = 'gid://gitlab/Board/1';
@@ -271,7 +278,7 @@ describe('Board List Header Component', () => {
findCaret().vm.$emit('click');
await nextTick();
- expect(updateListHandler).not.toHaveBeenCalled();
+ expect(updateListHandlerSuccess).not.toHaveBeenCalled();
});
it('calls update list mutation when user is logged in', async () => {
@@ -280,7 +287,50 @@ describe('Board List Header Component', () => {
findCaret().vm.$emit('click');
await nextTick();
- expect(updateListHandler).toHaveBeenCalledWith({ listId: mockLabelList.id, collapsed: true });
+ expect(updateListHandlerSuccess).toHaveBeenCalledWith({
+ listId: mockLabelList.id,
+ collapsed: true,
+ });
+ });
+
+ describe('when fetch list query fails', () => {
+ const errorMessage = 'Failed to fetch list';
+ const listQueryHandlerFailure = jest.fn().mockRejectedValue(new Error(errorMessage));
+
+ beforeEach(() => {
+ createComponent({
+ listQueryHandler: listQueryHandlerFailure,
+ injectedProps: { isApolloBoard: true },
+ });
+ });
+
+ it('sets error', async () => {
+ await waitForPromises();
+
+ expect(cacheUpdates.setError).toHaveBeenCalled();
+ });
+ });
+
+ describe('when update list mutation fails', () => {
+ const errorMessage = 'Failed to update list';
+ const updateListHandlerFailure = jest.fn().mockRejectedValue(new Error(errorMessage));
+
+ beforeEach(() => {
+ createComponent({
+ currentUserId: 1,
+ updateListHandler: updateListHandlerFailure,
+ injectedProps: { isApolloBoard: true },
+ });
+ });
+
+ it('sets error', async () => {
+ await waitForPromises();
+
+ findCaret().vm.$emit('click');
+ await waitForPromises();
+
+ expect(cacheUpdates.setError).toHaveBeenCalled();
+ });
});
});
});
diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js
index affe1260c66..7314386477b 100644
--- a/spec/frontend/boards/components/board_settings_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js
@@ -6,11 +6,13 @@ import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import waitForPromises from 'helpers/wait_for_promises';
import { stubComponent } from 'helpers/stub_component';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
import { inactiveId, LIST } from '~/boards/constants';
import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql';
+import * as cacheUpdates from '~/boards/graphql/cache_updates';
import actions from '~/boards/stores/actions';
import getters from '~/boards/stores/getters';
import mutations from '~/boards/stores/mutations';
@@ -31,12 +33,17 @@ describe('BoardSettingsSidebar', () => {
const destroyBoardListMutationHandlerSuccess = jest
.fn()
.mockResolvedValue(destroyBoardListMutationResponse);
+ const errorMessage = 'Failed to delete list';
+ const destroyBoardListMutationHandlerFailure = jest
+ .fn()
+ .mockRejectedValue(new Error(errorMessage));
const createComponent = ({
canAdminList = false,
list = {},
sidebarType = LIST,
activeId = inactiveId,
+ destroyBoardListMutationHandler = destroyBoardListMutationHandlerSuccess,
isApolloBoard = false,
} = {}) => {
const boardLists = {
@@ -49,9 +56,7 @@ describe('BoardSettingsSidebar', () => {
actions,
});
- mockApollo = createMockApollo([
- [destroyBoardListMutation, destroyBoardListMutationHandlerSuccess],
- ]);
+ mockApollo = createMockApollo([[destroyBoardListMutation, destroyBoardListMutationHandler]]);
wrapper = extendedWrapper(
shallowMount(BoardSettingsSidebar, {
@@ -90,6 +95,10 @@ describe('BoardSettingsSidebar', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findRemoveButton = () => wrapper.findComponent(GlButton);
+ beforeEach(() => {
+ cacheUpdates.setError = jest.fn();
+ });
+
it('finds a MountingPortal component', () => {
createComponent();
@@ -214,5 +223,23 @@ describe('BoardSettingsSidebar', () => {
createComponent({ canAdminList: true, activeId: listId, list: mockLabelList });
expect(findModal().props('modalId')).toBe(modalID);
});
+
+ it('sets error when destroy list mutation fails', async () => {
+ createComponent({
+ canAdminList: true,
+ activeId: listId,
+ list: mockLabelList,
+ destroyBoardListMutationHandler: destroyBoardListMutationHandlerFailure,
+ isApolloBoard: true,
+ });
+
+ findRemoveButton().vm.$emit('click');
+
+ wrapper.findComponent(GlModal).vm.$emit('primary');
+
+ await waitForPromises();
+
+ expect(cacheUpdates.setError).toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/boards/components/board_top_bar_spec.js b/spec/frontend/boards/components/board_top_bar_spec.js
index afc7da97617..ae2d589a34f 100644
--- a/spec/frontend/boards/components/board_top_bar_spec.js
+++ b/spec/frontend/boards/components/board_top_bar_spec.js
@@ -3,6 +3,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import BoardTopBar from '~/boards/components/board_top_bar.vue';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
@@ -11,6 +12,7 @@ import ConfigToggle from '~/boards/components/config_toggle.vue';
import IssueBoardFilteredSearch from '~/boards/components/issue_board_filtered_search.vue';
import NewBoardButton from '~/boards/components/new_board_button.vue';
import ToggleFocus from '~/boards/components/toggle_focus.vue';
+import * as cacheUpdates from '~/boards/graphql/cache_updates';
import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import groupBoardQuery from '~/boards/graphql/group_board.query.graphql';
@@ -32,12 +34,18 @@ describe('BoardTopBar', () => {
const projectBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectBoardResponse);
const groupBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupBoardResponse);
-
- const createComponent = ({ provide = {} } = {}) => {
+ const errorMessage = 'Failed to fetch board';
+ const boardQueryHandlerFailure = jest.fn().mockRejectedValue(new Error(errorMessage));
+
+ const createComponent = ({
+ provide = {},
+ projectBoardQueryHandler = projectBoardQueryHandlerSuccess,
+ groupBoardQueryHandler = groupBoardQueryHandlerSuccess,
+ } = {}) => {
const store = createStore();
mockApollo = createMockApollo([
- [projectBoardQuery, projectBoardQueryHandlerSuccess],
- [groupBoardQuery, groupBoardQueryHandlerSuccess],
+ [projectBoardQuery, projectBoardQueryHandler],
+ [groupBoardQuery, groupBoardQueryHandler],
]);
wrapper = shallowMount(BoardTopBar, {
@@ -65,6 +73,10 @@ describe('BoardTopBar', () => {
});
};
+ beforeEach(() => {
+ cacheUpdates.setError = jest.fn();
+ });
+
afterEach(() => {
mockApollo = null;
});
@@ -134,5 +146,25 @@ describe('BoardTopBar', () => {
expect(queryHandler).toHaveBeenCalled();
expect(notCalledHandler).not.toHaveBeenCalled();
});
+
+ it.each`
+ boardType
+ ${WORKSPACE_GROUP}
+ ${WORKSPACE_PROJECT}
+ `('sets error when $boardType board query fails', async ({ boardType }) => {
+ createComponent({
+ provide: {
+ boardType,
+ isProjectBoard: boardType === WORKSPACE_PROJECT,
+ isGroupBoard: boardType === WORKSPACE_GROUP,
+ isApolloBoard: true,
+ },
+ groupBoardQueryHandler: boardQueryHandlerFailure,
+ projectBoardQueryHandler: boardQueryHandlerFailure,
+ });
+
+ await waitForPromises();
+ expect(cacheUpdates.setError).toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index 13c017706ef..74d91eeaa26 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -9,6 +9,7 @@ import groupBoardsQuery from '~/boards/graphql/group_boards.query.graphql';
import projectBoardsQuery from '~/boards/graphql/project_boards.query.graphql';
import groupRecentBoardsQuery from '~/boards/graphql/group_recent_boards.query.graphql';
import projectRecentBoardsQuery from '~/boards/graphql/project_recent_boards.query.graphql';
+import * as cacheUpdates from '~/boards/graphql/cache_updates';
import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
@@ -37,7 +38,6 @@ describe('BoardsSelector', () => {
const createStore = () => {
store = new Vuex.Store({
actions: {
- setError: jest.fn(),
setBoardConfig: jest.fn(),
},
state: {
@@ -77,16 +77,19 @@ describe('BoardsSelector', () => {
.fn()
.mockResolvedValue(mockEmptyProjectRecentBoardsResponse);
+ const boardsHandlerFailure = jest.fn().mockRejectedValue(new Error('error'));
+
const createComponent = ({
projectBoardsQueryHandler = projectBoardsQueryHandlerSuccess,
projectRecentBoardsQueryHandler = projectRecentBoardsQueryHandlerSuccess,
+ groupBoardsQueryHandler = groupBoardsQueryHandlerSuccess,
isGroupBoard = false,
isProjectBoard = false,
provide = {},
} = {}) => {
fakeApollo = createMockApollo([
[projectBoardsQuery, projectBoardsQueryHandler],
- [groupBoardsQuery, groupBoardsQueryHandlerSuccess],
+ [groupBoardsQuery, groupBoardsQueryHandler],
[projectRecentBoardsQuery, projectRecentBoardsQueryHandler],
[groupRecentBoardsQuery, groupRecentBoardsQueryHandlerSuccess],
]);
@@ -115,6 +118,10 @@ describe('BoardsSelector', () => {
});
};
+ beforeEach(() => {
+ cacheUpdates.setError = jest.fn();
+ });
+
afterEach(() => {
fakeApollo = null;
});
@@ -246,6 +253,29 @@ describe('BoardsSelector', () => {
expect(queryHandler).toHaveBeenCalled();
expect(notCalledHandler).not.toHaveBeenCalled();
});
+
+ it.each`
+ boardType
+ ${WORKSPACE_GROUP}
+ ${WORKSPACE_PROJECT}
+ `('sets error when fetching $boardType boards fails', async ({ boardType }) => {
+ createStore();
+ createComponent({
+ isGroupBoard: boardType === WORKSPACE_GROUP,
+ isProjectBoard: boardType === WORKSPACE_PROJECT,
+ projectBoardsQueryHandler: boardsHandlerFailure,
+ groupBoardsQueryHandler: boardsHandlerFailure,
+ });
+
+ await nextTick();
+
+ // Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
+ findDropdown().vm.$emit('show');
+
+ await waitForPromises();
+
+ expect(cacheUpdates.setError).toHaveBeenCalled();
+ });
});
describe('dropdown visibility', () => {
diff --git a/spec/frontend/jobs/components/log/line_header_spec.js b/spec/frontend/jobs/components/log/line_header_spec.js
index 16fe753e08a..c02d8c22655 100644
--- a/spec/frontend/jobs/components/log/line_header_spec.js
+++ b/spec/frontend/jobs/components/log/line_header_spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import setWindowLocation from 'helpers/set_window_location_helper';
import DurationBadge from '~/jobs/components/log/duration_badge.vue';
import LineHeader from '~/jobs/components/log/line_header.vue';
import LineNumber from '~/jobs/components/log/line_number.vue';
@@ -15,7 +16,7 @@ describe('Job Log Header Line', () => {
style: 'term-fg-l-green',
},
],
- lineNumber: 0,
+ lineNumber: 76,
},
isClosed: true,
path: '/jashkenas/underscore/-/jobs/335',
@@ -89,4 +90,30 @@ describe('Job Log Header Line', () => {
expect(wrapper.findComponent(DurationBadge).exists()).toBe(true);
});
});
+
+ describe('line highlighting', () => {
+ describe('with hash', () => {
+ beforeEach(() => {
+ setWindowLocation(`http://foo.com/root/ci-project/-/jobs/6353#L77`);
+
+ createComponent(data);
+ });
+
+ it('highlights line', () => {
+ expect(wrapper.classes()).toContain('gl-bg-gray-700');
+ });
+ });
+
+ describe('without hash', () => {
+ beforeEach(() => {
+ setWindowLocation(`http://foo.com/root/ci-project/-/jobs/6353`);
+
+ createComponent(data);
+ });
+
+ it('does not highlight line', () => {
+ expect(wrapper.classes()).not.toContain('gl-bg-gray-700');
+ });
+ });
+ });
});
diff --git a/spec/frontend/jobs/components/log/line_spec.js b/spec/frontend/jobs/components/log/line_spec.js
index 50ebd1610d2..fad7a03beef 100644
--- a/spec/frontend/jobs/components/log/line_spec.js
+++ b/spec/frontend/jobs/components/log/line_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import Line from '~/jobs/components/log/line.vue';
import LineNumber from '~/jobs/components/log/line_number.vue';
+import setWindowLocation from 'helpers/set_window_location_helper';
const httpUrl = 'http://example.com';
const httpsUrl = 'https://example.com';
@@ -203,7 +204,7 @@ describe('Job Log Line', () => {
searchResults: mockSearchResults,
});
- expect(wrapper.classes()).toContain('gl-bg-gray-500');
+ expect(wrapper.classes()).toContain('gl-bg-gray-700');
});
it('does not apply highlight class to search result elements', () => {
@@ -218,7 +219,49 @@ describe('Job Log Line', () => {
searchResults: mockSearchResults,
});
- expect(wrapper.classes()).not.toContain('gl-bg-gray-500');
+ expect(wrapper.classes()).not.toContain('gl-bg-gray-700');
+ });
+ });
+
+ describe('job log hash highlighting', () => {
+ describe('with hash', () => {
+ beforeEach(() => {
+ setWindowLocation(`http://foo.com/root/ci-project/-/jobs/6353#L77`);
+ });
+
+ it('applies highlight class to job log line', () => {
+ createComponent({
+ line: {
+ offset: 24526,
+ content: [{ text: 'job log content' }],
+ section: 'custom-section',
+ lineNumber: 76,
+ },
+ path: '/root/ci-project/-/jobs/6353',
+ });
+
+ expect(wrapper.classes()).toContain('gl-bg-gray-700');
+ });
+ });
+
+ describe('without hash', () => {
+ beforeEach(() => {
+ setWindowLocation(`http://foo.com/root/ci-project/-/jobs/6353`);
+ });
+
+ it('does not apply highlight class to job log line', () => {
+ createComponent({
+ line: {
+ offset: 24500,
+ content: [{ text: 'line' }],
+ section: 'custom-section',
+ lineNumber: 10,
+ },
+ path: '/root/ci-project/-/jobs/6353',
+ });
+
+ expect(wrapper.classes()).not.toContain('gl-bg-gray-700');
+ });
});
});
});
diff --git a/spec/frontend/repository/components/blob_button_group_spec.js b/spec/frontend/repository/components/blob_button_group_spec.js
index 2c63deb99c9..1a077028704 100644
--- a/spec/frontend/repository/components/blob_button_group_spec.js
+++ b/spec/frontend/repository/components/blob_button_group_spec.js
@@ -17,6 +17,7 @@ const DEFAULT_PROPS = {
isLocked: false,
canLock: true,
showForkSuggestion: false,
+ isUsingLfs: true,
};
const DEFAULT_INJECT = {
@@ -146,7 +147,7 @@ describe('BlobButtonGroup component', () => {
createComponent();
const { targetBranch, originalBranch } = DEFAULT_INJECT;
- const { name, canPushCode, deletePath, emptyRepo } = DEFAULT_PROPS;
+ const { name, canPushCode, deletePath, emptyRepo, isUsingLfs } = DEFAULT_PROPS;
const title = `Delete ${name}`;
expect(findDeleteBlobModal().props()).toMatchObject({
@@ -157,6 +158,7 @@ describe('BlobButtonGroup component', () => {
canPushCode,
deletePath,
emptyRepo,
+ isUsingLfs,
});
});
});
diff --git a/spec/frontend/repository/components/blob_viewers/index_spec.js b/spec/frontend/repository/components/blob_viewers/index_spec.js
new file mode 100644
index 00000000000..d3ea46262e1
--- /dev/null
+++ b/spec/frontend/repository/components/blob_viewers/index_spec.js
@@ -0,0 +1,11 @@
+import { loadViewer, viewers } from '~/repository/components/blob_viewers';
+import { OPENAPI_FILE_TYPE, JSON_LANGUAGE } from '~/repository/constants';
+
+describe('Blob Viewers index', () => {
+ describe('loadViewer', () => {
+ it('loads the openapi viewer', () => {
+ const result = loadViewer(OPENAPI_FILE_TYPE, false, true, JSON_LANGUAGE);
+ expect(result).toBe(viewers[OPENAPI_FILE_TYPE]);
+ });
+ });
+});
diff --git a/spec/frontend/repository/components/delete_blob_modal_spec.js b/spec/frontend/repository/components/delete_blob_modal_spec.js
index 90f2150222c..e1723a091c4 100644
--- a/spec/frontend/repository/components/delete_blob_modal_spec.js
+++ b/spec/frontend/repository/components/delete_blob_modal_spec.js
@@ -1,7 +1,10 @@
-import { GlFormTextarea, GlModal, GlFormInput, GlToggle, GlForm } from '@gitlab/ui';
-import { shallowMount, mount } from '@vue/test-utils';
+import { GlFormTextarea, GlModal, GlFormInput, GlToggle, GlForm, GlSprintf } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { RENDER_ALL_SLOTS_TEMPLATE, stubComponent } from 'helpers/stub_component';
import DeleteBlobModal from '~/repository/components/delete_blob_modal.vue';
+import { sprintf } from '~/locale';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
@@ -17,6 +20,8 @@ const initialProps = {
emptyRepo: false,
};
+const { i18n } = DeleteBlobModal;
+
describe('DeleteBlobModal', () => {
let wrapper;
@@ -30,10 +35,14 @@ describe('DeleteBlobModal', () => {
static: true,
visible: true,
},
+ stubs: {
+ GlSprintf,
+ GlModal: stubComponent(GlModal, { template: RENDER_ALL_SLOTS_TEMPLATE }),
+ },
});
};
- const createComponent = createComponentFactory(shallowMount);
+ const createComponent = createComponentFactory(shallowMountExtended);
const createFullComponent = createComponentFactory(mount);
const findModal = () => wrapper.findComponent(GlModal);
@@ -49,6 +58,35 @@ describe('DeleteBlobModal', () => {
await findCommitTextarea().vm.$emit('input', commitText);
};
+ describe('LFS files', () => {
+ const lfsTitleText = i18n.LFS_WARNING_TITLE;
+ const primaryLfsText = sprintf(i18n.LFS_WARNING_PRIMARY_CONTENT, {
+ branch: initialProps.targetBranch,
+ });
+
+ const secondaryLfsText = sprintf(i18n.LFS_WARNING_SECONDARY_CONTENT, {
+ linkStart: '',
+ linkEnd: '',
+ });
+
+ beforeEach(() => createComponent({ isUsingLfs: true }));
+
+ it('renders a modal containing LFS text', () => {
+ expect(findModal().props('title')).toBe(lfsTitleText);
+ expect(findModal().text()).toContain(primaryLfsText);
+ expect(findModal().text()).toContain(secondaryLfsText);
+ });
+
+ it('hides the LFS content if the continue button is clicked', async () => {
+ findModal().vm.$emit('primary', { preventDefault: jest.fn() });
+ await nextTick();
+
+ expect(findModal().props('title')).not.toBe(lfsTitleText);
+ expect(findModal().text()).not.toContain(primaryLfsText);
+ expect(findModal().text()).not.toContain(secondaryLfsText);
+ });
+ });
+
it('renders Modal component', () => {
createComponent();
diff --git a/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js b/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js
index 688dccbda79..55d5b34ae70 100644
--- a/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js
@@ -1,11 +1,17 @@
-import { GlDropdownSectionHeader } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlDisclosureDropdownGroup } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import OkrActionsSplitButton from '~/work_items/components/work_item_links/okr_actions_split_button.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
const createComponent = () => {
- return extendedWrapper(shallowMount(OkrActionsSplitButton));
+ return extendedWrapper(
+ shallowMount(OkrActionsSplitButton, {
+ stubs: {
+ GlDisclosureDropdown,
+ },
+ }),
+ );
};
describe('RelatedItemsTree', () => {
@@ -18,11 +24,11 @@ describe('RelatedItemsTree', () => {
describe('OkrActionsSplitButton', () => {
describe('template', () => {
it('renders objective and key results sections', () => {
- expect(wrapper.findAllComponents(GlDropdownSectionHeader).at(0).text()).toContain(
+ expect(wrapper.findAllComponents(GlDisclosureDropdownGroup).at(0).props('group').name).toBe(
'Objective',
);
- expect(wrapper.findAllComponents(GlDropdownSectionHeader).at(1).text()).toContain(
+ expect(wrapper.findAllComponents(GlDisclosureDropdownGroup).at(1).props('group').name).toBe(
'Key result',
);
});
diff --git a/spec/lib/gitlab/database/click_house_client_spec.rb b/spec/lib/gitlab/database/click_house_client_spec.rb
index 502d879bf6a..8e1d7f3c434 100644
--- a/spec/lib/gitlab/database/click_house_client_spec.rb
+++ b/spec/lib/gitlab/database/click_house_client_spec.rb
@@ -24,13 +24,6 @@ RSpec.describe 'ClickHouse::Client', feature_category: :database do
expect(databases).not_to be_empty
end
- it 'returns data from the DB via `select` method' do
- result = ClickHouse::Client.select("SELECT 1 AS value", :main)
-
- # returns JSON if successful. Otherwise error
- expect(result).to eq([{ 'value' => 1 }])
- end
-
it 'does not return data via `execute` method' do
result = ClickHouse::Client.execute("SELECT 1 AS value", :main)
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/batched_background_migration_failed_jobs_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/batched_background_migration_failed_jobs_metric_spec.rb
new file mode 100644
index 00000000000..e66dd04b69b
--- /dev/null
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/batched_background_migration_failed_jobs_metric_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Usage::Metrics::Instrumentations::BatchedBackgroundMigrationFailedJobsMetric, feature_category: :database do
+ let(:expected_value) do
+ [
+ {
+ job_class_name: 'job',
+ number_of_failed_jobs: 1,
+ table_name: 'jobs'
+ },
+ {
+ job_class_name: 'test',
+ number_of_failed_jobs: 2,
+ table_name: 'users'
+ }
+ ]
+ end
+
+ let_it_be(:active_migration) do
+ create(:batched_background_migration, :active, table_name: 'users', job_class_name: 'test', created_at: 5.days.ago)
+ end
+
+ let_it_be(:failed_migration) do
+ create(:batched_background_migration, :failed, table_name: 'jobs', job_class_name: 'job', created_at: 4.days.ago)
+ end
+
+ let_it_be(:batched_job) { create(:batched_background_migration_job, :failed, batched_migration: active_migration) }
+
+ let_it_be(:batched_job_2) { create(:batched_background_migration_job, :failed, batched_migration: active_migration) }
+
+ let_it_be(:batched_job_3) { create(:batched_background_migration_job, :failed, batched_migration: failed_migration) }
+
+ let_it_be(:old_migration) { create(:batched_background_migration, :failed, created_at: 99.days.ago) }
+
+ let_it_be(:old_batched_job) { create(:batched_background_migration_job, :failed, batched_migration: old_migration) }
+
+ it_behaves_like 'a correct instrumented metric value', { time_frame: '7d' }
+end
diff --git a/spec/lib/peek/views/click_house_spec.rb b/spec/lib/peek/views/click_house_spec.rb
new file mode 100644
index 00000000000..9d7d06204fc
--- /dev/null
+++ b/spec/lib/peek/views/click_house_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Peek::Views::ClickHouse, :click_house, :request_store, feature_category: :database do
+ before do
+ allow(::Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true)
+ end
+
+ describe '#results' do
+ let(:results) { described_class.new.results }
+
+ it 'includes performance details' do
+ ::Gitlab::SafeRequestStore.clear!
+
+ data = ClickHouse::Client.select('SELECT 1 AS value', :main)
+ ClickHouse::Client.execute('INSERT INTO events (id) VALUES (1)', :main)
+
+ expect(data).to eq([{ 'value' => 1 }])
+
+ expect(results[:calls]).to eq(2)
+ expect(results[:duration]).to be_kind_of(String)
+
+ expect(results[:details]).to match_array([
+ a_hash_including({
+ sql: 'SELECT 1 AS value',
+ database: 'database: main'
+ }),
+ a_hash_including({
+ sql: 'INSERT INTO events (id) VALUES (1)',
+ database: 'database: main',
+ statistics: include('written_rows=>"1"')
+ })
+ ])
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index d71ae75aefb..e138d7a4c1b 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -7300,6 +7300,32 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
end
end
+ describe '#pages_variables' do
+ let(:group) { build(:group, path: 'group') }
+ let(:project) { build(:project, path: 'project', namespace: group) }
+
+ it 'returns the pages variables' do
+ expect(project.pages_variables.to_hash).to eq({
+ 'CI_PAGES_DOMAIN' => 'example.com',
+ 'CI_PAGES_URL' => 'http://group.example.com/project'
+ })
+ end
+
+ it 'returns the pages variables' do
+ build(
+ :project_setting,
+ project: project,
+ pages_unique_domain_enabled: true,
+ pages_unique_domain: 'unique-domain'
+ )
+
+ expect(project.pages_variables.to_hash).to eq({
+ 'CI_PAGES_DOMAIN' => 'example.com',
+ 'CI_PAGES_URL' => 'http://unique-domain.example.com'
+ })
+ end
+ end
+
describe '#closest_setting' do
shared_examples_for 'fetching closest setting' do
let!(:namespace) { create(:namespace) }
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index fb7d487b29b..259f5156d42 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -295,6 +295,20 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
expect(WebMock).not_to have_requested(:post, stubbed_hostname(project_hook.url))
end
+ context 'when silent mode is enabled' do
+ before do
+ stub_application_setting(silent_mode_enabled: true)
+ end
+
+ it 'blocks and logs an error' do
+ stub_full_request(project_hook.url, method: :post)
+
+ expect(Gitlab::AuthLogger).to receive(:error).with(include(message: 'GitLab is in silent mode'))
+ expect(service_instance.execute).to be_error
+ expect(WebMock).not_to have_requested(:post, stubbed_hostname(project_hook.url))
+ end
+ end
+
it 'handles exceptions' do
exceptions = Gitlab::HTTP::HTTP_ERRORS + [
Gitlab::Json::LimitedEncoder::LimitExceeded, URI::InvalidURIError
@@ -733,6 +747,19 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state,
end
end
+ context 'when silent mode is enabled' do
+ before do
+ stub_application_setting(silent_mode_enabled: true)
+ end
+
+ it 'does not queue a worker and logs an error' do
+ expect(WebHookWorker).not_to receive(:perform_async)
+ expect(Gitlab::AuthLogger).to receive(:error).with(include(message: 'GitLab is in silent mode'))
+
+ service_instance.async_execute
+ end
+ end
+
context 'when hook has custom context attributes' do
it 'includes the attributes in the worker context' do
expect(WebHookWorker).to receive(:perform_async) do
diff --git a/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb
index a196b63585c..a33a846417b 100644
--- a/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb
@@ -83,6 +83,20 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
expect(find_hooks.disabled).to be_empty
end
end
+
+ context 'when silent mode is enabled' do
+ before do
+ stub_application_setting(silent_mode_enabled: true)
+ end
+
+ it 'causes no hooks to be considered executable' do
+ expect(find_hooks.executable).to be_empty
+ end
+
+ it 'causes all hooks to be considered disabled' do
+ expect(find_hooks.disabled.count).to eq(16)
+ end
+ end
end
describe '#executable?', :freeze_time do
diff --git a/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb
index f98528ffedc..8cadad0959b 100644
--- a/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb
@@ -2,7 +2,7 @@
RSpec.shared_examples 'a hook that does not get automatically disabled on failure' do
describe '.executable/.disabled', :freeze_time do
- let!(:executables) do
+ let!(:webhooks) do
[
[0, Time.current],
[0, 1.minute.from_now],
@@ -29,9 +29,23 @@ RSpec.shared_examples 'a hook that does not get automatically disabled on failur
it 'finds the correct set of project hooks' do
expect(find_hooks).to all(be_executable)
- expect(find_hooks.executable).to match_array executables
+ expect(find_hooks.executable).to match_array(webhooks)
expect(find_hooks.disabled).to be_empty
end
+
+ context 'when silent mode is enabled' do
+ before do
+ stub_application_setting(silent_mode_enabled: true)
+ end
+
+ it 'causes no hooks to be considered executable' do
+ expect(find_hooks.executable).to be_empty
+ end
+
+ it 'causes all hooks to be considered disabled' do
+ expect(find_hooks.disabled).to match_array(webhooks)
+ end
+ end
end
describe '#executable?', :freeze_time do
diff --git a/spec/workers/users/deactivate_dormant_users_worker_spec.rb b/spec/workers/users/deactivate_dormant_users_worker_spec.rb
index 39d282a6e18..c28be165fd7 100644
--- a/spec/workers/users/deactivate_dormant_users_worker_spec.rb
+++ b/spec/workers/users/deactivate_dormant_users_worker_spec.rb
@@ -10,6 +10,13 @@ RSpec.describe Users::DeactivateDormantUsersWorker, feature_category: :seat_cost
let_it_be(:inactive) { create(:user, last_activity_on: nil, created_at: User::MINIMUM_DAYS_CREATED.days.ago.to_date) }
let_it_be(:inactive_recently_created) { create(:user, last_activity_on: nil, created_at: (User::MINIMUM_DAYS_CREATED - 1).days.ago.to_date) }
+ let(:admin_bot) { create(:user, :admin_bot) }
+ let(:deactivation_service) { instance_spy(Users::DeactivateService) }
+
+ before do
+ allow(Users::DeactivateService).to receive(:new).and_return(deactivation_service)
+ end
+
subject(:worker) { described_class.new }
it 'does not run for SaaS', :saas do
@@ -17,8 +24,7 @@ RSpec.describe Users::DeactivateDormantUsersWorker, feature_category: :seat_cost
worker.perform
- expect(User.dormant.count).to eq(1)
- expect(User.with_no_activity.count).to eq(1)
+ expect(deactivation_service).not_to have_received(:execute)
end
context 'when automatic deactivation of dormant users is enabled' do
@@ -29,29 +35,33 @@ RSpec.describe Users::DeactivateDormantUsersWorker, feature_category: :seat_cost
it 'deactivates dormant users' do
worker.perform
- expect(User.dormant.count).to eq(0)
- expect(User.with_no_activity.count).to eq(0)
+ expect(deactivation_service).to have_received(:execute).twice
end
where(:user_type, :expected_state) do
- :human | 'deactivated'
- :support_bot | 'active'
- :alert_bot | 'active'
+ :human | 'deactivated'
+ :support_bot | 'active'
+ :alert_bot | 'active'
:visual_review_bot | 'active'
- :service_user | 'deactivated'
- :ghost | 'active'
- :project_bot | 'active'
- :migration_bot | 'active'
- :security_bot | 'active'
- :automation_bot | 'active'
+ :service_user | 'deactivated'
+ :ghost | 'active'
+ :project_bot | 'active'
+ :migration_bot | 'active'
+ :security_bot | 'active'
+ :automation_bot | 'active'
end
+
with_them do
it 'deactivates certain user types' do
user = create(:user, user_type: user_type, state: :active, last_activity_on: Gitlab::CurrentSettings.deactivate_dormant_users_period.days.ago.to_date)
worker.perform
- expect(user.reload.state).to eq(expected_state)
+ if expected_state == 'deactivated'
+ expect(deactivation_service).to have_received(:execute).with(user)
+ else
+ expect(deactivation_service).not_to have_received(:execute).with(user)
+ end
end
end
@@ -61,22 +71,14 @@ RSpec.describe Users::DeactivateDormantUsersWorker, feature_category: :seat_cost
worker.perform
- expect(human_user.reload.state).to eq('blocked')
- expect(service_user.reload.state).to eq('blocked')
+ expect(deactivation_service).not_to have_received(:execute).with(human_user)
+ expect(deactivation_service).not_to have_received(:execute).with(service_user)
end
it 'does not deactivate recently created users' do
worker.perform
- expect(inactive_recently_created.reload.state).to eq('active')
- end
-
- it 'triggers update of highest user role for deactivated users', :clean_gitlab_redis_shared_state do
- [dormant, inactive].each do |user|
- expect(UpdateHighestRoleWorker).to receive(:perform_in).with(anything, user.id)
- end
-
- worker.perform
+ expect(deactivation_service).not_to have_received(:execute).with(inactive_recently_created)
end
end
@@ -88,8 +90,7 @@ RSpec.describe Users::DeactivateDormantUsersWorker, feature_category: :seat_cost
it 'does nothing' do
worker.perform
- expect(User.dormant.count).to eq(1)
- expect(User.with_no_activity.count).to eq(1)
+ expect(deactivation_service).not_to have_received(:execute)
end
end
end