diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-07 18:09:49 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-07 18:09:49 +0300 |
commit | 84f9f0cb8137637708a41152347e7754c1e9fb83 (patch) | |
tree | 6db9d8931bdb3c5b932b36345373936e2a543126 /spec | |
parent | 75f809a2ff829574ab91628407993187d55e14a4 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r-- | spec/config/settings_spec.rb | 28 | ||||
-rw-r--r-- | spec/frontend/analytics/shared/components/metric_popover_spec.js | 66 | ||||
-rw-r--r-- | spec/frontend/projects/settings/repository/branch_rules/app_spec.js | 56 | ||||
-rw-r--r-- | spec/lib/gitlab/ci/build/auto_retry_spec.rb | 8 | ||||
-rw-r--r-- | spec/lib/gitlab/ci/components/instance_path_spec.rb | 116 | ||||
-rw-r--r-- | spec/lib/gitlab/ci/config/entry/include_spec.rb | 6 | ||||
-rw-r--r-- | spec/lib/gitlab/ci/config/external/file/component_spec.rb | 179 | ||||
-rw-r--r-- | spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb | 22 | ||||
-rw-r--r-- | spec/lib/gitlab/ci/config/external/mapper_spec.rb | 4 | ||||
-rw-r--r-- | spec/lib/gitlab/ci/config/external/processor_spec.rb | 38 | ||||
-rw-r--r-- | spec/lib/gitlab/ci/pipeline/chain/metrics_spec.rb | 55 | ||||
-rw-r--r-- | spec/models/ci/job_artifact_spec.rb | 8 | ||||
-rw-r--r-- | spec/models/environment_spec.rb | 29 | ||||
-rw-r--r-- | spec/services/ci/components/fetch_service_spec.rb | 141 |
14 files changed, 705 insertions, 51 deletions
diff --git a/spec/config/settings_spec.rb b/spec/config/settings_spec.rb index 4917b043812..1aa1d885be3 100644 --- a/spec/config/settings_spec.rb +++ b/spec/config/settings_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Settings, feature_category: :authentication_and_authorization do + using RSpec::Parameterized::TableSyntax + describe 'omniauth' do it 'defaults to enabled' do expect(described_class.omniauth.enabled).to be true @@ -15,6 +17,32 @@ RSpec.describe Settings, feature_category: :authentication_and_authorization do end end + describe '.build_ci_component_fqdn' do + subject(:fqdn) { described_class.build_ci_component_fqdn } + + where(:host, :port, :relative_url, :result) do + 'acme.com' | 9090 | '/gitlab' | 'acme.com:9090/gitlab/' + 'acme.com' | 443 | '/gitlab' | 'acme.com/gitlab/' + 'acme.com' | 443 | '' | 'acme.com/' + 'acme.com' | 9090 | '' | 'acme.com:9090/' + 'test' | 9090 | '' | 'test:9090/' + end + + with_them do + before do + allow(Gitlab.config).to receive(:gitlab).and_return( + Settingslogic.new({ + 'host' => host, + 'https' => true, + 'port' => port, + 'relative_url_root' => relative_url + })) + end + + it { is_expected.to eq(result) } + end + end + describe '.attr_encrypted_db_key_base_truncated' do it 'is a string with maximum 32 bytes size' do expect(described_class.attr_encrypted_db_key_base_truncated.bytesize) diff --git a/spec/frontend/analytics/shared/components/metric_popover_spec.js b/spec/frontend/analytics/shared/components/metric_popover_spec.js index 6a58f8c6d29..e0bfff3e664 100644 --- a/spec/frontend/analytics/shared/components/metric_popover_spec.js +++ b/spec/frontend/analytics/shared/components/metric_popover_spec.js @@ -1,6 +1,7 @@ import { GlLink, GlIcon } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import MetricPopover from '~/analytics/shared/components/metric_popover.vue'; +import { METRIC_POPOVER_LABEL } from '~/analytics/shared/constants'; const MOCK_METRIC = { key: 'deployment-frequency', @@ -27,10 +28,11 @@ describe('MetricPopover', () => { }; const findMetricLabel = () => wrapper.findByTestId('metric-label'); - const findAllMetricLinks = () => wrapper.findAll('[data-testid="metric-link"]'); + const findMetricLink = () => wrapper.find('[data-testid="metric-link"]'); const findMetricDescription = () => wrapper.findByTestId('metric-description'); const findMetricDocsLink = () => wrapper.findByTestId('metric-docs-link'); const findMetricDocsLinkIcon = () => findMetricDocsLink().findComponent(GlIcon); + const findMetricDetailsIcon = () => findMetricLink().findComponent(GlIcon); afterEach(() => { wrapper.destroy(); @@ -47,17 +49,14 @@ describe('MetricPopover', () => { }); describe('with links', () => { + const METRIC_NAME = 'Deployment frequency'; + const LINK_URL = '/groups/gitlab-org/-/analytics/ci_cd?tab=deployment-frequency'; const links = [ { - name: 'Deployment frequency', - url: '/groups/gitlab-org/-/analytics/ci_cd?tab=deployment-frequency', + name: METRIC_NAME, + url: LINK_URL, label: 'Dashboard', }, - { - name: 'Another link', - url: '/groups/gitlab-org/-/analytics/another-link', - label: 'Another link', - }, ]; const docsLink = { name: 'Deployment frequency', @@ -68,37 +67,34 @@ describe('MetricPopover', () => { const linksWithDocs = [...links, docsLink]; describe.each` - hasDocsLink | allLinks | displayedMetricLinks - ${true} | ${linksWithDocs} | ${links} - ${false} | ${links} | ${links} - `( - 'when one link has docs_link=$hasDocsLink', - ({ hasDocsLink, allLinks, displayedMetricLinks }) => { - beforeEach(() => { - wrapper = createComponent({ metric: { ...MOCK_METRIC, links: allLinks } }); - }); + hasDocsLink | allLinks + ${true} | ${linksWithDocs} + ${false} | ${links} + `('when one link has docs_link=$hasDocsLink', ({ hasDocsLink, allLinks }) => { + beforeEach(() => { + wrapper = createComponent({ metric: { ...MOCK_METRIC, links: allLinks } }); + }); - displayedMetricLinks.forEach((link, idx) => { - it(`renders a link for "${link.name}"`, () => { - const allLinkContainers = findAllMetricLinks(); + describe('Metric title row', () => { + it(`renders a link for "${METRIC_NAME}"`, () => { + expect(findMetricLink().text()).toContain(METRIC_POPOVER_LABEL); + expect(findMetricLink().findComponent(GlLink).attributes('href')).toBe(LINK_URL); + }); - expect(allLinkContainers.at(idx).text()).toContain(link.name); - expect(allLinkContainers.at(idx).findComponent(GlLink).attributes('href')).toBe( - link.url, - ); - }); + it('renders the chart icon', () => { + expect(findMetricDetailsIcon().attributes('name')).toBe('chart'); }); + }); - it(`${hasDocsLink ? 'renders' : "doesn't render"} a docs link`, () => { - expect(findMetricDocsLink().exists()).toBe(hasDocsLink); + it(`${hasDocsLink ? 'renders' : "doesn't render"} a docs link`, () => { + expect(findMetricDocsLink().exists()).toBe(hasDocsLink); - if (hasDocsLink) { - expect(findMetricDocsLink().attributes('href')).toBe(docsLink.url); - expect(findMetricDocsLink().text()).toBe(docsLink.label); - expect(findMetricDocsLinkIcon().attributes('name')).toBe('external-link'); - } - }); - }, - ); + if (hasDocsLink) { + expect(findMetricDocsLink().attributes('href')).toBe(docsLink.url); + expect(findMetricDocsLink().text()).toBe(docsLink.label); + expect(findMetricDocsLinkIcon().attributes('name')).toBe('external-link'); + } + }); + }); }); }); diff --git a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js index 447d7e86ceb..56b39f04580 100644 --- a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js +++ b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js @@ -1,9 +1,10 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import { GlModal } from '@gitlab/ui'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { mountExtended } from 'helpers/vue_test_utils_helper'; -import BranchRules, { i18n } from '~/projects/settings/repository/branch_rules/app.vue'; +import BranchRules from '~/projects/settings/repository/branch_rules/app.vue'; import BranchRule from '~/projects/settings/repository/branch_rules/components/branch_rule.vue'; import branchRulesQuery from 'ee_else_ce/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql'; import { createAlert } from '~/flash'; @@ -11,8 +12,19 @@ import { branchRulesMockResponse, appProvideMock, } from 'ee_else_ce_jest/projects/settings/repository/branch_rules/mock_data'; +import { + I18N, + BRANCH_PROTECTION_MODAL_ID, + PROTECTED_BRANCHES_ANCHOR, +} from '~/projects/settings/repository/branch_rules/constants'; +import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component'; +import { expandSection } from '~/settings_panels'; +import { scrollToElement } from '~/lib/utils/common_utils'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; jest.mock('~/flash'); +jest.mock('~/settings_panels'); +jest.mock('~/lib/utils/common_utils'); Vue.use(VueApollo); @@ -28,6 +40,8 @@ describe('Branch rules app', () => { wrapper = mountExtended(BranchRules, { apolloProvider: fakeApollo, provide: appProvideMock, + stubs: { GlModal: stubComponent(GlModal, { template: RENDER_ALL_SLOTS_TEMPLATE }) }, + directives: { GlModal: createMockDirective() }, }); await waitForPromises(); @@ -35,17 +49,19 @@ describe('Branch rules app', () => { const findAllBranchRules = () => wrapper.findAllComponents(BranchRule); const findEmptyState = () => wrapper.findByTestId('empty'); + const findAddBranchRuleButton = () => wrapper.findByRole('button', I18N.addBranchRule); + const findModal = () => wrapper.findComponent(GlModal); beforeEach(() => createComponent()); it('displays an error if branch rules query fails', async () => { await createComponent({ queryHandler: jest.fn().mockRejectedValue() }); - expect(createAlert).toHaveBeenCalledWith({ message: i18n.queryError }); + expect(createAlert).toHaveBeenCalledWith({ message: I18N.queryError }); }); it('displays an empty state if no branch rules are present', async () => { await createComponent({ queryHandler: jest.fn().mockRejectedValue() }); - expect(findEmptyState().text()).toBe(i18n.emptyState); + expect(findEmptyState().text()).toBe(I18N.emptyState); }); it('renders branch rules', () => { @@ -61,4 +77,38 @@ describe('Branch rules app', () => { expect(findAllBranchRules().at(1).props('branchProtection')).toEqual(nodes[1].branchProtection); }); + + describe('Add branch rule', () => { + it('renders an Add branch rule button', () => { + expect(findAddBranchRuleButton().exists()).toBe(true); + }); + + it('renders a modal with correct props/attributes', () => { + expect(findModal().props()).toMatchObject({ + modalId: BRANCH_PROTECTION_MODAL_ID, + title: I18N.addBranchRule, + }); + + expect(findModal().attributes('ok-title')).toBe(I18N.createProtectedBranch); + }); + + it('renders correct modal id for the default action', () => { + const binding = getBinding(findAddBranchRuleButton().element, 'gl-modal'); + + expect(binding.value).toBe(BRANCH_PROTECTION_MODAL_ID); + }); + + it('renders the correct modal content', () => { + expect(findModal().text()).toContain(I18N.branchRuleModalDescription); + expect(findModal().text()).toContain(I18N.branchRuleModalContent); + }); + + it('when the primary modal action is clicked, takes user to the correct location', () => { + findAddBranchRuleButton().trigger('click'); + findModal().vm.$emit('ok'); + + expect(expandSection).toHaveBeenCalledWith(PROTECTED_BRANCHES_ANCHOR); + expect(scrollToElement).toHaveBeenCalledWith(PROTECTED_BRANCHES_ANCHOR); + }); + }); }); diff --git a/spec/lib/gitlab/ci/build/auto_retry_spec.rb b/spec/lib/gitlab/ci/build/auto_retry_spec.rb index d69b6679e30..314714c543b 100644 --- a/spec/lib/gitlab/ci/build/auto_retry_spec.rb +++ b/spec/lib/gitlab/ci/build/auto_retry_spec.rb @@ -112,5 +112,13 @@ RSpec.describe Gitlab::Ci::Build::AutoRetry, feature_category: :pipeline_authori expect(result).to eq ['always'] end end + + context 'with retry[:when] set to nil' do + let(:build) { create(:ci_build, options: { retry: { when: nil } }) } + + it 'returns always array' do + expect(result).to eq ['always'] + end + end end end diff --git a/spec/lib/gitlab/ci/components/instance_path_spec.rb b/spec/lib/gitlab/ci/components/instance_path_spec.rb new file mode 100644 index 00000000000..d9beae0555c --- /dev/null +++ b/spec/lib/gitlab/ci/components/instance_path_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline_authoring do + let_it_be(:user) { create(:user) } + + let(:path) { described_class.new(address: address, content_filename: 'template.yml') } + let(:settings) { Settingslogic.new({ 'component_fqdn' => current_host }) } + let(:current_host) { 'acme.com/' } + + before do + allow(::Settings).to receive(:gitlab_ci).and_return(settings) + end + + describe 'FQDN path' do + let_it_be(:existing_project) { create(:project, :repository) } + + let(:project_path) { existing_project.full_path } + let(:address) { "acme.com/#{project_path}/component@#{version}" } + let(:version) { 'master' } + + context 'when project exists' do + it 'provides the expected attributes', :aggregate_failures do + expect(path.project).to eq(existing_project) + expect(path.host).to eq(current_host) + expect(path.sha).to eq(existing_project.commit('master').id) + expect(path.project_file_path).to eq('component/template.yml') + end + + context 'when content exists' do + let(:content) { 'image: alpine' } + + before do + allow_next_instance_of(Repository) do |instance| + allow(instance) + .to receive(:blob_data_at) + .with(existing_project.commit('master').id, 'component/template.yml') + .and_return(content) + end + end + + context 'when user has permissions to read code' do + before do + existing_project.add_developer(user) + end + + it 'fetches the content' do + expect(path.fetch_content!(current_user: user)).to eq(content) + end + end + + context 'when user does not have permissions to download code' do + it 'raises an error when fetching the content' do + expect { path.fetch_content!(current_user: user) } + .to raise_error(Gitlab::Access::AccessDeniedError) + end + end + end + end + + context 'when project path is nested under a subgroup' do + let(:existing_group) { create(:group, :nested) } + let(:existing_project) { create(:project, :repository, group: existing_group) } + + it 'provides the expected attributes', :aggregate_failures do + expect(path.project).to eq(existing_project) + expect(path.host).to eq(current_host) + expect(path.sha).to eq(existing_project.commit('master').id) + expect(path.project_file_path).to eq('component/template.yml') + end + end + + context 'when current GitLab instance is installed on a relative URL' do + let(:address) { "acme.com/gitlab/#{project_path}/component@#{version}" } + let(:current_host) { 'acme.com/gitlab/' } + + it 'provides the expected attributes', :aggregate_failures do + expect(path.project).to eq(existing_project) + expect(path.host).to eq(current_host) + expect(path.sha).to eq(existing_project.commit('master').id) + expect(path.project_file_path).to eq('component/template.yml') + end + end + + context 'when version does not exist' do + let(:version) { 'non-existent' } + + it 'provides the expected attributes', :aggregate_failures do + expect(path.project).to eq(existing_project) + expect(path.host).to eq(current_host) + expect(path.sha).to be_nil + expect(path.project_file_path).to eq('component/template.yml') + end + + it 'returns nil when fetching the content' do + expect(path.fetch_content!(current_user: user)).to be_nil + end + end + + context 'when project does not exist' do + let(:project_path) { 'non-existent/project' } + + it 'provides the expected attributes', :aggregate_failures do + expect(path.project).to be_nil + expect(path.host).to eq(current_host) + expect(path.sha).to be_nil + expect(path.project_file_path).to be_nil + end + + it 'returns nil when fetching the content' do + expect(path.fetch_content!(current_user: user)).to be_nil + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/include_spec.rb b/spec/lib/gitlab/ci/config/entry/include_spec.rb index fd7f85c9298..5eecff5b592 100644 --- a/spec/lib/gitlab/ci/config/entry/include_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/include_spec.rb @@ -44,6 +44,12 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Include do it { is_expected.to be_valid } end + context 'when using "component"' do + let(:config) { { component: 'path/to/component@1.0' } } + + it { is_expected.to be_valid } + end + context 'when using "artifact"' do context 'and specifying "job"' do let(:config) { { artifact: 'test.yml', job: 'generator' } } diff --git a/spec/lib/gitlab/ci/config/external/file/component_spec.rb b/spec/lib/gitlab/ci/config/external/file/component_spec.rb new file mode 100644 index 00000000000..9863941c370 --- /dev/null +++ b/spec/lib/gitlab/ci/config/external/file/component_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: :pipeline_authoring do + let_it_be(:context_project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + let_it_be(:project_variables) { project.predefined_variables } + + let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) } + let(:external_resource) { described_class.new(params, context) } + let(:params) { { component: 'gitlab.com/acme/components/my-component@1.0' } } + let(:fetch_service) { instance_double(::Ci::Components::FetchService) } + let(:response) { ServiceResponse.error(message: 'some error message') } + + let(:context_params) do + { + project: context_project, + sha: '12345', + user: user, + variables: project_variables + } + end + + before do + allow(::Ci::Components::FetchService) + .to receive(:new) + .with( + address: params[:component], + current_user: context.user + ).and_return(fetch_service) + + allow(fetch_service).to receive(:execute).and_return(response) + end + + describe '#matching?' do + subject(:matching) { external_resource.matching? } + + context 'when component is specified' do + let(:params) { { component: 'some-value' } } + + it { is_expected.to be_truthy } + + context 'when feature flag ci_include_components is disabled' do + before do + stub_feature_flags(ci_include_components: false) + end + + it { is_expected.to be_falsey } + end + end + + context 'when component is not specified' do + let(:params) { { local: 'some-value' } } + + it { is_expected.to be_falsy } + end + end + + describe '#valid?' do + subject(:valid?) do + external_resource.validate! + external_resource.valid? + end + + context 'when the context project does not have a repository' do + before do + allow(context_project).to receive(:repository).and_return(nil) + end + + it 'is invalid' do + expect(subject).to be_falsy + expect(external_resource.error_message).to eq('Unable to use components outside of a project context') + end + end + + context 'when location is not provided' do + let(:params) { { component: 123 } } + + it 'is invalid' do + expect(subject).to be_falsy + expect(external_resource.error_message).to eq('Included file `123` needs to be a string') + end + end + + context 'when component path is provided' do + context 'when component is not found' do + let(:response) do + ServiceResponse.error(message: 'Content not found') + end + + it 'is invalid' do + expect(subject).to be_falsy + expect(external_resource.error_message).to eq('Content not found') + end + end + + context 'when component is found' do + let(:content) do + <<~COMPONENT + job: + script: echo + COMPONENT + end + + let(:response) do + ServiceResponse.success(payload: { + content: content, + path: instance_double(::Gitlab::Ci::Components::InstancePath, project: project, sha: '12345') + }) + end + + it 'is valid' do + expect(subject).to be_truthy + expect(external_resource.content).to eq(content) + end + + context 'when content is not a valid YAML' do + let(:content) { 'the-content' } + + it 'is invalid' do + expect(subject).to be_falsy + expect(external_resource.error_message).to match(/does not have valid YAML syntax/) + end + end + end + end + end + + describe '#metadata' do + subject(:metadata) { external_resource.metadata } + + let(:component_path) do + instance_double(::Gitlab::Ci::Components::InstancePath, + project: project, + sha: '12345', + project_file_path: 'my-component/template.yml') + end + + let(:response) do + ServiceResponse.success(payload: { path: component_path }) + end + + it 'returns the metadata' do + is_expected.to include( + context_project: context_project.full_path, + context_sha: context.sha, + type: :component, + location: 'gitlab.com/acme/components/my-component@1.0', + blob: a_string_ending_with("#{project.full_path}/-/blob/12345/my-component/template.yml"), + raw: nil, + extra: {} + ) + end + end + + describe '#expand_context' do + let(:component_path) do + instance_double(::Gitlab::Ci::Components::InstancePath, + project: project, + sha: '12345') + end + + let(:response) do + ServiceResponse.success(payload: { path: component_path }) + end + + subject { external_resource.send(:expand_context_attrs) } + + it 'inherits user and variables while changes project and sha' do + is_expected.to include( + project: project, + sha: '12345', + user: context.user, + variables: context.variables) + end + end +end diff --git a/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb index 5f321a696c9..11c79e19cff 100644 --- a/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb @@ -17,11 +17,14 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category: describe '#process' do let(:locations) do - [{ local: 'file.yml' }, - { file: 'file.yml', project: 'namespace/project' }, - { remote: 'https://example.com/.gitlab-ci.yml' }, - { template: 'file.yml' }, - { artifact: 'generated.yml', job: 'test' }] + [ + { local: 'file.yml' }, + { file: 'file.yml', project: 'namespace/project' }, + { component: 'gitlab.com/org/component@1.0' }, + { remote: 'https://example.com/.gitlab-ci.yml' }, + { template: 'file.yml' }, + { artifact: 'generated.yml', job: 'test' } + ] end subject(:process) { matcher.process(locations) } @@ -30,6 +33,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category: is_expected.to contain_exactly( an_instance_of(Gitlab::Ci::Config::External::File::Local), an_instance_of(Gitlab::Ci::Config::External::File::Project), + an_instance_of(Gitlab::Ci::Config::External::File::Component), an_instance_of(Gitlab::Ci::Config::External::File::Remote), an_instance_of(Gitlab::Ci::Config::External::File::Template), an_instance_of(Gitlab::Ci::Config::External::File::Artifact) @@ -42,8 +46,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category: it 'raises an error' do expect { process }.to raise_error( Gitlab::Ci::Config::External::Mapper::AmbigiousSpecificationError, - '`{"invalid":"file.yml"}` does not have a valid subkey for include. ' \ - 'Valid subkeys are: `local`, `project`, `remote`, `template`, `artifact`' + /`{"invalid":"file.yml"}` does not have a valid subkey for include. Valid subkeys are:/ ) end @@ -53,8 +56,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category: it 'raises an error with a masked sentence' do expect { process }.to raise_error( Gitlab::Ci::Config::External::Mapper::AmbigiousSpecificationError, - '`{"invalid":"xxxxxxxxxxxxxx.yml"}` does not have a valid subkey for include. ' \ - 'Valid subkeys are: `local`, `project`, `remote`, `template`, `artifact`' + /`{"invalid":"xxxxxxxxxxxxxx.yml"}` does not have a valid subkey for include. Valid subkeys are:/ ) end end @@ -66,7 +68,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category: it 'raises an error' do expect { process }.to raise_error( Gitlab::Ci::Config::External::Mapper::AmbigiousSpecificationError, - "Each include must use only one of: `local`, `project`, `remote`, `template`, `artifact`" + /Each include must use only one of:/ ) end end diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb index 344e9095fab..a053c3047de 100644 --- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb @@ -124,7 +124,7 @@ RSpec.shared_context 'gitlab_ci_config_external_mapper' do |ci_batch_request_for end it 'returns ambigious specification error' do - expect { subject }.to raise_error(described_class::AmbigiousSpecificationError, '`{"invalid":"secret-file.yml"}` does not have a valid subkey for include. Valid subkeys are: `local`, `project`, `remote`, `template`, `artifact`') + expect { subject }.to raise_error(described_class::AmbigiousSpecificationError, /`{"invalid":"secret-file.yml"}` does not have a valid subkey for include. Valid subkeys are:/) end end @@ -138,7 +138,7 @@ RSpec.shared_context 'gitlab_ci_config_external_mapper' do |ci_batch_request_for end it 'returns ambigious specification error' do - expect { subject }.to raise_error(described_class::AmbigiousSpecificationError, 'Each include must use only one of: `local`, `project`, `remote`, `template`, `artifact`') + expect { subject }.to raise_error(described_class::AmbigiousSpecificationError, /Each include must use only one of/) end end diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb index 311b433b7d2..bb65c2ef10c 100644 --- a/spec/lib/gitlab/ci/config/external/processor_spec.rb +++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb @@ -400,6 +400,44 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel end end + describe 'include:component' do + let(:values) do + { + include: { component: "#{Gitlab.config.gitlab.host}/#{another_project.full_path}/component-x@master" }, + image: 'image:1.0' + } + end + + let(:other_project_files) do + { + '/component-x/template.yml' => <<~YAML + component_x_job: + script: echo Component X + YAML + } + end + + before do + another_project.add_developer(user) + end + + it 'appends the file to the values' do + output = processor.perform + expect(output.keys).to match_array([:image, :component_x_job]) + end + + context 'when feature flag ci_include_components is disabled' do + before do + stub_feature_flags(ci_include_components: false) + end + + it 'returns an error' do + expect { processor.perform } + .to raise_error(described_class::IncludeError, /does not have a valid subkey for include./) + end + end + end + context 'when a valid project file is defined' do let(:values) do { diff --git a/spec/lib/gitlab/ci/pipeline/chain/metrics_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/metrics_spec.rb new file mode 100644 index 00000000000..b955d0e7cee --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/metrics_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Pipeline::Chain::Metrics, feature_category: :continuous_integration do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + let_it_be(:pipeline) do + create(:ci_pipeline, project: project, ref: 'master', user: user, name: 'Build pipeline') + end + + let(:command) do + Gitlab::Ci::Pipeline::Chain::Command.new( + project: project, + current_user: user, + origin_ref: 'master') + end + + let(:step) { described_class.new(pipeline, command) } + + subject(:run_chain) { step.perform! } + + it 'does not break the chain' do + run_chain + + expect(step.break?).to be false + end + + context 'with pipeline name' do + it 'creates snowplow event' do + run_chain + + expect_snowplow_event( + category: described_class.to_s, + action: 'create_pipeline_with_name', + project: pipeline.project, + user: pipeline.user, + namespace: pipeline.project.namespace + ) + end + end + + context 'without pipeline name' do + let_it_be(:pipeline) do + create(:ci_pipeline, project: project, ref: 'master', user: user) + end + + it 'does not create snowplow event' do + run_chain + + expect_no_snowplow_event + end + end +end diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index a1fd51f60ea..917c0d33183 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -27,6 +27,14 @@ RSpec.describe Ci::JobArtifact do subject { build(:ci_job_artifact, :archive, job: job, size: 107464) } end + describe 'after_destroy callback' do + it 'logs the job artifact destroy' do + expect(Gitlab::Ci::Artifacts::Logger).to receive(:log_deleted).with(artifact, :log_destroy) + + artifact.destroy! + end + end + describe '.not_expired' do it 'returns artifacts that have not expired' do _expired_artifact = create(:ci_job_artifact, :expired) diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 0d53ebdefe9..23de9a50e50 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -62,6 +62,33 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ expect(environment).not_to be_valid end end + + context 'tier' do + let!(:env) { build(:environment, tier: nil) } + + before do + # Disable `before_validation: :ensure_environment_tier` since it always set tier and interfere with tests. + # See: https://github.com/thoughtbot/shoulda/issues/178#issuecomment-1654014 + + allow_any_instance_of(described_class).to receive(:ensure_environment_tier).and_return(env) + end + + context 'presence is checked' do + it 'during create and update' do + expect(env).to validate_presence_of(:tier).on(:create) + expect(env).to validate_presence_of(:tier).on(:update) + end + end + + context 'when FF is disabled' do + before do + stub_feature_flags(validate_environment_tier_presence: false) + end + + it { expect(env).to validate_presence_of(:tier).on(:create) } + it { expect(env).not_to validate_presence_of(:tier).on(:update) } + end + end end describe 'preloading deployment associations' do @@ -145,7 +172,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ environment = create(:environment, name: 'gprd') environment.update_column(:tier, nil) - expect { environment.stop! }.to change { environment.reload.tier }.from(nil).to('production') + expect { environment.save! }.to change { environment.reload.tier }.from(nil).to('production') end it 'does not overwrite the existing environment tier' do diff --git a/spec/services/ci/components/fetch_service_spec.rb b/spec/services/ci/components/fetch_service_spec.rb new file mode 100644 index 00000000000..f2eaa8d31b4 --- /dev/null +++ b/spec/services/ci/components/fetch_service_spec.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::Components::FetchService, feature_category: :pipeline_authoring do + let_it_be(:project) { create(:project, :repository, create_tag: 'v1.0') } + let_it_be(:user) { create(:user) } + let_it_be(:current_user) { user } + let_it_be(:current_host) { Gitlab.config.gitlab.host } + + let(:service) do + described_class.new(address: address, current_user: current_user) + end + + before do + project.add_developer(user) + end + + describe '#execute', :aggregate_failures do + subject(:result) { service.execute } + + shared_examples 'an external component' do + shared_examples 'component address' do + context 'when content exists' do + let(:sha) { project.commit(version).id } + + let(:content) do + <<~COMPONENT + job: + script: echo + COMPONENT + end + + before do + stub_project_blob(sha, component_yaml_path, content) + end + + it 'returns the content' do + expect(result).to be_success + expect(result.payload[:content]).to eq(content) + end + end + + context 'when content does not exist' do + it 'returns an error' do + expect(result).to be_error + expect(result.reason).to eq(:content_not_found) + end + end + end + + context 'when user does not have permissions to read the code' do + let(:version) { 'master' } + let(:current_user) { create(:user) } + + it 'returns an error' do + expect(result).to be_error + expect(result.reason).to eq(:not_allowed) + end + end + + context 'when version is a branch name' do + it_behaves_like 'component address' do + let(:version) { project.default_branch } + end + end + + context 'when version is a tag name' do + it_behaves_like 'component address' do + let(:version) { project.repository.tags.first.name } + end + end + + context 'when version is a commit sha' do + it_behaves_like 'component address' do + let(:version) { project.repository.tags.first.id } + end + end + + context 'when version is not provided' do + let(:version) { nil } + + it 'returns an error' do + expect(result).to be_error + expect(result.reason).to eq(:content_not_found) + end + end + + context 'when project does not exist' do + let(:component_path) { 'unknown/component' } + let(:version) { '1.0' } + + it 'returns an error' do + expect(result).to be_error + expect(result.reason).to eq(:content_not_found) + end + end + + context 'when host is different than the current instance host' do + let(:current_host) { 'another-host.com' } + let(:version) { '1.0' } + + it 'returns an error' do + expect(result).to be_error + expect(result.reason).to eq(:unsupported_path) + end + end + end + + context 'when address points to an external component' do + let(:address) { "#{current_host}/#{component_path}@#{version}" } + + context 'when component path is the full path to a project' do + let(:component_path) { project.full_path } + let(:component_yaml_path) { 'template.yml' } + + it_behaves_like 'an external component' + end + + context 'when component path points to a directory in a project' do + let(:component_path) { "#{project.full_path}/my-component" } + let(:component_yaml_path) { 'my-component/template.yml' } + + it_behaves_like 'an external component' + end + + context 'when component path points to a nested directory in a project' do + let(:component_path) { "#{project.full_path}/my-dir/my-component" } + let(:component_yaml_path) { 'my-dir/my-component/template.yml' } + + it_behaves_like 'an external component' + end + end + end + + def stub_project_blob(ref, path, content) + allow_next_instance_of(Repository) do |instance| + allow(instance).to receive(:blob_data_at).with(ref, path).and_return(content) + end + end +end |