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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/projects/feature_flags_controller_spec.rb52
-rw-r--r--spec/factories/ci/builds.rb10
-rw-r--r--spec/features/boards/sidebar_milestones_spec.rb4
-rw-r--r--spec/frontend/boards/components/board_content_sidebar_spec.js28
-rw-r--r--spec/frontend/notes/components/noteable_discussion_spec.js12
-rw-r--r--spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js503
-rw-r--r--spec/frontend/sidebar/mock_data.js79
-rw-r--r--spec/migrations/clean_up_pending_builds_table_spec.rb47
-rw-r--r--spec/models/ci/build_spec.rb2
-rw-r--r--spec/requests/api/ci/runner/jobs_request_post_spec.rb40
-rw-r--r--spec/requests/api/feature_flags_spec.rb62
-rw-r--r--spec/serializers/merge_request_diff_entity_spec.rb48
-rw-r--r--spec/services/ci/register_job_service_spec.rb86
-rw-r--r--spec/services/ci/retry_build_service_spec.rb2
-rw-r--r--spec/services/deployments/update_environment_service_spec.rb36
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',