From 9bf8cb8d34039f3cef9e1b2f812ce634f2bebe69 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 17 May 2023 03:07:10 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .../rspec/avoid_conditional_statements.yml | 1 - Gemfile.checksum | 2 +- Gemfile.lock | 2 +- .../components/toolbar_table_button.vue | 92 ++++++++++------- .../stylesheets/components/content_editor.scss | 4 +- .../metrics/dashboard/prometheus_api_proxy.rb | 2 + app/controllers/concerns/metrics_dashboard.rb | 2 + .../projects/metrics_dashboard_controller.rb | 2 + .../projects/prometheus/metrics_controller.rb | 5 + app/helpers/application_settings_helper.rb | 3 +- app/helpers/profiles_helper.rb | 5 + app/models/application_setting_implementation.rb | 3 +- app/models/concerns/recoverable_by_any_email.rb | 45 ++++++++ app/models/namespaces/traversal/linear_scopes.rb | 9 -- app/models/user.rb | 1 + .../_user_restrictions.html.haml | 1 + app/views/profiles/accounts/show.html.haml | 65 +++++++----- .../password_reset_any_verified_email.yml | 8 ++ .../use_traversal_ids_for_ancestor_scopes.yml | 8 -- ...low_account_deletion_to_application_settings.rb | 7 ++ db/schema_migrations/20230428070443 | 1 + db/structure.sql | 1 + doc/user/project/import/github.md | 5 +- locale/gitlab.pot | 11 +- .../controllers/concerns/metrics_dashboard_spec.rb | 14 ++- .../projects/environments_controller_spec.rb | 12 +++ .../projects/grafana_api_controller_spec.rb | 13 +++ .../projects/prometheus/alerts_controller_spec.rb | 16 ++- .../projects/prometheus/metrics_controller_spec.rb | 15 ++- spec/features/tags/developer_creates_tag_spec.rb | 24 +++-- spec/features/users/password_spec.rb | 54 ++++++++++ .../components/toolbar_table_button_spec.js | 97 ++++++++++++++---- spec/helpers/profiles_helper_spec.rb | 6 ++ .../partitioning/foreign_keys_generator_spec.rb | 2 +- spec/mailers/devise_mailer_spec.rb | 16 ++- ...inalize_issues_iid_scoping_to_namespace_spec.rb | 2 +- spec/models/ci/runner_spec.rb | 3 +- .../concerns/recoverable_by_any_email_spec.rb | 113 +++++++++++++++++++++ spec/requests/api/settings_spec.rb | 5 +- spec/requests/projects/metrics_dashboard_spec.rb | 24 +++++ spec/support/helpers/migrations_helpers.rb | 34 ++++++- .../prometheus_api_proxy_shared_examples.rb | 15 +++ .../metrics_dashboard_shared_examples.rb | 17 ++++ .../namespaces/traversal_scope_examples.rb | 50 ++------- 44 files changed, 636 insertions(+), 181 deletions(-) create mode 100644 app/models/concerns/recoverable_by_any_email.rb create mode 100644 config/feature_flags/development/password_reset_any_verified_email.yml delete mode 100644 config/feature_flags/development/use_traversal_ids_for_ancestor_scopes.yml create mode 100644 db/migrate/20230428070443_add_allow_account_deletion_to_application_settings.rb create mode 100644 db/schema_migrations/20230428070443 create mode 100644 spec/models/concerns/recoverable_by_any_email_spec.rb diff --git a/.rubocop_todo/rspec/avoid_conditional_statements.yml b/.rubocop_todo/rspec/avoid_conditional_statements.yml index 43ffaaa452a..4817708667a 100644 --- a/.rubocop_todo/rspec/avoid_conditional_statements.yml +++ b/.rubocop_todo/rspec/avoid_conditional_statements.yml @@ -78,7 +78,6 @@ RSpec/AvoidConditionalStatements: - 'spec/features/projects_spec.rb' - 'spec/features/search/user_uses_header_search_field_spec.rb' - 'spec/features/snippets/explore_spec.rb' - - 'spec/features/tags/developer_creates_tag_spec.rb' - 'spec/features/usage_stats_consent_spec.rb' - 'spec/features/users/login_spec.rb' - 'spec/features/users/overview_spec.rb' diff --git a/Gemfile.checksum b/Gemfile.checksum index 4409e0357fb..edecf558466 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -518,7 +518,7 @@ {"name":"rexml","version":"3.2.5","platform":"ruby","checksum":"a33c3bf95fda7983ec7f05054f3a985af41dbc25a0339843bd2479e93cabb123"}, {"name":"rinku","version":"2.0.0","platform":"ruby","checksum":"3e695aaf9f24baba3af45823b5c427b58a624582132f18482320e2737f9f8a85"}, {"name":"rotp","version":"6.2.0","platform":"ruby","checksum":"239a2eefba6f1bd4157b2c735d0f975598e0ef94823eea2f35d103d2e5cc0787"}, -{"name":"rouge","version":"4.1.0","platform":"ruby","checksum":"0f6fc19a0d66db782f6fa67f56356af4ef001cd43bbd8ad5aa798a081de4dd10"}, +{"name":"rouge","version":"4.1.1","platform":"ruby","checksum":"41cc3ed28de7a9f5c0145bcdbeae8f5c16133065d570e21393aac935a235fd4b"}, {"name":"rqrcode","version":"0.7.0","platform":"ruby","checksum":"8b3a5cba9cc199ba2d781a7c767cb55679f29a3621aa0506a799cec3760d16a1"}, {"name":"rqrcode-rails3","version":"0.1.7","platform":"ruby","checksum":"6f0582f26485123e5ed6f2a8a2871f00d86d353e0f58c8429a5a13212bcf48c4"}, {"name":"rspec","version":"3.12.0","platform":"ruby","checksum":"ccc41799a43509dc0be84070e3f0410ac95cbd480ae7b6c245543eb64162399c"}, diff --git a/Gemfile.lock b/Gemfile.lock index 3d28928425f..b0bc178f8b5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1270,7 +1270,7 @@ GEM rexml (3.2.5) rinku (2.0.0) rotp (6.2.0) - rouge (4.1.0) + rouge (4.1.1) rqrcode (0.7.0) chunky_png rqrcode-rails3 (0.1.7) diff --git a/app/assets/javascripts/content_editor/components/toolbar_table_button.vue b/app/assets/javascripts/content_editor/components/toolbar_table_button.vue index bf2740f9864..a4440659d61 100644 --- a/app/assets/javascripts/content_editor/components/toolbar_table_button.vue +++ b/app/assets/javascripts/content_editor/components/toolbar_table_button.vue @@ -1,11 +1,5 @@ diff --git a/app/assets/stylesheets/components/content_editor.scss b/app/assets/stylesheets/components/content_editor.scss index 4e3fb819f4c..c50fcd9218b 100644 --- a/app/assets/stylesheets/components/content_editor.scss +++ b/app/assets/stylesheets/components/content_editor.scss @@ -155,7 +155,9 @@ } } - +.content-editor-table-dropdown .gl-new-dropdown-panel { + min-width: auto; +} .bubble-menu-form { width: 320px; diff --git a/app/controllers/concerns/metrics/dashboard/prometheus_api_proxy.rb b/app/controllers/concerns/metrics/dashboard/prometheus_api_proxy.rb index ea9fd2de961..6a24a7308b7 100644 --- a/app/controllers/concerns/metrics/dashboard/prometheus_api_proxy.rb +++ b/app/controllers/concerns/metrics/dashboard/prometheus_api_proxy.rb @@ -9,6 +9,8 @@ module Metrics::Dashboard::PrometheusApiProxy end def prometheus_proxy + return not_found if Feature.enabled?(:remove_monitor_metrics) + variable_substitution_result = proxy_variable_substitution_service.new(proxyable, permit_params).execute diff --git a/app/controllers/concerns/metrics_dashboard.rb b/app/controllers/concerns/metrics_dashboard.rb index 7e202235cfa..7a84c597424 100644 --- a/app/controllers/concerns/metrics_dashboard.rb +++ b/app/controllers/concerns/metrics_dashboard.rb @@ -10,6 +10,8 @@ module MetricsDashboard extend ActiveSupport::Concern def metrics_dashboard + return not_found if Feature.enabled?(:remove_monitor_metrics) + result = dashboard_finder.find( project_for_dashboard, current_user, diff --git a/app/controllers/projects/metrics_dashboard_controller.rb b/app/controllers/projects/metrics_dashboard_controller.rb index 510c882d537..c95594d87c0 100644 --- a/app/controllers/projects/metrics_dashboard_controller.rb +++ b/app/controllers/projects/metrics_dashboard_controller.rb @@ -17,6 +17,8 @@ module Projects urgency :low def show + return not_found if Feature.enabled?(:remove_monitor_metrics) + if environment render 'projects/environments/metrics' elsif default_environment diff --git a/app/controllers/projects/prometheus/metrics_controller.rb b/app/controllers/projects/prometheus/metrics_controller.rb index c20c80ba334..396841e667d 100644 --- a/app/controllers/projects/prometheus/metrics_controller.rb +++ b/app/controllers/projects/prometheus/metrics_controller.rb @@ -3,6 +3,7 @@ module Projects module Prometheus class MetricsController < Projects::ApplicationController + before_action :check_feature_availability! before_action :authorize_admin_project! before_action :require_prometheus_metrics! @@ -127,6 +128,10 @@ module Projects def metrics_params params.require(:prometheus_metric).permit(:title, :query, :y_label, :unit, :legend, :group) end + + def check_feature_availability! + render_404 if Feature.enabled?(:remove_monitor_metrics) + end end end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index dab682d88e0..82ebf53334e 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -491,7 +491,8 @@ module ApplicationSettingsHelper :deactivation_email_additional_text, :projects_api_rate_limit_unauthenticated, :gitlab_dedicated_instance, - :ci_max_includes + :ci_max_includes, + :allow_account_deletion ].tap do |settings| next if Gitlab.com? diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb index 979b979fba7..26463003f8d 100644 --- a/app/helpers/profiles_helper.rb +++ b/app/helpers/profiles_helper.rb @@ -68,6 +68,11 @@ module ProfilesHelper def ssh_key_expiration_policy_enabled? false end + + # Overridden in EE::ProfilesHelper#prevent_delete_account? + def prevent_delete_account? + false + end end ProfilesHelper.prepend_mod diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 845d402f550..47ba96e238e 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -253,7 +253,8 @@ module ApplicationSettingImplementation user_defaults_to_private_profile: false, projects_api_rate_limit_unauthenticated: 400, gitlab_dedicated_instance: false, - ci_max_includes: 150 + ci_max_includes: 150, + allow_account_deletion: true }.tap do |hsh| hsh.merge!(non_production_defaults) unless Rails.env.production? end diff --git a/app/models/concerns/recoverable_by_any_email.rb b/app/models/concerns/recoverable_by_any_email.rb new file mode 100644 index 00000000000..aaea7707d51 --- /dev/null +++ b/app/models/concerns/recoverable_by_any_email.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +# Concern that overrides the Devise methods +# to send reset password instructions to any verified user email +module RecoverableByAnyEmail + extend ActiveSupport::Concern + + class_methods do + def send_reset_password_instructions(attributes = {}) + return super unless Feature.enabled?(:password_reset_any_verified_email) + + email = attributes.delete(:email) + super unless email + + recoverable = by_email_with_errors(email) + recoverable.send_reset_password_instructions(to: email) if recoverable&.persisted? + recoverable + end + + private + + def by_email_with_errors(email) + record = find_by_any_email(email, confirmed: true) || new + record.errors.add(:email, :invalid) unless record.persisted? + record + end + end + + def send_reset_password_instructions(opts = {}) + return super() unless Feature.enabled?(:password_reset_any_verified_email) + + token = set_reset_password_token + send_reset_password_instructions_notification(token, opts) + + token + end + + private + + def send_reset_password_instructions_notification(token, opts = {}) + return super(token) unless Feature.enabled?(:password_reset_any_verified_email) + + send_devise_notification(:reset_password_instructions, token, opts) + end +end diff --git a/app/models/namespaces/traversal/linear_scopes.rb b/app/models/namespaces/traversal/linear_scopes.rb index 792964a6c7f..c50d3dd1de6 100644 --- a/app/models/namespaces/traversal/linear_scopes.rb +++ b/app/models/namespaces/traversal/linear_scopes.rb @@ -25,8 +25,6 @@ module Namespaces end def self_and_ancestors(include_self: true, upto: nil, hierarchy_order: nil) - return super unless use_traversal_ids_for_ancestor_scopes? - self_and_ancestors_from_inner_join( include_self: include_self, upto: upto, hierarchy_order: @@ -35,8 +33,6 @@ module Namespaces end def self_and_ancestor_ids(include_self: true) - return super unless use_traversal_ids_for_ancestor_scopes? - self_and_ancestors(include_self: include_self).as_ids end @@ -87,11 +83,6 @@ module Namespaces use_traversal_ids? end - def use_traversal_ids_for_ancestor_scopes? - Feature.enabled?(:use_traversal_ids_for_ancestor_scopes) && - use_traversal_ids? - end - def use_traversal_ids_for_descendants_scopes? Feature.enabled?(:use_traversal_ids_for_descendants_scopes) && use_traversal_ids? diff --git a/app/models/user.rb b/app/models/user.rb index dc70ff2e232..0c8ff873ba6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -91,6 +91,7 @@ class User < ApplicationRecord # Must be included after `devise` include EncryptedUserPassword + include RecoverableByAnyEmail include AdminChangedPasswordNotifier diff --git a/app/views/admin/application_settings/_user_restrictions.html.haml b/app/views/admin/application_settings/_user_restrictions.html.haml index c35056383fa..c21d1ec47e6 100644 --- a/app/views/admin/application_settings/_user_restrictions.html.haml +++ b/app/views/admin/application_settings/_user_restrictions.html.haml @@ -5,3 +5,4 @@ = render_if_exists 'admin/application_settings/updating_name_disabled_for_users', form: form = form.gitlab_ui_checkbox_component :can_create_group, _("Allow new users to create top-level groups") = form.gitlab_ui_checkbox_component :user_defaults_to_private_profile, _("Make new users' profiles private by default") + = render_if_exists 'admin/application_settings/allow_account_deletion', form: form diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 0505a205333..ea8d6b7fda2 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -59,36 +59,45 @@ .col-lg-12 %hr -.row.gl-mt-3.js-search-settings-section - .col-lg-4.profile-settings-sidebar - %h4.gl-mt-0.danger-title - = s_('Profiles|Delete account') - .col-lg-8 - - if current_user.can_be_removed? && can?(current_user, :destroy_user, current_user) +- if prevent_delete_account? + .row.gl-mt-3.js-search-settings-section + .col-lg-4.profile-settings-sidebar + %h4.gl-mt-0.danger-title + = s_('Profiles|Delete account') + .col-lg-8 %p - = s_('Profiles|Deleting an account has the following effects:') - = render 'users/deletion_guidance', user: current_user - - -# Delete button here - = render Pajamas::ButtonComponent.new(variant: :danger, button_options: { id: 'delete-account-button', disabled: true, data: { qa_selector: 'delete_account_button' }}) do + = s_('Profiles|Account deletion is not allowed by your administrator.') +- else + .row.gl-mt-3.js-search-settings-section + .col-lg-4.profile-settings-sidebar + %h4.gl-mt-0.danger-title = s_('Profiles|Delete account') - - #delete-account-modal{ data: { action_url: user_registration_path, - confirm_with_password: ('true' if current_user.confirm_deletion_with_password?), - username: current_user.username } } - - else - - if current_user.solo_owned_groups.present? - %p - = s_('Profiles|Your account is currently an owner in these groups:') - %strong= current_user.solo_owned_groups.map(&:name).join(', ') - %p - = s_('Profiles|You must transfer ownership or delete these groups before you can delete your account.') - - elsif !current_user.can_remove_self? - %p - = s_('Profiles|GitLab is unable to verify your identity automatically. For security purposes, you must set a password by %{openingTag}resetting your password%{closingTag} to delete your account.').html_safe % { openingTag: "".html_safe, closingTag: ''.html_safe} + .col-lg-8 + - if current_user.can_be_removed? && can?(current_user, :destroy_user, current_user) %p - = s_('Profiles|If after setting a password, the option to delete your account is still not available, please %{link_start}submit a request%{link_end} to begin the account deletion process.').html_safe % { link_start: ''.html_safe, link_end: ''.html_safe} + = s_('Profiles|Deleting an account has the following effects:') + = render 'users/deletion_guidance', user: current_user + + -# Delete button here + = render Pajamas::ButtonComponent.new(variant: :danger, button_options: { id: 'delete-account-button', disabled: true, data: { qa_selector: 'delete_account_button' }}) do + = s_('Profiles|Delete account') + + #delete-account-modal{ data: { action_url: user_registration_path, + confirm_with_password: ('true' if current_user.confirm_deletion_with_password?), + username: current_user.username } } - else - %p - = s_("Profiles|You don't have access to delete this user.") + - if current_user.solo_owned_groups.present? + %p + = s_('Profiles|Your account is currently an owner in these groups:') + %strong= current_user.solo_owned_groups.map(&:name).join(', ') + %p + = s_('Profiles|You must transfer ownership or delete these groups before you can delete your account.') + - elsif !current_user.can_remove_self? + %p + = s_('Profiles|GitLab is unable to verify your identity automatically. For security purposes, you must set a password by %{openingTag}resetting your password%{closingTag} to delete your account.').html_safe % { openingTag: "".html_safe, closingTag: ''.html_safe} + %p + = s_('Profiles|If after setting a password, the option to delete your account is still not available, please %{link_start}submit a request%{link_end} to begin the account deletion process.').html_safe % { link_start: ''.html_safe, link_end: ''.html_safe} + - else + %p + = s_("Profiles|You don't have access to delete this user.") .gl-mb-3 diff --git a/config/feature_flags/development/password_reset_any_verified_email.yml b/config/feature_flags/development/password_reset_any_verified_email.yml new file mode 100644 index 00000000000..9438c6ef414 --- /dev/null +++ b/config/feature_flags/development/password_reset_any_verified_email.yml @@ -0,0 +1,8 @@ +--- +name: password_reset_any_verified_email +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119231 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/410038 +milestone: '16.0' +type: development +group: group::authentication and authorization +default_enabled: false diff --git a/config/feature_flags/development/use_traversal_ids_for_ancestor_scopes.yml b/config/feature_flags/development/use_traversal_ids_for_ancestor_scopes.yml deleted file mode 100644 index 0ac765b6ab3..00000000000 --- a/config/feature_flags/development/use_traversal_ids_for_ancestor_scopes.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: use_traversal_ids_for_ancestor_scopes -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67652 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340159 -milestone: '14.3' -type: development -group: group::tenant scale -default_enabled: true diff --git a/db/migrate/20230428070443_add_allow_account_deletion_to_application_settings.rb b/db/migrate/20230428070443_add_allow_account_deletion_to_application_settings.rb new file mode 100644 index 00000000000..1731d91eb5c --- /dev/null +++ b/db/migrate/20230428070443_add_allow_account_deletion_to_application_settings.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddAllowAccountDeletionToApplicationSettings < Gitlab::Database::Migration[2.1] + def change + add_column :application_settings, :allow_account_deletion, :boolean, default: true, null: false + end +end diff --git a/db/schema_migrations/20230428070443 b/db/schema_migrations/20230428070443 new file mode 100644 index 00000000000..c1798ec1bf4 --- /dev/null +++ b/db/schema_migrations/20230428070443 @@ -0,0 +1 @@ +8277328b39ff873c549453bbdc8b0ae67e49cc23fd6e8166aea68c1d61fc7116 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index e8fdacd0ee9..ed00829cb6e 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -11832,6 +11832,7 @@ CREATE TABLE application_settings ( remember_me_enabled boolean DEFAULT true NOT NULL, encrypted_anthropic_api_key bytea, encrypted_anthropic_api_key_iv bytea, + allow_account_deletion boolean DEFAULT true NOT NULL, CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)), CONSTRAINT app_settings_container_registry_pre_import_tags_rate_positive CHECK ((container_registry_pre_import_tags_rate >= (0)::numeric)), CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)), diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md index b2b1ede12d4..b7b728649a7 100644 --- a/doc/user/project/import/github.md +++ b/doc/user/project/import/github.md @@ -137,11 +137,12 @@ Use one of the following tabs to filter the list of repositories: - **Collaborated**: Filter the list to the repositories that you have contributed to. - **Organization**: Filter the list to the repositories that belong to an organization you are a member of. -When the **Organization** tab is selected, you can further narrow down your search by selecting an available GitHub organization from a dropdown. +When the **Organization** tab is selected, you can further narrow down your search by selecting an available GitHub organization from a dropdown list. ### Select additional items to import -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/373705) in GitLab 15.5. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/373705) in GitLab 15.5. +> - Importing collaborators as an additional item was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/398154) in GitLab 16.0. To make imports as fast as possible, the following items aren't imported from GitHub by default: diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 673b6e60a01..27dc7af7e5c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5370,6 +5370,9 @@ msgstr "" msgid "ApplicationSettings|Allowed domains for sign-ups" msgstr "" +msgid "ApplicationSettings|Allows users to delete their own accounts" +msgstr "" + msgid "ApplicationSettings|Any user that visits %{host} and creates an account must be explicitly approved by an administrator before they can sign in. Only effective if sign-ups are enabled." msgstr "" @@ -23791,7 +23794,7 @@ msgstr "" msgid "Input the remote repository URL" msgstr "" -msgid "Insert a %{rows}x%{cols} table." +msgid "Insert a %{rows}×%{cols} table" msgstr "" msgid "Insert a quote" @@ -34378,6 +34381,12 @@ msgstr "" msgid "Profiles|Account could not be deleted. GitLab was unable to verify your identity." msgstr "" +msgid "Profiles|Account deletion is not allowed by your administrator." +msgstr "" + +msgid "Profiles|Account deletion is not allowed." +msgstr "" + msgid "Profiles|Account scheduled for removal." msgstr "" diff --git a/spec/controllers/concerns/metrics_dashboard_spec.rb b/spec/controllers/concerns/metrics_dashboard_spec.rb index d68a9d70ec6..4a9c7c493a7 100644 --- a/spec/controllers/concerns/metrics_dashboard_spec.rb +++ b/spec/controllers/concerns/metrics_dashboard_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe MetricsDashboard do +RSpec.describe MetricsDashboard, feature_category: :metrics do include MetricsDashboardHelpers describe 'GET #metrics_dashboard' do @@ -11,6 +11,7 @@ RSpec.describe MetricsDashboard do let_it_be(:environment) { create(:environment, project: project) } before do + stub_feature_flags(remove_monitor_metrics: false) sign_in(user) project.add_maintainer(user) end @@ -179,5 +180,16 @@ RSpec.describe MetricsDashboard do end end end + + context 'when metrics dashboard feature is unavailable' do + it 'returns 404 not found' do + stub_feature_flags(remove_monitor_metrics: true) + + routes.draw { get "metrics_dashboard" => "anonymous#metrics_dashboard" } + response = get :metrics_dashboard, format: :json + + expect(response).to have_gitlab_http_status(:not_found) + end + end end end diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index f097d08fe1b..22804339fef 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -922,6 +922,18 @@ RSpec.describe Projects::EnvironmentsController, feature_category: :continuous_d get :metrics, params: environment_params end + + context 'when metrics dashboard feature is unavailable' do + before do + stub_feature_flags(remove_monitor_metrics: true) + end + + it 'returns 404 not found' do + get :metrics_dashboard, params: environment_params(dashboard_params) + + expect(response).to have_gitlab_http_status(:not_found) + end + end end end diff --git a/spec/controllers/projects/grafana_api_controller_spec.rb b/spec/controllers/projects/grafana_api_controller_spec.rb index fa20fc5037f..9bc4a83030e 100644 --- a/spec/controllers/projects/grafana_api_controller_spec.rb +++ b/spec/controllers/projects/grafana_api_controller_spec.rb @@ -250,6 +250,19 @@ RSpec.describe Projects::GrafanaApiController, feature_category: :metrics do it_behaves_like 'error response', :bad_request end + + context 'when metrics dashboard feature is unavailable' do + before do + stub_feature_flags(remove_monitor_metrics: true) + end + + it 'returns 404 Not found' do + get :metrics_dashboard, params: params + + expect(response).to have_gitlab_http_status(:not_found) + expect(response.body).to be_empty + end + end end end end diff --git a/spec/controllers/projects/prometheus/alerts_controller_spec.rb b/spec/controllers/projects/prometheus/alerts_controller_spec.rb index 91d3ba7e106..44292b9ce19 100644 --- a/spec/controllers/projects/prometheus/alerts_controller_spec.rb +++ b/spec/controllers/projects/prometheus/alerts_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Projects::Prometheus::AlertsController do +RSpec.describe Projects::Prometheus::AlertsController, feature_category: :incident_management do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project) } let_it_be(:environment) { create(:environment, project: project) } @@ -115,11 +115,15 @@ RSpec.describe Projects::Prometheus::AlertsController do end end - describe 'GET #metrics_dashboard' do + describe 'GET #metrics_dashboard', feature_category: :metrics do let!(:alert) do create(:prometheus_alert, project: project, environment: environment, prometheus_metric: metric) end + before do + stub_feature_flags(remove_monitor_metrics: false) + end + it 'returns a json object with the correct keys' do get :metrics_dashboard, params: request_params(id: metric.id, environment_id: alert.environment.id), format: :json @@ -148,6 +152,14 @@ RSpec.describe Projects::Prometheus::AlertsController do expect(response).to have_gitlab_http_status(:not_found) end + + it 'returns 404 when metrics dashboard feature is unavailable' do + stub_feature_flags(remove_monitor_metrics: true) + + get :metrics_dashboard, params: request_params(id: 0), format: :json + + expect(response).to have_gitlab_http_status(:not_found) + end end def project_params(opts = {}) diff --git a/spec/controllers/projects/prometheus/metrics_controller_spec.rb b/spec/controllers/projects/prometheus/metrics_controller_spec.rb index 327651b2058..8f8edebbc30 100644 --- a/spec/controllers/projects/prometheus/metrics_controller_spec.rb +++ b/spec/controllers/projects/prometheus/metrics_controller_spec.rb @@ -2,13 +2,14 @@ require 'spec_helper' -RSpec.describe Projects::Prometheus::MetricsController do +RSpec.describe Projects::Prometheus::MetricsController, feature_category: :metrics do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :with_prometheus_integration) } let(:prometheus_adapter) { double('prometheus_adapter', can_query?: true) } before do + stub_feature_flags(remove_monitor_metrics: false) project.add_maintainer(user) sign_in(user) end @@ -79,6 +80,18 @@ RSpec.describe Projects::Prometheus::MetricsController do expect(response).to have_gitlab_http_status(:not_found) end end + + context 'when metrics dashboard feature is unavailable' do + before do + stub_feature_flags(remove_monitor_metrics: true) + end + + it 'renders 404' do + get :active_common, params: project_params(format: :json) + + expect(response).to have_gitlab_http_status(:not_found) + end + end end describe 'POST #validate_query' do diff --git a/spec/features/tags/developer_creates_tag_spec.rb b/spec/features/tags/developer_creates_tag_spec.rb index cb59ee17514..be9f19fe84a 100644 --- a/spec/features/tags/developer_creates_tag_spec.rb +++ b/spec/features/tags/developer_creates_tag_spec.rb @@ -20,7 +20,10 @@ RSpec.describe 'Developer creates tag', :js, feature_category: :source_code_mana end it 'with an invalid name displays an error' do - create_tag_in_form(tag: 'v 1.0', ref: 'master') + fill_in 'tag_name', with: 'v 1.0' + select_ref(ref: 'master') + + click_button 'Create tag' expect(page).to have_content 'Tag name invalid' end @@ -39,13 +42,20 @@ RSpec.describe 'Developer creates tag', :js, feature_category: :source_code_mana end it 'that already exists displays an error' do - create_tag_in_form(tag: 'v1.1.0', ref: 'master') + fill_in 'tag_name', with: 'v1.1.0' + select_ref(ref: 'master') + + click_button 'Create tag' expect(page).to have_content 'Tag v1.1.0 already exists' end it 'with multiline message displays the message in a
 block' do
-      create_tag_in_form(tag: 'v3.0', ref: 'master', message: "Awesome tag message\n\n- hello\n- world")
+      fill_in 'tag_name', with: 'v3.0'
+      select_ref(ref: 'master')
+      fill_in 'message', with: "Awesome tag message\n\n- hello\n- world"
+
+      click_button 'Create tag'
 
       expect(page).to have_current_path(
         project_tag_path(project, 'v3.0'), ignore_query: true)
@@ -67,14 +77,6 @@ RSpec.describe 'Developer creates tag', :js, feature_category: :source_code_mana
     end
   end
 
-  def create_tag_in_form(tag:, ref:, message: nil, desc: nil)
-    fill_in 'tag_name', with: tag
-    select_ref(ref: ref)
-    fill_in 'message', with: message unless message.nil?
-    fill_in 'release_description', with: desc unless desc.nil?
-    click_button 'Create tag'
-  end
-
   def select_ref(ref:)
     ref_selector = '.ref-selector'
     find(ref_selector).click
diff --git a/spec/features/users/password_spec.rb b/spec/features/users/password_spec.rb
index ccd383c8a15..59f49c791b6 100644
--- a/spec/features/users/password_spec.rb
+++ b/spec/features/users/password_spec.rb
@@ -3,6 +3,8 @@
 require 'spec_helper'
 
 RSpec.describe 'User password', feature_category: :system_access do
+  include EmailHelpers
+
   describe 'send password reset' do
     context 'when recaptcha is enabled' do
       before do
@@ -26,5 +28,57 @@ RSpec.describe 'User password', feature_category: :system_access do
         expect(page).not_to have_css('.g-recaptcha')
       end
     end
+
+    context 'when user has multiple emails' do
+      let_it_be(:user) { create(:user, email: 'primary@example.com') }
+      let_it_be(:verified_email) { create(:email, :confirmed, user: user, email: 'second@example.com') }
+      let_it_be(:unverified_email) { create(:email, user: user, email: 'unverified@example.com') }
+
+      let(:ff_enabled) { true }
+
+      before do
+        stub_feature_flags(password_reset_any_verified_email: ff_enabled)
+
+        perform_enqueued_jobs do
+          visit new_user_password_path
+          fill_in 'user_email', with: email
+          click_button 'Reset password'
+        end
+      end
+
+      context 'when user enters the primary email' do
+        let(:email) { user.email }
+
+        it 'send the email to the correct email address' do
+          expect(ActionMailer::Base.deliveries.first.to).to include(email)
+        end
+      end
+
+      context 'when user enters a secondary verified email' do
+        let(:email) { verified_email.email }
+
+        context 'when password_reset_any_verified_email FF is enabled' do
+          it 'send the email to the correct email address' do
+            expect(ActionMailer::Base.deliveries.first.to).to include(email)
+          end
+        end
+
+        context 'when password_reset_any_verified_email FF is not enabled' do
+          let(:ff_enabled) { false }
+
+          it 'does not send an email' do
+            expect(ActionMailer::Base.deliveries.count).to eq(0)
+          end
+        end
+      end
+
+      context 'when user enters an unverified email' do
+        let(:email) { unverified_email.email }
+
+        it 'does not send an email' do
+          expect(ActionMailer::Base.deliveries.count).to eq(0)
+        end
+      end
+    end
   end
 end
diff --git a/spec/frontend/content_editor/components/toolbar_table_button_spec.js b/spec/frontend/content_editor/components/toolbar_table_button_spec.js
index 35741971488..be6e47e067f 100644
--- a/spec/frontend/content_editor/components/toolbar_table_button_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_table_button_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlButton } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlButton } from '@gitlab/ui';
 import { mountExtended } from 'helpers/vue_test_utils_helper';
 import ToolbarTableButton from '~/content_editor/components/toolbar_table_button.vue';
 import { stubComponent } from 'helpers/stub_component';
@@ -14,12 +14,13 @@ describe('content_editor/components/toolbar_table_button', () => {
         tiptapEditor: editor,
       },
       stubs: {
-        GlDropdown: stubComponent(GlDropdown),
+        GlDisclosureDropdown: stubComponent(GlDisclosureDropdown),
       },
     });
   };
 
-  const findDropdown = () => wrapper.findComponent(GlDropdown);
+  const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+  const findButton = (row, col) => wrapper.findComponent({ ref: `table-${row}-${col}` });
   const getNumButtons = () => findDropdown().findAllComponents(GlButton).length;
 
   beforeEach(() => {
@@ -32,32 +33,44 @@ describe('content_editor/components/toolbar_table_button', () => {
     editor.destroy();
   });
 
-  it('renders a grid of 5x5 buttons to create a table', () => {
-    expect(getNumButtons()).toBe(25); // 5x5
-  });
-
   describe.each`
     row  | col  | numButtons | tableSize
-    ${3} | ${4} | ${25}      | ${'3x4'}
-    ${4} | ${4} | ${25}      | ${'4x4'}
-    ${4} | ${5} | ${30}      | ${'4x5'}
-    ${5} | ${4} | ${30}      | ${'5x4'}
-    ${5} | ${5} | ${36}      | ${'5x5'}
+    ${3} | ${4} | ${25}      | ${'3×4'}
+    ${4} | ${4} | ${25}      | ${'4×4'}
+    ${4} | ${5} | ${30}      | ${'4×5'}
+    ${5} | ${4} | ${30}      | ${'5×4'}
+    ${5} | ${5} | ${36}      | ${'5×5'}
   `('button($row, $col) in the table creator grid', ({ row, col, numButtons, tableSize }) => {
-    describe('on mouse over', () => {
+    describe('a11y tests', () => {
+      it('is in its own gridcell', () => {
+        expect(findButton(row, col).element.parentElement.getAttribute('role')).toBe('gridcell');
+      });
+
+      it('has an aria-label', () => {
+        expect(findButton(row, col).attributes('aria-label')).toBe(`Insert a ${tableSize} table`);
+      });
+    });
+
+    describe.each`
+      event          | triggerEvent
+      ${'mouseover'} | ${(button) => button.trigger('mouseover')}
+      ${'focus'}     | ${(button) => button.element.dispatchEvent(new FocusEvent('focus'))}
+    `('on $event', ({ triggerEvent }) => {
       beforeEach(async () => {
-        const button = wrapper.findByTestId(`table-${row}-${col}`);
-        await button.trigger('mouseover');
+        const button = wrapper.findComponent({ ref: `table-${row}-${col}` });
+        await triggerEvent(button);
       });
 
       it('marks all rows and cols before it as active', () => {
         const prevRow = Math.max(1, row - 1);
         const prevCol = Math.max(1, col - 1);
-        expect(wrapper.findByTestId(`table-${prevRow}-${prevCol}`).element).toHaveClass('active');
+        expect(wrapper.findComponent({ ref: `table-${prevRow}-${prevCol}` }).element).toHaveClass(
+          'active',
+        );
       });
 
       it('shows a help text indicating the size of the table being inserted', () => {
-        expect(findDropdown().element).toHaveText(`Insert a ${tableSize} table.`);
+        expect(findDropdown().element).toHaveText(`Insert a ${tableSize} table`);
       });
 
       it('adds another row and col of buttons to create a bigger table', () => {
@@ -71,7 +84,7 @@ describe('content_editor/components/toolbar_table_button', () => {
       beforeEach(async () => {
         commands = mockChainedCommands(editor, ['focus', 'insertTable', 'run']);
 
-        const button = wrapper.findByTestId(`table-${row}-${col}`);
+        const button = wrapper.findComponent({ ref: `table-${row}-${col}` });
         await button.trigger('mouseover');
         await button.trigger('click');
       });
@@ -95,8 +108,8 @@ describe('content_editor/components/toolbar_table_button', () => {
       expect(getNumButtons()).toBe(i * i);
 
       // eslint-disable-next-line no-await-in-loop
-      await wrapper.findByTestId(`table-${i}-${i}`).trigger('mouseover');
-      expect(findDropdown().element).toHaveText(`Insert a ${i}x${i} table.`);
+      await wrapper.findComponent({ ref: `table-${i}-${i}` }).trigger('mouseover');
+      expect(findDropdown().element).toHaveText(`Insert a ${i}×${i} table`);
     }
 
     expect(getNumButtons()).toBe(100); // 10x10 (and not 11x11)
@@ -105,10 +118,50 @@ describe('content_editor/components/toolbar_table_button', () => {
   describe('a11y tests', () => {
     it('sets text, title, and text-sr-only properties to the table button dropdown', () => {
       expect(findDropdown().props()).toMatchObject({
-        text: 'Insert table',
+        toggleText: 'Insert table',
         textSrOnly: true,
       });
-      expect(findDropdown().attributes('title')).toBe('Insert table');
+      expect(findDropdown().attributes('aria-label')).toBe('Insert table');
+    });
+
+    it('renders a role=grid of 5x5 gridcells to create a table', () => {
+      expect(getNumButtons()).toBe(25); // 5x5
+      expect(wrapper.find('[role="grid"]').exists()).toBe(true);
+      wrapper.findAll('[role="row"]').wrappers.forEach((row) => {
+        expect(row.findAll('[role="gridcell"]')).toHaveLength(5);
+      });
+    });
+
+    it('sets aria-rowcount and aria-colcount on the dropdown contents', () => {
+      expect(wrapper.find('[role="grid"]').attributes()).toMatchObject({
+        'aria-rowcount': '10',
+        'aria-colcount': '10',
+      });
+    });
+
+    it('allows navigating the grid with the arrow keys', async () => {
+      const dispatchKeyboardEvent = (button, key) =>
+        button.element.dispatchEvent(new KeyboardEvent('keydown', { key }));
+
+      let button = findButton(3, 4);
+      await button.trigger('mouseover');
+      expect(button.element).toHaveClass('active');
+
+      button = findButton(3, 5);
+      await dispatchKeyboardEvent(button, 'ArrowRight');
+      expect(button.element).toHaveClass('active');
+
+      button = findButton(4, 5);
+      await dispatchKeyboardEvent(button, 'ArrowDown');
+      expect(button.element).toHaveClass('active');
+
+      button = findButton(4, 4);
+      await dispatchKeyboardEvent(button, 'ArrowLeft');
+      expect(button.element).toHaveClass('active');
+
+      button = findButton(3, 4);
+      await dispatchKeyboardEvent(button, 'ArrowUp');
+      expect(button.element).toHaveClass('active');
     });
   });
 });
diff --git a/spec/helpers/profiles_helper_spec.rb b/spec/helpers/profiles_helper_spec.rb
index ebe86ccb08d..4c43b1ec4cf 100644
--- a/spec/helpers/profiles_helper_spec.rb
+++ b/spec/helpers/profiles_helper_spec.rb
@@ -118,6 +118,12 @@ RSpec.describe ProfilesHelper do
     end
   end
 
+  describe '#prevent_delete_account?' do
+    it 'returns false' do
+      expect(helper.prevent_delete_account?).to eq false
+    end
+  end
+
   def stub_auth0_omniauth_provider
     provider = OpenStruct.new(
       'name' => example_omniauth_provider,
diff --git a/spec/lib/generators/gitlab/partitioning/foreign_keys_generator_spec.rb b/spec/lib/generators/gitlab/partitioning/foreign_keys_generator_spec.rb
index 7c7ca8207ff..8c6dcbf4b96 100644
--- a/spec/lib/generators/gitlab/partitioning/foreign_keys_generator_spec.rb
+++ b/spec/lib/generators/gitlab/partitioning/foreign_keys_generator_spec.rb
@@ -60,7 +60,7 @@ feature_category: :continuous_integration do
         RemoveFkToTestTmpBuildsTestTmpMetadataOnBuildsId
       ])
 
-      schema_migrate_up!
+      schema_migrate_up!(only_databases: [:main])
 
       fks = Gitlab::Database::PostgresForeignKey
         .by_referenced_table_identifier('public._test_tmp_builds')
diff --git a/spec/mailers/devise_mailer_spec.rb b/spec/mailers/devise_mailer_spec.rb
index 6eb0e817803..171251f51ef 100644
--- a/spec/mailers/devise_mailer_spec.rb
+++ b/spec/mailers/devise_mailer_spec.rb
@@ -102,9 +102,12 @@ RSpec.describe DeviseMailer do
   end
 
   describe '#reset_password_instructions' do
-    subject { described_class.reset_password_instructions(user, 'faketoken') }
-
     let_it_be(:user) { create(:user) }
+    let(:params) { {} }
+
+    subject do
+      described_class.reset_password_instructions(user, 'faketoken', params)
+    end
 
     it_behaves_like 'an email sent from GitLab'
     it_behaves_like 'it should not have Gmail Actions links'
@@ -135,6 +138,15 @@ RSpec.describe DeviseMailer do
     it 'has the mailgun suppression bypass header' do
       is_expected.to have_header 'X-Mailgun-Suppressions-Bypass', 'true'
     end
+
+    context 'with email in params' do
+      let(:email) { 'example@example.com' }
+      let(:params) { { to: email } }
+
+      it 'is sent to the specified email' do
+        is_expected.to deliver_to email
+      end
+    end
   end
 
   describe '#email_changed' do
diff --git a/spec/migrations/finalize_issues_iid_scoping_to_namespace_spec.rb b/spec/migrations/finalize_issues_iid_scoping_to_namespace_spec.rb
index 1834e8c6e0e..c4f091d0d80 100644
--- a/spec/migrations/finalize_issues_iid_scoping_to_namespace_spec.rb
+++ b/spec/migrations/finalize_issues_iid_scoping_to_namespace_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
 require_migration!
 
 RSpec.describe FinalizeIssuesIidScopingToNamespace, :migration, feature_category: :team_planning do
-  let(:batched_migrations) { table(:batched_background_migrations) }
+  let(:batched_migrations) { table(:batched_background_migrations, database: :main) }
 
   let!(:migration) { described_class::MIGRATION }
 
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index d202fef0ed0..b0ff070e4a6 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -317,8 +317,7 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
     before do
       stub_feature_flags(
         use_traversal_ids: false,
-        use_traversal_ids_for_ancestors: false,
-        use_traversal_ids_for_ancestor_scopes: false
+        use_traversal_ids_for_ancestors: false
       )
     end
 
diff --git a/spec/models/concerns/recoverable_by_any_email_spec.rb b/spec/models/concerns/recoverable_by_any_email_spec.rb
new file mode 100644
index 00000000000..11dd89d97c9
--- /dev/null
+++ b/spec/models/concerns/recoverable_by_any_email_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe RecoverableByAnyEmail, feature_category: :system_access do
+  describe '.send_reset_password_instructions' do
+    let_it_be(:user) { create(:user, email: 'test@example.com') }
+    let_it_be(:verified_email) { create(:email, :confirmed, user: user) }
+    let_it_be(:unverified_email) { create(:email, user: user) }
+
+    let(:ff_enabled) { true }
+
+    before do
+      stub_feature_flags(password_reset_any_verified_email: ff_enabled)
+    end
+
+    subject(:send_reset_password_instructions) do
+      User.send_reset_password_instructions(email: email)
+    end
+
+    shared_examples 'sends the password reset email' do
+      it 'finds the user' do
+        expect(send_reset_password_instructions).to eq(user)
+      end
+
+      it 'sends the email' do
+        expect { send_reset_password_instructions }.to have_enqueued_mail(DeviseMailer, :reset_password_instructions)
+      end
+    end
+
+    shared_examples 'does not send the password reset email' do
+      it 'does not find the user' do
+        expect(subject.id).to be_nil
+        expect(subject.errors).not_to be_empty
+      end
+
+      it 'does not send any email' do
+        subject
+
+        expect { subject }.not_to have_enqueued_mail(DeviseMailer, :reset_password_instructions)
+      end
+    end
+
+    context 'with user primary email' do
+      let(:email) { user.email }
+
+      it_behaves_like 'sends the password reset email'
+    end
+
+    context 'with user verified email' do
+      let(:email) { verified_email.email }
+
+      context 'when password_reset_any_verified_email FF is enabled' do
+        it_behaves_like 'sends the password reset email'
+      end
+
+      context 'when password_reset_any_verified_email FF is not enabled' do
+        let(:ff_enabled) { false }
+
+        it_behaves_like 'does not send the password reset email'
+      end
+    end
+
+    context 'with user unverified email' do
+      let(:email) { unverified_email.email }
+
+      it_behaves_like 'does not send the password reset email'
+    end
+  end
+
+  describe '#send_reset_password_instructions' do
+    let_it_be(:user) { create(:user) }
+    let_it_be(:opts) { { email: 'random@email.com' } }
+    let_it_be(:token) { 'passwordresettoken' }
+
+    before do
+      stub_feature_flags(password_reset_any_verified_email: ff_enabled)
+
+      allow(user).to receive(:set_reset_password_token).and_return(token)
+    end
+
+    subject { user.send_reset_password_instructions(opts) }
+
+    context 'when password_reset_any_verified_email FF is not enabled' do
+      let(:ff_enabled) { false }
+
+      # original Devise behavior
+      it 'calls send_reset_password_instructions_notification just with token' do
+        expect(user).to receive(:send_reset_password_instructions_notification).with(token)
+
+        subject
+      end
+    end
+
+    context 'when password_reset_any_verified_email FF is enabled' do
+      let(:ff_enabled) { true }
+
+      it 'sends the email' do
+        expect { subject }.to have_enqueued_mail(DeviseMailer, :reset_password_instructions)
+      end
+
+      it 'calls send_reset_password_instructions_notification with correct arguments' do
+        expect(user).to receive(:send_reset_password_instructions_notification).with(token, opts)
+
+        subject
+      end
+
+      it 'returns the generated token' do
+        expect(subject).to eq(token)
+      end
+    end
+  end
+end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 3f66cbaf2b7..9a898159088 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -76,6 +76,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
       expect(json_response['slack_app_verification_token']).to be_nil
       expect(json_response['valid_runner_registrars']).to match_array(%w(project group))
       expect(json_response['ci_max_includes']).to eq(150)
+      expect(json_response['allow_account_deletion']).to eq(true)
     end
   end
 
@@ -188,7 +189,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
             slack_app_secret: 'SLACK_APP_SECRET',
             slack_app_signing_secret: 'SLACK_APP_SIGNING_SECRET',
             slack_app_verification_token: 'SLACK_APP_VERIFICATION_TOKEN',
-            valid_runner_registrars: ['group']
+            valid_runner_registrars: ['group'],
+            allow_account_deletion: false
           }
 
         expect(response).to have_gitlab_http_status(:ok)
@@ -265,6 +267,7 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
         expect(json_response['slack_app_signing_secret']).to eq('SLACK_APP_SIGNING_SECRET')
         expect(json_response['slack_app_verification_token']).to eq('SLACK_APP_VERIFICATION_TOKEN')
         expect(json_response['valid_runner_registrars']).to eq(['group'])
+        expect(json_response['allow_account_deletion']).to be(false)
       end
     end
 
diff --git a/spec/requests/projects/metrics_dashboard_spec.rb b/spec/requests/projects/metrics_dashboard_spec.rb
index d0181275927..7e94bc6134d 100644
--- a/spec/requests/projects/metrics_dashboard_spec.rb
+++ b/spec/requests/projects/metrics_dashboard_spec.rb
@@ -9,12 +9,28 @@ RSpec.describe 'Projects::MetricsDashboardController', feature_category: :metric
   let_it_be(:user) { project.first_owner }
 
   before do
+    stub_feature_flags(remove_monitor_metrics: false)
     project.add_developer(user)
     login_as(user)
     stub_feature_flags(remove_monitor_metrics: false)
   end
 
+  shared_examples 'metrics dashboard is unavailable' do
+    context 'when metrics dashboard feature is unavailable' do
+      before do
+        stub_feature_flags(remove_monitor_metrics: true)
+      end
+
+      it 'returns 404 not found' do
+        send_request
+        expect(response).to have_gitlab_http_status(:not_found)
+      end
+    end
+  end
+
   describe 'GET /:namespace/:project/-/metrics' do
+    include_examples 'metrics dashboard is unavailable'
+
     it "redirects to default environment's metrics dashboard" do
       send_request
       expect(response).to redirect_to(dashboard_route(environment: environment))
@@ -70,6 +86,8 @@ RSpec.describe 'Projects::MetricsDashboardController', feature_category: :metric
   end
 
   describe 'GET /:namespace/:project/-/metrics?environment=:environment.id' do
+    include_examples 'metrics dashboard is unavailable'
+
     it 'returns 200' do
       send_request(environment: environment2.id)
       expect(response).to have_gitlab_http_status(:ok)
@@ -91,6 +109,8 @@ RSpec.describe 'Projects::MetricsDashboardController', feature_category: :metric
   describe 'GET /:namespace/:project/-/metrics/:dashboard_path' do
     let(:dashboard_path) { '.gitlab/dashboards/dashboard_path.yml' }
 
+    include_examples 'metrics dashboard is unavailable'
+
     it 'returns 200' do
       send_request(dashboard_path: dashboard_path, environment: environment.id)
       expect(response).to have_gitlab_http_status(:ok)
@@ -105,6 +125,8 @@ RSpec.describe 'Projects::MetricsDashboardController', feature_category: :metric
   describe 'GET :/namespace/:project/-/metrics/:dashboard_path?environment=:environment.id' do
     let(:dashboard_path) { '.gitlab/dashboards/dashboard_path.yml' }
 
+    include_examples 'metrics dashboard is unavailable'
+
     it 'returns 200' do
       send_request(dahboard_path: dashboard_path, environment: environment.id)
       expect(response).to have_gitlab_http_status(:ok)
@@ -124,6 +146,8 @@ RSpec.describe 'Projects::MetricsDashboardController', feature_category: :metric
   end
 
   describe 'GET :/namespace/:project/-/metrics/:page' do
+    include_examples 'metrics dashboard is unavailable'
+
     it 'returns 200 with path param page' do
       send_request(page: 'panel/new', environment: environment.id)
 
diff --git a/spec/support/helpers/migrations_helpers.rb b/spec/support/helpers/migrations_helpers.rb
index 1b8c3388051..0084835ff8d 100644
--- a/spec/support/helpers/migrations_helpers.rb
+++ b/spec/support/helpers/migrations_helpers.rb
@@ -130,19 +130,45 @@ module MigrationsHelpers
     end
   end
 
-  def schema_migrate_down!
+  # TODO: use Gitlab::Database::EachDatabase class (https://gitlab.com/gitlab-org/gitlab/-/issues/410154)
+  def migrate_databases!(only_databases: nil, version: nil)
+    only_databases ||= if Gitlab::Database.database_mode == Gitlab::Database::MODE_SINGLE_DATABASE
+                         [:main]
+                       else
+                         %i[main ci]
+                       end
+
+    # unique in the context of database, host, port
+    configurations = Gitlab::Database.database_base_models.each_with_object({}) do |(_name, model), h|
+      config = model.connection_db_config
+
+      h[config.configuration_hash.slice(:database, :host, :port)] ||= config
+    end
+
+    with_reestablished_active_record_base do
+      configurations.each_value do |configuration|
+        next unless only_databases.include? configuration.name.to_sym
+
+        ActiveRecord::Base.establish_connection(configuration) # rubocop:disable Database/EstablishConnection
+
+        migration_context.migrate(version) # rubocop:disable Database/MultipleDatabases
+      end
+    end
+  end
+
+  def schema_migrate_down!(only_databases: nil)
     disable_migrations_output do
-      migration_context.down(migration_schema_version)
+      migrate_databases!(only_databases: only_databases, version: migration_schema_version)
     end
 
     reset_column_in_all_models
   end
 
-  def schema_migrate_up!
+  def schema_migrate_up!(only_databases: nil)
     reset_column_in_all_models
 
     disable_migrations_output do
-      migration_context.up
+      migrate_databases!(only_databases: only_databases)
     end
 
     reset_column_in_all_models
diff --git a/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb b/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb
index 19b1cee44ee..9cdde13b36b 100644
--- a/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb
@@ -21,6 +21,8 @@ RSpec.shared_examples_for 'metrics dashboard prometheus api proxy' do
   end
 
   before do
+    stub_feature_flags(remove_monitor_metrics: false)
+
     allow_next_instance_of(Prometheus::ProxyService, *service_params) do |proxy_service|
       allow(proxy_service).to receive(:execute).and_return(service_result)
     end
@@ -106,6 +108,19 @@ RSpec.shared_examples_for 'metrics dashboard prometheus api proxy' do
         end
       end
     end
+
+    context 'when metrics dashboard feature is unavailable' do
+      before do
+        stub_feature_flags(remove_monitor_metrics: true)
+      end
+
+      it 'returns 404 not found' do
+        get :prometheus_proxy, params: prometheus_proxy_params
+
+        expect(response).to have_gitlab_http_status(:not_found)
+        expect(response.body).to be_empty
+      end
+    end
   end
 
   context 'with inappropriate requests' do
diff --git a/spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb b/spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb
index cb8f6721d66..5b63ef10c85 100644
--- a/spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb
@@ -17,6 +17,10 @@ RSpec.shared_examples_for 'GET #metrics_dashboard for dashboard' do |dashboard_n
   let(:expected_keys) { %w(dashboard status metrics_data) }
   let(:status_code) { :ok }
 
+  before do
+    stub_feature_flags(remove_monitor_metrics: false)
+  end
+
   it_behaves_like 'GET #metrics_dashboard correctly formatted response'
 
   it 'returns correct dashboard' do
@@ -24,4 +28,17 @@ RSpec.shared_examples_for 'GET #metrics_dashboard for dashboard' do |dashboard_n
 
     expect(json_response['dashboard']['dashboard']).to eq(dashboard_name)
   end
+
+  context 'when metrics dashboard feature is unavailable' do
+    before do
+      stub_feature_flags(remove_monitor_metrics: true)
+    end
+
+    it 'returns 404 not found' do
+      get :metrics_dashboard, params: metrics_dashboard_req_params, format: :json
+
+      expect(response).to have_gitlab_http_status(:not_found)
+      expect(response.body).to be_empty
+    end
+  end
 end
diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
index 4afed5139d8..0c4e5ce51fc 100644
--- a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
+++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
@@ -139,29 +139,10 @@ RSpec.shared_examples 'namespace traversal scopes' do
   end
 
   describe '.self_and_ancestors' do
-    context "use_traversal_ids_ancestor_scopes feature flag is true" do
-      before do
-        stub_feature_flags(use_traversal_ids: true)
-        stub_feature_flags(use_traversal_ids_for_ancestor_scopes: true)
-      end
-
-      it_behaves_like '.self_and_ancestors'
-
-      it 'not make recursive queries' do
-        expect { described_class.where(id: [nested_group_1]).self_and_ancestors.load }.not_to make_queries_matching(/WITH RECURSIVE/)
-      end
-    end
-
-    context "use_traversal_ids_ancestor_scopes feature flag is false" do
-      before do
-        stub_feature_flags(use_traversal_ids_for_ancestor_scopes: false)
-      end
+    it_behaves_like '.self_and_ancestors'
 
-      it_behaves_like '.self_and_ancestors'
-
-      it 'makes recursive queries' do
-        expect { described_class.where(id: [nested_group_1]).self_and_ancestors.load }.to make_queries_matching(/WITH RECURSIVE/)
-      end
+    it 'not make recursive queries' do
+      expect { described_class.where(id: [nested_group_1]).self_and_ancestors.load }.not_to make_queries_matching(/WITH RECURSIVE/)
     end
   end
 
@@ -197,29 +178,10 @@ RSpec.shared_examples 'namespace traversal scopes' do
   end
 
   describe '.self_and_ancestor_ids' do
-    context "use_traversal_ids_ancestor_scopes feature flag is true" do
-      before do
-        stub_feature_flags(use_traversal_ids: true)
-        stub_feature_flags(use_traversal_ids_for_ancestor_scopes: true)
-      end
-
-      it_behaves_like '.self_and_ancestor_ids'
-
-      it 'makes recursive queries' do
-        expect { described_class.where(id: [nested_group_1]).self_and_ancestor_ids.load }.not_to make_queries_matching(/WITH RECURSIVE/)
-      end
-    end
-
-    context "use_traversal_ids_ancestor_scopes feature flag is false" do
-      before do
-        stub_feature_flags(use_traversal_ids_for_ancestor_scopes: false)
-      end
+    it_behaves_like '.self_and_ancestor_ids'
 
-      it_behaves_like '.self_and_ancestor_ids'
-
-      it 'makes recursive queries' do
-        expect { described_class.where(id: [nested_group_1]).self_and_ancestor_ids.load }.to make_queries_matching(/WITH RECURSIVE/)
-      end
+    it 'not make recursive queries' do
+      expect { described_class.where(id: [nested_group_1]).self_and_ancestor_ids.load }.not_to make_queries_matching(/WITH RECURSIVE/)
     end
   end
 
-- 
cgit v1.2.1