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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-03-15 18:09:07 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-03-15 18:09:07 +0300
commitf5c3f32975addd56fe8659f1c346d0e56f0b23d9 (patch)
tree9d6594793e656c52341a98d22d882d96d240433f /spec
parentc8b7a349bc50cff1e8ef18204042978476527b0b (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/experiments/application_experiment_spec.rb9
-rw-r--r--spec/features/admin/dashboard_spec.rb10
-rw-r--r--spec/frontend/issuable/components/csv_import_export_buttons_spec.js34
-rw-r--r--spec/frontend/issuable_show/components/issuable_body_spec.js71
-rw-r--r--spec/frontend/issuable_show/components/issuable_description_spec.js32
-rw-r--r--spec/frontend/issuable_show/components/issuable_header_spec.js21
-rw-r--r--spec/frontend/issuable_show/components/issuable_show_root_spec.js22
-rw-r--r--spec/frontend/issuable_show/mock_data.js8
-rw-r--r--spec/frontend/lib/utils/datetime_utility_spec.js29
-rw-r--r--spec/frontend/sidebar/components/reference/sidebar_reference_widget_spec.js93
-rw-r--r--spec/frontend/sidebar/mock_data.js12
-rw-r--r--spec/lib/gitlab/ci/config/entry/bridge_spec.rb46
-rw-r--r--spec/lib/gitlab/ci/config/entry/product/parallel_spec.rb34
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb26
-rw-r--r--spec/mailers/emails/service_desk_spec.rb11
-rw-r--r--spec/mailers/notify_spec.rb3
-rw-r--r--spec/models/issue_spec.rb13
-rw-r--r--spec/services/ci/create_pipeline_service/parallel_spec.rb118
-rw-r--r--spec/services/ci/pipeline_processing/shared_processing_service.rb78
-rw-r--r--spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb8
-rw-r--r--spec/services/notification_service_spec.rb30
-rw-r--r--spec/support/gitlab_experiment.rb1
-rw-r--r--spec/views/projects/issues/import_csv/_button.html.haml_spec.rb43
23 files changed, 650 insertions, 102 deletions
diff --git a/spec/experiments/application_experiment_spec.rb b/spec/experiments/application_experiment_spec.rb
index 0a46155b56d..2481ee5a806 100644
--- a/spec/experiments/application_experiment_spec.rb
+++ b/spec/experiments/application_experiment_spec.rb
@@ -58,6 +58,15 @@ RSpec.describe ApplicationExperiment, :experiment do
end
describe "publishing results" do
+ it "doesn't track or push data to the client if we shouldn't track", :snowplow do
+ allow(subject).to receive(:should_track?).and_return(false)
+ expect(Gon).not_to receive(:push)
+
+ subject.publish(:action)
+
+ expect_no_snowplow_event
+ end
+
it "tracks the assignment" do
expect(subject).to receive(:track).with(:assignment)
diff --git a/spec/features/admin/dashboard_spec.rb b/spec/features/admin/dashboard_spec.rb
index c040811ada1..618fae3e46b 100644
--- a/spec/features/admin/dashboard_spec.rb
+++ b/spec/features/admin/dashboard_spec.rb
@@ -30,7 +30,6 @@ RSpec.describe 'admin visits dashboard' do
describe 'Users statistic' do
let_it_be(:users_statistics) { create(:users_statistics) }
- let_it_be(:users_count_label) { Gitlab.ee? ? 'Billable users 71' : 'Active users 71' }
it 'shows correct amounts of users', :aggregate_failures do
visit admin_dashboard_stats_path
@@ -42,9 +41,16 @@ RSpec.describe 'admin visits dashboard' do
expect(page).to have_content('Users with highest role Maintainer 6')
expect(page).to have_content('Users with highest role Owner 5')
expect(page).to have_content('Bots 2')
+
+ if Gitlab.ee?
+ expect(page).to have_content('Billable users 69')
+ else
+ expect(page).not_to have_content('Billable users 69')
+ end
+
expect(page).to have_content('Blocked users 7')
expect(page).to have_content('Total users 78')
- expect(page).to have_content(users_count_label)
+ expect(page).to have_content('Active users 71')
end
end
end
diff --git a/spec/frontend/issuable/components/csv_import_export_buttons_spec.js b/spec/frontend/issuable/components/csv_import_export_buttons_spec.js
index e96409b61b9..e32bf35b13a 100644
--- a/spec/frontend/issuable/components/csv_import_export_buttons_spec.js
+++ b/spec/frontend/issuable/components/csv_import_export_buttons_spec.js
@@ -94,11 +94,37 @@ describe('CsvImportExportButtons', () => {
expect(findImportCsvButton().exists()).toBe(true);
});
- it('import button has a tooltip', () => {
- const tooltip = getBinding(findImportDropdown().element, 'gl-tooltip');
+ describe('when showLabel=false', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ showImportButton: true, showLabel: false });
+ });
- expect(tooltip).toBeDefined();
- expect(tooltip.value).toBe('Import issues');
+ it('does not have a button text', () => {
+ expect(findImportCsvButton().props('text')).toBe(null);
+ });
+
+ it('import button has a tooltip', () => {
+ const tooltip = getBinding(findImportDropdown().element, 'gl-tooltip');
+
+ expect(tooltip).toBeDefined();
+ expect(tooltip.value).toBe('Import issues');
+ });
+ });
+
+ describe('when showLabel=true', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ showImportButton: true, showLabel: true });
+ });
+
+ it('displays a button text', () => {
+ expect(findImportCsvButton().props('text')).toBe('Import issues');
+ });
+
+ it('import button has no tooltip', () => {
+ const tooltip = getBinding(findImportDropdown().element, 'gl-tooltip');
+
+ expect(tooltip.value).toBe(null);
+ });
});
it('renders the import modal', () => {
diff --git a/spec/frontend/issuable_show/components/issuable_body_spec.js b/spec/frontend/issuable_show/components/issuable_body_spec.js
index bf166bea1e5..6fa298ca3f2 100644
--- a/spec/frontend/issuable_show/components/issuable_body_spec.js
+++ b/spec/frontend/issuable_show/components/issuable_body_spec.js
@@ -6,11 +6,13 @@ import IssuableBody from '~/issuable_show/components/issuable_body.vue';
import IssuableDescription from '~/issuable_show/components/issuable_description.vue';
import IssuableEditForm from '~/issuable_show/components/issuable_edit_form.vue';
import IssuableTitle from '~/issuable_show/components/issuable_title.vue';
+import TaskList from '~/task_list';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { mockIssuableShowProps, mockIssuable } from '../mock_data';
jest.mock('~/autosave');
+jest.mock('~/flash');
const issuableBodyProps = {
...mockIssuableShowProps,
@@ -80,6 +82,75 @@ describe('IssuableBody', () => {
});
});
+ describe('watchers', () => {
+ describe('editFormVisible', () => {
+ it('calls initTaskList in nextTick', async () => {
+ jest.spyOn(wrapper.vm, 'initTaskList');
+ wrapper.setProps({
+ editFormVisible: true,
+ });
+
+ await wrapper.vm.$nextTick();
+
+ wrapper.setProps({
+ editFormVisible: false,
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.initTaskList).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('mounted', () => {
+ it('initializes TaskList instance when enabledEdit and enableTaskList props are true', () => {
+ expect(wrapper.vm.taskList instanceof TaskList).toBe(true);
+ expect(wrapper.vm.taskList).toMatchObject({
+ dataType: 'issue',
+ fieldName: 'description',
+ lockVersion: issuableBodyProps.taskListLockVersion,
+ selector: '.js-detail-page-description',
+ onSuccess: expect.any(Function),
+ onError: expect.any(Function),
+ });
+ });
+
+ it('does not initialize TaskList instance when either enabledEdit or enableTaskList prop is false', () => {
+ const wrapperNoTaskList = createComponent({
+ ...issuableBodyProps,
+ enableTaskList: false,
+ });
+
+ expect(wrapperNoTaskList.vm.taskList).not.toBeDefined();
+
+ wrapperNoTaskList.destroy();
+ });
+ });
+
+ describe('methods', () => {
+ describe('handleTaskListUpdateSuccess', () => {
+ it('emits `task-list-update-success` event on component', () => {
+ const updatedIssuable = {
+ foo: 'bar',
+ };
+
+ wrapper.vm.handleTaskListUpdateSuccess(updatedIssuable);
+
+ expect(wrapper.emitted('task-list-update-success')).toBeTruthy();
+ expect(wrapper.emitted('task-list-update-success')[0]).toEqual([updatedIssuable]);
+ });
+ });
+
+ describe('handleTaskListUpdateFailure', () => {
+ it('emits `task-list-update-failure` event on component', () => {
+ wrapper.vm.handleTaskListUpdateFailure();
+
+ expect(wrapper.emitted('task-list-update-failure')).toBeTruthy();
+ });
+ });
+ });
+
describe('template', () => {
it('renders issuable-title component', () => {
const titleEl = wrapper.find(IssuableTitle);
diff --git a/spec/frontend/issuable_show/components/issuable_description_spec.js b/spec/frontend/issuable_show/components/issuable_description_spec.js
index 29ecce1002d..1058e5decfd 100644
--- a/spec/frontend/issuable_show/components/issuable_description_spec.js
+++ b/spec/frontend/issuable_show/components/issuable_description_spec.js
@@ -5,9 +5,14 @@ import IssuableDescription from '~/issuable_show/components/issuable_description
import { mockIssuable } from '../mock_data';
-const createComponent = (issuable = mockIssuable) =>
+const createComponent = ({
+ issuable = mockIssuable,
+ enableTaskList = true,
+ canEdit = true,
+ taskListUpdatePath = `${mockIssuable.webUrl}.json`,
+} = {}) =>
shallowMount(IssuableDescription, {
- propsData: { issuable },
+ propsData: { issuable, enableTaskList, canEdit, taskListUpdatePath },
});
describe('IssuableDescription', () => {
@@ -38,4 +43,27 @@ describe('IssuableDescription', () => {
});
});
});
+
+ describe('templates', () => {
+ it('renders container element with class `js-task-list-container` when canEdit and enableTaskList props are true', () => {
+ expect(wrapper.classes()).toContain('js-task-list-container');
+ });
+
+ it('renders container element without class `js-task-list-container` when canEdit and enableTaskList props are true', () => {
+ const wrapperNoTaskList = createComponent({
+ enableTaskList: false,
+ });
+
+ expect(wrapperNoTaskList.classes()).not.toContain('js-task-list-container');
+
+ wrapperNoTaskList.destroy();
+ });
+
+ it('renders hidden textarea element when issuable.description is present and enableTaskList prop is true', () => {
+ const textareaEl = wrapper.find('textarea.gl-display-none.js-task-list-field');
+
+ expect(textareaEl.exists()).toBe(true);
+ expect(textareaEl.attributes('data-update-url')).toBe(`${mockIssuable.webUrl}.json`);
+ });
+ });
});
diff --git a/spec/frontend/issuable_show/components/issuable_header_spec.js b/spec/frontend/issuable_show/components/issuable_header_spec.js
index 2164caa40a8..b85f2dd1999 100644
--- a/spec/frontend/issuable_show/components/issuable_header_spec.js
+++ b/spec/frontend/issuable_show/components/issuable_header_spec.js
@@ -119,6 +119,27 @@ describe('IssuableHeader', () => {
expect(avatarEl.find(GlAvatarLabeled).find(GlIcon).exists()).toBe(false);
});
+ it('renders tast status text when `taskCompletionStatus` prop is defined', () => {
+ let taskStatusEl = wrapper.findByTestId('task-status');
+
+ expect(taskStatusEl.exists()).toBe(true);
+ expect(taskStatusEl.text()).toContain('0 of 5 tasks completed');
+
+ const wrapperSingleTask = createComponent({
+ ...issuableHeaderProps,
+ taskCompletionStatus: {
+ completedCount: 0,
+ count: 1,
+ },
+ });
+
+ taskStatusEl = wrapperSingleTask.findByTestId('task-status');
+
+ expect(taskStatusEl.text()).toContain('0 of 1 task completed');
+
+ wrapperSingleTask.destroy();
+ });
+
it('renders sidebar toggle button', () => {
const toggleButtonEl = wrapper.findByTestId('sidebar-toggle');
diff --git a/spec/frontend/issuable_show/components/issuable_show_root_spec.js b/spec/frontend/issuable_show/components/issuable_show_root_spec.js
index 3e3778492d2..b4c125f4910 100644
--- a/spec/frontend/issuable_show/components/issuable_show_root_spec.js
+++ b/spec/frontend/issuable_show/components/issuable_show_root_spec.js
@@ -54,6 +54,7 @@ describe('IssuableShowRoot', () => {
editFormVisible,
descriptionPreviewPath,
descriptionHelpPath,
+ taskCompletionStatus,
} = mockIssuableShowProps;
const { blocked, confidential, createdAt, author } = mockIssuable;
@@ -72,6 +73,7 @@ describe('IssuableShowRoot', () => {
confidential,
createdAt,
author,
+ taskCompletionStatus,
});
expect(issuableHeader.find('.issuable-status-box').text()).toContain('Open');
expect(issuableHeader.find('.detail-page-header-actions button.js-close').exists()).toBe(
@@ -111,6 +113,26 @@ describe('IssuableShowRoot', () => {
expect(wrapper.emitted('edit-issuable')).toBeTruthy();
});
+ it('component emits `task-list-update-success` event bubbled via issuable-body', () => {
+ const issuableBody = wrapper.find(IssuableBody);
+ const eventParam = {
+ foo: 'bar',
+ };
+
+ issuableBody.vm.$emit('task-list-update-success', eventParam);
+
+ expect(wrapper.emitted('task-list-update-success')).toBeTruthy();
+ expect(wrapper.emitted('task-list-update-success')[0]).toEqual([eventParam]);
+ });
+
+ it('component emits `task-list-update-failure` event bubbled via issuable-body', () => {
+ const issuableBody = wrapper.find(IssuableBody);
+
+ issuableBody.vm.$emit('task-list-update-failure');
+
+ expect(wrapper.emitted('task-list-update-failure')).toBeTruthy();
+ });
+
it('component emits `sidebar-toggle` event bubbled via issuable-sidebar', () => {
const issuableSidebar = wrapper.find(IssuableSidebar);
diff --git a/spec/frontend/issuable_show/mock_data.js b/spec/frontend/issuable_show/mock_data.js
index af854f420bc..9ecff705617 100644
--- a/spec/frontend/issuable_show/mock_data.js
+++ b/spec/frontend/issuable_show/mock_data.js
@@ -12,6 +12,7 @@ export const mockIssuable = {
blocked: false,
confidential: false,
updatedBy: issuable.author,
+ type: 'ISSUE',
currentUserTodos: {
nodes: [
{
@@ -26,11 +27,18 @@ export const mockIssuableShowProps = {
issuable: mockIssuable,
descriptionHelpPath: '/help/user/markdown',
descriptionPreviewPath: '/gitlab-org/gitlab-shell/preview_markdown',
+ taskListUpdatePath: `${mockIssuable.webUrl}.json`,
+ taskListLockVersion: 1,
editFormVisible: false,
enableAutocomplete: true,
enableAutosave: true,
+ enableTaskList: true,
enableEdit: true,
showFieldTitle: false,
statusBadgeClass: 'status-box-open',
statusIcon: 'issue-open-m',
+ taskCompletionStatus: {
+ completedCount: 0,
+ count: 5,
+ },
};
diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js
index cfb88820c7d..2df0cb00f9a 100644
--- a/spec/frontend/lib/utils/datetime_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime_utility_spec.js
@@ -1136,3 +1136,32 @@ describe('getStartOfDay', () => {
},
);
});
+
+describe('getStartOfWeek', () => {
+ beforeEach(() => {
+ timezoneMock.register('US/Eastern');
+ });
+
+ afterEach(() => {
+ timezoneMock.unregister();
+ });
+
+ it.each`
+ inputAsString | options | expectedAsString
+ ${'2021-01-29T18:08:23.014Z'} | ${undefined} | ${'2021-01-25T05:00:00.000Z'}
+ ${'2021-01-29T13:08:23.014-05:00'} | ${undefined} | ${'2021-01-25T05:00:00.000Z'}
+ ${'2021-01-30T03:08:23.014+09:00'} | ${undefined} | ${'2021-01-25T05:00:00.000Z'}
+ ${'2021-01-28T18:08:23.014-10:00'} | ${undefined} | ${'2021-01-25T05:00:00.000Z'}
+ ${'2021-01-28T18:08:23.014-10:00'} | ${{}} | ${'2021-01-25T05:00:00.000Z'}
+ ${'2021-01-28T18:08:23.014-10:00'} | ${{ utc: false }} | ${'2021-01-25T05:00:00.000Z'}
+ ${'2021-01-28T18:08:23.014-10:00'} | ${{ utc: true }} | ${'2021-01-26T00:00:00.000Z'}
+ `(
+ 'when the provided date is $inputAsString and the options parameter is $options, returns $expectedAsString',
+ ({ inputAsString, options, expectedAsString }) => {
+ const inputDate = new Date(inputAsString);
+ const actual = datetimeUtility.getStartOfWeek(inputDate, options);
+
+ expect(actual.toISOString()).toEqual(expectedAsString);
+ },
+ );
+});
diff --git a/spec/frontend/sidebar/components/reference/sidebar_reference_widget_spec.js b/spec/frontend/sidebar/components/reference/sidebar_reference_widget_spec.js
new file mode 100644
index 00000000000..1dbb7702a15
--- /dev/null
+++ b/spec/frontend/sidebar/components/reference/sidebar_reference_widget_spec.js
@@ -0,0 +1,93 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { IssuableType } from '~/issue_show/constants';
+import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue';
+import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
+import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { issueReferenceResponse } from '../../mock_data';
+
+describe('Sidebar Reference Widget', () => {
+ let wrapper;
+ let fakeApollo;
+ const referenceText = 'reference';
+
+ const createComponent = ({
+ issuableType,
+ referenceQuery = issueReferenceQuery,
+ referenceQueryHandler = jest.fn().mockResolvedValue(issueReferenceResponse(referenceText)),
+ } = {}) => {
+ Vue.use(VueApollo);
+
+ fakeApollo = createMockApollo([[referenceQuery, referenceQueryHandler]]);
+
+ wrapper = shallowMount(SidebarReferenceWidget, {
+ apolloProvider: fakeApollo,
+ provide: {
+ fullPath: 'group/project',
+ iid: '1',
+ },
+ propsData: {
+ issuableType,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe.each([
+ [IssuableType.Issue, issueReferenceQuery],
+ [IssuableType.MergeRequest, mergeRequestReferenceQuery],
+ ])('when issuableType is %s', (issuableType, referenceQuery) => {
+ it('displays the reference text', async () => {
+ createComponent({
+ issuableType,
+ referenceQuery,
+ });
+
+ await waitForPromises();
+
+ expect(wrapper.text()).toContain(referenceText);
+ });
+
+ it('displays loading icon while fetching and hides clipboard icon', async () => {
+ createComponent({
+ issuableType,
+ referenceQuery,
+ });
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.find(ClipboardButton).exists()).toBe(false);
+ });
+
+ it('calls createFlash with correct parameters', async () => {
+ const mockError = new Error('mayday');
+
+ createComponent({
+ issuableType,
+ referenceQuery,
+ referenceQueryHandler: jest.fn().mockRejectedValue(mockError),
+ });
+
+ await waitForPromises();
+
+ const [
+ [
+ {
+ message,
+ error: { networkError },
+ },
+ ],
+ ] = wrapper.emitted('fetch-error');
+ expect(message).toBe('An error occurred while fetching reference');
+ expect(networkError).toEqual(mockError);
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js
index 6268499b4e8..e751f1239c8 100644
--- a/spec/frontend/sidebar/mock_data.js
+++ b/spec/frontend/sidebar/mock_data.js
@@ -233,4 +233,16 @@ export const issueConfidentialityResponse = (confidential = false) => ({
},
});
+export const issueReferenceResponse = (reference) => ({
+ data: {
+ workspace: {
+ __typename: 'Project',
+ issuable: {
+ __typename: 'Issue',
+ id: 'gid://gitlab/Issue/4',
+ reference,
+ },
+ },
+ },
+});
export default mockData;
diff --git a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
index b3b7901074a..179578fe0a8 100644
--- a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb
@@ -244,6 +244,52 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do
end
end
end
+
+ context 'when bridge config contains parallel' do
+ let(:config) { { trigger: 'some/project', parallel: parallel_config } }
+
+ context 'when parallel config is a number' do
+ let(:parallel_config) { 2 }
+
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
+
+ describe '#errors' do
+ it 'returns an error message' do
+ expect(subject.errors)
+ .to include(/cannot use "parallel: <number>"/)
+ end
+ end
+ end
+
+ context 'when parallel config is a matrix' do
+ let(:parallel_config) do
+ { matrix: [{ PROVIDER: 'aws', STACK: %w[monitoring app1] },
+ { PROVIDER: 'gcp', STACK: %w[data] }] }
+ end
+
+ describe '#valid?' do
+ it { is_expected.to be_valid }
+ end
+
+ describe '#value' do
+ it 'is returns a bridge job configuration' do
+ expect(subject.value).to eq(
+ name: :my_bridge,
+ trigger: { project: 'some/project' },
+ ignore: false,
+ stage: 'test',
+ only: { refs: %w[branches tags] },
+ parallel: { matrix: [{ 'PROVIDER' => ['aws'], 'STACK' => %w(monitoring app1) },
+ { 'PROVIDER' => ['gcp'], 'STACK' => %w(data) }] },
+ variables: {},
+ scheduling_type: :stage
+ )
+ end
+ end
+ end
+ end
end
describe '#manual_action?' do
diff --git a/spec/lib/gitlab/ci/config/entry/product/parallel_spec.rb b/spec/lib/gitlab/ci/config/entry/product/parallel_spec.rb
index bc09e20d748..937642f07e7 100644
--- a/spec/lib/gitlab/ci/config/entry/product/parallel_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/product/parallel_spec.rb
@@ -4,21 +4,23 @@ require 'fast_spec_helper'
require_dependency 'active_model'
RSpec.describe ::Gitlab::Ci::Config::Entry::Product::Parallel do
- subject(:parallel) { described_class.new(config) }
+ let(:metadata) { {} }
- context 'with invalid config' do
- shared_examples 'invalid config' do |error_message|
- describe '#valid?' do
- it { is_expected.not_to be_valid }
- end
+ subject(:parallel) { described_class.new(config, **metadata) }
- describe '#errors' do
- it 'returns error about invalid type' do
- expect(parallel.errors).to match(a_collection_including(error_message))
- end
+ shared_examples 'invalid config' do |error_message|
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
+
+ describe '#errors' do
+ it 'returns error about invalid type' do
+ expect(parallel.errors).to match(a_collection_including(error_message))
end
end
+ end
+ context 'with invalid config' do
context 'when it is not a numeric value' do
let(:config) { true }
@@ -63,6 +65,12 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Product::Parallel do
expect(parallel.value).to match(number: config)
end
end
+
+ context 'when :numeric is not allowed' do
+ let(:metadata) { { allowed_strategies: [:matrix] } }
+
+ it_behaves_like 'invalid config', /cannot use "parallel: <number>"/
+ end
end
end
@@ -89,6 +97,12 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Product::Parallel do
])
end
end
+
+ context 'when :matrix is not allowed' do
+ let(:metadata) { { allowed_strategies: [:numeric] } }
+
+ it_behaves_like 'invalid config', /cannot use "parallel: matrix"/
+ end
end
end
end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index aceb2299cc0..9178707a3d0 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -180,6 +180,32 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
end
+ context 'when with_lock_retries re-runs the block' do
+ it 'only creates constraint for unique definitions' do
+ expected_sql = <<~SQL
+ ALTER TABLE "#{table_name}"\nADD CONSTRAINT "check_cda6f69506" CHECK (char_length("name") <= 255)
+ SQL
+
+ expect(model).to receive(:create_table).twice.and_call_original
+
+ expect(model).to receive(:execute).with(expected_sql).and_raise(ActiveRecord::LockWaitTimeout)
+ expect(model).to receive(:execute).with(expected_sql).and_call_original
+
+ model.create_table_with_constraints table_name do |t|
+ t.timestamps_with_timezone
+ t.integer :some_id, null: false
+ t.boolean :active, null: false, default: true
+ t.text :name
+
+ t.text_limit :name, 255
+ end
+
+ expect_table_columns_to_match(column_attributes, table_name)
+
+ expect_check_constraint(table_name, 'check_cda6f69506', 'char_length(name) <= 255')
+ end
+ end
+
context 'when constraints are given invalid names' do
let(:expected_max_length) { described_class::MAX_IDENTIFIER_NAME_LENGTH }
let(:expected_error_message) { "The maximum allowed constraint name is #{expected_max_length} characters" }
diff --git a/spec/mailers/emails/service_desk_spec.rb b/spec/mailers/emails/service_desk_spec.rb
index 7d04b373be6..cb74194020d 100644
--- a/spec/mailers/emails/service_desk_spec.rb
+++ b/spec/mailers/emails/service_desk_spec.rb
@@ -13,8 +13,13 @@ RSpec.describe Emails::ServiceDesk do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:email) { 'someone@gitlab.com' }
let(:template) { double(content: template_content) }
+ before_all do
+ issue.issue_email_participants.create!(email: email)
+ end
+
before do
stub_const('ServiceEmailClass', Class.new(ApplicationMailer))
@@ -72,6 +77,10 @@ RSpec.describe Emails::ServiceDesk do
let(:template_content) { 'custom text' }
let(:issue) { create(:issue, project: project)}
+ before do
+ issue.issue_email_participants.create!(email: email)
+ end
+
context 'when a template is in the repository' do
let(:project) { create(:project, :custom_repo, files: { ".gitlab/service_desk_templates/#{template_key}.md" => template_content }) }
@@ -151,7 +160,7 @@ RSpec.describe Emails::ServiceDesk do
let_it_be(:note) { create(:note_on_issue, noteable: issue, project: project) }
let_it_be(:default_text) { note.note }
- subject { ServiceEmailClass.service_desk_new_note_email(issue.id, note.id) }
+ subject { ServiceEmailClass.service_desk_new_note_email(issue.id, note.id, email) }
it_behaves_like 'read template from repository', 'new_note'
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 399f6ff5576..79358d3e40c 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -1218,6 +1218,7 @@ RSpec.describe Notify do
context 'for service desk issues' do
before do
issue.update!(external_author: 'service.desk@example.com')
+ issue.issue_email_participants.create!(email: 'service.desk@example.com')
end
def expect_sender(username)
@@ -1266,7 +1267,7 @@ RSpec.describe Notify do
describe 'new note email' do
let_it_be(:first_note) { create(:discussion_note_on_issue, note: 'Hello world') }
- subject { described_class.service_desk_new_note_email(issue.id, first_note.id) }
+ subject { described_class.service_desk_new_note_email(issue.id, first_note.id, 'service.desk@example.com') }
it_behaves_like 'an unsubscribeable thread'
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 965da59a0ed..a3e245f4def 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -1259,11 +1259,22 @@ RSpec.describe Issue do
end
end
+ describe '#email_participants_emails' do
+ let_it_be(:issue) { create(:issue) }
+
+ it 'returns a list of emails' do
+ participant1 = issue.issue_email_participants.create(email: 'a@gitlab.com')
+ participant2 = issue.issue_email_participants.create(email: 'b@gitlab.com')
+
+ expect(issue.email_participants_emails).to contain_exactly(participant1.email, participant2.email)
+ end
+ end
+
describe '#email_participants_downcase' do
it 'returns a list of emails with all uppercase letters replaced with their lowercase counterparts' do
participant = create(:issue_email_participant, email: 'SomEoNe@ExamPLe.com')
- expect(participant.issue.email_participants_downcase).to match([participant.email.downcase])
+ expect(participant.issue.email_participants_emails_downcase).to match([participant.email.downcase])
end
end
end
diff --git a/spec/services/ci/create_pipeline_service/parallel_spec.rb b/spec/services/ci/create_pipeline_service/parallel_spec.rb
new file mode 100644
index 00000000000..5e34a67d376
--- /dev/null
+++ b/spec/services/ci/create_pipeline_service/parallel_spec.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Ci::CreatePipelineService do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { project.owner }
+
+ let(:service) { described_class.new(project, user, { ref: 'master' }) }
+ let(:pipeline) { service.execute(:push) }
+
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ context 'job:parallel' do
+ context 'numeric' do
+ let(:config) do
+ <<-EOY
+ job:
+ script: "echo job"
+ parallel: 3
+ EOY
+ end
+
+ it 'creates the pipeline' do
+ expect(pipeline).to be_created_successfully
+ end
+
+ it 'creates 3 jobs' do
+ expect(pipeline.processables.pluck(:name)).to contain_exactly(
+ 'job 1/3', 'job 2/3', 'job 3/3'
+ )
+ end
+ end
+
+ context 'matrix' do
+ let(:config) do
+ <<-EOY
+ job:
+ script: "echo job"
+ parallel:
+ matrix:
+ - PROVIDER: ovh
+ STACK: [monitoring, app]
+ - PROVIDER: [gcp, vultr]
+ STACK: [data]
+ EOY
+ end
+
+ it 'creates the pipeline' do
+ expect(pipeline).to be_created_successfully
+ end
+
+ it 'creates 4 builds with the corresponding matrix variables' do
+ expect(pipeline.processables.pluck(:name)).to contain_exactly(
+ 'job: [gcp, data]', 'job: [ovh, app]', 'job: [ovh, monitoring]', 'job: [vultr, data]'
+ )
+
+ job1 = find_job('job: [gcp, data]')
+ job2 = find_job('job: [ovh, app]')
+ job3 = find_job('job: [ovh, monitoring]')
+ job4 = find_job('job: [vultr, data]')
+
+ expect(job1.scoped_variables.to_hash).to include('PROVIDER' => 'gcp', 'STACK' => 'data')
+ expect(job2.scoped_variables.to_hash).to include('PROVIDER' => 'ovh', 'STACK' => 'app')
+ expect(job3.scoped_variables.to_hash).to include('PROVIDER' => 'ovh', 'STACK' => 'monitoring')
+ expect(job4.scoped_variables.to_hash).to include('PROVIDER' => 'vultr', 'STACK' => 'data')
+ end
+
+ context 'when a bridge is using parallel:matrix' do
+ let(:config) do
+ <<-EOY
+ job:
+ stage: test
+ script: "echo job"
+
+ deploy:
+ stage: deploy
+ trigger:
+ include: child.yml
+ parallel:
+ matrix:
+ - PROVIDER: ovh
+ STACK: [monitoring, app]
+ - PROVIDER: [gcp, vultr]
+ STACK: [data]
+ EOY
+ end
+
+ it 'creates the pipeline' do
+ expect(pipeline).to be_created_successfully
+ end
+
+ it 'creates 1 build and 4 bridges with the corresponding matrix variables' do
+ expect(pipeline.processables.pluck(:name)).to contain_exactly(
+ 'job', 'deploy: [gcp, data]', 'deploy: [ovh, app]', 'deploy: [ovh, monitoring]', 'deploy: [vultr, data]'
+ )
+
+ bridge1 = find_job('deploy: [gcp, data]')
+ bridge2 = find_job('deploy: [ovh, app]')
+ bridge3 = find_job('deploy: [ovh, monitoring]')
+ bridge4 = find_job('deploy: [vultr, data]')
+
+ expect(bridge1.scoped_variables.to_hash).to include('PROVIDER' => 'gcp', 'STACK' => 'data')
+ expect(bridge2.scoped_variables.to_hash).to include('PROVIDER' => 'ovh', 'STACK' => 'app')
+ expect(bridge3.scoped_variables.to_hash).to include('PROVIDER' => 'ovh', 'STACK' => 'monitoring')
+ expect(bridge4.scoped_variables.to_hash).to include('PROVIDER' => 'vultr', 'STACK' => 'data')
+ end
+ end
+ end
+ end
+
+ private
+
+ def find_job(name)
+ pipeline.processables.find { |job| job.name == name }
+ end
+end
diff --git a/spec/services/ci/pipeline_processing/shared_processing_service.rb b/spec/services/ci/pipeline_processing/shared_processing_service.rb
index 215f33a42a3..13c924a3089 100644
--- a/spec/services/ci/pipeline_processing/shared_processing_service.rb
+++ b/spec/services/ci/pipeline_processing/shared_processing_service.rb
@@ -1,21 +1,13 @@
# frozen_string_literal: true
RSpec.shared_examples 'Pipeline Processing Service' do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
+ let(:user) { project.owner }
let(:pipeline) do
create(:ci_empty_pipeline, ref: 'master', project: project)
end
- before do
- stub_ci_pipeline_to_return_yaml_file
-
- stub_not_protect_default_branch
-
- project.add_developer(user)
- end
-
context 'when simple pipeline is defined' do
before do
create_build('linux', stage_idx: 0)
@@ -866,10 +858,74 @@ RSpec.shared_examples 'Pipeline Processing Service' do
end
end
+ context 'when a bridge job has parallel:matrix config', :sidekiq_inline do
+ let(:parent_config) do
+ <<-EOY
+ test:
+ stage: test
+ script: echo test
+
+ deploy:
+ stage: deploy
+ trigger:
+ include: .child.yml
+ parallel:
+ matrix:
+ - PROVIDER: ovh
+ STACK: [monitoring, app]
+ EOY
+ end
+
+ let(:child_config) do
+ <<-EOY
+ test:
+ stage: test
+ script: echo test
+ EOY
+ end
+
+ let(:pipeline) do
+ Ci::CreatePipelineService.new(project, user, { ref: 'master' }).execute(:push)
+ end
+
+ before do
+ allow_next_instance_of(Repository) do |repository|
+ allow(repository)
+ .to receive(:blob_data_at)
+ .with(an_instance_of(String), '.gitlab-ci.yml')
+ .and_return(parent_config)
+
+ allow(repository)
+ .to receive(:blob_data_at)
+ .with(an_instance_of(String), '.child.yml')
+ .and_return(child_config)
+ end
+ end
+
+ it 'creates pipeline with bridges, then passes the matrix variables to downstream jobs' do
+ expect(all_builds_names).to contain_exactly('test', 'deploy: [ovh, monitoring]', 'deploy: [ovh, app]')
+ expect(all_builds_statuses).to contain_exactly('pending', 'created', 'created')
+
+ succeed_pending
+
+ # bridge jobs directly transition to success
+ expect(all_builds_statuses).to contain_exactly('success', 'success', 'success')
+
+ bridge1 = all_builds.find_by(name: 'deploy: [ovh, monitoring]')
+ bridge2 = all_builds.find_by(name: 'deploy: [ovh, app]')
+
+ downstream_job1 = bridge1.downstream_pipeline.processables.first
+ downstream_job2 = bridge2.downstream_pipeline.processables.first
+
+ expect(downstream_job1.scoped_variables.to_hash).to include('PROVIDER' => 'ovh', 'STACK' => 'monitoring')
+ expect(downstream_job2.scoped_variables.to_hash).to include('PROVIDER' => 'ovh', 'STACK' => 'app')
+ end
+ end
+
private
def all_builds
- pipeline.builds.order(:stage_idx, :id)
+ pipeline.processables.order(:stage_idx, :id)
end
def builds
diff --git a/spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb b/spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb
index 2936d6fae4d..a9f9db8c689 100644
--- a/spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb
+++ b/spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb
@@ -1,21 +1,19 @@
# frozen_string_literal: true
RSpec.shared_context 'Pipeline Processing Service Tests With Yaml' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { project.owner }
+
where(:test_file_path) do
Dir.glob(Rails.root.join('spec/services/ci/pipeline_processing/test_cases/*.yml'))
end
with_them do
let(:test_file) { YAML.load_file(test_file_path) }
-
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
let(:pipeline) { Ci::CreatePipelineService.new(project, user, ref: 'master').execute(:pipeline) }
before do
stub_ci_pipeline_yaml_file(YAML.dump(test_file['config']))
- stub_not_protect_default_branch
- project.add_developer(user)
end
it 'follows transitions' do
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 03dc3be194c..f3cd2776ce7 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -315,17 +315,17 @@ RSpec.describe NotificationService, :mailer do
describe 'Notes' do
context 'issue note' do
let_it_be(:project) { create(:project, :private) }
- let_it_be(:issue) { create(:issue, project: project, assignees: [assignee]) }
+ let_it_be_with_reload(:issue) { create(:issue, project: project, assignees: [assignee]) }
let_it_be(:mentioned_issue) { create(:issue, assignees: issue.assignees) }
let_it_be_with_reload(:author) { create(:user) }
let(:note) { create(:note_on_issue, author: author, noteable: issue, project_id: issue.project_id, note: '@mention referenced, @unsubscribed_mentioned and @outsider also') }
subject { notification.new_note(note) }
- context 'on service desk issue' do
+ context 'issue_email_participants' do
before do
allow(Notify).to receive(:service_desk_new_note_email)
- .with(Integer, Integer).and_return(mailer)
+ .with(Integer, Integer, String).and_return(mailer)
allow(::Gitlab::IncomingEmail).to receive(:enabled?) { true }
allow(::Gitlab::IncomingEmail).to receive(:supports_wildcard?) { true }
@@ -336,7 +336,7 @@ RSpec.describe NotificationService, :mailer do
def should_email!
expect(Notify).to receive(:service_desk_new_note_email)
- .with(issue.id, note.id)
+ .with(issue.id, note.id, issue.external_author)
end
def should_not_email!
@@ -365,33 +365,19 @@ RSpec.describe NotificationService, :mailer do
let(:project) { issue.project }
let(:note) { create(:note, noteable: issue, project: project) }
- context 'a non-service-desk issue' do
+ context 'do not exist' do
it_should_not_email!
end
- context 'a service-desk issue' do
+ context 'do exist' do
+ let!(:issue_email_participant) { issue.issue_email_participants.create!(email: 'service.desk@example.com') }
+
before do
issue.update!(external_author: 'service.desk@example.com')
project.update!(service_desk_enabled: true)
end
it_should_email!
-
- context 'where the project has disabled the feature' do
- before do
- project.update!(service_desk_enabled: false)
- end
-
- it_should_not_email!
- end
-
- context 'when the support bot has unsubscribed' do
- before do
- issue.unsubscribe(User.support_bot, project)
- end
-
- it_should_not_email!
- end
end
end
diff --git a/spec/support/gitlab_experiment.rb b/spec/support/gitlab_experiment.rb
index 4015db329fc..bd0c88f8049 100644
--- a/spec/support/gitlab_experiment.rb
+++ b/spec/support/gitlab_experiment.rb
@@ -6,6 +6,7 @@ require_relative 'stub_snowplow'
# This is a temporary fix until we have a larger discussion around the
# challenges raised in https://gitlab.com/gitlab-org/gitlab/-/issues/300104
+require Rails.root.join('app', 'experiments', 'application_experiment')
class ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
def initialize(...)
super(...)
diff --git a/spec/views/projects/issues/import_csv/_button.html.haml_spec.rb b/spec/views/projects/issues/import_csv/_button.html.haml_spec.rb
deleted file mode 100644
index 8bc0a00d71c..00000000000
--- a/spec/views/projects/issues/import_csv/_button.html.haml_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'projects/issues/import_csv/_button' do
- include Devise::Test::ControllerHelpers
-
- context 'when the user does not have edit permissions' do
- before do
- render
- end
-
- it 'shows a dropdown button to import CSV' do
- expect(rendered).to have_text('Import CSV')
- end
-
- it 'does not show a button to import from Jira' do
- expect(rendered).not_to have_text('Import from Jira')
- end
- end
-
- context 'when the user has edit permissions' do
- let(:project) { create(:project) }
- let(:current_user) { create(:user, maintainer_projects: [project]) }
-
- before do
- allow(view).to receive(:project_import_jira_path).and_return('import/jira')
- allow(view).to receive(:current_user).and_return(current_user)
-
- assign(:project, project)
-
- render
- end
-
- it 'shows a dropdown button to import CSV' do
- expect(rendered).to have_text('Import CSV')
- end
-
- it 'shows a button to import from Jira' do
- expect(rendered).to have_text('Import from Jira')
- end
- end
-end