diff options
Diffstat (limited to 'spec/support')
66 files changed, 1746 insertions, 289 deletions
diff --git a/spec/support/action_cable.rb b/spec/support/action_cable.rb new file mode 100644 index 00000000000..64cfc435875 --- /dev/null +++ b/spec/support/action_cable.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.before(:each, type: :channel) do + stub_action_cable_connection + end +end diff --git a/spec/support/helpers/api_helpers.rb b/spec/support/helpers/api_helpers.rb index eb9594a4fb6..b1e6078c4f2 100644 --- a/spec/support/helpers/api_helpers.rb +++ b/spec/support/helpers/api_helpers.rb @@ -40,17 +40,6 @@ module ApiHelpers end end - def basic_auth_header(user = nil) - return { 'HTTP_AUTHORIZATION' => user } unless user.respond_to?(:username) - - { - 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials( - user.username, - create(:personal_access_token, user: user).token - ) - } - end - def expect_empty_array_response expect_successful_response_with_paginated_array expect(json_response.length).to eq(0) diff --git a/spec/support/helpers/design_management_test_helpers.rb b/spec/support/helpers/design_management_test_helpers.rb index bf41e2f5079..1daa92e8ad4 100644 --- a/spec/support/helpers/design_management_test_helpers.rb +++ b/spec/support/helpers/design_management_test_helpers.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true module DesignManagementTestHelpers - def enable_design_management(enabled = true, ref_filter = true) + def enable_design_management(enabled = true) stub_lfs_setting(enabled: enabled) - stub_feature_flags(design_management_reference_filter_gfm_pipeline: ref_filter) end def delete_designs(*designs) diff --git a/spec/support/helpers/filter_spec_helper.rb b/spec/support/helpers/filter_spec_helper.rb index c165128040f..ca844b33ba8 100644 --- a/spec/support/helpers/filter_spec_helper.rb +++ b/spec/support/helpers/filter_spec_helper.rb @@ -56,14 +56,11 @@ module FilterSpecHelper pipeline.call(body) end - def reference_pipeline(context = {}) + def reference_pipeline(filter: described_class, **context) context.reverse_merge!(project: project) if defined?(project) context.reverse_merge!(current_user: current_user) if defined?(current_user) - filters = [ - Banzai::Filter::AutolinkFilter, - described_class - ] + filters = [Banzai::Filter::AutolinkFilter, filter].compact redact = context.delete(:redact) filters.push(Banzai::Filter::ReferenceRedactorFilter) if redact @@ -75,8 +72,13 @@ module FilterSpecHelper reference_pipeline(context).call(body) end - def reference_filter(html, context = {}) - reference_pipeline(context).to_document(html) + def reference_filter(text, context = {}) + reference_pipeline(**context).to_document(text) + end + + # Use to test no-ops + def null_filter(text, context = {}) + reference_pipeline(filter: nil, **context).to_document(text) end # Modify a String reference to make it invalid diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index b3d7f7bcece..87525734490 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -11,9 +11,19 @@ module GraphqlHelpers underscored_field_name.to_s.camelize(:lower) end - # Run a loader's named resolver + # Run a loader's named resolver in a way that closely mimics the framework. + # + # First the `ready?` method is called. If it turns out that the resolver is not + # ready, then the early return is returned instead. + # + # Then the resolve method is called. def resolve(resolver_class, obj: nil, args: {}, ctx: {}, field: nil) - resolver_class.new(object: obj, context: ctx, field: field).resolve(args) + resolver = resolver_class.new(object: obj, context: ctx, field: field) + ready, early_return = sync_all { resolver.ready?(**args) } + + return early_return unless ready + + resolver.resolve(args) end # Eagerly run a loader's named resolver @@ -51,12 +61,12 @@ module GraphqlHelpers # BatchLoader::GraphQL returns a wrapper, so we need to :sync in order # to get the actual values def batch_sync(max_queries: nil, &blk) - wrapper = proc do - lazy_vals = yield - lazy_vals.is_a?(Array) ? lazy_vals.map { |val| sync(val) } : sync(lazy_vals) - end + batch(max_queries: max_queries) { sync_all(&blk) } + end - batch(max_queries: max_queries, &wrapper) + def sync_all(&blk) + lazy_vals = yield + lazy_vals.is_a?(Array) ? lazy_vals.map { |val| sync(val) } : sync(lazy_vals) end def graphql_query_for(name, attributes = {}, fields = nil) @@ -67,10 +77,14 @@ module GraphqlHelpers QUERY end - def graphql_mutation(name, input, fields = nil) + def graphql_mutation(name, input, fields = nil, &block) + raise ArgumentError, 'Please pass either `fields` parameter or a block to `#graphql_mutation`, but not both.' if fields.present? && block_given? + mutation_name = GraphqlHelpers.fieldnamerize(name) input_variable_name = "$#{input_variable_name_for_mutation(name)}" mutation_field = GitlabSchema.mutation.fields[mutation_name] + + fields = yield if block_given? fields ||= all_graphql_fields_for(mutation_field.type.to_graphql) query = <<~MUTATION @@ -139,7 +153,15 @@ module GraphqlHelpers end def wrap_fields(fields) - fields = Array.wrap(fields).join("\n") + fields = Array.wrap(fields).map do |field| + case field + when Symbol + GraphqlHelpers.fieldnamerize(field) + else + field + end + end.join("\n") + return unless fields.present? <<~FIELDS @@ -257,8 +279,13 @@ module GraphqlHelpers end def graphql_dig_at(data, *path) - keys = path.map { |segment| GraphqlHelpers.fieldnamerize(segment) } - data.dig(*keys) + keys = path.map { |segment| segment.is_a?(Integer) ? segment : GraphqlHelpers.fieldnamerize(segment) } + + # Allows for array indexing, like this + # ['project', 'boards', 'edges', 0, 'node', 'lists'] + keys.reduce(data) do |memo, key| + memo.is_a?(Array) ? memo[key] : memo&.dig(key) + end end def graphql_errors @@ -294,6 +321,22 @@ module GraphqlHelpers graphql_data.fetch(GraphqlHelpers.fieldnamerize(mutation_name)) end + def scalar_fields_of(type_name) + GitlabSchema.types[type_name].fields.map do |name, field| + next if nested_fields?(field) || required_arguments?(field) + + name + end.compact + end + + def nested_fields_of(type_name) + GitlabSchema.types[type_name].fields.map do |name, field| + next if !nested_fields?(field) || required_arguments?(field) + + [name, field] + end.compact + end + def nested_fields?(field) !scalar?(field) && !enum?(field) end diff --git a/spec/support/helpers/http_basic_auth_helpers.rb b/spec/support/helpers/http_basic_auth_helpers.rb new file mode 100644 index 00000000000..c0b24b3dfa4 --- /dev/null +++ b/spec/support/helpers/http_basic_auth_helpers.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module HttpBasicAuthHelpers + def user_basic_auth_header(user) + access_token = create(:personal_access_token, user: user) + + basic_auth_header(user.username, access_token.token) + end + + def job_basic_auth_header(job) + basic_auth_header(Ci::Build::CI_REGISTRY_USER, job.token) + end + + def client_basic_auth_header(client) + basic_auth_header(client.uid, client.secret) + end + + def basic_auth_header(username, password) + { + 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials( + username, + password + ) + } + end +end diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb index cb880939b1c..92f6d673255 100644 --- a/spec/support/helpers/login_helpers.rb +++ b/spec/support/helpers/login_helpers.rb @@ -57,13 +57,13 @@ module LoginHelpers def gitlab_sign_in_via(provider, user, uid, saml_response = nil) mock_auth_hash_with_saml_xml(provider, uid, user.email, saml_response) visit new_user_session_path - click_link provider + click_button provider end def gitlab_enable_admin_mode_sign_in_via(provider, user, uid, saml_response = nil) mock_auth_hash_with_saml_xml(provider, uid, user.email, saml_response) visit new_admin_session_path - click_link provider + click_button provider end # Requires Javascript driver. @@ -103,7 +103,7 @@ module LoginHelpers check 'remember_me' if remember_me - click_link "oauth-login-#{provider}" + click_button "oauth-login-#{provider}" end def fake_successful_u2f_authentication diff --git a/spec/support/helpers/markdown_feature.rb b/spec/support/helpers/markdown_feature.rb index eea03fb9325..40e0d4413e2 100644 --- a/spec/support/helpers/markdown_feature.rb +++ b/spec/support/helpers/markdown_feature.rb @@ -36,12 +36,12 @@ class MarkdownFeature end end - def project_wiki - @project_wiki ||= ProjectWiki.new(project, user) + def wiki + @wiki ||= ProjectWiki.new(project, user) end - def project_wiki_page - @project_wiki_page ||= build(:wiki_page, wiki: project_wiki) + def wiki_page + @wiki_page ||= build(:wiki_page, wiki: wiki) end def issue diff --git a/spec/support/helpers/partitioning_helpers.rb b/spec/support/helpers/partitioning_helpers.rb new file mode 100644 index 00000000000..98a13915d76 --- /dev/null +++ b/spec/support/helpers/partitioning_helpers.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module PartitioningHelpers + def expect_table_partitioned_by(table, columns, part_type: :range) + columns_with_part_type = columns.map { |c| [part_type.to_s, c] } + actual_columns = find_partitioned_columns(table) + + expect(columns_with_part_type).to match_array(actual_columns) + end + + def expect_range_partition_of(partition_name, table_name, min_value, max_value) + definition = find_partition_definition(partition_name) + + expect(definition).not_to be_nil + expect(definition['base_table']).to eq(table_name.to_s) + expect(definition['condition']).to eq("FOR VALUES FROM (#{min_value}) TO (#{max_value})") + end + + private + + def find_partitioned_columns(table) + connection.select_rows(<<~SQL) + select + case partstrat + when 'l' then 'list' + when 'r' then 'range' + when 'h' then 'hash' + end as partstrat, + cols.column_name + from ( + select partrelid, partstrat, unnest(partattrs) as col_pos + from pg_partitioned_table + ) pg_part + inner join pg_class + on pg_part.partrelid = pg_class.oid + inner join information_schema.columns cols + on cols.table_name = pg_class.relname + and cols.ordinal_position = pg_part.col_pos + where pg_class.relname = '#{table}'; + SQL + end + + def find_partition_definition(partition) + connection.select_one(<<~SQL) + select + parent_class.relname as base_table, + pg_get_expr(pg_class.relpartbound, inhrelid) as condition + from pg_class + inner join pg_inherits i on pg_class.oid = inhrelid + inner join pg_class parent_class on parent_class.oid = inhparent + where pg_class.relname = '#{partition}' and pg_class.relispartition; + SQL + end +end diff --git a/spec/support/helpers/prometheus_helpers.rb b/spec/support/helpers/prometheus_helpers.rb index fdce00e7dec..d49abbf3f19 100644 --- a/spec/support/helpers/prometheus_helpers.rb +++ b/spec/support/helpers/prometheus_helpers.rb @@ -236,4 +236,51 @@ module PrometheusHelpers ] } end + + def prometheus_alert_payload(firing: [], resolved: []) + status = firing.any? ? 'firing' : 'resolved' + alerts = firing + resolved + alert_name = alerts.first&.title || '' + prometheus_metric_id = alerts.first&.prometheus_metric_id&.to_s + + alerts_map = \ + firing.map { |alert| prometheus_map_alert_payload('firing', alert) } + + resolved.map { |alert| prometheus_map_alert_payload('resolved', alert) } + + # See https://prometheus.io/docs/alerting/configuration/#%3Cwebhook_config%3E + { + 'version' => '4', + 'receiver' => 'gitlab', + 'status' => status, + 'alerts' => alerts_map, + 'groupLabels' => { + 'alertname' => alert_name + }, + 'commonLabels' => { + 'alertname' => alert_name, + 'gitlab' => 'hook', + 'gitlab_alert_id' => prometheus_metric_id + }, + 'commonAnnotations' => {}, + 'externalURL' => '', + 'groupKey' => "{}:{alertname=\'#{alert_name}\'}" + } + end + + private + + def prometheus_map_alert_payload(status, alert) + { + 'status' => status, + 'labels' => { + 'alertname' => alert.title, + 'gitlab' => 'hook', + 'gitlab_alert_id' => alert.prometheus_metric_id.to_s + }, + 'annotations' => {}, + 'startsAt' => '2018-09-24T08:57:31.095725221Z', + 'endsAt' => '0001-01-01T00:00:00Z', + 'generatorURL' => 'http://prometheus-prometheus-server-URL' + } + end end diff --git a/spec/support/helpers/stub_action_cable_connection.rb b/spec/support/helpers/stub_action_cable_connection.rb new file mode 100644 index 00000000000..b4e9c2ae48c --- /dev/null +++ b/spec/support/helpers/stub_action_cable_connection.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module StubActionCableConnection + def stub_action_cable_connection(current_user: nil, request: ActionDispatch::TestRequest.create) + stub_connection(current_user: current_user, request: request) + end +end diff --git a/spec/support/helpers/stub_feature_flags.rb b/spec/support/helpers/stub_feature_flags.rb index 5b8a85b206f..696148cacaf 100644 --- a/spec/support/helpers/stub_feature_flags.rb +++ b/spec/support/helpers/stub_feature_flags.rb @@ -1,6 +1,38 @@ # frozen_string_literal: true module StubFeatureFlags + class StubFeatureGate + attr_reader :flipper_id + + def initialize(flipper_id) + @flipper_id = flipper_id + end + end + + def stub_all_feature_flags + adapter = Flipper::Adapters::Memory.new + flipper = Flipper.new(adapter) + + allow(Feature).to receive(:flipper).and_return(flipper) + + # All new requested flags are enabled by default + allow(Feature).to receive(:enabled?).and_wrap_original do |m, *args| + feature_flag = m.call(*args) + + # If feature flag is not persisted we mark the feature flag as enabled + # We do `m.call` as we want to validate the execution of method arguments + # and a feature flag state if it is not persisted + unless Feature.persisted_name?(args.first) + # TODO: this is hack to support `promo_feature_available?` + # We enable all feature flags by default unless they are `promo_` + # Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/218667 + feature_flag = true unless args.first.to_s.start_with?('promo_') + end + + feature_flag + end + end + # Stub Feature flags with `flag_name: true/false` # # @param [Hash] features where key is feature name and value is boolean whether enabled or not. @@ -14,23 +46,29 @@ module StubFeatureFlags # Enable `ci_live_trace` feature flag only on the specified projects. def stub_feature_flags(features) features.each do |feature_name, actors| - allow(Feature).to receive(:enabled?).with(feature_name, any_args).and_return(false) - allow(Feature).to receive(:enabled?).with(feature_name.to_s, any_args).and_return(false) + # Remove feature flag overwrite + feature = Feature.get(feature_name) # rubocop:disable Gitlab/AvoidFeatureGet + feature.remove Array(actors).each do |actor| raise ArgumentError, "actor cannot be Hash" if actor.is_a?(Hash) - case actor - when false, true - allow(Feature).to receive(:enabled?).with(feature_name, any_args).and_return(actor) - allow(Feature).to receive(:enabled?).with(feature_name.to_s, any_args).and_return(actor) - when nil, ActiveRecord::Base, Symbol, RSpec::Mocks::Double - allow(Feature).to receive(:enabled?).with(feature_name, actor, any_args).and_return(true) - allow(Feature).to receive(:enabled?).with(feature_name.to_s, actor, any_args).and_return(true) + # Control a state of feature flag + if actor == true || actor.nil? || actor.respond_to?(:flipper_id) + feature.enable(actor) + elsif actor == false + feature.disable else - raise ArgumentError, "#stub_feature_flags accepts only `nil`, `true`, `false`, `ActiveRecord::Base` or `Symbol` as actors" + raise ArgumentError, "#stub_feature_flags accepts only `nil`, `bool`, an object responding to `#flipper_id` or including `FeatureGate`." end end end end + + def stub_feature_flag_gate(object) + return if object.nil? + return object if object.is_a?(FeatureGate) + + StubFeatureGate.new(object) + end end diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb index 120d432655b..4da8f760056 100644 --- a/spec/support/helpers/stub_gitlab_calls.rb +++ b/spec/support/helpers/stub_gitlab_calls.rb @@ -141,6 +141,12 @@ module StubGitlabCalls .to_return(status: 200, body: "", headers: {}) end + def stub_webide_config_file(content, sha: anything) + allow_any_instance_of(Repository) + .to receive(:blob_data_at).with(sha, '.gitlab/.gitlab-webide.yml') + .and_return(content) + end + def project_hash_array f = File.read(Rails.root.join('spec/support/gitlab_stubs/projects.json')) Gitlab::Json.parse(f) diff --git a/spec/support/helpers/trigger_helpers.rb b/spec/support/helpers/trigger_helpers.rb new file mode 100644 index 00000000000..fa4f499b900 --- /dev/null +++ b/spec/support/helpers/trigger_helpers.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module TriggerHelpers + def expect_function_to_exist(name) + expect(find_function_def(name)).not_to be_nil + end + + def expect_function_not_to_exist(name) + expect(find_function_def(name)).to be_nil + end + + def expect_function_to_contain(name, *statements) + return_stmt, *body_stmts = parsed_function_statements(name).reverse + + expect(return_stmt).to eq('return old') + expect(body_stmts).to contain_exactly(*statements) + end + + def expect_trigger_not_to_exist(table_name, name) + expect(find_trigger_def(table_name, name)).to be_nil + end + + def expect_valid_function_trigger(table_name, name, fn_name, fires_on) + events, timing, definition = cleaned_trigger_def(table_name, name) + + events = events&.split(',') + expected_timing, expected_events = fires_on.first + expect(timing).to eq(expected_timing.to_s) + expect(events).to match_array(Array.wrap(expected_events)) + expect(definition).to eq("execute procedure #{fn_name}()") + end + + private + + def parsed_function_statements(name) + cleaned_definition = find_function_def(name)['body'].downcase.gsub(/\s+/, ' ') + statements = cleaned_definition.sub(/\A\s*begin\s*(.*)\s*end\s*\Z/, "\\1") + statements.split(';').map! { |stmt| stmt.strip.presence }.compact! + end + + def find_function_def(name) + connection.select_one(<<~SQL) + SELECT prosrc AS body + FROM pg_proc + WHERE proname = '#{name}' + SQL + end + + def cleaned_trigger_def(table_name, name) + find_trigger_def(table_name, name).values_at('event', 'action_timing', 'action_statement').map!(&:downcase) + end + + def find_trigger_def(table_name, name) + connection.select_one(<<~SQL) + SELECT + string_agg(event_manipulation, ',') AS event, + action_timing, + action_statement + FROM information_schema.triggers + WHERE event_object_table = '#{table_name}' + AND trigger_name = '#{name}' + GROUP BY 2, 3 + SQL + end +end diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb index 382e4f6a1a4..f6c415a75bc 100644 --- a/spec/support/helpers/usage_data_helpers.rb +++ b/spec/support/helpers/usage_data_helpers.rb @@ -78,6 +78,7 @@ module UsageDataHelpers labels lfs_objects merge_requests + merge_requests_users milestone_lists milestones notes @@ -117,12 +118,18 @@ module UsageDataHelpers projects_with_expiration_policy_enabled_with_cadence_set_to_14d projects_with_expiration_policy_enabled_with_cadence_set_to_1month projects_with_expiration_policy_enabled_with_cadence_set_to_3month + projects_with_terraform_reports + projects_with_terraform_states pages_domains protected_branches releases remote_mirrors snippets + personal_snippets + project_snippets suggestions + terraform_reports + terraform_states todos uploads web_hooks @@ -157,6 +164,11 @@ module UsageDataHelpers object_store ).freeze + def stub_usage_data_connections + allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false) + allow(Gitlab::Prometheus::Internal).to receive(:prometheus_enabled?).and_return(false) + end + def stub_object_store_settings allow(Settings).to receive(:[]).with('artifacts') .and_return( @@ -209,4 +221,16 @@ module UsageDataHelpers 'proxy_download' => false } } ) end + + def expect_prometheus_api_to(*receive_matchers) + expect_next_instance_of(Gitlab::PrometheusClient) do |client| + receive_matchers.each { |m| expect(client).to m } + end + end + + def allow_prometheus_queries + allow_next_instance_of(Gitlab::PrometheusClient) do |client| + allow(client).to receive(:aggregate).and_return({}) + end + end end diff --git a/spec/support/helpers/wiki_helpers.rb b/spec/support/helpers/wiki_helpers.rb index e6818ff8f0c..ae0d53d1297 100644 --- a/spec/support/helpers/wiki_helpers.rb +++ b/spec/support/helpers/wiki_helpers.rb @@ -8,14 +8,14 @@ module WikiHelpers find('.svg-content .js-lazy-loaded') if example.nil? || example.metadata.key?(:js) end - def upload_file_to_wiki(project, user, file_name) + def upload_file_to_wiki(container, user, file_name) opts = { file_name: file_name, file_content: File.read(expand_fixture_path(file_name)) } ::Wikis::CreateAttachmentService.new( - container: project, + container: container, current_user: user, params: opts ).execute[:result][:file_path] diff --git a/spec/support/import_export/common_util.rb b/spec/support/import_export/common_util.rb index 0069ae81b76..c0c3559cca0 100644 --- a/spec/support/import_export/common_util.rb +++ b/spec/support/import_export/common_util.rb @@ -19,7 +19,7 @@ module ImportExport end def setup_reader(reader) - if reader == :ndjson_reader && Feature.enabled?(:project_import_ndjson) + if reader == :ndjson_reader && Feature.enabled?(:project_import_ndjson, default_enabled: true) allow_any_instance_of(Gitlab::ImportExport::JSON::LegacyReader::File).to receive(:exist?).and_return(false) allow_any_instance_of(Gitlab::ImportExport::JSON::NdjsonReader).to receive(:exist?).and_return(true) else diff --git a/spec/support/let_it_be.rb b/spec/support/let_it_be.rb new file mode 100644 index 00000000000..ade585faaec --- /dev/null +++ b/spec/support/let_it_be.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +TestProf::LetItBe.configure do |config| + config.alias_to :let_it_be_with_refind, refind: true +end + +TestProf::LetItBe.configure do |config| + config.alias_to :let_it_be_with_reload, reload: true +end diff --git a/spec/support/matchers/exceed_query_limit.rb b/spec/support/matchers/exceed_query_limit.rb index b9630b00038..cc0abfa0dd6 100644 --- a/spec/support/matchers/exceed_query_limit.rb +++ b/spec/support/matchers/exceed_query_limit.rb @@ -59,11 +59,15 @@ module ExceedQueryLimitHelpers def verify_count(&block) @subject_block = block - actual_count > expected_count + threshold + actual_count > maximum + end + + def maximum + expected_count + threshold end def failure_message - threshold_message = threshold > 0 ? " (+#{@threshold})" : '' + threshold_message = threshold > 0 ? " (+#{threshold})" : '' counts = "#{expected_count}#{threshold_message}" "Expected a maximum of #{counts} queries, got #{actual_count}:\n\n#{log_message}" end @@ -73,6 +77,55 @@ module ExceedQueryLimitHelpers end end +RSpec::Matchers.define :issue_fewer_queries_than do + supports_block_expectations + + include ExceedQueryLimitHelpers + + def control + block_arg + end + + def control_recorder + @control_recorder ||= ActiveRecord::QueryRecorder.new(&control) + end + + def expected_count + control_recorder.count + end + + def verify_count(&block) + @subject_block = block + + # These blocks need to be evaluated in an expected order, in case + # the events in expected affect the counts in actual + expected_count + actual_count + + actual_count < expected_count + end + + match do |block| + verify_count(&block) + end + + def failure_message + <<~MSG + Expected to issue fewer than #{expected_count} queries, but got #{actual_count} + + #{log_message} + MSG + end + + failure_message_when_negated do |actual| + <<~MSG + Expected query count of #{actual_count} to be less than #{expected_count} + + #{log_message} + MSG + end +end + RSpec::Matchers.define :issue_same_number_of_queries_as do supports_block_expectations @@ -82,30 +135,66 @@ RSpec::Matchers.define :issue_same_number_of_queries_as do block_arg end + chain :or_fewer do + @or_fewer = true + end + + chain :ignoring_cached_queries do + @skip_cached = true + end + def control_recorder @control_recorder ||= ActiveRecord::QueryRecorder.new(&control) end def expected_count - @expected_count ||= control_recorder.count + control_recorder.count end def verify_count(&block) @subject_block = block - (expected_count - actual_count).abs <= threshold + # These blocks need to be evaluated in an expected order, in case + # the events in expected affect the counts in actual + expected_count + actual_count + + if @or_fewer + actual_count <= expected_count + else + (expected_count - actual_count).abs <= threshold + end end match do |block| verify_count(&block) end + def failure_message + <<~MSG + Expected #{expected_count_message} queries, but got #{actual_count} + + #{log_message} + MSG + end + failure_message_when_negated do |actual| - failure_message + <<~MSG + Expected #{actual_count} not to equal #{expected_count_message} + + #{log_message} + MSG + end + + def expected_count_message + or_fewer_msg = "or fewer" if @or_fewer + threshold_msg = "(+/- #{threshold})" unless threshold.zero? + + ["#{expected_count}", or_fewer_msg, threshold_msg].compact.join(' ') end def skip_cached - false + @skip_cached || false end end diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb index 3e2193a9069..7fa06e25405 100644 --- a/spec/support/matchers/graphql_matchers.rb +++ b/spec/support/matchers/graphql_matchers.rb @@ -82,12 +82,30 @@ RSpec::Matchers.define :have_graphql_mutation do |mutation_class| end end +# note: connection arguments do not have to be named, they will be inferred. RSpec::Matchers.define :have_graphql_arguments do |*expected| include GraphqlHelpers + def expected_names(field) + @names ||= Array.wrap(expected).map { |name| GraphqlHelpers.fieldnamerize(name) } + + if field.type.try(:ancestors)&.include?(GraphQL::Types::Relay::BaseConnection) + @names | %w(after before first last) + else + @names + end + end + match do |field| - argument_names = expected.map { |name| GraphqlHelpers.fieldnamerize(name) } - expect(field.arguments.keys).to contain_exactly(*argument_names) + names = expected_names(field) + + expect(field.arguments.keys).to contain_exactly(*names) + end + + failure_message do |field| + names = expected_names(field) + + "expected that #{field.name} would have the following fields: #{names.inspect}, but it has #{field.arguments.keys.inspect}." end end diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb index 1c9f9e5161e..7d011c5eb95 100644 --- a/spec/support/rspec.rb +++ b/spec/support/rspec.rb @@ -4,6 +4,7 @@ require_relative "helpers/stub_configuration" require_relative "helpers/stub_metrics" require_relative "helpers/stub_object_storage" require_relative "helpers/stub_env" +require_relative "helpers/expect_offense" RSpec.configure do |config| config.mock_with :rspec @@ -13,4 +14,6 @@ RSpec.configure do |config| config.include StubMetrics config.include StubObjectStorage config.include StubENV + + config.include ExpectOffense, type: :rubocop end diff --git a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb index 617701abf27..2b8daa80ab4 100644 --- a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb +++ b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb @@ -45,11 +45,32 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests allow_gitaly_n_plus_1 { create(:project, group: subgroup) } end - let!(:merge_request1) { create(:merge_request, assignees: [user], author: user, source_project: project2, target_project: project1, target_branch: 'merged-target') } - let!(:merge_request2) { create(:merge_request, :conflict, assignees: [user], author: user, source_project: project2, target_project: project1, state: 'closed') } - let!(:merge_request3) { create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, state: 'locked', title: 'thing WIP thing') } - let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3, title: 'WIP thing') } - let!(:merge_request5) { create(:merge_request, :simple, author: user, source_project: project4, target_project: project4, title: '[WIP]') } + let!(:merge_request1) do + create(:merge_request, assignees: [user], author: user, + source_project: project2, target_project: project1, + target_branch: 'merged-target') + end + let!(:merge_request2) do + create(:merge_request, :conflict, assignees: [user], author: user, + source_project: project2, target_project: project1, + state: 'closed') + end + let!(:merge_request3) do + create(:merge_request, :simple, author: user, assignees: [user2], + source_project: project2, target_project: project2, + state: 'locked', + title: 'thing WIP thing') + end + let!(:merge_request4) do + create(:merge_request, :simple, author: user, + source_project: project3, target_project: project3, + title: 'WIP thing') + end + let_it_be(:merge_request5) do + create(:merge_request, :simple, author: user, + source_project: project4, target_project: project4, + title: '[WIP]') + end before do project1.add_maintainer(user) diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index fe3c32ec0c5..79b5ff44d4f 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -80,10 +80,13 @@ RSpec.shared_context 'project navbar structure' do nav_sub_items: [] }, { + nav_item: _('Members'), + nav_sub_items: [] + }, + { nav_item: _('Settings'), nav_sub_items: [ _('General'), - _('Members'), _('Integrations'), _('Webhooks'), _('Access Tokens'), diff --git a/spec/support/shared_contexts/project_service_shared_context.rb b/spec/support/shared_contexts/project_service_shared_context.rb index 21d67ea71a8..5b0dd26bd7b 100644 --- a/spec/support/shared_contexts/project_service_shared_context.rb +++ b/spec/support/shared_contexts/project_service_shared_context.rb @@ -5,7 +5,6 @@ shared_context 'project service activation' do let(:user) { create(:user) } before do - stub_feature_flags(integration_form_refactor: false) project.add_maintainer(user) sign_in(user) end diff --git a/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb b/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb new file mode 100644 index 00000000000..f0722beb3ed --- /dev/null +++ b/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +shared_context 'jira projects request context' do + let(:url) { 'https://jira.example.com' } + let(:username) { 'jira-username' } + let(:password) { 'jira-password' } + let!(:jira_service) do + create(:jira_service, + project: project, + url: url, + username: username, + password: password + ) + end + + let_it_be(:jira_projects_json) do + '{ + "self": "https://your-domain.atlassian.net/rest/api/2/project/search?startAt=0&maxResults=2", + "nextPage": "https://your-domain.atlassian.net/rest/api/2/project/search?startAt=2&maxResults=2", + "maxResults": 2, + "startAt": 0, + "total": 7, + "isLast": false, + "values": [ + { + "self": "https://your-domain.atlassian.net/rest/api/2/project/EX", + "id": "10000", + "key": "EX", + "name": "Example", + "avatarUrls": { + "48x48": "https://your-domain.atlassian.net/secure/projectavatar?size=large&pid=10000", + "24x24": "https://your-domain.atlassian.net/secure/projectavatar?size=small&pid=10000", + "16x16": "https://your-domain.atlassian.net/secure/projectavatar?size=xsmall&pid=10000", + "32x32": "https://your-domain.atlassian.net/secure/projectavatar?size=medium&pid=10000" + }, + "projectCategory": { + "self": "https://your-domain.atlassian.net/rest/api/2/projectCategory/10000", + "id": "10000", + "name": "FIRST", + "description": "First Project Category" + }, + "simplified": false, + "style": "classic", + "insight": { + "totalIssueCount": 100, + "lastIssueUpdateTime": "2020-03-31T05:45:24.792+0000" + } + }, + { + "self": "https://your-domain.atlassian.net/rest/api/2/project/ABC", + "id": "10001", + "key": "ABC", + "name": "Alphabetical", + "avatarUrls": { + "48x48": "https://your-domain.atlassian.net/secure/projectavatar?size=large&pid=10001", + "24x24": "https://your-domain.atlassian.net/secure/projectavatar?size=small&pid=10001", + "16x16": "https://your-domain.atlassian.net/secure/projectavatar?size=xsmall&pid=10001", + "32x32": "https://your-domain.atlassian.net/secure/projectavatar?size=medium&pid=10001" + }, + "projectCategory": { + "self": "https://your-domain.atlassian.net/rest/api/2/projectCategory/10000", + "id": "10000", + "name": "FIRST", + "description": "First Project Category" + }, + "simplified": false, + "style": "classic", + "insight": { + "totalIssueCount": 100, + "lastIssueUpdateTime": "2020-03-31T05:45:24.792+0000" + } + } + ] + }' + end + + let_it_be(:empty_jira_projects_json) do + '{ + "self": "https://your-domain.atlassian.net/rest/api/2/project/search?startAt=0&maxResults=2", + "nextPage": "https://your-domain.atlassian.net/rest/api/2/project/search?startAt=2&maxResults=2", + "maxResults": 2, + "startAt": 0, + "total": 7, + "isLast": false, + "values": [] + }' + end + + let(:test_url) { "#{url}/rest/api/2/project/search?maxResults=50&query=&startAt=0" } + let(:start_at_20_url) { "#{url}/rest/api/2/project/search?maxResults=50&query=&startAt=20" } + let(:start_at_1_url) { "#{url}/rest/api/2/project/search?maxResults=50&query=&startAt=1" } + let(:max_results_1_url) { "#{url}/rest/api/2/project/search?maxResults=1&query=&startAt=0" } + + before do + WebMock.stub_request(:get, test_url).with(basic_auth: [username, password]) + .to_return(body: jira_projects_json, headers: { "Content-Type": "application/json" }) + WebMock.stub_request(:get, start_at_20_url).with(basic_auth: [username, password]) + .to_return(body: empty_jira_projects_json, headers: { "Content-Type": "application/json" }) + WebMock.stub_request(:get, start_at_1_url).with(basic_auth: [username, password]) + .to_return(body: jira_projects_json, headers: { "Content-Type": "application/json" }) + WebMock.stub_request(:get, max_results_1_url).with(basic_auth: [username, password]) + .to_return(body: jira_projects_json, headers: { "Content-Type": "application/json" }) + end +end diff --git a/spec/support/shared_contexts/spam_constants.rb b/spec/support/shared_contexts/spam_constants.rb index b6e92ea3050..32371f4b92f 100644 --- a/spec/support/shared_contexts/spam_constants.rb +++ b/spec/support/shared_contexts/spam_constants.rb @@ -1,7 +1,10 @@ # frozen_string_literal: true shared_context 'includes Spam constants' do - REQUIRE_RECAPTCHA = Spam::SpamConstants::REQUIRE_RECAPTCHA - DISALLOW = Spam::SpamConstants::DISALLOW - ALLOW = Spam::SpamConstants::ALLOW + before do + stub_const('CONDITIONAL_ALLOW', Spam::SpamConstants::CONDITIONAL_ALLOW) + stub_const('DISALLOW', Spam::SpamConstants::DISALLOW) + stub_const('ALLOW', Spam::SpamConstants::ALLOW) + stub_const('BLOCK_USER', Spam::SpamConstants::BLOCK_USER) + end end diff --git a/spec/support/shared_examples/controllers/import_controller_new_import_ui_shared_examples.rb b/spec/support/shared_examples/controllers/import_controller_new_import_ui_shared_examples.rb new file mode 100644 index 00000000000..88ad1f6cde2 --- /dev/null +++ b/spec/support/shared_examples/controllers/import_controller_new_import_ui_shared_examples.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'import controller with new_import_ui feature flag' do + include ImportSpecHelper + + context 'with new_import_ui feature flag enabled' do + let(:group) { create(:group) } + + before do + stub_feature_flags(new_import_ui: true) + group.add_owner(user) + end + + it "returns variables for json request" do + project = create(:project, import_type: provider_name, creator_id: user.id) + stub_client(client_repos_field => [repo]) + + get :status, format: :json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) + expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_id) + expect(json_response.dig("namespaces", 0, "id")).to eq(group.id) + end + + it "does not show already added project" do + project = create(:project, import_type: provider_name, namespace: user.namespace, import_status: :finished, import_source: import_source) + stub_client(client_repos_field => [repo]) + + get :status, format: :json + + expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) + expect(json_response.dig("provider_repos")).to eq([]) + end + end +end diff --git a/spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb b/spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb index 2dbaea57c44..62a1a07b6c1 100644 --- a/spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb +++ b/spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb @@ -34,7 +34,7 @@ RSpec.shared_examples 'issuables list meta-data' do |issuable_type, action = nil aggregate_failures do expect(meta_data.keys).to match_array(issuables.map(&:id)) - expect(meta_data.values).to all(be_kind_of(Issuable::IssuableMeta)) + expect(meta_data.values).to all(be_kind_of(Gitlab::IssuableMetadata::IssuableMeta)) end end diff --git a/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb b/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb index d9656824452..925c45005f0 100644 --- a/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb +++ b/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb @@ -2,15 +2,7 @@ RSpec.shared_examples 'milestone tabs' do def go(path, extra_params = {}) - params = - case milestone - when DashboardMilestone - { id: milestone.safe_title, title: milestone.title } - when GroupMilestone - { group_id: group.to_param, id: milestone.safe_title, title: milestone.title } - else - { namespace_id: project.namespace.to_param, project_id: project, id: milestone.iid } - end + params = { namespace_id: project.namespace.to_param, project_id: project, id: milestone.iid } get path, params: params.merge(extra_params) end diff --git a/spec/support/shared_examples/controllers/namespace_storage_limit_alert_shared_examples.rb b/spec/support/shared_examples/controllers/namespace_storage_limit_alert_shared_examples.rb new file mode 100644 index 00000000000..7885eb6c1f8 --- /dev/null +++ b/spec/support/shared_examples/controllers/namespace_storage_limit_alert_shared_examples.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'namespace storage limit alert' do + let(:alert_level) { :info } + + before do + allow_next_instance_of(Namespaces::CheckStorageSizeService, namespace, user) do |check_storage_size_service| + expect(check_storage_size_service).to receive(:execute).and_return( + ServiceResponse.success( + payload: { + alert_level: alert_level, + usage_message: "Usage", + explanation_message: "Explanation", + root_namespace: namespace + } + ) + ) + end + + allow(controller).to receive(:current_user).and_return(user) + end + + render_views + + it 'does render' do + subject + + expect(response.body).to match(/Explanation/) + expect(response.body).to have_css('.js-namespace-storage-alert-dismiss') + end + + context 'when alert_level is error' do + let(:alert_level) { :error } + + it 'does not render a dismiss button' do + subject + + expect(response.body).not_to have_css('.js-namespace-storage-alert-dismiss') + end + end + + context 'when cookie is set' do + before do + cookies["hide_storage_limit_alert_#{namespace.id}_info"] = 'true' + end + + it 'does not render alert' do + subject + + expect(response.body).not_to match(/Explanation/) + end + end +end diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb new file mode 100644 index 00000000000..c128bbe5e02 --- /dev/null +++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb @@ -0,0 +1,302 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'wiki controller actions' do + 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' } + + before do + create(:wiki_page, wiki: wiki, title: wiki_title, content: 'hello world') + + sign_in(user) + end + + describe 'GET #new' do + subject { get :new, params: routing_params } + + it 'redirects to #show and appends a `random_title` param' do + subject + + expect(response).to be_redirect + expect(response.redirect_url).to match(%r{ + #{Regexp.quote(wiki.wiki_base_path)} # wiki base path + /[-\h]{36} # page slug + \?random_title=true\Z # random_title param + }x) + end + + context 'when the wiki repository cannot be created' do + before do + expect(Wiki).to receive(:for_container).and_return(wiki) + expect(wiki).to receive(:wiki) { raise Wiki::CouldNotCreateWikiError } + end + + it 'redirects to the wiki container and displays an error message' do + subject + + expect(response).to redirect_to(container) + expect(flash[:notice]).to eq('Could not create Wiki Repository at this time. Please try again later.') + end + end + end + + describe 'GET #pages' do + before do + get :pages, params: routing_params.merge(id: wiki_title) + end + + it 'assigns the page collections' do + expect(assigns(:wiki_pages)).to contain_exactly(an_instance_of(WikiPage)) + expect(assigns(:wiki_entries)).to contain_exactly(an_instance_of(WikiPage)) + end + + it 'does not load the page content' do + expect(assigns(:page)).to be_nil + end + + it 'does not load the sidebar' do + expect(assigns(:sidebar_wiki_entries)).to be_nil + expect(assigns(:sidebar_limited)).to be_nil + end + end + + describe 'GET #history' do + before do + allow(controller) + .to receive(:can?) + .with(any_args) + .and_call_original + + # The :create_wiki permission is irrelevant to reading history. + expect(controller) + .not_to receive(:can?) + .with(anything, :create_wiki, any_args) + + allow(controller) + .to receive(:can?) + .with(anything, :read_wiki, any_args) + .and_return(allow_read_wiki) + end + + shared_examples 'fetching history' do |expected_status| + before do + get :history, params: routing_params.merge(id: wiki_title) + end + + it "returns status #{expected_status}" do + expect(response).to have_gitlab_http_status(expected_status) + end + end + + it_behaves_like 'fetching history', :ok do + let(:allow_read_wiki) { true } + + it 'assigns @page_versions' do + expect(assigns(:page_versions)).to be_present + end + end + + it_behaves_like 'fetching history', :not_found do + let(:allow_read_wiki) { false } + end + end + + describe 'GET #show' do + render_views + + let(:random_title) { nil } + + subject { get :show, params: routing_params.merge(id: id, random_title: random_title) } + + context 'when page exists' do + let(:id) { wiki_title } + + it 'renders the page' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(assigns(:page).title).to eq(wiki_title) + expect(assigns(:sidebar_wiki_entries)).to contain_exactly(an_instance_of(WikiPage)) + expect(assigns(:sidebar_limited)).to be(false) + end + + context 'when page content encoding is invalid' do + it 'sets flash error' do + allow(controller).to receive(:valid_encoding?).and_return(false) + + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(flash[:notice]).to eq(_('The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.')) + end + end + end + + context 'when the page does not exist' do + let(:id) { 'does not exist' } + + before do + subject + end + + it 'builds a new wiki page with the id as the title' do + expect(assigns(:page).title).to eq(id) + end + + context 'when a random_title param is present' do + let(:random_title) { true } + + it 'builds a new wiki page with no title' do + expect(assigns(:page).title).to be_empty + end + end + end + + context 'when page is a file' do + include WikiHelpers + + let(:id) { upload_file_to_wiki(container, user, file_name) } + + context 'when file is an image' do + let(:file_name) { 'dk.png' } + + it 'delivers the image' do + subject + + expect(response.headers['Content-Disposition']).to match(/^inline/) + expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" + end + + context 'when file is a svg' do + let(:file_name) { 'unsanitized.svg' } + + it 'delivers the image' do + subject + + expect(response.headers['Content-Disposition']).to match(/^inline/) + expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" + end + end + + it_behaves_like 'project cache control headers' do + let(:project) { container } + end + end + + context 'when file is a pdf' do + let(:file_name) { 'git-cheat-sheet.pdf' } + + it 'sets the content type to sets the content response headers' do + subject + + expect(response.headers['Content-Disposition']).to match(/^inline/) + expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" + end + + it_behaves_like 'project cache control headers' do + let(:project) { container } + end + end + end + end + + describe 'POST #preview_markdown' do + it 'renders json in a correct format' do + post :preview_markdown, params: routing_params.merge(id: 'page/path', text: '*Markdown* text') + + expect(json_response.keys).to match_array(%w(body references)) + end + end + + describe 'GET #edit' do + subject { get(:edit, params: routing_params.merge(id: wiki_title)) } + + context 'when page content encoding is invalid' do + it 'redirects to show' do + allow(controller).to receive(:valid_encoding?).and_return(false) + + subject + + expect(response).to redirect_to_wiki(wiki, wiki.list_pages.first) + end + end + + context 'when the page has nil content' do + let(:page) { create(:wiki_page) } + + it 'redirects to show' do + allow(page).to receive(:content).and_return(nil) + allow(controller).to receive(:page).and_return(page) + + subject + + expect(response).to redirect_to_wiki(wiki, page) + end + end + + context 'when page content encoding is valid' do + render_views + + it 'shows the edit page' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to include(s_('Wiki|Edit Page')) + end + end + end + + describe 'PATCH #update' do + let(:new_title) { 'New title' } + let(:new_content) { 'New content' } + + subject do + patch(:update, + params: routing_params.merge( + id: wiki_title, + wiki: { title: new_title, content: new_content } + )) + end + + context 'when page content encoding is invalid' do + it 'redirects to show' do + allow(controller).to receive(:valid_encoding?).and_return(false) + + subject + expect(response).to redirect_to_wiki(wiki, wiki.list_pages.first) + end + end + + context 'when page content encoding is valid' do + render_views + + it 'updates the page' do + subject + + wiki_page = wiki.list_pages(load_content: true).first + + expect(wiki_page.title).to eq new_title + expect(wiki_page.content).to eq new_content + end + end + + context 'when user does not have edit permissions' do + before do + sign_out(:user) + end + + it 'renders the empty state' do + subject + + expect(response).to render_template('shared/wikis/empty') + end + end + end + + def redirect_to_wiki(wiki, page) + redirect_to(controller.wiki_page_path(wiki, page)) + end +end diff --git a/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb b/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb index fb3b17d05ee..e0a032b1a43 100644 --- a/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb +++ b/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb @@ -6,13 +6,14 @@ RSpec.shared_examples 'comment on merge request file' do page.within('.js-discussion-note-form') do fill_in(:note_note, with: 'Line is wrong') - click_button('Comment') + find('.js-comment-button').click end wait_for_requests page.within('.notes_holder') do expect(page).to have_content('Line is wrong') + expect(page).not_to have_content('Comment on lines') end visit(merge_request_path(merge_request)) diff --git a/spec/support/shared_examples/features/discussion_comments_shared_example.rb b/spec/support/shared_examples/features/discussion_comments_shared_example.rb index 81433d124c9..6007798c290 100644 --- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb +++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb @@ -15,7 +15,7 @@ RSpec.shared_examples 'thread comments' do |resource_name| find("#{form_selector} .note-textarea").send_keys(comment) - click_button 'Comment' + find('.js-comment-button').click expect(page).to have_content(comment) @@ -30,6 +30,8 @@ RSpec.shared_examples 'thread comments' do |resource_name| click_button 'Comment & close issue' + wait_for_all_requests + expect(page).to have_content(comment) expect(page).to have_content "@#{user.username} closed" @@ -144,7 +146,7 @@ RSpec.shared_examples 'thread comments' do |resource_name| find("#{comments_selector} .js-vue-discussion-reply").click find("#{comments_selector} .note-textarea").send_keys(text) - click_button "Comment" + find("#{comments_selector} .js-comment-button").click wait_for_requests end diff --git a/spec/support/shared_examples/graphql/container_expiration_policy_shared_examples.rb b/spec/support/shared_examples/graphql/container_expiration_policy_shared_examples.rb new file mode 100644 index 00000000000..9914de7c847 --- /dev/null +++ b/spec/support/shared_examples/graphql/container_expiration_policy_shared_examples.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'exposing container expiration policy option' do |model_option| + it 'exposes all options' do + expect(described_class.values.keys).to contain_exactly(*expected_values) + end + + it 'uses all possible options from model' do + all_options = ContainerExpirationPolicy.public_send("#{model_option}_options").keys + expect(described_class::OPTIONS_MAPPING.keys).to contain_exactly(*all_options) + end +end diff --git a/spec/support/shared_examples/graphql/label_fields.rb b/spec/support/shared_examples/graphql/label_fields.rb new file mode 100644 index 00000000000..b1bfb395bc6 --- /dev/null +++ b/spec/support/shared_examples/graphql/label_fields.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a GraphQL type with labels' do + it 'has label fields' do + expected_fields = %w[label labels] + + expect(described_class).to include_graphql_fields(*expected_fields) + end + + describe 'label field' do + subject { described_class.fields['label'] } + + it { is_expected.to have_graphql_type(Types::LabelType) } + it { is_expected.to have_graphql_arguments(:title) } + end + + describe 'labels field' do + subject { described_class.fields['labels'] } + + it { is_expected.to have_graphql_type(Types::LabelType.connection_type) } + it { is_expected.to have_graphql_arguments(:search_term) } + end +end + +RSpec.shared_examples 'querying a GraphQL type with labels' do + let_it_be(:current_user) { create(:user) } + + let_it_be(:label_a) { create(label_factory, :described, **label_attrs) } + let_it_be(:label_b) { create(label_factory, :described, **label_attrs) } + let_it_be(:label_c) { create(label_factory, :described, :scoped, prefix: 'matching', **label_attrs) } + let_it_be(:label_d) { create(label_factory, :described, :scoped, prefix: 'matching', **label_attrs) } + + let(:label_title) { label_b.title } + + let(:label_params) { { title: label_title } } + let(:labels_params) { nil } + + let(:label_response) { graphql_data.dig(*path_prefix, 'label') } + let(:labels_response) { graphql_data.dig(*path_prefix, 'labels', 'nodes') } + + let(:query) do + make_query( + [ + query_graphql_field(:label, label_params, all_graphql_fields_for(Label)), + query_graphql_field(:labels, labels_params, [ + query_graphql_field(:nodes, nil, all_graphql_fields_for(Label)) + ]) + ] + ) + end + + context 'running a query' do + before do + run_query(query) + end + + context 'minimum required arguments' do + it 'returns the label information' do + expect(label_response).to include( + 'title' => label_title, + 'description' => label_b.description + ) + end + + it 'returns the labels information' do + expect(labels_response.pluck('title')).to contain_exactly( + label_a.title, + label_b.title, + label_c.title, + label_d.title + ) + end + end + + context 'with a search param' do + let(:labels_params) { { search_term: 'matching' } } + + it 'finds the matching labels' do + expect(labels_response.pluck('title')).to contain_exactly( + label_c.title, + label_d.title + ) + end + end + + context 'the label does not exist' do + let(:label_title) { 'not-a-label' } + + it 'returns nil' do + expect(label_response).to be_nil + end + end + end + + describe 'performance' do + def query_for(*labels) + selections = labels.map do |label| + %Q[#{label.title.gsub(/:+/, '_')}: label(title: "#{label.title}") { description }] + end + + make_query(selections) + end + + before do + run_query(query_for(label_a)) + end + + it 'batches queries for labels by title' do + pending('See: https://gitlab.com/gitlab-org/gitlab/-/issues/217767') + + multi_selection = query_for(label_b, label_c) + single_selection = query_for(label_d) + + expect { run_query(multi_selection) } + .to issue_same_number_of_queries_as { run_query(single_selection) } + end + end + + # Run a known good query with the current user + def run_query(query) + post_graphql(query, current_user: current_user) + expect(graphql_errors).not_to be_present + end +end diff --git a/spec/support/shared_examples/graphql/members_shared_examples.rb b/spec/support/shared_examples/graphql/members_shared_examples.rb new file mode 100644 index 00000000000..a58e716efd2 --- /dev/null +++ b/spec/support/shared_examples/graphql/members_shared_examples.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a working membership object query' do |model_option| + let_it_be(:member_source) { member.source } + let_it_be(:member_source_type) { member_source.class.to_s.downcase } + + it 'contains edge to expected project' do + expect( + graphql_data.dig('user', "#{member_source_type}Memberships", 'nodes', 0, member_source_type, 'id') + ).to eq(member.send(member_source_type).to_global_id.to_s) + end + + it 'contains correct access level' do + expect( + graphql_data.dig('user', "#{member_source_type}Memberships", 'nodes', 0, 'accessLevel', 'integerValue') + ).to eq(30) + + expect( + graphql_data.dig('user', "#{member_source_type}Memberships", 'nodes', 0, 'accessLevel', 'stringValue') + ).to eq('DEVELOPER') + end +end diff --git a/spec/support/shared_examples/graphql/resolves_issuable_shared_examples.rb b/spec/support/shared_examples/graphql/resolves_issuable_shared_examples.rb index b56181371c3..58cd3d21f66 100644 --- a/spec/support/shared_examples/graphql/resolves_issuable_shared_examples.rb +++ b/spec/support/shared_examples/graphql/resolves_issuable_shared_examples.rb @@ -22,26 +22,16 @@ RSpec.shared_examples 'resolving an issuable in GraphQL' do |type| .with(full_path: parent.full_path) .and_return(resolved_parent) - expect(resolver_class).to receive(:new) + expect(resolver_class.single).to receive(:new) .with(object: resolved_parent, context: context, field: nil) .and_call_original subject end - it 'uses correct Resolver to resolve issuable parent' do - resolver_class = type == :epic ? 'Resolvers::GroupResolver' : 'Resolvers::ProjectResolver' - - expect(resolver_class.constantize).to receive(:new) - .with(object: nil, context: context, field: nil) - .and_call_original - - subject - end - it 'returns nil if issuable is not found' do result = mutation.resolve_issuable(type: type, parent_path: parent.full_path, iid: "100") - result = type == :merge_request ? result.sync : result + result = result.respond_to?(:sync) ? result.sync : result expect(result).to be_nil end diff --git a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb index fb7e24eecf2..2ef71d275a2 100644 --- a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb +++ b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb @@ -23,7 +23,7 @@ # graphql_query_for( # 'project', # { 'fullPath' => sort_project.full_path }, -# "issues(#{params}) { #{page_info} edges { node { iid weight } } }" +# query_graphql_field('issues', params, "#{page_info} edges { node { id } }") # ) # end # @@ -47,11 +47,13 @@ RSpec.shared_examples 'sorted paginated query' do end describe do - let(:params) { "sort: #{sort_param}" } - let(:start_cursor) { graphql_data_at(*data_path, :pageInfo, :startCursor) } - let(:end_cursor) { graphql_data_at(*data_path, :pageInfo, :endCursor) } - let(:sorted_edges) { graphql_data_at(*data_path, :edges) } - let(:page_info) { "pageInfo { startCursor endCursor }" } + let(:sort_argument) { "sort: #{sort_param}" if sort_param.present? } + let(:first_argument) { "first: #{first_param}" if first_param.present? } + let(:params) { sort_argument } + let(:start_cursor) { graphql_data_at(*data_path, :pageInfo, :startCursor) } + let(:end_cursor) { graphql_data_at(*data_path, :pageInfo, :endCursor) } + let(:sorted_edges) { graphql_data_at(*data_path, :edges) } + let(:page_info) { "pageInfo { startCursor endCursor }" } def pagination_query(params, page_info) raise('pagination_query(params, page_info) must be defined in the test, see example in comment') unless defined?(super) @@ -75,12 +77,12 @@ RSpec.shared_examples 'sorted paginated query' do end context 'when paginating' do - let(:params) { "sort: #{sort_param}, first: #{first_param}" } + let(:params) { [sort_argument, first_argument].compact.join(',') } it 'paginates correctly' do expect(pagination_results_data(sorted_edges)).to eq expected_results.first(first_param) - cursored_query = pagination_query("sort: #{sort_param}, after: \"#{end_cursor}\"", page_info) + cursored_query = pagination_query([sort_argument, "after: \"#{end_cursor}\""].compact.join(','), page_info) post_graphql(cursored_query, current_user: current_user) response_data = graphql_dig_at(Gitlab::Json.parse(response.body), :data, *data_path, :edges) diff --git a/spec/support/shared_examples/integrations/test_examples.rb b/spec/support/shared_examples/integrations/test_examples.rb new file mode 100644 index 00000000000..eb2e83ce5d1 --- /dev/null +++ b/spec/support/shared_examples/integrations/test_examples.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'tests for integration with pipeline data' do + it 'tests the integration with pipeline data' do + create(:ci_empty_pipeline, project: project) + allow(Gitlab::DataBuilder::Pipeline).to receive(:build).and_return(sample_data) + + expect(integration).to receive(:test).with(sample_data).and_return(success_result) + expect(subject).to eq(success_result) + end +end diff --git a/spec/support/shared_examples/lib/gitlab/gl_repository_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/gl_repository_shared_examples.rb new file mode 100644 index 00000000000..97f4341340d --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/gl_repository_shared_examples.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'parsing gl_repository identifier' do + subject { described_class.new(identifier) } + + it 'returns correct information' do + aggregate_failures do + expect(subject.repo_type).to eq(expected_type) + expect(subject.fetch_container!).to eq(expected_container) + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/import/stuck_import_job_workers_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import/stuck_import_job_workers_shared_examples.rb new file mode 100644 index 00000000000..06ea540706a --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/import/stuck_import_job_workers_shared_examples.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +shared_examples 'stuck import job detection' do + context 'when the job has completed' do + context 'when the import status was already updated' do + before do + allow(Gitlab::SidekiqStatus).to receive(:completed_jids) do + import_state.start + import_state.finish + + [import_state.jid] + end + end + + it 'does not mark the import as failed' do + worker.perform + + expect(import_state.reload.status).to eq('finished') + end + end + + context 'when the import status was not updated' do + before do + allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([import_state.jid]) + end + + it 'marks the import as failed' do + worker.perform + + expect(import_state.reload.status).to eq('failed') + end + end + end + + context 'when the job is still in Sidekiq' do + before do + allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([]) + end + + it 'does not mark the import as failed' do + expect { worker.perform }.not_to change { import_state.reload.status } + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/position_formatters_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/position_formatters_shared_examples.rb index c9300aff3e6..326800e6dc2 100644 --- a/spec/support/shared_examples/lib/gitlab/position_formatters_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/position_formatters_shared_examples.rb @@ -32,7 +32,21 @@ RSpec.shared_examples "position formatter" do subject { formatter.to_h } - it { is_expected.to eq(formatter_hash) } + context 'when file_identifier_hash is disabled' do + before do + stub_feature_flags(file_identifier_hash: false) + end + + it { is_expected.to eq(formatter_hash.except(:file_identifier_hash)) } + end + + context 'when file_identifier_hash is enabled' do + before do + stub_feature_flags(file_identifier_hash: true) + end + + it { is_expected.to eq(formatter_hash) } + end end describe '#==' do diff --git a/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb index 69ae9339f10..4aeae788114 100644 --- a/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb @@ -7,26 +7,6 @@ RSpec.shared_examples 'a repo type' do it { is_expected.to eq(expected_identifier) } end - describe '#fetch_id' do - it 'finds an id match in the identifier' do - expect(described_class.fetch_id(expected_identifier)).to eq(expected_id) - end - - it 'does not break on other identifiers' do - expect(described_class.fetch_id('wiki-noid')).to eq(nil) - end - end - - describe '#fetch_container!' do - it 'returns the container' do - expect(described_class.fetch_container!(expected_identifier)).to eq expected_container - end - - it 'raises an exception if the identifier is invalid' do - expect { described_class.fetch_container!('project-noid') }.to raise_error ArgumentError - end - end - describe '#path_suffix' do subject { described_class.path_suffix } diff --git a/spec/support/shared_examples/models/application_setting_shared_examples.rb b/spec/support/shared_examples/models/application_setting_shared_examples.rb index aed85a6630a..01513161d24 100644 --- a/spec/support/shared_examples/models/application_setting_shared_examples.rb +++ b/spec/support/shared_examples/models/application_setting_shared_examples.rb @@ -288,11 +288,37 @@ RSpec.shared_examples 'application settings examples' do end describe '#pick_repository_storage' do - it 'uses Array#sample to pick a random storage' do - array = double('array', sample: 'random') - expect(setting).to receive(:repository_storages).and_return(array) + before do + allow(setting).to receive(:repository_storages_weighted).and_return({ 'default' => 20, 'backup' => 80 }) + end + + it 'chooses repository based on weight' do + picked_storages = { 'default' => 0.0, 'backup' => 0.0 } + 10_000.times { picked_storages[setting.pick_repository_storage] += 1 } + + expect(((picked_storages['default'] / 10_000) * 100).round.to_i).to be_between(19, 21) + expect(((picked_storages['backup'] / 10_000) * 100).round.to_i).to be_between(79, 81) + end + end + + describe '#normalized_repository_storage_weights' do + using RSpec::Parameterized::TableSyntax - expect(setting.pick_repository_storage).to eq('random') + where(:storages, :normalized) do + { 'default' => 0, 'backup' => 100 } | { 'default' => 0.0, 'backup' => 1.0 } + { 'default' => 100, 'backup' => 100 } | { 'default' => 0.5, 'backup' => 0.5 } + { 'default' => 20, 'backup' => 80 } | { 'default' => 0.2, 'backup' => 0.8 } + { 'default' => 0, 'backup' => 0 } | { 'default' => 0.0, 'backup' => 0.0 } + end + + with_them do + before do + allow(setting).to receive(:repository_storages_weighted).and_return(storages) + end + + it 'normalizes storage weights' do + expect(setting.normalized_repository_storage_weights).to eq(normalized) + end end end diff --git a/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb index fa6b0c3afdd..239588d3b2f 100644 --- a/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb @@ -54,7 +54,7 @@ RSpec.shared_examples 'cluster application helm specs' do |application_name| context 'managed_apps_local_tiller feature flag is enabled' do before do - stub_feature_flags(managed_apps_local_tiller: true) + stub_feature_flags(managed_apps_local_tiller: application.cluster.clusterable) end it 'does not include cert files' do diff --git a/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb index 0b21e9a3aa7..7f0c60d4204 100644 --- a/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb @@ -18,7 +18,7 @@ RSpec.shared_examples 'cluster application initial status specs' do context 'local tiller feature flag is enabled' do before do - stub_feature_flags(managed_apps_local_tiller: true) + stub_feature_flags(managed_apps_local_tiller: cluster.clusterable) end it 'sets a default status' do diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb index c2fd04d648b..0efa5e56199 100644 --- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb @@ -66,7 +66,7 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| context 'managed_apps_local_tiller feature flag enabled' do before do - stub_feature_flags(managed_apps_local_tiller: true) + stub_feature_flags(managed_apps_local_tiller: subject.cluster.clusterable) end it 'does not update the helm version' do @@ -197,12 +197,73 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| describe '#make_externally_installed' do subject { create(application_name, :installing) } + let(:old_helm) { create(:clusters_applications_helm, version: '1.2.3') } + it 'is installed' do subject.make_externally_installed expect(subject).to be_installed end + context 'local tiller flag enabled' do + before do + stub_feature_flags(managed_apps_local_tiller: true) + end + + context 'helm record does not exist' do + subject { build(application_name, :installing, :no_helm_installed) } + + it 'does not create a helm record' do + subject.make_externally_installed! + + subject.cluster.reload + expect(subject.cluster.application_helm).to be_nil + end + end + + context 'helm record exists' do + subject { build(application_name, :installing, cluster: old_helm.cluster) } + + it 'does not update helm version' do + subject.make_externally_installed! + + subject.cluster.application_helm.reload + + expect(subject.cluster.application_helm.version).to eq('1.2.3') + end + end + end + + context 'local tiller flag disabled' do + before do + stub_feature_flags(managed_apps_local_tiller: false) + end + + context 'helm record does not exist' do + subject { build(application_name, :installing, :no_helm_installed) } + + it 'creates a helm record' do + subject.make_externally_installed! + + subject.cluster.reload + expect(subject.cluster.application_helm).to be_present + expect(subject.cluster.application_helm).to be_persisted + end + end + + context 'helm record exists' do + subject { build(application_name, :installing, cluster: old_helm.cluster) } + + it 'does not update helm version' do + subject.make_externally_installed! + + subject.cluster.application_helm.reload + + expect(subject.cluster.application_helm.version).to eq('1.2.3') + end + end + end + context 'application is updated' do subject { create(application_name, :updated) } diff --git a/spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb b/spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb deleted file mode 100644 index 76339837351..00000000000 --- a/spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb +++ /dev/null @@ -1,117 +0,0 @@ -# frozen_string_literal: true - -# Include these shared examples in specs of Replicators that include -# BlobReplicatorStrategy. -# -# A let variable called model_record should be defined in the spec. It should be -# a valid, unpersisted instance of the model class. -# -RSpec.shared_examples 'a blob replicator' do - include EE::GeoHelpers - - let_it_be(:primary) { create(:geo_node, :primary) } - let_it_be(:secondary) { create(:geo_node) } - - subject(:replicator) { model_record.replicator } - - before do - stub_current_geo_node(primary) - end - - describe '#handle_after_create_commit' do - it 'creates a Geo::Event' do - expect do - replicator.handle_after_create_commit - end.to change { ::Geo::Event.count }.by(1) - - expect(::Geo::Event.last.attributes).to include( - "replicable_name" => replicator.replicable_name, "event_name" => "created", "payload" => { "model_record_id" => replicator.model_record.id }) - end - - it 'schedules the checksum calculation if needed' do - expect(Geo::BlobVerificationPrimaryWorker).to receive(:perform_async) - expect(replicator).to receive(:needs_checksum?).and_return(true) - - replicator.handle_after_create_commit - end - - it 'does not schedule the checksum calculation if feature flag is disabled' do - stub_feature_flags(geo_self_service_framework: false) - - expect(Geo::BlobVerificationPrimaryWorker).not_to receive(:perform_async) - allow(replicator).to receive(:needs_checksum?).and_return(true) - - replicator.handle_after_create_commit - end - end - - describe '#calculate_checksum!' do - it 'calculates the checksum' do - model_record.save! - - replicator.calculate_checksum! - - expect(model_record.reload.verification_checksum).not_to be_nil - expect(model_record.reload.verified_at).not_to be_nil - end - - it 'saves the error message and increments retry counter' do - model_record.save! - - allow(model_record).to receive(:calculate_checksum!) do - raise StandardError.new('Failure to calculate checksum') - end - - replicator.calculate_checksum! - - expect(model_record.reload.verification_failure).to eq 'Failure to calculate checksum' - expect(model_record.verification_retry_count).to be 1 - end - end - - describe '#consume_created_event' do - it 'invokes Geo::BlobDownloadService' do - service = double(:service) - - expect(service).to receive(:execute) - expect(::Geo::BlobDownloadService).to receive(:new).with(replicator: replicator).and_return(service) - - replicator.consume_event_created - end - end - - describe '#carrierwave_uploader' do - it 'is implemented' do - expect do - replicator.carrierwave_uploader - end.not_to raise_error - end - end - - describe '#model' do - let(:invoke_model) { replicator.class.model } - - it 'is implemented' do - expect do - invoke_model - end.not_to raise_error - end - - it 'is a Class' do - expect(invoke_model).to be_a(Class) - end - - # For convenience (and reliability), instead of asking developers to include shared examples on each model spec as well - context 'replicable model' do - it 'defines #replicator' do - expect(model_record).to respond_to(:replicator) - end - - it 'invokes replicator.handle_after_create_commit on create' do - expect(replicator).to receive(:handle_after_create_commit) - - model_record.save! - end - end - end -end diff --git a/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb b/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb index 30c8c7d0fe5..f37ef3533c3 100644 --- a/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb @@ -148,18 +148,18 @@ RSpec.shared_examples 'model with repository' do expect(subject).to eq('picked') end - it 'picks from the latest available storage', :request_store do + it 'picks from the available storages based on weight', :request_store do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') Gitlab::CurrentSettings.expire_current_application_settings Gitlab::CurrentSettings.current_application_settings settings = ApplicationSetting.last - settings.repository_storages = %w(picked) + settings.repository_storages_weighted = { 'picked' => 100, 'default' => 0 } settings.save! - expect(Gitlab::CurrentSettings.repository_storages).to eq(%w(default)) + expect(Gitlab::CurrentSettings.repository_storages_weighted).to eq({ 'default' => 100 }) expect(subject).to eq('picked') - expect(Gitlab::CurrentSettings.repository_storages).to eq(%w(picked)) + expect(Gitlab::CurrentSettings.repository_storages_weighted).to eq({ 'default' => 0, 'picked' => 100 }) end end end diff --git a/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb index 4bcea36fd42..d21823661f8 100644 --- a/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb @@ -26,7 +26,7 @@ RSpec.shared_examples 'includes Limitable concern' do subject.dup.save end - it 'cannot create new models exceding the plan limits' do + it 'cannot create new models exceeding the plan limits' do expect { subject.save }.not_to change { described_class.count } expect(subject.errors[:base]).to contain_exactly("Maximum number of #{subject.class.limit_name.humanize(capitalize: false)} (1) exceeded") end diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb index 84569e95e11..a881d5f036c 100644 --- a/spec/support/shared_examples/models/wiki_shared_examples.rb +++ b/spec/support/shared_examples/models/wiki_shared_examples.rb @@ -32,6 +32,13 @@ RSpec.shared_examples 'wiki model' do it 'returns the wiki base path' do expect(subject.wiki_base_path).to eq("#{wiki_container.web_url(only_path: true)}/-/wikis") end + + it 'includes the relative URL root' do + allow(Rails.application.routes).to receive(:default_url_options).and_return(script_name: '/root') + + expect(subject.wiki_base_path).to start_with('/root/') + expect(subject.wiki_base_path).not_to start_with('/root/root') + end end describe '#wiki' do diff --git a/spec/support/shared_examples/path_extraction_shared_examples.rb b/spec/support/shared_examples/path_extraction_shared_examples.rb new file mode 100644 index 00000000000..19c6f2404e5 --- /dev/null +++ b/spec/support/shared_examples/path_extraction_shared_examples.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'assigns ref vars' do + it 'assigns the repository var' do + assign_ref_vars + + expect(@repo).to eq container.repository + end + + context 'ref contains %20' do + let(:ref) { 'foo%20bar' } + + it 'is not converted to a space in @id' do + container.repository.add_branch(owner, 'foo%20bar', 'master') + + assign_ref_vars + + expect(@id).to start_with('foo%20bar/') + end + end + + context 'ref contains trailing space' do + let(:ref) { 'master ' } + + it 'strips surrounding space' do + assign_ref_vars + + expect(@ref).to eq('master') + end + end + + context 'ref contains leading space' do + let(:ref) { ' master ' } + + it 'strips surrounding space' do + assign_ref_vars + + expect(@ref).to eq('master') + end + end + + context 'path contains space' do + let(:params) { { path: 'with space', ref: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' } } + + it 'is not converted to %20 in @path' do + assign_ref_vars + + expect(@path).to eq(params[:path]) + end + end + + context 'subclass overrides get_id' do + it 'uses ref returned by get_id' do + allow_next_instance_of(self.class) do |instance| + allow(instance).to receive(:get_id) { '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' } + end + + assign_ref_vars + + expect(@id).to eq(get_id) + end + end +end + +RSpec.shared_examples 'extracts refs' do + describe '#extract_ref' do + it 'returns an empty pair when no repository_container is set' do + allow_any_instance_of(described_class).to receive(:repository_container).and_return(nil) + expect(extract_ref('master/CHANGELOG')).to eq(['', '']) + end + + context 'without a path' do + it 'extracts a valid branch' do + expect(extract_ref('master')).to eq(['master', '']) + end + + it 'extracts a valid tag' do + expect(extract_ref('v2.0.0')).to eq(['v2.0.0', '']) + end + + it 'extracts a valid commit ref without a path' do + expect(extract_ref('f4b14494ef6abf3d144c28e4af0c20143383e062')).to eq( + ['f4b14494ef6abf3d144c28e4af0c20143383e062', ''] + ) + end + + it 'falls back to a primitive split for an invalid ref' do + expect(extract_ref('stable')).to eq(['stable', '']) + end + + it 'extracts the longest matching ref' do + expect(extract_ref('release/app/v1.0.0/README.md')).to eq( + ['release/app/v1.0.0', 'README.md']) + end + end + + context 'with a path' do + it 'extracts a valid branch' do + expect(extract_ref('foo/bar/baz/CHANGELOG')).to eq( + ['foo/bar/baz', 'CHANGELOG']) + end + + it 'extracts a valid tag' do + expect(extract_ref('v2.0.0/CHANGELOG')).to eq(['v2.0.0', 'CHANGELOG']) + end + + it 'extracts a valid commit SHA' do + expect(extract_ref('f4b14494ef6abf3d144c28e4af0c20143383e062/CHANGELOG')).to eq( + %w(f4b14494ef6abf3d144c28e4af0c20143383e062 CHANGELOG) + ) + end + + it 'falls back to a primitive split for an invalid ref' do + expect(extract_ref('stable/CHANGELOG')).to eq(%w(stable CHANGELOG)) + end + end + end +end 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 37a504cd56a..37ee2548dfe 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 @@ -86,7 +86,7 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type| page.within '.time-tracking-component-wrap' do find('.help-button').click - expect(find_link('Learn more')[:href]).to have_content('/help/workflow/time_tracking.md') + expect(find_link('Learn more')[:href]).to have_content('/help/user/project/time_tracking.md') end end end diff --git a/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb b/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb index 0f277c11913..3e058838773 100644 --- a/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb @@ -54,6 +54,29 @@ RSpec.shared_examples 'returns repositories for allowed users' do |user_type, sc expect(response).to match_response_schema('registry/repositories') end end + + context 'with tags_count param' do + let(:url) { "/#{scope}s/#{object.id}/registry/repositories?tags_count=true" } + + before do + stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA latest), with_manifest: true) + stub_container_registry_tags(repository: test_repository.path, tags: %w(rootA latest), with_manifest: true) + end + + it 'returns a list of repositories and their tags_count' do + subject + + expect(response.body).to include('tags_count') + expect(json_response[0]['tags_count']).to eq(2) + end + + it 'returns a matching schema' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('registry/repositories') + end + end end end diff --git a/spec/support/shared_examples/requests/api/diff_discussions_shared_examples.rb b/spec/support/shared_examples/requests/api/diff_discussions_shared_examples.rb index 3d25b9076ad..518c5b8dc28 100644 --- a/spec/support/shared_examples/requests/api/diff_discussions_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/diff_discussions_shared_examples.rb @@ -30,7 +30,9 @@ RSpec.shared_examples 'diff discussions API' do |parent_type, noteable_type, id_ it "creates a new diff note" do line_range = { "start_line_code" => Gitlab::Git.diff_line_code(diff_note.position.file_path, 1, 1), - "end_line_code" => Gitlab::Git.diff_line_code(diff_note.position.file_path, 2, 2) + "end_line_code" => Gitlab::Git.diff_line_code(diff_note.position.file_path, 2, 2), + "start_line_type" => diff_note.position.type, + "end_line_type" => diff_note.position.type } position = diff_note.position.to_h.merge({ line_range: line_range }) diff --git a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb index feb3ba46353..f26af6cb766 100644 --- a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb @@ -45,44 +45,37 @@ RSpec.shared_examples 'group and project boards query' do end describe 'sorting and pagination' do + let(:data_path) { [board_parent_type, :boards] } + + def pagination_query(params, page_info) + graphql_query_for( + board_parent_type, + { 'fullPath' => board_parent.full_path }, + query_graphql_field('boards', params, "#{page_info} edges { node { id } }") + ) + end + + def pagination_results_data(data) + data.map { |board| board.dig('node', 'id') } + end + context 'when using default sorting' do let!(:board_B) { create(:board, resource_parent: board_parent, name: 'B') } let!(:board_C) { create(:board, resource_parent: board_parent, name: 'C') } let!(:board_a) { create(:board, resource_parent: board_parent, name: 'a') } let!(:board_A) { create(:board, resource_parent: board_parent, name: 'A') } - - before do - post_graphql(query, current_user: current_user) - end - - it_behaves_like 'a working graphql query' + let(:boards) { [board_a, board_A, board_B, board_C] } context 'when ascending' do - let(:boards) { [board_a, board_A, board_B, board_C] } - let(:expected_boards) do - if board_parent.multiple_issue_boards_available? - boards - else - [boards.first] - end - end - - it 'sorts boards' do - expect(grab_names).to eq expected_boards.map(&:name) - end - - context 'when paginating' do - let(:params) { 'first: 2' } - - it 'sorts boards' do - expect(grab_names).to eq expected_boards.first(2).map(&:name) - - cursored_query = query("after: \"#{end_cursor}\"") - post_graphql(cursored_query, current_user: current_user) - - response_data = Gitlab::Json.parse(response.body)['data'][board_parent_type]['boards']['edges'] - - expect(grab_names(response_data)).to eq expected_boards.drop(2).first(2).map(&:name) + it_behaves_like 'sorted paginated query' do + let(:sort_param) { } + let(:first_param) { 2 } + let(:expected_results) do + if board_parent.multiple_issue_boards_available? + boards.map { |board| board.to_global_id.to_s } + else + [boards.first.to_global_id.to_s] + end end end end diff --git a/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb b/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb new file mode 100644 index 00000000000..ded381fd402 --- /dev/null +++ b/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'issuable update endpoint' do + let(:area) { entity.class.name.underscore.pluralize } + + describe 'PUT /projects/:id/issues/:issue_id' do + let(:url) { "/projects/#{project.id}/#{area}/#{entity.iid}" } + + it 'clears labels when labels param is nil' do + put api(url, user), params: { labels: 'label1' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['labels']).to contain_exactly('label1') + + put api(url, user), params: { labels: nil } + + expect(response).to have_gitlab_http_status(:ok) + json_response = Gitlab::Json.parse(response.body) + expect(json_response['labels']).to be_empty + end + + it 'updates the issuable with labels param as array' do + stub_const("Gitlab::QueryLimiting::Transaction::THRESHOLD", 110) + + params = { labels: ['label1', 'label2', 'foo, bar', '&,?'] } + + put api(url, user), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['labels']).to include 'label1' + expect(json_response['labels']).to include 'label2' + expect(json_response['labels']).to include 'foo' + expect(json_response['labels']).to include 'bar' + expect(json_response['labels']).to include '&' + expect(json_response['labels']).to include '?' + end + end +end diff --git a/spec/support/shared_examples/requests/api/resource_label_events_api_shared_examples.rb b/spec/support/shared_examples/requests/api/resource_label_events_api_shared_examples.rb index f49f944f38d..675b6c5cef6 100644 --- a/spec/support/shared_examples/requests/api/resource_label_events_api_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/resource_label_events_api_shared_examples.rb @@ -93,6 +93,20 @@ RSpec.shared_examples 'resource_label_events API' do |parent_type, eventable_typ end end + describe 'pagination' do + let!(:event1) { create_event(label) } + let!(:event2) { create_event(label) } + + # https://gitlab.com/gitlab-org/gitlab/-/issues/220192 + it "returns the second page" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events?page=2&per_page=1", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.count).to eq(1) + expect(json_response.first['id']).to eq(event2.id) + end + end + def create_event(label) create(:resource_label_event, eventable.class.name.underscore => eventable, label: label) end diff --git a/spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb b/spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb new file mode 100644 index 00000000000..bca51dab353 --- /dev/null +++ b/spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'resource_milestone_events API' do |parent_type, eventable_type, id_name| + describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_milestone_events" do + let!(:event) { create_event(milestone) } + + it "returns an array of resource milestone events" do + url = "/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events" + get api(url, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['id']).to eq(event.id) + expect(json_response.first['milestone']['id']).to eq(event.milestone.id) + expect(json_response.first['action']).to eq(event.action) + end + + it "returns a 404 error when eventable id not found" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{non_existing_record_id}/resource_milestone_events", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it "returns 404 when not authorized" do + parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + private_user = create(:user) + + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events", private_user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_milestone_events/:event_id" do + let!(:event) { create_event(milestone) } + + it "returns a resource milestone event by id" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events/#{event.id}", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['id']).to eq(event.id) + expect(json_response['milestone']['id']).to eq(event.milestone.id) + expect(json_response['action']).to eq(event.action) + end + + it "returns 404 when not authorized" do + parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + private_user = create(:user) + + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events/#{event.id}", private_user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it "returns a 404 error if resource milestone event not found" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events/#{non_existing_record_id}", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + def create_event(milestone, action: :add) + create(:resource_milestone_event, eventable.class.name.underscore => eventable, milestone: milestone, action: action) + end +end diff --git a/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb b/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb index db5c4b45b70..1ef08de31a9 100644 --- a/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb +++ b/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb @@ -8,7 +8,7 @@ RSpec.shared_examples 'diff file base entity' do :file_hash, :file_path, :old_path, :new_path, :viewer, :diff_refs, :stored_externally, :external_storage, :renamed_file, :deleted_file, - :a_mode, :b_mode, :new_file) + :a_mode, :b_mode, :new_file, :file_identifier_hash) end # Converted diff files from GitHub import does not contain blob file diff --git a/spec/support/shared_examples/serializers/import/import_entity_shared_examples.rb b/spec/support/shared_examples/serializers/import/import_entity_shared_examples.rb new file mode 100644 index 00000000000..6422c4beb1d --- /dev/null +++ b/spec/support/shared_examples/serializers/import/import_entity_shared_examples.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'exposes required fields for import entity' do + describe 'exposes required fields' do + it 'correctly exposes id' do + expect(subject[:id]).to eql(expected_values[:id]) + end + + it 'correctly exposes full name' do + expect(subject[:full_name]).to eql(expected_values[:full_name]) + end + + it 'correctly exposes sanitized name' do + expect(subject[:sanitized_name]).to eql(expected_values[:sanitized_name]) + end + + it 'correctly exposes provider link' do + expect(subject[:provider_link]).to eql(expected_values[:provider_link]) + end + end +end diff --git a/spec/support/shared_examples/services/container_expiration_policy_shared_examples.rb b/spec/support/shared_examples/services/container_expiration_policy_shared_examples.rb new file mode 100644 index 00000000000..28bf46a57d5 --- /dev/null +++ b/spec/support/shared_examples/services/container_expiration_policy_shared_examples.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'updating the container expiration policy attributes' do |mode:, from: {}, to:| + if mode == :create + it 'creates a new container expiration policy' do + expect { subject } + .to change { project.reload.container_expiration_policy.present? }.from(false).to(true) + .and change { ContainerExpirationPolicy.count }.by(1) + end + else + it_behaves_like 'not creating the container expiration policy' + end + + it 'updates the container expiration policy' do + if from.empty? + subject + + expect(container_expiration_policy.reload.cadence).to eq(to[:cadence]) + expect(container_expiration_policy.keep_n).to eq(to[:keep_n]) + expect(container_expiration_policy.older_than).to eq(to[:older_than]) + else + expect { subject } + .to change { container_expiration_policy.reload.cadence }.from(from[:cadence]).to(to[:cadence]) + .and change { container_expiration_policy.reload.keep_n }.from(from[:keep_n]).to(to[:keep_n]) + .and change { container_expiration_policy.reload.older_than }.from(from[:older_than]).to(to[:older_than]) + end + end +end + +RSpec.shared_examples 'not creating the container expiration policy' do + it "doesn't create the container expiration policy" do + expect { subject }.not_to change { ContainerExpirationPolicy.count } + end +end + +RSpec.shared_examples 'creating the container expiration policy' do + it_behaves_like 'updating the container expiration policy attributes', mode: :create, to: { cadence: '3month', keep_n: 100, older_than: '14d' } + + it_behaves_like 'returning a success' +end diff --git a/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb index 71bdd46572f..efcb83a34af 100644 --- a/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb +++ b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb @@ -45,7 +45,7 @@ RSpec.shared_examples 'WikiPages::CreateService#execute' do |container_type| expect { service.execute }.to change { Event.count }.by 1 expect(Event.recent.first).to have_attributes( - action: Event::CREATED, + action: 'created', target: have_attributes(canonical_slug: page_title) ) end diff --git a/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb index 62541eb3da9..1231c012c31 100644 --- a/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb +++ b/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb @@ -27,7 +27,7 @@ RSpec.shared_examples 'WikiPages::DestroyService#execute' do |container_type| expect { service.execute(page) }.to change { Event.count }.by 1 expect(Event.recent.first).to have_attributes( - action: Event::DESTROYED, + action: 'destroyed', target: have_attributes(canonical_slug: page.slug) ) end diff --git a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb index 0dfc99d043b..77354fec069 100644 --- a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb +++ b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb @@ -48,7 +48,7 @@ RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type| expect { service.execute(page) }.to change { Event.count }.by 1 expect(Event.recent.first).to have_attributes( - action: Event::UPDATED, + action: 'updated', wiki_page: page, target_title: page.title ) diff --git a/spec/support/shared_examples/uncached_response_shared_examples.rb b/spec/support/shared_examples/uncached_response_shared_examples.rb new file mode 100644 index 00000000000..3997017ff35 --- /dev/null +++ b/spec/support/shared_examples/uncached_response_shared_examples.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +# +# Pairs with lib/gitlab/no_cache_headers.rb +# + +RSpec.shared_examples 'uncached response' do + it 'defines an uncached header response' do + expect(response.headers["Cache-Control"]).to include("no-store", "no-cache") + expect(response.headers["Pragma"]).to eq("no-cache") + expect(response.headers["Expires"]).to eq("Fri, 01 Jan 1990 00:00:00 GMT") + end +end |