From 5eeb39104356356bec81abe19479bca591e6f299 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 23 Jun 2020 15:08:41 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitlab/ci/frontend.gitlab-ci.yml | 3 + .gitlab/ci/rails.gitlab-ci.yml | 2 + Gemfile | 1 + Gemfile.lock | 3 + .../components/sidebar/sidebar_assignees.vue | 15 +- app/assets/stylesheets/framework/buttons.scss | 21 ++- app/assets/stylesheets/framework/common.scss | 1 - .../pages/profiles/preferences.scss | 5 - .../concerns/membership_actions.rb | 5 +- .../resource_milestone_event_finder.rb | 71 ++++----- app/helpers/members_helper.rb | 8 + app/models/issue_assignee.rb | 5 +- app/models/merge_request_assignee.rb | 4 +- app/models/snippet.rb | 1 + app/models/snippet_statistics.rb | 28 ++++ app/models/user.rb | 5 +- .../alert_management/alerts/update_service.rb | 27 ++-- app/services/members/destroy_service.rb | 13 +- .../members/unassign_issuables_service.rb | 23 +++ app/views/profiles/preferences/show.html.haml | 11 +- app/views/shared/members/_member.html.haml | 2 +- app/workers/all_queues.yml | 8 + .../unassign_issuables_worker.rb | 32 ++++ ...-image-on-auto-browser-performance-tes.yml | 5 + .../unreleased/32942-add-dart-ci-template.yml | 5 + .../36720-unassig-user-on-remove-option.yml | 5 + .../dz-scope-project-snippet-routes.yml | 5 + ...tion-for-resource-milestone-events-api.yml | 5 + ...-add-count-logic-to-snippet-statistics.yml | 5 + .../unreleased/limit-alert-assignees.yml | 5 + .../nfriend-fix-theme-alignment.yml | 5 + config/karma.config.js | 2 +- config/routes/project.rb | 13 ++ config/sidekiq_queues.yml | 2 + doc/api/members.md | 1 + doc/api/resource_weight_events.md | 108 ++++++++++++++ .../database/database_reviewer_guidelines.md | 95 ++++++++++++ doc/development/database/index.md | 8 + doc/install/aws/index.md | 2 +- doc/user/infrastructure/index.md | 7 + doc/user/project/clusters/serverless/index.md | 2 +- lib/api/members.rb | 4 +- lib/api/resource_milestone_events.rb | 3 +- lib/gitlab/ci/templates/Dart.gitlab-ci.yml | 22 +++ .../Browser-Performance-Testing.gitlab-ci.yml | 6 +- locale/gitlab.pot | 6 + package.json | 2 +- scripts/frontend/merge_coverage_frontend.js | 2 +- scripts/merge-simplecov | 1 + .../resource_milestone_event_finder_spec.rb | 16 +- .../alert_managment_sidebar_assignees_spec.js | 22 ++- spec/models/issue_assignee_spec.rb | 35 ++++- spec/models/merge_request_assignee_spec.rb | 22 +++ spec/models/snippet_spec.rb | 11 ++ spec/models/snippet_statistics_spec.rb | 81 ++++++++++ spec/routing/project_routing_spec.rb | 4 + .../alerts/update_service_spec.rb | 139 ++++++++++-------- spec/services/members/destroy_service_spec.rb | 29 +++- .../unassign_issuables_service_spec.rb | 66 +++++++++ spec/simplecov_env.rb | 10 ++ ...ce_milestone_events_api_shared_examples.rb | 37 +++++ .../unassign_issuables_worker_spec.rb | 27 ++++ yarn.lock | 8 +- 63 files changed, 947 insertions(+), 180 deletions(-) create mode 100644 app/services/members/unassign_issuables_service.rb create mode 100644 app/workers/members_destroyer/unassign_issuables_worker.rb create mode 100644 changelogs/unreleased/221189-suppress-progress-on-pulling-image-on-auto-browser-performance-tes.yml create mode 100644 changelogs/unreleased/32942-add-dart-ci-template.yml create mode 100644 changelogs/unreleased/36720-unassig-user-on-remove-option.yml create mode 100644 changelogs/unreleased/dz-scope-project-snippet-routes.yml create mode 100644 changelogs/unreleased/fix-pagination-for-resource-milestone-events-api.yml create mode 100644 changelogs/unreleased/fj-223700-add-count-logic-to-snippet-statistics.yml create mode 100644 changelogs/unreleased/limit-alert-assignees.yml create mode 100644 changelogs/unreleased/nfriend-fix-theme-alignment.yml create mode 100644 doc/api/resource_weight_events.md create mode 100644 doc/development/database/database_reviewer_guidelines.md create mode 100644 lib/gitlab/ci/templates/Dart.gitlab-ci.yml create mode 100644 spec/services/members/unassign_issuables_service_spec.rb create mode 100644 spec/workers/members_destroyer/unassign_issuables_worker_spec.rb diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml index 62123ea07ff..72ef88cfeee 100644 --- a/.gitlab/ci/frontend.gitlab-ci.yml +++ b/.gitlab/ci/frontend.gitlab-ci.yml @@ -159,6 +159,7 @@ karma: - tmp/tests/frontend/ reports: junit: junit_karma.xml + cobertura: coverage-javascript/cobertura-coverage.xml karma-as-if-foss: extends: @@ -226,6 +227,8 @@ coverage-frontend: expire_in: 31d paths: - coverage-frontend/ + reports: + cobertura: coverage-frontend/cobertura-coverage.xml .qa-frontend-node: extends: diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index 535cbce0bba..657501337c4 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -256,6 +256,8 @@ rspec:coverage: - coverage/index.html - coverage/assets/ - tmp/memory_test/ + reports: + cobertura: coverage/coverage.xml # EE/FOSS: default refs (MRs, master, schedules) jobs # ####################################################### diff --git a/Gemfile b/Gemfile index 2e03d8ebcb0..5a6c0e13503 100644 --- a/Gemfile +++ b/Gemfile @@ -371,6 +371,7 @@ group :development, :test do gem 'scss_lint', '~> 0.56.0', require: false gem 'haml_lint', '~> 0.34.0', require: false gem 'simplecov', '~> 0.18.5', require: false + gem 'simplecov-cobertura', '~> 1.3.1', require: false gem 'bundler-audit', '~> 0.6.1', require: false gem 'benchmark-ips', '~> 2.3.0', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 6eb3fc1b8dd..db03015c1e0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1021,6 +1021,8 @@ GEM simplecov (0.18.5) docile (~> 1.1) simplecov-html (~> 0.11) + simplecov-cobertura (1.3.1) + simplecov (~> 0.8) simplecov-html (0.12.2) sixarm_ruby_unaccent (1.2.0) slack-messenger (2.3.3) @@ -1382,6 +1384,7 @@ DEPENDENCIES sidekiq-cron (~> 1.0) simple_po_parser (~> 1.1.2) simplecov (~> 0.18.5) + simplecov-cobertura (~> 1.3.1) slack-messenger (~> 2.3.3) snowplow-tracker (~> 0.6.1) spring (~> 2.0.0) diff --git a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue index 6c01fbcd788..1c249bc7cee 100644 --- a/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue +++ b/app/assets/javascripts/alert_management/components/sidebar/sidebar_assignees.vue @@ -25,6 +25,9 @@ export default { UPDATE_ALERT_ASSIGNEES_ERROR: s__( 'AlertManagement|There was an error while updating the assignee(s) of the alert. Please try again.', ), + UPDATE_ALERT_ASSIGNEES_GRAPHQL_ERROR: s__( + 'AlertManagement|This assignee cannot be assigned to this alert.', + ), components: { GlIcon, GlDropdown, @@ -156,9 +159,17 @@ export default { projectPath: this.projectPath, }, }) - .then(() => { + .then(({ data: { alertSetAssignees: { errors } = [] } = {} } = {}) => { this.hideDropdown(); - this.$emit('alert-refresh'); + + if (errors[0]) { + return this.$emit( + 'alert-sidebar-error', + `${this.$options.UPDATE_ALERT_ASSIGNEES_GRAPHQL_ERROR} ${errors[0]}.`, + ); + } + + return this.$emit('alert-refresh'); }) .catch(() => { this.$emit('alert-sidebar-error', this.$options.UPDATE_ALERT_ASSIGNEES_ERROR); diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index f47d0cab31f..1ed4484a934 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -171,7 +171,7 @@ @include btn-green; } - &.btn-inverted { + &.btn-inverted:not(.disabled):not(:disabled) { &.btn-success { @include btn-outline($white, $green-600, $green-500, $green-100, $green-700, $green-500, $green-200, $green-600, $green-800); } @@ -501,18 +501,19 @@ // All disabled buttons, regardless of color, type, etc %disabled { - background-color: $gray-light !important; - border-color: $gray-200 !important; - color: $gl-text-color-disabled !important; - opacity: 1 !important; - cursor: default !important; + background-color: $gray-light; + border-color: $gray-200; + color: $gl-text-color-disabled; + opacity: 1; + text-decoration: none; + cursor: default; &.cursor-not-allowed { - cursor: not-allowed !important; + cursor: not-allowed; } i { - color: $gl-text-color-disabled !important; + color: $gl-text-color-disabled; } } @@ -526,6 +527,10 @@ fieldset[disabled] .btn, &:hover { @extend %disabled; } + + &.btn-link { + background-color: transparent; + } } [readonly] { diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 5166c4c640d..d6d81680f58 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -421,7 +421,6 @@ img.emoji { .append-bottom-10 { margin-bottom: 10px; } .append-bottom-15 { margin-bottom: 15px; } .append-bottom-20 { margin-bottom: 20px; } -.prepend-bottom-32 { margin-bottom: 32px; } .ml-10 { margin-left: 4.5rem; } .inline { display: inline-block; } .center { text-align: center; } diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss index 3bab84af492..12386fa66ec 100644 --- a/app/assets/stylesheets/pages/profiles/preferences.scss +++ b/app/assets/stylesheets/pages/profiles/preferences.scss @@ -19,11 +19,6 @@ $ui-light-bg: #dfdfdf; $ui-dark-mode-bg: #1f1f1f; - label { - margin: 0 $gl-padding-32 $gl-padding 0; - text-align: center; - } - .preview { font-size: 0; height: 48px; diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb index 4ab02005b45..8c7f156f7f8 100644 --- a/app/controllers/concerns/membership_actions.rb +++ b/app/controllers/concerns/membership_actions.rb @@ -31,7 +31,10 @@ module MembershipActions def destroy member = membershipable.members_and_requesters.find(params[:id]) - Members::DestroyService.new(current_user).execute(member) + # !! is used in case unassign_issuables contains empty string which would result in nil + unassign_issuables = !!ActiveRecord::Type::Boolean.new.cast(params.delete(:unassign_issuables)) + + Members::DestroyService.new(current_user).execute(member, unassign_issuables: unassign_issuables) respond_to do |format| format.html do diff --git a/app/finders/resource_milestone_event_finder.rb b/app/finders/resource_milestone_event_finder.rb index 7af34f0a4bc..f3b779c8f77 100644 --- a/app/finders/resource_milestone_event_finder.rb +++ b/app/finders/resource_milestone_event_finder.rb @@ -1,69 +1,56 @@ # frozen_string_literal: true class ResourceMilestoneEventFinder - include FinderMethods - - MAX_PER_PAGE = 100 - - attr_reader :params, :current_user, :eventable - - def initialize(current_user, eventable, params = {}) + def initialize(current_user, eventable) @current_user = current_user @eventable = eventable - @params = params end + # Returns the ResourceMilestoneEvents of the eventable + # visible to the user. + # + # @return ResourceMilestoneEvent::ActiveRecord_AssociationRelation def execute - Kaminari.paginate_array(visible_events) + eventable.resource_milestone_events.include_relations + .where(milestone_id: readable_milestone_ids) # rubocop: disable CodeReuse/ActiveRecord end private - def visible_events - @visible_events ||= visible_to_user(events) + attr_reader :current_user, :eventable + + def readable_milestone_ids + readable_milestones = events_milestones.select do |milestone| + parent_availabilities[key_for_parent(milestone.parent)] + end + + readable_milestones.map(&:id).uniq end - def events - @events ||= eventable.resource_milestone_events.include_relations.page(page).per(per_page) + # rubocop: disable CodeReuse/ActiveRecord + def events_milestones + @events_milestones ||= Milestone.where(id: unique_milestone_ids_from_events) + .includes(:project, :group) end + # rubocop: enable CodeReuse/ActiveRecord - def visible_to_user(events) - events.select { |event| visible_for_user?(event) } - end - - def visible_for_user?(event) - milestone = event_milestones[event.milestone_id] - return if milestone.blank? - - parent = milestone.parent - parent_availabilities[key_for_parent(parent)] + def relevant_milestone_parents + events_milestones.map(&:parent).uniq end def parent_availabilities - @parent_availabilities ||= relevant_parents.to_h do |parent| + @parent_availabilities ||= relevant_milestone_parents.to_h do |parent| [key_for_parent(parent), Ability.allowed?(current_user, :read_milestone, parent)] end end + # rubocop: disable CodeReuse/ActiveRecord + def unique_milestone_ids_from_events + eventable.resource_milestone_events.select(:milestone_id).distinct + end + # rubocop: enable CodeReuse/ActiveRecord + def key_for_parent(parent) "#{parent.class.name}_#{parent.id}" end - - def event_milestones - @milestones ||= events.map(&:milestone).uniq.to_h do |milestone| - [milestone.id, milestone] - end - end - - def relevant_parents - @relevant_parents ||= event_milestones.map { |_id, milestone| milestone.parent } - end - - def per_page - [params[:per_page], MAX_PER_PAGE].compact.min - end - - def page - params[:page] || 1 - end end diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb index 31995c27fac..c9a0521228c 100644 --- a/app/helpers/members_helper.rb +++ b/app/helpers/members_helper.rb @@ -48,6 +48,14 @@ module MembersHelper "#{request.path}?#{options.to_param}" end + def member_path(member, unassign_issuables: false) + if member.is_a?(GroupMember) + group_group_member_path(member.source, member, { unassign_issuables: unassign_issuables }) + else + project_project_member_path(member.source, member, { unassign_issuables: unassign_issuables }) + end + end + private def source_text(member) diff --git a/app/models/issue_assignee.rb b/app/models/issue_assignee.rb index 8128b8a538e..e57acbae546 100644 --- a/app/models/issue_assignee.rb +++ b/app/models/issue_assignee.rb @@ -2,9 +2,12 @@ class IssueAssignee < ApplicationRecord belongs_to :issue - belongs_to :assignee, class_name: "User", foreign_key: :user_id + belongs_to :assignee, class_name: "User", foreign_key: :user_id, inverse_of: :issue_assignees validates :assignee, uniqueness: { scope: :issue_id } + + scope :in_projects, ->(project_ids) { joins(:issue).where("issues.project_id in (?)", project_ids) } + scope :on_issues, ->(issue_ids) { where(issue_id: issue_ids) } end IssueAssignee.prepend_if_ee('EE::IssueAssignee') diff --git a/app/models/merge_request_assignee.rb b/app/models/merge_request_assignee.rb index fe642bee8e2..2ac1de4321a 100644 --- a/app/models/merge_request_assignee.rb +++ b/app/models/merge_request_assignee.rb @@ -2,7 +2,9 @@ class MergeRequestAssignee < ApplicationRecord belongs_to :merge_request - belongs_to :assignee, class_name: "User", foreign_key: :user_id + belongs_to :assignee, class_name: "User", foreign_key: :user_id, inverse_of: :merge_request_assignees validates :assignee, uniqueness: { scope: :merge_request_id } + + scope :in_projects, ->(project_ids) { joins(:merge_request).where("merge_requests.target_project_id in (?)", project_ids) } end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 10b9b4eae3d..079b4b2baf4 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -69,6 +69,7 @@ class Snippet < ApplicationRecord validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values } after_save :store_mentions!, if: :any_mentionable_attributes_changed? + after_create :create_statistics # Scopes scope :are_internal, -> { where(visibility_level: Snippet::INTERNAL) } diff --git a/app/models/snippet_statistics.rb b/app/models/snippet_statistics.rb index 1ff324e3789..8030328ebe4 100644 --- a/app/models/snippet_statistics.rb +++ b/app/models/snippet_statistics.rb @@ -4,4 +4,32 @@ class SnippetStatistics < ApplicationRecord belongs_to :snippet validates :snippet, presence: true + + delegate :repository, to: :snippet + + def update_commit_count + self.commit_count = repository.commit_count + end + + def update_repository_size + self.repository_size = repository.size.megabytes + end + + def update_file_count + count = if snippet.repository_exists? + repository.ls_files(repository.root_ref).size + else + 0 + end + + self.file_count = count + end + + def refresh! + update_commit_count + update_repository_size + update_file_count + + save! + end end diff --git a/app/models/user.rb b/app/models/user.rb index 68d8c449fd6..4bebc5417c1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -163,9 +163,10 @@ class User < ApplicationRecord has_many :award_emoji, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id # rubocop:disable Cop/ActiveRecordDependent - has_many :issue_assignees + has_many :issue_assignees, inverse_of: :assignee + has_many :merge_request_assignees, inverse_of: :assignee has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue - has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent + has_many :assigned_merge_requests, class_name: "MergeRequest", through: :merge_request_assignees, source: :merge_request has_many :custom_attributes, class_name: 'UserCustomAttribute' has_many :callouts, class_name: 'UserCallout' diff --git a/app/services/alert_management/alerts/update_service.rb b/app/services/alert_management/alerts/update_service.rb index ffabbb37289..1c1addb4a7a 100644 --- a/app/services/alert_management/alerts/update_service.rb +++ b/app/services/alert_management/alerts/update_service.rb @@ -19,6 +19,8 @@ module AlertManagement return error_no_updates if params.empty? filter_assignees + return error_no_assignee_permissions if unauthorized_assignees? + old_assignees = alert.assignees.to_a if alert.update(params) @@ -38,10 +40,6 @@ module AlertManagement current_user&.can?(:update_alert_management_alert, alert) end - def assignee_todo_allowed? - assignee&.can?(:read_alert_management_alert, alert) - end - def todo_service strong_memoize(:todo_service) do TodoService.new @@ -64,18 +62,20 @@ module AlertManagement error(_('Please provide attributes to update')) end + def error_no_assignee_permissions + error(_('Assignee has no permissions')) + end + # ----- Assignee-related behavior ------ + def unauthorized_assignees? + params[:assignees]&.any? { |user| !user.can?(:read_alert_management_alert, alert) } + end + def filter_assignees return if params[:assignees].nil? - params[:assignees] = Array(assignee) - end - - def assignee - strong_memoize(:assignee) do - # Take first assignee while multiple are not currently supported - params[:assignees]&.first - end + # Always take first assignee while multiple are not currently supported + params[:assignees] = Array(params[:assignees].first) end def process_assignement(old_assignees) @@ -84,8 +84,7 @@ module AlertManagement end def assign_todo - # Remove check in follow-up issue https://gitlab.com/gitlab-org/gitlab/-/issues/222672 - return unless assignee_todo_allowed? + return if alert.assignees.empty? todo_service.assign_alert(alert, current_user) end diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index 20f64a99ad7..6e89cadb82d 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -2,7 +2,9 @@ module Members class DestroyService < Members::BaseService - def execute(member, skip_authorization: false, skip_subresources: false) + WAIT_FOR_DELETE = 1.hour + + def execute(member, skip_authorization: false, skip_subresources: false, unassign_issuables: false) raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_destroy_member?(member) @skip_auth = skip_authorization @@ -19,6 +21,7 @@ module Members delete_subresources(member) unless skip_subresources enqueue_delete_todos(member) + enqueue_unassign_issuables(member) if unassign_issuables after_execute(member: member) @@ -64,6 +67,14 @@ module Members raise "Unknown member type: #{member}!" end end + + def enqueue_unassign_issuables(member) + source_type = member.is_a?(GroupMember) ? 'Group' : 'Project' + + member.run_after_commit do + MembersDestroyer::UnassignIssuablesWorker.perform_in(WAIT_FOR_DELETE, member.user_id, member.source_id, source_type) + end + end end end diff --git a/app/services/members/unassign_issuables_service.rb b/app/services/members/unassign_issuables_service.rb new file mode 100644 index 00000000000..95e07deb761 --- /dev/null +++ b/app/services/members/unassign_issuables_service.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Members + class UnassignIssuablesService + attr_reader :user, :entity + + def initialize(user, entity) + @user = user + @entity = entity + end + + def execute + return unless entity && user + + project_ids = entity.is_a?(Group) ? entity.all_projects.select(:id) : [entity.id] + + user.issue_assignees.on_issues(Issue.in_projects(project_ids).select(:id)).delete_all + user.merge_request_assignees.in_projects(project_ids).delete_all + + user.invalidate_cache_counts + end + end +end diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index cc44d137848..d18dc0d7dc7 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -8,11 +8,12 @@ %p = s_('Preferences|Customize the appearance of the application header and navigation sidebar.') .col-lg-8.application-theme - - Gitlab::Themes.each do |theme| - = label_tag do - .preview{ class: theme.css_class } - = f.radio_button :theme_id, theme.id, checked: Gitlab::Themes.for_user(@user).id == theme.id - = theme.name + .row + - Gitlab::Themes.each do |theme| + %label.col-6.col-sm-4.col-md-3.gl-mb-5.gl-text-center + .preview{ class: theme.css_class } + = f.radio_button :theme_id, theme.id, checked: Gitlab::Themes.for_user(@user).id == theme.id + = theme.name .col-sm-12 %hr diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index f7d90a588c7..6b8739194d4 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -118,7 +118,7 @@ data: { confirm: leave_confirmation_message(member.source) }, class: "btn btn-remove align-self-center m-0 #{'ml-sm-2' unless force_mobile_view}" - elsif !user&.project_bot? - = link_to member, + = link_to member_path(member.member), method: :delete, data: { confirm: remove_member_message(member), qa_selector: 'delete_member_button' }, class: "btn btn-remove align-self-center m-0 #{'ml-sm-2' unless force_mobile_view}", diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 8750223e2bf..2053a60fc23 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -1115,6 +1115,14 @@ :weight: 1 :idempotent: :tags: [] +- :name: unassign_issuables:members_destroyer_unassign_issuables + :feature_category: :authentication_and_authorization + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: update_namespace_statistics:namespaces_root_statistics :feature_category: :source_code_management :has_external_dependencies: diff --git a/app/workers/members_destroyer/unassign_issuables_worker.rb b/app/workers/members_destroyer/unassign_issuables_worker.rb new file mode 100644 index 00000000000..2c17120bf48 --- /dev/null +++ b/app/workers/members_destroyer/unassign_issuables_worker.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module MembersDestroyer + class UnassignIssuablesWorker + include ApplicationWorker + + ENTITY_TYPES = %w(Group Project).freeze + + queue_namespace :unassign_issuables + feature_category :authentication_and_authorization + + idempotent! + + def perform(user_id, entity_id, entity_type) + unless ENTITY_TYPES.include?(entity_type) + logger.error( + message: "#{entity_type} is not a supported entity.", + entity_type: entity_type, + entity_id: entity_id, + user_id: user_id + ) + + return + end + + user = User.find(user_id) + entity = entity_type.constantize.find(entity_id) + + ::Members::UnassignIssuablesService.new(user, entity).execute + end + end +end diff --git a/changelogs/unreleased/221189-suppress-progress-on-pulling-image-on-auto-browser-performance-tes.yml b/changelogs/unreleased/221189-suppress-progress-on-pulling-image-on-auto-browser-performance-tes.yml new file mode 100644 index 00000000000..b553b0db1c2 --- /dev/null +++ b/changelogs/unreleased/221189-suppress-progress-on-pulling-image-on-auto-browser-performance-tes.yml @@ -0,0 +1,5 @@ +--- +title: Suppress progress on pulling on Performance Test +merge_request: 34368 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/32942-add-dart-ci-template.yml b/changelogs/unreleased/32942-add-dart-ci-template.yml new file mode 100644 index 00000000000..15c8ab78af6 --- /dev/null +++ b/changelogs/unreleased/32942-add-dart-ci-template.yml @@ -0,0 +1,5 @@ +--- +title: Added CI template for Dart +merge_request: 32942 +author: agilob +type: added diff --git a/changelogs/unreleased/36720-unassig-user-on-remove-option.yml b/changelogs/unreleased/36720-unassig-user-on-remove-option.yml new file mode 100644 index 00000000000..db5a1861053 --- /dev/null +++ b/changelogs/unreleased/36720-unassig-user-on-remove-option.yml @@ -0,0 +1,5 @@ +--- +title: Extend members REST API with the option to unassign Issues and Merge Requests when member leaves team +merge_request: 34388 +author: +type: changed diff --git a/changelogs/unreleased/dz-scope-project-snippet-routes.yml b/changelogs/unreleased/dz-scope-project-snippet-routes.yml new file mode 100644 index 00000000000..66902b26bd9 --- /dev/null +++ b/changelogs/unreleased/dz-scope-project-snippet-routes.yml @@ -0,0 +1,5 @@ +--- +title: Copy project snippet routes under - scope +merge_request: 35022 +author: +type: other diff --git a/changelogs/unreleased/fix-pagination-for-resource-milestone-events-api.yml b/changelogs/unreleased/fix-pagination-for-resource-milestone-events-api.yml new file mode 100644 index 00000000000..18cc7e3bc29 --- /dev/null +++ b/changelogs/unreleased/fix-pagination-for-resource-milestone-events-api.yml @@ -0,0 +1,5 @@ +--- +title: Fix pagination for resource milestone events api +merge_request: 33845 +author: +type: fixed diff --git a/changelogs/unreleased/fj-223700-add-count-logic-to-snippet-statistics.yml b/changelogs/unreleased/fj-223700-add-count-logic-to-snippet-statistics.yml new file mode 100644 index 00000000000..32c9129f6c0 --- /dev/null +++ b/changelogs/unreleased/fj-223700-add-count-logic-to-snippet-statistics.yml @@ -0,0 +1,5 @@ +--- +title: Add snippet statistics logic +merge_request: 35118 +author: +type: added diff --git a/changelogs/unreleased/limit-alert-assignees.yml b/changelogs/unreleased/limit-alert-assignees.yml new file mode 100644 index 00000000000..8e57e2c0562 --- /dev/null +++ b/changelogs/unreleased/limit-alert-assignees.yml @@ -0,0 +1,5 @@ +--- +title: Limit alert assignment to only users who can read alerts +merge_request: 34681 +author: +type: fixed diff --git a/changelogs/unreleased/nfriend-fix-theme-alignment.yml b/changelogs/unreleased/nfriend-fix-theme-alignment.yml new file mode 100644 index 00000000000..e3e55479a07 --- /dev/null +++ b/changelogs/unreleased/nfriend-fix-theme-alignment.yml @@ -0,0 +1,5 @@ +--- +title: Fix alignment of navigation theme options +merge_request: 35041 +author: +type: fixed diff --git a/config/karma.config.js b/config/karma.config.js index 97794225a3f..31fdd5bffd1 100644 --- a/config/karma.config.js +++ b/config/karma.config.js @@ -181,7 +181,7 @@ module.exports = function(config) { if (process.env.BABEL_ENV === 'coverage' || process.env.NODE_ENV === 'coverage') { karmaConfig.reporters.push('coverage-istanbul'); karmaConfig.coverageIstanbulReporter = { - reports: ['html', 'text-summary'], + reports: ['html', 'text-summary', 'cobertura'], dir: 'coverage-javascript/', subdir: '.', fixWebpackSourcePaths: true, diff --git a/config/routes/project.rb b/config/routes/project.rb index d00e0ab1d04..48279b36fc1 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -369,6 +369,19 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end end + # Serve snippet routes under /-/snippets. + # To ensure an old unscoped routing is used for the UI we need to + # add prefix 'as' to the scope routing and place it below original routing. + # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/29572 + scope '-', as: :scoped do + resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do # rubocop: disable Cop/PutProjectRoutesUnderScope + member do + get :raw + post :mark_as_spam + end + end + end + namespace :prometheus do resources :alerts, constraints: { id: /\d+/ }, only: [:index, :create, :show, :update, :destroy] do # rubocop: disable Cop/PutProjectRoutesUnderScope post :notify, on: :collection diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 0052910b56e..edeff06c63c 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -262,6 +262,8 @@ - 1 - - todos_destroyer - 1 +- - unassign_issuables + - 1 - - update_external_pull_requests - 3 - - update_highest_role diff --git a/doc/api/members.md b/doc/api/members.md index dadd609b7ed..979d8a7a186 100644 --- a/doc/api/members.md +++ b/doc/api/members.md @@ -373,6 +373,7 @@ DELETE /projects/:id/members/:user_id | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user | | `user_id` | integer | yes | The user ID of the member | +| `unassign_issuables` | boolean | false | Flag indicating if the removed member should be unassigned from any issues or merge requests within given group or project | ```shell curl --request DELETE --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/groups/:id/members/:user_id" diff --git a/doc/api/resource_weight_events.md b/doc/api/resource_weight_events.md new file mode 100644 index 00000000000..700ef288440 --- /dev/null +++ b/doc/api/resource_weight_events.md @@ -0,0 +1,108 @@ +--- +stage: Plan +group: Project Management +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +--- + +# Resource weight events API + +Resource weight events keep track of what happens to GitLab [issues](../user/project/issues/). + +Use them to track which weight was set, who did it, and when it happened. + +## Issues + +### List project issue weight events + +Gets a list of all weight events for a single issue. + +```plaintext +GET /projects/:id/issues/:issue_iid/resource_weight_events +``` + +| Attribute | Type | Required | Description | +| ----------- | -------------- | -------- | ------------------------------------------------------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | +| `issue_iid` | integer | yes | The IID of an issue | + +Example request: + +```shell +curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/5/issues/11/resource_weight_events" +``` + +Example response: + +```json +[ + { + "id": 142, + "user": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://gitlab.example.com/root" + }, + "created_at": "2018-08-20T13:38:20.077Z", + "issue_id": 253, + "weight": 3 + }, + { + "id": 143, + "user": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://gitlab.example.com/root" + }, + "created_at": "2018-08-21T14:38:20.077Z", + "issue_id": 253, + "weight": 2 + } +] +``` + +### Get single issue weight event + +Returns a single weight event for a specific project issue + +```plaintext +GET /projects/:id/issues/:issue_iid/resource_weight_events/:resource_weight_event_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +| ----------------------------- | -------------- | -------- | ------------------------------------------------------------------------------- | +| `id` | integer/string | yes | The ID or [URL-encoded path](README.md#namespaced-path-encoding) of the project | +| `issue_iid` | integer | yes | The IID of an issue | +| `resource_weight_event_id` | integer | yes | The ID of a weight event | + +Example request: + +```shell +curl --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/5/issues/11/resource_weight_events/143" +``` + +Example response: + +```json +{ +"id": 143, +"user": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://gitlab.example.com/root" +}, +"created_at": "2018-08-21T14:38:20.077Z", +"issue_id": 253, +"weight": 2 +} +``` diff --git a/doc/development/database/database_reviewer_guidelines.md b/doc/development/database/database_reviewer_guidelines.md new file mode 100644 index 00000000000..f3f0a69d54a --- /dev/null +++ b/doc/development/database/database_reviewer_guidelines.md @@ -0,0 +1,95 @@ +# Database Reviewer Guidelines + +This page includes introductory material for new database reviewers. + +If you are interested in getting an application update reviewed, +check the [database review guidelines](../database_review.md). + +## Scope of work done by a database reviewer + +Database reviewers are domain experts who have substantial experience with databases, +`SQL`, and query performance optimization. + +A database review is required whenever an application update [touches the database](../database_review.md#general-process). + +The database reviewer is tasked with reviewing the database specific updates and +making sure that any queries or modifications will perform without issues +at the scale of GitLab.com. + +For more information on the database review process, check the [database review guidelines](../database_review.md). + +## How to apply for becoming a database reviewer + +Team members are encouraged to self-identify as database domain experts and add it to their [team profile](https://gitlab.com/gitlab-com/www-gitlab-com/-/blob/master/data/team.yml) + +```yaml + projects: + gitlab: + - reviewer database +``` + +Assign the MR which adds your expertise to the `team.yml` file to a database maintainer +or the [Database Team's Engineering Manager](https://about.gitlab.com/handbook/engineering/development/enablement/database/). + +Once the `team.yml` update is merged, the [Reviewer roulette](../code_review.md#reviewer-roulette) +may recommend you as a database reviewer. + +## Resources for database reviewers + +As a database reviewer, join the internal `#database` Slack channel and ask questions or discuss +database related issues with other database reviewers and maintainers. + +There is also an optional database office hours call held bi-weekly, alternating between +European/US and APAC friendly hours. You can join the office hours call and bring topics +that require a more in-depth discussion between the database reviewers and maintainers: + +- [Database Office Hours Agenda](https://docs.google.com/document/d/1wgfmVL30F8SdMg-9yY6Y8djPSxWNvKmhR5XmsvYX1EI/edit). +- [Youtube playlist with past recordings](https://www.youtube.com/playlist?list=PL05JrBw4t0Kp-kqXeiF7fF7cFYaKtdqXM). + +You should also join the [#database-labs](../understanding_explain_plans.md#database-lab) +Slack channel and get familiar with how to use Joe, the slackbot that provides developers +with their own clone of the production database. + +Understanding and efficiently using `EXPLAIN` plans is at the core of the database review process. +The following guides provide a quick introduction and links to follow on more advanced topics: + +- Guide on [understanding EXPLAIN plans](../understanding_explain_plans.md). +- [Explaining the unexplainable series in depesz](https://www.depesz.com/tag/unexplainable/). + +Finally, you can find various guides in the [Database guides](index.md) page that cover more specific +topics and use cases. The most frequently required during database reviewing are the following: + +- [Migrations style guide](../migration_style_guide.md) for creating safe SQL migrations. +- [What requires downtime?](../what_requires_downtime.md). +- [SQL guidelines](../sql.md) for working with SQL queries. + +## How to apply for becoming a database maintainer + +Once a database reviewer feels confident on switching to a database maintainer, +they can update their [team profile](https://gitlab.com/gitlab-com/www-gitlab-com/-/blob/master/data/team.yml) +to a `trainee_maintainer database`: + +```yaml + projects: + gitlab: + - trainee_maintainer database +``` + +The first step is to a create a [Trainee Database Maintainer Issue](https://gitlab.com/gitlab-com/www-gitlab-com/issues/new?issuable_template=trainee-database-maintainer). +Use and follow the process described in the 'Trainee database maintainer' template. + +Note that [trainee maintainers](https://about.gitlab.com/handbook/engineering/workflow/code-review/#trainee-maintainer) +are three times as likely to be picked by the [Danger bot](../dangerbot.md) as other reviewers. + +## What to do if you feel overwhelmed + +Similar to all types of reviews, [unblocking others is always a top priority](https://about.gitlab.com/handbook/values/#global-optimization). +Database reviewers are expected to [review assigned merge requests in a timely manner](../code_review.md#review-turnaround-time) +or let the author know as soon as possible and help them find another reviewer or maintainer. + +We are doing reviews to help the rest of the GitLab team and, at the same time, get exposed +to more use cases, get a lot of insights and hone our database and data management skills. + +If you are feeling overwhelmed, think you are at capacity, and are unable to accept any more +reviews until some have been completed, communicate this through your GitLab status by setting +the `:red_circle:` emoji and mentioning that you are at capacity in the status text. diff --git a/doc/development/database/index.md b/doc/development/database/index.md index 665af623059..9ea5b6fcaac 100644 --- a/doc/development/database/index.md +++ b/doc/development/database/index.md @@ -1,5 +1,13 @@ # Database guides +## Database Reviews + +- If you're creating a database MR for review, check out our [Database review guidelines](../database_review.md). + + It provides an introduction on database-related changes, migrations, and complex SQL queries. + +- If you're a database reviewer or want to become one, check out our [introduction to reviewing database changes](database_reviewer_guidelines.md). + ## Tooling - [Understanding EXPLAIN plans](../understanding_explain_plans.md) diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md index 83030b6d01d..d0f90d45ea7 100644 --- a/doc/install/aws/index.md +++ b/doc/install/aws/index.md @@ -8,7 +8,7 @@ This page offers a walkthrough of a common configuration for GitLab on AWS. You should customize it to accommodate your needs. NOTE: **Note** -For organizations with 300 users or less, the recommended AWS installation method is to launch an EC2 single box [Omnibus Installation](https://about.gitlab.com/install/) and implement a snapshot strategy for backing up the data. +For organizations with 1,000 users or less, the recommended AWS installation method is to launch an EC2 single box [Omnibus Installation](https://about.gitlab.com/install/) and implement a snapshot strategy for backing up the data. See the [1,000 user reference architecture](../../administration/reference_architectures/1k_users.md) for more. ## Introduction diff --git a/doc/user/infrastructure/index.md b/doc/user/infrastructure/index.md index 38ba3979006..a6dbd13260e 100644 --- a/doc/user/infrastructure/index.md +++ b/doc/user/infrastructure/index.md @@ -6,6 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Infrastructure as code with Terraform and GitLab +## Motivation + +The Terraform integration features within GitLab enable your GitOps / Infrastructure-as-Code (IaC) +workflows to tie into GitLab's authentication and authorization. These features focus on +lowering the barrier to entry for teams to adopt Terraform, collaborate effectively within +GitLab, and support Terraform best practices. + ## GitLab managed Terraform State > [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2673) in GitLab 13.0. diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index 45fb313d177..8f7a0022ff4 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -835,7 +835,7 @@ The instructions below relate to installing and running Certbot on a Linux serve ## Using an older version of `gitlabktl` There may be situations where you want to run an older version of `gitlabktl`. This -requires setting an older version of the `gitlabktl` image in the `.gitlab-ci.yml file.` +requires setting an older version of the `gitlabktl` image in the `.gitlab-ci.yml` file. To set an older version, add `image:` to the `functions:deploy` block. For example: diff --git a/lib/api/members.rb b/lib/api/members.rb index 37d4ca29b68..341d4837468 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -145,6 +145,8 @@ module API desc 'Removes a user from a group or project.' params do requires :user_id, type: Integer, desc: 'The user ID of the member' + optional :unassign_issuables, type: Boolean, default: false, + desc: 'Flag indicating if the removed member should be unassigned from any issues or merge requests within given group or project' end # rubocop: disable CodeReuse/ActiveRecord delete ":id/members/:user_id" do @@ -152,7 +154,7 @@ module API member = source.members.find_by!(user_id: params[:user_id]) destroy_conditionally!(member) do - ::Members::DestroyService.new(current_user).execute(member) + ::Members::DestroyService.new(current_user).execute(member, unassign_issuables: params[:unassign_issuables]) end end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/api/resource_milestone_events.rb b/lib/api/resource_milestone_events.rb index 30ff5a9b4be..e466e236371 100644 --- a/lib/api/resource_milestone_events.rb +++ b/lib/api/resource_milestone_events.rb @@ -26,8 +26,7 @@ module API get ":id/#{eventables_str}/:eventable_id/resource_milestone_events" do eventable = find_noteable(eventable_type, params[:eventable_id]) - opts = { page: params[:page], per_page: params[:per_page] } - events = ResourceMilestoneEventFinder.new(current_user, eventable, opts).execute + events = ResourceMilestoneEventFinder.new(current_user, eventable).execute present paginate(events), with: Entities::ResourceMilestoneEvent end diff --git a/lib/gitlab/ci/templates/Dart.gitlab-ci.yml b/lib/gitlab/ci/templates/Dart.gitlab-ci.yml new file mode 100644 index 00000000000..cc383f89b0c --- /dev/null +++ b/lib/gitlab/ci/templates/Dart.gitlab-ci.yml @@ -0,0 +1,22 @@ +# https://hub.docker.com/r/google/dart +image: google/dart:2.8.4 + +variables: + # Use to learn more: + # pub run test --help + PUB_VARS: "--platform vm --timeout 30s --concurrency=6 --test-randomize-ordering-seed=random --reporter=expanded" + +# Cache downloaded dependencies and plugins between builds. +# To keep cache across branches add 'key: "$CI_JOB_NAME"' +cache: + paths: + - .pub-cache/global_packages + +before_script: + - export PATH="$PATH":"~/.pub-cache/bin" + - pub get --no-precompile + +test: + stage: test + script: + - pub run test $PUB_VARS diff --git a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml index 9a34f8cb113..d4ac8036594 100644 --- a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml @@ -4,6 +4,7 @@ performance: allow_failure: true variables: DOCKER_TLS_CERTDIR: "" + SITESPEED_IMAGE: "sitespeedio/sitespeed.io:11.2.0" services: - docker:19.03.11-dind script: @@ -17,13 +18,14 @@ performance: - mkdir gitlab-exporter - wget -O gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/1.0.0/index.js - mkdir sitespeed-results + - docker pull --quiet ${SITESPEED_IMAGE} - | if [ -f .gitlab-urls.txt ] then sed -i -e 's@^@'"$CI_ENVIRONMENT_URL"'@' .gitlab-urls.txt - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:11.2.0 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results .gitlab-urls.txt + docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io ${SITESPEED_IMAGE} --plugins.add ./gitlab-exporter --outputFolder sitespeed-results .gitlab-urls.txt else - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:11.2.0 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL" + docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io ${SITESPEED_IMAGE} --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL" fi - mv sitespeed-results/data/performance.json performance.json artifacts: diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ec654041b5f..b837fd415fe 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2011,6 +2011,9 @@ msgstr "" msgid "AlertManagement|There was an error while updating the status of the alert. Please try again." msgstr "" +msgid "AlertManagement|This assignee cannot be assigned to this alert." +msgstr "" + msgid "AlertManagement|Tool" msgstr "" @@ -3020,6 +3023,9 @@ msgid_plural "%d Assignees" msgstr[0] "" msgstr[1] "" +msgid "Assignee has no permissions" +msgstr "" + msgid "Assignee lists not available with your current license" msgstr "" diff --git a/package.json b/package.json index 2abe1e69f84..38d7089ef77 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@babel/plugin-syntax-import-meta": "^7.10.1", "@babel/preset-env": "^7.10.1", "@gitlab/at.js": "1.5.5", - "@gitlab/svgs": "1.140.0", + "@gitlab/svgs": "1.141.0", "@gitlab/ui": "17.2.0", "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "^6.0.3-1", diff --git a/scripts/frontend/merge_coverage_frontend.js b/scripts/frontend/merge_coverage_frontend.js index 507695b45e5..99034176b29 100644 --- a/scripts/frontend/merge_coverage_frontend.js +++ b/scripts/frontend/merge_coverage_frontend.js @@ -26,6 +26,6 @@ reportFiles const context = createContext({ coverageMap: coverageMap, dir: 'coverage-frontend' }); -['json', 'lcov', 'text-summary', 'clover'].forEach(reporter => { +['json', 'lcov', 'text-summary', 'clover', 'cobertura'].forEach(reporter => { create(reporter, {}).execute(context); }); diff --git a/scripts/merge-simplecov b/scripts/merge-simplecov index 746be3317a7..c00dae81c4d 100755 --- a/scripts/merge-simplecov +++ b/scripts/merge-simplecov @@ -2,6 +2,7 @@ require_relative '../spec/simplecov_env' SimpleCovEnv.configure_profile +SimpleCovEnv.configure_formatter module SimpleCov module ResultMerger diff --git a/spec/finders/resource_milestone_event_finder_spec.rb b/spec/finders/resource_milestone_event_finder_spec.rb index ff4508996e2..27e124afe2e 100644 --- a/spec/finders/resource_milestone_event_finder_spec.rb +++ b/spec/finders/resource_milestone_event_finder_spec.rb @@ -42,18 +42,6 @@ RSpec.describe ResourceMilestoneEventFinder do expect(subject).to be_empty end - it 'paginates results' do - milestone = create(:milestone, project: issue_project) - create_event(milestone) - create_event(milestone) - issue_project.add_guest(user) - - paginated = described_class.new(user, issue, per_page: 1).execute - - expect(subject.count).to eq 2 - expect(paginated.count).to eq 1 - end - context 'when multiple events share the same milestone' do it 'avoids N+1 queries' do issue_project.add_developer(user) @@ -71,8 +59,8 @@ RSpec.describe ResourceMilestoneEventFinder do create_event(milestone2, :add) create_event(milestone2, :remove) - # 1 events + 1 milestones + 1 project + 1 user + 4 ability - expect { described_class.new(user, issue).execute }.not_to exceed_query_limit(control_count + 7) + # 1 milestones + 1 project + 1 user + 4 ability + expect { described_class.new(user, issue).execute }.not_to exceed_query_limit(control_count + 6) end end diff --git a/spec/frontend/alert_management/components/alert_managment_sidebar_assignees_spec.js b/spec/frontend/alert_management/components/alert_managment_sidebar_assignees_spec.js index 2452ba7c63b..2cab761f730 100644 --- a/spec/frontend/alert_management/components/alert_managment_sidebar_assignees_spec.js +++ b/spec/frontend/alert_management/components/alert_managment_sidebar_assignees_spec.js @@ -59,7 +59,7 @@ describe('Alert Details Sidebar Assignees', () => { describe('updating the alert status', () => { const mockUpdatedMutationResult = { data: { - updateAlertStatus: { + alertSetAssignees: { errors: [], alert: { assigneeUsernames: ['root'], @@ -125,6 +125,26 @@ describe('Alert Details Sidebar Assignees', () => { }); }); + it('shows an error when request contains error messages', () => { + wrapper.setData({ isDropdownSearching: false }); + const errorMutationResult = { + data: { + alertSetAssignees: { + errors: ['There was a problem for sure.'], + alert: {}, + }, + }, + }; + + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(errorMutationResult); + + return wrapper.vm.$nextTick().then(() => { + const SideBarAssigneeItem = wrapper.findAll(SidebarAssignee).at(0); + SideBarAssigneeItem.vm.$emit('click'); + expect(wrapper.emitted('alert-refresh')).toBeUndefined(); + }); + }); + it('stops updating and cancels loading when the request fails', () => { jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error())); wrapper.vm.updateAlertAssignees('root'); diff --git a/spec/models/issue_assignee_spec.rb b/spec/models/issue_assignee_spec.rb index 2d59ba15101..df8e91cd133 100644 --- a/spec/models/issue_assignee_spec.rb +++ b/spec/models/issue_assignee_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe IssueAssignee do +RSpec.describe IssueAssignee do let(:issue) { create(:issue) } subject { issue.issue_assignees.build(assignee: create(:user)) } @@ -15,4 +15,37 @@ describe IssueAssignee do describe 'validations' do it { is_expected.to validate_uniqueness_of(:assignee).scoped_to(:issue_id) } end + + describe 'scopes' do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:project_issue) { create(:issue, project: project, assignee_ids: [user.id]) } + + before do + issue.update!(assignee_ids: [user.id]) + end + + context 'in_projects' do + it 'returns issue assignees for given project' do + expect(IssueAssignee.count).to eq 2 + + assignees = IssueAssignee.in_projects([project]) + + expect(assignees.count).to eq 1 + expect(assignees.first.user_id).to eq project_issue.issue_assignees.first.user_id + expect(assignees.first.issue_id).to eq project_issue.issue_assignees.first.issue_id + end + end + + context 'on_issues' do + it 'returns issue assignees for given issues' do + expect(IssueAssignee.count).to eq 2 + + assignees = IssueAssignee.on_issues([project_issue]) + + expect(assignees.count).to eq 1 + expect(assignees.first.issue_id).to eq project_issue.issue_assignees.first.issue_id + end + end + end end diff --git a/spec/models/merge_request_assignee_spec.rb b/spec/models/merge_request_assignee_spec.rb index d6aab15d990..bd2df428ed0 100644 --- a/spec/models/merge_request_assignee_spec.rb +++ b/spec/models/merge_request_assignee_spec.rb @@ -15,4 +15,26 @@ describe MergeRequestAssignee do describe 'validations' do it { is_expected.to validate_uniqueness_of(:assignee).scoped_to(:merge_request_id) } end + + describe 'scopes' do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:project_merge_request) { create(:merge_request, target_project: project, source_project: project, assignee_ids: [user.id]) } + + before do + merge_request.update!(assignee_ids: [user.id]) + end + + context 'in_projects' do + it 'returns issue assignees for given project' do + expect(MergeRequestAssignee.count).to eq 2 + + assignees = MergeRequestAssignee.in_projects([project]) + + expect(assignees.count).to eq 1 + expect(assignees.first.user_id).to eq project_merge_request.merge_request_assignees.first.user_id + expect(assignees.first.merge_request_id).to eq project_merge_request.merge_request_assignees.first.merge_request_id + end + end + end end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index 6edaf0410f5..7ded8c380fe 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -92,6 +92,17 @@ describe Snippet do end end + describe 'callbacks' do + it 'creates snippet statistics when the snippet is created' do + snippet = build(:snippet) + expect(snippet.statistics).to be_nil + + snippet.save + + expect(snippet.statistics).to be_persisted + end + end + describe '#to_reference' do context 'when snippet belongs to a project' do let(:project) { build(:project, name: 'sample-project') } diff --git a/spec/models/snippet_statistics_spec.rb b/spec/models/snippet_statistics_spec.rb index d76fe5fb589..1bfb80e2c1d 100644 --- a/spec/models/snippet_statistics_spec.rb +++ b/spec/models/snippet_statistics_spec.rb @@ -3,6 +3,87 @@ require 'spec_helper' describe SnippetStatistics do + let_it_be(:snippet_without_repo) { create(:snippet) } + let_it_be(:snippet_with_repo) { create(:snippet, :repository) } + + let(:statistics) { snippet_with_repo.statistics } + it { is_expected.to belong_to(:snippet) } it { is_expected.to validate_presence_of(:snippet) } + + describe '#update_commit_count' do + subject { statistics.update_commit_count } + + it 'updates the count of commits' do + commit_count = snippet_with_repo.repository.commit_count + + subject + + expect(statistics.commit_count).to eq commit_count + end + + context 'when the snippet does not have a repository' do + let(:statistics) { snippet_without_repo.statistics } + + it 'returns 0' do + expect(subject).to eq 0 + expect(statistics.commit_count).to eq 0 + end + end + end + + describe '#update_file_count' do + subject { statistics.update_file_count } + + it 'updates the count of files' do + file_count = snippet_with_repo.repository.ls_files(nil).count + + subject + + expect(statistics.file_count).to eq file_count + end + + context 'when the snippet does not have a repository' do + let(:statistics) { snippet_without_repo.statistics } + + it 'returns 0' do + expect(subject).to eq 0 + expect(statistics.file_count).to eq 0 + end + end + end + + describe '#update_repository_size' do + subject { statistics.update_repository_size } + + it 'updates the repository_size' do + repository_size = snippet_with_repo.repository.size.megabytes.to_i + + subject + + expect(statistics.repository_size).to eq repository_size + end + + context 'when the snippet does not have a repository' do + let(:statistics) { snippet_without_repo.statistics } + + it 'returns 0' do + expect(subject).to eq 0 + expect(statistics.repository_size).to eq 0 + end + end + end + + describe '#refresh!' do + subject { statistics.refresh! } + + it 'retrieves and saves statistic data from repository' do + expect(statistics).to receive(:update_commit_count) + expect(statistics).to receive(:update_file_count) + expect(statistics).to receive(:update_repository_size) + expect(statistics).to receive(:save!) + + subject + end + end end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 966d6f7b106..0a57c76ee75 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -407,6 +407,10 @@ describe 'project routing' do it 'to #destroy' do expect(delete('/gitlab/gitlabhq/snippets/1')).to route_to('projects/snippets#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end + + it 'to #show from scope routing' do + expect(get('/gitlab/gitlabhq/-/snippets/1')).to route_to('projects/snippets#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + end end # test_project_hook POST /:project_id/hooks/:id/test(.:format) hooks#test diff --git a/spec/services/alert_management/alerts/update_service_spec.rb b/spec/services/alert_management/alerts/update_service_spec.rb index e185e67c5cf..69a9287ce2a 100644 --- a/spec/services/alert_management/alerts/update_service_spec.rb +++ b/spec/services/alert_management/alerts/update_service_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' describe AlertManagement::Alerts::UpdateService do let_it_be(:user_with_permissions) { create(:user) } + let_it_be(:other_user_with_permissions) { create(:user) } let_it_be(:user_without_permissions) { create(:user) } let_it_be(:alert, reload: true) { create(:alert_management_alert) } let_it_be(:project) { alert.project } @@ -15,119 +16,131 @@ describe AlertManagement::Alerts::UpdateService do before_all do project.add_developer(user_with_permissions) + project.add_developer(other_user_with_permissions) end describe '#execute' do + shared_examples 'does not add a todo' do + specify { expect { response }.not_to change(Todo, :count) } + end + + shared_examples 'does not add a system note' do + specify { expect { response }.not_to change(Note, :count) } + end + + shared_examples 'error response' do |message| + it_behaves_like 'does not add a todo' + it_behaves_like 'does not add a system note' + + it 'has an informative message' do + expect(response).to be_error + expect(response.message).to eq(message) + end + end + subject(:response) { service.execute } context 'when the current_user is nil' do let(:current_user) { nil } - it 'results in an error' do - expect(response).to be_error - expect(response.message).to eq('You have no permissions') - end + it_behaves_like 'error response', 'You have no permissions' end - context 'when user does not have permission to update alerts' do + context 'when current_user does not have permission to update alerts' do let(:current_user) { user_without_permissions } - it 'results in an error' do - expect(response).to be_error - expect(response.message).to eq('You have no permissions') - end + it_behaves_like 'error response', 'You have no permissions' end context 'when no parameters are included' do - it 'results in an error' do - expect(response).to be_error - expect(response.message).to eq('Please provide attributes to update') - end + it_behaves_like 'error response', 'Please provide attributes to update' end - context 'when an error occures during update' do + context 'when an error occurs during update' do let(:params) { { title: nil } } - it 'results in an error' do - expect { response }.not_to change { alert.reload.notes.count } - expect(response).to be_error - expect(response.message).to eq("Title can't be blank") - end + it_behaves_like 'error response', "Title can't be blank" end context 'when a model attribute is included without assignees' do let(:params) { { title: 'This is an updated alert.' } } + it_behaves_like 'does not add a todo' + it_behaves_like 'does not add a system note' + it 'updates the attribute' do original_title = alert.title expect { response }.to change { alert.title }.from(original_title).to(params[:title]) expect(response).to be_success end - - it 'skips adding a todo' do - expect { response }.not_to change(Todo, :count) - end end context 'when assignees are included' do - let(:params) { { assignees: [user_with_permissions] } } + shared_examples 'adds a todo' do + let(:assignee) { expected_assignees.first } - after do - alert.assignees = [] + specify do + expect { response }.to change { assignee.reload.todos.count }.by(1) + expect(assignee.todos.last.author).to eq(current_user) + end end - it 'assigns the user' do - expect { response }.to change { alert.reload.assignees }.from([]).to(params[:assignees]) - expect(response).to be_success + shared_examples 'adds a system note' do + specify { expect { response }.to change { alert.reload.notes.count }.by(1) } end - it 'creates a system note for the assignment' do - expect { response }.to change { alert.reload.notes.count }.by(1) - end + shared_examples 'successful assignment' do + it_behaves_like 'adds a system note' + it_behaves_like 'adds a todo' - it 'adds a todo' do - expect { response }.to change { Todo.where(user: user_with_permissions).count }.by(1) - end - - context 'when current user is not the assignee' do - let(:assignee_user) { create(:user) } - let(:params) { { assignees: [assignee_user] } } - - it 'skips adding todo for assignee without permission to read alert' do - expect { response }.not_to change(Todo, :count) + after do + alert.assignees = [] end - context 'when assignee has read permission' do - before do - project.add_developer(assignee_user) - end + specify do + expect { response }.to change { alert.reload.assignees }.from([]).to(expected_assignees) + expect(response).to be_success + end + end - it 'adds a todo' do - response + let(:expected_assignees) { params[:assignees] } - expect(Todo.first.author).to eq(current_user) - end + context 'when the assignee is the current user' do + let(:params) { { assignees: [current_user] } } + + it_behaves_like 'successful assignment' + end + + context 'when the assignee has read permissions' do + let(:params) { { assignees: [other_user_with_permissions] } } + + it_behaves_like 'successful assignment' + end + + context 'when the assignee does not have read permissions' do + let(:params) { { assignees: [user_without_permissions] } } + + it_behaves_like 'error response', 'Assignee has no permissions' + end + + context 'when user is already assigned' do + let(:params) { { assignees: [user_with_permissions] } } + + before do + alert.assignees << user_with_permissions end - context 'when current_user is nil' do - let(:current_user) { nil } - - it 'skips adding todo if current_user is nil' do - project.add_developer(assignee_user) - - expect { response }.not_to change(Todo, :count) - end - end + it_behaves_like 'does not add a system note' + # TODO: We should not add another todo in this scenario + it_behaves_like 'adds a todo' end context 'with multiple users included' do let(:params) { { assignees: [user_with_permissions, user_without_permissions] } } + let(:expected_assignees) { [user_with_permissions] } - it 'assigns the first permissioned user' do - expect { response }.to change { alert.reload.assignees }.from([]).to([user_with_permissions]) - expect(response).to be_success - end + it_behaves_like 'successful assignment' end end end diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb index 73ac0bd7716..5b01a538d1f 100644 --- a/spec/services/members/destroy_service_spec.rb +++ b/spec/services/members/destroy_service_spec.rb @@ -56,12 +56,23 @@ describe Members::DestroyService do expect(member_user.todos_pending_count).to be(1) expect(member_user.todos_done_count).to be(1) - described_class.new(current_user).execute(member, opts) + service = described_class.new(current_user) + + if opts[:unassign_issuables] + expect(service).to receive(:enqueue_unassign_issuables).with(member) + end + + service.execute(member, opts) expect(member_user.assigned_open_merge_requests_count).to be(0) expect(member_user.assigned_open_issues_count).to be(0) expect(member_user.todos_pending_count).to be(0) expect(member_user.todos_done_count).to be(0) + + unless opts[:unassign_issuables] + expect(member_user.assigned_merge_requests.opened.count).to be(1) + expect(member_user.assigned_issues.opened.count).to be(1) + end end end @@ -100,7 +111,7 @@ describe Members::DestroyService do it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' it_behaves_like 'a service destroying a member with access' do - let(:opts) { { skip_authorization: true } } + let(:opts) { { skip_authorization: true, unassign_issuables: true } } end end @@ -114,7 +125,7 @@ describe Members::DestroyService do it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' it_behaves_like 'a service destroying a member with access' do - let(:opts) { { skip_authorization: true } } + let(:opts) { { skip_authorization: true, unassign_issuables: true } } end end end @@ -133,6 +144,12 @@ describe Members::DestroyService do end it_behaves_like 'a service destroying a member with access' + + context 'unassign issuables' do + it_behaves_like 'a service destroying a member with access' do + let(:opts) { { unassign_issuables: true } } + end + end end context 'with a group member' do @@ -143,6 +160,12 @@ describe Members::DestroyService do end it_behaves_like 'a service destroying a member with access' + + context 'unassign issuables' do + it_behaves_like 'a service destroying a member with access' do + let(:opts) { { unassign_issuables: true } } + end + end end end end diff --git a/spec/services/members/unassign_issuables_service_spec.rb b/spec/services/members/unassign_issuables_service_spec.rb new file mode 100644 index 00000000000..3f7ccb7bab3 --- /dev/null +++ b/spec/services/members/unassign_issuables_service_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Members::UnassignIssuablesService do + let_it_be(:group) { create(:group, :private) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:user, reload: true) { create(:user) } + let_it_be(:assigned_issue1, reload: true) { create(:issue, project: project, assignees: [user]) } + let_it_be(:assigned_issue2, reload: true) { create(:issue, project: project, assignees: [user]) } + + let!(:assigned_merge_request1) { create(:merge_request, :simple, :closed, target_project: project, source_project: project, assignees: [user], title: 'Test1') } + let!(:assigned_merge_request2) { create(:merge_request, :simple, :opened, target_project: project, source_project: project, assignees: [user], title: 'Test2') } + + describe '#execute' do + RSpec.shared_examples 'un-assigning issuables' do |issue_count, mr_count, open_issue_count, open_mr_count| + it 'removes issuable assignments', :aggregate_failures do + expect(user.assigned_issues.count).to eq(issue_count) + expect(user.assigned_merge_requests.count).to eq(mr_count) + + subject + + expect(user.assigned_issues.count).to eq(0) + expect(user.assigned_merge_requests.count).to eq(0) + end + + it 'invalidates user cache', :aggregate_failures, :clean_gitlab_redis_cache do + expect(user.assigned_open_merge_requests_count).to eq(open_mr_count) + expect(user.assigned_open_issues_count).to eq(open_issue_count) + + subject + + expect(user.assigned_open_merge_requests_count).to eq(0) + expect(user.assigned_open_issues_count).to eq(0) + end + end + + context 'when a user leaves a project' do + before do + project.add_maintainer(user) + end + + subject { described_class.new(user, project).execute } + + it_behaves_like 'un-assigning issuables', 2, 2, 2, 1 + end + + context 'when a user leaves a group' do + let_it_be(:project2) { create(:project, group: group) } + + let_it_be(:assigned_issue3, reload: true) { create(:issue, project: project2, assignees: [user]) } + let_it_be(:assigned_issue4, reload: true) { create(:issue, project: project2, assignees: [user]) } + + let!(:assigned_merge_request3) { create(:merge_request, :simple, :closed, target_project: project2, source_project: project2, assignees: [user], title: 'Test1') } + let!(:assigned_merge_request4) { create(:merge_request, :simple, :opened, target_project: project2, source_project: project2, assignees: [user], title: 'Test2') } + + before do + group.add_maintainer(user) + end + + subject { described_class.new(user, group).execute } + + it_behaves_like 'un-assigning issuables', 4, 4, 4, 2 + end + end +end diff --git a/spec/simplecov_env.rb b/spec/simplecov_env.rb index d618c83d78e..efd0c07b1b2 100644 --- a/spec/simplecov_env.rb +++ b/spec/simplecov_env.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'simplecov' +require 'simplecov-cobertura' require 'active_support/core_ext/numeric/time' require_relative '../lib/gitlab/utils' @@ -12,10 +13,19 @@ module SimpleCovEnv configure_profile configure_job + configure_formatter SimpleCov.start end + def configure_formatter + SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new([ + SimpleCov::Formatter::SimpleFormatter, + SimpleCov::Formatter::HTMLFormatter, + SimpleCov::Formatter::CoberturaFormatter + ]) + end + def configure_job SimpleCov.configure do if ENV['CI_JOB_NAME'] diff --git a/spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb b/spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb index bca51dab353..d21a9f419fd 100644 --- a/spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb @@ -16,6 +16,29 @@ RSpec.shared_examples 'resource_milestone_events API' do |parent_type, eventable expect(json_response.first['action']).to eq(event.action) end + context 'when there is an event with a milestone which is not visible for requesting user' do + let!(:private_project) { create(:project, :private) } + let!(:private_milestone) { create(:milestone, project: private_project) } + + let!(:other_user) { create(:user) } + + it 'returns the expected events' do + create_event(private_milestone) + + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events", other_user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq('1') + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + + expect(json_response.first['id']).to eq(event.id) + expect(json_response.first['milestone']['id']).to eq(event.milestone.id) + expect(json_response.first['action']).to eq(event.action) + end + end + it "returns a 404 error when eventable id not found" do get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{non_existing_record_id}/resource_milestone_events", user) @@ -60,6 +83,20 @@ RSpec.shared_examples 'resource_milestone_events API' do |parent_type, eventable end end + describe 'pagination' do + let!(:event1) { create_event(milestone) } + let!(:event2) { create_event(milestone) } + + # https://gitlab.com/gitlab-org/gitlab/-/issues/220192 + it 'returns the second page' do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events?page=2&per_page=1", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.count).to eq(1) + expect(json_response.first['id']).to eq(event2.id) + end + end + def create_event(milestone, action: :add) create(:resource_milestone_event, eventable.class.name.underscore => eventable, milestone: milestone, action: action) end diff --git a/spec/workers/members_destroyer/unassign_issuables_worker_spec.rb b/spec/workers/members_destroyer/unassign_issuables_worker_spec.rb new file mode 100644 index 00000000000..2a325be1225 --- /dev/null +++ b/spec/workers/members_destroyer/unassign_issuables_worker_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MembersDestroyer::UnassignIssuablesWorker do + let_it_be(:group) { create(:group, :private) } + let_it_be(:user, reload: true) { create(:user) } + + context 'when unsupported membership source entity' do + it 'exits early and logs error' do + params = { message: "SomeEntity is not a supported entity.", entity_type: 'SomeEntity', entity_id: group.id, user_id: user.id } + + expect(Sidekiq.logger).to receive(:error).with(params) + + described_class.new.perform(user.id, group.id, 'SomeEntity') + end + end + + it "calls the Members::UnassignIssuablesService with the params it was given" do + service = double + + expect(Members::UnassignIssuablesService).to receive(:new).with(user, group).and_return(service) + expect(service).to receive(:execute) + + described_class.new.perform(user.id, group.id, 'Group') + end +end diff --git a/yarn.lock b/yarn.lock index 6dc60a5a81f..fe31bbf292c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -843,10 +843,10 @@ eslint-plugin-vue "^6.2.1" vue-eslint-parser "^7.0.0" -"@gitlab/svgs@1.140.0": - version "1.140.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.140.0.tgz#593f1f65b0df57c3399fcfb9f472f59aa64da074" - integrity sha512-6gANJGi2QkpvOgFTMcY3SIwEqhO69i6R3jU4BSskkVziwDdAWxGonln22a4Iu//Iv0NrsFDpAA0jIVfnJzw0iA== +"@gitlab/svgs@1.141.0": + version "1.141.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.141.0.tgz#0d6c03511180669538be5c63a96b2ae28840bbf4" + integrity sha512-6k4HA0jVGMb/47bqcflSdpLGpo0rN2yd5K2X39LVQxukrg56PdZQvFPxT2UDOgChLstEtmN/iJTZuXqpeVOg+g== "@gitlab/ui@17.2.0": version "17.2.0"