summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/features/projects/settings/operations_settings_spec.rb2
-rw-r--r--spec/finders/packages/go/package_finder_spec.rb71
-rw-r--r--spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js12
-rw-r--r--spec/frontend/error_tracking_settings/store/getters_spec.js4
-rw-r--r--spec/frontend/mr_notes/stores/actions_spec.js69
-rw-r--r--spec/frontend/mr_notes/stores/mutations_spec.js12
-rw-r--r--spec/lib/gitlab/object_hierarchy_spec.rb49
-rw-r--r--spec/lib/gitlab/sql/cte_spec.rb11
-rw-r--r--spec/lib/gitlab/sql/recursive_cte_spec.rb13
-rw-r--r--spec/migrations/20210226141517_dedup_issue_metrics_spec.rb66
-rw-r--r--spec/models/issue_spec.rb24
-rw-r--r--spec/models/packages/go/module_version_spec.rb14
-rw-r--r--spec/models/project_spec.rb2
-rw-r--r--spec/requests/api/deploy_keys_spec.rb31
-rw-r--r--spec/services/packages/go/create_package_service_spec.rb73
-rw-r--r--spec/services/packages/go/sync_packages_service_spec.rb40
-rw-r--r--spec/support/shared_contexts/requests/api/go_modules_shared_context.rb14
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb44
-rw-r--r--spec/workers/packages/go/sync_packages_worker_spec.rb101
19 files changed, 595 insertions, 57 deletions
diff --git a/spec/features/projects/settings/operations_settings_spec.rb b/spec/features/projects/settings/operations_settings_spec.rb
index fe0ee52e4fa..ca976997142 100644
--- a/spec/features/projects/settings/operations_settings_spec.rb
+++ b/spec/features/projects/settings/operations_settings_spec.rb
@@ -146,7 +146,7 @@ RSpec.describe 'Projects > Settings > For a forked project', :js do
click_button('Connect')
- assert_text('Connection has failed. Re-check Auth Token and try again.')
+ assert_text('Connection failed. Check Auth Token and try again.')
end
end
end
diff --git a/spec/finders/packages/go/package_finder_spec.rb b/spec/finders/packages/go/package_finder_spec.rb
new file mode 100644
index 00000000000..b6fad1e7061
--- /dev/null
+++ b/spec/finders/packages/go/package_finder_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Go::PackageFinder do
+ include_context 'basic Go module'
+
+ let_it_be(:mod) { create :go_module, project: project }
+ let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.1' }
+ let_it_be(:package) { create :golang_package, project: project, name: mod.name, version: 'v1.0.1' }
+
+ let(:finder) { described_class.new(project, mod_name, version_name) }
+
+ describe '#exists?' do
+ subject { finder.exists? }
+
+ context 'with a valid name and version' do
+ let(:mod_name) { mod.name }
+ let(:version_name) { version.name }
+
+ it 'executes SELECT 1' do
+ expect { subject }.to exceed_query_limit(0).for_query(/^SELECT 1/)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with an invalid name' do
+ let(:mod_name) { 'foo/bar' }
+ let(:version_name) { 'baz' }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with an invalid version' do
+ let(:mod_name) { mod.name }
+ let(:version_name) { 'baz' }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '#execute' do
+ subject { finder.execute }
+
+ context 'with a valid name and version' do
+ let(:mod_name) { mod.name }
+ let(:version_name) { version.name }
+
+ it 'executes a single query' do
+ expect { subject }.not_to exceed_query_limit(1)
+ end
+
+ it { is_expected.to eq(package) }
+ end
+
+ context 'with an invalid name' do
+ let(:mod_name) { 'foo/bar' }
+ let(:version_name) { 'baz' }
+
+ it { is_expected.to eq(nil) }
+ end
+
+ context 'with an invalid version' do
+ let(:mod_name) { mod.name }
+ let(:version_name) { 'baz' }
+
+ it { is_expected.to eq(nil) }
+ end
+ end
+end
diff --git a/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js b/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js
index 7ebaf0c3f2a..f02a261f323 100644
--- a/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js
+++ b/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js
@@ -44,13 +44,13 @@ describe('error tracking settings form', () => {
const pageText = wrapper.text();
expect(pageText).toContain(
- "If you self-host Sentry, enter the full URL of your Sentry instance. If you're using Sentry's hosted solution, enter https://sentry.io",
+ "If you self-host Sentry, enter your Sentry instance's full URL. If you use Sentry's hosted solution, enter https://sentry.io",
);
expect(pageText).toContain(
- "After adding your Auth Token, use the 'Connect' button to load projects",
+ 'After adding your Auth Token, select the Connect button to load projects.',
);
- expect(pageText).not.toContain('Connection has failed. Re-check Auth Token and try again');
+ expect(pageText).not.toContain('Connection failed. Check Auth Token and try again.');
expect(wrapper.findAll(GlFormInput).at(0).attributes('placeholder')).toContain(
'https://mysentryserver.com',
);
@@ -80,9 +80,7 @@ describe('error tracking settings form', () => {
});
it('does not show an error', () => {
- expect(wrapper.text()).not.toContain(
- 'Connection has failed. Re-check Auth Token and try again',
- );
+ expect(wrapper.text()).not.toContain('Connection failed. Check Auth Token and try again.');
});
});
@@ -96,7 +94,7 @@ describe('error tracking settings form', () => {
});
it('shows an error', () => {
- expect(wrapper.text()).toContain('Connection has failed. Re-check Auth Token and try again');
+ expect(wrapper.text()).toContain('Connection failed. Check Auth Token and try again.');
});
});
});
diff --git a/spec/frontend/error_tracking_settings/store/getters_spec.js b/spec/frontend/error_tracking_settings/store/getters_spec.js
index b135fdee40b..4bb8d38e294 100644
--- a/spec/frontend/error_tracking_settings/store/getters_spec.js
+++ b/spec/frontend/error_tracking_settings/store/getters_spec.js
@@ -78,7 +78,7 @@ describe('Error Tracking Settings - Getters', () => {
describe('projectSelectionLabel', () => {
it('should show the correct message when the token is empty', () => {
expect(getters.projectSelectionLabel(state)).toEqual(
- 'To enable project selection, enter a valid Auth Token',
+ 'To enable project selection, enter a valid Auth Token.',
);
});
@@ -86,7 +86,7 @@ describe('Error Tracking Settings - Getters', () => {
state.token = 'test-token';
expect(getters.projectSelectionLabel(state)).toEqual(
- "Click 'Connect' to re-establish the connection to Sentry and activate the dropdown.",
+ 'Click Connect to reestablish the connection to Sentry and activate the dropdown.',
);
});
});
diff --git a/spec/frontend/mr_notes/stores/actions_spec.js b/spec/frontend/mr_notes/stores/actions_spec.js
index dbceedface1..c6578453d85 100644
--- a/spec/frontend/mr_notes/stores/actions_spec.js
+++ b/spec/frontend/mr_notes/stores/actions_spec.js
@@ -1,5 +1,9 @@
+import MockAdapter from 'axios-mock-adapter';
+
import testAction from 'helpers/vuex_action_helper';
-import { setEndpoints } from '~/mr_notes/stores/actions';
+import axios from '~/lib/utils/axios_utils';
+
+import { setEndpoints, setMrMetadata, fetchMrMetadata } from '~/mr_notes/stores/actions';
import mutationTypes from '~/mr_notes/stores/mutation_types';
describe('MR Notes Mutator Actions', () => {
@@ -22,4 +26,67 @@ describe('MR Notes Mutator Actions', () => {
);
});
});
+
+ describe('setMrMetadata', () => {
+ it('should trigger the SET_MR_METADATA state mutation', async () => {
+ const mrMetadata = { propA: 'a', propB: 'b' };
+
+ await testAction(
+ setMrMetadata,
+ mrMetadata,
+ {},
+ [
+ {
+ type: mutationTypes.SET_MR_METADATA,
+ payload: mrMetadata,
+ },
+ ],
+ [],
+ );
+ });
+ });
+
+ describe('fetchMrMetadata', () => {
+ const mrMetadata = { meta: true, data: 'foo' };
+ const state = {
+ endpoints: {
+ metadata: 'metadata',
+ },
+ };
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ mock.onGet(state.endpoints.metadata).reply(200, mrMetadata);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('should fetch the data from the API', async () => {
+ await fetchMrMetadata({ state, dispatch: () => {} });
+
+ await axios.waitForAll();
+
+ expect(mock.history.get).toHaveLength(1);
+ expect(mock.history.get[0].url).toBe(state.endpoints.metadata);
+ });
+
+ it('should set the fetched data into state', () => {
+ return testAction(
+ fetchMrMetadata,
+ {},
+ state,
+ [],
+ [
+ {
+ type: 'setMrMetadata',
+ payload: mrMetadata,
+ },
+ ],
+ );
+ });
+ });
});
diff --git a/spec/frontend/mr_notes/stores/mutations_spec.js b/spec/frontend/mr_notes/stores/mutations_spec.js
index 422db3d5a38..35b8a2e4be2 100644
--- a/spec/frontend/mr_notes/stores/mutations_spec.js
+++ b/spec/frontend/mr_notes/stores/mutations_spec.js
@@ -12,4 +12,16 @@ describe('MR Notes Mutations', () => {
expect(state.endpoints).toEqual(endpoints);
});
});
+
+ describe(mutationTypes.SET_MR_METADATA, () => {
+ it('store the provided MR Metadata in the state', () => {
+ const state = {};
+ const metadata = { propA: 'A', propB: 'B' };
+
+ mutations[mutationTypes.SET_MR_METADATA](state, metadata);
+
+ expect(state.mrMetadata.propA).toBe('A');
+ expect(state.mrMetadata.propB).toBe('B');
+ });
+ });
});
diff --git a/spec/lib/gitlab/object_hierarchy_spec.rb b/spec/lib/gitlab/object_hierarchy_spec.rb
index 98a194ac73c..c48a7b8303f 100644
--- a/spec/lib/gitlab/object_hierarchy_spec.rb
+++ b/spec/lib/gitlab/object_hierarchy_spec.rb
@@ -3,14 +3,16 @@
require 'spec_helper'
RSpec.describe Gitlab::ObjectHierarchy do
- let!(:parent) { create(:group) }
- let!(:child1) { create(:group, parent: parent) }
- let!(:child2) { create(:group, parent: child1) }
+ let_it_be(:parent) { create(:group) }
+ let_it_be(:child1) { create(:group, parent: parent) }
+ let_it_be(:child2) { create(:group, parent: child1) }
+
+ let(:options) { {} }
shared_context 'Gitlab::ObjectHierarchy test cases' do
describe '#base_and_ancestors' do
let(:relation) do
- described_class.new(Group.where(id: child2.id)).base_and_ancestors
+ described_class.new(Group.where(id: child2.id), options: options).base_and_ancestors
end
it 'includes the base rows' do
@@ -22,13 +24,13 @@ RSpec.describe Gitlab::ObjectHierarchy do
end
it 'can find ancestors upto a certain level' do
- relation = described_class.new(Group.where(id: child2)).base_and_ancestors(upto: child1)
+ relation = described_class.new(Group.where(id: child2), options: options).base_and_ancestors(upto: child1)
expect(relation).to contain_exactly(child2)
end
it 'uses ancestors_base #initialize argument' do
- relation = described_class.new(Group.where(id: child2.id), Group.none).base_and_ancestors
+ relation = described_class.new(Group.where(id: child2.id), Group.none, options: options).base_and_ancestors
expect(relation).to include(parent, child1, child2)
end
@@ -40,7 +42,7 @@ RSpec.describe Gitlab::ObjectHierarchy do
describe 'hierarchy_order option' do
let(:relation) do
- described_class.new(Group.where(id: child2.id)).base_and_ancestors(hierarchy_order: hierarchy_order)
+ described_class.new(Group.where(id: child2.id), options: options).base_and_ancestors(hierarchy_order: hierarchy_order)
end
context ':asc' do
@@ -63,7 +65,7 @@ RSpec.describe Gitlab::ObjectHierarchy do
describe '#base_and_descendants' do
let(:relation) do
- described_class.new(Group.where(id: parent.id)).base_and_descendants
+ described_class.new(Group.where(id: parent.id), options: options).base_and_descendants
end
it 'includes the base rows' do
@@ -75,7 +77,7 @@ RSpec.describe Gitlab::ObjectHierarchy do
end
it 'uses descendants_base #initialize argument' do
- relation = described_class.new(Group.none, Group.where(id: parent.id)).base_and_descendants
+ relation = described_class.new(Group.none, Group.where(id: parent.id), options: options).base_and_descendants
expect(relation).to include(parent, child1, child2)
end
@@ -87,7 +89,7 @@ RSpec.describe Gitlab::ObjectHierarchy do
context 'when with_depth is true' do
let(:relation) do
- described_class.new(Group.where(id: parent.id)).base_and_descendants(with_depth: true)
+ described_class.new(Group.where(id: parent.id), options: options).base_and_descendants(with_depth: true)
end
it 'includes depth in the results' do
@@ -106,14 +108,14 @@ RSpec.describe Gitlab::ObjectHierarchy do
describe '#descendants' do
it 'includes only the descendants' do
- relation = described_class.new(Group.where(id: parent)).descendants
+ relation = described_class.new(Group.where(id: parent), options: options).descendants
expect(relation).to contain_exactly(child1, child2)
end
end
describe '#max_descendants_depth' do
- subject { described_class.new(base_relation).max_descendants_depth }
+ subject { described_class.new(base_relation, options: options).max_descendants_depth }
context 'when base relation is empty' do
let(:base_relation) { Group.where(id: nil) }
@@ -136,13 +138,13 @@ RSpec.describe Gitlab::ObjectHierarchy do
describe '#ancestors' do
it 'includes only the ancestors' do
- relation = described_class.new(Group.where(id: child2)).ancestors
+ relation = described_class.new(Group.where(id: child2), options: options).ancestors
expect(relation).to contain_exactly(child1, parent)
end
it 'can find ancestors upto a certain level' do
- relation = described_class.new(Group.where(id: child2)).ancestors(upto: child1)
+ relation = described_class.new(Group.where(id: child2), options: options).ancestors(upto: child1)
expect(relation).to be_empty
end
@@ -150,7 +152,7 @@ RSpec.describe Gitlab::ObjectHierarchy do
describe '#all_objects' do
let(:relation) do
- described_class.new(Group.where(id: child1.id)).all_objects
+ described_class.new(Group.where(id: child1.id), options: options).all_objects
end
it 'includes the base rows' do
@@ -166,13 +168,13 @@ RSpec.describe Gitlab::ObjectHierarchy do
end
it 'uses ancestors_base #initialize argument for ancestors' do
- relation = described_class.new(Group.where(id: child1.id), Group.where(id: non_existing_record_id)).all_objects
+ relation = described_class.new(Group.where(id: child1.id), Group.where(id: non_existing_record_id), options: options).all_objects
expect(relation).to include(parent)
end
it 'uses descendants_base #initialize argument for descendants' do
- relation = described_class.new(Group.where(id: non_existing_record_id), Group.where(id: child1.id)).all_objects
+ relation = described_class.new(Group.where(id: non_existing_record_id), Group.where(id: child1.id), options: options).all_objects
expect(relation).to include(child2)
end
@@ -210,6 +212,19 @@ RSpec.describe Gitlab::ObjectHierarchy do
expect(parent.self_and_descendants.to_sql).to include("DISTINCT")
expect(child2.self_and_ancestors.to_sql).to include("DISTINCT")
end
+
+ context 'when the skip_ordering option is set' do
+ let(:options) { { skip_ordering: true } }
+
+ it_behaves_like 'Gitlab::ObjectHierarchy test cases'
+
+ it 'does not include ROW_NUMBER()' do
+ query = described_class.new(Group.where(id: parent.id), options: options).base_and_descendants.to_sql
+
+ expect(query).to include("DISTINCT")
+ expect(query).not_to include("ROW_NUMBER()")
+ end
+ end
end
context 'when the use_distinct_in_object_hierarchy feature flag is disabled' do
diff --git a/spec/lib/gitlab/sql/cte_spec.rb b/spec/lib/gitlab/sql/cte_spec.rb
index fdc150cd4b9..33f382cde26 100644
--- a/spec/lib/gitlab/sql/cte_spec.rb
+++ b/spec/lib/gitlab/sql/cte_spec.rb
@@ -41,4 +41,15 @@ RSpec.describe Gitlab::SQL::CTE do
expect(relation.to_a).to eq(User.where(id: user.id).to_a)
end
end
+
+ it_behaves_like 'CTE with MATERIALIZED keyword examples' do
+ let(:expected_query_block_with_materialized) { 'WITH "some_cte" AS MATERIALIZED (' }
+ let(:expected_query_block_without_materialized) { 'WITH "some_cte" AS (' }
+
+ let(:query) do
+ cte = described_class.new(:some_cte, User.active, **options)
+
+ User.with(cte.to_arel).to_sql
+ end
+ end
end
diff --git a/spec/lib/gitlab/sql/recursive_cte_spec.rb b/spec/lib/gitlab/sql/recursive_cte_spec.rb
index 02611620989..edcacd404c2 100644
--- a/spec/lib/gitlab/sql/recursive_cte_spec.rb
+++ b/spec/lib/gitlab/sql/recursive_cte_spec.rb
@@ -57,4 +57,17 @@ RSpec.describe Gitlab::SQL::RecursiveCTE do
expect(relation.to_a).to eq(User.where(id: user.id).to_a)
end
end
+
+ it_behaves_like 'CTE with MATERIALIZED keyword examples' do
+ # MATERIALIZED keyword is not needed for recursive queries
+ let(:expected_query_block_with_materialized) { 'WITH RECURSIVE "some_cte" AS (' }
+ let(:expected_query_block_without_materialized) { 'WITH RECURSIVE "some_cte" AS (' }
+
+ let(:query) do
+ recursive_cte = described_class.new(:some_cte)
+ recursive_cte << User.active
+
+ User.with.recursive(recursive_cte.to_arel).to_sql
+ end
+ end
end
diff --git a/spec/migrations/20210226141517_dedup_issue_metrics_spec.rb b/spec/migrations/20210226141517_dedup_issue_metrics_spec.rb
new file mode 100644
index 00000000000..043884eb7b2
--- /dev/null
+++ b/spec/migrations/20210226141517_dedup_issue_metrics_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20210226141517_dedup_issue_metrics.rb')
+
+RSpec.describe DedupIssueMetrics, :migration, schema: 20210205104425 do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:issues) { table(:issues) }
+ let(:metrics) { table(:issue_metrics) }
+ let(:issue_params) { { title: 'title', project_id: project.id } }
+
+ let!(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
+ let!(:project) { projects.create!(namespace_id: namespace.id) }
+ let!(:issue_1) { issues.create!(issue_params) }
+ let!(:issue_2) { issues.create!(issue_params) }
+ let!(:issue_3) { issues.create!(issue_params) }
+
+ let!(:duplicated_metrics_1) { metrics.create!(issue_id: issue_1.id, first_mentioned_in_commit_at: 1.day.ago, first_added_to_board_at: 5.days.ago, updated_at: 2.months.ago) }
+ let!(:duplicated_metrics_2) { metrics.create!(issue_id: issue_1.id, first_mentioned_in_commit_at: Time.now, first_associated_with_milestone_at: Time.now, updated_at: 1.month.ago) }
+
+ let!(:duplicated_metrics_3) { metrics.create!(issue_id: issue_3.id, first_mentioned_in_commit_at: 1.day.ago, updated_at: 2.months.ago) }
+ let!(:duplicated_metrics_4) { metrics.create!(issue_id: issue_3.id, first_added_to_board_at: 1.day.ago, updated_at: 1.month.ago) }
+
+ let!(:non_duplicated_metrics) { metrics.create!(issue_id: issue_2.id, first_added_to_board_at: 2.days.ago) }
+
+ it 'deduplicates issue_metrics table' do
+ expect { migrate! }.to change { metrics.count }.from(5).to(3)
+ end
+
+ it 'merges `duplicated_metrics_1` with `duplicated_metrics_2`' do
+ migrate!
+
+ expect(metrics.where(id: duplicated_metrics_1.id)).not_to exist
+
+ merged_metrics = metrics.find_by(id: duplicated_metrics_2.id)
+
+ expect(merged_metrics).to be_present
+ expect(merged_metrics.first_mentioned_in_commit_at).to be_like_time(duplicated_metrics_2.first_mentioned_in_commit_at)
+ expect(merged_metrics.first_added_to_board_at).to be_like_time(duplicated_metrics_1.first_added_to_board_at)
+ end
+
+ it 'merges `duplicated_metrics_3` with `duplicated_metrics_4`' do
+ migrate!
+
+ expect(metrics.where(id: duplicated_metrics_3.id)).not_to exist
+
+ merged_metrics = metrics.find_by(id: duplicated_metrics_4.id)
+
+ expect(merged_metrics).to be_present
+ expect(merged_metrics.first_mentioned_in_commit_at).to be_like_time(duplicated_metrics_3.first_mentioned_in_commit_at)
+ expect(merged_metrics.first_added_to_board_at).to be_like_time(duplicated_metrics_4.first_added_to_board_at)
+ end
+
+ it 'does not change non duplicated records' do
+ expect { migrate! }.not_to change { non_duplicated_metrics.reload.attributes }
+ end
+
+ it 'does nothing when there are no metrics' do
+ metrics.delete_all
+
+ migrate!
+
+ expect(metrics.count).to eq(0)
+ end
+end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index a3e245f4def..ac98e7b047f 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -85,18 +85,14 @@ RSpec.describe Issue do
describe 'callbacks' do
describe '#ensure_metrics' do
it 'creates metrics after saving' do
- issue = create(:issue, project: reusable_project)
-
- expect(issue.metrics).to be_persisted
+ expect(subject.metrics).to be_persisted
expect(Issue::Metrics.count).to eq(1)
end
it 'does not create duplicate metrics for an issue' do
- issue = create(:issue, project: reusable_project)
+ subject.close!
- issue.close!
-
- expect(issue.metrics).to be_persisted
+ expect(subject.metrics).to be_persisted
expect(Issue::Metrics.count).to eq(1)
end
@@ -105,6 +101,20 @@ RSpec.describe Issue do
create(:issue, project: reusable_project)
end
+
+ context 'when metrics record is missing' do
+ before do
+ subject.metrics.delete
+ subject.reload
+ subject.metrics # make sure metrics association is cached (currently nil)
+ end
+
+ it 'creates the metrics record' do
+ subject.update!(title: 'title')
+
+ expect(subject.metrics).to be_present
+ end
+ end
end
describe '#record_create_action' do
diff --git a/spec/models/packages/go/module_version_spec.rb b/spec/models/packages/go/module_version_spec.rb
index c4c6a07d9e9..7fa416d8537 100644
--- a/spec/models/packages/go/module_version_spec.rb
+++ b/spec/models/packages/go/module_version_spec.rb
@@ -3,19 +3,9 @@
require 'spec_helper'
RSpec.describe Packages::Go::ModuleVersion, type: :model do
- let_it_be(:user) { create :user }
- let_it_be(:project) { create :project_empty_repo, creator: user, path: 'my-go-lib' }
- let_it_be(:mod) { create :go_module, project: project }
+ include_context 'basic Go module'
- before :all do
- create :go_module_commit, :files, project: project, tag: 'v1.0.0', files: { 'README.md' => 'Hi' }
- create :go_module_commit, :module, project: project, tag: 'v1.0.1'
- create :go_module_commit, :package, project: project, tag: 'v1.0.2', path: 'pkg'
- create :go_module_commit, :module, project: project, tag: 'v1.0.3', name: 'mod'
- create :go_module_commit, :files, project: project, files: { 'y.go' => "package a\n" }
- create :go_module_commit, :module, project: project, name: 'v2'
- create :go_module_commit, :files, project: project, tag: 'v2.0.0', files: { 'v2/x.go' => "package a\n" }
- end
+ let_it_be(:mod) { create :go_module, project: project }
shared_examples '#files' do |desc, *entries|
it "returns #{desc}" do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index b59f9b7fed1..e4295d2ab40 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -4109,7 +4109,7 @@ RSpec.describe Project, factory_default: :keep do
subject { described_class.wrap_with_cte(projects) }
it 'wrapped query matches original' do
- expect(subject.to_sql).to match(/^WITH "projects_cte" AS/)
+ expect(subject.to_sql).to match(/^WITH "projects_cte" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/)
expect(subject).to match_array(projects)
end
end
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index 591d994fec9..a01c66a311c 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -3,12 +3,13 @@
require 'spec_helper'
RSpec.describe API::DeployKeys do
- let(:user) { create(:user) }
- let(:maintainer) { create(:user) }
- let(:admin) { create(:admin) }
- let(:project) { create(:project, creator_id: user.id) }
- let(:project2) { create(:project, creator_id: user.id) }
- let(:deploy_key) { create(:deploy_key, public: true) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:project) { create(:project, creator_id: user.id) }
+ let_it_be(:project2) { create(:project, creator_id: user.id) }
+
+ let(:deploy_key) { create(:deploy_key, public: true) }
let!(:deploy_keys_project) do
create(:deploy_keys_project, project: project, deploy_key: deploy_key)
@@ -44,18 +45,30 @@ RSpec.describe API::DeployKeys do
end
describe 'GET /projects/:id/deploy_keys' do
- before do
- deploy_key
+ let(:deploy_key) { create(:deploy_key, public: true, user: admin) }
+
+ def perform_request
+ get api("/projects/#{project.id}/deploy_keys", admin)
end
it 'returns array of ssh keys' do
- get api("/projects/#{project.id}/deploy_keys", admin)
+ perform_request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(deploy_key.title)
end
+
+ it 'returns multiple deploy keys without N + 1' do
+ perform_request
+
+ control_count = ActiveRecord::QueryRecorder.new { perform_request }.count
+
+ create(:deploy_key, public: true, projects: [project], user: maintainer)
+
+ expect { perform_request }.not_to exceed_query_limit(control_count)
+ end
end
describe 'GET /projects/:id/deploy_keys/:key_id' do
diff --git a/spec/services/packages/go/create_package_service_spec.rb b/spec/services/packages/go/create_package_service_spec.rb
new file mode 100644
index 00000000000..5c5fec0aa3a
--- /dev/null
+++ b/spec/services/packages/go/create_package_service_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Go::CreatePackageService do
+ let_it_be(:project) { create :project_empty_repo, path: 'my-go-lib' }
+ let_it_be(:mod) { create :go_module, project: project }
+
+ before :all do
+ create :go_module_commit, :module, project: project, tag: 'v1.0.0'
+ end
+
+ shared_examples 'creates a package' do |files:|
+ it "returns a valid package with #{files ? files.to_s : 'no'} file(s)" do
+ expect(subject).to be_valid
+ expect(subject.name).to eq(version.mod.name)
+ expect(subject.version).to eq(version.name)
+ expect(subject.package_type).to eq('golang')
+ expect(subject.created_at).to eq(version.commit.committed_date)
+ expect(subject.package_files.count).to eq(files)
+ end
+ end
+
+ shared_examples 'creates a package file' do |type|
+ it "returns a package with a #{type} file" do
+ file_name = "#{version.name}.#{type}"
+ expect(subject.package_files.map { |f| f.file_name }).to include(file_name)
+
+ file = subject.package_files.with_file_name(file_name).first
+ expect(file).not_to be_nil
+ expect(file.file).not_to be_nil
+ expect(file.size).to eq(file.file.size)
+ expect(file.file_name).to eq(file_name)
+ expect(file.file_md5).not_to be_nil
+ expect(file.file_sha1).not_to be_nil
+ expect(file.file_sha256).not_to be_nil
+ end
+ end
+
+ describe '#execute' do
+ subject { described_class.new(project, nil, version: version).execute }
+
+ let(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.0' }
+
+ context 'with no existing package' do
+ it_behaves_like 'creates a package', files: 2
+ it_behaves_like 'creates a package file', :mod
+ it_behaves_like 'creates a package file', :zip
+
+ it 'creates a new package' do
+ expect { subject }
+ .to change { project.packages.count }.by(1)
+ .and change { Packages::PackageFile.count }.by(2)
+ end
+ end
+
+ context 'with an existing package' do
+ before do
+ described_class.new(project, version: version).execute
+ end
+
+ it_behaves_like 'creates a package', files: 2
+ it_behaves_like 'creates a package file', :mod
+ it_behaves_like 'creates a package file', :zip
+
+ it 'does not create a package or files' do
+ expect { subject }
+ .to not_change { project.packages.count }
+ .and not_change { Packages::PackageFile.count }
+ end
+ end
+ end
+end
diff --git a/spec/services/packages/go/sync_packages_service_spec.rb b/spec/services/packages/go/sync_packages_service_spec.rb
new file mode 100644
index 00000000000..565b0f252ce
--- /dev/null
+++ b/spec/services/packages/go/sync_packages_service_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Go::SyncPackagesService do
+ include_context 'basic Go module'
+
+ let(:params) { { info: true, mod: true, zip: true } }
+
+ describe '#execute_async' do
+ it 'schedules a package refresh' do
+ expect(::Packages::Go::SyncPackagesWorker).to receive(:perform_async).once
+
+ described_class.new(project, 'master').execute_async
+ end
+ end
+
+ describe '#initialize' do
+ context 'without a project' do
+ it 'raises an error' do
+ expect { described_class.new(nil, 'master') }
+ .to raise_error(ArgumentError, 'project is required')
+ end
+ end
+
+ context 'without a ref' do
+ it 'raises an error' do
+ expect { described_class.new(project, nil) }
+ .to raise_error(ArgumentError, 'ref is required')
+ end
+ end
+
+ context 'with an invalid ref' do
+ it 'raises an error' do
+ expect { described_class.new(project, 'not-a-ref') }
+ .to raise_error(ArgumentError)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_contexts/requests/api/go_modules_shared_context.rb b/spec/support/shared_contexts/requests/api/go_modules_shared_context.rb
new file mode 100644
index 00000000000..5a90c3076b1
--- /dev/null
+++ b/spec/support/shared_contexts/requests/api/go_modules_shared_context.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'basic Go module' do
+ let_it_be(:user) { create :user }
+ let_it_be(:project) { create :project_empty_repo, creator: user, path: 'my-go-lib' }
+
+ let_it_be(:commit_v1_0_0) { create :go_module_commit, :files, project: project, tag: 'v1.0.0', files: { 'README.md' => 'Hi' } }
+ let_it_be(:commit_v1_0_1) { create :go_module_commit, :module, project: project, tag: 'v1.0.1' }
+ let_it_be(:commit_v1_0_2) { create :go_module_commit, :package, project: project, tag: 'v1.0.2', path: 'pkg' }
+ let_it_be(:commit_v1_0_3) { create :go_module_commit, :module, project: project, tag: 'v1.0.3', name: 'mod' }
+ let_it_be(:commit_file_y) { create :go_module_commit, :files, project: project, files: { 'y.go' => "package a\n" } }
+ let_it_be(:commit_mod_v2) { create :go_module_commit, :module, project: project, name: 'v2' }
+ let_it_be(:commit_v2_0_0) { create :go_module_commit, :files, project: project, tag: 'v2.0.0', files: { 'v2/x.go' => "package a\n" } }
+end
diff --git a/spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb
new file mode 100644
index 00000000000..88e6ffd15a8
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'CTE with MATERIALIZED keyword examples' do
+ describe 'adding MATERIALIZE to the CTE' do
+ let(:options) { {} }
+
+ before do
+ # Clear the cached value before the test
+ Gitlab::Database::AsWithMaterialized.clear_memoization(:materialized_supported)
+ end
+
+ context 'when PG version is <12' do
+ it 'does not add MATERIALIZE keyword' do
+ allow(Gitlab::Database).to receive(:version).and_return('11.1')
+
+ expect(query).to include(expected_query_block_without_materialized)
+ end
+ end
+
+ context 'when PG version is >=12' do
+ it 'adds MATERIALIZE keyword' do
+ allow(Gitlab::Database).to receive(:version).and_return('12.1')
+
+ expect(query).to include(expected_query_block_with_materialized)
+ end
+
+ context 'when version is higher than 12' do
+ it 'adds MATERIALIZE keyword' do
+ allow(Gitlab::Database).to receive(:version).and_return('15.1')
+
+ expect(query).to include(expected_query_block_with_materialized)
+ end
+ end
+
+ context 'when materialized is disabled' do
+ let(:options) { { materialized: false } }
+
+ it 'does not add MATERIALIZE keyword' do
+ expect(query).to include(expected_query_block_without_materialized)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/workers/packages/go/sync_packages_worker_spec.rb b/spec/workers/packages/go/sync_packages_worker_spec.rb
new file mode 100644
index 00000000000..ad1a85b26e4
--- /dev/null
+++ b/spec/workers/packages/go/sync_packages_worker_spec.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Go::SyncPackagesWorker, type: :worker do
+ include_context 'basic Go module'
+
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ def perform(ref_name, path)
+ described_class.new.perform(project.id, ref_name, path)
+ end
+
+ def validate_package(package, mod, ver)
+ expect(package).not_to be_nil
+ expect(package.name).to eq(mod.name)
+ expect(package.version).to eq(ver.name)
+ expect(package.package_type).to eq('golang')
+ expect(package.created_at).to eq(ver.commit.committed_date)
+ expect(package.package_files.count).to eq(2)
+ end
+
+ shared_examples 'it creates a package' do |path, version, exists: false|
+ subject { perform(version, path) }
+
+ it "returns a package for example.com/project#{path.empty? ? '' : '/' + path}@#{version}" do
+ expect { subject }
+ .to change { project.packages.count }.by(exists ? 0 : 1)
+ .and change { Packages::PackageFile.count }.by(exists ? 0 : 2)
+
+ mod = create :go_module, project: project, path: path
+ ver = create :go_module_version, :tagged, mod: mod, name: version
+ validate_package(subject, mod, ver)
+ end
+ end
+
+ describe '#perform' do
+ context 'with no existing packages' do
+ it_behaves_like 'it creates a package', '', 'v1.0.1'
+ it_behaves_like 'it creates a package', '', 'v1.0.2'
+ it_behaves_like 'it creates a package', '', 'v1.0.3'
+ it_behaves_like 'it creates a package', 'mod', 'v1.0.3'
+ it_behaves_like 'it creates a package', 'v2', 'v2.0.0'
+ end
+
+ context 'with existing packages' do
+ before do
+ mod = create :go_module, project: project
+ ver = create :go_module_version, :tagged, mod: mod, name: 'v1.0.1'
+ Packages::Go::CreatePackageService.new(project, nil, version: ver).execute
+ end
+
+ it_behaves_like 'it creates a package', '', 'v1.0.1', exists: true
+ it_behaves_like 'it creates a package', '', 'v1.0.2'
+ it_behaves_like 'it creates a package', '', 'v1.0.3'
+ it_behaves_like 'it creates a package', 'mod', 'v1.0.3'
+ it_behaves_like 'it creates a package', 'v2', 'v2.0.0'
+ end
+
+ context 'with a package that exceeds project limits' do
+ before do
+ Plan.default.actual_limits.update!({ 'golang_max_file_size': 1 })
+ end
+
+ it 'logs an exception' do
+ expect(Gitlab::ErrorTracking).to receive(:log_exception).with(instance_of(::Packages::Go::CreatePackageService::GoZipSizeError))
+
+ perform('v2.0.0', 'v2')
+ end
+ end
+
+ where(:path, :version) do
+ [
+ ['', 'v1.0.1'],
+ ['', 'v1.0.2'],
+ ['', 'v1.0.3'],
+ ['mod', 'v1.0.3'],
+ ['v2', 'v2.0.0']
+ ]
+ end
+
+ with_them do
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { [project.id, version, path] }
+
+ it 'creates a package' do
+ expect { subject }
+ .to change { project.packages.count }.by(1)
+ .and change { Packages::PackageFile.count }.by(2)
+
+ mod = create :go_module, project: project, path: path
+ ver = create :go_module_version, :tagged, mod: mod, name: version
+ package = ::Packages::Go::PackageFinder.new(project, mod.name, ver.name).execute
+ validate_package(package, mod, ver)
+ end
+ end
+ end
+ end
+end