diff options
Diffstat (limited to 'spec')
-rw-r--r-- | spec/controllers/projects/feature_flags_controller_spec.rb | 52 | ||||
-rw-r--r-- | spec/factories/ci/builds.rb | 10 | ||||
-rw-r--r-- | spec/features/boards/sidebar_milestones_spec.rb | 4 | ||||
-rw-r--r-- | spec/frontend/boards/components/board_content_sidebar_spec.js | 28 | ||||
-rw-r--r-- | spec/frontend/notes/components/noteable_discussion_spec.js | 12 | ||||
-rw-r--r-- | spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js | 503 | ||||
-rw-r--r-- | spec/frontend/sidebar/mock_data.js | 79 | ||||
-rw-r--r-- | spec/migrations/clean_up_pending_builds_table_spec.rb | 47 | ||||
-rw-r--r-- | spec/models/ci/build_spec.rb | 2 | ||||
-rw-r--r-- | spec/requests/api/ci/runner/jobs_request_post_spec.rb | 40 | ||||
-rw-r--r-- | spec/requests/api/feature_flags_spec.rb | 62 | ||||
-rw-r--r-- | spec/serializers/merge_request_diff_entity_spec.rb | 48 | ||||
-rw-r--r-- | spec/services/ci/register_job_service_spec.rb | 86 | ||||
-rw-r--r-- | spec/services/ci/retry_build_service_spec.rb | 2 | ||||
-rw-r--r-- | spec/services/deployments/update_environment_service_spec.rb | 36 |
15 files changed, 940 insertions, 71 deletions
diff --git a/spec/controllers/projects/feature_flags_controller_spec.rb b/spec/controllers/projects/feature_flags_controller_spec.rb index cd7d1ea0e8a..752e8b652e0 100644 --- a/spec/controllers/projects/feature_flags_controller_spec.rb +++ b/spec/controllers/projects/feature_flags_controller_spec.rb @@ -371,6 +371,58 @@ RSpec.describe Projects::FeatureFlagsController do end end + describe 'GET edit' do + subject { get(:edit, params: params) } + + context 'with legacy flags' do + let!(:feature_flag) { create(:operations_feature_flag, project: project) } + + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + iid: feature_flag.iid + } + end + + context 'removed' do + before do + stub_feature_flags(remove_legacy_flags: true, remove_legacy_flags_override: false) + end + + it 'returns not found' do + is_expected.to have_gitlab_http_status(:not_found) + end + end + + context 'removed' do + before do + stub_feature_flags(remove_legacy_flags: false) + end + + it 'returns ok' do + is_expected.to have_gitlab_http_status(:ok) + end + end + end + + context 'with new version flags' do + let!(:feature_flag) { create(:operations_feature_flag, :new_version_flag, project: project) } + + let(:params) do + { + namespace_id: project.namespace, + project_id: project, + iid: feature_flag.iid + } + end + + it 'returns successfully' do + is_expected.to have_gitlab_http_status(:ok) + end + end + end + describe 'POST create.json' do subject { post(:create, params: params, format: :json) } diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index f99021ad223..55be7fd72b0 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -79,6 +79,7 @@ FactoryBot.define do trait :pending do queued_at { 'Di 29. Okt 09:50:59 CET 2013' } + status { 'pending' } end @@ -286,6 +287,15 @@ FactoryBot.define do trait :queued do queued_at { Time.now } + + after(:create) do |build| + build.create_queuing_entry! + end + end + + trait :picked do + running + runner factory: :ci_runner end diff --git a/spec/features/boards/sidebar_milestones_spec.rb b/spec/features/boards/sidebar_milestones_spec.rb index 54182781a30..be7435263b1 100644 --- a/spec/features/boards/sidebar_milestones_spec.rb +++ b/spec/features/boards/sidebar_milestones_spec.rb @@ -38,7 +38,7 @@ RSpec.describe 'Project issue boards sidebar milestones', :js do wait_for_requests - page.within('.value') do + page.within('[data-testid="select-milestone"]') do expect(page).to have_content(milestone.title) end end @@ -56,7 +56,7 @@ RSpec.describe 'Project issue boards sidebar milestones', :js do wait_for_requests - page.within('.value') do + page.within('[data-testid="select-milestone"]') do expect(page).not_to have_content(milestone.title) end end diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js index 01c99a02db2..e97bdba5fea 100644 --- a/spec/frontend/boards/components/board_content_sidebar_spec.js +++ b/spec/frontend/boards/components/board_content_sidebar_spec.js @@ -1,11 +1,11 @@ import { GlDrawer } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vuex from 'vuex'; +import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue'; import { stubComponent } from 'helpers/stub_component'; import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue'; import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue'; import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue'; -import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue'; import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue'; import { ISSUABLE } from '~/boards/constants'; import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue'; @@ -68,6 +68,9 @@ describe('BoardContentSidebar', () => { iterations: { loading: false, }, + attributesList: { + loading: false, + }, }, }, }, @@ -84,38 +87,41 @@ describe('BoardContentSidebar', () => { }); it('confirms we render GlDrawer', () => { - expect(wrapper.find(GlDrawer).exists()).toBe(true); + expect(wrapper.findComponent(GlDrawer).exists()).toBe(true); }); it('does not render GlDrawer when isSidebarOpen is false', () => { createStore({ mockGetters: { isSidebarOpen: () => false } }); createComponent(); - expect(wrapper.find(GlDrawer).exists()).toBe(false); + expect(wrapper.findComponent(GlDrawer).exists()).toBe(false); }); it('applies an open attribute', () => { - expect(wrapper.find(GlDrawer).props('open')).toBe(true); + expect(wrapper.findComponent(GlDrawer).props('open')).toBe(true); }); it('renders BoardSidebarLabelsSelect', () => { - expect(wrapper.find(BoardSidebarLabelsSelect).exists()).toBe(true); + expect(wrapper.findComponent(BoardSidebarLabelsSelect).exists()).toBe(true); }); it('renders BoardSidebarTitle', () => { - expect(wrapper.find(BoardSidebarTitle).exists()).toBe(true); + expect(wrapper.findComponent(BoardSidebarTitle).exists()).toBe(true); }); it('renders BoardSidebarDueDate', () => { - expect(wrapper.find(BoardSidebarDueDate).exists()).toBe(true); + expect(wrapper.findComponent(BoardSidebarDueDate).exists()).toBe(true); }); it('renders BoardSidebarSubscription', () => { - expect(wrapper.find(SidebarSubscriptionsWidget).exists()).toBe(true); + expect(wrapper.findComponent(SidebarSubscriptionsWidget).exists()).toBe(true); }); - it('renders BoardSidebarMilestoneSelect', () => { - expect(wrapper.find(BoardSidebarMilestoneSelect).exists()).toBe(true); + it('renders SidebarDropdownWidget for milestones', () => { + expect(wrapper.findComponent(SidebarDropdownWidget).exists()).toBe(true); + expect(wrapper.findComponent(SidebarDropdownWidget).props('issuableAttribute')).toEqual( + 'milestone', + ); }); describe('when we emit close', () => { @@ -128,7 +134,7 @@ describe('BoardContentSidebar', () => { }); it('calls toggleBoardItem with correct parameters', async () => { - wrapper.find(GlDrawer).vm.$emit('close'); + wrapper.findComponent(GlDrawer).vm.$emit('close'); expect(toggleBoardItem).toHaveBeenCalledTimes(1); expect(toggleBoardItem).toHaveBeenCalledWith(expect.any(Object), { diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js index 735bc2b70dd..a364a524e7b 100644 --- a/spec/frontend/notes/components/noteable_discussion_spec.js +++ b/spec/frontend/notes/components/noteable_discussion_spec.js @@ -56,6 +56,18 @@ describe('noteable_discussion component', () => { expect(wrapper.find('.discussion-header').exists()).toBe(true); }); + it('should hide actions when diff refs do not exists', async () => { + const discussion = { ...discussionMock }; + discussion.diff_file = { ...mockDiffFile, diff_refs: null }; + discussion.diff_discussion = true; + discussion.expanded = false; + + wrapper.setProps({ discussion }); + await nextTick(); + + expect(wrapper.vm.canShowReplyActions).toBe(false); + }); + describe('actions', () => { it('should toggle reply form', async () => { await nextTick(); diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js new file mode 100644 index 00000000000..8d58854b013 --- /dev/null +++ b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js @@ -0,0 +1,503 @@ +import { + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlLink, + GlSearchBoxByType, + GlFormInput, + GlLoadingIcon, +} from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; +import { createLocalVue, shallowMount, mount } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; + +import createMockApollo from 'helpers/mock_apollo_helper'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import createFlash from '~/flash'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { IssuableType } from '~/issue_show/constants'; +import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.vue'; +import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; +import { IssuableAttributeType } from '~/sidebar/constants'; +import projectIssueMilestoneMutation from '~/sidebar/queries/project_issue_milestone.mutation.graphql'; +import projectIssueMilestoneQuery from '~/sidebar/queries/project_issue_milestone.query.graphql'; +import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql'; + +import { + mockIssue, + mockProjectMilestonesResponse, + noCurrentMilestoneResponse, + mockMilestoneMutationResponse, + mockMilestone2, + emptyProjectMilestonesResponse, +} from '../mock_data'; + +jest.mock('~/flash'); + +const localVue = createLocalVue(); + +describe('SidebarDropdownWidget', () => { + let wrapper; + let mockApollo; + + const promiseData = { issuableSetAttribute: { issue: { attribute: { id: '123' } } } }; + const firstErrorMsg = 'first error'; + const promiseWithErrors = { + ...promiseData, + issuableSetAttribute: { ...promiseData.issuableSetAttribute, errors: [firstErrorMsg] }, + }; + + const mutationSuccess = () => jest.fn().mockResolvedValue({ data: promiseData }); + const mutationError = () => + jest.fn().mockRejectedValue('Failed to set milestone on this issue. Please try again.'); + const mutationSuccessWithErrors = () => jest.fn().mockResolvedValue({ data: promiseWithErrors }); + + const findGlLink = () => wrapper.findComponent(GlLink); + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdownText = () => wrapper.findComponent(GlDropdownText); + const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); + const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findDropdownItemWithText = (text) => + findAllDropdownItems().wrappers.find((x) => x.text() === text); + + const findSidebarEditableItem = () => wrapper.findComponent(SidebarEditableItem); + const findEditButton = () => findSidebarEditableItem().find('[data-testid="edit-button"]'); + const findEditableLoadingIcon = () => findSidebarEditableItem().findComponent(GlLoadingIcon); + const findAttributeItems = () => wrapper.findByTestId('milestone-items'); + const findSelectedAttribute = () => wrapper.findByTestId('select-milestone'); + const findNoAttributeItem = () => wrapper.findByTestId('no-milestone-item'); + const findLoadingIconDropdown = () => wrapper.findByTestId('loading-icon-dropdown'); + + const waitForDropdown = async () => { + // BDropdown first changes its `visible` property + // in a requestAnimationFrame callback. + // It then emits `shown` event in a watcher for `visible` + // Hence we need both of these: + await waitForPromises(); + await wrapper.vm.$nextTick(); + }; + + const waitForApollo = async () => { + jest.runOnlyPendingTimers(); + await waitForPromises(); + }; + + // Used with createComponentWithApollo which uses 'mount' + const clickEdit = async () => { + await findEditButton().trigger('click'); + + await waitForDropdown(); + + // We should wait for attributes list to be fetched. + await waitForApollo(); + }; + + // Used with createComponent which shallow mounts components + const toggleDropdown = async () => { + wrapper.vm.$refs.editable.expand(); + + await waitForDropdown(); + }; + + const createComponentWithApollo = async ({ + requestHandlers = [], + projectMilestonesSpy = jest.fn().mockResolvedValue(mockProjectMilestonesResponse), + currentMilestoneSpy = jest.fn().mockResolvedValue(noCurrentMilestoneResponse), + } = {}) => { + localVue.use(VueApollo); + mockApollo = createMockApollo([ + [projectMilestonesQuery, projectMilestonesSpy], + [projectIssueMilestoneQuery, currentMilestoneSpy], + ...requestHandlers, + ]); + + wrapper = extendedWrapper( + mount(SidebarDropdownWidget, { + localVue, + provide: { canUpdate: true }, + apolloProvider: mockApollo, + propsData: { + workspacePath: mockIssue.projectPath, + attrWorkspacePath: mockIssue.projectPath, + iid: mockIssue.iid, + issuableType: IssuableType.Issue, + issuableAttribute: IssuableAttributeType.Milestone, + }, + attachTo: document.body, + }), + ); + + await waitForApollo(); + }; + + const createComponent = ({ data = {}, mutationPromise = mutationSuccess, queries = {} } = {}) => { + wrapper = extendedWrapper( + shallowMount(SidebarDropdownWidget, { + provide: { canUpdate: true }, + data() { + return data; + }, + propsData: { + workspacePath: '', + attrWorkspacePath: '', + iid: '', + issuableType: IssuableType.Issue, + issuableAttribute: IssuableAttributeType.Milestone, + }, + mocks: { + $apollo: { + mutate: mutationPromise(), + queries: { + currentAttribute: { loading: false }, + attributesList: { loading: false }, + ...queries, + }, + }, + }, + stubs: { + SidebarEditableItem, + GlSearchBoxByType, + GlDropdown, + }, + }), + ); + + // We need to mock out `showDropdown` which + // invokes `show` method of BDropdown used inside GlDropdown. + jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation(); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('when not editing', () => { + beforeEach(() => { + createComponent({ + data: { + currentAttribute: { id: 'id', title: 'title', webUrl: 'webUrl' }, + }, + stubs: { + GlDropdown, + SidebarEditableItem, + }, + }); + }); + + it('shows the current attribute', () => { + expect(findSelectedAttribute().text()).toBe('title'); + }); + + it('links to the current attribute', () => { + expect(findGlLink().attributes().href).toBe('webUrl'); + }); + + it('does not show a loading spinner next to the heading', () => { + expect(findEditableLoadingIcon().exists()).toBe(false); + }); + + it('shows a loading spinner while fetching the current attribute', () => { + createComponent({ + queries: { + currentAttribute: { loading: true }, + }, + }); + + expect(findEditableLoadingIcon().exists()).toBe(true); + }); + + it('shows the loading spinner and the title of the selected attribute while updating', () => { + createComponent({ + data: { + updating: true, + selectedTitle: 'Some milestone title', + }, + queries: { + currentAttribute: { loading: false }, + }, + }); + + expect(findEditableLoadingIcon().exists()).toBe(true); + expect(findSelectedAttribute().text()).toBe('Some milestone title'); + }); + + describe('when current attribute does not exist', () => { + it('renders "None" as the selected attribute title', () => { + createComponent(); + + expect(findSelectedAttribute().text()).toBe('None'); + }); + }); + }); + + describe('when a user can edit', () => { + describe('when user is editing', () => { + describe('when rendering the dropdown', () => { + it('shows a loading spinner while fetching a list of attributes', async () => { + createComponent({ + queries: { + attributesList: { loading: true }, + }, + }); + + await toggleDropdown(); + + expect(findLoadingIconDropdown().exists()).toBe(true); + }); + + describe('GlDropdownItem with the right title and id', () => { + const id = 'id'; + const title = 'title'; + + beforeEach(async () => { + createComponent({ + data: { attributesList: [{ id, title }], currentAttribute: { id, title } }, + }); + + await toggleDropdown(); + }); + + it('does not show a loading spinner', () => { + expect(findLoadingIconDropdown().exists()).toBe(false); + }); + + it('renders title $title', () => { + expect(findDropdownItemWithText(title).exists()).toBe(true); + }); + + it('checks the correct dropdown item', () => { + expect( + findAllDropdownItems() + .filter((w) => w.props('isChecked') === true) + .at(0) + .text(), + ).toBe(title); + }); + }); + + describe('when no data is assigned', () => { + beforeEach(async () => { + createComponent(); + + await toggleDropdown(); + }); + + it('finds GlDropdownItem with "No milestone"', () => { + expect(findNoAttributeItem().text()).toBe('No milestone'); + }); + + it('"No milestone" is checked', () => { + expect(findNoAttributeItem().props('isChecked')).toBe(true); + }); + + it('does not render any dropdown item', () => { + expect(findAttributeItems().exists()).toBe(false); + }); + }); + + describe('when clicking on dropdown item', () => { + describe('when currentAttribute is equal to attribute id', () => { + it('does not call setIssueAttribute mutation', async () => { + createComponent({ + data: { + attributesList: [{ id: 'id', title: 'title' }], + currentAttribute: { id: 'id', title: 'title' }, + }, + }); + + await toggleDropdown(); + + findDropdownItemWithText('title').vm.$emit('click'); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(0); + }); + }); + + describe('when currentAttribute is not equal to attribute id', () => { + describe('when error', () => { + const bootstrapComponent = (mutationResp) => { + createComponent({ + data: { + attributesList: [ + { id: '123', title: '123' }, + { id: 'id', title: 'title' }, + ], + currentAttribute: '123', + }, + mutationPromise: mutationResp, + }); + }; + + describe.each` + description | mutationResp | expectedMsg + ${'top-level error'} | ${mutationError} | ${'Failed to set milestone on this issue. Please try again.'} + ${'user-recoverable error'} | ${mutationSuccessWithErrors} | ${firstErrorMsg} + `(`$description`, ({ mutationResp, expectedMsg }) => { + beforeEach(async () => { + bootstrapComponent(mutationResp); + + await toggleDropdown(); + + findDropdownItemWithText('title').vm.$emit('click'); + }); + + it(`calls createFlash with "${expectedMsg}"`, async () => { + await wrapper.vm.$nextTick(); + expect(createFlash).toHaveBeenCalledWith({ + message: expectedMsg, + captureError: true, + error: expectedMsg, + }); + }); + }); + }); + }); + }); + }); + + describe('when a user is searching', () => { + describe('when search result is not found', () => { + it('renders "No milestone found"', async () => { + createComponent(); + + await toggleDropdown(); + + findSearchBox().vm.$emit('input', 'non existing milestones'); + + await wrapper.vm.$nextTick(); + + expect(findDropdownText().text()).toBe('No milestone found'); + }); + }); + }); + }); + }); + + describe('with mock apollo', () => { + let error; + + beforeEach(() => { + jest.spyOn(Sentry, 'captureException'); + error = new Error('mayday'); + }); + + describe("when issuable type is 'issue'", () => { + describe('when dropdown is expanded and user can edit', () => { + let milestoneMutationSpy; + beforeEach(async () => { + milestoneMutationSpy = jest.fn().mockResolvedValue(mockMilestoneMutationResponse); + + await createComponentWithApollo({ + requestHandlers: [[projectIssueMilestoneMutation, milestoneMutationSpy]], + }); + + await clickEdit(); + }); + + it('renders the dropdown on clicking edit', async () => { + expect(findDropdown().isVisible()).toBe(true); + }); + + it('focuses on the input when dropdown is shown', async () => { + expect(document.activeElement).toEqual(wrapper.findComponent(GlFormInput).element); + }); + + describe('when currentAttribute is not equal to attribute id', () => { + describe('when update is successful', () => { + beforeEach(() => { + findDropdownItemWithText(mockMilestone2.title).vm.$emit('click'); + }); + + it('calls setIssueAttribute mutation', () => { + expect(milestoneMutationSpy).toHaveBeenCalledWith({ + iid: mockIssue.iid, + attributeId: getIdFromGraphQLId(mockMilestone2.id), + fullPath: mockIssue.projectPath, + }); + }); + + it('sets the value returned from the mutation to currentAttribute', async () => { + expect(findSelectedAttribute().text()).toBe(mockMilestone2.title); + }); + }); + }); + + describe('milestones', () => { + let projectMilestonesSpy; + + it('should call createFlash if milestones query fails', async () => { + await createComponentWithApollo({ + projectMilestonesSpy: jest.fn().mockRejectedValue(error), + }); + + await clickEdit(); + + expect(createFlash).toHaveBeenCalledWith({ + message: wrapper.vm.i18n.listFetchError, + captureError: true, + error: expect.any(Error), + }); + }); + + it('only fetches attributes when dropdown is opened', async () => { + projectMilestonesSpy = jest.fn().mockResolvedValueOnce(emptyProjectMilestonesResponse); + await createComponentWithApollo({ projectMilestonesSpy }); + + expect(projectMilestonesSpy).not.toHaveBeenCalled(); + + await clickEdit(); + + expect(projectMilestonesSpy).toHaveBeenNthCalledWith(1, { + fullPath: mockIssue.projectPath, + title: '', + state: 'active', + }); + }); + + describe('when a user is searching', () => { + const mockSearchTerm = 'foobar'; + + beforeEach(async () => { + projectMilestonesSpy = jest + .fn() + .mockResolvedValueOnce(emptyProjectMilestonesResponse); + await createComponentWithApollo({ projectMilestonesSpy }); + + await clickEdit(); + }); + + it('sends a projectMilestones query with the entered search term "foo"', async () => { + findSearchBox().vm.$emit('input', mockSearchTerm); + await wrapper.vm.$nextTick(); + + // Account for debouncing + jest.runAllTimers(); + + expect(projectMilestonesSpy).toHaveBeenNthCalledWith(2, { + fullPath: mockIssue.projectPath, + title: mockSearchTerm, + state: 'active', + }); + }); + }); + }); + }); + + describe('currentAttributes', () => { + it('should call createFlash if currentAttributes query fails', async () => { + await createComponentWithApollo({ + currentMilestoneSpy: jest.fn().mockRejectedValue(error), + }); + + expect(createFlash).toHaveBeenCalledWith({ + message: wrapper.vm.i18n.currentFetchError, + captureError: true, + error: expect.any(Error), + }); + }); + }); + }); + }); +}); diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js index b052038661a..2c0a213df6d 100644 --- a/spec/frontend/sidebar/mock_data.js +++ b/spec/frontend/sidebar/mock_data.js @@ -513,4 +513,83 @@ export const participantsQueryResponse = { }, }; +export const mockGroupPath = 'gitlab-org'; +export const mockProjectPath = `${mockGroupPath}/some-project`; + +export const mockIssue = { + projectPath: mockProjectPath, + iid: '1', + groupPath: mockGroupPath, +}; + +export const mockIssueId = 'gid://gitlab/Issue/1'; + +export const mockMilestone1 = { + __typename: 'Milestone', + id: 'gid://gitlab/Milestone/1', + title: 'Foobar Milestone', + webUrl: 'http://gdk.test:3000/groups/gitlab-org/-/milestones/1', + state: 'active', +}; + +export const mockMilestone2 = { + __typename: 'Milestone', + id: 'gid://gitlab/Milestone/2', + title: 'Awesome Milestone', + webUrl: 'http://gdk.test:3000/groups/gitlab-org/-/milestones/2', + state: 'active', +}; + +export const mockProjectMilestonesResponse = { + data: { + workspace: { + attributes: { + nodes: [mockMilestone1, mockMilestone2], + }, + __typename: 'MilestoneConnection', + }, + __typename: 'Project', + }, +}; + +export const noCurrentMilestoneResponse = { + data: { + workspace: { + issuable: { id: mockIssueId, attribute: null, __typename: 'Issue' }, + __typename: 'Project', + }, + }, +}; + +export const mockMilestoneMutationResponse = { + data: { + issuableSetAttribute: { + errors: [], + issuable: { + id: 'gid://gitlab/Issue/1', + attribute: { + id: 'gid://gitlab/Milestone/2', + title: 'Awesome Milestone', + state: 'active', + __typename: 'Milestone', + }, + __typename: 'Issue', + }, + __typename: 'UpdateIssuePayload', + }, + }, +}; + +export const emptyProjectMilestonesResponse = { + data: { + workspace: { + attributes: { + nodes: [], + }, + __typename: 'MilestoneConnection', + }, + __typename: 'Project', + }, +}; + export default mockData; diff --git a/spec/migrations/clean_up_pending_builds_table_spec.rb b/spec/migrations/clean_up_pending_builds_table_spec.rb new file mode 100644 index 00000000000..9211b41d81e --- /dev/null +++ b/spec/migrations/clean_up_pending_builds_table_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20210525075724_clean_up_pending_builds_table.rb') + +RSpec.describe CleanUpPendingBuildsTable do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:queue) { table(:ci_pending_builds) } + let(:builds) { table(:ci_builds) } + + before do + namespaces.create!(id: 123, name: 'sample', path: 'sample') + projects.create!(id: 123, name: 'sample', path: 'sample', namespace_id: 123) + + builds.create!(id: 1, project_id: 123, status: 'pending', type: 'Ci::Build') + builds.create!(id: 2, project_id: 123, status: 'pending', type: 'GenericCommitStatus') + builds.create!(id: 3, project_id: 123, status: 'success', type: 'Ci::Bridge') + builds.create!(id: 4, project_id: 123, status: 'success', type: 'Ci::Build') + builds.create!(id: 5, project_id: 123, status: 'running', type: 'Ci::Build') + builds.create!(id: 6, project_id: 123, status: 'created', type: 'Ci::Build') + + queue.create!(id: 1, project_id: 123, build_id: 1) + queue.create!(id: 2, project_id: 123, build_id: 4) + queue.create!(id: 3, project_id: 123, build_id: 5) + end + + it 'removes duplicated data from pending builds table' do + migrate! + + expect(queue.all.count).to eq 1 + expect(queue.first.id).to eq 1 + expect(builds.all.count).to eq 6 + end + + context 'when there are multiple batches' do + before do + stub_const("#{described_class}::BATCH_SIZE", 1) + end + + it 'iterates the data correctly' do + migrate! + + expect(queue.all.count).to eq 1 + end + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index d5a12ba6e6e..b0883d39a82 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -354,7 +354,7 @@ RSpec.describe Ci::Build do it 'does not push build to the queue' do build.enqueue - expect(::Ci::PendingBuild.all.count).to be_zero + expect(build.queuing_entry).not_to be_present end end diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb index cd2fa2ded23..8896bd44077 100644 --- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb +++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb @@ -23,7 +23,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do let(:runner) { create(:ci_runner, :project, projects: [project]) } let(:user) { create(:user) } let(:job) do - create(:ci_build, :artifacts, :extended_options, + create(:ci_build, :pending, :queued, :artifacts, :extended_options, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) end @@ -129,7 +129,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do context 'when other projects have pending jobs' do before do job.success - create(:ci_build, :pending) + create(:ci_build, :pending, :queued) end it_behaves_like 'no jobs available' @@ -239,7 +239,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end context 'when job is made for tag' do - let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } it 'sets branch as ref_type' do request_job @@ -297,7 +297,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end context 'when job filtered by job_age' do - let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, queued_at: 60.seconds.ago) } + let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, queued_at: 60.seconds.ago) } context 'job is queued less than job_age parameter' do let(:job_age) { 120 } @@ -359,7 +359,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end context 'when job is for a release' do - let!(:job) { create(:ci_build, :release_options, pipeline: pipeline) } + let!(:job) { create(:ci_build, :pending, :queued, :release_options, pipeline: pipeline) } context 'when `multi_build_steps` is passed by the runner' do it 'exposes release info' do @@ -398,7 +398,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do context 'when job is made for merge request' do let(:pipeline) { create(:ci_pipeline, source: :merge_request_event, project: project, ref: 'feature', merge_request: merge_request) } - let!(:job) { create(:ci_build, pipeline: pipeline, name: 'spinach', ref: 'feature', stage: 'test', stage_idx: 0) } + let!(:job) { create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'spinach', ref: 'feature', stage: 'test', stage_idx: 0) } let(:merge_request) { create(:merge_request) } it 'sets branch as ref_type' do @@ -479,9 +479,9 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end context 'when project and pipeline have multiple jobs' do - let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } - let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } - let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } + let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job2) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } + let!(:test_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } before do job.success @@ -531,8 +531,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end context 'when pipeline have jobs with artifacts' do - let!(:job) { create(:ci_build, :tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } - let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } + let!(:job) { create(:ci_build, :pending, :queued, :tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:test_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } before do job.success @@ -551,10 +551,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end context 'when explicit dependencies are defined' do - let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } - let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } + let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job2) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } let!(:test_job) do - create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'deploy', + create(:ci_build, :pending, :queued, pipeline: pipeline, token: 'test-job-token', name: 'deploy', stage: 'deploy', stage_idx: 1, options: { script: ['bash'], dependencies: [job2.name] }) end @@ -575,10 +575,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end context 'when dependencies is an empty array' do - let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } - let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } + let!(:job) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:job2) { create(:ci_build, :pending, :queued, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) } let!(:empty_dependencies_job) do - create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'empty_dependencies_job', + create(:ci_build, :pending, :queued, pipeline: pipeline, token: 'test-job-token', name: 'empty_dependencies_job', stage: 'deploy', stage_idx: 1, options: { script: ['bash'], dependencies: [] }) end @@ -739,7 +739,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end describe 'port support' do - let(:job) { create(:ci_build, pipeline: pipeline, options: options) } + let(:job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) } context 'when job image has ports' do let(:options) do @@ -791,7 +791,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do describe 'a job with excluded artifacts' do context 'when excluded paths are defined' do let(:job) do - create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'test', + create(:ci_build, :pending, :queued, pipeline: pipeline, token: 'test-job-token', name: 'test', stage: 'deploy', stage_idx: 1, options: { artifacts: { paths: ['abc'], exclude: ['cde'] } }) end @@ -839,7 +839,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do subject { request_job } context 'when triggered by a user' do - let(:job) { create(:ci_build, user: user, project: project) } + let(:job) { create(:ci_build, :pending, :queued, user: user, project: project) } subject { request_job(id: job.id) } diff --git a/spec/requests/api/feature_flags_spec.rb b/spec/requests/api/feature_flags_spec.rb index dd12648f4dd..923ebefe01f 100644 --- a/spec/requests/api/feature_flags_spec.rb +++ b/spec/requests/api/feature_flags_spec.rb @@ -148,6 +148,18 @@ RSpec.describe API::FeatureFlags do expect(json_response['version']).to eq('legacy_flag') end + context 'without legacy flags' do + before do + stub_feature_flags(remove_legacy_flags: true, remove_legacy_flags_override: false) + end + + it 'returns not found' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + it_behaves_like 'check user permission' end @@ -492,6 +504,18 @@ RSpec.describe API::FeatureFlags do end it_behaves_like 'check user permission' + + context 'without legacy flags' do + before do + stub_feature_flags(remove_legacy_flags: true, remove_legacy_flags_override: false) + end + + it 'returns not found' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end end context 'when feature flag exists already' do @@ -537,6 +561,18 @@ RSpec.describe API::FeatureFlags do end end end + + context 'without legacy flags' do + before do + stub_feature_flags(remove_legacy_flags: true, remove_legacy_flags_override: false) + end + + it 'returns not found' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end end context 'with a version 2 flag' do @@ -612,6 +648,18 @@ RSpec.describe API::FeatureFlags do }) end + context 'without legacy flags' do + before do + stub_feature_flags(remove_legacy_flags: true, remove_legacy_flags_override: false) + end + + it 'returns not found' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + it_behaves_like 'check user permission' context 'when strategies become empty array after the removal' do @@ -976,6 +1024,20 @@ RSpec.describe API::FeatureFlags do expect(feature_flag.reload.strategies.first.scopes.count).to eq(0) end end + + context 'without legacy flags' do + before do + stub_feature_flags(remove_legacy_flags: true, remove_legacy_flags_override: false) + end + + it 'returns not found' do + params = { description: 'new description' } + + put api("/projects/#{project.id}/feature_flags/other_flag_name", user), params: params + + expect(response).to have_gitlab_http_status(:not_found) + end + end end describe 'DELETE /projects/:id/feature_flags/:name' do diff --git a/spec/serializers/merge_request_diff_entity_spec.rb b/spec/serializers/merge_request_diff_entity_spec.rb index a3b356505b8..5bc7eea92a8 100644 --- a/spec/serializers/merge_request_diff_entity_spec.rb +++ b/spec/serializers/merge_request_diff_entity_spec.rb @@ -11,7 +11,13 @@ RSpec.describe MergeRequestDiffEntity do let(:merge_request_diff) { merge_request_diffs.first } let(:entity) do - described_class.new(merge_request_diff, request: request, merge_request: merge_request, merge_request_diffs: merge_request_diffs) + described_class.new( + merge_request_diff, + request: request, + merge_request: merge_request, + merge_request_diff: merge_request_diff, + merge_request_diffs: merge_request_diffs + ) end subject { entity.as_json } @@ -26,6 +32,46 @@ RSpec.describe MergeRequestDiffEntity do end end + describe '#version_index' do + shared_examples 'version_index is nil' do + it 'returns nil' do + expect(subject[:version_index]).to be_nil + end + end + + context 'when diff is not present' do + let(:entity) do + described_class.new( + merge_request_diff, + request: request, + merge_request: merge_request, + merge_request_diffs: merge_request_diffs + ) + end + + it_behaves_like 'version_index is nil' + end + + context 'when diff is not included in @merge_request_diffs' do + let(:merge_request_diff) { create(:merge_request_diff) } + let(:merge_request_diff_2) { create(:merge_request_diff) } + + before do + merge_request_diffs << merge_request_diff_2 + end + + it_behaves_like 'version_index is nil' + end + + context 'when @merge_request_diffs.size <= 1' do + before do + expect(merge_request_diffs.size).to eq(1) + end + + it_behaves_like 'version_index is nil' + end + end + describe '#short_commit_sha' do it 'returns short sha' do expect(subject[:short_commit_sha]).to eq('b83d6e39') diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb index 99b176a692c..554fd4d4fb0 100644 --- a/spec/services/ci/register_job_service_spec.rb +++ b/spec/services/ci/register_job_service_spec.rb @@ -11,7 +11,7 @@ module Ci let!(:shared_runner) { create(:ci_runner, :instance) } let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) } let!(:group_runner) { create(:ci_runner, :group, groups: [group]) } - let!(:pending_job) { create(:ci_build, pipeline: pipeline) } + let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) } describe '#execute' do context 'checks database loadbalancing stickiness' do @@ -104,11 +104,11 @@ module Ci let!(:project3) { create :project, shared_runners_enabled: true } let!(:pipeline3) { create :ci_pipeline, project: project3 } let!(:build1_project1) { pending_job } - let!(:build2_project1) { FactoryBot.create :ci_build, pipeline: pipeline } - let!(:build3_project1) { FactoryBot.create :ci_build, pipeline: pipeline } - let!(:build1_project2) { FactoryBot.create :ci_build, pipeline: pipeline2 } - let!(:build2_project2) { FactoryBot.create :ci_build, pipeline: pipeline2 } - let!(:build1_project3) { FactoryBot.create :ci_build, pipeline: pipeline3 } + let!(:build2_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) } + let!(:build3_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) } + let!(:build1_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) } + let!(:build2_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) } + let!(:build1_project3) { create(:ci_build, :pending, :queued, pipeline: pipeline3) } context 'when using fair scheduling' do context 'when all builds are pending' do @@ -255,17 +255,17 @@ module Ci let!(:pipeline3) { create(:ci_pipeline, project: project3) } let!(:build1_project1) { pending_job } - let!(:build2_project1) { create(:ci_build, pipeline: pipeline) } - let!(:build3_project1) { create(:ci_build, pipeline: pipeline) } - let!(:build1_project2) { create(:ci_build, pipeline: pipeline2) } - let!(:build2_project2) { create(:ci_build, pipeline: pipeline2) } - let!(:build1_project3) { create(:ci_build, pipeline: pipeline3) } + let!(:build2_project1) { create(:ci_build, :queued, pipeline: pipeline) } + let!(:build3_project1) { create(:ci_build, :queued, pipeline: pipeline) } + let!(:build1_project2) { create(:ci_build, :queued, pipeline: pipeline2) } + let!(:build2_project2) { create(:ci_build, :queued, pipeline: pipeline2) } + let!(:build1_project3) { create(:ci_build, :queued, pipeline: pipeline3) } # these shouldn't influence the scheduling let!(:unrelated_group) { create(:group) } let!(:unrelated_project) { create(:project, group_runners_enabled: true, group: unrelated_group) } let!(:unrelated_pipeline) { create(:ci_pipeline, project: unrelated_project) } - let!(:build1_unrelated_project) { create(:ci_build, pipeline: unrelated_pipeline) } + let!(:build1_unrelated_project) { create(:ci_build, :pending, :queued, pipeline: unrelated_pipeline) } let!(:unrelated_group_runner) { create(:ci_runner, :group, groups: [unrelated_group]) } it 'does not consider builds from other group runners' do @@ -346,7 +346,7 @@ module Ci subject { described_class.new(specific_runner).execute } context 'with multiple builds are in queue' do - let!(:other_build) { create :ci_build, pipeline: pipeline } + let!(:other_build) { create(:ci_build, :pending, :queued, pipeline: pipeline) } before do allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_project_runner) @@ -387,7 +387,7 @@ module Ci let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) } context 'when a job is protected' do - let!(:pending_job) { create(:ci_build, :protected, pipeline: pipeline) } + let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) } it 'picks the job' do expect(execute(specific_runner)).to eq(pending_job) @@ -395,7 +395,7 @@ module Ci end context 'when a job is unprotected' do - let!(:pending_job) { create(:ci_build, pipeline: pipeline) } + let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) } it 'picks the job' do expect(execute(specific_runner)).to eq(pending_job) @@ -403,7 +403,7 @@ module Ci end context 'when protected attribute of a job is nil' do - let!(:pending_job) { create(:ci_build, pipeline: pipeline) } + let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) } before do pending_job.update_attribute(:protected, nil) @@ -419,7 +419,7 @@ module Ci let!(:specific_runner) { create(:ci_runner, :project, :ref_protected, projects: [project]) } context 'when a job is protected' do - let!(:pending_job) { create(:ci_build, :protected, pipeline: pipeline) } + let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) } it 'picks the job' do expect(execute(specific_runner)).to eq(pending_job) @@ -427,7 +427,7 @@ module Ci end context 'when a job is unprotected' do - let!(:pending_job) { create(:ci_build, pipeline: pipeline) } + let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) } it 'does not pick the job' do expect(execute(specific_runner)).to be_nil @@ -435,7 +435,7 @@ module Ci end context 'when protected attribute of a job is nil' do - let!(:pending_job) { create(:ci_build, pipeline: pipeline) } + let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) } before do pending_job.update_attribute(:protected, nil) @@ -449,7 +449,7 @@ module Ci context 'runner feature set is verified' do let(:options) { { artifacts: { reports: { junit: "junit.xml" } } } } - let!(:pending_job) { create(:ci_build, :pending, pipeline: pipeline, options: options) } + let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) } subject { execute(specific_runner, params) } @@ -485,7 +485,7 @@ module Ci shared_examples 'validation is active' do context 'when depended job has not been completed yet' do - let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) } + let!(:pre_stage_job) { create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) } it { expect(subject).to eq(pending_job) } end @@ -522,7 +522,7 @@ module Ci shared_examples 'validation is not active' do context 'when depended job has not been completed yet' do - let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) } + let!(:pre_stage_job) { create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) } it { expect(subject).to eq(pending_job) } end @@ -547,7 +547,7 @@ module Ci let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0) } let!(:pending_job) do - create(:ci_build, :pending, + create(:ci_build, :pending, :queued, pipeline: pipeline, stage_idx: 1, options: { script: ["bash"], dependencies: ['test'] }) end @@ -558,7 +558,7 @@ module Ci end context 'when build is degenerated' do - let!(:pending_job) { create(:ci_build, :pending, :degenerated, pipeline: pipeline) } + let!(:pending_job) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) } subject { execute(specific_runner, {}) } @@ -573,7 +573,7 @@ module Ci context 'when build has data integrity problem' do let!(:pending_job) do - create(:ci_build, :pending, pipeline: pipeline) + create(:ci_build, :pending, :queued, pipeline: pipeline) end before do @@ -598,7 +598,7 @@ module Ci context 'when build fails to be run!' do let!(:pending_job) do - create(:ci_build, :pending, pipeline: pipeline) + create(:ci_build, :pending, :queued, pipeline: pipeline) end before do @@ -640,12 +640,12 @@ module Ci context 'when only some builds can be matched by runner' do let!(:specific_runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[matching]) } - let!(:pending_job) { create(:ci_build, pipeline: pipeline, tag_list: %w[matching]) } + let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[matching]) } before do # create additional matching and non-matching jobs - create_list(:ci_build, 2, pipeline: pipeline, tag_list: %w[matching]) - create(:ci_build, pipeline: pipeline, tag_list: %w[non-matching]) + create_list(:ci_build, 2, :pending, :queued, pipeline: pipeline, tag_list: %w[matching]) + create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[non-matching]) end it 'observes queue size of only matching jobs' do @@ -693,7 +693,7 @@ module Ci end context 'when there is another build in queue' do - let!(:next_pending_job) { create(:ci_build, pipeline: pipeline) } + let!(:next_pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) } it 'skips this build and picks another build' do expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment) @@ -732,6 +732,22 @@ module Ci include_examples 'handles runner assignment' end + + context 'when joining with pending builds table' do + before do + stub_feature_flags(ci_pending_builds_queue_join: true) + end + + include_examples 'handles runner assignment' + end + + context 'when not joining with pending builds table' do + before do + stub_feature_flags(ci_pending_builds_queue_join: false) + end + + include_examples 'handles runner assignment' + end end describe '#register_success' do @@ -775,8 +791,8 @@ module Ci end context 'when project already has running jobs' do - let!(:build2) { create( :ci_build, :running, pipeline: pipeline, runner: shared_runner) } - let!(:build3) { create( :ci_build, :running, pipeline: pipeline, runner: shared_runner) } + let!(:build2) { create(:ci_build, :running, pipeline: pipeline, runner: shared_runner) } + let!(:build3) { create(:ci_build, :running, pipeline: pipeline, runner: shared_runner) } it 'counts job queuing time histogram with expected labels' do allow(attempt_counter).to receive(:increment) @@ -859,9 +875,9 @@ module Ci end context 'when max queue depth is reached' do - let!(:pending_job) { create(:ci_build, :pending, :degenerated, pipeline: pipeline) } - let!(:pending_job_2) { create(:ci_build, :pending, :degenerated, pipeline: pipeline) } - let!(:pending_job_3) { create(:ci_build, :pending, pipeline: pipeline) } + let!(:pending_job) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) } + let!(:pending_job_2) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) } + let!(:pending_job_3) { create(:ci_build, :pending, :queued, pipeline: pipeline) } before do stub_const("#{described_class}::MAX_QUEUE_DEPTH", 2) diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index 31532a5d9a8..f047bf649fb 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -66,7 +66,7 @@ RSpec.describe Ci::RetryBuildService do let_it_be(:another_pipeline) { create(:ci_empty_pipeline, project: project) } let_it_be(:build) do - create(:ci_build, :failed, :expired, :erased, :queued, :coverage, :tags, + create(:ci_build, :failed, :picked, :expired, :erased, :queued, :coverage, :tags, :allowed_to_fail, :on_tag, :triggered, :teardown_environment, :resource_group, description: 'my-job', stage: 'test', stage_id: stage.id, pipeline: pipeline, auto_canceled_by: another_pipeline, diff --git a/spec/services/deployments/update_environment_service_spec.rb b/spec/services/deployments/update_environment_service_spec.rb index 4d15258a186..6996563fdb8 100644 --- a/spec/services/deployments/update_environment_service_spec.rb +++ b/spec/services/deployments/update_environment_service_spec.rb @@ -95,6 +95,42 @@ RSpec.describe Deployments::UpdateEnvironmentService do end end + context 'when external URL is specified and the tier is unset' do + let(:options) { { name: 'production', url: external_url } } + + before do + environment.update_columns(external_url: external_url, tier: nil) + job.update!(environment: 'production') + end + + context 'when external URL is valid' do + let(:external_url) { 'https://google.com' } + + it 'succeeds to update the tier automatically' do + expect { subject.execute }.to change { environment.tier }.from(nil).to('production') + end + end + + context 'when external URL is invalid' do + let(:external_url) { 'google.com' } + + it 'fails to update the tier due to validation error' do + expect { subject.execute }.not_to change { environment.tier } + end + + it 'tracks an exception' do + expect(Gitlab::ErrorTracking).to receive(:track_exception) + .with(an_instance_of(described_class::EnvironmentUpdateFailure), + project_id: project.id, + environment_id: environment.id, + reason: %q{External url is blocked: Only allowed schemes are http, https}) + .once + + subject.execute + end + end + end + context 'when variables are used' do let(:options) do { name: 'review-apps/$CI_COMMIT_REF_NAME', |