summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-17 03:07:10 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-17 03:07:10 +0000
commit9bf8cb8d34039f3cef9e1b2f812ce634f2bebe69 (patch)
treec1e4d7a8dc008004b3e3861a4d8f6d9439ffabf8
parente91080371b32e69d038b3a94261688c09dbcd641 (diff)
downloadgitlab-ce-9bf8cb8d34039f3cef9e1b2f812ce634f2bebe69.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/rspec/avoid_conditional_statements.yml1
-rw-r--r--Gemfile.checksum2
-rw-r--r--Gemfile.lock2
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_table_button.vue92
-rw-r--r--app/assets/stylesheets/components/content_editor.scss4
-rw-r--r--app/controllers/concerns/metrics/dashboard/prometheus_api_proxy.rb2
-rw-r--r--app/controllers/concerns/metrics_dashboard.rb2
-rw-r--r--app/controllers/projects/metrics_dashboard_controller.rb2
-rw-r--r--app/controllers/projects/prometheus/metrics_controller.rb5
-rw-r--r--app/helpers/application_settings_helper.rb3
-rw-r--r--app/helpers/profiles_helper.rb5
-rw-r--r--app/models/application_setting_implementation.rb3
-rw-r--r--app/models/concerns/recoverable_by_any_email.rb45
-rw-r--r--app/models/namespaces/traversal/linear_scopes.rb9
-rw-r--r--app/models/user.rb1
-rw-r--r--app/views/admin/application_settings/_user_restrictions.html.haml1
-rw-r--r--app/views/profiles/accounts/show.html.haml65
-rw-r--r--config/feature_flags/development/password_reset_any_verified_email.yml8
-rw-r--r--config/feature_flags/development/use_traversal_ids_for_ancestor_scopes.yml8
-rw-r--r--db/migrate/20230428070443_add_allow_account_deletion_to_application_settings.rb7
-rw-r--r--db/schema_migrations/202304280704431
-rw-r--r--db/structure.sql1
-rw-r--r--doc/user/project/import/github.md5
-rw-r--r--locale/gitlab.pot11
-rw-r--r--spec/controllers/concerns/metrics_dashboard_spec.rb14
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb12
-rw-r--r--spec/controllers/projects/grafana_api_controller_spec.rb13
-rw-r--r--spec/controllers/projects/prometheus/alerts_controller_spec.rb16
-rw-r--r--spec/controllers/projects/prometheus/metrics_controller_spec.rb15
-rw-r--r--spec/features/tags/developer_creates_tag_spec.rb24
-rw-r--r--spec/features/users/password_spec.rb54
-rw-r--r--spec/frontend/content_editor/components/toolbar_table_button_spec.js97
-rw-r--r--spec/helpers/profiles_helper_spec.rb6
-rw-r--r--spec/lib/generators/gitlab/partitioning/foreign_keys_generator_spec.rb2
-rw-r--r--spec/mailers/devise_mailer_spec.rb16
-rw-r--r--spec/migrations/finalize_issues_iid_scoping_to_namespace_spec.rb2
-rw-r--r--spec/models/ci/runner_spec.rb3
-rw-r--r--spec/models/concerns/recoverable_by_any_email_spec.rb113
-rw-r--r--spec/requests/api/settings_spec.rb5
-rw-r--r--spec/requests/projects/metrics_dashboard_spec.rb24
-rw-r--r--spec/support/helpers/migrations_helpers.rb34
-rw-r--r--spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/namespaces/traversal_scope_examples.rb50
44 files changed, 636 insertions, 181 deletions
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 @@
<script>
-import {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownForm,
- GlButton,
- GlTooltipDirective as GlTooltip,
-} from '@gitlab/ui';
+import { GlDisclosureDropdown, GlButton, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { clamp } from '../services/utils';
@@ -19,9 +13,7 @@ const MAX_COLS = 10;
export default {
components: {
GlButton,
- GlDropdown,
- GlDropdownDivider,
- GlDropdownForm,
+ GlDisclosureDropdown,
},
directives: {
GlTooltip,
@@ -61,45 +53,75 @@ export default {
.run();
this.resetState();
+ this.$refs.dropdown.close();
this.$emit('execute', { contentType: 'table' });
},
getButtonLabel(rows, cols) {
- return sprintf(__('Insert a %{rows}x%{cols} table.'), { rows, cols });
+ return sprintf(__('Insert a %{rows}×%{cols} table'), { rows, cols });
},
+ onKeydown(key) {
+ const delta = {
+ ArrowUp: { rows: -1, cols: 0 },
+ ArrowDown: { rows: 1, cols: 0 },
+ ArrowLeft: { rows: 0, cols: -1 },
+ ArrowRight: { rows: 0, cols: 1 },
+ }[key] || { rows: 0, cols: 0 };
+
+ const rows = clamp(this.rows + delta.rows, 1, this.maxRows);
+ const cols = clamp(this.cols + delta.cols, 1, this.maxCols);
+
+ this.setRowsAndCols(rows, cols);
+ },
+ setFocus(row, col) {
+ this.$refs[`table-${row}-${col}`][0].$el.focus();
+ },
+ },
+ MAX_COLS,
+ MAX_ROWS,
+ popperOptions: {
+ strategy: 'fixed',
},
};
</script>
<template>
- <gl-dropdown
+ <gl-disclosure-dropdown
+ ref="dropdown"
v-gl-tooltip
size="small"
category="tertiary"
icon="table"
- :title="__('Insert table')"
- :text="__('Insert table')"
- class="content-editor-dropdown"
- right
+ :aria-label="__('Insert table')"
+ :toggle-text="__('Insert table')"
+ :popper-opts="$options.popperOptions"
+ class="content-editor-table-dropdown"
text-sr-only
- lazy
+ :fluid-width="true"
+ @shown="setFocus(1, 1)"
>
- <gl-dropdown-form class="gl-px-3! gl-pb-2!">
- <div v-for="r of list(maxRows)" :key="r" class="gl-display-flex">
- <gl-button
- v-for="c of list(maxCols)"
- :key="c"
- :data-testid="`table-${r}-${c}`"
- :class="{ 'active gl-bg-blue-50!': r <= rows && c <= cols }"
- :aria-label="getButtonLabel(r, c)"
- class="table-creator-grid-item gl-display-inline gl-rounded-0! gl-w-6! gl-h-6! gl-p-0!"
- @mouseover="setRowsAndCols(r, c)"
- @click="insertTable()"
- />
- </div>
- <gl-dropdown-divider class="gl-my-3! gl-mx-n3!" />
- <div class="gl-px-1">
- {{ getButtonLabel(rows, cols) }}
+ <div
+ class="gl-p-3 gl-pt-2"
+ role="grid"
+ :aria-colcount="$options.MAX_COLS"
+ :aria-rowcount="$options.MAX_ROWS"
+ >
+ <div v-for="r of list(maxRows)" :key="r" class="gl-display-flex" role="row">
+ <div v-for="c of list(maxCols)" :key="c" role="gridcell">
+ <gl-button
+ :ref="`table-${r}-${c}`"
+ :class="{ 'active gl-bg-blue-50!': r <= rows && c <= cols }"
+ :aria-label="getButtonLabel(r, c)"
+ class="table-creator-grid-item gl-display-inline gl-rounded-0! gl-w-6! gl-h-6! gl-p-0!"
+ @mouseover="setRowsAndCols(r, c)"
+ @focus="setRowsAndCols(r, c)"
+ @click="insertTable()"
+ @keydown="onKeydown($event.key)"
+ />
+ </div>
</div>
- </gl-dropdown-form>
- </gl-dropdown>
+ </div>
+ <div class="gl-border-t gl-px-4 gl-pt-3 gl-pb-2">
+ {{ getButtonLabel(rows, cols) }}
+ </div>
+ </gl-disclosure-dropdown>
</template>
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: "<a href='#{reset_profile_password_path}' rel=\"nofollow\" data-method=\"put\">".html_safe, closingTag: '</a>'.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: '<a href="https://support.gitlab.io/account-deletion/" rel="nofollow noreferrer noopener" target="_blank">'.html_safe, link_end: '</a>'.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: "<a href='#{reset_profile_password_path}' rel=\"nofollow\" data-method=\"put\">".html_safe, closingTag: '</a>'.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: '<a href="https://support.gitlab.io/account-deletion/" rel="nofollow noreferrer noopener" target="_blank">'.html_safe, link_end: '</a>'.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 <pre> 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