summaryrefslogtreecommitdiff
path: root/spec/support
diff options
context:
space:
mode:
Diffstat (limited to 'spec/support')
-rw-r--r--spec/support/before_all_adapter.rb27
-rw-r--r--spec/support/database/ci_tables.rb22
-rw-r--r--spec/support/database/prevent_cross_database_modification.rb109
-rw-r--r--spec/support/database/prevent_cross_joins.rb77
-rw-r--r--spec/support/database_cleaner.rb6
-rw-r--r--spec/support/database_load_balancing.rb19
-rw-r--r--spec/support/db_cleaner.rb8
-rw-r--r--spec/support/enable_multiple_database_metrics_by_default.rb8
-rw-r--r--spec/support/helpers/board_helpers.rb17
-rw-r--r--spec/support/helpers/dependency_proxy_helpers.rb3
-rw-r--r--spec/support/helpers/features/invite_members_modal_helper.rb12
-rw-r--r--spec/support/helpers/features/top_nav_spec_helpers.rb24
-rw-r--r--spec/support/helpers/graphql_helpers.rb2
-rw-r--r--spec/support/helpers/login_helpers.rb10
-rw-r--r--spec/support/helpers/migrations_helpers.rb17
-rw-r--r--spec/support/helpers/test_env.rb15
-rw-r--r--spec/support/helpers/tracking_helpers.rb7
-rw-r--r--spec/support/helpers/x509_helpers.rb11
-rw-r--r--spec/support/import_export/common_util.rb6
-rw-r--r--spec/support/matchers/background_migrations_matchers.rb30
-rw-r--r--spec/support/matchers/schema_matcher.rb11
-rw-r--r--spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb4
-rw-r--r--spec/support/shared_contexts/fixtures/analytics_shared_context.rb70
-rw-r--r--spec/support/shared_contexts/graphql/requests/packages_shared_context.rb6
-rw-r--r--spec/support/shared_contexts/issuable/merge_request_shared_context.rb1
-rw-r--r--spec/support/shared_contexts/issuable/project_shared_context.rb1
-rw-r--r--spec/support/shared_contexts/lib/api/helpers/packages/dependency_proxy_helpers_shared_context.rb9
-rw-r--r--spec/support/shared_contexts/lib/gitlab/database/background_migration_job_shared_context.rb21
-rw-r--r--spec/support/shared_contexts/load_balancing_configuration_shared_context.rb19
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb9
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb8
-rw-r--r--spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/controllers/unique_visits_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/features/dependency_proxy_shared_examples.rb35
-rw-r--r--spec/support/shared_examples/features/discussion_comments_shared_example.rb4
-rw-r--r--spec/support/shared_examples/features/manage_applications_shared_examples.rb67
-rw-r--r--spec/support/shared_examples/features/packages_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb6
-rw-r--r--spec/support/shared_examples/features/rss_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/variable_list_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/features/wiki/user_views_asciidoc_page_with_includes_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/finders/security/jobs_finder_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/lib/gitlab/ci/reports/security/locations/locations_shared_examples.rb68
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb43
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/lib/menus_shared_examples.rb (renamed from spec/support/shared_examples/helpers/groups_shared_examples.rb)28
-rw-r--r--spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb164
-rw-r--r--spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb246
-rw-r--r--spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb62
-rw-r--r--spec/support/shared_examples/models/mentionable_shared_examples.rb40
-rw-r--r--spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/models/update_project_statistics_shared_examples.rb110
-rw-r--r--spec/support/shared_examples/namespaces/linear_traversal_examples.rb23
-rw-r--r--spec/support/shared_examples/namespaces/traversal_examples.rb86
-rw-r--r--spec/support/shared_examples/namespaces/traversal_scope_examples.rb68
-rw-r--r--spec/support/shared_examples/policies/project_policy_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb178
-rw-r--r--spec/support/shared_examples/services/jira/requests/base_shared_examples.rb85
-rw-r--r--spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb398
75 files changed, 1963 insertions, 571 deletions
diff --git a/spec/support/before_all_adapter.rb b/spec/support/before_all_adapter.rb
new file mode 100644
index 00000000000..f48e0f46e80
--- /dev/null
+++ b/spec/support/before_all_adapter.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class BeforeAllAdapter # rubocop:disable Gitlab/NamespacedClass
+ def self.all_connection_pools
+ ::ActiveRecord::Base.connection_handler.all_connection_pools
+ end
+
+ def self.begin_transaction
+ self.all_connection_pools.each do |connection_pool|
+ connection_pool.connection.begin_transaction(joinable: false)
+ end
+ end
+
+ def self.rollback_transaction
+ self.all_connection_pools.each do |connection_pool|
+ if connection_pool.connection.open_transactions.zero?
+ warn "!!! before_all transaction has been already rollbacked and " \
+ "could work incorrectly"
+ next
+ end
+
+ connection_pool.connection.rollback_transaction
+ end
+ end
+end
+
+TestProf::BeforeAll.adapter = ::BeforeAllAdapter
diff --git a/spec/support/database/ci_tables.rb b/spec/support/database/ci_tables.rb
new file mode 100644
index 00000000000..99fc7ac2501
--- /dev/null
+++ b/spec/support/database/ci_tables.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+# This module stores the CI-related database tables which are
+# going to be moved to a separate database.
+module Database
+ module CiTables
+ def self.include?(name)
+ ci_tables.include?(name)
+ end
+
+ def self.ci_tables
+ @@ci_tables ||= Set.new.tap do |tables| # rubocop:disable Style/ClassVars
+ tables.merge(Ci::ApplicationRecord.descendants.map(&:table_name).compact)
+
+ # It was decided that taggings/tags are best placed with CI
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/333413
+ tables.add('taggings')
+ tables.add('tags')
+ end
+ end
+ end
+end
diff --git a/spec/support/database/prevent_cross_database_modification.rb b/spec/support/database/prevent_cross_database_modification.rb
new file mode 100644
index 00000000000..460ee99391b
--- /dev/null
+++ b/spec/support/database/prevent_cross_database_modification.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+module Database
+ module PreventCrossDatabaseModification
+ CrossDatabaseModificationAcrossUnsupportedTablesError = Class.new(StandardError)
+
+ module GitlabDatabaseMixin
+ def allow_cross_database_modification_within_transaction(url:)
+ cross_database_context = Database::PreventCrossDatabaseModification.cross_database_context
+ return yield unless cross_database_context && cross_database_context[:enabled]
+
+ transaction_tracker_enabled_was = cross_database_context[:enabled]
+ cross_database_context[:enabled] = false
+
+ yield
+ ensure
+ cross_database_context[:enabled] = transaction_tracker_enabled_was if cross_database_context
+ end
+ end
+
+ module SpecHelpers
+ def with_cross_database_modification_prevented
+ subscriber = ActiveSupport::Notifications.subscribe('sql.active_record') do |name, start, finish, id, payload|
+ PreventCrossDatabaseModification.prevent_cross_database_modification!(payload[:connection], payload[:sql])
+ end
+
+ PreventCrossDatabaseModification.reset_cross_database_context!
+ PreventCrossDatabaseModification.cross_database_context.merge!(enabled: true, subscriber: subscriber)
+
+ yield if block_given?
+ ensure
+ cleanup_with_cross_database_modification_prevented if block_given?
+ end
+
+ def cleanup_with_cross_database_modification_prevented
+ ActiveSupport::Notifications.unsubscribe(PreventCrossDatabaseModification.cross_database_context[:subscriber])
+ PreventCrossDatabaseModification.cross_database_context[:enabled] = false
+ end
+ end
+
+ def self.cross_database_context
+ Thread.current[:transaction_tracker]
+ end
+
+ def self.reset_cross_database_context!
+ Thread.current[:transaction_tracker] = initial_data
+ end
+
+ def self.initial_data
+ {
+ enabled: false,
+ transaction_depth_by_db: Hash.new { |h, k| h[k] = 0 },
+ modified_tables_by_db: Hash.new { |h, k| h[k] = Set.new }
+ }
+ end
+
+ def self.prevent_cross_database_modification!(connection, sql)
+ return unless cross_database_context[:enabled]
+
+ database = connection.pool.db_config.name
+
+ if sql.start_with?('SAVEPOINT')
+ cross_database_context[:transaction_depth_by_db][database] += 1
+
+ return
+ elsif sql.start_with?('RELEASE SAVEPOINT', 'ROLLBACK TO SAVEPOINT')
+ cross_database_context[:transaction_depth_by_db][database] -= 1
+ if cross_database_context[:transaction_depth_by_db][database] <= 0
+ cross_database_context[:modified_tables_by_db][database].clear
+ end
+
+ return
+ end
+
+ return if cross_database_context[:transaction_depth_by_db].values.all?(&:zero?)
+
+ tables = PgQuery.parse(sql).dml_tables
+
+ return if tables.empty?
+
+ cross_database_context[:modified_tables_by_db][database].merge(tables)
+
+ all_tables = cross_database_context[:modified_tables_by_db].values.map(&:to_a).flatten
+
+ unless PreventCrossJoins.only_ci_or_only_main?(all_tables)
+ raise Database::PreventCrossDatabaseModification::CrossDatabaseModificationAcrossUnsupportedTablesError,
+ "Cross-database data modification queries (CI and Main) were detected within " \
+ "a transaction '#{all_tables.join(", ")}' discovered"
+ end
+ end
+ end
+end
+
+Gitlab::Database.singleton_class.prepend(
+ Database::PreventCrossDatabaseModification::GitlabDatabaseMixin)
+
+RSpec.configure do |config|
+ config.include(::Database::PreventCrossDatabaseModification::SpecHelpers)
+
+ # Using before and after blocks because the around block causes problems with the let_it_be
+ # record creations. It makes an extra savepoint which breaks the transaction count logic.
+ config.before(:each, :prevent_cross_database_modification) do
+ with_cross_database_modification_prevented
+ end
+
+ config.after(:each, :prevent_cross_database_modification) do
+ cleanup_with_cross_database_modification_prevented
+ end
+end
diff --git a/spec/support/database/prevent_cross_joins.rb b/spec/support/database/prevent_cross_joins.rb
new file mode 100644
index 00000000000..789721ccd38
--- /dev/null
+++ b/spec/support/database/prevent_cross_joins.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+# This module tries to discover and prevent cross-joins across tables
+# This will forbid usage of tables between CI and main database
+# on a same query unless explicitly allowed by. This will change execution
+# from a given point to allow cross-joins. The state will be cleared
+# on a next test run.
+#
+# This method should be used to mark METHOD introducing cross-join
+# not a test using the cross-join.
+#
+# class User
+# def ci_owned_runners
+# ::Gitlab::Database.allow_cross_joins_across_databases!(url: link-to-issue-url)
+#
+# ...
+# end
+# end
+
+module Database
+ module PreventCrossJoins
+ CrossJoinAcrossUnsupportedTablesError = Class.new(StandardError)
+
+ def self.validate_cross_joins!(sql)
+ return if Thread.current[:allow_cross_joins_across_databases]
+
+ # PgQuery might fail in some cases due to limited nesting:
+ # https://github.com/pganalyze/pg_query/issues/209
+ tables = PgQuery.parse(sql).tables
+
+ unless only_ci_or_only_main?(tables)
+ raise CrossJoinAcrossUnsupportedTablesError,
+ "Unsupported cross-join across '#{tables.join(", ")}' discovered " \
+ "when executing query '#{sql}'"
+ end
+ end
+
+ # Returns true if a set includes only CI tables, or includes only non-CI tables
+ def self.only_ci_or_only_main?(tables)
+ tables.all? { |table| CiTables.include?(table) } ||
+ tables.none? { |table| CiTables.include?(table) }
+ end
+
+ module SpecHelpers
+ def with_cross_joins_prevented
+ subscriber = ActiveSupport::Notifications.subscribe('sql.active_record') do |event|
+ ::Database::PreventCrossJoins.validate_cross_joins!(event.payload[:sql])
+ end
+
+ Thread.current[:allow_cross_joins_across_databases] = false
+
+ yield
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+ end
+
+ module GitlabDatabaseMixin
+ def allow_cross_joins_across_databases(url:)
+ Thread.current[:allow_cross_joins_across_databases] = true
+ super
+ end
+ end
+ end
+end
+
+Gitlab::Database.singleton_class.prepend(
+ Database::PreventCrossJoins::GitlabDatabaseMixin)
+
+RSpec.configure do |config|
+ config.include(::Database::PreventCrossJoins::SpecHelpers)
+
+ # TODO: remove `:prevent_cross_joins` to enable the check by default
+ config.around(:each, :prevent_cross_joins) do |example|
+ with_cross_joins_prevented { example.run }
+ end
+end
diff --git a/spec/support/database_cleaner.rb b/spec/support/database_cleaner.rb
index f6339d7343c..6a0e398daa1 100644
--- a/spec/support/database_cleaner.rb
+++ b/spec/support/database_cleaner.rb
@@ -5,10 +5,12 @@ require_relative 'db_cleaner'
RSpec.configure do |config|
include DbCleaner
- # Ensure all sequences are reset at the start of the suite run
+ # Ensure the database is empty at the start of the suite run with :deletion strategy
+ # neither the sequence is reset nor the tables are vacuum, but this provides
+ # better I/O performance on machines with slower storage
config.before(:suite) do
setup_database_cleaner
- DatabaseCleaner.clean_with(:truncation)
+ DatabaseCleaner.clean_with(:deletion)
end
config.append_after(:context, :migration) do
diff --git a/spec/support/database_load_balancing.rb b/spec/support/database_load_balancing.rb
new file mode 100644
index 00000000000..03fa7886295
--- /dev/null
+++ b/spec/support/database_load_balancing.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+ config.before(:each, :db_load_balancing) do
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
+
+ proxy = ::Gitlab::Database::LoadBalancing::ConnectionProxy.new([Gitlab::Database.main.config['host']])
+
+ allow(ActiveRecord::Base).to receive(:load_balancing_proxy).and_return(proxy)
+
+ ::Gitlab::Database::LoadBalancing::Session.clear_session
+ redis_shared_state_cleanup!
+ end
+
+ config.after(:each, :db_load_balancing) do
+ ::Gitlab::Database::LoadBalancing::Session.clear_session
+ redis_shared_state_cleanup!
+ end
+end
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index ff913ebf22b..155dc3c17d9 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
module DbCleaner
+ def all_connection_classes
+ ::ActiveRecord::Base.connection_handler.connection_pool_names.map(&:constantize)
+ end
+
def delete_from_all_tables!(except: [])
except << 'ar_internal_metadata'
@@ -12,7 +16,9 @@ module DbCleaner
end
def setup_database_cleaner
- DatabaseCleaner[:active_record, { connection: ActiveRecord::Base }]
+ all_connection_classes.each do |connection_class|
+ DatabaseCleaner[:active_record, { connection: connection_class }]
+ end
end
end
diff --git a/spec/support/enable_multiple_database_metrics_by_default.rb b/spec/support/enable_multiple_database_metrics_by_default.rb
new file mode 100644
index 00000000000..6eeb4acd3d6
--- /dev/null
+++ b/spec/support/enable_multiple_database_metrics_by_default.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+ config.before do
+ # Enable this by default in all tests so it behaves like a FF
+ stub_env('GITLAB_MULTIPLE_DATABASE_METRICS', '1')
+ end
+end
diff --git a/spec/support/helpers/board_helpers.rb b/spec/support/helpers/board_helpers.rb
index c4e69d06f52..d7277ba9a20 100644
--- a/spec/support/helpers/board_helpers.rb
+++ b/spec/support/helpers/board_helpers.rb
@@ -23,4 +23,21 @@ module BoardHelpers
wait_for_requests
end
end
+
+ def drag(selector: '.board-list', list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, perform_drop: true)
+ inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
+ # ensure there is enough horizontal space for four board lists
+ resize_window(2000, 800)
+
+ drag_to(selector: selector,
+ scrollable: '#board-app',
+ list_from_index: list_from_index,
+ from_index: from_index,
+ to_index: to_index,
+ list_to_index: list_to_index,
+ perform_drop: perform_drop)
+ end
+
+ wait_for_requests
+ end
end
diff --git a/spec/support/helpers/dependency_proxy_helpers.rb b/spec/support/helpers/dependency_proxy_helpers.rb
index 0d8f56906e3..9413cb93199 100644
--- a/spec/support/helpers/dependency_proxy_helpers.rb
+++ b/spec/support/helpers/dependency_proxy_helpers.rb
@@ -34,7 +34,8 @@ module DependencyProxyHelpers
def build_jwt(user = nil, expire_time: nil)
JSONWebToken::HMACToken.new(::Auth::DependencyProxyAuthenticationService.secret).tap do |jwt|
- jwt['user_id'] = user.id if user
+ jwt['user_id'] = user.id if user.is_a?(User)
+ jwt['deploy_token'] = user.token if user.is_a?(DeployToken)
jwt.expire_time = expire_time || jwt.issued_at + 1.minute
end
end
diff --git a/spec/support/helpers/features/invite_members_modal_helper.rb b/spec/support/helpers/features/invite_members_modal_helper.rb
index 7b8cd6963c0..69ba20c1ca4 100644
--- a/spec/support/helpers/features/invite_members_modal_helper.rb
+++ b/spec/support/helpers/features/invite_members_modal_helper.rb
@@ -5,7 +5,7 @@ module Spec
module Helpers
module Features
module InviteMembersModalHelper
- def invite_member(name, role: 'Guest', expires_at: nil)
+ def invite_member(name, role: 'Guest', expires_at: nil, area_of_focus: false)
click_on 'Invite members'
page.within '#invite-members-modal' do
@@ -14,6 +14,7 @@ module Spec
wait_for_requests
click_button name
choose_options(role, expires_at)
+ choose_area_of_focus if area_of_focus
click_button 'Invite'
@@ -41,7 +42,14 @@ module Spec
click_button role
end
- fill_in 'YYYY-MM-DD', with: expires_at.try(:strftime, '%Y-%m-%d')
+ fill_in 'YYYY-MM-DD', with: expires_at.strftime('%Y-%m-%d') if expires_at
+ end
+
+ def choose_area_of_focus
+ page.within '[data-testid="area-of-focus-checks"]' do
+ check 'Contribute to the codebase'
+ check 'Collaborate on open issues and merge requests'
+ end
end
end
end
diff --git a/spec/support/helpers/features/top_nav_spec_helpers.rb b/spec/support/helpers/features/top_nav_spec_helpers.rb
index 87ed897ec74..de495eceabc 100644
--- a/spec/support/helpers/features/top_nav_spec_helpers.rb
+++ b/spec/support/helpers/features/top_nav_spec_helpers.rb
@@ -8,38 +8,24 @@ module Spec
module Features
module TopNavSpecHelpers
def open_top_nav
- return unless Feature.enabled?(:combined_menu, default_enabled: :yaml)
-
find('.js-top-nav-dropdown-toggle').click
end
def within_top_nav
- if Feature.enabled?(:combined_menu, default_enabled: :yaml)
- within('.js-top-nav-dropdown-menu') do
- yield
- end
- else
- within('.navbar-sub-nav') do
- yield
- end
+ within('.js-top-nav-dropdown-menu') do
+ yield
end
end
def open_top_nav_projects
- if Feature.enabled?(:combined_menu, default_enabled: :yaml)
- open_top_nav
+ open_top_nav
- within_top_nav do
- click_button('Projects')
- end
- else
- find('#nav-projects-dropdown').click
+ within_top_nav do
+ click_button('Projects')
end
end
def open_top_nav_groups
- return unless Feature.enabled?(:combined_menu, default_enabled: :yaml)
-
open_top_nav
within_top_nav do
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 38cf828ca5e..6f17d3cb496 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -654,7 +654,7 @@ module GraphqlHelpers
Class.new(Types::BaseObject) do
graphql_name 'TestType'
- field :name, GraphQL::STRING_TYPE, null: true
+ field :name, GraphQL::Types::String, null: true
yield(self) if block_given?
end
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index cc88a3fc71e..d9157fa7485 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -88,9 +88,10 @@ module LoginHelpers
# Private: Login as the specified user
#
- # user - User instance to login with
+ # user - User instance to login with
# remember - Whether or not to check "Remember me" (default: false)
- def gitlab_sign_in_with(user, remember: false)
+ # two_factor_auth - If two-factor authentication is enabled (default: false)
+ def gitlab_sign_in_with(user, remember: false, two_factor_auth: false)
visit new_user_session_path
fill_in "user_login", with: user.email
@@ -98,6 +99,11 @@ module LoginHelpers
check 'user_remember_me' if remember
click_button "Sign in"
+
+ if two_factor_auth
+ fill_in "user_otp_attempt", with: user.reload.current_otp
+ click_button "Verify code"
+ end
end
def login_via(provider, user, uid, remember_me: false, additional_info: {})
diff --git a/spec/support/helpers/migrations_helpers.rb b/spec/support/helpers/migrations_helpers.rb
index fa50b234bd5..ef212938af5 100644
--- a/spec/support/helpers/migrations_helpers.rb
+++ b/spec/support/helpers/migrations_helpers.rb
@@ -16,6 +16,23 @@ module MigrationsHelpers
end
end
+ def partitioned_table(name, by: :created_at, strategy: :monthly)
+ klass = Class.new(active_record_base) do
+ include PartitionedTable
+
+ self.table_name = name
+ self.primary_key = :id
+
+ partitioned_by by, strategy: strategy
+
+ def self.name
+ table_name.singularize.camelcase
+ end
+ end
+
+ klass.tap { Gitlab::Database::Partitioning::PartitionManager.new.sync_partitions }
+ end
+
def migrations_paths
ActiveRecord::Migrator.migrations_paths
end
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 8814d260fb3..aa5fcf222f2 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -158,7 +158,7 @@ module TestEnv
component_timed_setup('Gitaly',
install_dir: gitaly_dir,
version: Gitlab::GitalyClient.expected_server_version,
- task: "gitlab:gitaly:install",
+ task: "gitlab:gitaly:test_install",
task_args: [gitaly_dir, repos_path, gitaly_url].compact) do
Gitlab::SetupHelper::Gitaly.create_configuration(
gitaly_dir,
@@ -263,8 +263,13 @@ module TestEnv
# Feature specs are run through Workhorse
def setup_workhorse
+ # Always rebuild the config file
+ if skip_compile_workhorse?
+ Gitlab::SetupHelper::Workhorse.create_configuration(workhorse_dir, nil, force: true)
+ return
+ end
+
start = Time.now
- return if skip_compile_workhorse?
FileUtils.rm_rf(workhorse_dir)
Gitlab::SetupHelper::Workhorse.compile_into(workhorse_dir)
@@ -305,12 +310,6 @@ module TestEnv
config_path = Gitlab::SetupHelper::Workhorse.get_config_path(workhorse_dir, {})
- # This should be set up in setup_workhorse, but since
- # component_needs_update? only checks that versions are consistent,
- # we need to ensure the config file exists. This line can be removed
- # later after a new Workhorse version is updated.
- Gitlab::SetupHelper::Workhorse.create_configuration(workhorse_dir, nil) unless File.exist?(config_path)
-
workhorse_pid = spawn(
{ 'PATH' => "#{ENV['PATH']}:#{workhorse_dir}" },
File.join(workhorse_dir, 'gitlab-workhorse'),
diff --git a/spec/support/helpers/tracking_helpers.rb b/spec/support/helpers/tracking_helpers.rb
new file mode 100644
index 00000000000..c0374578531
--- /dev/null
+++ b/spec/support/helpers/tracking_helpers.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module TrackingHelpers
+ def stub_do_not_track(value)
+ request.headers['DNT'] = value
+ end
+end
diff --git a/spec/support/helpers/x509_helpers.rb b/spec/support/helpers/x509_helpers.rb
index ce0fa268ace..1dc8b1d4845 100644
--- a/spec/support/helpers/x509_helpers.rb
+++ b/spec/support/helpers/x509_helpers.rb
@@ -290,6 +290,17 @@ module X509Helpers
SIGNEDDATA
end
+ def unsigned_tag_base_data
+ <<~SIGNEDDATA
+ object 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ type commit
+ tag v1.0.0
+ tagger Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> 1393491299 +0200
+
+ Release
+ SIGNEDDATA
+ end
+
def certificate_crl
'http://ch.siemens.com/pki?ZZZZZZA2.crl'
end
diff --git a/spec/support/import_export/common_util.rb b/spec/support/import_export/common_util.rb
index 5fb6af99b79..1aa20dab6f8 100644
--- a/spec/support/import_export/common_util.rb
+++ b/spec/support/import_export/common_util.rb
@@ -83,7 +83,7 @@ module ImportExport
path = File.join(dir_path, "#{exportable_path}.json")
return unless File.exist?(path)
- ActiveSupport::JSON.decode(IO.read(path))
+ Gitlab::Json.parse(IO.read(path))
end
def consume_relations(dir_path, exportable_path, key)
@@ -93,7 +93,7 @@ module ImportExport
relations = []
File.foreach(path) do |line|
- json = ActiveSupport::JSON.decode(line)
+ json = Gitlab::Json.parse(line)
relations << json
end
@@ -101,7 +101,7 @@ module ImportExport
end
def project_json(filename)
- ActiveSupport::JSON.decode(IO.read(filename))
+ Gitlab::Json.parse(IO.read(filename))
end
end
end
diff --git a/spec/support/matchers/background_migrations_matchers.rb b/spec/support/matchers/background_migrations_matchers.rb
index 08bbbcc7438..d3833a1e8e8 100644
--- a/spec/support/matchers/background_migrations_matchers.rb
+++ b/spec/support/matchers/background_migrations_matchers.rb
@@ -64,3 +64,33 @@ RSpec::Matchers.define :be_scheduled_migration_with_multiple_args do |*expected|
arg.sort == expected.sort
end
end
+
+RSpec::Matchers.define :have_scheduled_batched_migration do |table_name: nil, column_name: nil, job_arguments: [], **attributes|
+ define_method :matches? do |migration|
+ # Default arguments passed by BatchedMigrationWrapper (values don't matter here)
+ expect(migration).to be_background_migration_with_arguments([
+ _start_id = 1,
+ _stop_id = 2,
+ table_name,
+ column_name,
+ _sub_batch_size = 10,
+ _pause_ms = 100,
+ *job_arguments
+ ])
+
+ batched_migrations =
+ Gitlab::Database::BackgroundMigration::BatchedMigration
+ .for_configuration(migration, table_name, column_name, job_arguments)
+
+ expect(batched_migrations.count).to be(1)
+ expect(batched_migrations).to all(have_attributes(attributes)) if attributes.present?
+ end
+
+ define_method :does_not_match? do |migration|
+ batched_migrations =
+ Gitlab::Database::BackgroundMigration::BatchedMigration
+ .where(job_class_name: migration)
+
+ expect(batched_migrations.count).to be(0)
+ end
+end
diff --git a/spec/support/matchers/schema_matcher.rb b/spec/support/matchers/schema_matcher.rb
index 94e4359b1dd..5e08e96f4e1 100644
--- a/spec/support/matchers/schema_matcher.rb
+++ b/spec/support/matchers/schema_matcher.rb
@@ -45,6 +45,17 @@ RSpec::Matchers.define :match_response_schema do |schema, dir: nil, **options|
end
end
+RSpec::Matchers.define :match_metric_definition_schema do |path, dir: nil, **options|
+ match do |data|
+ schema_path = Pathname.new(Rails.root.join(dir.to_s, path).to_s)
+ validator = SchemaPath.validator(schema_path)
+
+ data = data.stringify_keys if data.is_a? Hash
+
+ validator.valid?(data)
+ end
+end
+
RSpec::Matchers.define :match_snowplow_schema do |schema, dir: nil, **options|
match do |data|
schema_path = Pathname.new(Rails.root.join(dir.to_s, 'spec', "fixtures/product_intelligence/#{schema}.json").to_s)
diff --git a/spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb
index b10844320d0..07012914a4d 100644
--- a/spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb
+++ b/spec/support/shared_contexts/features/integrations/project_integrations_shared_context.rb
@@ -3,8 +3,8 @@
RSpec.shared_context 'project service activation' do
include_context 'integration activation'
- let(:project) { create(:project) }
- let(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
before do
project.add_maintainer(user)
diff --git a/spec/support/shared_contexts/fixtures/analytics_shared_context.rb b/spec/support/shared_contexts/fixtures/analytics_shared_context.rb
new file mode 100644
index 00000000000..13d3697a378
--- /dev/null
+++ b/spec/support/shared_contexts/fixtures/analytics_shared_context.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'Analytics fixtures shared context' do
+ include JavaScriptFixturesHelpers
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :repository, namespace: group) }
+ let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:milestone) { create(:milestone, project: project) }
+
+ let(:issue) { create(:issue, project: project, created_at: 4.days.ago) }
+ let(:issue_1) { create(:issue, project: project, created_at: 5.days.ago) }
+ let(:issue_2) { create(:issue, project: project, created_at: 4.days.ago, milestone: milestone) }
+ let(:issue_3) { create(:issue, project: project, created_at: 3.days.ago, milestone: milestone) }
+
+ let(:mr_1) { create(:merge_request, source_project: project, allow_broken: true, created_at: 20.days.ago) }
+ let(:mr_2) { create(:merge_request, source_project: project, allow_broken: true, created_at: 19.days.ago) }
+
+ let(:pipeline_1) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr_1.source_branch, sha: mr_1.source_branch_sha, head_pipeline_of: mr_1) }
+ let(:pipeline_2) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr_2.source_branch, sha: mr_2.source_branch_sha, head_pipeline_of: mr_2) }
+
+ let(:build_1) { create(:ci_build, :success, pipeline: pipeline_1, author: user) }
+ let(:build_2) { create(:ci_build, :success, pipeline: pipeline_2, author: user) }
+
+ let(:params) { { created_after: 3.months.ago, created_before: Time.now, group_id: group.full_path } }
+
+ def prepare_cycle_analytics_data
+ group.add_maintainer(user)
+ project.add_maintainer(user)
+
+ create_commit_referencing_issue(issue_1)
+ create_commit_referencing_issue(issue_2)
+
+ create_merge_request_closing_issue(user, project, issue_1)
+ create_merge_request_closing_issue(user, project, issue_2)
+ merge_merge_requests_closing_issue(user, project, issue_3)
+ end
+
+ def create_deployment
+ deploy_master(user, project, environment: 'staging')
+ deploy_master(user, project)
+ end
+
+ def update_metrics
+ issue_1.metrics.update!(first_added_to_board_at: 3.days.ago, first_mentioned_in_commit_at: 2.days.ago)
+ issue_2.metrics.update!(first_added_to_board_at: 2.days.ago, first_mentioned_in_commit_at: 1.day.ago)
+
+ mr_1.metrics.update!({
+ merged_at: 5.days.ago,
+ first_deployed_to_production_at: 1.day.ago,
+ latest_build_started_at: 5.days.ago,
+ latest_build_finished_at: 1.day.ago,
+ pipeline: build_1.pipeline
+ })
+
+ mr_2.metrics.update!({
+ merged_at: 10.days.ago,
+ first_deployed_to_production_at: 5.days.ago,
+ latest_build_started_at: 9.days.ago,
+ latest_build_finished_at: 7.days.ago,
+ pipeline: build_2.pipeline
+ })
+ end
+
+ before do
+ stub_licensed_features(cycle_analytics_for_groups: true)
+
+ prepare_cycle_analytics_data
+ end
+end
diff --git a/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb b/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb
index 334b11c9f6e..645ea742f07 100644
--- a/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb
+++ b/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb
@@ -9,6 +9,7 @@ RSpec.shared_context 'package details setup' do
let(:depth) { 3 }
let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
let(:package_files) { all_graphql_fields_for('PackageFile') }
+ let(:dependency_links) { all_graphql_fields_for('PackageDependencyLink') }
let(:user) { project.owner }
let(:package_details) { graphql_data_at(:package) }
let(:metadata_response) { graphql_data_at(:package, :metadata) }
@@ -28,6 +29,11 @@ RSpec.shared_context 'package details setup' do
#{package_files}
}
}
+ dependencyLinks {
+ nodes {
+ #{dependency_links}
+ }
+ }
FIELDS
end
end
diff --git a/spec/support/shared_contexts/issuable/merge_request_shared_context.rb b/spec/support/shared_contexts/issuable/merge_request_shared_context.rb
index debcd9a3054..2c56411ca4c 100644
--- a/spec/support/shared_contexts/issuable/merge_request_shared_context.rb
+++ b/spec/support/shared_contexts/issuable/merge_request_shared_context.rb
@@ -11,6 +11,7 @@ RSpec.shared_context 'merge request show action' do
before do
allow(view).to receive(:experiment_enabled?).and_return(false)
allow(view).to receive(:current_user).and_return(user)
+ allow(view).to receive(:can_admin_project_member?)
assign(:project, project)
assign(:merge_request, merge_request)
assign(:note, note)
diff --git a/spec/support/shared_contexts/issuable/project_shared_context.rb b/spec/support/shared_contexts/issuable/project_shared_context.rb
index 5e5f6f2b7a6..b1bb9d80d78 100644
--- a/spec/support/shared_contexts/issuable/project_shared_context.rb
+++ b/spec/support/shared_contexts/issuable/project_shared_context.rb
@@ -12,5 +12,6 @@ RSpec.shared_context 'project show action' do
stub_template 'shared/issuable/_sidebar' => ''
stub_template 'projects/issues/_discussion' => ''
allow(view).to receive(:user_status).and_return('')
+ allow(view).to receive(:can_admin_project_member?)
end
end
diff --git a/spec/support/shared_contexts/lib/api/helpers/packages/dependency_proxy_helpers_shared_context.rb b/spec/support/shared_contexts/lib/api/helpers/packages/dependency_proxy_helpers_shared_context.rb
new file mode 100644
index 00000000000..7c8b6250d24
--- /dev/null
+++ b/spec/support/shared_contexts/lib/api/helpers/packages/dependency_proxy_helpers_shared_context.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'dependency proxy helpers context' do
+ def allow_fetch_application_setting(attribute:, return_value:)
+ attributes = double
+ allow(::Gitlab::CurrentSettings.current_application_settings).to receive(:attributes).and_return(attributes)
+ allow(attributes).to receive(:fetch).with(attribute, false).and_return(return_value)
+ end
+end
diff --git a/spec/support/shared_contexts/lib/gitlab/database/background_migration_job_shared_context.rb b/spec/support/shared_contexts/lib/gitlab/database/background_migration_job_shared_context.rb
new file mode 100644
index 00000000000..382eb796f8e
--- /dev/null
+++ b/spec/support/shared_contexts/lib/gitlab/database/background_migration_job_shared_context.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'background migration job class' do
+ let!(:job_class_name) { 'TestJob' }
+ let!(:job_class) { Class.new }
+ let!(:job_perform_method) do
+ ->(*arguments) do
+ Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
+ # Value is 'TestJob' defined by :job_class_name in the let! above.
+ # Scoping prohibits us from directly referencing job_class_name.
+ RSpec.current_example.example_group_instance.job_class_name,
+ arguments
+ )
+ end
+ end
+
+ before do
+ job_class.define_method(:perform, job_perform_method)
+ expect(Gitlab::BackgroundMigration).to receive(:migration_class_for).with(job_class_name).at_least(:once) { job_class }
+ end
+end
diff --git a/spec/support/shared_contexts/load_balancing_configuration_shared_context.rb b/spec/support/shared_contexts/load_balancing_configuration_shared_context.rb
deleted file mode 100644
index a61b8e9a074..00000000000
--- a/spec/support/shared_contexts/load_balancing_configuration_shared_context.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_context 'clear DB Load Balancing configuration' do
- def clear_load_balancing_configuration
- proxy = ::Gitlab::Database::LoadBalancing.instance_variable_get(:@proxy)
- proxy.load_balancer.release_host if proxy
- ::Gitlab::Database::LoadBalancing.instance_variable_set(:@proxy, nil)
-
- ::Gitlab::Database::LoadBalancing::Session.clear_session
- end
-
- around do |example|
- clear_load_balancing_configuration
-
- example.run
-
- clear_load_balancing_configuration
- end
-end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index b7eb03de8f0..8ae0885056e 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -176,6 +176,15 @@ RSpec.shared_context 'group navbar structure' do
}
end
+ let(:ci_cd_nav_item) do
+ {
+ nav_item: _('CI/CD'),
+ nav_sub_items: [
+ s_('Runners|Runners')
+ ]
+ }
+ end
+
let(:issues_nav_items) do
[
_('List'),
diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
index b0d7274269b..b432aa24bb8 100644
--- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -38,12 +38,14 @@ RSpec.shared_context 'GroupPolicy context' do
delete_metrics_dashboard_annotation
update_metrics_dashboard_annotation
create_custom_emoji
+ create_package
create_package_settings
]
end
let(:maintainer_permissions) do
%i[
+ destroy_package
create_projects
read_cluster create_cluster update_cluster admin_cluster add_cluster
]
diff --git a/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb b/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb
index c69a987c00d..b90270356f8 100644
--- a/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb
+++ b/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb
@@ -11,7 +11,7 @@ RSpec.shared_context 'conan api setup' do
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let(:project) { package.project }
- let(:job) { create(:ci_build, :running, user: user, project: project) }
+ let(:job) { create(:ci_build, :running, user: user) }
let(:job_token) { job.token }
let(:auth_token) { personal_access_token.token }
let(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
diff --git a/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb b/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb
index c737091df48..815108be447 100644
--- a/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb
+++ b/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb
@@ -11,7 +11,7 @@ RSpec.shared_context 'npm api setup' do
let_it_be(:package, reload: true) { create(:npm_package, project: project, name: "@#{group.path}/scoped_package") }
let_it_be(:token) { create(:oauth_access_token, scopes: 'api', resource_owner: user) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
- let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running, project: project) }
+ let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running) }
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
diff --git a/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb b/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb
index ea72398010c..6b49a415889 100644
--- a/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb
+++ b/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb
@@ -6,20 +6,20 @@ RSpec.shared_context 'stubbed service ping metrics definitions' do
let(:metrics_definitions) { standard_metrics + subscription_metrics + operational_metrics + optional_metrics }
let(:standard_metrics) do
[
- metric_attributes('uuid', "Standard")
+ metric_attributes('uuid', "standard")
]
end
let(:operational_metrics) do
[
- metric_attributes('counts.merge_requests', "Operational"),
- metric_attributes('counts.todos', "Operational")
+ metric_attributes('counts.merge_requests', "operational"),
+ metric_attributes('counts.todos', "operational")
]
end
let(:optional_metrics) do
[
- metric_attributes('counts.boards', "Optional"),
+ metric_attributes('counts.boards', "optional"),
metric_attributes('gitaly.filesystems', '').except('data_category')
]
end
diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
index 9c8006ce4f1..cadc753513d 100644
--- a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
+++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
@@ -109,6 +109,18 @@ RSpec.shared_examples 'multiple issue boards' do
assert_boards_nav_active
end
+
+ it 'switches current board back' do
+ in_boards_switcher_dropdown do
+ click_link board.name
+ end
+
+ wait_for_requests
+
+ page.within('.boards-switcher') do
+ expect(page).to have_content(board.name)
+ end
+ end
end
context 'unauthorized user' do
diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
index 422282da4d8..a9c6da7bc2b 100644
--- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
@@ -80,7 +80,6 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do
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("provider_repos", 1, "id")).to eq(org_repo.id)
- expect(json_response.dig("namespaces", 0, "id")).to eq(group.id)
end
it "does not show already added project" do
@@ -156,7 +155,6 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do
expect(json_response.dig("imported_projects").count).to eq(0)
expect(json_response.dig("provider_repos").count).to eq(1)
expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_2.id)
- expect(json_response.dig("namespaces", 0, "id")).to eq(group.id)
end
it 'filters the list, ignoring the case of the name' do
@@ -166,7 +164,6 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do
expect(json_response.dig("imported_projects").count).to eq(0)
expect(json_response.dig("provider_repos").count).to eq(1)
expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_2.id)
- expect(json_response.dig("namespaces", 0, "id")).to eq(group.id)
end
context 'when user input contains html' do
diff --git a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb
index ecb9abc5c46..b9ae0e23e26 100644
--- a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb
@@ -18,7 +18,6 @@ RSpec.shared_examples 'import controller status' do
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
diff --git a/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb b/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb
index 3f97c031e27..30914e61df0 100644
--- a/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
RSpec.shared_examples 'tracking unique visits' do |method|
+ include TrackingHelpers
+
let(:request_params) { {} }
it 'tracks unique visit if the format is HTML' do
@@ -14,14 +16,15 @@ RSpec.shared_examples 'tracking unique visits' do |method|
expect(Gitlab::UsageDataCounters::HLLRedisCounter)
.to receive(:track_event).with(target_id, values: kind_of(String))
- request.headers['DNT'] = '0'
+ stub_do_not_track('0')
get method, params: request_params, format: :html
end
it 'does not track unique visit if DNT is enabled' do
expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
- request.headers['DNT'] = '1'
+
+ stub_do_not_track('1')
get method, params: request_params, format: :html
end
diff --git a/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb b/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb
new file mode 100644
index 00000000000..d29c677a962
--- /dev/null
+++ b/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a successful blob pull' do
+ it 'sends a file' do
+ expect(controller).to receive(:send_file).with(blob.file.path, {})
+
+ subject
+ end
+
+ it 'returns Content-Disposition: attachment', :aggregate_failures do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['Content-Disposition']).to match(/^attachment/)
+ end
+end
+
+RSpec.shared_examples 'a successful manifest pull' do
+ it 'sends a file' do
+ expect(controller).to receive(:send_file).with(manifest.file.path, type: manifest.content_type)
+
+ subject
+ end
+
+ it 'returns Content-Disposition: attachment', :aggregate_failures do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['Docker-Content-Digest']).to eq(manifest.digest)
+ expect(response.headers['Content-Length']).to eq(manifest.size)
+ expect(response.headers['Docker-Distribution-Api-Version']).to eq(DependencyProxy::DISTRIBUTION_API_VERSION)
+ expect(response.headers['Etag']).to eq("\"#{manifest.digest}\"")
+ expect(response.headers['Content-Disposition']).to match(/^attachment/)
+ end
+end
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 ff2878f77b4..fb2e422559d 100644
--- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb
+++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
@@ -308,7 +308,7 @@ RSpec.shared_examples 'thread comments for issue, epic and merge request' do |re
let(:reply_id) { find("#{comments_selector} .note:last-of-type", match: :first)['data-note-id'] }
it 'can be replied to after resolving' do
- find('button[data-qa-selector="resolve_discussion_button"]').click
+ find('button[data-qa-selector="resolve_discussion_button"]').click # rubocop:disable QA/SelectorUsage
wait_for_requests
refresh
@@ -320,7 +320,7 @@ RSpec.shared_examples 'thread comments for issue, epic and merge request' do |re
it 'shows resolved thread when toggled' do
submit_reply('a')
- find('button[data-qa-selector="resolve_discussion_button"]').click
+ find('button[data-qa-selector="resolve_discussion_button"]').click # rubocop:disable QA/SelectorUsage
wait_for_requests
expect(page).to have_selector(".note-row-#{note_id}", visible: true)
diff --git a/spec/support/shared_examples/features/manage_applications_shared_examples.rb b/spec/support/shared_examples/features/manage_applications_shared_examples.rb
new file mode 100644
index 00000000000..38bb87eaed2
--- /dev/null
+++ b/spec/support/shared_examples/features/manage_applications_shared_examples.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'manage applications' do
+ let_it_be(:application_name) { 'application foo bar' }
+ let_it_be(:application_name_changed) { "#{application_name} changed" }
+ let_it_be(:application_redirect_uri) { 'https://foo.bar' }
+
+ it 'allows user to manage applications' do
+ visit new_application_path
+
+ expect(page).to have_content 'Add new application'
+
+ fill_in :doorkeeper_application_name, with: application_name
+ fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri
+ check :doorkeeper_application_scopes_read_user
+ click_on 'Save application'
+
+ validate_application(application_name, 'Yes')
+
+ application = Doorkeeper::Application.find_by(name: application_name)
+ expect(page).to have_css("button[title=\"Copy secret\"][data-clipboard-text=\"#{application.secret}\"]", text: 'Copy')
+
+ click_on 'Edit'
+
+ application_name_changed = "#{application_name} changed"
+
+ fill_in :doorkeeper_application_name, with: application_name_changed
+ uncheck :doorkeeper_application_confidential
+ click_on 'Save application'
+
+ validate_application(application_name_changed, 'No')
+
+ visit_applications_path
+
+ page.within '.oauth-applications' do
+ click_on 'Destroy'
+ end
+ expect(page.find('.oauth-applications')).not_to have_content 'test_changed'
+ end
+
+ context 'when scopes are blank' do
+ it 'returns an error' do
+ visit new_application_path
+
+ expect(page).to have_content 'Add new application'
+
+ fill_in :doorkeeper_application_name, with: application_name
+ fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri
+ click_on 'Save application'
+
+ expect(page).to have_content("Scopes can't be blank")
+ end
+ end
+
+ def visit_applications_path
+ visit defined?(applications_path) ? applications_path : new_application_path
+ end
+
+ def validate_application(name, confidential)
+ aggregate_failures do
+ expect(page).to have_content name
+ expect(page).to have_content 'Application ID'
+ expect(page).to have_content 'Secret'
+ expect(page).to have_content "Confidential #{confidential}"
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb
index 9e88db2e1c0..96be30b9f1f 100644
--- a/spec/support/shared_examples/features/packages_shared_examples.rb
+++ b/spec/support/shared_examples/features/packages_shared_examples.rb
@@ -14,7 +14,7 @@ RSpec.shared_examples 'packages list' do |check_project_name: false|
end
def package_table_row(index)
- page.all("#{packages_table_selector} > [data-qa-selector=\"package_row\"]")[index].text
+ page.all("#{packages_table_selector} > [data-qa-selector=\"package_row\"]")[index].text # rubocop:disable QA/SelectorUsage
end
end
@@ -34,10 +34,8 @@ RSpec.shared_examples 'package details link' do |property|
expect(page).to have_css('.packages-app h1[data-testid="title"]', text: package.name)
- page.within(%Q([name="#{package.name}"])) do
- expect(page).to have_content('Installation')
- expect(page).to have_content('Registry setup')
- end
+ expect(page).to have_content('Installation')
+ expect(page).to have_content('Registry setup')
end
end
@@ -92,7 +90,7 @@ RSpec.shared_examples 'shared package sorting' do
end
def packages_table_selector
- '[data-qa-selector="packages-table"]'
+ '[data-qa-selector="packages-table"]' # rubocop:disable QA/SelectorUsage
end
def click_sort_option(option, ascending)
@@ -100,7 +98,7 @@ def click_sort_option(option, ascending)
# Reset the sort direction
click_button 'Sort direction' if page.has_selector?('svg[aria-label="Sorting Direction: Ascending"]', wait: 0)
- find('button.dropdown-menu-toggle').click
+ find('button.gl-dropdown-toggle').click
page.within('.dropdown-menu') do
click_button option
diff --git a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb
index 56154c7cd03..8212f14d6be 100644
--- a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb
+++ b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb
@@ -23,6 +23,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
end
click_on_protect
+ wait_for_requests
expect(ProtectedBranch.count).to eq(1)
expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id])
diff --git a/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb b/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb
index 28fe198c9c3..14142793a0d 100644
--- a/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb
+++ b/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb
@@ -23,7 +23,7 @@ RSpec.shared_examples 'Deploy keys with protected branches' do
find(".js-allowed-to-push").click
wait_for_requests
- within('.qa-allowed-to-push-dropdown') do
+ within('.qa-allowed-to-push-dropdown') do # rubocop:disable QA/SelectorUsage
dropdown_headers = page.all('.dropdown-header').map(&:text)
expect(dropdown_headers).to contain_exactly(*all_dropdown_sections)
@@ -38,7 +38,7 @@ RSpec.shared_examples 'Deploy keys with protected branches' do
find(".js-allowed-to-merge").click
wait_for_requests
- within('.qa-allowed-to-merge-dropdown') do
+ within('.qa-allowed-to-merge-dropdown') do # rubocop:disable QA/SelectorUsage
dropdown_headers = page.all('.dropdown-header').map(&:text)
expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys)
@@ -68,7 +68,7 @@ RSpec.shared_examples 'Deploy keys with protected branches' do
find(".js-allowed-to-push").click
wait_for_requests
- within('.qa-allowed-to-push-dropdown') do
+ within('.qa-allowed-to-push-dropdown') do # rubocop:disable QA/SelectorUsage
dropdown_headers = page.all('.dropdown-header').map(&:text)
expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys)
diff --git a/spec/support/shared_examples/features/rss_shared_examples.rb b/spec/support/shared_examples/features/rss_shared_examples.rb
index 1b0d3f9605a..c7c2aeea358 100644
--- a/spec/support/shared_examples/features/rss_shared_examples.rb
+++ b/spec/support/shared_examples/features/rss_shared_examples.rb
@@ -9,7 +9,7 @@ end
RSpec.shared_examples "it has an RSS button with current_user's feed token" do
it "shows the RSS button with current_user's feed token" do
expect(page)
- .to have_css("a:has(.qa-rss-icon)[href*='feed_token=#{user.feed_token}']")
+ .to have_css("a:has(.qa-rss-icon)[href*='feed_token=#{user.feed_token}']") # rubocop:disable QA/SelectorUsage
end
end
@@ -22,6 +22,6 @@ end
RSpec.shared_examples "it has an RSS button without a feed token" do
it "shows the RSS button without a feed token" do
expect(page)
- .to have_css("a:has(.qa-rss-icon):not([href*='feed_token'])")
+ .to have_css("a:has(.qa-rss-icon):not([href*='feed_token'])") # rubocop:disable QA/SelectorUsage
end
end
diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb
index 997500415a9..52451839281 100644
--- a/spec/support/shared_examples/features/variable_list_shared_examples.rb
+++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb
@@ -91,7 +91,7 @@ RSpec.shared_examples 'variable list' do
end
page.within('#add-ci-variable') do
- find('[data-qa-selector="ci_variable_key_field"] input').set('new_key')
+ find('[data-qa-selector="ci_variable_key_field"] input').set('new_key') # rubocop:disable QA/SelectorUsage
click_button('Update variable')
end
@@ -173,7 +173,7 @@ RSpec.shared_examples 'variable list' do
click_button('Add variable')
page.within('#add-ci-variable') do
- find('[data-qa-selector="ci_variable_key_field"] input').set('empty_mask_key')
+ find('[data-qa-selector="ci_variable_key_field"] input').set('empty_mask_key') # rubocop:disable QA/SelectorUsage
find('[data-testid="ci-variable-protected-checkbox"]').click
find('[data-testid="ci-variable-masked-checkbox"]').click
@@ -286,8 +286,8 @@ RSpec.shared_examples 'variable list' do
wait_for_requests
page.within('#add-ci-variable') do
- find('[data-qa-selector="ci_variable_key_field"] input').set(key)
- find('[data-qa-selector="ci_variable_value_field"]').set(value) if value.present?
+ find('[data-qa-selector="ci_variable_key_field"] input').set(key) # rubocop:disable QA/SelectorUsage
+ find('[data-qa-selector="ci_variable_value_field"]').set(value) if value.present? # rubocop:disable QA/SelectorUsage
find('[data-testid="ci-variable-protected-checkbox"]').click if protected
find('[data-testid="ci-variable-masked-checkbox"]').click if masked
diff --git a/spec/support/shared_examples/features/wiki/user_views_asciidoc_page_with_includes_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_asciidoc_page_with_includes_shared_examples.rb
index 3b2fda4e05b..6fdc5ecae73 100644
--- a/spec/support/shared_examples/features/wiki/user_views_asciidoc_page_with_includes_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_views_asciidoc_page_with_includes_shared_examples.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'User views AsciiDoc page with includes' do
- let_it_be(:wiki_content_selector) { '[data-qa-selector=wiki_page_content]' }
+ let_it_be(:wiki_content_selector) { '[data-qa-selector=wiki_page_content]' } # rubocop:disable QA/SelectorUsage
let!(:included_wiki_page) { create_wiki_page('included_page', content: 'Content from the included page')}
let!(:wiki_page) { create_wiki_page('home', content: "Content from the main page.\ninclude::included_page.asciidoc[]") }
diff --git a/spec/support/shared_examples/finders/security/jobs_finder_shared_examples.rb b/spec/support/shared_examples/finders/security/jobs_finder_shared_examples.rb
index a332b213866..117b35201f6 100644
--- a/spec/support/shared_examples/finders/security/jobs_finder_shared_examples.rb
+++ b/spec/support/shared_examples/finders/security/jobs_finder_shared_examples.rb
@@ -68,20 +68,6 @@ RSpec.shared_examples ::Security::JobsFinder do |default_job_types|
end
end
- context 'when using legacy CI build metadata config storage' do
- before do
- stub_feature_flags(ci_build_metadata_config: false)
- end
-
- it_behaves_like 'JobsFinder core functionality'
- end
-
- context 'when using the new CI build metadata config storage' do
- before do
- stub_feature_flags(ci_build_metadata_config: true)
- end
-
- it_behaves_like 'JobsFinder core functionality'
- end
+ it_behaves_like 'JobsFinder core functionality'
end
end
diff --git a/spec/support/shared_examples/lib/gitlab/ci/reports/security/locations/locations_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/ci/reports/security/locations/locations_shared_examples.rb
new file mode 100644
index 00000000000..3aa04a77e57
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/ci/reports/security/locations/locations_shared_examples.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'vulnerability location' do
+ describe '#initialize' do
+ subject { described_class.new(**params) }
+
+ context 'when all params are given' do
+ it 'initializes an instance' do
+ expect { subject }.not_to raise_error
+
+ expect(subject).to have_attributes(**params)
+ end
+ end
+
+ where(:param) do
+ mandatory_params
+ end
+
+ with_them do
+ context "when param #{params[:param]} is missing" do
+ before do
+ params.delete(param)
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
+ end
+ end
+ end
+
+ describe '#fingerprint' do
+ subject { described_class.new(**params).fingerprint }
+
+ it "generates expected fingerprint" do
+ expect(subject).to eq(expected_fingerprint)
+ end
+ end
+
+ describe '#fingerprint_path' do
+ subject { described_class.new(**params).fingerprint_path }
+
+ it "generates expected fingerprint" do
+ expect(subject).to eq(expected_fingerprint_path)
+ end
+ end
+
+ describe '#==' do
+ let(:location_1) { create(:ci_reports_security_locations_sast) }
+ let(:location_2) { create(:ci_reports_security_locations_sast) }
+
+ subject { location_1 == location_2 }
+
+ it "returns true when fingerprints are equal" do
+ allow(location_1).to receive(:fingerprint).and_return('fingerprint')
+ allow(location_2).to receive(:fingerprint).and_return('fingerprint')
+
+ expect(subject).to eq(true)
+ end
+
+ it "returns false when fingerprints are different" do
+ allow(location_1).to receive(:fingerprint).and_return('fingerprint')
+ allow(location_2).to receive(:fingerprint).and_return('another_fingerprint')
+
+ expect(subject).to eq(false)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb
index 20f3270526e..7888ade56eb 100644
--- a/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb
@@ -21,3 +21,46 @@ RSpec.shared_examples 'marks background migration job records' do
expect(jobs_updated).to eq(1)
end
end
+
+RSpec.shared_examples 'finalized background migration' do
+ it 'processed the scheduled sidekiq queue' do
+ queued = Sidekiq::ScheduledSet
+ .new
+ .select do |scheduled|
+ scheduled.klass == 'BackgroundMigrationWorker' &&
+ scheduled.args.first == job_class_name
+ end
+ expect(queued.size).to eq(0)
+ end
+
+ it 'processed the async sidekiq queue' do
+ queued = Sidekiq::Queue.new('BackgroundMigrationWorker')
+ .select { |scheduled| scheduled.klass == job_class_name }
+ expect(queued.size).to eq(0)
+ end
+
+ include_examples 'removed tracked jobs', 'pending'
+end
+
+RSpec.shared_examples 'finalized tracked background migration' do
+ include_examples 'finalized background migration'
+ include_examples 'removed tracked jobs', 'succeeded'
+end
+
+RSpec.shared_examples 'removed tracked jobs' do |status|
+ it "removes '#{status}' tracked jobs" do
+ jobs = Gitlab::Database::BackgroundMigrationJob
+ .where(status: Gitlab::Database::BackgroundMigrationJob.statuses[status])
+ .for_migration_class(job_class_name)
+ expect(jobs).to be_empty
+ end
+end
+
+RSpec.shared_examples 'retained tracked jobs' do |status|
+ it "retains '#{status}' tracked jobs" do
+ jobs = Gitlab::Database::BackgroundMigrationJob
+ .where(status: Gitlab::Database::BackgroundMigrationJob.statuses[status])
+ .for_migration_class(job_class_name)
+ expect(jobs).to be_present
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb
index 88e6ffd15a8..a617342ff8c 100644
--- a/spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb
@@ -11,7 +11,7 @@ RSpec.shared_examples 'CTE with MATERIALIZED keyword examples' do
context 'when PG version is <12' do
it 'does not add MATERIALIZE keyword' do
- allow(Gitlab::Database).to receive(:version).and_return('11.1')
+ allow(Gitlab::Database.main).to receive(:version).and_return('11.1')
expect(query).to include(expected_query_block_without_materialized)
end
@@ -19,14 +19,14 @@ RSpec.shared_examples 'CTE with MATERIALIZED keyword examples' do
context 'when PG version is >=12' do
it 'adds MATERIALIZE keyword' do
- allow(Gitlab::Database).to receive(:version).and_return('12.1')
+ allow(Gitlab::Database.main).to receive(:version).and_return('12.1')
expect(query).to include(expected_query_block_with_materialized)
end
context 'when version is higher than 12' do
it 'adds MATERIALIZE keyword' do
- allow(Gitlab::Database).to receive(:version).and_return('15.1')
+ allow(Gitlab::Database.main).to receive(:version).and_return('15.1')
expect(query).to include(expected_query_block_with_materialized)
end
diff --git a/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb
index 72d672fd36c..69a1f7ad11e 100644
--- a/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb
@@ -14,10 +14,10 @@ RSpec.shared_examples 'performs validation' do |validation_option|
it 'performs validation' do
expect(model).to receive(:disable_statement_timeout).and_call_original
expect(model).to receive(:statement_timeout_disabled?).and_return(false)
- expect(model).to receive(:execute).with(/statement_timeout/)
+ expect(model).to receive(:execute).with(/SET statement_timeout TO/)
expect(model).to receive(:execute).ordered.with(/NOT VALID/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
- expect(model).to receive(:execute).ordered.with(/RESET ALL/)
+ expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/)
model.add_concurrent_foreign_key(*args, **options.merge(validation_option))
end
diff --git a/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb
index 8d758ed1655..ead8b174d46 100644
--- a/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb
@@ -22,7 +22,7 @@ RSpec.shared_examples 'SQL set operator' do |operator_keyword|
end
it 'skips Model.none segments' do
- empty_relation = User.none
+ empty_relation = User.none.select(:id)
set_operator = described_class.new([empty_relation, relation_1, relation_2])
expect {User.where("users.id IN (#{set_operator.to_sql})").to_a}.not_to raise_error
@@ -44,6 +44,17 @@ RSpec.shared_examples 'SQL set operator' do |operator_keyword|
end
end
+ context 'when uneven select values are used' do
+ let(:relation_1) { User.where(email: 'alice@example.com').select(*User.column_names) }
+ let(:relation_2) { User.where(email: 'bob@example.com') }
+
+ it 'raises error' do
+ expect do
+ described_class.new([relation_1, relation_2])
+ end.to raise_error /Relations with uneven select values were passed/
+ end
+ end
+
describe 'remove_order parameter' do
let(:scopes) do
[
diff --git a/spec/support/shared_examples/helpers/groups_shared_examples.rb b/spec/support/shared_examples/lib/menus_shared_examples.rb
index 9c74d25b31f..2c2cb362b07 100644
--- a/spec/support/shared_examples/helpers/groups_shared_examples.rb
+++ b/spec/support/shared_examples/lib/menus_shared_examples.rb
@@ -1,30 +1,16 @@
# frozen_string_literal: true
-# This shared_example requires the following variables:
-# - current_user
-# - group
-# - type, the issuable type (ie :issues, :merge_requests)
-# - count_service, the Service used by the specified issuable type
-
-RSpec.shared_examples 'cached issuables count' do
- subject { helper.cached_issuables_count(group, type: type) }
-
- before do
- allow(helper).to receive(:current_user) { current_user }
- allow(count_service).to receive(:new).and_call_original
- end
+RSpec.shared_examples_for 'pill_count formatted results' do
+ let(:count_service) { raise NotImplementedError }
- it 'calls the correct service class' do
- subject
- expect(count_service).to have_received(:new).with(group, current_user)
- end
+ subject(:pill_count) { menu.pill_count }
it 'returns all digits for count value under 1000' do
allow_next_instance_of(count_service) do |service|
allow(service).to receive(:count).and_return(999)
end
- expect(subject).to eq('999')
+ expect(pill_count).to eq('999')
end
it 'returns truncated digits for count value over 1000' do
@@ -32,7 +18,7 @@ RSpec.shared_examples 'cached issuables count' do
allow(service).to receive(:count).and_return(2300)
end
- expect(subject).to eq('2.3k')
+ expect(pill_count).to eq('2.3k')
end
it 'returns truncated digits for count value over 10000' do
@@ -40,7 +26,7 @@ RSpec.shared_examples 'cached issuables count' do
allow(service).to receive(:count).and_return(12560)
end
- expect(subject).to eq('12.6k')
+ expect(pill_count).to eq('12.6k')
end
it 'returns truncated digits for count value over 100000' do
@@ -48,6 +34,6 @@ RSpec.shared_examples 'cached issuables count' do
allow(service).to receive(:count).and_return(112560)
end
- expect(subject).to eq('112.6k')
+ expect(pill_count).to eq('112.6k')
end
end
diff --git a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb
index a84658780b9..c6d6ff6bc1d 100644
--- a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb
+++ b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb
@@ -1,106 +1,126 @@
# frozen_string_literal: true
RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role|
+ let(:db_config_name) { ::Gitlab::Database.db_config_names.first }
+
+ let(:expected_payload_defaults) do
+ metrics =
+ ::Gitlab::Metrics::Subscribers::ActiveRecord.load_balancing_metric_counter_keys +
+ ::Gitlab::Metrics::Subscribers::ActiveRecord.load_balancing_metric_duration_keys +
+ ::Gitlab::Metrics::Subscribers::ActiveRecord.db_counter_keys
+
+ metrics.each_with_object({}) do |key, result|
+ result[key] = 0
+ end
+ end
+
+ def transform_hash(hash, another_hash)
+ another_hash.each do |key, value|
+ raise "Unexpected key: #{key}" unless hash[key]
+ end
+
+ hash.merge(another_hash)
+ end
+
it 'prevents db counters from leaking to the next transaction' do
2.times do
Gitlab::WithRequestStore.with_request_store do
subscriber.sql(event)
- connection = event.payload[:connection]
-
- if db_role == :primary
- expected = {
- db_count: record_query ? 1 : 0,
- db_write_count: record_write_query ? 1 : 0,
- db_cached_count: record_cached_query ? 1 : 0,
- db_primary_cached_count: record_cached_query ? 1 : 0,
- db_primary_count: record_query ? 1 : 0,
- db_primary_duration_s: record_query ? 0.002 : 0,
- db_replica_cached_count: 0,
- db_replica_count: 0,
- db_replica_duration_s: 0.0,
- db_primary_wal_count: record_wal_query ? 1 : 0,
- db_primary_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0,
- db_replica_wal_cached_count: 0,
- db_replica_wal_count: 0
- }
- expected[:"db_primary_#{::Gitlab::Database.dbname(connection)}_duration_s"] = 0.002 if record_query
- elsif db_role == :replica
- expected = {
- db_count: record_query ? 1 : 0,
- db_write_count: record_write_query ? 1 : 0,
- db_cached_count: record_cached_query ? 1 : 0,
- db_primary_cached_count: 0,
- db_primary_count: 0,
- db_primary_duration_s: 0.0,
- db_replica_cached_count: record_cached_query ? 1 : 0,
- db_replica_count: record_query ? 1 : 0,
- db_replica_duration_s: record_query ? 0.002 : 0,
- db_replica_wal_count: record_wal_query ? 1 : 0,
- db_replica_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0,
- db_primary_wal_cached_count: 0,
- db_primary_wal_count: 0
- }
- expected[:"db_replica_#{::Gitlab::Database.dbname(connection)}_duration_s"] = 0.002 if record_query
- else
- expected = {
- db_count: record_query ? 1 : 0,
- db_write_count: record_write_query ? 1 : 0,
- db_cached_count: record_cached_query ? 1 : 0
- }
- end
+
+ expected = if db_role == :primary
+ transform_hash(expected_payload_defaults, {
+ db_count: record_query ? 1 : 0,
+ db_write_count: record_write_query ? 1 : 0,
+ db_cached_count: record_cached_query ? 1 : 0,
+ db_primary_cached_count: record_cached_query ? 1 : 0,
+ "db_primary_#{db_config_name}_cached_count": record_cached_query ? 1 : 0,
+ db_primary_count: record_query ? 1 : 0,
+ "db_primary_#{db_config_name}_count": record_query ? 1 : 0,
+ db_primary_duration_s: record_query ? 0.002 : 0,
+ "db_primary_#{db_config_name}_duration_s": record_query ? 0.002 : 0,
+ db_primary_wal_count: record_wal_query ? 1 : 0,
+ "db_primary_#{db_config_name}_wal_count": record_wal_query ? 1 : 0,
+ db_primary_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0,
+ "db_primary_#{db_config_name}_wal_cached_count": record_wal_query && record_cached_query ? 1 : 0
+ })
+ elsif db_role == :replica
+ transform_hash(expected_payload_defaults, {
+ db_count: record_query ? 1 : 0,
+ db_write_count: record_write_query ? 1 : 0,
+ db_cached_count: record_cached_query ? 1 : 0,
+ db_replica_cached_count: record_cached_query ? 1 : 0,
+ "db_replica_#{db_config_name}_cached_count": record_cached_query ? 1 : 0,
+ db_replica_count: record_query ? 1 : 0,
+ "db_replica_#{db_config_name}_count": record_query ? 1 : 0,
+ db_replica_duration_s: record_query ? 0.002 : 0,
+ "db_replica_#{db_config_name}_duration_s": record_query ? 0.002 : 0,
+ db_replica_wal_count: record_wal_query ? 1 : 0,
+ "db_replica_#{db_config_name}_wal_count": record_wal_query ? 1 : 0,
+ db_replica_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0,
+ "db_replica_#{db_config_name}_wal_cached_count": record_wal_query && record_cached_query ? 1 : 0
+ })
+ else
+ {
+ db_count: record_query ? 1 : 0,
+ db_write_count: record_write_query ? 1 : 0,
+ db_cached_count: record_cached_query ? 1 : 0
+ }
+ end
expect(described_class.db_counter_payload).to eq(expected)
end
end
end
- context 'when multiple_database_metrics is disabled' do
+ context 'when the GITLAB_MULTIPLE_DATABASE_METRICS env var is disabled' do
before do
- stub_feature_flags(multiple_database_metrics: false)
+ stub_env('GITLAB_MULTIPLE_DATABASE_METRICS', nil)
end
it 'does not include per database metrics' do
Gitlab::WithRequestStore.with_request_store do
subscriber.sql(event)
- connection = event.payload[:connection]
- expect(described_class.db_counter_payload).not_to include(:"db_replica_#{::Gitlab::Database.dbname(connection)}_duration_s")
+ expect(described_class.db_counter_payload).not_to include(:"db_replica_#{db_config_name}_duration_s")
+ expect(described_class.db_counter_payload).not_to include(:"db_replica_#{db_config_name}_count")
end
end
end
end
RSpec.shared_examples 'record ActiveRecord metrics in a metrics transaction' do |db_role|
+ let(:db_config_name) { ::Gitlab::Database.db_config_name(ApplicationRecord.connection) }
+
it 'increments only db counters' do
if record_query
- expect(transaction).to receive(:increment).with(:gitlab_transaction_db_count_total, 1)
- expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_count_total".to_sym, 1) if db_role
+ expect(transaction).to receive(:increment).with(:gitlab_transaction_db_count_total, 1, { db_config_name: db_config_name })
+ expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_count_total".to_sym, 1, { db_config_name: db_config_name }) if db_role
else
- expect(transaction).not_to receive(:increment).with(:gitlab_transaction_db_count_total, 1)
- expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_count_total".to_sym, 1) if db_role
+ expect(transaction).not_to receive(:increment).with(:gitlab_transaction_db_count_total, 1, { db_config_name: db_config_name })
+ expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_count_total".to_sym, 1, { db_config_name: db_config_name }) if db_role
end
if record_write_query
- expect(transaction).to receive(:increment).with(:gitlab_transaction_db_write_count_total, 1)
+ expect(transaction).to receive(:increment).with(:gitlab_transaction_db_write_count_total, 1, { db_config_name: db_config_name })
else
- expect(transaction).not_to receive(:increment).with(:gitlab_transaction_db_write_count_total, 1)
+ expect(transaction).not_to receive(:increment).with(:gitlab_transaction_db_write_count_total, 1, { db_config_name: db_config_name })
end
if record_cached_query
- expect(transaction).to receive(:increment).with(:gitlab_transaction_db_cached_count_total, 1)
- expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_cached_count_total".to_sym, 1) if db_role
+ expect(transaction).to receive(:increment).with(:gitlab_transaction_db_cached_count_total, 1, { db_config_name: db_config_name })
+ expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_cached_count_total".to_sym, 1, { db_config_name: db_config_name }) if db_role
else
- expect(transaction).not_to receive(:increment).with(:gitlab_transaction_db_cached_count_total, 1)
- expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_cached_count_total".to_sym, 1) if db_role
+ expect(transaction).not_to receive(:increment).with(:gitlab_transaction_db_cached_count_total, 1, { db_config_name: db_config_name })
+ expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_cached_count_total".to_sym, 1, { db_config_name: db_config_name }) if db_role
end
if record_wal_query
if db_role
- expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1)
- expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_cached_count_total".to_sym, 1) if record_cached_query
+ expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1, { db_config_name: db_config_name })
+ expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_cached_count_total".to_sym, 1, { db_config_name: db_config_name }) if record_cached_query
end
else
- expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1) if db_role
+ expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1, { db_config_name: db_config_name }) if db_role
end
subscriber.sql(event)
@@ -108,14 +128,34 @@ RSpec.shared_examples 'record ActiveRecord metrics in a metrics transaction' do
it 'observes sql_duration metric' do
if record_query
- expect(transaction).to receive(:observe).with(:gitlab_sql_duration_seconds, 0.002)
- expect(transaction).to receive(:observe).with("gitlab_sql_#{db_role}_duration_seconds".to_sym, 0.002) if db_role
+ expect(transaction).to receive(:observe).with(:gitlab_sql_duration_seconds, 0.002, { db_config_name: db_config_name })
+ expect(transaction).to receive(:observe).with("gitlab_sql_#{db_role}_duration_seconds".to_sym, 0.002, { db_config_name: db_config_name }) if db_role
else
expect(transaction).not_to receive(:observe)
end
subscriber.sql(event)
end
+
+ context 'when the GITLAB_MULTIPLE_DATABASE_METRICS env var is disabled' do
+ before do
+ stub_env('GITLAB_MULTIPLE_DATABASE_METRICS', nil)
+ end
+
+ it 'does not include db_config_name label' do
+ allow(transaction).to receive(:increment) do |*args|
+ labels = args[2] || {}
+ expect(labels).not_to include(:db_config_name)
+ end
+
+ allow(transaction).to receive(:observe) do |*args|
+ labels = args[2] || {}
+ expect(labels).not_to include(:db_config_name)
+ end
+
+ subscriber.sql(event)
+ end
+ end
end
RSpec.shared_examples 'record ActiveRecord metrics' do |db_role|
diff --git a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
index 99a09993900..f92ed3d7396 100644
--- a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
@@ -62,26 +62,6 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
.to raise_error(ActiveModel::MissingAttributeError)
end
end
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(efficient_counter_attribute: false)
- end
-
- it 'delegates to ActiveRecord update!' do
- expect { subject }
- .to change { model.reset.read_attribute(attribute) }.by(increment)
- end
-
- it 'does not increment the counter in Redis' do
- subject
-
- Gitlab::Redis::SharedState.with do |redis|
- counter = redis.get(model.counter_key(attribute))
- expect(counter).to be_nil
- end
- end
- end
end
end
end
diff --git a/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb b/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb
new file mode 100644
index 00000000000..7b33a95bfa1
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb
@@ -0,0 +1,246 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a model including Escalatable' do
+ # rubocop:disable Rails/SaveBang -- Usage of factory symbol as argument causes a false-positive
+ let_it_be(:escalatable_factory) { factory_from_class(described_class) }
+ let_it_be(:triggered_escalatable, reload: true) { create(escalatable_factory, :triggered) }
+ let_it_be(:acknowledged_escalatable, reload: true) { create(escalatable_factory, :acknowledged) }
+ let_it_be(:resolved_escalatable, reload: true) { create(escalatable_factory, :resolved) }
+ let_it_be(:ignored_escalatable, reload: true) { create(escalatable_factory, :ignored) }
+
+ context 'validations' do
+ it { is_expected.to validate_presence_of(:status) }
+
+ context 'when status is triggered' do
+ subject { triggered_escalatable }
+
+ context 'when resolved_at is blank' do
+ it { is_expected.to be_valid }
+ end
+
+ context 'when resolved_at is present' do
+ before do
+ triggered_escalatable.resolved_at = Time.current
+ end
+
+ it { is_expected.to be_invalid }
+ end
+ end
+
+ context 'when status is acknowledged' do
+ subject { acknowledged_escalatable }
+
+ context 'when resolved_at is blank' do
+ it { is_expected.to be_valid }
+ end
+
+ context 'when resolved_at is present' do
+ before do
+ acknowledged_escalatable.resolved_at = Time.current
+ end
+
+ it { is_expected.to be_invalid }
+ end
+ end
+
+ context 'when status is resolved' do
+ subject { resolved_escalatable }
+
+ context 'when resolved_at is blank' do
+ before do
+ resolved_escalatable.resolved_at = nil
+ end
+
+ it { is_expected.to be_invalid }
+ end
+
+ context 'when resolved_at is present' do
+ it { is_expected.to be_valid }
+ end
+ end
+
+ context 'when status is ignored' do
+ subject { ignored_escalatable }
+
+ context 'when resolved_at is blank' do
+ it { is_expected.to be_valid }
+ end
+
+ context 'when resolved_at is present' do
+ before do
+ ignored_escalatable.resolved_at = Time.current
+ end
+
+ it { is_expected.to be_invalid }
+ end
+ end
+ end
+
+ context 'scopes' do
+ let(:all_escalatables) { described_class.where(id: [triggered_escalatable, acknowledged_escalatable, ignored_escalatable, resolved_escalatable])}
+
+ describe '.order_status' do
+ subject { all_escalatables.order_status(order) }
+
+ context 'descending' do
+ let(:order) { :desc }
+
+ # Downward arrow in UI always corresponds to default sort
+ it { is_expected.to eq([triggered_escalatable, acknowledged_escalatable, resolved_escalatable, ignored_escalatable]) }
+ end
+
+ context 'ascending' do
+ let(:order) { :asc }
+
+ it { is_expected.to eq([ignored_escalatable, resolved_escalatable, acknowledged_escalatable, triggered_escalatable]) }
+ end
+ end
+ end
+
+ describe '.status_value' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status, :status_value) do
+ :triggered | 0
+ :acknowledged | 1
+ :resolved | 2
+ :ignored | 3
+ :unknown | nil
+ end
+
+ with_them do
+ it 'returns status value by its name' do
+ expect(described_class.status_value(status)).to eq(status_value)
+ end
+ end
+ end
+
+ describe '.status_name' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:raw_status, :status) do
+ 0 | :triggered
+ 1 | :acknowledged
+ 2 | :resolved
+ 3 | :ignored
+ -1 | nil
+ end
+
+ with_them do
+ it 'returns status name by its values' do
+ expect(described_class.status_name(raw_status)).to eq(status)
+ end
+ end
+ end
+
+ describe '#trigger' do
+ subject { escalatable.trigger }
+
+ context 'when escalatable is in triggered state' do
+ let(:escalatable) { triggered_escalatable }
+
+ it 'does not change the escalatable status' do
+ expect { subject }.not_to change { escalatable.reload.status }
+ end
+ end
+
+ context 'when escalatable is not in triggered state' do
+ let(:escalatable) { resolved_escalatable }
+
+ it 'changes the escalatable status to triggered' do
+ expect { subject }.to change { escalatable.triggered? }.to(true)
+ end
+
+ it 'resets resolved at' do
+ expect { subject }.to change { escalatable.reload.resolved_at }.to nil
+ end
+ end
+ end
+
+ describe '#acknowledge' do
+ subject { escalatable.acknowledge }
+
+ let(:escalatable) { resolved_escalatable }
+
+ it 'changes the escalatable status to acknowledged' do
+ expect { subject }.to change { escalatable.acknowledged? }.to(true)
+ end
+
+ it 'resets ended at' do
+ expect { subject }.to change { escalatable.reload.resolved_at }.to nil
+ end
+ end
+
+ describe '#resolve' do
+ let!(:resolved_at) { Time.current }
+
+ subject do
+ escalatable.resolved_at = resolved_at
+ escalatable.resolve
+ end
+
+ context 'when escalatable is already resolved' do
+ let(:escalatable) { resolved_escalatable }
+
+ it 'does not change the escalatable status' do
+ expect { subject }.not_to change { resolved_escalatable.reload.status }
+ end
+ end
+
+ context 'when escalatable is not resolved' do
+ let(:escalatable) { triggered_escalatable }
+
+ it 'changes escalatable status to "resolved"' do
+ expect { subject }.to change { escalatable.resolved? }.to(true)
+ end
+ end
+ end
+
+ describe '#ignore' do
+ subject { escalatable.ignore }
+
+ let(:escalatable) { resolved_escalatable }
+
+ it 'changes the escalatable status to ignored' do
+ expect { subject }.to change { escalatable.ignored? }.to(true)
+ end
+
+ it 'resets ended at' do
+ expect { subject }.to change { escalatable.reload.resolved_at }.to nil
+ end
+ end
+
+ describe '#status_event_for' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:for_status, :event) do
+ :triggered | :trigger
+ 'triggered' | :trigger
+ :acknowledged | :acknowledge
+ 'acknowledged' | :acknowledge
+ :resolved | :resolve
+ 'resolved' | :resolve
+ :ignored | :ignore
+ 'ignored' | :ignore
+ :unknown | nil
+ nil | nil
+ '' | nil
+ 1 | nil
+ end
+
+ with_them do
+ let(:escalatable) { build(escalatable_factory) }
+
+ it 'returns event by status name' do
+ expect(escalatable.status_event_for(for_status)).to eq(event)
+ end
+ end
+ end
+
+ private
+
+ def factory_from_class(klass)
+ klass.name.underscore.tr('/', '_')
+ end
+end
+# rubocop:enable Rails/SaveBang
diff --git a/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb b/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb
index cf38a583944..457ee49938f 100644
--- a/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb
+++ b/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb
@@ -13,6 +13,7 @@ RSpec.shared_examples 'value stream analytics stage' do
describe 'associations' do
it { is_expected.to belong_to(:end_event_label) }
it { is_expected.to belong_to(:start_event_label) }
+ it { is_expected.to belong_to(:stage_event_hash) }
end
describe 'validation' do
@@ -138,6 +139,67 @@ RSpec.shared_examples 'value stream analytics stage' do
expect(stage_1.events_hash_code).not_to eq(stage_2.events_hash_code)
end
end
+
+ # rubocop: disable Rails/SaveBang
+ describe '#event_hash' do
+ it 'associates the same stage event hash record' do
+ first = create(factory)
+ second = create(factory)
+
+ expect(first.stage_event_hash_id).to eq(second.stage_event_hash_id)
+ end
+
+ it 'does not introduce duplicated stage event hash records' do
+ expect do
+ create(factory)
+ create(factory)
+ end.to change { Analytics::CycleAnalytics::StageEventHash.count }.from(0).to(1)
+ end
+
+ it 'creates different hash record for different event configurations' do
+ expect do
+ create(factory, start_event_identifier: :issue_created, end_event_identifier: :issue_stage_end)
+ create(factory, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged)
+ end.to change { Analytics::CycleAnalytics::StageEventHash.count }.from(0).to(2)
+ end
+
+ context 'when the stage event hash changes' do
+ let(:stage) { create(factory, start_event_identifier: :issue_created, end_event_identifier: :issue_stage_end) }
+
+ it 'deletes the old, unused stage event hash record' do
+ old_stage_event_hash = stage.stage_event_hash
+
+ stage.update!(end_event_identifier: :issue_deployed_to_production)
+
+ expect(stage.stage_event_hash_id).not_to eq(old_stage_event_hash.id)
+
+ old_stage_event_hash_from_db = Analytics::CycleAnalytics::StageEventHash.find_by_id(old_stage_event_hash.id)
+ expect(old_stage_event_hash_from_db).to be_nil
+ end
+
+ it 'does not delete used stage event hash record' do
+ other_stage = create(factory, start_event_identifier: :issue_created, end_event_identifier: :issue_stage_end)
+
+ stage.update!(end_event_identifier: :issue_deployed_to_production)
+
+ expect(stage.stage_event_hash_id).not_to eq(other_stage.stage_event_hash_id)
+
+ old_stage_event_hash_from_db = Analytics::CycleAnalytics::StageEventHash.find_by_id(other_stage.stage_event_hash_id)
+ expect(old_stage_event_hash_from_db).not_to be_nil
+ end
+ end
+
+ context 'when the stage events hash code does not change' do
+ it 'does not trigger extra query on save' do
+ stage = create(factory, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged)
+
+ expect(Analytics::CycleAnalytics::StageEventHash).not_to receive(:record_id_by_hash_sha256)
+
+ stage.update!(name: 'new title')
+ end
+ end
+ end
+ # rubocop: enable Rails/SaveBang
end
RSpec.shared_examples 'value stream analytics label based stage' do
diff --git a/spec/support/shared_examples/models/mentionable_shared_examples.rb b/spec/support/shared_examples/models/mentionable_shared_examples.rb
index 04630484964..07c5f730e95 100644
--- a/spec/support/shared_examples/models/mentionable_shared_examples.rb
+++ b/spec/support/shared_examples/models/mentionable_shared_examples.rb
@@ -207,7 +207,7 @@ RSpec.shared_examples 'an editable mentionable' do
end
RSpec.shared_examples 'mentions in description' do |mentionable_type|
- describe 'when storing user mentions' do
+ shared_examples 'when storing user mentions' do
before do
mentionable.store_mentions!
end
@@ -238,10 +238,26 @@ RSpec.shared_examples 'mentions in description' do |mentionable_type|
end
end
end
+
+ context 'when store_mentions_without_subtransaction is enabled' do
+ before do
+ stub_feature_flags(store_mentions_without_subtransaction: true)
+ end
+
+ it_behaves_like 'when storing user mentions'
+ end
+
+ context 'when store_mentions_without_subtransaction is disabled' do
+ before do
+ stub_feature_flags(store_mentions_without_subtransaction: false)
+ end
+
+ it_behaves_like 'when storing user mentions'
+ end
end
RSpec.shared_examples 'mentions in notes' do |mentionable_type|
- context 'when mentionable notes contain mentions' do
+ shared_examples 'when mentionable notes contain mentions' do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:group) { create(:group) }
@@ -261,6 +277,22 @@ RSpec.shared_examples 'mentions in notes' do |mentionable_type|
expect(mentionable.referenced_groups(user)).to eq [group]
end
end
+
+ context 'when store_mentions_without_subtransaction is enabled' do
+ before do
+ stub_feature_flags(store_mentions_without_subtransaction: true)
+ end
+
+ it_behaves_like 'when mentionable notes contain mentions'
+ end
+
+ context 'when store_mentions_without_subtransaction is disabled' do
+ before do
+ stub_feature_flags(store_mentions_without_subtransaction: false)
+ end
+
+ it_behaves_like 'when mentionable notes contain mentions'
+ end
end
RSpec.shared_examples 'load mentions from DB' do |mentionable_type|
@@ -278,7 +310,7 @@ RSpec.shared_examples 'load mentions from DB' do |mentionable_type|
context 'when stored user mention contains ids of inexistent records' do
before do
- user_mention = note.send(:model_user_mention)
+ user_mention = note.user_mentions.first
mention_ids = {
mentioned_users_ids: user_mention.mentioned_users_ids.to_a << non_existing_record_id,
mentioned_projects_ids: user_mention.mentioned_projects_ids.to_a << non_existing_record_id,
@@ -302,7 +334,7 @@ RSpec.shared_examples 'load mentions from DB' do |mentionable_type|
let(:group_member) { create(:group_member, user: create(:user), group: private_group) }
before do
- user_mention = note.send(:model_user_mention)
+ user_mention = note.user_mentions.first
mention_ids = {
mentioned_projects_ids: user_mention.mentioned_projects_ids.to_a << private_project.id,
mentioned_groups_ids: user_mention.mentioned_groups_ids.to_a << private_group.id
diff --git a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
index 5459d17b1df..274fbae3dfd 100644
--- a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
+++ b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
@@ -128,10 +128,6 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze|
it { is_expected.not_to allow_value(12.hours.to_i).for(:valid_time_duration_seconds) }
end
- describe '#signing_keys' do
- it { is_expected.to validate_absence_of(:signing_keys) }
- end
-
describe '#file' do
it { is_expected.not_to validate_presence_of(:file) }
end
@@ -141,7 +137,15 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze|
end
describe '#file_signature' do
- it { is_expected.to validate_absence_of(:file_signature) }
+ it { is_expected.not_to validate_absence_of(:file_signature) }
+ end
+
+ describe '#signed_file' do
+ it { is_expected.not_to validate_presence_of(:signed_file) }
+ end
+
+ describe '#signed_file_store' do
+ it { is_expected.to validate_presence_of(:signed_file_store) }
end
end
diff --git a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
index 7b591ad84d1..2e01de2ea84 100644
--- a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
+++ b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
@@ -22,116 +22,6 @@ RSpec.shared_examples 'UpdateProjectStatistics' do |with_counter_attribute|
it { is_expected.to be_new_record }
- context 'when feature flag efficient_counter_attribute is disabled' do
- before do
- stub_feature_flags(efficient_counter_attribute: false)
- end
-
- context 'when creating' do
- it 'updates the project statistics' do
- delta0 = reload_stat
-
- subject.save!
-
- delta1 = reload_stat
-
- expect(delta1).to eq(delta0 + read_attribute)
- expect(delta1).to be > delta0
- end
-
- it 'schedules a namespace statistics worker' do
- expect(Namespaces::ScheduleAggregationWorker)
- .to receive(:perform_async).once
-
- subject.save!
- end
- end
-
- context 'when updating' do
- let(:delta) { 42 }
-
- before do
- subject.save!
- end
-
- it 'updates project statistics' do
- expect(ProjectStatistics)
- .to receive(:increment_statistic)
- .and_call_original
-
- subject.write_attribute(statistic_attribute, read_attribute + delta)
-
- expect { subject.save! }
- .to change { reload_stat }
- .by(delta)
- end
-
- it 'schedules a namespace statistics worker' do
- expect(Namespaces::ScheduleAggregationWorker)
- .to receive(:perform_async).once
-
- subject.write_attribute(statistic_attribute, read_attribute + delta)
- subject.save!
- end
-
- it 'avoids N + 1 queries' do
- subject.write_attribute(statistic_attribute, read_attribute + delta)
-
- control_count = ActiveRecord::QueryRecorder.new do
- subject.save!
- end
-
- subject.write_attribute(statistic_attribute, read_attribute + delta)
-
- expect do
- subject.save!
- end.not_to exceed_query_limit(control_count)
- end
- end
-
- context 'when destroying' do
- before do
- subject.save!
- end
-
- it 'updates the project statistics' do
- delta0 = reload_stat
-
- subject.destroy!
-
- delta1 = reload_stat
-
- expect(delta1).to eq(delta0 - read_attribute)
- expect(delta1).to be < delta0
- end
-
- it 'schedules a namespace statistics worker' do
- expect(Namespaces::ScheduleAggregationWorker)
- .to receive(:perform_async).once
-
- subject.destroy!
- end
-
- context 'when it is destroyed from the project level' do
- it 'does not update the project statistics' do
- expect(ProjectStatistics)
- .not_to receive(:increment_statistic)
-
- project.update!(pending_delete: true)
- project.destroy!
- end
-
- it 'does not schedule a namespace statistics worker' do
- expect(Namespaces::ScheduleAggregationWorker)
- .not_to receive(:perform_async)
-
- project.update!(pending_delete: true)
- project.destroy!
- end
- end
- end
- end
-
def expect_flush_counter_increments_worker_performed
expect(FlushCounterIncrementsWorker)
.to receive(:perform_in)
diff --git a/spec/support/shared_examples/namespaces/linear_traversal_examples.rb b/spec/support/shared_examples/namespaces/linear_traversal_examples.rb
deleted file mode 100644
index 2fd90c36953..00000000000
--- a/spec/support/shared_examples/namespaces/linear_traversal_examples.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-# Traversal examples common to linear and recursive methods are in
-# spec/support/shared_examples/namespaces/traversal_examples.rb
-
-RSpec.shared_examples 'linear namespace traversal' do
- context 'when use_traversal_ids feature flag is enabled' do
- before do
- stub_feature_flags(use_traversal_ids: true)
- end
-
- context 'scopes' do
- describe '.as_ids' do
- let_it_be(:namespace1) { create(:group) }
- let_it_be(:namespace2) { create(:group) }
-
- subject { Namespace.where(id: [namespace1, namespace2]).as_ids.pluck(:id) }
-
- it { is_expected.to contain_exactly(namespace1.id, namespace2.id) }
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/namespaces/traversal_examples.rb b/spec/support/shared_examples/namespaces/traversal_examples.rb
index f09634556c3..d126b242fb0 100644
--- a/spec/support/shared_examples/namespaces/traversal_examples.rb
+++ b/spec/support/shared_examples/namespaces/traversal_examples.rb
@@ -55,12 +55,34 @@ RSpec.shared_examples 'namespace traversal' do
end
describe '#ancestors' do
- it 'returns the correct ancestors' do
+ before do
# #reload is called to make sure traversal_ids are reloaded
- expect(very_deep_nested_group.reload.ancestors).to contain_exactly(group, nested_group, deep_nested_group)
- expect(deep_nested_group.reload.ancestors).to contain_exactly(group, nested_group)
- expect(nested_group.reload.ancestors).to contain_exactly(group)
- expect(group.reload.ancestors).to eq([])
+ reload_models(group, nested_group, deep_nested_group, very_deep_nested_group)
+ end
+
+ it 'returns the correct ancestors' do
+ expect(very_deep_nested_group.ancestors).to contain_exactly(group, nested_group, deep_nested_group)
+ expect(deep_nested_group.ancestors).to contain_exactly(group, nested_group)
+ expect(nested_group.ancestors).to contain_exactly(group)
+ expect(group.ancestors).to eq([])
+ end
+
+ context 'with asc hierarchy_order' do
+ it 'returns the correct ancestors' do
+ expect(very_deep_nested_group.ancestors(hierarchy_order: :asc)).to eq [deep_nested_group, nested_group, group]
+ expect(deep_nested_group.ancestors(hierarchy_order: :asc)).to eq [nested_group, group]
+ expect(nested_group.ancestors(hierarchy_order: :asc)).to eq [group]
+ expect(group.ancestors(hierarchy_order: :asc)).to eq([])
+ end
+ end
+
+ context 'with desc hierarchy_order' do
+ it 'returns the correct ancestors' do
+ expect(very_deep_nested_group.ancestors(hierarchy_order: :desc)).to eq [group, nested_group, deep_nested_group]
+ expect(deep_nested_group.ancestors(hierarchy_order: :desc)).to eq [group, nested_group]
+ expect(nested_group.ancestors(hierarchy_order: :desc)).to eq [group]
+ expect(group.ancestors(hierarchy_order: :desc)).to eq([])
+ end
end
describe '#recursive_ancestors' do
@@ -78,6 +100,24 @@ RSpec.shared_examples 'namespace traversal' do
expect(group.ancestor_ids).to be_empty
end
+ context 'with asc hierarchy_order' do
+ it 'returns the correct ancestor ids' do
+ expect(very_deep_nested_group.ancestor_ids(hierarchy_order: :asc)).to eq [deep_nested_group.id, nested_group.id, group.id]
+ expect(deep_nested_group.ancestor_ids(hierarchy_order: :asc)).to eq [nested_group.id, group.id]
+ expect(nested_group.ancestor_ids(hierarchy_order: :asc)).to eq [group.id]
+ expect(group.ancestor_ids(hierarchy_order: :asc)).to eq([])
+ end
+ end
+
+ context 'with desc hierarchy_order' do
+ it 'returns the correct ancestor ids' do
+ expect(very_deep_nested_group.ancestor_ids(hierarchy_order: :desc)).to eq [group.id, nested_group.id, deep_nested_group.id]
+ expect(deep_nested_group.ancestor_ids(hierarchy_order: :desc)).to eq [group.id, nested_group.id]
+ expect(nested_group.ancestor_ids(hierarchy_order: :desc)).to eq [group.id]
+ expect(group.ancestor_ids(hierarchy_order: :desc)).to eq([])
+ end
+ end
+
describe '#recursive_ancestor_ids' do
let_it_be(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] }
@@ -93,6 +133,24 @@ RSpec.shared_examples 'namespace traversal' do
expect(group.self_and_ancestors).to contain_exactly(group)
end
+ context 'with asc hierarchy_order' do
+ it 'returns the correct ancestors' do
+ expect(very_deep_nested_group.self_and_ancestors(hierarchy_order: :asc)).to eq [very_deep_nested_group, deep_nested_group, nested_group, group]
+ expect(deep_nested_group.self_and_ancestors(hierarchy_order: :asc)).to eq [deep_nested_group, nested_group, group]
+ expect(nested_group.self_and_ancestors(hierarchy_order: :asc)).to eq [nested_group, group]
+ expect(group.self_and_ancestors(hierarchy_order: :asc)).to eq([group])
+ end
+ end
+
+ context 'with desc hierarchy_order' do
+ it 'returns the correct ancestors' do
+ expect(very_deep_nested_group.self_and_ancestors(hierarchy_order: :desc)).to eq [group, nested_group, deep_nested_group, very_deep_nested_group]
+ expect(deep_nested_group.self_and_ancestors(hierarchy_order: :desc)).to eq [group, nested_group, deep_nested_group]
+ expect(nested_group.self_and_ancestors(hierarchy_order: :desc)).to eq [group, nested_group]
+ expect(group.self_and_ancestors(hierarchy_order: :desc)).to eq([group])
+ end
+ end
+
describe '#recursive_self_and_ancestors' do
let_it_be(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] }
@@ -108,6 +166,24 @@ RSpec.shared_examples 'namespace traversal' do
expect(group.self_and_ancestor_ids).to contain_exactly(group.id)
end
+ context 'with asc hierarchy_order' do
+ it 'returns the correct ancestor ids' do
+ expect(very_deep_nested_group.self_and_ancestor_ids(hierarchy_order: :asc)).to eq [very_deep_nested_group.id, deep_nested_group.id, nested_group.id, group.id]
+ expect(deep_nested_group.self_and_ancestor_ids(hierarchy_order: :asc)).to eq [deep_nested_group.id, nested_group.id, group.id]
+ expect(nested_group.self_and_ancestor_ids(hierarchy_order: :asc)).to eq [nested_group.id, group.id]
+ expect(group.self_and_ancestor_ids(hierarchy_order: :asc)).to eq([group.id])
+ end
+ end
+
+ context 'with desc hierarchy_order' do
+ it 'returns the correct ancestor ids' do
+ expect(very_deep_nested_group.self_and_ancestor_ids(hierarchy_order: :desc)).to eq [group.id, nested_group.id, deep_nested_group.id, very_deep_nested_group.id]
+ expect(deep_nested_group.self_and_ancestor_ids(hierarchy_order: :desc)).to eq [group.id, nested_group.id, deep_nested_group.id]
+ expect(nested_group.self_and_ancestor_ids(hierarchy_order: :desc)).to eq [group.id, nested_group.id]
+ expect(group.self_and_ancestor_ids(hierarchy_order: :desc)).to eq([group.id])
+ end
+ end
+
describe '#recursive_self_and_ancestor_ids' do
let_it_be(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] }
diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
new file mode 100644
index 00000000000..4d328c03641
--- /dev/null
+++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'namespace traversal scopes' do
+ # Hierarchy 1
+ let_it_be(:group_1) { create(:group) }
+ let_it_be(:nested_group_1) { create(:group, parent: group_1) }
+ let_it_be(:deep_nested_group_1) { create(:group, parent: nested_group_1) }
+
+ # Hierarchy 2
+ let_it_be(:group_2) { create(:group) }
+ let_it_be(:nested_group_2) { create(:group, parent: group_2) }
+ let_it_be(:deep_nested_group_2) { create(:group, parent: nested_group_2) }
+
+ # All groups
+ let_it_be(:groups) do
+ [
+ group_1, nested_group_1, deep_nested_group_1,
+ group_2, nested_group_2, deep_nested_group_2
+ ]
+ end
+
+ describe '.as_ids' do
+ subject { described_class.where(id: [group_1, group_2]).as_ids.pluck(:id) }
+
+ it { is_expected.to contain_exactly(group_1.id, group_2.id) }
+ end
+
+ describe '.without_sti_condition' do
+ subject { described_class.without_sti_condition }
+
+ it { expect(subject.where_values_hash).not_to have_key(:type) }
+ end
+
+ describe '.self_and_descendants' do
+ subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_descendants }
+
+ it { is_expected.to contain_exactly(nested_group_1, deep_nested_group_1, nested_group_2, deep_nested_group_2) }
+
+ context 'with duplicate descendants' do
+ subject { described_class.where(id: [group_1, group_2, nested_group_1]).self_and_descendants }
+
+ it { is_expected.to match_array(groups) }
+ end
+
+ context 'when include_self is false' do
+ subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_descendants(include_self: false) }
+
+ it { is_expected.to contain_exactly(deep_nested_group_1, deep_nested_group_2) }
+ end
+ end
+
+ describe '.self_and_descendant_ids' do
+ subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_descendant_ids.pluck(:id) }
+
+ it { is_expected.to contain_exactly(nested_group_1.id, deep_nested_group_1.id, nested_group_2.id, deep_nested_group_2.id) }
+
+ context 'when include_self is false' do
+ subject do
+ described_class
+ .where(id: [nested_group_1, nested_group_2])
+ .self_and_descendant_ids(include_self: false)
+ .pluck(:id)
+ end
+
+ it { is_expected.to contain_exactly(deep_nested_group_1.id, deep_nested_group_2.id) }
+ end
+ end
+end
diff --git a/spec/support/shared_examples/policies/project_policy_shared_examples.rb b/spec/support/shared_examples/policies/project_policy_shared_examples.rb
index 013c9b61b99..a4243db6bc9 100644
--- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb
+++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb
@@ -330,3 +330,18 @@ RSpec.shared_examples 'project policies as admin without admin mode' do
end
end
end
+
+RSpec.shared_examples 'package access with repository disabled' do
+ context 'when repository is disabled' do
+ before do
+ project.project_feature.update!(
+ # Disable merge_requests and builds as well, since merge_requests and
+ # builds cannot have higher visibility than repository.
+ merge_requests_access_level: ProjectFeature::DISABLED,
+ builds_access_level: ProjectFeature::DISABLED,
+ repository_access_level: ProjectFeature::DISABLED)
+ end
+
+ it { is_expected.to be_allowed(:read_package) }
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
index 1f68dd7a382..a3ed74085fb 100644
--- a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
@@ -4,7 +4,7 @@ RSpec.shared_context 'Debian repository shared context' do |container_type, can_
include_context 'workhorse headers'
before do
- stub_feature_flags(debian_packages: true)
+ stub_feature_flags(debian_packages: true, debian_group_packages: true)
end
let_it_be(:private_container, freeze: can_freeze) { create(container_type, :private) }
@@ -29,6 +29,8 @@ RSpec.shared_context 'Debian repository shared context' do |container_type, can_
let_it_be(:public_project) { create(:project, :public, group: public_container) }
let_it_be(:private_project_distribution) { create(:debian_project_distribution, container: private_project, codename: 'existing-codename') }
let_it_be(:public_project_distribution) { create(:debian_project_distribution, container: public_project, codename: 'existing-codename') }
+
+ let(:project) { { private: private_project, public: public_project }[visibility_level] }
else
let_it_be(:private_project) { private_container }
let_it_be(:public_project) { public_container }
@@ -45,12 +47,8 @@ RSpec.shared_context 'Debian repository shared context' do |container_type, can_
let(:architecture) { { private: private_architecture, public: public_architecture }[visibility_level] }
let(:component) { { private: private_component, public: public_component }[visibility_level] }
let(:component_file) { { private: private_component_file, public: public_component_file }[visibility_level] }
-
- let(:source_package) { 'sample' }
- let(:letter) { source_package[0..2] == 'lib' ? source_package[0..3] : source_package[0] }
- let(:package_name) { 'libsample0' }
- let(:package_version) { '1.2.3~alpha2' }
- let(:file_name) { "#{package_name}_#{package_version}_#{architecture.name}.deb" }
+ let(:package) { { private: private_package, public: public_package }[visibility_level] }
+ let(:letter) { package.name[0..2] == 'lib' ? package.name[0..3] : package.name[0] }
let(:method) { :get }
@@ -94,6 +92,10 @@ RSpec.shared_context 'Debian repository shared context' do |container_type, can_
end
end
+RSpec.shared_context 'with file_name' do |file_name|
+ let(:file_name) { file_name }
+end
+
RSpec.shared_context 'Debian repository auth headers' do |user_role, user_token, auth_method = :token|
let(:token) { user_token ? personal_access_token.token : 'wrong' }
diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
index c15c59e1a1d..0390e60747f 100644
--- a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
@@ -46,6 +46,8 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
end
shared_examples 'handling all conditions' do
+ include_context 'dependency proxy helpers context'
+
where(:auth, :package_name_type, :request_forward, :visibility, :user_role, :expected_result, :expected_status) do
nil | :scoped_naming_convention | true | :public | nil | :accept | :ok
nil | :scoped_naming_convention | false | :public | nil | :accept | :ok
@@ -243,7 +245,7 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
project.send("add_#{user_role}", user) if user_role
project.update!(visibility: visibility.to_s)
package.update!(name: package_name) unless package_name == 'non-existing-package'
- stub_application_setting(npm_package_requests_forwarding: request_forward)
+ allow_fetch_application_setting(attribute: "npm_package_requests_forwarding", return_value: request_forward)
end
example_name = "#{params[:expected_result]} metadata request"
diff --git a/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
index e6b3dc74b74..86b6975bf9f 100644
--- a/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
@@ -10,9 +10,10 @@ end
RSpec.shared_examples 'accept package tags request' do |status:|
using RSpec::Parameterized::TableSyntax
+ include_context 'dependency proxy helpers context'
before do
- stub_application_setting(npm_package_requests_forwarding: false)
+ allow_fetch_application_setting(attribute: "npm_package_requests_forwarding", return_value: false)
end
context 'with valid package name' do
diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
index 8a351226123..ed6d9ed43c8 100644
--- a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
@@ -228,6 +228,35 @@ RSpec.shared_examples 'pypi simple API endpoint' do
it_behaves_like 'PyPI package versions', :developer, :success
end
+
+ context 'package request forward' do
+ include_context 'dependency proxy helpers context'
+
+ where(:forward, :package_in_project, :shared_examples_name, :expected_status) do
+ true | true | 'PyPI package versions' | :success
+ true | false | 'process PyPI api request' | :redirect
+ false | true | 'PyPI package versions' | :success
+ false | false | 'process PyPI api request' | :not_found
+ end
+
+ with_them do
+ let_it_be(:package) { create(:pypi_package, project: project, name: 'foobar') }
+
+ let(:package_name) do
+ if package_in_project
+ 'foobar'
+ else
+ 'barfoo'
+ end
+ end
+
+ before do
+ allow_fetch_application_setting(attribute: "pypi_package_requests_forwarding", return_value: forward)
+ end
+
+ it_behaves_like params[:shared_examples_name], :reporter, params[:expected_status]
+ end
+ end
end
RSpec.shared_examples 'pypi file download endpoint' do
diff --git a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb
index afc902dd184..104e91add8b 100644
--- a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb
@@ -128,17 +128,25 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name|
if issuable_name == 'merge_request'
it 'calls update service with :use_specialized_service param' do
- expect(::MergeRequests::UpdateService).to receive(:new).with(project: project, current_user: user, params: hash_including(use_specialized_service: true))
-
- post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user), params: { duration: '2h' }
+ expect(::MergeRequests::UpdateService).to receive(:new).with(
+ project: project,
+ current_user: user,
+ params: hash_including(
+ use_specialized_service: true,
+ spend_time: hash_including(duration: 7200, summary: 'summary')))
+
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user), params: { duration: '2h', summary: 'summary' }
end
end
if issuable_name == 'issue'
it 'calls update service without :use_specialized_service param' do
- expect(::Issues::UpdateService).to receive(:new).with(project: project, current_user: user, params: hash_not_including(use_specialized_service: true))
+ expect(::Issues::UpdateService).to receive(:new).with(
+ project: project,
+ current_user: user,
+ params: { spend_time: { duration: 3600, summary: 'summary', user_id: user.id } })
- post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user), params: { duration: '2h' }
+ post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user), params: { duration: '1h', summary: 'summary' }
end
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 7608f1c7f8a..32adf98969c 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
@@ -63,3 +63,19 @@ end
RSpec.shared_examples 'diff file discussion entity' do
it_behaves_like 'diff file base entity'
end
+
+RSpec.shared_examples 'diff file with conflict_type' do
+ describe '#conflict_type' do
+ it 'returns nil by default' do
+ expect(subject[:conflict_type]).to be_nil
+ end
+
+ context 'when there is matching conflict file' do
+ let(:options) { { conflicts: { diff_file.new_path => double(diff_lines_for_serializer: [], conflict_type: :both_modified) } } }
+
+ it 'returns false' do
+ expect(subject[:conflict_type]).to eq(:both_modified)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb
index 7d4fbeea0dc..d9b837258ce 100644
--- a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb
@@ -100,8 +100,8 @@ RSpec.shared_examples 'issues move service' do |group|
create(:labeled_issue, project: project, labels: [bug, development], assignees: [assignee])
end
- it 'returns false' do
- expect(described_class.new(parent, user, params).execute(issue)).to eq false
+ it 'returns nil' do
+ expect(described_class.new(parent, user, params).execute(issue)).to be_nil
end
it 'keeps issues labels' do
diff --git a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
index eafcbd77040..f6e25ee6647 100644
--- a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
@@ -69,6 +69,10 @@ RSpec.shared_examples 'a browsable' do
end
RSpec.shared_examples 'an accessible' do
+ before do
+ stub_feature_flags(container_registry_migration_phase1: false)
+ end
+
let(:access) do
[{ 'type' => 'repository',
'name' => project.full_path,
@@ -203,9 +207,7 @@ RSpec.shared_examples 'a container registry auth service' do
end
end
- context 'for private project' do
- let_it_be(:project) { create(:project) }
-
+ shared_examples 'private project' do
context 'allow to use scope-less authentication' do
it_behaves_like 'a valid token'
end
@@ -345,8 +347,20 @@ RSpec.shared_examples 'a container registry auth service' do
end
end
- context 'for public project' do
- let_it_be(:project) { create(:project, :public) }
+ context 'for private project' do
+ let_it_be_with_reload(:project) { create(:project) }
+
+ it_behaves_like 'private project'
+ end
+
+ context 'for public project with private container registry' do
+ let_it_be_with_reload(:project) { create(:project, :public, :container_registry_private) }
+
+ it_behaves_like 'private project'
+ end
+
+ context 'for public project with container_registry `enabled`' do
+ let_it_be(:project) { create(:project, :public, :container_registry_enabled) }
context 'allow anyone to pull images' do
let(:current_params) do
@@ -394,8 +408,8 @@ RSpec.shared_examples 'a container registry auth service' do
end
end
- context 'for internal project' do
- let_it_be(:project) { create(:project, :internal) }
+ context 'for internal project with container_registry `enabled`' do
+ let_it_be(:project) { create(:project, :internal, :container_registry_enabled) }
context 'for internal user' do
context 'allow anyone to pull images' do
@@ -470,6 +484,12 @@ RSpec.shared_examples 'a container registry auth service' do
end
end
end
+
+ context 'for internal project with private container registry' do
+ let_it_be_with_reload(:project) { create(:project, :internal, :container_registry_private) }
+
+ it_behaves_like 'private project'
+ end
end
context 'delete authorized as maintainer' do
@@ -630,12 +650,8 @@ RSpec.shared_examples 'a container registry auth service' do
end
end
- context 'for project with private container registry' do
- let_it_be(:project, reload: true) { create(:project, :public) }
-
- before do
- project.project_feature.update!(container_registry_access_level: ProjectFeature::PRIVATE)
- end
+ context 'for public project with private container registry' do
+ let_it_be_with_reload(:project) { create(:project, :public, :container_registry_private) }
it_behaves_like 'pullable for being team member'
@@ -675,11 +691,7 @@ RSpec.shared_examples 'a container registry auth service' do
end
context 'for project without container registry' do
- let_it_be(:project) { create(:project, :public, container_registry_enabled: false) }
-
- before do
- project.update!(container_registry_enabled: false)
- end
+ let_it_be_with_reload(:project) { create(:project, :public, :container_registry_disabled) }
context 'disallow when pulling' do
let(:current_params) do
@@ -719,12 +731,16 @@ RSpec.shared_examples 'a container registry auth service' do
context 'support for multiple scopes' do
let_it_be(:internal_project) { create(:project, :internal) }
let_it_be(:private_project) { create(:project, :private) }
+ let_it_be(:public_project) { create(:project, :public) }
+ let_it_be(:public_project_private_container_registry) { create(:project, :public, :container_registry_private) }
let(:current_params) do
{
scopes: [
"repository:#{internal_project.full_path}:pull",
- "repository:#{private_project.full_path}:pull"
+ "repository:#{private_project.full_path}:pull",
+ "repository:#{public_project.full_path}:pull",
+ "repository:#{public_project_private_container_registry.full_path}:pull"
]
}
end
@@ -744,13 +760,19 @@ RSpec.shared_examples 'a container registry auth service' do
'actions' => ['pull'] },
{ 'type' => 'repository',
'name' => private_project.full_path,
+ 'actions' => ['pull'] },
+ { 'type' => 'repository',
+ 'name' => public_project.full_path,
+ 'actions' => ['pull'] },
+ { 'type' => 'repository',
+ 'name' => public_project_private_container_registry.full_path,
'actions' => ['pull'] }
]
end
end
end
- context 'user only has access to internal project' do
+ context 'user only has access to internal and public projects' do
let_it_be(:current_user) { create(:user) }
it_behaves_like 'a browsable' do
@@ -758,16 +780,35 @@ RSpec.shared_examples 'a container registry auth service' do
[
{ 'type' => 'repository',
'name' => internal_project.full_path,
+ 'actions' => ['pull'] },
+ { 'type' => 'repository',
+ 'name' => public_project.full_path,
'actions' => ['pull'] }
]
end
end
end
- context 'anonymous access is rejected' do
+ context 'anonymous user has access only to public project' do
let(:current_user) { nil }
- it_behaves_like 'a forbidden'
+ it_behaves_like 'a browsable' do
+ let(:access) do
+ [
+ { 'type' => 'repository',
+ 'name' => public_project.full_path,
+ 'actions' => ['pull'] }
+ ]
+ end
+ end
+
+ context 'with no public container registry' do
+ before do
+ public_project.project_feature.update_column(:container_registry_access_level, ProjectFeature::PRIVATE)
+ end
+
+ it_behaves_like 'a forbidden'
+ end
end
end
@@ -796,8 +837,8 @@ RSpec.shared_examples 'a container registry auth service' do
it_behaves_like 'a forbidden'
end
- context 'for public project' do
- let_it_be(:project) { create(:project, :public) }
+ context 'for public project with container registry `enabled`' do
+ let_it_be_with_reload(:project) { create(:project, :public, :container_registry_enabled) }
context 'when pulling and pushing' do
let(:current_params) do
@@ -818,6 +859,19 @@ RSpec.shared_examples 'a container registry auth service' do
end
end
+ context 'for public project with container registry `private`' do
+ let_it_be_with_reload(:project) { create(:project, :public, :container_registry_private) }
+
+ context 'when pulling and pushing' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull,push"] }
+ end
+
+ it_behaves_like 'a forbidden'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+
context 'for registry catalog' do
let(:current_params) do
{ scopes: ["registry:catalog:*"] }
@@ -830,15 +884,15 @@ RSpec.shared_examples 'a container registry auth service' do
context 'for deploy tokens' do
let(:current_params) do
- { scopes: ["repository:#{project.full_path}:pull"] }
+ { scopes: ["repository:#{project.full_path}:pull"], deploy_token: deploy_token }
end
context 'when deploy token has read and write registry as scopes' do
- let(:current_user) { create(:deploy_token, write_registry: true, projects: [project]) }
+ let(:deploy_token) { create(:deploy_token, write_registry: true, projects: [project]) }
shared_examples 'able to login' do
context 'registry provides read_container_image authentication_abilities' do
- let(:current_params) { {} }
+ let(:current_params) { { deploy_token: deploy_token } }
let(:authentication_abilities) { [:read_container_image] }
it_behaves_like 'an authenticated'
@@ -854,7 +908,7 @@ RSpec.shared_examples 'a container registry auth service' do
context 'when pushing' do
let(:current_params) do
- { scopes: ["repository:#{project.full_path}:push"] }
+ { scopes: ["repository:#{project.full_path}:push"], deploy_token: deploy_token }
end
it_behaves_like 'a pushable'
@@ -872,7 +926,7 @@ RSpec.shared_examples 'a container registry auth service' do
context 'when pushing' do
let(:current_params) do
- { scopes: ["repository:#{project.full_path}:push"] }
+ { scopes: ["repository:#{project.full_path}:push"], deploy_token: deploy_token }
end
it_behaves_like 'a pushable'
@@ -890,7 +944,25 @@ RSpec.shared_examples 'a container registry auth service' do
context 'when pushing' do
let(:current_params) do
- { scopes: ["repository:#{project.full_path}:push"] }
+ { scopes: ["repository:#{project.full_path}:push"], deploy_token: deploy_token }
+ end
+
+ it_behaves_like 'a pushable'
+ end
+
+ it_behaves_like 'able to login'
+ end
+
+ context 'for public project with private container registry' do
+ let_it_be_with_reload(:project) { create(:project, :public, :container_registry_private) }
+
+ context 'when pulling' do
+ it_behaves_like 'a pullable'
+ end
+
+ context 'when pushing' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:push"], deploy_token: deploy_token }
end
it_behaves_like 'a pushable'
@@ -901,26 +973,26 @@ RSpec.shared_examples 'a container registry auth service' do
end
context 'when deploy token does not have read_registry scope' do
- let(:current_user) { create(:deploy_token, projects: [project], read_registry: false) }
+ let(:deploy_token) do
+ create(:deploy_token, projects: [project], read_registry: false)
+ end
shared_examples 'unable to login' do
context 'registry provides no container authentication_abilities' do
- let(:current_params) { {} }
let(:authentication_abilities) { [] }
it_behaves_like 'a forbidden'
end
context 'registry provides inapplicable container authentication_abilities' do
- let(:current_params) { {} }
let(:authentication_abilities) { [:download_code] }
it_behaves_like 'a forbidden'
end
end
- context 'for public project' do
- let_it_be(:project) { create(:project, :public) }
+ context 'for public project with container registry `enabled`' do
+ let_it_be_with_reload(:project) { create(:project, :public, :container_registry_enabled) }
context 'when pulling' do
it_behaves_like 'a pullable'
@@ -929,6 +1001,16 @@ RSpec.shared_examples 'a container registry auth service' do
it_behaves_like 'unable to login'
end
+ context 'for public project with container registry `private`' do
+ let_it_be_with_reload(:project) { create(:project, :public, :container_registry_private) }
+
+ context 'when pulling' do
+ it_behaves_like 'an inaccessible'
+ end
+
+ it_behaves_like 'unable to login'
+ end
+
context 'for internal project' do
let_it_be(:project) { create(:project, :internal) }
@@ -958,16 +1040,24 @@ RSpec.shared_examples 'a container registry auth service' do
end
context 'when deploy token is not related to the project' do
- let_it_be(:current_user) { create(:deploy_token, read_registry: false) }
+ let_it_be(:deploy_token) { create(:deploy_token, read_registry: false) }
- context 'for public project' do
- let_it_be(:project) { create(:project, :public) }
+ context 'for public project with container registry `enabled`' do
+ let_it_be_with_reload(:project) { create(:project, :public, :container_registry_enabled) }
context 'when pulling' do
it_behaves_like 'a pullable'
end
end
+ context 'for public project with container registry `private`' do
+ let_it_be_with_reload(:project) { create(:project, :public, :container_registry_private) }
+
+ context 'when pulling' do
+ it_behaves_like 'an inaccessible'
+ end
+ end
+
context 'for internal project' do
let_it_be(:project) { create(:project, :internal) }
@@ -986,14 +1076,20 @@ RSpec.shared_examples 'a container registry auth service' do
end
context 'when deploy token has been revoked' do
- let(:current_user) { create(:deploy_token, :revoked, projects: [project]) }
+ let(:deploy_token) { create(:deploy_token, :revoked, projects: [project]) }
- context 'for public project' do
- let_it_be(:project) { create(:project, :public) }
+ context 'for public project with container registry `enabled`' do
+ let_it_be(:project) { create(:project, :public, :container_registry_enabled) }
it_behaves_like 'a pullable'
end
+ context 'for public project with container registry `private`' do
+ let_it_be(:project) { create(:project, :public, :container_registry_private) }
+
+ it_behaves_like 'an inaccessible'
+ end
+
context 'for internal project' do
let_it_be(:project) { create(:project, :internal) }
diff --git a/spec/support/shared_examples/services/jira/requests/base_shared_examples.rb b/spec/support/shared_examples/services/jira/requests/base_shared_examples.rb
new file mode 100644
index 00000000000..56a6d24d557
--- /dev/null
+++ b/spec/support/shared_examples/services/jira/requests/base_shared_examples.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a service that handles Jira API errors' do
+ include AfterNextHelpers
+ using RSpec::Parameterized::TableSyntax
+
+ where(:exception_class, :exception_message, :expected_message) do
+ Errno::ECONNRESET | '' | 'A connection error occurred'
+ Errno::ECONNREFUSED | '' | 'A connection error occurred'
+ Errno::ETIMEDOUT | '' | 'A timeout error occurred'
+ Timeout::Error | '' | 'A timeout error occurred'
+ URI::InvalidURIError | '' | 'The Jira API URL'
+ SocketError | '' | 'The Jira API URL'
+ OpenSSL::SSL::SSLError | 'foo' | 'An SSL error occurred while connecting to Jira: foo'
+ JIRA::HTTPError | 'Unauthorized' | 'The credentials for accessing Jira are not valid'
+ JIRA::HTTPError | 'Forbidden' | 'The credentials for accessing Jira are not allowed'
+ JIRA::HTTPError | 'Bad Request' | 'An error occurred while requesting data from Jira'
+ JIRA::HTTPError | 'Foo' | 'An error occurred while requesting data from Jira.'
+ JIRA::HTTPError | '{"errorMessages":["foo","bar"]}' | 'An error occurred while requesting data from Jira: foo and bar'
+ JIRA::HTTPError | '{"errorMessages":[""]}' | 'An error occurred while requesting data from Jira.'
+ end
+
+ with_them do
+ it 'handles the error' do
+ stub_client_and_raise(exception_class, exception_message)
+
+ expect(subject).to be_a(ServiceResponse)
+ expect(subject).to be_error
+ expect(subject.message).to include(expected_message)
+ end
+ end
+
+ context 'when the JSON in JIRA::HTTPError is unsafe' do
+ before do
+ stub_client_and_raise(JIRA::HTTPError, error)
+ end
+
+ context 'when JSON is malformed' do
+ let(:error) { '{"errorMessages":' }
+
+ it 'returns the default error message' do
+ expect(subject.message).to eq('An error occurred while requesting data from Jira. Check your Jira integration configuration and try again.')
+ end
+ end
+
+ context 'when JSON contains tags' do
+ let(:error) { '{"errorMessages":["<script>alert(true)</script>foo"]}' }
+
+ it 'sanitizes it' do
+ expect(subject.message).to eq('An error occurred while requesting data from Jira: foo. Check your Jira integration configuration and try again.')
+ end
+ end
+ end
+
+ it 'allows unknown exception classes to bubble' do
+ stub_client_and_raise(StandardError)
+
+ expect { subject }.to raise_exception(StandardError)
+ end
+
+ it 'logs the error' do
+ stub_client_and_raise(Timeout::Error, 'foo')
+
+ expect(Gitlab::ProjectServiceLogger).to receive(:error).with(
+ hash_including(
+ client_url: be_present,
+ message: 'Error sending message',
+ service_class: described_class.name,
+ error: hash_including(
+ exception_class: Timeout::Error.name,
+ exception_message: 'foo',
+ exception_backtrace: be_present
+ )
+ )
+ )
+ expect(subject).to be_error
+ end
+
+ def stub_client_and_raise(exception_class, message = '')
+ # `JIRA::HTTPError` classes take a response from the JIRA API, rather than a `String`.
+ message = double(body: message) if exception_class == JIRA::HTTPError
+
+ allow_next(JIRA::Client).to receive(:get).and_raise(exception_class, message)
+ end
+end
diff --git a/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb b/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb
index 9ffeba1b1d0..c979fdc2bb0 100644
--- a/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb
+++ b/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb
@@ -1,165 +1,259 @@
# frozen_string_literal: true
RSpec.shared_examples 'Generate Debian Distribution and component files' do
- let_it_be(:component_main) { create("debian_#{container_type}_component", distribution: distribution, name: 'main') }
- let_it_be(:component_contrib) { create("debian_#{container_type}_component", distribution: distribution, name: 'contrib') }
-
- let_it_be(:architecture_all) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'all') }
- let_it_be(:architecture_amd64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'amd64') }
- let_it_be(:architecture_arm64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'arm64') }
-
- let_it_be(:component_file1) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T08:00:00Z', file_sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', file_md5: 'd41d8cd98f00b204e9800998ecf8427e', file_fixture: nil, size: 0) } # updated
- let_it_be(:component_file2) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_all, updated_at: '2020-01-24T09:00:00Z', file_sha256: 'a') } # destroyed
- let_it_be(:component_file3) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_amd64, updated_at: '2020-01-24T10:54:59Z', file_sha256: 'b') } # destroyed, 1 second before last generation
- let_it_be(:component_file4) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'c') } # kept, last generation
- let_it_be(:component_file5) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'd') } # kept, last generation
- let_it_be(:component_file6) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-25T15:17:18Z', file_sha256: 'e') } # kept, less than 1 hour ago
-
- def check_component_file(release_date, component_name, component_file_type, architecture_name, expected_content)
- component_file = distribution
- .component_files
- .with_component_name(component_name)
- .with_file_type(component_file_type)
- .with_architecture_name(architecture_name)
- .order_updated_asc
- .last
-
- expect(component_file).not_to be_nil
- expect(component_file.updated_at).to eq(release_date)
-
- unless expected_content.nil?
- component_file.file.use_file do |file_path|
- expect(File.read(file_path)).to eq(expected_content)
- end
+ def check_release_files(expected_release_content)
+ distribution.reload
+
+ distribution.file.use_file do |file_path|
+ expect(File.read(file_path)).to eq(expected_release_content)
+ end
+
+ expect(distribution.file_signature).to start_with("-----BEGIN PGP SIGNATURE-----\n")
+ expect(distribution.file_signature).to end_with("\n-----END PGP SIGNATURE-----\n")
+
+ distribution.signed_file.use_file do |file_path|
+ expect(File.read(file_path)).to start_with("-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA512\n\n#{expected_release_content}-----BEGIN PGP SIGNATURE-----\n")
+ expect(File.read(file_path)).to end_with("\n-----END PGP SIGNATURE-----\n")
end
end
- it 'generates Debian distribution and component files', :aggregate_failures do
- current_time = Time.utc(2020, 01, 25, 15, 17, 18, 123456)
-
- travel_to(current_time) do
- expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
-
- initial_count = 6
- destroyed_count = 2
- # updated_count = 1
- created_count = 5
-
- expect { subject }
- .to not_change { Packages::Package.count }
- .and not_change { Packages::PackageFile.count }
- .and change { distribution.reload.updated_at }.to(current_time.round)
- .and change { distribution.component_files.reset.count }.from(initial_count).to(initial_count - destroyed_count + created_count)
- .and change { component_file1.reload.updated_at }.to(current_time.round)
-
- debs = package.package_files.with_debian_file_type(:deb).preload_debian_file_metadata.to_a
- pool_prefix = "pool/unstable/#{project.id}/p/#{package.name}"
- expected_main_amd64_content = <<~EOF
- Package: libsample0
- Source: #{package.name}
- Version: #{package.version}
- Installed-Size: 7
- Maintainer: #{debs[0].debian_fields['Maintainer']}
- Architecture: amd64
- Description: Some mostly empty lib
- Used in GitLab tests.
- .
- Testing another paragraph.
- Multi-Arch: same
- Homepage: #{debs[0].debian_fields['Homepage']}
- Section: libs
- Priority: optional
- Filename: #{pool_prefix}/libsample0_1.2.3~alpha2_amd64.deb
- Size: 409600
- MD5sum: #{debs[0].file_md5}
- SHA256: #{debs[0].file_sha256}
-
- Package: sample-dev
- Source: #{package.name} (#{package.version})
- Version: 1.2.3~binary
- Installed-Size: 7
- Maintainer: #{debs[1].debian_fields['Maintainer']}
- Architecture: amd64
- Depends: libsample0 (= 1.2.3~binary)
- Description: Some mostly empty development files
- Used in GitLab tests.
- .
- Testing another paragraph.
- Multi-Arch: same
- Homepage: #{debs[1].debian_fields['Homepage']}
- Section: libdevel
- Priority: optional
- Filename: #{pool_prefix}/sample-dev_1.2.3~binary_amd64.deb
- Size: 409600
- MD5sum: #{debs[1].file_md5}
- SHA256: #{debs[1].file_sha256}
- EOF
-
- check_component_file(current_time.round, 'main', :packages, 'all', nil)
- check_component_file(current_time.round, 'main', :packages, 'amd64', expected_main_amd64_content)
- check_component_file(current_time.round, 'main', :packages, 'arm64', nil)
-
- check_component_file(current_time.round, 'contrib', :packages, 'all', nil)
- check_component_file(current_time.round, 'contrib', :packages, 'amd64', nil)
- check_component_file(current_time.round, 'contrib', :packages, 'arm64', nil)
-
- main_amd64_size = expected_main_amd64_content.length
- main_amd64_md5sum = Digest::MD5.hexdigest(expected_main_amd64_content)
- main_amd64_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_content)
-
- contrib_all_size = component_file1.size
- contrib_all_md5sum = component_file1.file_md5
- contrib_all_sha256 = component_file1.file_sha256
-
- expected_release_content = <<~EOF
- Codename: unstable
- Date: Sat, 25 Jan 2020 15:17:18 +0000
- Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000
- Architectures: all amd64 arm64
- Components: contrib main
- MD5Sum:
- #{contrib_all_md5sum} #{contrib_all_size} contrib/binary-all/Packages
- d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-amd64/Packages
- d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-arm64/Packages
- d41d8cd98f00b204e9800998ecf8427e 0 main/binary-all/Packages
- #{main_amd64_md5sum} #{main_amd64_size} main/binary-amd64/Packages
- d41d8cd98f00b204e9800998ecf8427e 0 main/binary-arm64/Packages
- SHA256:
- #{contrib_all_sha256} #{contrib_all_size} contrib/binary-all/Packages
- e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-amd64/Packages
- e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-arm64/Packages
- e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-all/Packages
- #{main_amd64_sha256} #{main_amd64_size} main/binary-amd64/Packages
- e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-arm64/Packages
- EOF
-
- distribution.file.use_file do |file_path|
- expect(File.read(file_path)).to eq(expected_release_content)
+ context 'with Debian components and architectures' do
+ let_it_be(:component_main) { create("debian_#{container_type}_component", distribution: distribution, name: 'main') }
+ let_it_be(:component_contrib) { create("debian_#{container_type}_component", distribution: distribution, name: 'contrib') }
+
+ let_it_be(:architecture_all) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'all') }
+ let_it_be(:architecture_amd64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'amd64') }
+ let_it_be(:architecture_arm64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'arm64') }
+
+ let_it_be(:component_file1) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T08:00:00Z', file_sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', file_md5: 'd41d8cd98f00b204e9800998ecf8427e', file_fixture: nil, size: 0) } # updated
+ let_it_be(:component_file2) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_all, updated_at: '2020-01-24T09:00:00Z', file_sha256: 'a') } # destroyed
+ let_it_be(:component_file3) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_amd64, updated_at: '2020-01-24T10:54:59Z', file_sha256: 'b') } # destroyed, 1 second before last generation
+ let_it_be(:component_file4) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'c') } # kept, last generation
+ let_it_be(:component_file5) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'd') } # kept, last generation
+ let_it_be(:component_file6) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-25T15:17:18Z', file_sha256: 'e') } # kept, less than 1 hour ago
+
+ def check_component_file(release_date, component_name, component_file_type, architecture_name, expected_content)
+ component_file = distribution
+ .component_files
+ .with_component_name(component_name)
+ .with_file_type(component_file_type)
+ .with_architecture_name(architecture_name)
+ .order_updated_asc
+ .last
+
+ expect(component_file).not_to be_nil
+ expect(component_file.updated_at).to eq(release_date)
+
+ unless expected_content.nil?
+ component_file.file.use_file do |file_path|
+ expect(File.read(file_path)).to eq(expected_content)
+ end
end
end
+
+ it 'generates Debian distribution and component files', :aggregate_failures do
+ current_time = Time.utc(2020, 01, 25, 15, 17, 18, 123456)
+
+ travel_to(current_time) do
+ expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
+
+ components_count = 2
+ architectures_count = 3
+
+ initial_count = 6
+ destroyed_count = 2
+ updated_count = 1
+ created_count = components_count * (architectures_count * 2 + 1) - updated_count
+
+ expect { subject }
+ .to not_change { Packages::Package.count }
+ .and not_change { Packages::PackageFile.count }
+ .and change { distribution.reload.updated_at }.to(current_time.round)
+ .and change { distribution.component_files.reset.count }.from(initial_count).to(initial_count - destroyed_count + created_count)
+ .and change { component_file1.reload.updated_at }.to(current_time.round)
+
+ package_files = package.package_files.order(id: :asc).preload_debian_file_metadata.to_a
+ pool_prefix = 'pool/unstable'
+ pool_prefix += "/#{project.id}" if container_type == :group
+ pool_prefix += "/p/#{package.name}/#{package.version}"
+ expected_main_amd64_content = <<~EOF
+ Package: libsample0
+ Source: #{package.name}
+ Version: #{package.version}
+ Installed-Size: 7
+ Maintainer: #{package_files[2].debian_fields['Maintainer']}
+ Architecture: amd64
+ Description: Some mostly empty lib
+ Used in GitLab tests.
+ .
+ Testing another paragraph.
+ Multi-Arch: same
+ Homepage: #{package_files[2].debian_fields['Homepage']}
+ Section: libs
+ Priority: optional
+ Filename: #{pool_prefix}/libsample0_1.2.3~alpha2_amd64.deb
+ Size: 409600
+ MD5sum: #{package_files[2].file_md5}
+ SHA256: #{package_files[2].file_sha256}
+
+ Package: sample-dev
+ Source: #{package.name} (#{package.version})
+ Version: 1.2.3~binary
+ Installed-Size: 7
+ Maintainer: #{package_files[3].debian_fields['Maintainer']}
+ Architecture: amd64
+ Depends: libsample0 (= 1.2.3~binary)
+ Description: Some mostly empty development files
+ Used in GitLab tests.
+ .
+ Testing another paragraph.
+ Multi-Arch: same
+ Homepage: #{package_files[3].debian_fields['Homepage']}
+ Section: libdevel
+ Priority: optional
+ Filename: #{pool_prefix}/sample-dev_1.2.3~binary_amd64.deb
+ Size: 409600
+ MD5sum: #{package_files[3].file_md5}
+ SHA256: #{package_files[3].file_sha256}
+ EOF
+
+ expected_main_amd64_di_content = <<~EOF
+ Section: misc
+ Priority: extra
+ Filename: #{pool_prefix}/sample-udeb_1.2.3~alpha2_amd64.udeb
+ Size: 409600
+ MD5sum: #{package_files[4].file_md5}
+ SHA256: #{package_files[4].file_sha256}
+ EOF
+
+ expected_main_source_content = <<~EOF
+ Package: #{package.name}
+ Binary: sample-dev, libsample0, sample-udeb
+ Version: #{package.version}
+ Maintainer: #{package_files[1].debian_fields['Maintainer']}
+ Build-Depends: debhelper-compat (= 13)
+ Architecture: any
+ Standards-Version: 4.5.0
+ Format: 3.0 (native)
+ Files:
+ #{package_files[1].file_md5} #{package_files[1].size} #{package_files[1].file_name}
+ d5ca476e4229d135a88f9c729c7606c9 864 sample_1.2.3~alpha2.tar.xz
+ Checksums-Sha256:
+ #{package_files[1].file_sha256} #{package_files[1].size} #{package_files[1].file_name}
+ 40e4682bb24a73251ccd7c7798c0094a649091e5625d6a14bcec9b4e7174f3da 864 sample_1.2.3~alpha2.tar.xz
+ Checksums-Sha1:
+ #{package_files[1].file_sha1} #{package_files[1].size} #{package_files[1].file_name}
+ c5cfc111ea924842a89a06d5673f07dfd07de8ca 864 sample_1.2.3~alpha2.tar.xz
+ Homepage: #{package_files[1].debian_fields['Homepage']}
+ Section: misc
+ Priority: extra
+ Directory: #{pool_prefix}
+ EOF
+
+ check_component_file(current_time.round, 'main', :packages, 'all', nil)
+ check_component_file(current_time.round, 'main', :packages, 'amd64', expected_main_amd64_content)
+ check_component_file(current_time.round, 'main', :packages, 'arm64', nil)
+
+ check_component_file(current_time.round, 'main', :di_packages, 'all', nil)
+ check_component_file(current_time.round, 'main', :di_packages, 'amd64', expected_main_amd64_di_content)
+ check_component_file(current_time.round, 'main', :di_packages, 'arm64', nil)
+
+ check_component_file(current_time.round, 'main', :source, nil, expected_main_source_content)
+
+ check_component_file(current_time.round, 'contrib', :packages, 'all', nil)
+ check_component_file(current_time.round, 'contrib', :packages, 'amd64', nil)
+ check_component_file(current_time.round, 'contrib', :packages, 'arm64', nil)
+
+ check_component_file(current_time.round, 'contrib', :di_packages, 'all', nil)
+ check_component_file(current_time.round, 'contrib', :di_packages, 'amd64', nil)
+ check_component_file(current_time.round, 'contrib', :di_packages, 'arm64', nil)
+
+ check_component_file(current_time.round, 'contrib', :source, nil, nil)
+
+ main_amd64_size = expected_main_amd64_content.length
+ main_amd64_md5sum = Digest::MD5.hexdigest(expected_main_amd64_content)
+ main_amd64_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_content)
+
+ contrib_all_size = component_file1.size
+ contrib_all_md5sum = component_file1.file_md5
+ contrib_all_sha256 = component_file1.file_sha256
+
+ main_amd64_di_size = expected_main_amd64_di_content.length
+ main_amd64_di_md5sum = Digest::MD5.hexdigest(expected_main_amd64_di_content)
+ main_amd64_di_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_di_content)
+
+ main_source_size = expected_main_source_content.length
+ main_source_md5sum = Digest::MD5.hexdigest(expected_main_source_content)
+ main_source_sha256 = Digest::SHA256.hexdigest(expected_main_source_content)
+
+ expected_release_content = <<~EOF
+ Codename: unstable
+ Date: Sat, 25 Jan 2020 15:17:18 +0000
+ Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000
+ Architectures: all amd64 arm64
+ Components: contrib main
+ MD5Sum:
+ #{contrib_all_md5sum} #{contrib_all_size} contrib/binary-all/Packages
+ d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-all/Packages
+ d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-amd64/Packages
+ d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-amd64/Packages
+ d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-arm64/Packages
+ d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-arm64/Packages
+ d41d8cd98f00b204e9800998ecf8427e 0 contrib/source/Source
+ d41d8cd98f00b204e9800998ecf8427e 0 main/binary-all/Packages
+ d41d8cd98f00b204e9800998ecf8427e 0 main/debian-installer/binary-all/Packages
+ #{main_amd64_md5sum} #{main_amd64_size} main/binary-amd64/Packages
+ #{main_amd64_di_md5sum} #{main_amd64_di_size} main/debian-installer/binary-amd64/Packages
+ d41d8cd98f00b204e9800998ecf8427e 0 main/binary-arm64/Packages
+ d41d8cd98f00b204e9800998ecf8427e 0 main/debian-installer/binary-arm64/Packages
+ #{main_source_md5sum} #{main_source_size} main/source/Source
+ SHA256:
+ #{contrib_all_sha256} #{contrib_all_size} contrib/binary-all/Packages
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-all/Packages
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-amd64/Packages
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-amd64/Packages
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-arm64/Packages
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-arm64/Packages
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/source/Source
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-all/Packages
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/debian-installer/binary-all/Packages
+ #{main_amd64_sha256} #{main_amd64_size} main/binary-amd64/Packages
+ #{main_amd64_di_sha256} #{main_amd64_di_size} main/debian-installer/binary-amd64/Packages
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-arm64/Packages
+ e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/debian-installer/binary-arm64/Packages
+ #{main_source_sha256} #{main_source_size} main/source/Source
+ EOF
+
+ check_release_files(expected_release_content)
+ end
+
+ create_list(:debian_package, 10, project: project, published_in: project_distribution)
+ control_count = ActiveRecord::QueryRecorder.new { subject2 }.count
+
+ create_list(:debian_package, 10, project: project, published_in: project_distribution)
+ expect { subject3 }.not_to exceed_query_limit(control_count)
+ end
end
-end
-RSpec.shared_examples 'Generate minimal Debian Distribution' do
- it 'generates minimal distribution', :aggregate_failures do
- travel_to(Time.utc(2020, 01, 25, 15, 17, 18, 123456)) do
- expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
-
- expect { subject }
- .to not_change { Packages::Package.count }
- .and not_change { Packages::PackageFile.count }
- .and not_change { distribution.component_files.reset.count }
-
- expected_release_content = <<~EOF
- Codename: unstable
- Date: Sat, 25 Jan 2020 15:17:18 +0000
- Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000
- MD5Sum:
- SHA256:
- EOF
-
- distribution.file.use_file do |file_path|
- expect(File.read(file_path)).to eq(expected_release_content)
+ context 'without components and architectures' do
+ it 'generates minimal distribution', :aggregate_failures do
+ travel_to(Time.utc(2020, 01, 25, 15, 17, 18, 123456)) do
+ expect(Gitlab::ErrorTracking).not_to receive(:log_exception)
+
+ expect { subject }
+ .to not_change { Packages::Package.count }
+ .and not_change { Packages::PackageFile.count }
+ .and not_change { distribution.component_files.reset.count }
+
+ expected_release_content = <<~EOF
+ Codename: unstable
+ Date: Sat, 25 Jan 2020 15:17:18 +0000
+ Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000
+ MD5Sum:
+ SHA256:
+ EOF
+
+ check_release_files(expected_release_content)
end
end
end