diff options
Diffstat (limited to 'spec')
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 |