diff options
Diffstat (limited to 'spec/support/shared_examples')
49 files changed, 1393 insertions, 243 deletions
diff --git a/spec/support/shared_examples/ci/edit_job_token_scope_shared_examples.rb b/spec/support/shared_examples/ci/edit_job_token_scope_shared_examples.rb new file mode 100644 index 00000000000..05b2b5f5de1 --- /dev/null +++ b/spec/support/shared_examples/ci/edit_job_token_scope_shared_examples.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'editable job token scope' do + shared_examples 'returns error' do |error| + it 'returns an error response', :aggregate_failures do + expect(result).to be_error + expect(result.message).to eq(error) + end + end + + context 'when job token scope is disabled for the given project' do + before do + allow(project).to receive(:ci_job_token_scope_enabled?).and_return(false) + end + + it_behaves_like 'returns error', 'Job token scope is disabled for this project' + end + + context 'when user does not have permissions to edit the job token scope' do + it_behaves_like 'returns error', 'Insufficient permissions to modify the job token scope' + end + + context 'when user has permissions to edit the job token scope' do + before do + project.add_maintainer(current_user) + end + + context 'when target project is not provided' do + let(:target_project) { nil } + + it_behaves_like 'returns error', Ci::JobTokenScope::EditScopeValidations::TARGET_PROJECT_UNAUTHORIZED_OR_UNFOUND + end + + context 'when target project is provided' do + context 'when user does not have permissions to read the target project' do + it_behaves_like 'returns error', Ci::JobTokenScope::EditScopeValidations::TARGET_PROJECT_UNAUTHORIZED_OR_UNFOUND + end + end + end +end diff --git a/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb b/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb index 70a684c12bf..017e55309f7 100644 --- a/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb @@ -44,11 +44,13 @@ RSpec.shared_examples 'project access tokens available #create' do end it 'creates project access token' do + access_level = access_token_params[:access_level] || Gitlab::Access::MAINTAINER subject expect(created_token.name).to eq(access_token_params[:name]) expect(created_token.scopes).to eq(access_token_params[:scopes]) expect(created_token.expires_at).to eq(access_token_params[:expires_at]) + expect(project.project_member(created_token.user).access_level).to eq(access_level) end it 'creates project bot user' do diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb index 9af35c189d0..e8f7e62d0d7 100644 --- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true RSpec.shared_examples 'wiki controller actions' do + let_it_be(:user) { create(:user) } + let_it_be(:other_user) { create(:user) } + let(:container) { raise NotImplementedError } let(:routing_params) { raise NotImplementedError } - - let_it_be(:user) { create(:user) } let(:wiki) { Wiki.for_container(container, user) } let(:wiki_title) { 'page title test' } @@ -458,6 +459,7 @@ RSpec.shared_examples 'wiki controller actions' do describe 'DELETE #destroy' do let(:id_param) { wiki_title } + let(:delete_user) { user } subject(:request) do delete(:destroy, @@ -466,13 +468,21 @@ RSpec.shared_examples 'wiki controller actions' do )) end + before do + sign_in(delete_user) + end + context 'when page exists' do - it 'deletes the page' do - expect do - request - end.to change { wiki.list_pages.size }.by(-1) + shared_examples 'deletes the page' do + specify do + expect do + request + end.to change { wiki.list_pages.size }.by(-1) + end end + it_behaves_like 'deletes the page' + context 'but page cannot be deleted' do before do allow_next_instance_of(WikiPage) do |page| @@ -489,6 +499,28 @@ RSpec.shared_examples 'wiki controller actions' do expect(assigns(:error)).to eq('Could not delete wiki page') end end + + context 'when user is a developer' do + let(:delete_user) { other_user } + + before do + container.add_developer(other_user) + end + + it_behaves_like 'deletes the page' + end + + context 'when user is a reporter' do + let(:delete_user) { other_user } + + before do + container.add_reporter(other_user) + end + + it 'returns 404' do + is_expected.to have_gitlab_http_status(:not_found) + end + end end context 'when page does not exist' do diff --git a/spec/support/shared_examples/features/cascading_settings_shared_examples.rb b/spec/support/shared_examples/features/cascading_settings_shared_examples.rb index 29ef3da9a85..395f4fc54e0 100644 --- a/spec/support/shared_examples/features/cascading_settings_shared_examples.rb +++ b/spec/support/shared_examples/features/cascading_settings_shared_examples.rb @@ -13,10 +13,22 @@ RSpec.shared_examples 'a cascading setting' do click_save_button end - it 'disables setting in subgroups' do - visit subgroup_path + shared_examples 'subgroup settings are disabled' do + it 'disables setting in subgroups' do + visit subgroup_path + + expect(find("#{setting_field_selector}[disabled]")).to be_checked + end + end + + include_examples 'subgroup settings are disabled' + + context 'when use_traversal_ids_for_ancestors is disabled' do + before do + stub_feature_flags(use_traversal_ids_for_ancestors: false) + end - expect(find("#{setting_field_selector}[disabled]")).to be_checked + include_examples 'subgroup settings are disabled' end it 'does not show enforcement checkbox in subgroups' do diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb index 4d2e13aa5bc..9e88db2e1c0 100644 --- a/spec/support/shared_examples/features/packages_shared_examples.rb +++ b/spec/support/shared_examples/features/packages_shared_examples.rb @@ -32,11 +32,9 @@ RSpec.shared_examples 'package details link' do |property| expect(page).to have_current_path(project_package_path(package.project, package)) - page.within('[data-qa-selector="package_title"]') do - expect(page).to have_content(package.name) - end + expect(page).to have_css('.packages-app h1[data-testid="title"]', text: package.name) - page.within('[data-qa-selector="package_information_content"]') do + page.within(%Q([name="#{package.name}"])) do expect(page).to have_content('Installation') expect(page).to have_content('Registry setup') end diff --git a/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb b/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb new file mode 100644 index 00000000000..bb5460e2a6f --- /dev/null +++ b/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'search timeouts' do |scope| + context 'when search times out' do + before do + allow_next_instance_of(SearchService) do |service| + allow(service).to receive(:search_objects).and_raise(ActiveRecord::QueryCanceled) + end + + visit(search_path(search: 'test', scope: scope)) + end + + it 'renders timeout information' do + expect(page).to have_content('Your search timed out') + end + + it 'sets tab count to 0' do + expect(page.find('.search-filter .active')).to have_text('0') + end + end +end diff --git a/spec/support/shared_examples/features/sidebar_shared_examples.rb b/spec/support/shared_examples/features/sidebar_shared_examples.rb index c9508818f74..5bfe929e957 100644 --- a/spec/support/shared_examples/features/sidebar_shared_examples.rb +++ b/spec/support/shared_examples/features/sidebar_shared_examples.rb @@ -175,12 +175,4 @@ RSpec.shared_examples 'issue boards sidebar' do end end end - - def refresh_and_click_first_card - page.refresh - - wait_for_requests - - first_card.click - end end diff --git a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb index f2576931642..dfc9a45bd0d 100644 --- a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb @@ -20,17 +20,6 @@ RSpec.shared_examples 'User creates wiki page' do click_link "Create your first page" end - it "shows validation error message if the form is force submitted", :js do - page.within(".wiki-form") do - fill_in(:wiki_content, with: "") - - page.execute_script("document.querySelector('.wiki-form').submit()") - page.accept_alert # manually force form submit - end - - expect(page).to have_content("The form contains the following error:").and have_content("Content can't be blank") - end - it "disables the submit button", :js do page.within(".wiki-form") do fill_in(:wiki_content, with: "") diff --git a/spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb index ee0261771f9..55c89977a99 100644 --- a/spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb @@ -7,18 +7,34 @@ RSpec.shared_examples 'User deletes wiki page' do include WikiHelpers + let_it_be(:developer) { create(:user) } + let(:wiki_page) { create(:wiki_page, wiki: wiki) } before do + wiki.container.add_developer(developer) + sign_in(user) visit wiki_page_path(wiki, wiki_page) end - it 'deletes a page', :js do - click_on('Edit') - click_on('Delete') - find('[data-testid="confirm_deletion_button"]').click + shared_examples 'deletes a wiki page' do + specify 'deletes a page', :js do + click_on('Edit') + click_on('Delete') + find('[data-testid="confirm_deletion_button"]').click + + expect(page).to have_content('Wiki page was successfully deleted.') + end + end + + context 'when user is the owner or maintainer' do + it_behaves_like 'deletes a wiki page' + end + + context 'when user is a developer' do + let(:user) { developer } - expect(page).to have_content('Wiki page was successfully deleted.') + it_behaves_like 'deletes a wiki page' end end diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb index db2a96d9649..9587da0233e 100644 --- a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb @@ -90,19 +90,6 @@ RSpec.shared_examples 'User updates wiki page' do expect(page).to have_field('wiki[message]', with: 'Update Wiki title') end - it 'shows a validation error message if the form is force submitted', :js do - fill_in(:wiki_content, with: '') - - page.execute_script("document.querySelector('.wiki-form').submit()") - page.accept_alert # manually force form submit - - expect(page).to have_selector('.wiki-form') - expect(page).to have_content('Edit Page') - expect(page).to have_content('The form contains the following error:') - expect(page).to have_content("Content can't be blank") - expect(find('textarea#wiki_content').value).to eq('') - end - it "disables the submit button", :js do page.within(".wiki-form") do fill_in(:wiki_content, with: "") diff --git a/spec/support/shared_examples/graphql/design_fields_shared_examples.rb b/spec/support/shared_examples/graphql/design_fields_shared_examples.rb index 9c2eb3e5a5c..efbcfaf0e91 100644 --- a/spec/support/shared_examples/graphql/design_fields_shared_examples.rb +++ b/spec/support/shared_examples/graphql/design_fields_shared_examples.rb @@ -27,6 +27,7 @@ RSpec.shared_examples 'a GraphQL type with design fields' do describe '#image' do let_it_be(:current_user) { create(:user) } + let(:schema) { GitlabSchema } let(:query) { GraphQL::Query.new(schema) } let(:context) { query.context } diff --git a/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb b/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb index 5e15c91cd41..011a2157f24 100644 --- a/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb +++ b/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb @@ -3,17 +3,13 @@ require 'spec_helper' RSpec.shared_examples 'a mutation which can mutate a spammable' do - describe "#additional_spam_params" do - it 'passes additional spam params to the service' do + describe "#spam_params" do + it 'passes spam params to the service constructor' do args = [ project: anything, current_user: anything, - params: hash_including( - api: true, - request: instance_of(ActionDispatch::Request), - captcha_response: captcha_response, - spam_log_id: spam_log_id - ) + params: anything, + spam_params: instance_of(::Spam::SpamParams) ] expect(service).to receive(:new).with(*args).and_call_original diff --git a/spec/support/shared_examples/graphql/spam_protection_shared_examples.rb b/spec/support/shared_examples/graphql/spam_protection_shared_examples.rb index 8fb89a4f80e..c0b71a494d0 100644 --- a/spec/support/shared_examples/graphql/spam_protection_shared_examples.rb +++ b/spec/support/shared_examples/graphql/spam_protection_shared_examples.rb @@ -57,7 +57,7 @@ RSpec.shared_examples 'has spam protection' do context 'and no CAPTCHA is required' do let(:render_captcha) { false } - it 'does not return a to-level error' do + it 'does not return a top-level error' do send_request expect(graphql_errors).to be_blank diff --git a/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb b/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb new file mode 100644 index 00000000000..845fa78a827 --- /dev/null +++ b/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'object cache helper' do + it { is_expected.to be_a(Gitlab::Json::PrecompiledJson) } + + it "uses the presenter" do + expect(presenter).to receive(:represent).with(presentable, project: project) + + subject + end + + it "is valid JSON" do + parsed = Gitlab::Json.parse(subject.to_s) + + expect(parsed).to be_a(Hash) + expect(parsed["id"]).to eq(presentable.id) + end + + it "fetches from the cache" do + expect(instance.cache).to receive(:fetch).with("#{presenter.class.name}:#{presentable.cache_key}:#{user.cache_key}", expires_in: described_class::DEFAULT_EXPIRY).once + + subject + end + + context "when a cache context is supplied" do + before do + kwargs[:cache_context] = -> (item) { item.project.cache_key } + end + + it "uses the context to augment the cache key" do + expect(instance.cache).to receive(:fetch).with("#{presenter.class.name}:#{presentable.cache_key}:#{project.cache_key}", expires_in: described_class::DEFAULT_EXPIRY).once + + subject + end + end + + context "when expires_in is supplied" do + it "sets the expiry when accessing the cache" do + kwargs[:expires_in] = 7.days + + expect(instance.cache).to receive(:fetch).with("#{presenter.class.name}:#{presentable.cache_key}:#{user.cache_key}", expires_in: 7.days).once + + subject + end + end +end + +RSpec.shared_examples_for 'collection cache helper' do + it { is_expected.to be_an(Gitlab::Json::PrecompiledJson) } + + it "uses the presenter" do + presentable.each do |item| + expect(presenter).to receive(:represent).with(item, project: project) + end + + subject + end + + it "is valid JSON" do + parsed = Gitlab::Json.parse(subject.to_s) + + expect(parsed).to be_an(Array) + + presentable.each_with_index do |item, i| + expect(parsed[i]["id"]).to eq(item.id) + end + end + + it "fetches from the cache" do + keys = presentable.map { |item| "#{presenter.class.name}:#{item.cache_key}:#{user.cache_key}" } + + expect(instance.cache).to receive(:fetch_multi).with(*keys, expires_in: described_class::DEFAULT_EXPIRY).once.and_call_original + + subject + end + + context "when a cache context is supplied" do + before do + kwargs[:cache_context] = -> (item) { item.project.cache_key } + end + + it "uses the context to augment the cache key" do + keys = presentable.map { |item| "#{presenter.class.name}:#{item.cache_key}:#{project.cache_key}" } + + expect(instance.cache).to receive(:fetch_multi).with(*keys, expires_in: described_class::DEFAULT_EXPIRY).once.and_call_original + + subject + end + end + + context "expires_in is supplied" do + it "sets the expiry when accessing the cache" do + keys = presentable.map { |item| "#{presenter.class.name}:#{item.cache_key}:#{user.cache_key}" } + kwargs[:expires_in] = 7.days + + expect(instance.cache).to receive(:fetch_multi).with(*keys, expires_in: 7.days).once.and_call_original + + subject + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb index 7d341d79bae..6e12b5a0e85 100644 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb @@ -3,6 +3,7 @@ RSpec.shared_examples_for 'value stream analytics event' do let(:params) { {} } let(:instance) { described_class.new(params) } + let(:expected_hash_code) { Digest::SHA256.hexdigest(instance.class.identifier.to_s) } it { expect(described_class.name).to be_a_kind_of(String) } it { expect(described_class.identifier).to be_a_kind_of(Symbol) } @@ -19,4 +20,16 @@ RSpec.shared_examples_for 'value stream analytics event' do expect(output_query).to be_a_kind_of(ActiveRecord::Relation) end end + + describe '#hash_code' do + it 'returns a hash that uniquely identifies an event' do + expect(instance.hash_code).to eq(expected_hash_code) + end + + it 'does not differ when the same object is built with the same params' do + another_instance_with_same_params = described_class.new(params) + + expect(another_instance_with_same_params.hash_code).to eq(instance.hash_code) + end + end end diff --git a/spec/support/shared_examples/lib/gitlab/import_export/relation_factory_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import_export/relation_factory_shared_examples.rb index 33061f17bde..3c5c65f0690 100644 --- a/spec/support/shared_examples/lib/gitlab/import_export/relation_factory_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/import_export/relation_factory_shared_examples.rb @@ -12,7 +12,7 @@ RSpec.shared_examples 'Notes user references' do 'id' => 111, 'access_level' => 30, 'source_id' => 1, - 'source_type' => importable.class.name == 'Project' ? 'Project' : 'Namespace', + 'source_type' => importable.instance_of?(Project) ? 'Project' : 'Namespace', 'user_id' => 3, 'notification_level' => 3, 'created_at' => '2016-11-18T09:29:42.634Z', diff --git a/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb index f018ece0d46..2633a89eeee 100644 --- a/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb @@ -19,7 +19,8 @@ RSpec.shared_examples 'network policy common specs' do creation_timestamp: nil, manifest: YAML.dump(policy.resource.deep_stringify_keys), is_autodevops: false, - is_enabled: true + is_enabled: true, + environment_ids: [] } end diff --git a/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb index eafb49cef71..e4f09dfa0b0 100644 --- a/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb @@ -33,3 +33,21 @@ RSpec.shared_examples 'search results sorted' do end end end + +RSpec.shared_examples 'search results sorted by popularity' do + context 'sort: popularity_desc' do + let(:sort) { 'popularity_desc' } + + it 'sorts results by upvotes' do + expect(results_popular.objects(scope).map(&:id)).to eq([popular_result.id, less_popular_result.id, non_popular_result.id]) + end + end + + context 'sort: popularity_asc' do + let(:sort) { 'popularity_asc' } + + it 'sorts results by created_at' do + expect(results_popular.objects(scope).map(&:id)).to eq([non_popular_result.id, less_popular_result.id, popular_result.id]) + end + end +end diff --git a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb index 1b110ab02b5..a84658780b9 100644 --- a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb +++ b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb @@ -5,9 +5,10 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| 2.times do Gitlab::WithRequestStore.with_request_store do subscriber.sql(event) + connection = event.payload[:connection] if db_role == :primary - expect(described_class.db_counter_payload).to eq( + expected = { db_count: record_query ? 1 : 0, db_write_count: record_write_query ? 1 : 0, db_cached_count: record_cached_query ? 1 : 0, @@ -18,10 +19,13 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| db_replica_count: 0, db_replica_duration_s: 0.0, db_primary_wal_count: record_wal_query ? 1 : 0, + db_primary_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0, + db_replica_wal_cached_count: 0, db_replica_wal_count: 0 - ) + } + expected[:"db_primary_#{::Gitlab::Database.dbname(connection)}_duration_s"] = 0.002 if record_query elsif db_role == :replica - expect(described_class.db_counter_payload).to eq( + expected = { db_count: record_query ? 1 : 0, db_write_count: record_write_query ? 1 : 0, db_cached_count: record_cached_query ? 1 : 0, @@ -32,15 +36,35 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| db_replica_count: record_query ? 1 : 0, db_replica_duration_s: record_query ? 0.002 : 0, db_replica_wal_count: record_wal_query ? 1 : 0, + db_replica_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0, + db_primary_wal_cached_count: 0, db_primary_wal_count: 0 - ) + } + expected[:"db_replica_#{::Gitlab::Database.dbname(connection)}_duration_s"] = 0.002 if record_query else - expect(described_class.db_counter_payload).to eq( + expected = { db_count: record_query ? 1 : 0, db_write_count: record_write_query ? 1 : 0, db_cached_count: record_cached_query ? 1 : 0 - ) + } end + + expect(described_class.db_counter_payload).to eq(expected) + end + end + end + + context 'when multiple_database_metrics is disabled' do + before do + stub_feature_flags(multiple_database_metrics: false) + end + + it 'does not include per database metrics' do + Gitlab::WithRequestStore.with_request_store do + subscriber.sql(event) + connection = event.payload[:connection] + + expect(described_class.db_counter_payload).not_to include(:"db_replica_#{::Gitlab::Database.dbname(connection)}_duration_s") end end end @@ -71,7 +95,10 @@ RSpec.shared_examples 'record ActiveRecord metrics in a metrics transaction' do end if record_wal_query - expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1) if db_role + if db_role + expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1) + expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_cached_count_total".to_sym, 1) if record_cached_query + end else expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1) if db_role end diff --git a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb index 42f82987989..03f565e0aac 100644 --- a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb +++ b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb @@ -165,9 +165,9 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true| 3.times { supply.next_value } end - current_value = described_class.public_send(method_name, scope_value, &:current_value) - - expect(current_value).to eq(iid + 3) + described_class.public_send(method_name, scope_value) do |supply| + expect(supply.next_value).to eq(iid + 4) + end end end diff --git a/spec/support/shared_examples/models/chat_integration_shared_examples.rb b/spec/support/shared_examples/models/chat_integration_shared_examples.rb index 9f3be3e2e06..72659dd5f3b 100644 --- a/spec/support/shared_examples/models/chat_integration_shared_examples.rb +++ b/spec/support/shared_examples/models/chat_integration_shared_examples.rb @@ -13,7 +13,7 @@ RSpec.shared_examples "chat integration" do |integration_name| end it { is_expected.to validate_presence_of(:webhook) } - it_behaves_like "issue tracker service URL attribute", :webhook + it_behaves_like "issue tracker integration URL attribute", :webhook end context "when integration is inactive" do @@ -163,7 +163,7 @@ RSpec.shared_examples "chat integration" do |integration_name| context "with issue events" do let(:opts) { { title: "Awesome issue", description: "please fix" } } let(:sample_data) do - service = Issues::CreateService.new(project: project, current_user: user, params: opts) + service = Issues::CreateService.new(project: project, current_user: user, params: opts, spam_params: nil) issue = service.execute service.hook_data(issue, "open") end diff --git a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb index 66448aca2c5..2d4c0b60f2b 100644 --- a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb @@ -8,7 +8,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| def execute_with_options(options) receive(:new).with(webhook_url, options.merge(http_client: Integrations::SlackMattermostNotifier::HTTPClient)) - .and_return(double(:slack_service).as_null_object) + .and_return(double(:slack_integration).as_null_object) end describe "Associations" do @@ -23,7 +23,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| end it { is_expected.to validate_presence_of(:webhook) } - it_behaves_like 'issue tracker service URL attribute', :webhook + it_behaves_like 'issue tracker integration URL attribute', :webhook end context 'when service is inactive' do diff --git a/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb b/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb index d23f95b2e9e..cf38a583944 100644 --- a/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb +++ b/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb @@ -122,6 +122,22 @@ RSpec.shared_examples 'value stream analytics stage' do expect(stage.parent_id).to eq(parent.id) end end + + describe '#hash_code' do + it 'does not differ when the same object is built with the same params' do + stage_1 = build(factory) + stage_2 = build(factory) + + expect(stage_1.events_hash_code).to eq(stage_2.events_hash_code) + end + + it 'differs when the stage events are different' do + stage_1 = build(factory, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) + stage_2 = build(factory, start_event_identifier: :issue_created, end_event_identifier: :issue_first_mentioned_in_commit) + + expect(stage_1.events_hash_code).not_to eq(stage_2.events_hash_code) + end + end end RSpec.shared_examples 'value stream analytics label based stage' do diff --git a/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb index 128999d02fa..e35ac9c0d0d 100644 --- a/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb +++ b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb @@ -66,14 +66,14 @@ RSpec.shared_examples Integrations::BaseSlashCommands do } end - let(:service) do - project.create_mattermost_slash_commands_service( + let(:integration) do + project.create_mattermost_slash_commands_integration( properties: { token: 'token' } ) end it 'generates the url' do - response = service.trigger(params) + response = integration.trigger(params) expect(response[:text]).to start_with(':wave: Hi there!') end diff --git a/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb b/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb new file mode 100644 index 00000000000..1fa340a0cf4 --- /dev/null +++ b/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +RSpec.shared_examples Integrations::HasWebHook do + include AfterNextHelpers + + describe 'callbacks' do + it 'calls #update_web_hook! when enabled' do + expect(integration).to receive(:update_web_hook!) + + integration.active = true + integration.save! + end + + it 'does not call #update_web_hook! when disabled' do + expect(integration).not_to receive(:update_web_hook!) + + integration.active = false + integration.save! + end + + it 'does not call #update_web_hook! when validation fails' do + expect(integration).not_to receive(:update_web_hook!) + + integration.active = true + integration.project = nil + expect(integration.save).to be(false) + end + end + + describe '#hook_url' do + it 'returns a string' do + expect(integration.hook_url).to be_a(String) + end + end + + describe '#hook_ssl_verification' do + it 'returns a boolean' do + expect(integration.hook_ssl_verification).to be_in([true, false]) + end + end + + describe '#update_web_hook!' do + def call + integration.update_web_hook! + end + + it 'creates or updates a service hook' do + expect { call }.to change(ServiceHook, :count).by(1) + expect(integration.service_hook.url).to eq(hook_url) + + integration.service_hook.update!(url: 'http://other.com') + + expect { call }.to change { integration.service_hook.reload.url }.from('http://other.com').to(hook_url) + end + + it 'raises an error if the service hook could not be saved' do + call + integration.service_hook.integration = nil + + expect { call }.to raise_error(ActiveRecord::RecordInvalid) + end + + it 'does not attempt to save the service hook if there are no changes' do + call + + expect(integration.service_hook).not_to receive(:save!) + + call + end + end + + describe '#execute_web_hook!' do + let(:args) { ['foo', [1, 2, 3]] } + + def call + integration.execute_web_hook!(*args) + end + + it 'creates the webhook if necessary and executes it' do + expect_next(ServiceHook).to receive(:execute).with(*args) + expect { call }.to change(ServiceHook, :count).by(1) + + expect(integration.service_hook).to receive(:execute).with(*args) + expect { call }.not_to change(ServiceHook, :count) + end + + it 'raises an error if the service hook could not be saved' do + expect_next(ServiceHook).to receive(:execute).with(*args) + + call + integration.service_hook.integration = nil + + expect(integration.service_hook).not_to receive(:execute) + expect { call }.to raise_error(ActiveRecord::RecordInvalid) + end + end +end diff --git a/spec/support/shared_examples/models/issue_tracker_service_shared_examples.rb b/spec/support/shared_examples/models/issue_tracker_service_shared_examples.rb index b275d594792..6d519e561ee 100644 --- a/spec/support/shared_examples/models/issue_tracker_service_shared_examples.rb +++ b/spec/support/shared_examples/models/issue_tracker_service_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples 'issue tracker service URL attribute' do |url_attr| +RSpec.shared_examples 'issue tracker integration URL attribute' do |url_attr| it { is_expected.to allow_value('https://example.com').for(url_attr) } it { is_expected.not_to allow_value('example.com').for(url_attr) } diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb index 7ede6f0d8d4..c111d250d34 100644 --- a/spec/support/shared_examples/models/member_shared_examples.rb +++ b/spec/support/shared_examples/models/member_shared_examples.rb @@ -75,3 +75,259 @@ RSpec.shared_examples '#valid_level_roles' do |entity_name| expect(presenter.valid_level_roles).to eq(expected_roles) end end + +RSpec.shared_examples_for "member creation" do + let_it_be(:user) { create(:user) } + let_it_be(:admin) { create(:admin) } + + describe '#execute' do + it 'returns a Member object', :aggregate_failures do + member = described_class.new(source, user, :maintainer).execute + + expect(member).to be_a member_type + expect(member).to be_persisted + end + + context 'when admin mode is enabled', :enable_admin_mode do + it 'sets members.created_by to the given admin current_user' do + member = described_class.new(source, user, :maintainer, current_user: admin).execute + + expect(member.created_by).to eq(admin) + end + end + + context 'when admin mode is disabled' do + it 'rejects setting members.created_by to the given admin current_user' do + member = described_class.new(source, user, :maintainer, current_user: admin).execute + + expect(member.created_by).to be_nil + end + end + + it 'sets members.expires_at to the given expires_at' do + member = described_class.new(source, user, :maintainer, expires_at: Date.new(2016, 9, 22)).execute + + expect(member.expires_at).to eq(Date.new(2016, 9, 22)) + end + + described_class.access_levels.each do |sym_key, int_access_level| + it "accepts the :#{sym_key} symbol as access level", :aggregate_failures do + expect(source.users).not_to include(user) + + member = described_class.new(source, user.id, sym_key).execute + + expect(member.access_level).to eq(int_access_level) + expect(source.users.reload).to include(user) + end + + it "accepts the #{int_access_level} integer as access level", :aggregate_failures do + expect(source.users).not_to include(user) + + member = described_class.new(source, user.id, int_access_level).execute + + expect(member.access_level).to eq(int_access_level) + expect(source.users.reload).to include(user) + end + end + + context 'with no current_user' do + context 'when called with a known user id' do + it 'adds the user as a member' do + expect(source.users).not_to include(user) + + described_class.new(source, user.id, :maintainer).execute + + expect(source.users.reload).to include(user) + end + end + + context 'when called with an unknown user id' do + it 'adds the user as a member' do + expect(source.users).not_to include(user) + + described_class.new(source, non_existing_record_id, :maintainer).execute + + expect(source.users.reload).not_to include(user) + end + end + + context 'when called with a user object' do + it 'adds the user as a member' do + expect(source.users).not_to include(user) + + described_class.new(source, user, :maintainer).execute + + expect(source.users.reload).to include(user) + end + end + + context 'when called with a requester user object' do + before do + source.request_access(user) + end + + it 'adds the requester as a member', :aggregate_failures do + expect(source.users).not_to include(user) + expect(source.requesters.exists?(user_id: user)).to be_truthy + + expect do + described_class.new(source, user, :maintainer).execute + end.to raise_error(Gitlab::Access::AccessDeniedError) + + expect(source.users.reload).not_to include(user) + expect(source.requesters.reload.exists?(user_id: user)).to be_truthy + end + end + + context 'when called with a known user email' do + it 'adds the user as a member' do + expect(source.users).not_to include(user) + + described_class.new(source, user.email, :maintainer).execute + + expect(source.users.reload).to include(user) + end + end + + context 'when called with an unknown user email' do + it 'creates an invited member' do + expect(source.users).not_to include(user) + + described_class.new(source, 'user@example.com', :maintainer).execute + + expect(source.members.invite.pluck(:invite_email)).to include('user@example.com') + end + end + + context 'when called with an unknown user email starting with a number' do + it 'creates an invited member', :aggregate_failures do + email_starting_with_number = "#{user.id}_email@example.com" + + described_class.new(source, email_starting_with_number, :maintainer).execute + + expect(source.members.invite.pluck(:invite_email)).to include(email_starting_with_number) + expect(source.users.reload).not_to include(user) + end + end + end + + context 'when current_user can update member', :enable_admin_mode do + it 'creates the member' do + expect(source.users).not_to include(user) + + described_class.new(source, user, :maintainer, current_user: admin).execute + + expect(source.users.reload).to include(user) + end + + context 'when called with a requester user object' do + before do + source.request_access(user) + end + + it 'adds the requester as a member', :aggregate_failures do + expect(source.users).not_to include(user) + expect(source.requesters.exists?(user_id: user)).to be_truthy + + described_class.new(source, user, :maintainer, current_user: admin).execute + + expect(source.users.reload).to include(user) + expect(source.requesters.reload.exists?(user_id: user)).to be_falsy + end + end + end + + context 'when current_user cannot update member' do + it 'does not create the member', :aggregate_failures do + expect(source.users).not_to include(user) + + member = described_class.new(source, user, :maintainer, current_user: user).execute + + expect(source.users.reload).not_to include(user) + expect(member).not_to be_persisted + end + + context 'when called with a requester user object' do + before do + source.request_access(user) + end + + it 'does not destroy the requester', :aggregate_failures do + expect(source.users).not_to include(user) + expect(source.requesters.exists?(user_id: user)).to be_truthy + + described_class.new(source, user, :maintainer, current_user: user).execute + + expect(source.users.reload).not_to include(user) + expect(source.requesters.exists?(user_id: user)).to be_truthy + end + end + end + + context 'when member already exists' do + before do + source.add_user(user, :developer) + end + + context 'with no current_user' do + it 'updates the member' do + expect(source.users).to include(user) + + described_class.new(source, user, :maintainer).execute + + expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER) + end + end + + context 'when current_user can update member', :enable_admin_mode do + it 'updates the member' do + expect(source.users).to include(user) + + described_class.new(source, user, :maintainer, current_user: admin).execute + + expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER) + end + end + + context 'when current_user cannot update member' do + it 'does not update the member' do + expect(source.users).to include(user) + + described_class.new(source, user, :maintainer, current_user: user).execute + + expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER) + end + end + end + end + + describe '.add_users' do + let_it_be(:user1) { create(:user) } + let_it_be(:user2) { create(:user) } + + it 'returns a Member objects' do + members = described_class.add_users(source, [user1, user2], :maintainer) + + expect(members).to be_a Array + expect(members.size).to eq(2) + expect(members.first).to be_a member_type + expect(members.first).to be_persisted + end + + it 'returns an empty array' do + members = described_class.add_users(source, [], :maintainer) + + expect(members).to be_a Array + expect(members).to be_empty + end + + it 'supports different formats' do + list = ['joe@local.test', admin, user1.id, user2.id.to_s] + + members = described_class.add_users(source, list, :maintainer) + + expect(members.size).to eq(4) + expect(members.first).to be_invite + end + end +end diff --git a/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb b/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb new file mode 100644 index 00000000000..c92e819db19 --- /dev/null +++ b/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'ci_cd_settings delegation' do + let(:exclude_attributes) { [] } + + context 'when ci_cd_settings is destroyed but project is not' do + it 'allows methods delegated to ci_cd_settings to be nil', :aggregate_failures do + project = create(:project) + attributes = project.ci_cd_settings.attributes.keys - %w(id project_id) - exclude_attributes + project.ci_cd_settings.destroy! + project.reload + attributes.each do |attr| + method = project.respond_to?("ci_#{attr}") ? "ci_#{attr}" : attr + expect(project.send(method)).to be_nil, "#{attr} was not nil" + end + end + end +end + +RSpec.shared_examples 'a ci_cd_settings predicate method' do |prefix: ''| + using RSpec::Parameterized::TableSyntax + + let_it_be(:project) { create(:project) } + + context 'when ci_cd_settings is nil' do + before do + allow(project).to receive(:ci_cd_settings).and_return(nil) + end + + it 'returns false' do + expect(project.send("#{prefix}#{delegated_method}")).to be(false) + end + end + + context 'when ci_cd_settings is not nil' do + where(:delegated_method_return, :subject_return) do + true | true + false | false + end + + with_them do + let(:ci_cd_settings_double) { double('ProjectCiCdSetting') } + + before do + allow(project).to receive(:ci_cd_settings).and_return(ci_cd_settings_double) + allow(ci_cd_settings_double).to receive(delegated_method).and_return(delegated_method_return) + end + + it 'returns the expected boolean value' do + expect(project.send("#{prefix}#{delegated_method}")).to be(subject_return) + end + end + end +end diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb index 2498bf35a09..bc5956e3eec 100644 --- a/spec/support/shared_examples/models/wiki_shared_examples.rb +++ b/spec/support/shared_examples/models/wiki_shared_examples.rb @@ -2,6 +2,7 @@ RSpec.shared_examples 'wiki model' do let_it_be(:user) { create(:user, :commit_email) } + let(:wiki_container) { raise NotImplementedError } let(:wiki_container_without_repo) { raise NotImplementedError } let(:wiki_lfs_enabled) { false } @@ -536,4 +537,98 @@ RSpec.shared_examples 'wiki model' do expect(subject.hook_attrs.keys).to contain_exactly(:web_url, :git_ssh_url, :git_http_url, :path_with_namespace, :default_branch) end end + + describe '#default_branch' do + subject { wiki.default_branch } + + before do + allow(Gitlab::DefaultBranch).to receive(:value).and_return('main') + end + + context 'when repository is not created' do + let(:wiki_container) { wiki_container_without_repo } + + it 'returns the instance default branch' do + expect(subject).to eq 'main' + end + end + + context 'when repository is empty' do + let(:wiki_container) { wiki_container_without_repo } + + before do + wiki.repository.create_if_not_exists + end + + it 'returns the instance default branch' do + expect(subject).to eq 'main' + end + end + + context 'when repository is not empty' do + it 'returns the repository default branch' do + wiki.create_page('index', 'test content') + + expect(subject).to eq wiki.repository.root_ref + end + end + end + + describe '#create_wiki_repository' do + let(:head_path) { Rails.root.join(TestEnv.repos_path, "#{wiki.disk_path}.git", 'HEAD') } + let(:default_branch) { 'foo' } + + before do + allow(Gitlab::CurrentSettings).to receive(:default_branch_name).and_return(default_branch) + end + + subject { wiki.create_wiki_repository } + + context 'when repository is not created' do + let(:wiki_container) { wiki_container_without_repo } + + it 'changes the HEAD reference to the default branch' do + expect(wiki.empty?).to eq true + + subject + + expect(File.read(head_path).squish).to eq "ref: refs/heads/#{default_branch}" + end + end + + context 'when repository is empty' do + let(:wiki_container) { wiki_container_without_repo } + + it 'changes the HEAD reference to the default branch' do + wiki.repository.create_if_not_exists + wiki.repository.raw_repository.write_ref('HEAD', 'refs/heads/bar') + + subject + + expect(File.read(head_path).squish).to eq "ref: refs/heads/#{default_branch}" + end + end + + context 'when repository is not empty' do + before do + wiki.create_page('index', 'test content') + end + + it 'does nothing when HEAD points to the right branch' do + expect(wiki.repository.raw_repository).not_to receive(:write_ref) + + subject + end + + context 'when HEAD points to the wrong branch' do + it 'rewrites HEAD with the right branch' do + wiki.repository.raw_repository.write_ref('HEAD', 'refs/heads/bar') + + subject + + expect(File.read(head_path).squish).to eq "ref: refs/heads/#{default_branch}" + end + end + end + end end diff --git a/spec/support/shared_examples/namespaces/traversal_examples.rb b/spec/support/shared_examples/namespaces/traversal_examples.rb index ccc64c80fd4..f09634556c3 100644 --- a/spec/support/shared_examples/namespaces/traversal_examples.rb +++ b/spec/support/shared_examples/namespaces/traversal_examples.rb @@ -12,16 +12,18 @@ RSpec.shared_examples 'namespace traversal' do it "makes a recursive query" do groups.each do |group| - expect { group.public_send(recursive_method).load }.to make_queries_matching(/WITH RECURSIVE/) + expect { group.public_send(recursive_method).try(:load) }.to make_queries_matching(/WITH RECURSIVE/) end end end - describe '#root_ancestor' do - let_it_be(:group) { create(:group) } - let_it_be(:nested_group) { create(:group, parent: group) } - let_it_be(:deep_nested_group) { create(:group, parent: nested_group) } + let_it_be(:group) { create(:group) } + let_it_be(:nested_group) { create(:group, parent: group) } + let_it_be(:deep_nested_group) { create(:group, parent: nested_group) } + let_it_be(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + let_it_be(:groups) { [group, nested_group, deep_nested_group, very_deep_nested_group] } + describe '#root_ancestor' do it 'returns the correct root ancestor' do expect(group.root_ancestor).to eq(group) expect(nested_group.root_ancestor).to eq(group) @@ -29,8 +31,6 @@ RSpec.shared_examples 'namespace traversal' do end describe '#recursive_root_ancestor' do - let(:groups) { [group, nested_group, deep_nested_group] } - it "is equivalent to #recursive_root_ancestor" do groups.each do |group| expect(group.root_ancestor).to eq(group.recursive_root_ancestor) @@ -40,12 +40,8 @@ RSpec.shared_examples 'namespace traversal' do end describe '#self_and_hierarchy' do - let!(:group) { create(:group, path: 'git_lab') } - let!(:nested_group) { create(:group, parent: group) } - let!(:deep_nested_group) { create(:group, parent: nested_group) } - let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } - let!(:another_group) { create(:group, path: 'gitllab') } - let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) } + let!(:another_group) { create(:group) } + let!(:another_group_nested) { create(:group, parent: another_group) } it 'returns the correct tree' do expect(group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group) @@ -54,18 +50,11 @@ RSpec.shared_examples 'namespace traversal' do end describe '#recursive_self_and_hierarchy' do - let(:groups) { [group, nested_group, very_deep_nested_group] } - it_behaves_like 'recursive version', :self_and_hierarchy end end describe '#ancestors' do - let_it_be(:group) { create(:group) } - let_it_be(:nested_group) { create(:group, parent: group) } - let_it_be(:deep_nested_group) { create(:group, parent: nested_group) } - let_it_be(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } - it 'returns the correct ancestors' do # #reload is called to make sure traversal_ids are reloaded expect(very_deep_nested_group.reload.ancestors).to contain_exactly(group, nested_group, deep_nested_group) @@ -75,18 +64,28 @@ RSpec.shared_examples 'namespace traversal' do end describe '#recursive_ancestors' do - let(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] } + let_it_be(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] } it_behaves_like 'recursive version', :ancestors end end - describe '#self_and_ancestors' do - let(:group) { create(:group) } - let(:nested_group) { create(:group, parent: group) } - let(:deep_nested_group) { create(:group, parent: nested_group) } - let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + describe '#ancestor_ids' do + it 'returns the correct ancestor ids' do + expect(very_deep_nested_group.ancestor_ids).to contain_exactly(group.id, nested_group.id, deep_nested_group.id) + expect(deep_nested_group.ancestor_ids).to contain_exactly(group.id, nested_group.id) + expect(nested_group.ancestor_ids).to contain_exactly(group.id) + expect(group.ancestor_ids).to be_empty + end + + describe '#recursive_ancestor_ids' do + let_it_be(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] } + + it_behaves_like 'recursive version', :ancestor_ids + end + end + describe '#self_and_ancestors' do it 'returns the correct ancestors' do expect(very_deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group) expect(deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group) @@ -95,19 +94,30 @@ RSpec.shared_examples 'namespace traversal' do end describe '#recursive_self_and_ancestors' do - let(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] } + let_it_be(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] } it_behaves_like 'recursive version', :self_and_ancestors end end + describe '#self_and_ancestor_ids' do + it 'returns the correct ancestor ids' do + expect(very_deep_nested_group.self_and_ancestor_ids).to contain_exactly(group.id, nested_group.id, deep_nested_group.id, very_deep_nested_group.id) + expect(deep_nested_group.self_and_ancestor_ids).to contain_exactly(group.id, nested_group.id, deep_nested_group.id) + expect(nested_group.self_and_ancestor_ids).to contain_exactly(group.id, nested_group.id) + expect(group.self_and_ancestor_ids).to contain_exactly(group.id) + end + + describe '#recursive_self_and_ancestor_ids' do + let_it_be(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] } + + it_behaves_like 'recursive version', :self_and_ancestor_ids + end + end + describe '#descendants' do - let!(:group) { create(:group, path: 'git_lab') } - let!(:nested_group) { create(:group, parent: group) } - let!(:deep_nested_group) { create(:group, parent: nested_group) } - let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } - let!(:another_group) { create(:group, path: 'gitllab') } - let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) } + let!(:another_group) { create(:group) } + let!(:another_group_nested) { create(:group, parent: another_group) } it 'returns the correct descendants' do expect(very_deep_nested_group.descendants.to_a).to eq([]) @@ -117,19 +127,13 @@ RSpec.shared_examples 'namespace traversal' do end describe '#recursive_descendants' do - let(:groups) { [group, nested_group, deep_nested_group, very_deep_nested_group] } - it_behaves_like 'recursive version', :descendants end end describe '#self_and_descendants' do - let!(:group) { create(:group, path: 'git_lab') } - let!(:nested_group) { create(:group, parent: group) } - let!(:deep_nested_group) { create(:group, parent: nested_group) } - let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } - let!(:another_group) { create(:group, path: 'gitllab') } - let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) } + let!(:another_group) { create(:group) } + let!(:another_group_nested) { create(:group, parent: another_group) } it 'returns the correct descendants' do expect(very_deep_nested_group.self_and_descendants).to contain_exactly(very_deep_nested_group) @@ -139,24 +143,18 @@ RSpec.shared_examples 'namespace traversal' do end describe '#recursive_self_and_descendants' do - let(:groups) { [group, nested_group, deep_nested_group, very_deep_nested_group] } + let_it_be(:groups) { [group, nested_group, deep_nested_group] } it_behaves_like 'recursive version', :self_and_descendants end end describe '#self_and_descendant_ids' do - let!(:group) { create(:group, path: 'git_lab') } - let!(:nested_group) { create(:group, parent: group) } - let!(:deep_nested_group) { create(:group, parent: nested_group) } - subject { group.self_and_descendant_ids.pluck(:id) } - it { is_expected.to contain_exactly(group.id, nested_group.id, deep_nested_group.id) } + it { is_expected.to contain_exactly(group.id, nested_group.id, deep_nested_group.id, very_deep_nested_group.id) } describe '#recursive_self_and_descendant_ids' do - let(:groups) { [group, nested_group, deep_nested_group] } - it_behaves_like 'recursive version', :self_and_descendant_ids end end diff --git a/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb index 3cdba315d1f..4d142199c95 100644 --- a/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb @@ -233,6 +233,7 @@ RSpec.shared_examples 'issuable quick actions' do context 'when user can update issuable' do let_it_be(:developer) { create(:user) } + let(:note_author) { developer } before do @@ -260,6 +261,7 @@ RSpec.shared_examples 'issuable quick actions' do context 'when user cannot update issuable' do let_it_be(:non_member) { create(:user) } + let(:note_author) { non_member } it 'applies commands that user can execute' do diff --git a/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb index 92849ddf1cb..052fd0622d0 100644 --- a/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb @@ -72,6 +72,24 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type| end end + it 'shows the time tracking report when link is clicked' do + submit_time('/estimate 1w') + submit_time('/spend 1d') + + wait_for_requests + + page.within '.time-tracking-component-wrap' do + click_link 'Time tracking report' + + wait_for_requests + end + + page.within '#time-tracking-report' do + expect(find('tbody')).to have_content maintainer.name + expect(find('tbody')).to have_content '1d' + end + end + it 'hides the help state when close icon is clicked' do page.within '.time-tracking-component-wrap' do find('.help-button').click diff --git a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb index 0530aa8c760..1f68dd7a382 100644 --- a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb @@ -12,15 +12,17 @@ RSpec.shared_context 'Debian repository shared context' do |container_type, can_ let_it_be(:user, freeze: true) { create(:user) } let_it_be(:personal_access_token, freeze: true) { create(:personal_access_token, user: user) } - let_it_be(:private_distribution, freeze: true) { create("debian_#{container_type}_distribution", container: private_container, codename: 'existing-codename') } + let_it_be(:private_distribution, freeze: true) { create("debian_#{container_type}_distribution", :with_file, container: private_container, codename: 'existing-codename') } let_it_be(:private_component, freeze: true) { create("debian_#{container_type}_component", distribution: private_distribution, name: 'existing-component') } let_it_be(:private_architecture_all, freeze: true) { create("debian_#{container_type}_architecture", distribution: private_distribution, name: 'all') } let_it_be(:private_architecture, freeze: true) { create("debian_#{container_type}_architecture", distribution: private_distribution, name: 'existing-arch') } + let_it_be(:private_component_file) { create("debian_#{container_type}_component_file", component: private_component, architecture: private_architecture) } - let_it_be(:public_distribution, freeze: true) { create("debian_#{container_type}_distribution", container: public_container, codename: 'existing-codename') } + let_it_be(:public_distribution, freeze: true) { create("debian_#{container_type}_distribution", :with_file, container: public_container, codename: 'existing-codename') } let_it_be(:public_component, freeze: true) { create("debian_#{container_type}_component", distribution: public_distribution, name: 'existing-component') } let_it_be(:public_architecture_all, freeze: true) { create("debian_#{container_type}_architecture", distribution: public_distribution, name: 'all') } let_it_be(:public_architecture, freeze: true) { create("debian_#{container_type}_architecture", distribution: public_distribution, name: 'existing-arch') } + let_it_be(:public_component_file) { create("debian_#{container_type}_component_file", component: public_component, architecture: public_architecture) } if container_type == :group let_it_be(:private_project) { create(:project, :private, group: private_container) } @@ -40,14 +42,15 @@ RSpec.shared_context 'Debian repository shared context' do |container_type, can_ let(:visibility_level) { :public } let(:distribution) { { private: private_distribution, public: public_distribution }[visibility_level] } + let(:architecture) { { private: private_architecture, public: public_architecture }[visibility_level] } + let(:component) { { private: private_component, public: public_component }[visibility_level] } + let(:component_file) { { private: private_component_file, public: public_component_file }[visibility_level] } - let(:component) { 'main' } - let(:architecture) { 'amd64' } let(:source_package) { 'sample' } let(:letter) { source_package[0..2] == 'lib' ? source_package[0..3] : source_package[0] } let(:package_name) { 'libsample0' } let(:package_version) { '1.2.3~alpha2' } - let(:file_name) { "#{package_name}_#{package_version}_#{architecture}.deb" } + let(:file_name) { "#{package_name}_#{package_version}_#{architecture.name}.deb" } let(:method) { :get } diff --git a/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb index 9cf5bc04f65..7e1f4500779 100644 --- a/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb @@ -31,6 +31,28 @@ RSpec.shared_examples 'a noteable graphql type we can query' do expect(graphql_data_at(*path_to_noteable, :discussions, :nodes)) .to match_array(expected) end + + it 'can fetch discussion noteable' do + create(discussion_factory, project: project, noteable: noteable) + fields = + <<-QL.strip_heredoc + discussions { + nodes { + noteable { + __typename + ... on #{noteable.class.name.demodulize} { + id + } + } + } + } + QL + + post_graphql(query(fields), current_user: current_user) + + data = graphql_data_at(*path_to_noteable, :discussions, :nodes, :noteable, :id) + expect(data[0]).to eq(global_id_of(noteable)) + end end describe '.notes' do diff --git a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb index 585c4fb8a4e..1ad38a17f9c 100644 --- a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -RSpec.shared_examples 'rejects helm packages access' do |user_type, status, add_member = true| +RSpec.shared_examples 'rejects helm packages access' do |user_type, status| context "for user type #{user_type}" do before do - project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + project.send("add_#{user_type}", user) if user_type != :anonymous && user_type != :not_a_member end it_behaves_like 'returning response status', status @@ -18,19 +18,170 @@ RSpec.shared_examples 'rejects helm packages access' do |user_type, status, add_ end end -RSpec.shared_examples 'process helm download content request' do |user_type, status, add_member = true| +RSpec.shared_examples 'process helm service index request' do |user_type, status| context "for user type #{user_type}" do before do - project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + project.send("add_#{user_type}", user) if user_type != :anonymous && user_type != :not_a_member end - it_behaves_like 'returning response status', status + it 'returns a valid YAML response', :aggregate_failures do + subject + + expect(response).to have_gitlab_http_status(status) + + expect(response.media_type).to eq('text/yaml') + expect(response.body).to start_with("---\napiVersion: v1\nentries:\n") + + yaml_response = YAML.safe_load(response.body) + + expect(yaml_response.keys).to contain_exactly('apiVersion', 'entries', 'generated', 'serverInfo') + expect(yaml_response['entries']).to be_a(Hash) + expect(yaml_response['entries'].keys).to contain_exactly(package.name) + expect(yaml_response['serverInfo']).to eq({ 'contextPath' => "/api/v4/projects/#{project.id}/packages/helm" }) + + package_entry = yaml_response['entries'][package.name] + + expect(package_entry.length).to eq(1) + expect(package_entry.first.keys).to contain_exactly('name', 'version', 'apiVersion', 'created', 'digest', 'urls') + expect(package_entry.first['digest']).to eq('fd2b2fa0329e80a2a602c2bb3b40608bcd6ee5cf96cf46fd0d2800a4c129c9db') + expect(package_entry.first['urls']).to eq(["charts/#{package.name}-#{package.version}.tgz"]) + end + end +end + +RSpec.shared_examples 'process helm workhorse authorization' do |user_type, status, test_bypass: false| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if user_type != :anonymous && user_type != :not_a_member + end + + it 'has the proper status and content type' do + subject + + expect(response).to have_gitlab_http_status(status) + expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + end + + context 'with a request that bypassed gitlab-workhorse' do + let(:headers) do + basic_auth_header(user.username, personal_access_token.token) + .merge(workhorse_headers) + .tap { |h| h.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) } + end + + before do + project.add_maintainer(user) + end + + it_behaves_like 'returning response status', :forbidden + end + end +end + +RSpec.shared_examples 'process helm upload' do |user_type, status| + shared_examples 'creates helm package files' do + it 'creates package files' do + expect(::Packages::Helm::ExtractionWorker).to receive(:perform_async).once + expect { subject } + .to change { project.packages.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + expect(response).to have_gitlab_http_status(status) + + package_file = project.packages.last.package_files.reload.last + expect(package_file.file_name).to eq('package.tgz') + end + end + + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if user_type != :anonymous && user_type != :not_a_member + end + + context 'with object storage disabled' do + before do + stub_package_file_object_storage(enabled: false) + end + + context 'without a file from workhorse' do + let(:send_rewritten_field) { false } + + it_behaves_like 'returning response status', :bad_request + end + + context 'with correct params' do + it_behaves_like 'package workhorse uploads' + it_behaves_like 'creates helm package files' + it_behaves_like 'a package tracking event', 'API::HelmPackages', 'push_package' + end + end + + context 'with object storage enabled' do + let(:tmp_object) do + fog_connection.directories.new(key: 'packages').files.create( # rubocop:disable Rails/SaveBang + key: "tmp/uploads/#{file_name}", + body: 'content' + ) + end + + let(:fog_file) { fog_to_uploaded_file(tmp_object) } + let(:params) { { chart: fog_file, 'chart.remote_id' => file_name } } + + context 'and direct upload enabled' do + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: true) + end + + it_behaves_like 'creates helm package files' + + ['123123', '../../123123'].each do |remote_id| + context "with invalid remote_id: #{remote_id}" do + let(:params) do + { + chart: fog_file, + 'chart.remote_id' => remote_id + } + end + + it_behaves_like 'returning response status', :forbidden + end + end + end + + context 'and direct upload disabled' do + context 'and background upload disabled' do + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: false, background_upload: false) + end + + it_behaves_like 'creates helm package files' + end + + context 'and background upload enabled' do + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: false, background_upload: true) + end + + it_behaves_like 'creates helm package files' + end + end + end + + it_behaves_like 'background upload schedules a file migration' + end +end + +RSpec.shared_examples 'process helm download content request' do |user_type, status| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if user_type != :anonymous && user_type != :not_a_member + end it_behaves_like 'a package tracking event', 'API::HelmPackages', 'pull_package' - it 'returns a valid package archive' do + it 'returns expected status and a valid package archive' do subject + expect(response).to have_gitlab_http_status(status) expect(response.media_type).to eq('application/octet-stream') end end @@ -51,3 +202,69 @@ RSpec.shared_examples 'rejects helm access with unknown project id' do end end end + +RSpec.shared_examples 'handling helm chart index requests' do + context 'with valid project' do + subject { get api(url), headers: headers } + + using RSpec::Parameterized::TableSyntax + + context 'personal token' do + where(:visibility, :user_role, :shared_examples_name, :expected_status) do + :public | :guest | 'process helm service index request' | :success + :public | :not_a_member | 'process helm service index request' | :success + :public | :anonymous | 'process helm service index request' | :success + :private | :reporter | 'process helm service index request' | :success + :private | :guest | 'rejects helm packages access' | :forbidden + :private | :not_a_member | 'rejects helm packages access' | :not_found + :private | :anonymous | 'rejects helm packages access' | :unauthorized + end + + with_them do + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, personal_access_token.token) } + + before do + project.update!(visibility: visibility.to_s) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status] + end + end + + context 'when an invalid token is passed' do + let(:headers) { basic_auth_header(user.username, 'wrong') } + + it_behaves_like 'returning response status', :unauthorized + end + + context 'with job token' do + where(:visibility, :user_role, :shared_examples_name, :expected_status) do + :public | :guest | 'process helm service index request' | :success + :public | :not_a_member | 'process helm service index request' | :success + :public | :anonymous | 'process helm service index request' | :success + :private | :reporter | 'process helm service index request' | :success + :private | :guest | 'rejects helm packages access' | :forbidden + :private | :not_a_member | 'rejects helm packages access' | :not_found + :private | :anonymous | 'rejects helm packages access' | :unauthorized + end + + with_them do + let_it_be(:ci_build) { create(:ci_build, project: project, user: user, status: :running) } + + let(:headers) { user_role == :anonymous ? {} : job_basic_auth_header(ci_build) } + + before do + project.update!(visibility: visibility.to_s) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status] + end + end + end + + it_behaves_like 'deploy token for package GET requests' + + it_behaves_like 'rejects helm access with unknown project id' do + subject { get api(url) } + end +end diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb index 617fdecbb5b..878cbc10a24 100644 --- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb @@ -136,8 +136,8 @@ RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, sta end end -RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = true| - RSpec.shared_examples 'creates nuget package files' do +RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = true, symbol_package = false| + shared_examples 'creates nuget package files' do it 'creates package files' do expect(::Packages::Nuget::ExtractionWorker).to receive(:perform_async).once expect { subject } @@ -146,7 +146,7 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = expect(response).to have_gitlab_http_status(status) package_file = target.packages.last.package_files.reload.last - expect(package_file.file_name).to eq('package.nupkg') + expect(package_file.file_name).to eq(file_name) end end @@ -169,7 +169,12 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = context 'with correct params' do it_behaves_like 'package workhorse uploads' it_behaves_like 'creates nuget package files' - it_behaves_like 'a package tracking event', 'API::NugetPackages', 'push_package' + + if symbol_package + it_behaves_like 'a package tracking event', 'API::NugetPackages', 'push_symbol_package' + else + it_behaves_like 'a package tracking event', 'API::NugetPackages', 'push_package' + end end end @@ -300,6 +305,18 @@ RSpec.shared_examples 'process nuget download content request' do |user_type, st it_behaves_like 'rejects nuget packages access', :anonymous, :not_found end + context 'with symbol package' do + let(:format) { 'snupkg' } + + it 'returns a valid package archive' do + subject + + expect(response.media_type).to eq('application/octet-stream') + end + + it_behaves_like 'a package tracking event', 'API::NugetPackages', 'pull_symbol_package' + end + context 'with lower case package name' do let_it_be(:package_name) { 'dummy.package' } @@ -407,3 +424,114 @@ RSpec.shared_examples 'rejects nuget access with unknown target id' do end end end + +RSpec.shared_examples 'nuget authorize upload endpoint' do + using RSpec::Parameterized::TableSyntax + + context 'with valid project' do + where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget workhorse authorization' | :success + 'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | true | true | 'process nuget workhorse authorization' | :success + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + let(:headers) { user_headers.merge(workhorse_headers) } + + before do + update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + it_behaves_like 'deploy token for package uploads' + + it_behaves_like 'job token for package uploads', authorize_endpoint: true do + let_it_be(:job) { create(:ci_build, :running, user: user, project: project) } + end + + it_behaves_like 'rejects nuget access with unknown target id' + + it_behaves_like 'rejects nuget access with invalid target id' +end + +RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false| + using RSpec::Parameterized::TableSyntax + + context 'with valid project' do + where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget upload' | :created + 'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | true | true | 'process nuget upload' | :created + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + let(:headers) { user_headers.merge(workhorse_headers) } + let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace } } + + before do + update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member], symbol_package + end + end + + it_behaves_like 'deploy token for package uploads' + + it_behaves_like 'job token for package uploads' do + let_it_be(:job) { create(:ci_build, :running, user: user, project: project) } + end + + it_behaves_like 'rejects nuget access with unknown target id' + + it_behaves_like 'rejects nuget access with invalid target id' + + context 'file size above maximum limit' do + let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) } + + before do + allow_next_instance_of(UploadedFile) do |uploaded_file| + allow(uploaded_file).to receive(:size).and_return(project.actual_limits.nuget_max_file_size + 1) + end + end + + it_behaves_like 'returning response status', :bad_request + end +end diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb index 42c29084d7b..ecde4ee8565 100644 --- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb @@ -100,7 +100,7 @@ RSpec.shared_examples 'job token for package GET requests' do end end -RSpec.shared_examples 'job token for package uploads' do |authorize_endpoint: false| +RSpec.shared_examples 'job token for package uploads' do |authorize_endpoint: false, accept_invalid_username: false| context 'with job token headers' do let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token).merge(workhorse_headers) } @@ -133,7 +133,11 @@ RSpec.shared_examples 'job token for package uploads' do |authorize_endpoint: fa context 'invalid user' do let(:headers) { basic_auth_header('foo', job.token).merge(workhorse_headers) } - it_behaves_like 'returning response status', :unauthorized + if accept_invalid_username + it_behaves_like 'returning response status', :success + else + it_behaves_like 'returning response status', :unauthorized + end end end end @@ -143,7 +147,7 @@ RSpec.shared_examples 'a package tracking event' do |category, action| stub_feature_flags(collect_package_events: true) end - it "creates a gitlab tracking event #{action}", :snowplow do + it "creates a gitlab tracking event #{action}", :snowplow, :aggregate_failures do expect { subject }.to change { Packages::Event.count }.by(1) expect_snowplow_event(category: category, action: action, **snowplow_gitlab_standard_context) diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb index 218a3462c35..92a7d7ab3a3 100644 --- a/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb +++ b/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb @@ -13,7 +13,7 @@ RSpec.shared_examples 'creates an alert management alert or errors' do it 'executes the alert service hooks' do expect_next_instance_of(AlertManagement::Alert) do |alert| - expect(alert).to receive(:execute_services) + expect(alert).to receive(:execute_integrations) end subject @@ -84,7 +84,7 @@ end # - `alert`, the alert for which events should be incremented RSpec.shared_examples 'adds an alert management alert event' do specify do - expect(alert).not_to receive(:execute_services) + expect(alert).not_to receive(:execute_integrations) expect { subject }.to change { alert.reload.events }.by(1) diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb index c6ac07b6dd5..98834f01ce2 100644 --- a/spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb +++ b/spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb @@ -20,7 +20,7 @@ end RSpec.shared_examples 'processes incident issues' do |with_issue: false| before do allow_next_instance_of(AlertManagement::Alert) do |alert| - allow(alert).to receive(:execute_services) + allow(alert).to receive(:execute_integrations) end end diff --git a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb index ba176b616c3..eafcbd77040 100644 --- a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb +++ b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb @@ -157,8 +157,13 @@ end RSpec.shared_examples 'a container registry auth service' do include_context 'container registry auth service context' + before do + stub_feature_flags(container_registry_migration_phase1: false) + end + describe '#full_access_token' do let_it_be(:project) { create(:project) } + let(:token) { described_class.full_access_token(project.full_path) } subject { { token: token } } @@ -172,6 +177,7 @@ RSpec.shared_examples 'a container registry auth service' do describe '#pull_access_token' do let_it_be(:project) { create(:project) } + let(:token) { described_class.pull_access_token(project.full_path) } subject { { token: token } } @@ -432,6 +438,7 @@ RSpec.shared_examples 'a container registry auth service' do context 'for external user' do context 'disallow anyone to pull or push images' do let_it_be(:current_user) { create(:user, external: true) } + let(:current_params) do { scopes: ["repository:#{project.full_path}:pull,push"] } end @@ -442,6 +449,7 @@ RSpec.shared_examples 'a container registry auth service' do context 'disallow anyone to delete images' do let_it_be(:current_user) { create(:user, external: true) } + let(:current_params) do { scopes: ["repository:#{project.full_path}:*"] } end @@ -452,6 +460,7 @@ RSpec.shared_examples 'a container registry auth service' do context 'disallow anyone to delete images since registry 2.7' do let_it_be(:current_user) { create(:user, external: true) } + let(:current_params) do { scopes: ["repository:#{project.full_path}:delete"] } end @@ -620,6 +629,22 @@ RSpec.shared_examples 'a container registry auth service' do end end end + + context 'for project with private container registry' do + let_it_be(:project, reload: true) { create(:project, :public) } + + before do + project.project_feature.update!(container_registry_access_level: ProjectFeature::PRIVATE) + end + + it_behaves_like 'pullable for being team member' + + context 'when you are admin' do + let_it_be(:current_user) { create(:admin) } + + it_behaves_like 'pullable for being team member' + end + end end context 'when pushing' do diff --git a/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb b/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb index cbe5c7d89db..0151723793e 100644 --- a/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb +++ b/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb @@ -3,7 +3,7 @@ RSpec.shared_examples 'mapping jira users' do let(:client) { double } - let_it_be(:jira_service) { create(:jira_service, project: project, active: true) } + let_it_be(:jira_integration) { create(:jira_integration, project: project, active: true) } before do allow(subject).to receive(:client).and_return(client) diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb index 72878e925dc..6bc4f171d9c 100644 --- a/spec/support/shared_examples/services/packages_shared_examples.rb +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -43,6 +43,7 @@ end RSpec.shared_examples 'assigns status to package' do context 'with status param' do let_it_be(:status) { 'hidden' } + let(:params) { super().merge(status: status) } it 'assigns the status to the package' do diff --git a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb index 275ddebc18c..14af35e58b7 100644 --- a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb +++ b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb @@ -123,9 +123,10 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| .with(repository.raw) .and_raise(Gitlab::Git::CommandError) - result = subject.execute + expect do + subject.execute + end.to raise_error(Gitlab::Git::CommandError) - expect(result).to be_error expect(project).not_to be_repository_read_only expect(project.repository_storage).to eq('default') expect(repository_storage_move).to be_failed @@ -149,9 +150,10 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| expect(original_repository_double).to receive(:remove) .and_raise(Gitlab::Git::CommandError) - result = subject.execute + expect do + subject.execute + end.to raise_error(Gitlab::Git::CommandError) - expect(result).to be_error expect(repository_storage_move).to be_cleanup_failed end end @@ -170,9 +172,10 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| allow(repository_double).to receive(:checksum) .and_return('not matching checksum') - result = subject.execute + expect do + subject.execute + end.to raise_error(UpdateRepositoryStorageMethods::Error, /Failed to verify \w+ repository checksum from \w+ to not matching checksum/) - expect(result).to be_error expect(project).not_to be_repository_read_only expect(project.repository_storage).to eq('default') end diff --git a/spec/support/shared_examples/services/service_ping/complete_service_ping_payload_shared_examples.rb b/spec/support/shared_examples/services/service_ping/complete_service_ping_payload_shared_examples.rb new file mode 100644 index 00000000000..8dcff99fb6f --- /dev/null +++ b/spec/support/shared_examples/services/service_ping/complete_service_ping_payload_shared_examples.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'complete service ping payload' do + it_behaves_like 'service ping payload with all expected metrics' do + let(:expected_metrics) do + standard_metrics + subscription_metrics + operational_metrics + optional_metrics + end + end +end diff --git a/spec/support/shared_examples/services/service_ping/service_ping_payload_with_all_expected_metrics_shared_examples.rb b/spec/support/shared_examples/services/service_ping/service_ping_payload_with_all_expected_metrics_shared_examples.rb new file mode 100644 index 00000000000..535e7291b7e --- /dev/null +++ b/spec/support/shared_examples/services/service_ping/service_ping_payload_with_all_expected_metrics_shared_examples.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'service ping payload with all expected metrics' do + specify do + aggregate_failures do + expected_metrics.each do |metric| + is_expected.to have_usage_metric metric['key_path'] + end + end + end +end diff --git a/spec/support/shared_examples/services/service_ping/service_ping_payload_without_restricted_metrics_shared_examples.rb b/spec/support/shared_examples/services/service_ping/service_ping_payload_without_restricted_metrics_shared_examples.rb new file mode 100644 index 00000000000..9f18174cbc7 --- /dev/null +++ b/spec/support/shared_examples/services/service_ping/service_ping_payload_without_restricted_metrics_shared_examples.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'service ping payload without restricted metrics' do + specify do + aggregate_failures do + restricted_metrics.each do |metric| + is_expected.not_to have_usage_metric metric['key_path'] + end + end + end +end diff --git a/spec/support/shared_examples/services/snippets_shared_examples.rb b/spec/support/shared_examples/services/snippets_shared_examples.rb index 0c4db7ded69..5a44f739b27 100644 --- a/spec/support/shared_examples/services/snippets_shared_examples.rb +++ b/spec/support/shared_examples/services/snippets_shared_examples.rb @@ -1,23 +1,6 @@ # frozen_string_literal: true RSpec.shared_examples 'checking spam' do - let(:request) { double(:request, headers: headers) } - let(:headers) { nil } - let(:api) { true } - let(:captcha_response) { 'abc123' } - let(:spam_log_id) { 1 } - let(:disable_spam_action_service) { false } - - let(:extra_opts) do - { - request: request, - api: api, - captcha_response: captcha_response, - spam_log_id: spam_log_id, - disable_spam_action_service: disable_spam_action_service - } - end - before do allow_next_instance_of(UserAgentDetailService) do |instance| allow(instance).to receive(:create) @@ -25,73 +8,20 @@ RSpec.shared_examples 'checking spam' do end it 'executes SpamActionService' do - spam_params = Spam::SpamParams.new( - api: api, - captcha_response: captcha_response, - spam_log_id: spam_log_id - ) expect_next_instance_of( Spam::SpamActionService, { spammable: kind_of(Snippet), - request: request, + spam_params: spam_params, user: an_instance_of(User), action: action } ) do |instance| - expect(instance).to receive(:execute).with(spam_params: spam_params) + expect(instance).to receive(:execute) end subject end - - context 'when CAPTCHA arguments are passed in the headers' do - let(:headers) do - { - 'X-GitLab-Spam-Log-Id' => spam_log_id, - 'X-GitLab-Captcha-Response' => captcha_response - } - end - - let(:extra_opts) do - { - request: request, - api: api, - disable_spam_action_service: disable_spam_action_service - } - end - - it 'executes the SpamActionService correctly' do - spam_params = Spam::SpamParams.new( - api: api, - captcha_response: captcha_response, - spam_log_id: spam_log_id - ) - expect_next_instance_of( - Spam::SpamActionService, - { - spammable: kind_of(Snippet), - request: request, - user: an_instance_of(User), - action: action - } - ) do |instance| - expect(instance).to receive(:execute).with(spam_params: spam_params) - end - - subject - end - end - - context 'when spam action service is disabled' do - let(:disable_spam_action_service) { true } - - it 'request parameter is not passed to the service' do - expect(Spam::SpamActionService).not_to receive(:new) - - subject - end - end end shared_examples 'invalid params error response' do diff --git a/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb b/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb index 555a6d5eed0..1646c18a0ed 100644 --- a/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb +++ b/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb @@ -25,6 +25,25 @@ RSpec.shared_examples 'Wikis::CreateAttachmentService#execute' do |container_typ container.add_developer(user) end + context 'creates wiki repository if it does not exist' do + let(:container) { create(container_type) } # rubocop:disable Rails/SaveBang + + it 'creates wiki repository' do + expect { service.execute }.to change { container.wiki.repository.exists? }.to(true) + end + + context 'if an error is raised creating the repository' do + it 'catches error and return gracefully' do + allow(container.wiki).to receive(:repository_exists?).and_return(false) + + result = service.execute + + expect(result[:status]).to eq :error + expect(result[:message]).to eq 'Error creating the wiki repository' + end + end + end + context 'creates branch if it does not exists' do let(:branch_name) { 'new_branch' } let(:opts) { file_opts.merge(branch_name: branch_name) } diff --git a/spec/support/shared_examples/workers/in_product_marketing_email_shared_example.rb b/spec/support/shared_examples/workers/in_product_marketing_email_shared_example.rb deleted file mode 100644 index c4391f61369..00000000000 --- a/spec/support/shared_examples/workers/in_product_marketing_email_shared_example.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'in-product marketing email' do - before do - stub_application_setting(in_product_marketing_emails_enabled: in_product_marketing_emails_enabled) - stub_experiment(in_product_marketing_emails: experiment_active) - allow(::Gitlab).to receive(:com?).and_return(is_gitlab_com) - end - - it 'executes the email service service' do - expect(Namespaces::InProductMarketingEmailsService).to receive(:send_for_all_tracks_and_intervals).exactly(executes_service).times - - subject.perform - end -end |