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-02-07 18:09:49 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-02-07 18:09:49 +0300
commit84f9f0cb8137637708a41152347e7754c1e9fb83 (patch)
tree6db9d8931bdb3c5b932b36345373936e2a543126 /spec
parent75f809a2ff829574ab91628407993187d55e14a4 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/config/settings_spec.rb28
-rw-r--r--spec/frontend/analytics/shared/components/metric_popover_spec.js66
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/app_spec.js56
-rw-r--r--spec/lib/gitlab/ci/build/auto_retry_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/components/instance_path_spec.rb116
-rw-r--r--spec/lib/gitlab/ci/config/entry/include_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/config/external/file/component_spec.rb179
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb22
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/external/processor_spec.rb38
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/metrics_spec.rb55
-rw-r--r--spec/models/ci/job_artifact_spec.rb8
-rw-r--r--spec/models/environment_spec.rb29
-rw-r--r--spec/services/ci/components/fetch_service_spec.rb141
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