Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
b7e512c897
commit
5eeb391043
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 #
|
||||
#######################################################
|
||||
|
||||
|
|
|
|||
1
Gemfile
1
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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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] {
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Suppress progress on pulling on Performance Test
|
||||
merge_request: 34368
|
||||
author: Takuya Noguchi
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Added CI template for Dart
|
||||
merge_request: 32942
|
||||
author: agilob
|
||||
type: added
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Copy project snippet routes under - scope
|
||||
merge_request: 35022
|
||||
author:
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix pagination for resource milestone events api
|
||||
merge_request: 33845
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add snippet statistics logic
|
||||
merge_request: 35118
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Limit alert assignment to only users who can read alerts
|
||||
merge_request: 34681
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix alignment of navigation theme options
|
||||
merge_request: 35041
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -262,6 +262,8 @@
|
|||
- 1
|
||||
- - todos_destroyer
|
||||
- 1
|
||||
- - unassign_issuables
|
||||
- 1
|
||||
- - update_external_pull_requests
|
||||
- 3
|
||||
- - update_highest_role
|
||||
|
|
|
|||
|
|
@ -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: <your_access_token>" "https://gitlab.example.com/api/v4/groups/:id/members/:user_id"
|
||||
|
|
|
|||
|
|
@ -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: <your_access_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: <your_access_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
|
||||
}
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
require_relative '../spec/simplecov_env'
|
||||
SimpleCovEnv.configure_profile
|
||||
SimpleCovEnv.configure_formatter
|
||||
|
||||
module SimpleCov
|
||||
module ResultMerger
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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') }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue