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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-09-12 18:12:04 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-09-12 18:12:04 +0300
commit1ba682300fb97a96de47cc5b261f6df93ca78bd0 (patch)
treea8f0ccf2892780510ac406373425ac6d554c8ee7 /spec
parent0127158127cb4f21b06ea39cc243d8ac17fc3e41 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/features/projects/feature_flags/user_creates_feature_flag_spec.rb29
-rw-r--r--spec/frontend/feature_flags/components/new_environments_dropdown_spec.js80
-rw-r--r--spec/frontend/feature_flags/components/strategy_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_view_spec.js22
-rw-r--r--spec/graphql/types/ci/job_type_spec.rb1
-rw-r--r--spec/helpers/sidekiq_helper_spec.rb2
-rw-r--r--spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb248
-rw-r--r--spec/models/concerns/issuable_spec.rb15
-rw-r--r--spec/models/concerns/transitionable_spec.rb42
-rw-r--r--spec/models/merge_request_spec.rb36
-rw-r--r--spec/models/performance_monitoring/prometheus_metric_spec.rb67
-rw-r--r--spec/models/performance_monitoring/prometheus_panel_spec.rb74
-rw-r--r--spec/requests/api/graphql/ci/jobs_spec.rb103
14 files changed, 292 insertions, 450 deletions
diff --git a/spec/features/projects/feature_flags/user_creates_feature_flag_spec.rb b/spec/features/projects/feature_flags/user_creates_feature_flag_spec.rb
index 4af5c91479a..127610cf4db 100644
--- a/spec/features/projects/feature_flags/user_creates_feature_flag_spec.rb
+++ b/spec/features/projects/feature_flags/user_creates_feature_flag_spec.rb
@@ -7,13 +7,14 @@ RSpec.describe 'User creates feature flag', :js do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
+ let!(:environment) { create(:environment, :production, project: project) }
before do
project.add_developer(user)
sign_in(user)
end
- it 'user creates a flag enabled for user ids' do
+ it 'user creates a flag enabled for user ids with existing environment' do
visit(new_project_feature_flag_path(project))
set_feature_flag_info('test_feature', 'Test feature')
within_strategy_row(1) do
@@ -29,6 +30,22 @@ RSpec.describe 'User creates feature flag', :js do
expect(page).to have_text('test_feature')
end
+ it 'user creates a flag enabled for user ids with non-existing environment' do
+ visit(new_project_feature_flag_path(project))
+ set_feature_flag_info('test_feature', 'Test feature')
+ within_strategy_row(1) do
+ select 'User IDs', from: 'Type'
+ fill_in 'User IDs', with: 'user1, user2'
+ environment_plus_button.click
+ environment_search_input.set('foo-bar')
+ environment_search_create_button.first.click
+ end
+ click_button 'Create feature flag'
+
+ expect_user_to_see_feature_flags_index_page
+ expect(page).to have_text('test_feature')
+ end
+
it 'user creates a flag with default environment scopes' do
visit(new_project_feature_flag_path(project))
set_feature_flag_info('test_flag', 'Test flag')
@@ -74,14 +91,18 @@ RSpec.describe 'User creates feature flag', :js do
end
def environment_plus_button
- find('.js-new-environments-dropdown')
+ find('[data-testid=new-environments-dropdown]')
end
def environment_search_input
- find('.js-new-environments-dropdown input')
+ find('[data-testid=new-environments-dropdown] input')
end
def environment_search_results
- all('.js-new-environments-dropdown button.dropdown-item')
+ all('[data-testid=new-environments-dropdown] li')
+ end
+
+ def environment_search_create_button
+ all('[data-testid=new-environments-dropdown] button')
end
end
diff --git a/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js b/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
index 6156addd63f..b503a6f829e 100644
--- a/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
+++ b/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
@@ -1,7 +1,6 @@
-import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NewEnvironmentsDropdown from '~/feature_flags/components/new_environments_dropdown.vue';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
@@ -13,87 +12,78 @@ describe('New Environments Dropdown', () => {
let wrapper;
let axiosMock;
- beforeEach(() => {
+ const createWrapper = (axiosResult = []) => {
axiosMock = new MockAdapter(axios);
- wrapper = shallowMount(NewEnvironmentsDropdown, {
+ axiosMock.onGet(TEST_HOST).reply(HTTP_STATUS_OK, axiosResult);
+
+ wrapper = shallowMountExtended(NewEnvironmentsDropdown, {
provide: { environmentsEndpoint: TEST_HOST },
+ stubs: {
+ GlCollapsibleListbox,
+ },
});
- });
+ };
+
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findCreateEnvironmentButton = () => wrapper.findByTestId('add-environment-button');
afterEach(() => {
axiosMock.restore();
});
describe('before results', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
it('should show a loading icon', () => {
- axiosMock.onGet(TEST_HOST).reply(() => {
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- });
- wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus');
- return axios.waitForAll();
+ expect(findListbox().props('searching')).toBe(true);
});
it('should not show any dropdown items', () => {
- axiosMock.onGet(TEST_HOST).reply(() => {
- expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(0);
- });
- wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus');
- return axios.waitForAll();
+ expect(findListbox().props('items')).toEqual([]);
});
});
describe('with empty results', () => {
- let item;
beforeEach(async () => {
- axiosMock.onGet(TEST_HOST).reply(HTTP_STATUS_OK, []);
- wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus');
- wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', TEST_SEARCH);
+ createWrapper();
+ findListbox().vm.$emit('search', TEST_SEARCH);
await axios.waitForAll();
- await nextTick();
- item = wrapper.findComponent(GlDropdownItem);
});
it('should display a Create item label', () => {
- expect(item.text()).toBe('Create production');
- });
-
- it('should display that no matching items are found', () => {
- expect(wrapper.findComponent({ ref: 'noResults' }).exists()).toBe(true);
+ expect(findCreateEnvironmentButton().text()).toBe(`Create ${TEST_SEARCH}`);
});
it('should emit a new scope when selected', () => {
- item.vm.$emit('click');
+ findCreateEnvironmentButton().vm.$emit('click');
expect(wrapper.emitted('add')).toEqual([[TEST_SEARCH]]);
});
});
describe('with results', () => {
- let items;
- beforeEach(() => {
- axiosMock.onGet(TEST_HOST).reply(HTTP_STATUS_OK, ['prod', 'production']);
- wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus');
- wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'prod');
- return axios.waitForAll().then(() => {
- items = wrapper.findAllComponents(GlDropdownItem);
- });
+ beforeEach(async () => {
+ createWrapper(['prod', 'production']);
+ findListbox().vm.$emit('search', TEST_SEARCH);
+ await axios.waitForAll();
});
- it('should display one item per result', () => {
- expect(items).toHaveLength(2);
+ it('should populate results properly', () => {
+ expect(findListbox().props().items).toHaveLength(2);
});
- it('should emit an add if an item is clicked', () => {
- items.at(0).vm.$emit('click');
+ it('should emit an add on selection', () => {
+ findListbox().vm.$emit('select', ['prod']);
expect(wrapper.emitted('add')).toEqual([['prod']]);
});
- it('should not display a create label', () => {
- items = items.filter((i) => i.text().startsWith('Create'));
- expect(items).toHaveLength(0);
- });
-
it('should not display a message about no results', () => {
expect(wrapper.findComponent({ ref: 'noResults' }).exists()).toBe(false);
});
+
+ it('should not display a footer with the create button', () => {
+ expect(findCreateEnvironmentButton().exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/feature_flags/components/strategy_spec.js b/spec/frontend/feature_flags/components/strategy_spec.js
index ca6e338ac6c..90021829212 100644
--- a/spec/frontend/feature_flags/components/strategy_spec.js
+++ b/spec/frontend/feature_flags/components/strategy_spec.js
@@ -1,11 +1,14 @@
import { GlAlert, GlFormSelect, GlLink, GlToken, GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import { last } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import Api from '~/api';
+import axios from '~/lib/utils/axios_utils';
import NewEnvironmentsDropdown from '~/feature_flags/components/new_environments_dropdown.vue';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import Strategy from '~/feature_flags/components/strategy.vue';
import StrategyParameters from '~/feature_flags/components/strategy_parameters.vue';
import {
@@ -22,16 +25,18 @@ import { userList } from '../mock_data';
jest.mock('~/api');
+const TEST_HOST = '/test';
const provide = {
strategyTypeDocsPagePath: 'link-to-strategy-docs',
environmentsScopeDocsPath: 'link-scope-docs',
- environmentsEndpoint: '',
+ environmentsEndpoint: TEST_HOST,
};
Vue.use(Vuex);
describe('Feature flags strategy', () => {
let wrapper;
+ let axiosMock;
const findStrategyParameters = () => wrapper.findComponent(StrategyParameters);
const findDocsLinks = () => wrapper.findAllComponents(GlLink);
@@ -45,6 +50,8 @@ describe('Feature flags strategy', () => {
provide,
},
) => {
+ axiosMock = new MockAdapter(axios);
+ axiosMock.onGet(TEST_HOST).reply(HTTP_STATUS_OK, []);
wrapper = mount(Strategy, { store: createStore({ projectId: '1' }), ...opts });
};
@@ -52,6 +59,10 @@ describe('Feature flags strategy', () => {
Api.searchFeatureFlagUserLists.mockResolvedValue({ data: [userList] });
});
+ afterEach(() => {
+ axiosMock.restore();
+ });
+
describe('helper links', () => {
const propsData = { strategy: {}, index: 0, userLists: [userList] };
factory({ propsData, provide });
diff --git a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
index 6a3337aa046..eadcd452929 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
@@ -29,6 +29,8 @@ describe('Blob Rich Viewer component', () => {
beforeEach(() => createComponent());
+ const findMarkdownFieldView = () => wrapper.findComponent(MarkdownFieldView);
+
describe('Markdown content', () => {
const generateDummyContent = (contentLength) => {
let generatedContent = '';
@@ -48,14 +50,17 @@ describe('Blob Rich Viewer component', () => {
expect(wrapper.text()).toContain('Line: 10');
expect(wrapper.text()).not.toContain('Line: 50');
expect(wrapper.emitted(CONTENT_LOADED_EVENT)).toBeUndefined();
+ expect(findMarkdownFieldView().props('isLoading')).toBe(true);
});
- it('renders the rest of the file later and emits a content loaded event', () => {
+ it('renders the rest of the file later and emits a content loaded event', async () => {
jest.runAllTimers();
+ await nextTick();
expect(wrapper.text()).toContain('Line: 10');
expect(wrapper.text()).toContain('Line: 50');
expect(wrapper.emitted(CONTENT_LOADED_EVENT)).toHaveLength(1);
+ expect(findMarkdownFieldView().props('isLoading')).toBe(false);
});
it('sanitizes the content', () => {
@@ -72,6 +77,7 @@ describe('Blob Rich Viewer component', () => {
it('renders the entire file immediately and emits a content loaded event', () => {
expect(wrapper.text()).toContain('Line: 5');
expect(wrapper.emitted(CONTENT_LOADED_EVENT)).toHaveLength(1);
+ expect(findMarkdownFieldView().props('isLoading')).toBe(false);
});
it('sanitizes the content', () => {
@@ -97,7 +103,7 @@ describe('Blob Rich Viewer component', () => {
});
it('is using Markdown View Field', () => {
- expect(wrapper.findComponent(MarkdownFieldView).exists()).toBe(true);
+ expect(findMarkdownFieldView().exists()).toBe(true);
});
it('scrolls to the hash location', () => {
diff --git a/spec/frontend/vue_shared/components/markdown/field_view_spec.js b/spec/frontend/vue_shared/components/markdown/field_view_spec.js
index 1bbbe0896f2..f61c67c4f9b 100644
--- a/spec/frontend/vue_shared/components/markdown/field_view_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_view_spec.js
@@ -6,15 +6,27 @@ import { renderGFM } from '~/behaviors/markdown/render_gfm';
jest.mock('~/behaviors/markdown/render_gfm');
describe('Markdown Field View component', () => {
- function createComponent() {
- shallowMount(MarkdownFieldView);
+ function createComponent(isLoading = false) {
+ shallowMount(MarkdownFieldView, { propsData: { isLoading } });
}
- beforeEach(() => {
+ it('processes rendering with GFM', () => {
createComponent();
- });
- it('processes rendering with GFM', () => {
expect(renderGFM).toHaveBeenCalledTimes(1);
});
+
+ describe('watchers', () => {
+ it('does not process rendering with GFM if isLoading is true', () => {
+ createComponent(true);
+
+ expect(renderGFM).not.toHaveBeenCalled();
+ });
+
+ it('processes rendering with GFM when isLoading is updated to `false`', () => {
+ createComponent(false);
+
+ expect(renderGFM).toHaveBeenCalledTimes(1);
+ });
+ });
});
diff --git a/spec/graphql/types/ci/job_type_spec.rb b/spec/graphql/types/ci/job_type_spec.rb
index f31c0d5255c..a69c6f37ee1 100644
--- a/spec/graphql/types/ci/job_type_spec.rb
+++ b/spec/graphql/types/ci/job_type_spec.rb
@@ -32,6 +32,7 @@ RSpec.describe Types::Ci::JobType, feature_category: :continuous_integration do
needs
pipeline
playable
+ previousStageJobs
previousStageJobsOrNeeds
project
queued_at
diff --git a/spec/helpers/sidekiq_helper_spec.rb b/spec/helpers/sidekiq_helper_spec.rb
index 6a0a92bafd8..594996bac95 100644
--- a/spec/helpers/sidekiq_helper_spec.rb
+++ b/spec/helpers/sidekiq_helper_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe SidekiqHelper do
+RSpec.describe SidekiqHelper, feature_category: :shared do
describe 'parse_sidekiq_ps' do
it 'parses line with time' do
line = '55137 10,0 2,1 S+ 2:30pm sidekiq 4.1.4 gitlab [0 of 25 busy] '
diff --git a/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb b/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb
deleted file mode 100644
index 559557f9313..00000000000
--- a/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb
+++ /dev/null
@@ -1,248 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Prometheus::AdditionalMetricsParser do
- include Prometheus::MetricBuilders
-
- let(:parser_error_class) { Gitlab::Prometheus::ParsingError }
-
- describe '#load_groups_from_yaml' do
- subject { described_class.load_groups_from_yaml('dummy.yaml') }
-
- describe 'parsing sample yaml' do
- let(:sample_yaml) do
- <<-EOF.strip_heredoc
- - group: group_a
- priority: 1
- metrics:
- - title: "title"
- required_metrics: [ metric_a, metric_b ]
- weight: 1
- queries: [{ query_range: 'query_range_a', label: label, unit: unit }]
- - title: "title"
- required_metrics: [metric_a]
- weight: 1
- queries: [{ query_range: 'query_range_empty' }]
- - group: group_b
- priority: 1
- metrics:
- - title: title
- required_metrics: ['metric_a']
- weight: 1
- queries: [{query_range: query_range_a}]
- EOF
- end
-
- before do
- allow(described_class).to receive(:load_yaml_file) { YAML.safe_load(sample_yaml) }
- end
-
- it 'parses to two metric groups with 2 and 1 metric respectively' do
- expect(subject.count).to eq(2)
- expect(subject[0].metrics.count).to eq(2)
- expect(subject[1].metrics.count).to eq(1)
- end
-
- it 'provide group data' do
- expect(subject[0]).to have_attributes(name: 'group_a', priority: 1)
- expect(subject[1]).to have_attributes(name: 'group_b', priority: 1)
- end
-
- it 'provides metrics data' do
- metrics = subject.flat_map(&:metrics)
-
- expect(metrics.count).to eq(3)
- expect(metrics[0]).to have_attributes(title: 'title', required_metrics: %w(metric_a metric_b), weight: 1)
- expect(metrics[1]).to have_attributes(title: 'title', required_metrics: %w(metric_a), weight: 1)
- expect(metrics[2]).to have_attributes(title: 'title', required_metrics: %w{metric_a}, weight: 1)
- end
-
- it 'provides query data' do
- queries = subject.flat_map(&:metrics).flat_map(&:queries)
-
- expect(queries.count).to eq(3)
- expect(queries[0]).to eq(query_range: 'query_range_a', label: 'label', unit: 'unit')
- expect(queries[1]).to eq(query_range: 'query_range_empty')
- expect(queries[2]).to eq(query_range: 'query_range_a')
- end
- end
-
- shared_examples 'required field' do |field_name|
- context "when #{field_name} is nil" do
- before do
- allow(described_class).to receive(:load_yaml_file) { YAML.safe_load(field_missing) }
- end
-
- it 'throws parsing error' do
- expect { subject }.to raise_error(parser_error_class, /#{field_name} can't be blank/i)
- end
- end
-
- context "when #{field_name} are not specified" do
- before do
- allow(described_class).to receive(:load_yaml_file) { YAML.safe_load(field_nil) }
- end
-
- it 'throws parsing error' do
- expect { subject }.to raise_error(parser_error_class, /#{field_name} can't be blank/i)
- end
- end
- end
-
- describe 'group required fields' do
- it_behaves_like 'required field', 'metrics' do
- let(:field_nil) do
- <<-EOF.strip_heredoc
- - group: group_a
- priority: 1
- metrics:
- EOF
- end
-
- let(:field_missing) do
- <<-EOF.strip_heredoc
- - group: group_a
- priority: 1
- EOF
- end
- end
-
- it_behaves_like 'required field', 'name' do
- let(:field_nil) do
- <<-EOF.strip_heredoc
- - group:
- priority: 1
- metrics: []
- EOF
- end
-
- let(:field_missing) do
- <<-EOF.strip_heredoc
- - priority: 1
- metrics: []
- EOF
- end
- end
-
- it_behaves_like 'required field', 'priority' do
- let(:field_nil) do
- <<-EOF.strip_heredoc
- - group: group_a
- priority:
- metrics: []
- EOF
- end
-
- let(:field_missing) do
- <<-EOF.strip_heredoc
- - group: group_a
- metrics: []
- EOF
- end
- end
- end
-
- describe 'metrics fields parsing' do
- it_behaves_like 'required field', 'title' do
- let(:field_nil) do
- <<-EOF.strip_heredoc
- - group: group_a
- priority: 1
- metrics:
- - title:
- required_metrics: []
- weight: 1
- queries: []
- EOF
- end
-
- let(:field_missing) do
- <<-EOF.strip_heredoc
- - group: group_a
- priority: 1
- metrics:
- - required_metrics: []
- weight: 1
- queries: []
- EOF
- end
- end
-
- it_behaves_like 'required field', 'required metrics' do
- let(:field_nil) do
- <<-EOF.strip_heredoc
- - group: group_a
- priority: 1
- metrics:
- - title: title
- required_metrics:
- weight: 1
- queries: []
- EOF
- end
-
- let(:field_missing) do
- <<-EOF.strip_heredoc
- - group: group_a
- priority: 1
- metrics:
- - title: title
- weight: 1
- queries: []
- EOF
- end
- end
-
- it_behaves_like 'required field', 'weight' do
- let(:field_nil) do
- <<-EOF.strip_heredoc
- - group: group_a
- priority: 1
- metrics:
- - title: title
- required_metrics: []
- weight:
- queries: []
- EOF
- end
-
- let(:field_missing) do
- <<-EOF.strip_heredoc
- - group: group_a
- priority: 1
- metrics:
- - title: title
- required_metrics: []
- queries: []
- EOF
- end
- end
-
- it_behaves_like 'required field', :queries do
- let(:field_nil) do
- <<-EOF.strip_heredoc
- - group: group_a
- priority: 1
- metrics:
- - title: title
- required_metrics: []
- weight: 1
- queries:
- EOF
- end
-
- let(:field_missing) do
- <<-EOF.strip_heredoc
- - group: group_a
- priority: 1
- metrics:
- - title: title
- required_metrics: []
- weight: 1
- EOF
- end
- end
- end
- end
-end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index c6f3cfc7b8a..705f8f46a90 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -626,6 +626,21 @@ RSpec.describe Issuable, feature_category: :team_planning do
end
end
+ describe "#importing_or_transitioning?" do
+ let(:merge_request) { build(:merge_request, transitioning: transitioning, importing: importing) }
+
+ where(:transitioning, :importing, :result) do
+ true | false | true
+ false | true | true
+ true | true | true
+ false | false | false
+ end
+
+ with_them do
+ it { expect(merge_request.importing_or_transitioning?).to eq(result) }
+ end
+ end
+
describe '#labels_array' do
let(:project) { create(:project) }
let(:bug) { create(:label, project: project, title: 'bug') }
diff --git a/spec/models/concerns/transitionable_spec.rb b/spec/models/concerns/transitionable_spec.rb
new file mode 100644
index 00000000000..a1f011ff72e
--- /dev/null
+++ b/spec/models/concerns/transitionable_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Transitionable, feature_category: :code_review_workflow do
+ let(:klass) do
+ Class.new do
+ include Transitionable
+
+ def initialize(transitioning)
+ @transitioning = transitioning
+ end
+
+ def project
+ Project.new
+ end
+ end
+ end
+
+ let(:object) { klass.new(transitioning) }
+
+ describe 'For a class' do
+ using RSpec::Parameterized::TableSyntax
+
+ describe '#transitioning?' do
+ where(:transitioning, :feature_flag, :result) do
+ true | true | true
+ false | false | false
+ true | false | false
+ false | true | false
+ end
+
+ with_them do
+ before do
+ stub_feature_flags(skip_validations_during_transitions: feature_flag)
+ end
+
+ it { expect(object.transitioning?).to eq(result) }
+ end
+ end
+ end
+end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 3a71ec01b5c..2728c9ae72b 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -360,6 +360,23 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
end
end
+ describe "#validate_reviewer_size_length" do
+ let(:merge_request) { build(:merge_request, transitioning: transitioning) }
+
+ where(:transitioning, :to_or_not_to) do
+ false | :to
+ true | :not_to
+ end
+
+ with_them do
+ it do
+ expect(merge_request).send(to_or_not_to, receive(:validate_reviewer_size_length))
+
+ merge_request.valid?
+ end
+ end
+ end
+
describe '#validate_target_project' do
let(:merge_request) do
build(:merge_request, source_project: project, target_project: project, importing: importing)
@@ -386,6 +403,23 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
it { expect(merge_request.valid?(false)).to eq true }
end
end
+
+ context "with the skip_validations_during_transition_feature_flag" do
+ let(:merge_request) { build(:merge_request, transitioning: transitioning) }
+
+ where(:transitioning, :to_or_not_to) do
+ false | :to
+ true | :not_to
+ end
+
+ with_them do
+ it do
+ expect(merge_request).send(to_or_not_to, receive(:validate_target_project))
+
+ merge_request.valid?
+ end
+ end
+ end
end
end
@@ -4487,6 +4521,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
shared_examples 'for an invalid state transition' do
specify 'is not a valid state transition' do
expect { transition! }.to raise_error(StateMachines::InvalidTransition)
+ expect(subject.transitioning?).to be_falsey
end
end
@@ -4496,6 +4531,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
.to change { subject.merge_status }
.from(merge_status.to_s)
.to(expected_merge_status)
+ expect(subject.transitioning?).to be_falsey
end
end
diff --git a/spec/models/performance_monitoring/prometheus_metric_spec.rb b/spec/models/performance_monitoring/prometheus_metric_spec.rb
deleted file mode 100644
index 58bb59793cf..00000000000
--- a/spec/models/performance_monitoring/prometheus_metric_spec.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe PerformanceMonitoring::PrometheusMetric do
- let(:json_content) do
- {
- "id" => "metric_of_ages",
- "unit" => "count",
- "label" => "Metric of Ages",
- "query_range" => "http_requests_total"
- }
- end
-
- describe '.from_json' do
- subject { described_class.from_json(json_content) }
-
- it 'creates a PrometheusMetric object' do
- expect(subject).to be_a described_class
- expect(subject.id).to eq(json_content['id'])
- expect(subject.unit).to eq(json_content['unit'])
- expect(subject.label).to eq(json_content['label'])
- expect(subject.query_range).to eq(json_content['query_range'])
- end
-
- describe 'validations' do
- context 'json_content is not a hash' do
- let(:json_content) { nil }
-
- subject { described_class.from_json(json_content) }
-
- it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
- end
-
- context 'when unit is missing' do
- before do
- json_content['unit'] = nil
- end
-
- subject { described_class.from_json(json_content) }
-
- it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
- end
-
- context 'when query and query_range is missing' do
- before do
- json_content['query_range'] = nil
- end
-
- subject { described_class.from_json(json_content) }
-
- it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
- end
-
- context 'when query_range is missing but query is available' do
- before do
- json_content['query_range'] = nil
- json_content['query'] = 'http_requests_total'
- end
-
- subject { described_class.from_json(json_content) }
-
- it { is_expected.to be_valid }
- end
- end
- end
-end
diff --git a/spec/models/performance_monitoring/prometheus_panel_spec.rb b/spec/models/performance_monitoring/prometheus_panel_spec.rb
deleted file mode 100644
index 3dc05576826..00000000000
--- a/spec/models/performance_monitoring/prometheus_panel_spec.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe PerformanceMonitoring::PrometheusPanel do
- let(:json_content) do
- {
- "max_value" => 1,
- "type" => "area-chart",
- "title" => "Chart Title",
- "y_label" => "Y-Axis",
- "weight" => 1,
- "metrics" => [{
- "id" => "metric_of_ages",
- "unit" => "count",
- "label" => "Metric of Ages",
- "query_range" => "http_requests_total"
- }]
- }
- end
-
- describe '#new' do
- it 'accepts old schema format' do
- expect { described_class.new(json_content) }.not_to raise_error
- end
-
- it 'accepts new schema format' do
- expect { described_class.new(json_content.merge("y_axis" => { "precision" => 0 })) }.not_to raise_error
- end
- end
-
- describe '.from_json' do
- describe 'validations' do
- context 'json_content is not a hash' do
- let(:json_content) { nil }
-
- subject { described_class.from_json(json_content) }
-
- it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
- end
-
- context 'when title is missing' do
- before do
- json_content['title'] = nil
- end
-
- subject { described_class.from_json(json_content) }
-
- it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
- end
-
- context 'when metrics are missing' do
- before do
- json_content.delete('metrics')
- end
-
- subject { described_class.from_json(json_content) }
-
- it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
- end
- end
- end
-
- describe '.id' do
- it 'returns hexdigest of group_title, type and title as the panel id' do
- group_title = 'Business Group'
- panel_type = 'area-chart'
- panel_title = 'New feature requests made'
-
- expect(Digest::SHA2).to receive(:hexdigest).with("#{group_title}#{panel_type}#{panel_title}").and_return('hexdigest')
- expect(described_class.new(title: panel_title, type: panel_type).id(group_title)).to eql 'hexdigest'
- end
- end
-end
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb
index 756fcd8b7cd..ab2ebf134d7 100644
--- a/spec/requests/api/graphql/ci/jobs_spec.rb
+++ b/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -139,7 +139,10 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati
let(:pipeline) do
pipeline = create(:ci_pipeline, project: project, user: user)
stage = create(:ci_stage, project: project, pipeline: pipeline, name: 'first', position: 1)
- create(:ci_build, stage_id: stage.id, pipeline: pipeline, name: 'my test job', scheduling_type: :stage)
+ create(
+ :ci_build, pipeline: pipeline, name: 'my test job',
+ scheduling_type: :stage, stage_id: stage.id, stage_idx: stage.position
+ )
pipeline
end
@@ -180,10 +183,10 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati
previousStageJobsOrNeeds {
nodes {
... on CiBuildNeed {
- #{all_graphql_fields_for('CiBuildNeed')}
+ name
}
... on CiJob {
- #{all_graphql_fields_for('CiJob', excluded: %w[aiFailureAnalysis])}
+ name
}
}
}
@@ -211,10 +214,12 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati
before do
build_stage = create(:ci_stage, position: 2, name: 'build', project: project, pipeline: pipeline)
test_stage = create(:ci_stage, position: 3, name: 'test', project: project, pipeline: pipeline)
+ deploy_stage = create(:ci_stage, position: 4, name: 'deploy', project: project, pipeline: pipeline)
create(:ci_build, pipeline: pipeline, name: 'docker 1 2', scheduling_type: :stage, ci_stage: build_stage, stage_idx: build_stage.position)
create(:ci_build, pipeline: pipeline, name: 'docker 2 2', ci_stage: build_stage, stage_idx: build_stage.position, scheduling_type: :dag)
create(:ci_build, pipeline: pipeline, name: 'rspec 1 2', scheduling_type: :stage, ci_stage: test_stage, stage_idx: test_stage.position)
+ create(:ci_build, pipeline: pipeline, name: 'deploy', scheduling_type: :stage, ci_stage: deploy_stage, stage_idx: deploy_stage.position)
test_job = create(:ci_build, pipeline: pipeline, name: 'rspec 2 2', scheduling_type: :dag, ci_stage: test_stage, stage_idx: test_stage.position)
create(:ci_build_need, build: test_job, name: 'my test job')
@@ -255,6 +260,14 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati
'previousStageJobsOrNeeds' => { 'nodes' => [
a_hash_including('name' => 'my test job')
] }
+ ),
+ a_hash_including(
+ 'name' => 'deploy',
+ 'needs' => { 'nodes' => [] },
+ 'previousStageJobsOrNeeds' => { 'nodes' => [
+ a_hash_including('name' => 'rspec 1 2'),
+ a_hash_including('name' => 'rspec 2 2')
+ ] }
)
)
end
@@ -613,3 +626,87 @@ RSpec.describe 'Query.project.pipeline', feature_category: :continuous_integrati
end
end
end
+
+RSpec.describe 'previousStageJobs', feature_category: :pipeline_composition do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+
+ let(:query) do
+ <<~QUERY
+ {
+ project(fullPath: "#{project.full_path}") {
+ pipeline(iid: "#{pipeline.iid}") {
+ stages {
+ nodes {
+ groups {
+ nodes {
+ jobs {
+ nodes {
+ name
+ previousStageJobs {
+ nodes {
+ name
+ downstreamPipeline {
+ id
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ QUERY
+ end
+
+ it 'does not produce N+1 queries', :request_store, :use_sql_query_cache do
+ user1 = create(:user)
+ user2 = create(:user)
+
+ create_stage_with_build_and_bridge('build', 0)
+ create_stage_with_build_and_bridge('test', 1)
+
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ post_graphql(query, current_user: user1)
+ end
+
+ expect(graphql_data_previous_stage_jobs).to eq(
+ 'build_build' => [],
+ 'test_build' => %w[build_build]
+ )
+
+ create_stage_with_build_and_bridge('deploy', 2)
+
+ expect do
+ post_graphql(query, current_user: user2)
+ end.to issue_same_number_of_queries_as(control)
+
+ expect(graphql_data_previous_stage_jobs).to eq(
+ 'build_build' => [],
+ 'test_build' => %w[build_build],
+ 'deploy_build' => %w[test_build]
+ )
+ end
+
+ def create_stage_with_build_and_bridge(stage_name, stage_position)
+ stage = create(:ci_stage, position: stage_position, name: "#{stage_name}_stage", project: project, pipeline: pipeline)
+
+ create(:ci_build, pipeline: pipeline, name: "#{stage_name}_build", ci_stage: stage, stage_idx: stage.position)
+ end
+
+ def graphql_data_previous_stage_jobs
+ stages = graphql_data.dig('project', 'pipeline', 'stages', 'nodes')
+ groups = stages.flat_map { |stage| stage.dig('groups', 'nodes') }
+ jobs = groups.flat_map { |group| group.dig('jobs', 'nodes') }
+
+ jobs.each_with_object({}) do |job, previous_stage_jobs|
+ previous_stage_jobs[job['name']] = job.dig('previousStageJobs', 'nodes').pluck('name')
+ end
+ end
+end