diff options
Diffstat (limited to 'spec')
12 files changed, 548 insertions, 4 deletions
diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb index c8a4ea799c3..1dbf9491118 100644 --- a/spec/finders/pipelines_finder_spec.rb +++ b/spec/finders/pipelines_finder_spec.rb @@ -64,6 +64,19 @@ describe PipelinesFinder do end end + context 'when project has child pipelines' do + let!(:parent_pipeline) { create(:ci_pipeline, project: project) } + let!(:child_pipeline) { create(:ci_pipeline, project: project, source: :parent_pipeline) } + + let!(:pipeline_source) do + create(:ci_sources_pipeline, pipeline: child_pipeline, source_pipeline: parent_pipeline) + end + + it 'filters out child pipelines and show only the parents' do + is_expected.to eq([parent_pipeline]) + end + end + HasStatus::AVAILABLE_STATUSES.each do |target| context "when status is #{target}" do let(:params) { { status: target } } diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js new file mode 100644 index 00000000000..1df583af711 --- /dev/null +++ b/spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js @@ -0,0 +1,143 @@ +import Vuex from 'vuex'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import GkeNetworkDropdown from '~/create_cluster/gke_cluster/components/gke_network_dropdown.vue'; +import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue'; +import createClusterDropdownState from '~/create_cluster/store/cluster_dropdown/state'; + +const localVue = createLocalVue(); + +localVue.use(Vuex); + +describe('GkeNetworkDropdown', () => { + let wrapper; + let store; + const defaultProps = { fieldName: 'field-name' }; + const selectedNetwork = { selfLink: '123456' }; + const projectId = '6789'; + const region = 'east-1'; + const setNetwork = jest.fn(); + const setSubnetwork = jest.fn(); + const fetchSubnetworks = jest.fn(); + + const buildStore = ({ clusterDropdownState } = {}) => + new Vuex.Store({ + state: { + selectedNetwork, + }, + actions: { + setNetwork, + setSubnetwork, + }, + getters: { + hasZone: () => false, + region: () => region, + projectId: () => projectId, + }, + modules: { + networks: { + namespaced: true, + state: { + ...createClusterDropdownState(), + ...(clusterDropdownState || {}), + }, + }, + subnetworks: { + namespaced: true, + actions: { + fetchItems: fetchSubnetworks, + }, + }, + }, + }); + + const buildWrapper = (propsData = defaultProps) => + shallowMount(GkeNetworkDropdown, { + propsData, + store, + localVue, + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('sets correct field-name', () => { + const fieldName = 'field-name'; + + store = buildStore(); + wrapper = buildWrapper({ fieldName }); + + expect(wrapper.find(ClusterFormDropdown).props('fieldName')).toBe(fieldName); + }); + + it('sets selected network as the dropdown value', () => { + store = buildStore(); + wrapper = buildWrapper(); + + expect(wrapper.find(ClusterFormDropdown).props('value')).toBe(selectedNetwork); + }); + + it('maps networks store items to the dropdown items property', () => { + const items = [{ name: 'network' }]; + + store = buildStore({ clusterDropdownState: { items } }); + wrapper = buildWrapper(); + + expect(wrapper.find(ClusterFormDropdown).props('items')).toBe(items); + }); + + describe('when network dropdown store is loading items', () => { + it('sets network dropdown as loading', () => { + store = buildStore({ clusterDropdownState: { isLoadingItems: true } }); + wrapper = buildWrapper(); + + expect(wrapper.find(ClusterFormDropdown).props('loading')).toBe(true); + }); + }); + + describe('when there is no selected zone', () => { + it('disables the network dropdown', () => { + store = buildStore(); + wrapper = buildWrapper(); + + expect(wrapper.find(ClusterFormDropdown).props('disabled')).toBe(true); + }); + }); + + describe('when an error occurs while loading networks', () => { + it('sets the network dropdown as having errors', () => { + store = buildStore({ clusterDropdownState: { loadingItemsError: new Error() } }); + wrapper = buildWrapper(); + + expect(wrapper.find(ClusterFormDropdown).props('hasErrors')).toBe(true); + }); + }); + + describe('when dropdown emits input event', () => { + beforeEach(() => { + store = buildStore(); + wrapper = buildWrapper(); + wrapper.find(ClusterFormDropdown).vm.$emit('input', selectedNetwork); + }); + + it('cleans selected subnetwork', () => { + expect(setSubnetwork).toHaveBeenCalledWith(expect.anything(), '', undefined); + }); + + it('dispatches the setNetwork action', () => { + expect(setNetwork).toHaveBeenCalledWith(expect.anything(), selectedNetwork, undefined); + }); + + it('fetches subnetworks for the selected project, region, and network', () => { + expect(fetchSubnetworks).toHaveBeenCalledWith( + expect.anything(), + { + project: projectId, + region, + network: selectedNetwork.selfLink, + }, + undefined, + ); + }); + }); +}); diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js new file mode 100644 index 00000000000..a1dc3960fe9 --- /dev/null +++ b/spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js @@ -0,0 +1,113 @@ +import Vuex from 'vuex'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import GkeSubnetworkDropdown from '~/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue'; +import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue'; +import createClusterDropdownState from '~/create_cluster/store/cluster_dropdown/state'; + +const localVue = createLocalVue(); + +localVue.use(Vuex); + +describe('GkeSubnetworkDropdown', () => { + let wrapper; + let store; + const defaultProps = { fieldName: 'field-name' }; + const selectedSubnetwork = '123456'; + const setSubnetwork = jest.fn(); + + const buildStore = ({ clusterDropdownState } = {}) => + new Vuex.Store({ + state: { + selectedSubnetwork, + }, + actions: { + setSubnetwork, + }, + getters: { + hasNetwork: () => false, + }, + modules: { + subnetworks: { + namespaced: true, + state: { + ...createClusterDropdownState(), + ...(clusterDropdownState || {}), + }, + }, + }, + }); + + const buildWrapper = (propsData = defaultProps) => + shallowMount(GkeSubnetworkDropdown, { + propsData, + store, + localVue, + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('sets correct field-name', () => { + const fieldName = 'field-name'; + + store = buildStore(); + wrapper = buildWrapper({ fieldName }); + + expect(wrapper.find(ClusterFormDropdown).props('fieldName')).toBe(fieldName); + }); + + it('sets selected subnetwork as the dropdown value', () => { + store = buildStore(); + wrapper = buildWrapper(); + + expect(wrapper.find(ClusterFormDropdown).props('value')).toBe(selectedSubnetwork); + }); + + it('maps subnetworks store items to the dropdown items property', () => { + const items = [{ name: 'subnetwork' }]; + + store = buildStore({ clusterDropdownState: { items } }); + wrapper = buildWrapper(); + + expect(wrapper.find(ClusterFormDropdown).props('items')).toBe(items); + }); + + describe('when subnetwork dropdown store is loading items', () => { + it('sets subnetwork dropdown as loading', () => { + store = buildStore({ clusterDropdownState: { isLoadingItems: true } }); + wrapper = buildWrapper(); + + expect(wrapper.find(ClusterFormDropdown).props('loading')).toBe(true); + }); + }); + + describe('when there is no selected network', () => { + it('disables the subnetwork dropdown', () => { + store = buildStore(); + wrapper = buildWrapper(); + + expect(wrapper.find(ClusterFormDropdown).props('disabled')).toBe(true); + }); + }); + + describe('when an error occurs while loading subnetworks', () => { + it('sets the subnetwork dropdown as having errors', () => { + store = buildStore({ clusterDropdownState: { loadingItemsError: new Error() } }); + wrapper = buildWrapper(); + + expect(wrapper.find(ClusterFormDropdown).props('hasErrors')).toBe(true); + }); + }); + + describe('when dropdown emits input event', () => { + it('dispatches the setSubnetwork action', () => { + store = buildStore(); + wrapper = buildWrapper(); + + wrapper.find(ClusterFormDropdown).vm.$emit('input', selectedSubnetwork); + + expect(setSubnetwork).toHaveBeenCalledWith(expect.anything(), selectedSubnetwork, undefined); + }); + }); +}); diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 27fa98bbde4..7bed1f72e0b 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -23,7 +23,7 @@ describe GitlabSchema.types['Project'] do only_allow_merge_if_all_discussions_are_resolved printing_merge_request_link_enabled namespace group statistics repository merge_requests merge_request issues issue pipelines removeSourceBranchAfterMerge sentryDetailedError snippets - grafanaIntegration autocloseReferencedIssues + grafanaIntegration autocloseReferencedIssues suggestion_commit_message ] is_expected.to include_graphql_fields(*expected_fields) diff --git a/spec/lib/gitlab/ci/build/policy/refs_spec.rb b/spec/lib/gitlab/ci/build/policy/refs_spec.rb index 8fc1e0a4e88..c32fdc5c72e 100644 --- a/spec/lib/gitlab/ci/build/policy/refs_spec.rb +++ b/spec/lib/gitlab/ci/build/policy/refs_spec.rb @@ -98,6 +98,34 @@ describe Gitlab::Ci::Build::Policy::Refs do .not_to be_satisfied_by(pipeline) end end + + context 'when source is pipeline' do + let(:pipeline) { build_stubbed(:ci_pipeline, source: :pipeline) } + + it 'is satisfied with only: pipelines' do + expect(described_class.new(%w[pipelines])) + .to be_satisfied_by(pipeline) + end + + it 'is satisfied with only: pipeline' do + expect(described_class.new(%w[pipeline])) + .to be_satisfied_by(pipeline) + end + end + + context 'when source is parent_pipeline' do + let(:pipeline) { build_stubbed(:ci_pipeline, source: :parent_pipeline) } + + it 'is satisfied with only: parent_pipelines' do + expect(described_class.new(%w[parent_pipelines])) + .to be_satisfied_by(pipeline) + end + + it 'is satisfied with only: parent_pipeline' do + expect(described_class.new(%w[parent_pipeline])) + .to be_satisfied_by(pipeline) + end + end end context 'when matching a ref by a regular expression' do diff --git a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb index aaea044595f..4c4359ad5d2 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb @@ -15,6 +15,42 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do stub_feature_flags(ci_root_config_content: false) end + context 'when bridge job is passed in as parameter' do + let(:ci_config_path) { nil } + let(:bridge) { create(:ci_bridge) } + + before do + command.bridge = bridge + end + + context 'when bridge job has downstream yaml' do + before do + allow(bridge).to receive(:yaml_for_downstream).and_return('the-yaml') + end + + it 'returns the content already available in command' do + subject.perform! + + expect(pipeline.config_source).to eq 'bridge_source' + expect(command.config_content).to eq 'the-yaml' + end + end + + context 'when bridge job does not have downstream yaml' do + before do + allow(bridge).to receive(:yaml_for_downstream).and_return(nil) + end + + it 'returns the next available source' do + subject.perform! + + expect(pipeline.config_source).to eq 'auto_devops_source' + template = Gitlab::Template::GitlabCiYmlTemplate.find('Beta/Auto-DevOps') + expect(command.config_content).to eq(template.content) + end + end + end + context 'when config is defined in a custom path in the repository' do let(:ci_config_path) { 'path/to/config.yml' } @@ -135,6 +171,23 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do end end + context 'when bridge job is passed in as parameter' do + let(:ci_config_path) { nil } + let(:bridge) { create(:ci_bridge) } + + before do + command.bridge = bridge + allow(bridge).to receive(:yaml_for_downstream).and_return('the-yaml') + end + + it 'returns the content already available in command' do + subject.perform! + + expect(pipeline.config_source).to eq 'bridge_source' + expect(command.config_content).to eq 'the-yaml' + end + end + context 'when config is defined in a custom path in the repository' do let(:ci_config_path) { 'path/to/config.yml' } let(:config_content_result) do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index a45ad514da2..07439880beb 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -201,6 +201,8 @@ ci_pipelines: - sourced_pipelines - triggered_by_pipeline - triggered_pipelines +- child_pipelines +- parent_pipeline - downstream_bridges - job_artifacts - vulnerabilities_occurrence_pipelines diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index adb49c8c7e7..e5014eeca09 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -535,6 +535,7 @@ Project: - merge_requests_disable_committers_approval - require_password_to_approve - autoclose_referenced_issues +- suggestion_commit_message ProjectTracingSetting: - external_url Author: diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index b30e88532e1..ce01765bb8c 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -2716,4 +2716,114 @@ describe Ci::Pipeline, :mailer do end end end + + describe '#parent_pipeline' do + let(:project) { create(:project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + + context 'when pipeline is triggered by a pipeline from the same project' do + let(:upstream_pipeline) { create(:ci_pipeline, project: pipeline.project) } + + before do + create(:ci_sources_pipeline, + source_pipeline: upstream_pipeline, + source_project: project, + pipeline: pipeline, + project: project) + end + + it 'returns the parent pipeline' do + expect(pipeline.parent_pipeline).to eq(upstream_pipeline) + end + + it 'is child' do + expect(pipeline).to be_child + end + end + + context 'when pipeline is triggered by a pipeline from another project' do + let(:upstream_pipeline) { create(:ci_pipeline) } + + before do + create(:ci_sources_pipeline, + source_pipeline: upstream_pipeline, + source_project: upstream_pipeline.project, + pipeline: pipeline, + project: project) + end + + it 'returns nil' do + expect(pipeline.parent_pipeline).to be_nil + end + + it 'is not child' do + expect(pipeline).not_to be_child + end + end + + context 'when pipeline is not triggered by a pipeline' do + it 'returns nil' do + expect(pipeline.parent_pipeline).to be_nil + end + + it 'is not child' do + expect(pipeline).not_to be_child + end + end + end + + describe '#child_pipelines' do + let(:project) { create(:project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + + context 'when pipeline triggered other pipelines on same project' do + let(:downstream_pipeline) { create(:ci_pipeline, project: pipeline.project) } + + before do + create(:ci_sources_pipeline, + source_pipeline: pipeline, + source_project: pipeline.project, + pipeline: downstream_pipeline, + project: pipeline.project) + end + + it 'returns the child pipelines' do + expect(pipeline.child_pipelines).to eq [downstream_pipeline] + end + + it 'is parent' do + expect(pipeline).to be_parent + end + end + + context 'when pipeline triggered other pipelines on another project' do + let(:downstream_pipeline) { create(:ci_pipeline) } + + before do + create(:ci_sources_pipeline, + source_pipeline: pipeline, + source_project: pipeline.project, + pipeline: downstream_pipeline, + project: downstream_pipeline.project) + end + + it 'returns empty array' do + expect(pipeline.child_pipelines).to be_empty + end + + it 'is not parent' do + expect(pipeline).not_to be_parent + end + end + + context 'when pipeline did not trigger any pipelines' do + it 'returns empty array' do + expect(pipeline.child_pipelines).to be_empty + end + + it 'is not parent' do + expect(pipeline).not_to be_parent + end + end + end end diff --git a/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb b/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb new file mode 100644 index 00000000000..33cd6e164b0 --- /dev/null +++ b/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Ci::CreatePipelineService do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:admin) } + let(:ref) { 'refs/heads/master' } + let(:service) { described_class.new(project, user, { ref: ref }) } + + context 'custom config content' do + let(:bridge) do + double(:bridge, yaml_for_downstream: <<~YML + rspec: + script: rspec + custom: + script: custom + YML + ) + end + + subject { service.execute(:push, bridge: bridge) } + + it 'creates a pipeline using the content passed in as param' do + expect(subject).to be_persisted + expect(subject.builds.map(&:name)).to eq %w[rspec custom] + expect(subject.config_source).to eq 'bridge_source' + end + end +end diff --git a/spec/services/suggestions/apply_service_spec.rb b/spec/services/suggestions/apply_service_spec.rb index bdbcb0fdb07..84529af7187 100644 --- a/spec/services/suggestions/apply_service_spec.rb +++ b/spec/services/suggestions/apply_service_spec.rb @@ -48,10 +48,34 @@ describe Suggestions::ApplyService do expect(commit.committer_email).to eq(user.commit_email) expect(commit.author_name).to eq(user.name) end + + context 'when a custom suggestion commit message' do + before do + project.update!(suggestion_commit_message: message) + + apply(suggestion) + end + + context 'is not specified' do + let(:message) { nil } + + it 'sets default commit message' do + expect(project.repository.commit.message).to eq("Apply suggestion to files/ruby/popen.rb") + end + end + + context 'is specified' do + let(:message) { 'refactor: %{project_path} %{project_name} %{file_path} %{branch_name} %{username} %{user_full_name}' } + + it 'sets custom commit message' do + expect(project.repository.commit.message).to eq("refactor: project-1 Project_1 files/ruby/popen.rb master test.user Test User") + end + end + end end - let(:project) { create(:project, :repository) } - let(:user) { create(:user, :commit_email) } + let(:project) { create(:project, :repository, path: 'project-1', name: 'Project_1') } + let(:user) { create(:user, :commit_email, name: 'Test User', username: 'test.user') } let(:position) { build_position } @@ -113,7 +137,8 @@ describe Suggestions::ApplyService do context 'non-fork project' do let(:merge_request) do create(:merge_request, source_project: project, - target_project: project) + target_project: project, + source_branch: 'master') end before do diff --git a/spec/views/projects/edit.html.haml_spec.rb b/spec/views/projects/edit.html.haml_spec.rb index 8005b549838..e95dec56a2d 100644 --- a/spec/views/projects/edit.html.haml_spec.rb +++ b/spec/views/projects/edit.html.haml_spec.rb @@ -28,6 +28,33 @@ describe 'projects/edit' do end end + context 'merge suggestions settings' do + it 'displays all possible variables' do + render + + expect(rendered).to have_content('%{project_path}') + expect(rendered).to have_content('%{project_name}') + expect(rendered).to have_content('%{file_path}') + expect(rendered).to have_content('%{branch_name}') + expect(rendered).to have_content('%{username}') + expect(rendered).to have_content('%{user_full_name}') + end + + it 'displays a placeholder if none is set' do + render + + expect(rendered).to have_field('project[suggestion_commit_message]', placeholder: 'Apply suggestion to %{file_path}') + end + + it 'displays the user entered value' do + project.update!(suggestion_commit_message: 'refactor: changed %{file_path}') + + render + + expect(rendered).to have_field('project[suggestion_commit_message]', with: 'refactor: changed %{file_path}') + end + end + context 'forking' do before do assign(:project, project) |