Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									15e5a05bcd
								
							
						
					
					
						commit
						26891eec2c
					
				|  | @ -821,10 +821,23 @@ | |||
|       when: never | ||||
|     - <<: *if-merge-request-targeting-stable-branch | ||||
|     - <<: *if-merge-request-labels-run-review-app | ||||
|     - <<: *if-dot-com-gitlab-org-and-security-merge-request | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-build-images-patterns | ||||
|     - <<: *if-dot-com-gitlab-org-and-security-merge-request | ||||
|     - <<: *if-merge-request | ||||
|       changes: *code-qa-patterns | ||||
|     # Rules to support .qa:rules:package-and-test-ee | ||||
|     - <<: *if-merge-request | ||||
|       changes: *dependency-patterns | ||||
|     - <<: *if-merge-request-labels-run-all-e2e | ||||
|     - <<: *if-dot-com-gitlab-org-and-security-merge-request-manual-ff-package-and-e2e | ||||
|       changes: *feature-flag-development-config-patterns | ||||
|     - <<: *if-merge-request | ||||
|       changes: *feature-flag-development-config-patterns | ||||
|     - <<: *if-merge-request | ||||
|       changes: *nodejs-patterns | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-qa-patterns | ||||
|     - <<: *if-force-ci | ||||
| 
 | ||||
| .build-images:rules:build-qa-image: | ||||
|   rules: | ||||
|  | @ -843,7 +856,6 @@ | |||
|     - <<: *if-dot-com-gitlab-org-schedule | ||||
|       variables: | ||||
|         ARCH: amd64,arm64 | ||||
|     - <<: *if-force-ci | ||||
|     - <<: *if-ruby2-branch | ||||
| 
 | ||||
| .build-images:rules:build-qa-image-as-if-foss: | ||||
|  | @ -872,7 +884,6 @@ | |||
|     - <<: *if-merge-request-targeting-stable-branch | ||||
|     - <<: *if-ruby2-branch | ||||
|     - <<: *if-merge-request-labels-run-review-app | ||||
|     - <<: *if-merge-request-labels-run-all-e2e | ||||
|     - <<: *if-auto-deploy-branches | ||||
|     - !reference [".releases:rules:canonical-dot-com-gitlab-stable-branch-only-setup-test-env-patterns", rules] | ||||
|     - <<: *if-default-refs | ||||
|  | @ -1275,6 +1286,8 @@ | |||
| ########## | ||||
| .notify:rules:create-issues-for-failing-tests: | ||||
|   rules: | ||||
|     - <<: *if-not-canonical-namespace | ||||
|       when: never | ||||
|     # Don't report child pipeline failures | ||||
|     - if: '$CI_PIPELINE_SOURCE == "parent_pipeline"' | ||||
|       when: never | ||||
|  | @ -1331,14 +1344,32 @@ | |||
|       when: never | ||||
|     - <<: *if-merge-request-targeting-stable-branch | ||||
|       allow_failure: true | ||||
|     - <<: *if-dot-com-gitlab-org-and-security-merge-request | ||||
|     - <<: *if-merge-request | ||||
|       changes: *code-backstage-qa-patterns | ||||
|       allow_failure: true | ||||
|     - <<: *if-dot-com-gitlab-org-schedule | ||||
|       allow_failure: true | ||||
|     - <<: *if-ruby2-branch | ||||
|     # Rules to support .qa:rules:package-and-test-ee | ||||
|     - <<: *if-merge-request | ||||
|       changes: *dependency-patterns | ||||
|       allow_failure: true | ||||
|     - <<: *if-merge-request-labels-run-all-e2e | ||||
|       allow_failure: true | ||||
|     - <<: *if-dot-com-gitlab-org-and-security-merge-request-manual-ff-package-and-e2e | ||||
|       changes: *feature-flag-development-config-patterns | ||||
|       allow_failure: true | ||||
|     - <<: *if-merge-request | ||||
|       changes: *feature-flag-development-config-patterns | ||||
|       allow_failure: true | ||||
|     - <<: *if-merge-request | ||||
|       changes: *nodejs-patterns | ||||
|       allow_failure: true | ||||
|     - <<: *if-merge-request | ||||
|       changes: *ci-qa-patterns | ||||
|       allow_failure: true | ||||
|     - <<: *if-force-ci | ||||
|       allow_failure: true | ||||
|     - <<: *if-ruby2-branch | ||||
| 
 | ||||
| .qa:rules:package-and-test-common: | ||||
|   rules: | ||||
|  |  | |||
|  | @ -1,13 +0,0 @@ | |||
| --- | ||||
| Gitlab/DeprecateTrackRedisHLLEvent: | ||||
|   Exclude: | ||||
|     - 'app/controllers/concerns/snippets_actions.rb' | ||||
|     - 'app/controllers/concerns/wiki_actions.rb' | ||||
|     - 'app/controllers/projects/blob_controller.rb' | ||||
|     - 'app/controllers/projects/pipelines_controller.rb' | ||||
|     - 'ee/app/controllers/admin/audit_logs_controller.rb' | ||||
|     - 'ee/app/controllers/admin/credentials_controller.rb' | ||||
|     - 'ee/app/controllers/groups/analytics/ci_cd_analytics_controller.rb' | ||||
|     - 'ee/app/controllers/groups/audit_events_controller.rb' | ||||
|     - 'ee/app/controllers/groups/epic_boards_controller.rb' | ||||
|     - 'spec/controllers/concerns/redis_tracking_spec.rb' | ||||
|  | @ -5494,7 +5494,6 @@ RSpec/MissingFeatureCategory: | |||
|     - 'spec/rubocop/cop/gitlab/change_timezone_spec.rb' | ||||
|     - 'spec/rubocop/cop/gitlab/const_get_inherit_false_spec.rb' | ||||
|     - 'spec/rubocop/cop/gitlab/delegate_predicate_methods_spec.rb' | ||||
|     - 'spec/rubocop/cop/gitlab/deprecate_track_redis_hll_event_spec.rb' | ||||
|     - 'spec/rubocop/cop/gitlab/event_store_subscriber_spec.rb' | ||||
|     - 'spec/rubocop/cop/gitlab/except_spec.rb' | ||||
|     - 'spec/rubocop/cop/gitlab/feature_available_usage_spec.rb' | ||||
|  |  | |||
|  | @ -131,6 +131,10 @@ export default { | |||
|         :message-url="view.message_url" | ||||
|         :config="$options.integrationViewConfigs[view.name]" | ||||
|       /> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="col-lg-4"></div> | ||||
|     <div class="col-lg-8"> | ||||
|       <hr /> | ||||
|     </div> | ||||
|     <div class="col-sm-12 js-hide-when-nothing-matches-search"> | ||||
|  |  | |||
|  | @ -2,7 +2,6 @@ | |||
| 
 | ||||
| module ProductAnalyticsTracking | ||||
|   include Gitlab::Tracking::Helpers | ||||
|   include RedisTracking | ||||
|   extend ActiveSupport::Concern | ||||
| 
 | ||||
|   class_methods do | ||||
|  | @ -39,4 +38,23 @@ module ProductAnalyticsTracking | |||
|       **optional_arguments | ||||
|     ) | ||||
|   end | ||||
| 
 | ||||
|   def track_unique_redis_hll_event(event_name, &block) | ||||
|     custom_id = block ? yield(self) : nil | ||||
| 
 | ||||
|     unique_id = custom_id || visitor_id | ||||
| 
 | ||||
|     return unless unique_id | ||||
| 
 | ||||
|     Gitlab::UsageDataCounters::HLLRedisCounter.track_event(event_name, values: unique_id) | ||||
|   end | ||||
| 
 | ||||
|   def visitor_id | ||||
|     return cookies[:visitor_id] if cookies[:visitor_id].present? | ||||
|     return unless current_user | ||||
| 
 | ||||
|     uuid = SecureRandom.uuid | ||||
|     cookies[:visitor_id] = { value: uuid, expires: 24.months } | ||||
|     uuid | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -1,49 +0,0 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # Example: | ||||
| # | ||||
| # # In controller include module | ||||
| # # Track event for index action | ||||
| # | ||||
| # include RedisTracking | ||||
| # | ||||
| # track_redis_hll_event :index, :show, name: 'i_analytics_dev_ops_score' | ||||
| # | ||||
| # You can also pass custom conditions using `if:`, using the same format as with Rails callbacks. | ||||
| # You can also pass an optional block that calculates and returns a custom id to track. | ||||
| module RedisTracking | ||||
|   include Gitlab::Tracking::Helpers | ||||
|   extend ActiveSupport::Concern | ||||
| 
 | ||||
|   class_methods do | ||||
|     def track_redis_hll_event(*controller_actions, name:, if: nil, &block) | ||||
|       custom_conditions = Array.wrap(binding.local_variable_get('if')) | ||||
|       conditions = [:trackable_html_request?, *custom_conditions] | ||||
| 
 | ||||
|       after_action only: controller_actions, if: conditions do | ||||
|         track_unique_redis_hll_event(name, &block) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def track_unique_redis_hll_event(event_name, &block) | ||||
|     custom_id = block ? yield(self) : nil | ||||
| 
 | ||||
|     unique_id = custom_id || visitor_id | ||||
| 
 | ||||
|     return unless unique_id | ||||
| 
 | ||||
|     Gitlab::UsageDataCounters::HLLRedisCounter.track_event(event_name, values: unique_id) | ||||
|   end | ||||
| 
 | ||||
|   def visitor_id | ||||
|     return cookies[:visitor_id] if cookies[:visitor_id].present? | ||||
|     return unless current_user | ||||
| 
 | ||||
|     uuid = SecureRandom.uuid | ||||
|     cookies[:visitor_id] = { value: uuid, expires: 24.months } | ||||
|     uuid | ||||
|   end | ||||
| end | ||||
|  | @ -9,13 +9,13 @@ module SnippetsActions | |||
|   include Gitlab::NoteableMetadata | ||||
|   include Snippets::SendBlob | ||||
|   include SnippetsSort | ||||
|   include RedisTracking | ||||
|   include ProductAnalyticsTracking | ||||
| 
 | ||||
|   included do | ||||
|     skip_before_action :verify_authenticity_token, | ||||
|       if: -> { action_name == 'show' && js_request? } | ||||
| 
 | ||||
|     track_redis_hll_event :show, name: 'i_snippets_show' | ||||
|     track_event :show, name: 'i_snippets_show' | ||||
| 
 | ||||
|     respond_to :html | ||||
|   end | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ module WikiActions | |||
|   include PreviewMarkdown | ||||
|   include SendsBlob | ||||
|   include Gitlab::Utils::StrongMemoize | ||||
|   include RedisTracking | ||||
|   include ProductAnalyticsTracking | ||||
|   extend ActiveSupport::Concern | ||||
| 
 | ||||
|   RESCUE_GIT_TIMEOUTS_IN = %w[show edit history diff pages].freeze | ||||
|  | @ -46,7 +46,7 @@ module WikiActions | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     track_redis_hll_event :show, name: 'wiki_action' | ||||
|     track_event :show, name: 'wiki_action' | ||||
| 
 | ||||
|     helper_method :view_file_button, :diff_file_html_data | ||||
| 
 | ||||
|  |  | |||
|  | @ -37,7 +37,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController | |||
|   end | ||||
| 
 | ||||
|   def preferences_param_names | ||||
|     [ | ||||
|     preferences_param_names = [ | ||||
|       :color_scheme_id, | ||||
|       :diffs_deletion_color, | ||||
|       :diffs_addition_color, | ||||
|  | @ -60,6 +60,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController | |||
|       :use_legacy_web_ide, | ||||
|       :use_new_navigation | ||||
|     ] | ||||
|     preferences_param_names << :enabled_following if ::Feature.enabled?(:disable_follow_users, user) | ||||
|     preferences_param_names | ||||
|   end | ||||
| end | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 
 | ||||
| class Projects::PipelinesController < Projects::ApplicationController | ||||
|   include ::Gitlab::Utils::StrongMemoize | ||||
|   include RedisTracking | ||||
|   include ProductAnalyticsTracking | ||||
|   include ProductAnalyticsTracking | ||||
|   include ProjectStatsRefreshConflictsGuard | ||||
| 
 | ||||
|  | @ -34,11 +34,11 @@ class Projects::PipelinesController < Projects::ApplicationController | |||
|     label: 'redis_hll_counters.analytics.analytics_total_unique_counts_monthly', | ||||
|     destinations: %i[redis_hll snowplow] | ||||
| 
 | ||||
|   track_redis_hll_event :charts, name: 'p_analytics_ci_cd_pipelines', if: -> { should_track_ci_cd_pipelines? } | ||||
|   track_redis_hll_event :charts, name: 'p_analytics_ci_cd_deployment_frequency', if: -> { should_track_ci_cd_deployment_frequency? } | ||||
|   track_redis_hll_event :charts, name: 'p_analytics_ci_cd_lead_time', if: -> { should_track_ci_cd_lead_time? } | ||||
|   track_redis_hll_event :charts, name: 'p_analytics_ci_cd_time_to_restore_service', if: -> { should_track_ci_cd_time_to_restore_service? } | ||||
|   track_redis_hll_event :charts, name: 'p_analytics_ci_cd_change_failure_rate', if: -> { should_track_ci_cd_change_failure_rate? } | ||||
|   track_event :charts, name: 'p_analytics_ci_cd_pipelines', conditions: -> { should_track_ci_cd_pipelines? } | ||||
|   track_event :charts, name: 'p_analytics_ci_cd_deployment_frequency', conditions: -> { should_track_ci_cd_deployment_frequency? } | ||||
|   track_event :charts, name: 'p_analytics_ci_cd_lead_time', conditions: -> { should_track_ci_cd_lead_time? } | ||||
|   track_event :charts, name: 'p_analytics_ci_cd_time_to_restore_service', conditions: -> { should_track_ci_cd_time_to_restore_service? } | ||||
|   track_event :charts, name: 'p_analytics_ci_cd_change_failure_rate', conditions: -> { should_track_ci_cd_change_failure_rate? } | ||||
| 
 | ||||
|   wrap_parameters Ci::Pipeline | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| class SearchController < ApplicationController | ||||
|   include ControllerWithCrossProjectAccessCheck | ||||
|   include SearchHelper | ||||
|   include RedisTracking | ||||
|   include ProductAnalyticsTracking | ||||
|   include ProductAnalyticsTracking | ||||
|   include SearchRateLimitable | ||||
| 
 | ||||
|  |  | |||
|  | @ -191,7 +191,12 @@ class UsersController < ApplicationController | |||
|   def follow | ||||
|     followee = current_user.follow(user) | ||||
| 
 | ||||
|     flash[:alert] = followee.errors.full_messages.join(', ') if followee&.errors&.any? | ||||
|     if followee | ||||
|       flash[:alert] = followee.errors.full_messages.join(', ') if followee&.errors&.any? | ||||
|     else | ||||
|       flash[:alert] = s_('Action not allowed.') | ||||
|     end | ||||
| 
 | ||||
|     redirect_path = referer_path(request) || @user | ||||
| 
 | ||||
|     redirect_to redirect_path | ||||
|  |  | |||
|  | @ -349,29 +349,30 @@ class User < ApplicationRecord | |||
|   # User's role | ||||
|   enum role: { software_developer: 0, development_team_lead: 1, devops_engineer: 2, systems_administrator: 3, security_analyst: 4, data_analyst: 5, product_manager: 6, product_designer: 7, other: 8 }, _suffix: true | ||||
| 
 | ||||
|   delegate  :notes_filter_for, | ||||
|             :set_notes_filter, | ||||
|             :first_day_of_week, :first_day_of_week=, | ||||
|             :timezone, :timezone=, | ||||
|             :time_display_relative, :time_display_relative=, | ||||
|             :time_format_in_24h, :time_format_in_24h=, | ||||
|             :show_whitespace_in_diffs, :show_whitespace_in_diffs=, | ||||
|             :view_diffs_file_by_file, :view_diffs_file_by_file=, | ||||
|             :pass_user_identities_to_ci_jwt, :pass_user_identities_to_ci_jwt=, | ||||
|             :tab_width, :tab_width=, | ||||
|             :sourcegraph_enabled, :sourcegraph_enabled=, | ||||
|             :gitpod_enabled, :gitpod_enabled=, | ||||
|             :setup_for_company, :setup_for_company=, | ||||
|             :render_whitespace_in_code, :render_whitespace_in_code=, | ||||
|             :markdown_surround_selection, :markdown_surround_selection=, | ||||
|             :markdown_automatic_lists, :markdown_automatic_lists=, | ||||
|             :diffs_deletion_color, :diffs_deletion_color=, | ||||
|             :diffs_addition_color, :diffs_addition_color=, | ||||
|             :use_legacy_web_ide, :use_legacy_web_ide=, | ||||
|             :use_new_navigation, :use_new_navigation=, | ||||
|             :pinned_nav_items, :pinned_nav_items=, | ||||
|             :achievements_enabled, :achievements_enabled=, | ||||
|             to: :user_preference | ||||
|   delegate :notes_filter_for, | ||||
|            :set_notes_filter, | ||||
|            :first_day_of_week, :first_day_of_week=, | ||||
|            :timezone, :timezone=, | ||||
|            :time_display_relative, :time_display_relative=, | ||||
|            :time_format_in_24h, :time_format_in_24h=, | ||||
|            :show_whitespace_in_diffs, :show_whitespace_in_diffs=, | ||||
|            :view_diffs_file_by_file, :view_diffs_file_by_file=, | ||||
|            :pass_user_identities_to_ci_jwt, :pass_user_identities_to_ci_jwt=, | ||||
|            :tab_width, :tab_width=, | ||||
|            :sourcegraph_enabled, :sourcegraph_enabled=, | ||||
|            :gitpod_enabled, :gitpod_enabled=, | ||||
|            :setup_for_company, :setup_for_company=, | ||||
|            :render_whitespace_in_code, :render_whitespace_in_code=, | ||||
|            :markdown_surround_selection, :markdown_surround_selection=, | ||||
|            :markdown_automatic_lists, :markdown_automatic_lists=, | ||||
|            :diffs_deletion_color, :diffs_deletion_color=, | ||||
|            :diffs_addition_color, :diffs_addition_color=, | ||||
|            :use_legacy_web_ide, :use_legacy_web_ide=, | ||||
|            :use_new_navigation, :use_new_navigation=, | ||||
|            :pinned_nav_items, :pinned_nav_items=, | ||||
|            :achievements_enabled, :achievements_enabled=, | ||||
|            :enabled_following, :enabled_following=, | ||||
|            to: :user_preference | ||||
| 
 | ||||
|   delegate :path, to: :namespace, allow_nil: true, prefix: true | ||||
|   delegate :job_title, :job_title=, to: :user_detail, allow_nil: true | ||||
|  | @ -1694,7 +1695,7 @@ class User < ApplicationRecord | |||
|   end | ||||
| 
 | ||||
|   def follow(user) | ||||
|     return false if self.id == user.id | ||||
|     return false unless following_users_allowed?(user) | ||||
| 
 | ||||
|     begin | ||||
|       followee = Users::UserFollowUser.create(follower_id: self.id, followee_id: user.id) | ||||
|  | @ -1713,6 +1714,18 @@ class User < ApplicationRecord | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def following_users_allowed?(user) | ||||
|     return false if self.id == user.id | ||||
| 
 | ||||
|     following_users_enabled? && user.following_users_enabled? | ||||
|   end | ||||
| 
 | ||||
|   def following_users_enabled? | ||||
|     return true unless ::Feature.enabled?(:disable_follow_users, self) | ||||
| 
 | ||||
|     enabled_following | ||||
|   end | ||||
| 
 | ||||
|   def forkable_namespaces | ||||
|     strong_memoize(:forkable_namespaces) do | ||||
|       personal_namespace = Namespace.where(id: namespace_id) | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ module Users | |||
|     attr_reader :user, :identity_params | ||||
| 
 | ||||
|     ATTRS_REQUIRING_PASSWORD_CHECK = %w[email].freeze | ||||
|     BATCH_SIZE = 100 | ||||
| 
 | ||||
|     def initialize(current_user, params = {}) | ||||
|       @current_user = current_user | ||||
|  | @ -34,7 +35,7 @@ module Users | |||
|       reset_unconfirmed_email | ||||
| 
 | ||||
|       if @user.save(validate: validate) && update_status | ||||
|         notify_success(user_exists) | ||||
|         after_update(user_exists) | ||||
|       else | ||||
|         messages = @user.errors.full_messages + Array(@user.status&.errors&.full_messages) | ||||
|         error(messages.uniq.join('. ')) | ||||
|  | @ -80,8 +81,6 @@ module Users | |||
| 
 | ||||
|     def notify_success(user_exists) | ||||
|       notify_new_user(@user, nil) unless user_exists | ||||
| 
 | ||||
|       success | ||||
|     end | ||||
| 
 | ||||
|     def discard_read_only_attributes | ||||
|  | @ -118,6 +117,30 @@ module Users | |||
|     def provider_params | ||||
|       identity_params.slice(*provider_attributes) | ||||
|     end | ||||
| 
 | ||||
|     def after_update(user_exists) | ||||
|       notify_success(user_exists) | ||||
|       remove_followers_and_followee! if ::Feature.enabled?(:disable_follow_users, user) | ||||
| 
 | ||||
|       success | ||||
|     end | ||||
| 
 | ||||
|     def remove_followers_and_followee! | ||||
|       return false unless user.user_preference.enabled_following_previously_changed?(from: true, to: false) | ||||
| 
 | ||||
|       # rubocop: disable CodeReuse/ActiveRecord | ||||
|       loop do | ||||
|         inner_query = Users::UserFollowUser | ||||
|                         .where(follower_id: user.id).or(Users::UserFollowUser.where(followee_id: user.id)) | ||||
|                         .select(:follower_id, :followee_id) | ||||
|                         .limit(BATCH_SIZE) | ||||
| 
 | ||||
|         deleted_records = Users::UserFollowUser.where('(follower_id, followee_id) IN (?)', inner_query).delete_all | ||||
| 
 | ||||
|         break if deleted_records == 0 | ||||
|       end | ||||
|       # rubocop: enable CodeReuse/ActiveRecord | ||||
|     end | ||||
|   end | ||||
| end | ||||
| 
 | ||||
|  |  | |||
|  | @ -170,5 +170,21 @@ | |||
|       - if Feature.enabled?(:user_time_settings) | ||||
|         .form-group | ||||
|           = f.gitlab_ui_checkbox_component :time_format_in_24h, s_('Preferences|Display time in 24-hour format') | ||||
|   - if Feature.enabled?(:disable_follow_users, @user) | ||||
|     .row.js-preferences-form.js-search-settings-section | ||||
|       .col-sm-12 | ||||
|         %hr | ||||
|       .col-lg-4.profile-settings-sidebar#disabled_following | ||||
|         %h4.gl-mt-0 | ||||
|           = s_('Preferences|Disable follow users feature') | ||||
|         %p | ||||
|           = s_('Preferences|Turns off the ability to follow or be followed by other users.') | ||||
|           = succeed '.' do | ||||
|             = link_to _('Learn more'), help_page_path('user/profile/index', anchor: 'follow-users'), target: '_blank', rel: 'noopener noreferrer' | ||||
|       .col-lg-8 | ||||
|         .form-group | ||||
|           = f.gitlab_ui_checkbox_component :enabled_following, | ||||
|           s_('Preferences|Disable follow users') | ||||
| 
 | ||||
| 
 | ||||
|   #js-profile-preferences-app{ data: data_attributes } | ||||
|  |  | |||
|  | @ -36,7 +36,7 @@ | |||
|           = render Pajamas::ButtonComponent.new(href: [:admin, @user], | ||||
|             icon: 'user', | ||||
|             button_options: { class: 'gl-flex-grow-1 gl-mx-1 has-tooltip', title: s_('UserProfile|View user in admin area'), data: {toggle: 'tooltip', placement: 'bottom', container: 'body'}}) | ||||
|         - if current_user && current_user.id != @user.id | ||||
|         - if current_user && current_user.following_users_allowed?(@user) | ||||
|           - if current_user.following?(@user) | ||||
|             = form_tag user_unfollow_path(@user, :json), class: link_classes + 'gl-display-inline-block' do | ||||
|               = render Pajamas::ButtonComponent.new(type: :submit, button_options: { class: 'gl-w-full', data: { track_action: 'click_button', track_label: 'unfollow_from_profile' } }) do | ||||
|  |  | |||
|  | @ -0,0 +1,8 @@ | |||
| --- | ||||
| name: disable_follow_users | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/116023 | ||||
| rollout_issue_url: | ||||
| milestone: '16.0' | ||||
| type: development | ||||
| group: group::authentication and authorization | ||||
| default_enabled: false | ||||
|  | @ -0,0 +1,6 @@ | |||
| --- | ||||
| migration_job_name: PopulateVulnerabilityDismissalFields | ||||
| description: This populates missing dismissal info for vulnerabilities. | ||||
| feature_category: vulnerability_management | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/405032 | ||||
| milestone: 15.11 | ||||
|  | @ -0,0 +1,9 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class AddDisabledFollowingToUserPreferences < Gitlab::Database::Migration[2.1] | ||||
|   enable_lock_retries! | ||||
| 
 | ||||
|   def change | ||||
|     add_column :user_preferences, :enabled_following, :boolean, default: true, null: false | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,15 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class ValidateFkProjectsCreatorId < Gitlab::Database::Migration[2.1] | ||||
|   TABLE_NAME = :projects | ||||
|   COLUMN_NAME = :creator_id | ||||
|   FK_NAME = :fk_03ec10b0d3 | ||||
| 
 | ||||
|   def up | ||||
|     validate_foreign_key TABLE_NAME, COLUMN_NAME, name: FK_NAME | ||||
|   end | ||||
| 
 | ||||
|   def down | ||||
|     # no-op | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,18 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class AddTempIndexToNullDismissedInfoVulnerabilities < Gitlab::Database::Migration[2.1] | ||||
|   INDEX_NAME = 'tmp_index_vulnerability_dismissal_info' | ||||
| 
 | ||||
|   disable_ddl_transaction! | ||||
| 
 | ||||
|   def up | ||||
|     # Temporary index to be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/406653 | ||||
|     add_concurrent_index :vulnerabilities, :id, | ||||
|       where: "state = 2 AND (dismissed_at IS NULL OR dismissed_by_id IS NULL)", | ||||
|       name: INDEX_NAME | ||||
|   end | ||||
| 
 | ||||
|   def down | ||||
|     remove_concurrent_index_by_name :vulnerabilities, INDEX_NAME | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,16 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class AddIndexToGroupGroupLinks < Gitlab::Database::Migration[2.1] | ||||
|   disable_ddl_transaction! | ||||
| 
 | ||||
|   INDEX_NAME = 'index_group_group_links_on_shared_with_group_and_group_access' | ||||
|   TABLE_NAME = :group_group_links | ||||
| 
 | ||||
|   def up | ||||
|     add_concurrent_index TABLE_NAME, [:shared_with_group_id, :group_access], name: INDEX_NAME | ||||
|   end | ||||
| 
 | ||||
|   def down | ||||
|     remove_concurrent_index_by_name TABLE_NAME, name: INDEX_NAME | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,29 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class QueuePopulateVulnerabilityDismissalFields < Gitlab::Database::Migration[2.1] | ||||
|   MIGRATION = "PopulateVulnerabilityDismissalFields" | ||||
|   DELAY_INTERVAL = 2.minutes | ||||
|   BATCH_SIZE = 1_000 | ||||
|   MAX_BATCH_SIZE = 10_000 | ||||
|   SUB_BATCH_SIZE = 200 | ||||
| 
 | ||||
|   disable_ddl_transaction! | ||||
| 
 | ||||
|   restrict_gitlab_migration gitlab_schema: :gitlab_main | ||||
| 
 | ||||
|   def up | ||||
|     queue_batched_background_migration( | ||||
|       MIGRATION, | ||||
|       :vulnerabilities, | ||||
|       :id, | ||||
|       job_interval: DELAY_INTERVAL, | ||||
|       batch_size: BATCH_SIZE, | ||||
|       max_batch_size: MAX_BATCH_SIZE, | ||||
|       sub_batch_size: SUB_BATCH_SIZE | ||||
|     ) | ||||
|   end | ||||
| 
 | ||||
|   def down | ||||
|     delete_batched_background_migration(MIGRATION, :vulnerabilities, :id, []) | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1 @@ | |||
| ea52a177f82cc872b1f38490ff17bf166a885a4df7cf2c6c99dc7b1cccd14d33 | ||||
|  | @ -0,0 +1 @@ | |||
| d1e5678ea93b5b8f5d76b0332e79934badbaa84546754b66916bb2816cfaf307 | ||||
|  | @ -0,0 +1 @@ | |||
| 31e8f1f8d65ea821efb158bd59657776de54ecbd5e10d3a64c5afe37706c5388 | ||||
|  | @ -0,0 +1 @@ | |||
| 5a1245d37e10d03320a3cd8afda34226e54c6f6641c3abedfcb1333ea6ed69a0 | ||||
|  | @ -0,0 +1 @@ | |||
| f9c342816a6c656b1c13b8e9d0a771c1ee6a9847c03a76577c662f9cf238ad03 | ||||
|  | @ -23487,6 +23487,7 @@ CREATE TABLE user_preferences ( | |||
|     achievements_enabled boolean DEFAULT true NOT NULL, | ||||
|     pinned_nav_items jsonb DEFAULT '{}'::jsonb NOT NULL, | ||||
|     pass_user_identities_to_ci_jwt boolean DEFAULT false NOT NULL, | ||||
|     enabled_following boolean DEFAULT true NOT NULL, | ||||
|     CONSTRAINT check_89bf269f41 CHECK ((char_length(diffs_deletion_color) <= 7)), | ||||
|     CONSTRAINT check_d07ccd35f7 CHECK ((char_length(diffs_addition_color) <= 7)) | ||||
| ); | ||||
|  | @ -30780,6 +30781,8 @@ CREATE UNIQUE INDEX index_group_deploy_tokens_on_group_and_deploy_token_ids ON g | |||
| 
 | ||||
| CREATE UNIQUE INDEX index_group_group_links_on_shared_group_and_shared_with_group ON group_group_links USING btree (shared_group_id, shared_with_group_id); | ||||
| 
 | ||||
| CREATE INDEX index_group_group_links_on_shared_with_group_and_group_access ON group_group_links USING btree (shared_with_group_id, group_access); | ||||
| 
 | ||||
| CREATE INDEX index_group_group_links_on_shared_with_group_and_shared_group ON group_group_links USING btree (shared_with_group_id, shared_group_id); | ||||
| 
 | ||||
| CREATE INDEX index_group_import_states_on_group_id ON group_import_states USING btree (group_id); | ||||
|  | @ -33010,6 +33013,8 @@ CREATE INDEX tmp_index_on_vulnerabilities_non_dismissed ON vulnerabilities USING | |||
| 
 | ||||
| CREATE INDEX tmp_index_project_statistics_cont_registry_size ON project_statistics USING btree (project_id) WHERE (container_registry_size = 0); | ||||
| 
 | ||||
| CREATE INDEX tmp_index_vulnerability_dismissal_info ON vulnerabilities USING btree (id) WHERE ((state = 2) AND ((dismissed_at IS NULL) OR (dismissed_by_id IS NULL))); | ||||
| 
 | ||||
| CREATE INDEX tmp_index_vulnerability_overlong_title_html ON vulnerabilities USING btree (id) WHERE (length(title_html) > 800); | ||||
| 
 | ||||
| CREATE UNIQUE INDEX uniq_pkgs_deb_grp_architectures_on_distribution_id_and_name ON packages_debian_group_architectures USING btree (distribution_id, name); | ||||
|  | @ -34470,7 +34475,7 @@ ALTER TABLE ONLY design_management_designs_versions | |||
|     ADD CONSTRAINT fk_03c671965c FOREIGN KEY (design_id) REFERENCES design_management_designs(id) ON DELETE CASCADE; | ||||
| 
 | ||||
| ALTER TABLE ONLY projects | ||||
|     ADD CONSTRAINT fk_03ec10b0d3 FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE SET NULL NOT VALID; | ||||
|     ADD CONSTRAINT fk_03ec10b0d3 FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE SET NULL; | ||||
| 
 | ||||
| ALTER TABLE ONLY analytics_dashboards_pointers | ||||
|     ADD CONSTRAINT fk_05d96922bd FOREIGN KEY (target_project_id) REFERENCES projects(id) ON DELETE CASCADE; | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ module API | |||
|     class User < UserBasic | ||||
|       include UsersHelper | ||||
|       include TimeZoneHelper | ||||
|       include Gitlab::Utils::StrongMemoize | ||||
| 
 | ||||
|       expose :created_at, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) } | ||||
|       expose :bio, :location, :public_email, :skype, :linkedin, :twitter, :discord, :website_url, :organization, :job_title, :pronouns | ||||
|  | @ -12,18 +13,28 @@ module API | |||
|       expose :work_information do |user| | ||||
|         work_information(user) | ||||
|       end | ||||
|       expose :followers, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) } do |user| | ||||
|       expose :followers, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) && following_users_allowed(opts[:current_user], user) } do |user| | ||||
|         user.followers.size | ||||
|       end | ||||
|       expose :following, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) } do |user| | ||||
|       expose :following, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) && following_users_allowed(opts[:current_user], user) } do |user| | ||||
|         user.followees.size | ||||
|       end | ||||
|       expose :is_followed, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) && opts[:current_user] } do |user, opts| | ||||
|       expose :is_followed, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) && opts[:current_user] && following_users_allowed(opts[:current_user], user) } do |user, opts| | ||||
|         user.followed_by?(opts[:current_user]) | ||||
|       end | ||||
|       expose :local_time do |user| | ||||
|         local_time(user.timezone) | ||||
|       end | ||||
| 
 | ||||
|       def following_users_allowed(current_user, user) | ||||
|         strong_memoize(:following_users_allowed) do | ||||
|           if current_user | ||||
|             current_user.following_users_allowed?(user) | ||||
|           else | ||||
|             true | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -195,12 +195,13 @@ module API | |||
|         not_found!('User') unless user | ||||
| 
 | ||||
|         followee = current_user.follow(user) | ||||
| 
 | ||||
|         not_modified! unless followee | ||||
| 
 | ||||
|         if followee&.errors&.any? | ||||
|           render_api_error!(followee.errors.full_messages.join(', '), 400) | ||||
|         elsif followee&.persisted? | ||||
|           present user, with: Entities::UserBasic | ||||
|         else | ||||
|           not_modified! | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,90 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module Gitlab | ||||
|   module BackgroundMigration | ||||
|     # Populates missing dismissal information for vulnerabilities. | ||||
|     class PopulateVulnerabilityDismissalFields < BatchedMigrationJob | ||||
|       feature_category :vulnerability_management | ||||
|       scope_to ->(relation) { relation.where('state = 2 AND (dismissed_at IS NULL OR dismissed_by_id IS NULL)') } | ||||
|       operation_name :populate_vulnerability_dismissal_fields | ||||
| 
 | ||||
|       # rubocop:disable Style/Documentation | ||||
|       class Vulnerability < ApplicationRecord | ||||
|         self.table_name = 'vulnerabilities' | ||||
| 
 | ||||
|         has_one :finding, class_name: 'Finding' | ||||
| 
 | ||||
|         def copy_dismissal_information | ||||
|           return unless finding&.dismissal_feedback | ||||
| 
 | ||||
|           update_columns( | ||||
|             dismissed_at: finding.dismissal_feedback.created_at, | ||||
|             dismissed_by_id: finding.dismissal_feedback.author_id | ||||
|           ) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       class Finding < ApplicationRecord | ||||
|         self.table_name = 'vulnerability_occurrences' | ||||
| 
 | ||||
|         validates :details, json_schema: { filename: "filename" } | ||||
| 
 | ||||
|         def dismissal_feedback | ||||
|           Feedback.dismissal.where(finding_uuid: uuid).first | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       class Feedback < ApplicationRecord | ||||
|         DISMISSAL_TYPE = 0 # dismissal | ||||
| 
 | ||||
|         self.table_name = 'vulnerability_feedback' | ||||
| 
 | ||||
|         scope :dismissal, -> { where(feedback_type: DISMISSAL_TYPE) } | ||||
|       end | ||||
|       # rubocop:enable Style/Documentation | ||||
| 
 | ||||
|       def perform | ||||
|         each_sub_batch do |sub_batch| | ||||
|           vulnerability_ids = sub_batch.pluck(:id) | ||||
|           Vulnerability.includes(:finding).where(id: vulnerability_ids).each do |vulnerability| | ||||
|             populate_for(vulnerability) | ||||
|           end | ||||
| 
 | ||||
|           log_info(vulnerability_ids) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       private | ||||
| 
 | ||||
|       def populate_for(vulnerability) | ||||
|         log_warning(vulnerability) unless vulnerability.copy_dismissal_information | ||||
|       rescue StandardError => error | ||||
|         log_error(error, vulnerability) | ||||
|       end | ||||
| 
 | ||||
|       def log_info(vulnerability_ids) | ||||
|         ::Gitlab::BackgroundMigration::Logger.info( | ||||
|           migrator: self.class.name, | ||||
|           message: 'Dismissal information has been copied', | ||||
|           count: vulnerability_ids.length | ||||
|         ) | ||||
|       end | ||||
| 
 | ||||
|       def log_warning(vulnerability) | ||||
|         ::Gitlab::BackgroundMigration::Logger.warn( | ||||
|           migrator: self.class.name, | ||||
|           message: 'Could not update vulnerability!', | ||||
|           vulnerability_id: vulnerability.id | ||||
|         ) | ||||
|       end | ||||
| 
 | ||||
|       def log_error(error, vulnerability) | ||||
|         ::Gitlab::BackgroundMigration::Logger.error( | ||||
|           migrator: self.class.name, | ||||
|           message: error.message, | ||||
|           vulnerability_id: vulnerability.id | ||||
|         ) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -2226,6 +2226,9 @@ msgstr "" | |||
| msgid "Action" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Action not allowed." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Action to take when receiving an alert. %{docsLink}" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -33326,6 +33329,12 @@ msgstr "" | |||
| msgid "Preferences|Diff colors" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Preferences|Disable follow users" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Preferences|Disable follow users feature" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Preferences|Display time in 24-hour format" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -33404,6 +33413,9 @@ msgstr "" | |||
| msgid "Preferences|Time preferences" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Preferences|Turns off the ability to follow or be followed by other users." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Preferences|Use relative times" | ||||
| msgstr "" | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,33 +0,0 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'rack/utils' | ||||
| 
 | ||||
| module RuboCop | ||||
|   module Cop | ||||
|     module Gitlab | ||||
|       # This cop prevents from using deprecated `track_redis_hll_event` method. | ||||
|       # | ||||
|       # @example | ||||
|       # | ||||
|       # # bad | ||||
|       #  track_redis_hll_event :show, name: 'p_analytics_valuestream' | ||||
|       # | ||||
|       # # good | ||||
|       #   track_event :show, name: 'g_analytics_valuestream', destinations: [:redis_hll] | ||||
|       class DeprecateTrackRedisHLLEvent < RuboCop::Cop::Base | ||||
|         MSG = '`track_redis_hll_event` is deprecated. Use `track_event` helper instead. ' \ | ||||
|               'See https://docs.gitlab.com/ee/development/service_ping/implement.html#add-new-events' | ||||
| 
 | ||||
|         def_node_matcher :track_redis_hll_event_used?, <<~PATTERN | ||||
|           (send _ :track_redis_hll_event ...) | ||||
|         PATTERN | ||||
| 
 | ||||
|         def on_send(node) | ||||
|           return unless track_redis_hll_event_used?(node) | ||||
| 
 | ||||
|           add_offense(node.loc.selector) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -1,135 +0,0 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require "spec_helper" | ||||
| 
 | ||||
| RSpec.describe RedisTracking do | ||||
|   include TrackingHelpers | ||||
| 
 | ||||
|   let(:user) { create(:user) } | ||||
| 
 | ||||
|   controller(ApplicationController) do | ||||
|     include RedisTracking | ||||
| 
 | ||||
|     skip_before_action :authenticate_user!, only: :show | ||||
|     track_redis_hll_event(:index, :show, | ||||
|       name: 'g_compliance_approval_rules', | ||||
|       if: [:custom_condition_one?, :custom_condition_two?]) { |controller| controller.get_custom_id } | ||||
| 
 | ||||
|     def index | ||||
|       render html: 'index' | ||||
|     end | ||||
| 
 | ||||
|     def new | ||||
|       render html: 'new' | ||||
|     end | ||||
| 
 | ||||
|     def show | ||||
|       render html: 'show' | ||||
|     end | ||||
| 
 | ||||
|     def get_custom_id | ||||
|       'some_custom_id' | ||||
|     end | ||||
| 
 | ||||
|     private | ||||
| 
 | ||||
|     def custom_condition_one? | ||||
|       true | ||||
|     end | ||||
| 
 | ||||
|     def custom_condition_two? | ||||
|       true | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def expect_tracking | ||||
|     expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event) | ||||
|       .with('g_compliance_approval_rules', values: instance_of(String)) | ||||
|   end | ||||
| 
 | ||||
|   def expect_no_tracking | ||||
|     expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) | ||||
|   end | ||||
| 
 | ||||
|   context 'when user is logged in' do | ||||
|     before do | ||||
|       sign_in(user) | ||||
|     end | ||||
| 
 | ||||
|     it 'tracks the event' do | ||||
|       expect_tracking | ||||
| 
 | ||||
|       get :index | ||||
|     end | ||||
| 
 | ||||
|     it 'tracks the event if DNT is not enabled' do | ||||
|       stub_do_not_track('0') | ||||
| 
 | ||||
|       expect_tracking | ||||
| 
 | ||||
|       get :index | ||||
|     end | ||||
| 
 | ||||
|     it 'does not track the event if DNT is enabled' do | ||||
|       stub_do_not_track('1') | ||||
| 
 | ||||
|       expect_no_tracking | ||||
| 
 | ||||
|       get :index | ||||
|     end | ||||
| 
 | ||||
|     it 'does not track the event if the format is not HTML' do | ||||
|       expect_no_tracking | ||||
| 
 | ||||
|       get :index, format: :json | ||||
|     end | ||||
| 
 | ||||
|     it 'does not track the event if a custom condition returns false' do | ||||
|       expect(controller).to receive(:custom_condition_two?).and_return(false) | ||||
| 
 | ||||
|       expect_no_tracking | ||||
| 
 | ||||
|       get :index | ||||
|     end | ||||
| 
 | ||||
|     it 'does not track the event for untracked actions' do | ||||
|       expect_no_tracking | ||||
| 
 | ||||
|       get :new | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'when user is not logged in' do | ||||
|     let(:visitor_id) { SecureRandom.uuid } | ||||
| 
 | ||||
|     it 'tracks the event when there is a visitor id' do | ||||
|       cookies[:visitor_id] = { value: visitor_id, expires: 24.months } | ||||
| 
 | ||||
|       expect_tracking | ||||
| 
 | ||||
|       get :show, params: { id: 1 } | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'when user is not logged in and there is no visitor_id' do | ||||
|     it 'does not track the event' do | ||||
|       expect_no_tracking | ||||
| 
 | ||||
|       get :index | ||||
|     end | ||||
| 
 | ||||
|     it 'tracks the event when there is custom id' do | ||||
|       expect_tracking | ||||
| 
 | ||||
|       get :show, params: { id: 1 } | ||||
|     end | ||||
| 
 | ||||
|     it 'does not track the event when there is no custom id' do | ||||
|       expect(controller).to receive(:get_custom_id).and_return(nil) | ||||
| 
 | ||||
|       expect_no_tracking | ||||
| 
 | ||||
|       get :show, params: { id: 2 } | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -109,5 +109,33 @@ RSpec.describe Profiles::PreferencesController do | |||
|         expect(response.parsed_body['type']).to eq('alert') | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'on disable_follow_users feature flag' do | ||||
|       context 'with feature flag disabled' do | ||||
|         before do | ||||
|           stub_feature_flags(disable_follow_users: false) | ||||
|         end | ||||
| 
 | ||||
|         it 'does not update enabled_following preference of user' do | ||||
|           prefs = { enabled_following: false } | ||||
| 
 | ||||
|           go params: prefs | ||||
|           user.reload | ||||
| 
 | ||||
|           expect(user.enabled_following).to eq(true) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'with feature flag enabled' do | ||||
|         it 'does not update enabled_following preference of user' do | ||||
|           prefs = { enabled_following: false } | ||||
| 
 | ||||
|           go params: prefs | ||||
|           user.reload | ||||
| 
 | ||||
|           expect(user.enabled_following).to eq(false) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -50,6 +50,7 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do | |||
| 
 | ||||
|     shared_examples 'graphql queries' do |path, jobs_query| | ||||
|       let_it_be(:variables) { {} } | ||||
|       let_it_be(:success_path) { '' } | ||||
| 
 | ||||
|       let_it_be(:query) do | ||||
|         get_graphql_query_as_string("#{path}/#{jobs_query}") | ||||
|  | @ -57,9 +58,10 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do | |||
| 
 | ||||
|       fixtures_path = 'graphql/jobs/' | ||||
| 
 | ||||
|       it "#{fixtures_path}#{jobs_query}.json" do | ||||
|       it "#{fixtures_path}#{jobs_query}.json", :aggregate_failures do | ||||
|         post_graphql(query, current_user: user, variables: variables) | ||||
| 
 | ||||
|         expect(graphql_data.dig(*success_path)).not_to be_nil | ||||
|         expect_graphql_errors_to_be_empty | ||||
|       end | ||||
| 
 | ||||
|  | @ -87,15 +89,18 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do | |||
| 
 | ||||
|     it_behaves_like 'graphql queries', 'jobs/components/table/graphql/queries', 'get_jobs.query.graphql' do | ||||
|       let(:variables) { { fullPath: 'frontend-fixtures/builds-project' } } | ||||
|       let(:success_path) { %w[project jobs] } | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'graphql queries', 'pages/admin/jobs/components/table/graphql/queries', 'get_all_jobs.query.graphql' do | ||||
|       let(:user) { create(:admin) } | ||||
|       let(:success_path) { 'jobs' } | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'graphql queries', 'pages/admin/jobs/components/table/graphql/queries', 'get_cancelable_jobs_count.query.graphql' do | ||||
|       let(:variables) { { statuses: %w[PENDING RUNNING] } } | ||||
|       let(:user) { create(:admin) } | ||||
|       let(:success_path) { %w[cancelable count] } | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|  |  | |||
|  | @ -30,21 +30,63 @@ RSpec.describe API::Entities::User do | |||
|     end | ||||
| 
 | ||||
|     %i(followers following is_followed).each do |relationship| | ||||
|       context 'when current user cannot read user profile' do | ||||
|         let(:can_read_user_profile) { false } | ||||
| 
 | ||||
|       shared_examples 'does not expose relationship' do | ||||
|         it "does not expose #{relationship}" do | ||||
|           expect(subject).not_to include(relationship) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when current user can read user profile' do | ||||
|         let(:can_read_user_profile) { true } | ||||
| 
 | ||||
|       shared_examples 'exposes relationship' do | ||||
|         it "exposes #{relationship}" do | ||||
|           expect(subject).to include(relationship) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       context 'when current user cannot read user profile' do | ||||
|         let(:can_read_user_profile) { false } | ||||
| 
 | ||||
|         it_behaves_like 'does not expose relationship' | ||||
|       end | ||||
| 
 | ||||
|       context 'when current user can read user profile' do | ||||
|         let(:can_read_user_profile) { true } | ||||
| 
 | ||||
|         it_behaves_like 'exposes relationship' | ||||
|       end | ||||
| 
 | ||||
|       context 'when current user can read user profile and disable_follow_users is switched off' do | ||||
|         let(:can_read_user_profile) { true } | ||||
| 
 | ||||
|         before do | ||||
|           stub_feature_flags(disable_follow_users: false) | ||||
|           user.enabled_following = false | ||||
|           user.save! | ||||
|         end | ||||
| 
 | ||||
|         it_behaves_like 'exposes relationship' | ||||
|       end | ||||
| 
 | ||||
|       context 'when current user can read user profile, disable_follow_users is switched on and user disabled it for themself' do | ||||
|         let(:can_read_user_profile) { true } | ||||
| 
 | ||||
|         before do | ||||
|           user.enabled_following = false | ||||
|           user.save! | ||||
|         end | ||||
| 
 | ||||
|         it_behaves_like 'does not expose relationship' | ||||
|       end | ||||
| 
 | ||||
|       context 'when current user can read user profile, disable_follow_users is switched on and current user disabled it for themself' do | ||||
|         let(:can_read_user_profile) { true } | ||||
| 
 | ||||
|         before do | ||||
|           current_user.enabled_following = false | ||||
|           current_user.save! | ||||
|         end | ||||
| 
 | ||||
|         it_behaves_like 'does not expose relationship' | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,114 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Gitlab::BackgroundMigration::PopulateVulnerabilityDismissalFields, schema: 20230412185837, feature_category: :vulnerability_management do # rubocop:disable Layout/LineLength | ||||
|   let(:users) { table(:users) } | ||||
|   let(:namespaces) { table(:namespaces) } | ||||
|   let(:projects) { table(:projects) } | ||||
|   let(:vulnerabilities) { table(:vulnerabilities) } | ||||
|   let(:findings) { table(:vulnerability_occurrences) } | ||||
|   let(:scanners) { table(:vulnerability_scanners) } | ||||
|   let(:identifiers) { table(:vulnerability_identifiers) } | ||||
|   let(:feedback) { table(:vulnerability_feedback) } | ||||
| 
 | ||||
|   let(:user) { users.create!(name: 'test', email: 'test@example.com', projects_limit: 5) } | ||||
|   let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') } | ||||
|   let(:project) { projects.create!(namespace_id: namespace.id, name: 'foo', project_namespace_id: namespace.id) } | ||||
|   let(:vulnerability_1) do | ||||
|     vulnerabilities.create!(title: 'title', state: 2, severity: 0, | ||||
|       confidence: 5, report_type: 2, project_id: project.id, author_id: user.id | ||||
|     ) | ||||
|   end | ||||
| 
 | ||||
|   let(:vulnerability_2) do | ||||
|     vulnerabilities.create!(title: 'title', state: 2, severity: 0, | ||||
|       confidence: 5, report_type: 2, project_id: project.id, author_id: user.id | ||||
|     ) | ||||
|   end | ||||
| 
 | ||||
|   let(:scanner) { scanners.create!(project_id: project.id, external_id: 'foo', name: 'bar') } | ||||
|   let(:identifier) do | ||||
|     identifiers.create!(project_id: project.id, fingerprint: 'foo', | ||||
|       external_type: 'bar', external_id: 'zoo', name: 'identifier' | ||||
|     ) | ||||
|   end | ||||
| 
 | ||||
|   let(:uuid) { SecureRandom.uuid } | ||||
| 
 | ||||
|   before do | ||||
|     feedback.create!(feedback_type: 0, | ||||
|       category: 'sast', | ||||
|       project_fingerprint: '418291a26024a1445b23fe64de9380cdcdfd1fa8', | ||||
|       project_id: project.id, | ||||
|       author_id: user.id, | ||||
|       created_at: Time.current, | ||||
|       finding_uuid: uuid | ||||
|     ) | ||||
| 
 | ||||
|     findings.create!(name: 'Finding', | ||||
|       report_type: 'sast', | ||||
|       project_fingerprint: '418291a26024a1445b23fe64de9380cdcdfd1f98', | ||||
|       location_fingerprint: 'bar', | ||||
|       severity: 1, | ||||
|       confidence: 1, | ||||
|       metadata_version: 1, | ||||
|       raw_metadata: '', | ||||
|       details: {}, | ||||
|       uuid: uuid, | ||||
|       project_id: project.id, | ||||
|       vulnerability_id: vulnerability_1.id, | ||||
|       scanner_id: scanner.id, | ||||
|       primary_identifier_id: identifier.id | ||||
|     ) | ||||
| 
 | ||||
|     allow(::Gitlab::BackgroundMigration::Logger).to receive_messages(info: true, warn: true, error: true) | ||||
|   end | ||||
| 
 | ||||
|   subject do | ||||
|     described_class.new( | ||||
|       start_id: vulnerability_1.id, | ||||
|       end_id: vulnerability_2.id, | ||||
|       batch_table: :vulnerabilities, | ||||
|       batch_column: :id, | ||||
|       sub_batch_size: 200, | ||||
|       pause_ms: 2.minutes, | ||||
|       connection: ApplicationRecord.connection | ||||
|     ) | ||||
|   end | ||||
| 
 | ||||
|   describe '#perform' do | ||||
|     it 'updates the missing dismissal information of the vulnerability' do | ||||
|       expect { subject.perform }.to change { vulnerability_1.reload.dismissed_at } | ||||
|         .from(nil) | ||||
|         .and change { vulnerability_1.reload.dismissed_by_id }.from(nil).to(user.id) | ||||
|     end | ||||
| 
 | ||||
|     it 'writes log messages', :aggregate_failures do | ||||
|       subject.perform | ||||
| 
 | ||||
|       expect(::Gitlab::BackgroundMigration::Logger).to have_received(:info).with(migrator: described_class.name, | ||||
|         message: 'Dismissal information has been copied', | ||||
|         count: 2 | ||||
|       ) | ||||
|       expect(::Gitlab::BackgroundMigration::Logger).to have_received(:warn).with(migrator: described_class.name, | ||||
|         message: 'Could not update vulnerability!', | ||||
|         vulnerability_id: vulnerability_2.id | ||||
|       ) | ||||
|     end | ||||
| 
 | ||||
|     context 'when logger throws exception StandardError' do | ||||
|       before do | ||||
|         allow(::Gitlab::BackgroundMigration::Logger).to receive(:warn).and_raise(StandardError) | ||||
|       end | ||||
| 
 | ||||
|       it 'logs StandardError' do | ||||
|         expect(::Gitlab::BackgroundMigration::Logger).to receive(:error).with({ | ||||
|           migrator: described_class.name, message: StandardError.to_s, vulnerability_id: vulnerability_2.id | ||||
|         }) | ||||
| 
 | ||||
|         subject.perform | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,37 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| require_migration! | ||||
| 
 | ||||
| RSpec.describe QueuePopulateVulnerabilityDismissalFields, feature_category: :vulnerability_management do | ||||
|   let!(:batched_migration) { described_class::MIGRATION } | ||||
| 
 | ||||
|   describe '#up' do | ||||
|     it 'schedules a new batched migration' do | ||||
|       reversible_migration do |migration| | ||||
|         migration.before -> { | ||||
|           expect(batched_migration).not_to have_scheduled_batched_migration | ||||
|         } | ||||
| 
 | ||||
|         migration.after -> { | ||||
|           expect(batched_migration).to have_scheduled_batched_migration( | ||||
|             table_name: :vulnerabilities, | ||||
|             column_name: :id, | ||||
|             interval: described_class::DELAY_INTERVAL, | ||||
|             batch_size: described_class::BATCH_SIZE, | ||||
|             sub_batch_size: described_class::SUB_BATCH_SIZE | ||||
|           ) | ||||
|         } | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#down' do | ||||
|     it 'deletes all batched migration records' do | ||||
|       migrate! | ||||
|       schema_migrate_down! | ||||
| 
 | ||||
|       expect(batched_migration).not_to have_scheduled_batched_migration | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -3840,6 +3840,54 @@ RSpec.describe User, feature_category: :user_profile do | |||
| 
 | ||||
|       expect(user.following?(followee)).to be_falsey | ||||
|     end | ||||
| 
 | ||||
|     it 'does not follow if user disabled following' do | ||||
|       user = create(:user) | ||||
|       user.enabled_following = false | ||||
| 
 | ||||
|       followee = create(:user) | ||||
| 
 | ||||
|       expect(user.follow(followee)).to eq(false) | ||||
| 
 | ||||
|       expect(user.following?(followee)).to be_falsey | ||||
|     end | ||||
| 
 | ||||
|     it 'does not follow if followee user disabled following' do | ||||
|       user = create(:user) | ||||
| 
 | ||||
|       followee = create(:user) | ||||
|       followee.enabled_following = false | ||||
| 
 | ||||
|       expect(user.follow(followee)).to eq(false) | ||||
| 
 | ||||
|       expect(user.following?(followee)).to be_falsey | ||||
|     end | ||||
| 
 | ||||
|     context 'when disable_follow_users feature flag is off' do | ||||
|       before do | ||||
|         stub_feature_flags(disable_follow_users: false) | ||||
|       end | ||||
| 
 | ||||
|       it 'follows user even if user disabled following' do | ||||
|         user = create(:user) | ||||
|         user.enabled_following = false | ||||
| 
 | ||||
|         followee = create(:user) | ||||
| 
 | ||||
|         expect(user.follow(followee)).to be_truthy | ||||
|         expect(user.following?(followee)).to be_truthy | ||||
|       end | ||||
| 
 | ||||
|       it 'follows user even if followee user disabled following' do | ||||
|         user = create(:user) | ||||
| 
 | ||||
|         followee = create(:user) | ||||
|         followee.enabled_following = false | ||||
| 
 | ||||
|         expect(user.follow(followee)).to be_truthy | ||||
|         expect(user.following?(followee)).to be_truthy | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#unfollow' do | ||||
|  | @ -3882,6 +3930,39 @@ RSpec.describe User, feature_category: :user_profile do | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#following_users_allowed?' do | ||||
|     using RSpec::Parameterized::TableSyntax | ||||
| 
 | ||||
|     let_it_be(:user) { create(:user) } | ||||
|     let_it_be(:followee) { create(:user) } | ||||
| 
 | ||||
|     where(:user_enabled_following, :followee_enabled_following, :feature_flag_status, :result) do | ||||
|       true  | true  | false | true | ||||
|       true  | false | false | true | ||||
|       true  | true  | true  | true | ||||
|       true  | false | true  | false | ||||
|       false | true  | false | true | ||||
|       false | true  | true  | false | ||||
|       false | false | false | true | ||||
|       false | false | true  | false | ||||
|     end | ||||
| 
 | ||||
|     with_them do | ||||
|       before do | ||||
|         user.enabled_following = user_enabled_following | ||||
|         followee.enabled_following = followee_enabled_following | ||||
|         followee.save! | ||||
|         stub_feature_flags(disable_follow_users: feature_flag_status) | ||||
|       end | ||||
| 
 | ||||
|       it { expect(user.following_users_allowed?(followee)).to eq result } | ||||
|     end | ||||
| 
 | ||||
|     it 'is false when user and followee is the same user' do | ||||
|       expect(user.following_users_allowed?(user)).to eq(false) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#notification_email_or_default' do | ||||
|     let(:email) { 'gonzo@muppets.com' } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1009,6 +1009,20 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_profile | |||
|         expect(response).to have_gitlab_http_status(:not_modified) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'on a user with disabled following' do | ||||
|       before do | ||||
|         user.enabled_following = false | ||||
|         user.save! | ||||
|       end | ||||
| 
 | ||||
|       it 'does not change following' do | ||||
|         post api("/users/#{followee.id}/follow", user) | ||||
| 
 | ||||
|         expect(user.followees).to be_empty | ||||
|         expect(response).to have_gitlab_http_status(:not_modified) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe 'POST /users/:id/unfollow' do | ||||
|  |  | |||
|  | @ -914,6 +914,35 @@ RSpec.describe UsersController, feature_category: :user_management do | |||
|         expect(user).not_to be_following(public_user) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when user or followee disabled following' do | ||||
|       before do | ||||
|         sign_in(user) | ||||
|       end | ||||
| 
 | ||||
|       it 'alerts and not follow if user disabled following' do | ||||
|         user.enabled_following = false | ||||
| 
 | ||||
|         post user_follow_url(username: public_user.username) | ||||
|         expect(response).to be_redirect | ||||
| 
 | ||||
|         expected_message = format(_('Action not allowed.')) | ||||
|         expect(flash[:alert]).to eq(expected_message) | ||||
|         expect(user).not_to be_following(public_user) | ||||
|       end | ||||
| 
 | ||||
|       it 'alerts and not follow if followee disabled following' do | ||||
|         public_user.enabled_following = false | ||||
|         public_user.save! | ||||
| 
 | ||||
|         post user_follow_url(username: public_user.username) | ||||
|         expect(response).to be_redirect | ||||
| 
 | ||||
|         expected_message = format(_('Action not allowed.')) | ||||
|         expect(flash[:alert]).to eq(expected_message) | ||||
|         expect(user).not_to be_following(public_user) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'token authentication' do | ||||
|  |  | |||
|  | @ -1,17 +0,0 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'rubocop_spec_helper' | ||||
| require_relative '../../../../rubocop/cop/gitlab/deprecate_track_redis_hll_event' | ||||
| 
 | ||||
| RSpec.describe RuboCop::Cop::Gitlab::DeprecateTrackRedisHLLEvent do | ||||
|   it 'does not flag the use of track_event' do | ||||
|     expect_no_offenses('track_event :show, name: "p_analytics_insights"') | ||||
|   end | ||||
| 
 | ||||
|   it 'flags the use of track_redis_hll_event' do | ||||
|     expect_offense(<<~SOURCE) | ||||
|       track_redis_hll_event :show, name: 'p_analytics_valuestream' | ||||
|       ^^^^^^^^^^^^^^^^^^^^^ `track_redis_hll_event` is deprecated[...] | ||||
|     SOURCE | ||||
|   end | ||||
| end | ||||
|  | @ -185,6 +185,49 @@ RSpec.describe Users::UpdateService, feature_category: :user_profile do | |||
|       end.not_to raise_error | ||||
|     end | ||||
| 
 | ||||
|     describe 'updates the enabled_following' do | ||||
|       let(:user) { create(:user) } | ||||
| 
 | ||||
|       before do | ||||
|         3.times do | ||||
|           user.follow(create(:user)) | ||||
|           create(:user).follow(user) | ||||
|         end | ||||
|         user.reload | ||||
|       end | ||||
| 
 | ||||
|       it 'removes followers and followees' do | ||||
|         expect do | ||||
|           update_user(user, enabled_following: false) | ||||
|         end.to change { user.followed_users.count }.from(3).to(0) | ||||
|                                                    .and change { user.following_users.count }.from(3).to(0) | ||||
|         expect(user.enabled_following).to eq(false) | ||||
|       end | ||||
| 
 | ||||
|       it 'does not remove followers/followees if feature flag is off' do | ||||
|         stub_feature_flags(disable_follow_users: false) | ||||
| 
 | ||||
|         expect do | ||||
|           update_user(user, enabled_following: false) | ||||
|         end.to not_change { user.followed_users.count } | ||||
|                                                    .and not_change { user.following_users.count } | ||||
|       end | ||||
| 
 | ||||
|       context 'when there is more followers/followees then batch limit' do | ||||
|         before do | ||||
|           stub_env('BATCH_SIZE', 1) | ||||
|         end | ||||
| 
 | ||||
|         it 'removes followers and followees' do | ||||
|           expect do | ||||
|             update_user(user, enabled_following: false) | ||||
|           end.to change { user.followed_users.count }.from(3).to(0) | ||||
|                                                      .and change { user.following_users.count }.from(3).to(0) | ||||
|           expect(user.enabled_following).to eq(false) | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def update_user(user, opts) | ||||
|       described_class.new(user, opts.merge(user: user)).execute | ||||
|     end | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue