diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-03 15:09:25 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-03 15:09:25 +0300 |
commit | aeee636c18f82107ec7a489f33c944c65ad5f34e (patch) | |
tree | 2c30286279e096c9114e9a41a3ed07a83293c059 /spec | |
parent | 3d8459c18b7a20d9142359bb9334b467e774eb36 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
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 |