Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
5b9a8005ea
commit
012ed4e4f6
|
@ -1 +1 @@
|
||||||
f8e688fbf64938cf8563f765c040af39f33e0790
|
4da75e5814680fe0d657bb734099527c74b76905
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
- if src
|
||||||
|
= image_tag src,
|
||||||
|
srcset: srcset,
|
||||||
|
alt: alt,
|
||||||
|
class: avatar_classes,
|
||||||
|
height: @size,
|
||||||
|
width: @size,
|
||||||
|
loading: "lazy",
|
||||||
|
**@avatar_options
|
||||||
|
- else
|
||||||
|
%div{ @avatar_options, alt: alt, class: avatar_classes }
|
||||||
|
= initial
|
|
@ -0,0 +1,65 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Pajamas
|
||||||
|
class AvatarComponent < Pajamas::Component
|
||||||
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
|
# @param record [User, Project, Group]
|
||||||
|
# @param alt [String] text for the alt tag
|
||||||
|
# @param class [String] custom CSS class(es)
|
||||||
|
# @param size [Integer] size in pixel
|
||||||
|
# @param [Hash] avatar_options
|
||||||
|
def initialize(record, alt: nil, class: "", size: 64, avatar_options: {})
|
||||||
|
@record = record
|
||||||
|
@alt = alt
|
||||||
|
@class = binding.local_variable_get(:class)
|
||||||
|
@size = filter_attribute(size.to_i, SIZE_OPTIONS, default: 64)
|
||||||
|
@avatar_options = avatar_options
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
SIZE_OPTIONS = [16, 24, 32, 48, 64, 96].freeze
|
||||||
|
|
||||||
|
def avatar_classes
|
||||||
|
classes = ["gl-avatar", "gl-avatar-s#{@size}", @class]
|
||||||
|
classes.push("gl-avatar-circle") if @record.is_a?(User)
|
||||||
|
|
||||||
|
unless src
|
||||||
|
classes.push("gl-avatar-identicon")
|
||||||
|
classes.push("gl-avatar-identicon-bg#{((@record.id || 0) % 7) + 1}")
|
||||||
|
end
|
||||||
|
|
||||||
|
classes.join(' ')
|
||||||
|
end
|
||||||
|
|
||||||
|
def src
|
||||||
|
strong_memoize(:src) do
|
||||||
|
if @record.is_a?(User)
|
||||||
|
# Users show a gravatar instead of an identicon. Also avatars of
|
||||||
|
# blocked users are only shown if the current_user is an admin.
|
||||||
|
# To not duplicate this logic, we are using existing helpers here.
|
||||||
|
current_user = helpers.current_user rescue nil
|
||||||
|
helpers.avatar_icon_for_user(@record, @size, current_user: current_user)
|
||||||
|
elsif @record.try(:avatar_url)
|
||||||
|
"#{@record.avatar_url}?width=#{@size}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def srcset
|
||||||
|
return unless src
|
||||||
|
|
||||||
|
retina_src = src.gsub(/(?<=width=)#{@size}+/, (@size * 2).to_s)
|
||||||
|
"#{src} 1x, #{retina_src} 2x"
|
||||||
|
end
|
||||||
|
|
||||||
|
def alt
|
||||||
|
@alt || @record.name
|
||||||
|
end
|
||||||
|
|
||||||
|
def initial
|
||||||
|
@record.name[0, 1].upcase
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -9,7 +9,7 @@ class Projects::IncidentsController < Projects::ApplicationController
|
||||||
before_action do
|
before_action do
|
||||||
push_frontend_feature_flag(:incident_timeline, @project)
|
push_frontend_feature_flag(:incident_timeline, @project)
|
||||||
push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)
|
push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)
|
||||||
push_frontend_feature_flag(:work_items_mvc_2)
|
push_force_frontend_feature_flag(:work_items_mvc_2, @project&.work_items_mvc_2_feature_flag_enabled?)
|
||||||
push_frontend_feature_flag(:work_items_hierarchy, @project)
|
push_frontend_feature_flag(:work_items_hierarchy, @project)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
||||||
before_action only: :show do
|
before_action only: :show do
|
||||||
push_frontend_feature_flag(:issue_assignees_widget, project)
|
push_frontend_feature_flag(:issue_assignees_widget, project)
|
||||||
push_frontend_feature_flag(:realtime_labels, project)
|
push_frontend_feature_flag(:realtime_labels, project)
|
||||||
push_frontend_feature_flag(:work_items_mvc_2)
|
push_force_frontend_feature_flag(:work_items_mvc_2, project&.work_items_mvc_2_feature_flag_enabled?)
|
||||||
push_frontend_feature_flag(:work_items_hierarchy, project)
|
push_frontend_feature_flag(:work_items_hierarchy, project)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
class Projects::WorkItemsController < Projects::ApplicationController
|
class Projects::WorkItemsController < Projects::ApplicationController
|
||||||
before_action do
|
before_action do
|
||||||
push_force_frontend_feature_flag(:work_items, project&.work_items_feature_flag_enabled?)
|
push_force_frontend_feature_flag(:work_items, project&.work_items_feature_flag_enabled?)
|
||||||
push_frontend_feature_flag(:work_items_mvc_2)
|
push_force_frontend_feature_flag(:work_items_mvc_2, project&.work_items_mvc_2_feature_flag_enabled?)
|
||||||
push_frontend_feature_flag(:work_items_hierarchy, project)
|
push_frontend_feature_flag(:work_items_hierarchy, project)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ class ProjectsController < Projects::ApplicationController
|
||||||
push_licensed_feature(:file_locks) if @project.present? && @project.licensed_feature_available?(:file_locks)
|
push_licensed_feature(:file_locks) if @project.present? && @project.licensed_feature_available?(:file_locks)
|
||||||
push_licensed_feature(:security_orchestration_policies) if @project.present? && @project.licensed_feature_available?(:security_orchestration_policies)
|
push_licensed_feature(:security_orchestration_policies) if @project.present? && @project.licensed_feature_available?(:security_orchestration_policies)
|
||||||
push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)
|
push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)
|
||||||
push_frontend_feature_flag(:work_items_mvc_2)
|
push_force_frontend_feature_flag(:work_items_mvc_2, @project&.work_items_mvc_2_feature_flag_enabled?)
|
||||||
push_frontend_feature_flag(:package_registry_access_level)
|
push_frontend_feature_flag(:package_registry_access_level)
|
||||||
push_frontend_feature_flag(:work_items_hierarchy, @project)
|
push_frontend_feature_flag(:work_items_hierarchy, @project)
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,9 +18,6 @@ module Mutations
|
||||||
argument :description_widget, ::Types::WorkItems::Widgets::DescriptionInputType,
|
argument :description_widget, ::Types::WorkItems::Widgets::DescriptionInputType,
|
||||||
required: false,
|
required: false,
|
||||||
description: 'Input for description widget.'
|
description: 'Input for description widget.'
|
||||||
argument :weight_widget, ::Types::WorkItems::Widgets::WeightInputType,
|
|
||||||
required: false,
|
|
||||||
description: 'Input for weight widget.'
|
|
||||||
argument :hierarchy_widget, ::Types::WorkItems::Widgets::HierarchyUpdateInputType,
|
argument :hierarchy_widget, ::Types::WorkItems::Widgets::HierarchyUpdateInputType,
|
||||||
required: false,
|
required: false,
|
||||||
description: 'Input for hierarchy widget.'
|
description: 'Input for hierarchy widget.'
|
||||||
|
|
|
@ -51,3 +51,5 @@ module Mutations
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Mutations::WorkItems::Update.prepend_mod
|
||||||
|
|
|
@ -10,6 +10,16 @@ module Types
|
||||||
field :type, ::Types::WorkItems::WidgetTypeEnum, null: true,
|
field :type, ::Types::WorkItems::WidgetTypeEnum, null: true,
|
||||||
description: 'Widget type.'
|
description: 'Widget type.'
|
||||||
|
|
||||||
|
ORPHAN_TYPES = [
|
||||||
|
::Types::WorkItems::Widgets::DescriptionType,
|
||||||
|
::Types::WorkItems::Widgets::HierarchyType,
|
||||||
|
::Types::WorkItems::Widgets::AssigneesType
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
def self.ce_orphan_types
|
||||||
|
ORPHAN_TYPES
|
||||||
|
end
|
||||||
|
|
||||||
def self.resolve_type(object, context)
|
def self.resolve_type(object, context)
|
||||||
case object
|
case object
|
||||||
when ::WorkItems::Widgets::Description
|
when ::WorkItems::Widgets::Description
|
||||||
|
@ -18,17 +28,14 @@ module Types
|
||||||
::Types::WorkItems::Widgets::HierarchyType
|
::Types::WorkItems::Widgets::HierarchyType
|
||||||
when ::WorkItems::Widgets::Assignees
|
when ::WorkItems::Widgets::Assignees
|
||||||
::Types::WorkItems::Widgets::AssigneesType
|
::Types::WorkItems::Widgets::AssigneesType
|
||||||
when ::WorkItems::Widgets::Weight
|
|
||||||
::Types::WorkItems::Widgets::WeightType
|
|
||||||
else
|
else
|
||||||
raise "Unknown GraphQL type for widget #{object}"
|
raise "Unknown GraphQL type for widget #{object}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
orphan_types ::Types::WorkItems::Widgets::DescriptionType,
|
orphan_types(*ORPHAN_TYPES)
|
||||||
::Types::WorkItems::Widgets::HierarchyType,
|
|
||||||
::Types::WorkItems::Widgets::AssigneesType,
|
|
||||||
::Types::WorkItems::Widgets::WeightType
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Types::WorkItems::WidgetInterface.prepend_mod
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Types
|
|
||||||
module WorkItems
|
|
||||||
module Widgets
|
|
||||||
class WeightInputType < BaseInputObject
|
|
||||||
graphql_name 'WorkItemWidgetWeightInput'
|
|
||||||
|
|
||||||
argument :weight, GraphQL::Types::Int,
|
|
||||||
required: true,
|
|
||||||
description: 'Weight of the work item.'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,21 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Types
|
|
||||||
module WorkItems
|
|
||||||
module Widgets
|
|
||||||
# Disabling widget level authorization as it might be too granular
|
|
||||||
# and we already authorize the parent work item
|
|
||||||
# rubocop:disable Graphql/AuthorizeTypes
|
|
||||||
class WeightType < BaseObject
|
|
||||||
graphql_name 'WorkItemWidgetWeight'
|
|
||||||
description 'Represents a weight widget'
|
|
||||||
|
|
||||||
implements Types::WorkItems::WidgetInterface
|
|
||||||
|
|
||||||
field :weight, GraphQL::Types::Int, null: true,
|
|
||||||
description: 'Weight of the work item.'
|
|
||||||
end
|
|
||||||
# rubocop:enable Graphql/AuthorizeTypes
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -855,6 +855,10 @@ class Group < Namespace
|
||||||
feature_flag_enabled_for_self_or_ancestor?(:work_items)
|
feature_flag_enabled_for_self_or_ancestor?(:work_items)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def work_items_mvc_2_feature_flag_enabled?
|
||||||
|
feature_flag_enabled_for_self_or_ancestor?(:work_items_mvc_2)
|
||||||
|
end
|
||||||
|
|
||||||
# Check for enabled features, similar to `Project#feature_available?`
|
# Check for enabled features, similar to `Project#feature_available?`
|
||||||
# NOTE: We still want to keep this after removing `Namespace#feature_available?`.
|
# NOTE: We still want to keep this after removing `Namespace#feature_available?`.
|
||||||
override :feature_available?
|
override :feature_available?
|
||||||
|
|
|
@ -100,6 +100,8 @@ class Issue < ApplicationRecord
|
||||||
validates :issue_type, presence: true
|
validates :issue_type, presence: true
|
||||||
validates :namespace, presence: true, if: -> { project.present? }
|
validates :namespace, presence: true, if: -> { project.present? }
|
||||||
|
|
||||||
|
validate :due_date_after_start_date
|
||||||
|
|
||||||
enum issue_type: WorkItems::Type.base_types
|
enum issue_type: WorkItems::Type.base_types
|
||||||
|
|
||||||
alias_method :issuing_parent, :project
|
alias_method :issuing_parent, :project
|
||||||
|
@ -660,6 +662,14 @@ class Issue < ApplicationRecord
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def due_date_after_start_date
|
||||||
|
return unless start_date.present? && due_date.present?
|
||||||
|
|
||||||
|
if due_date < start_date
|
||||||
|
errors.add(:due_date, 'must be greater than or equal to start date')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
override :persist_pg_full_text_search_vector
|
override :persist_pg_full_text_search_vector
|
||||||
def persist_pg_full_text_search_vector(search_vector)
|
def persist_pg_full_text_search_vector(search_vector)
|
||||||
Issues::SearchData.upsert({ project_id: project_id, issue_id: id, search_vector: search_vector }, unique_by: %i(project_id issue_id))
|
Issues::SearchData.upsert({ project_id: project_id, issue_id: id, search_vector: search_vector }, unique_by: %i(project_id issue_id))
|
||||||
|
|
|
@ -2983,6 +2983,10 @@ class Project < ApplicationRecord
|
||||||
group&.work_items_feature_flag_enabled? || Feature.enabled?(:work_items, self)
|
group&.work_items_feature_flag_enabled? || Feature.enabled?(:work_items, self)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def work_items_mvc_2_feature_flag_enabled?
|
||||||
|
group&.work_items_mvc_2_feature_flag_enabled? || Feature.enabled?(:work_items_mvc_2)
|
||||||
|
end
|
||||||
|
|
||||||
def enqueue_record_project_target_platforms
|
def enqueue_record_project_target_platforms
|
||||||
return unless Gitlab.com?
|
return unless Gitlab.com?
|
||||||
return unless Feature.enabled?(:record_projects_target_platforms, self)
|
return unless Feature.enabled?(:record_projects_target_platforms, self)
|
||||||
|
|
|
@ -40,3 +40,5 @@ class WorkItem < Issue
|
||||||
Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter.track_work_item_created_action(author: author)
|
Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter.track_work_item_created_action(author: author)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
WorkItem.prepend_mod
|
||||||
|
|
|
@ -21,11 +21,11 @@ module WorkItems
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
WIDGETS_FOR_TYPE = {
|
WIDGETS_FOR_TYPE = {
|
||||||
issue: [Widgets::Assignees, Widgets::Description, Widgets::Hierarchy, Widgets::Weight],
|
issue: [Widgets::Assignees, Widgets::Description, Widgets::Hierarchy],
|
||||||
incident: [Widgets::Description, Widgets::Hierarchy],
|
incident: [Widgets::Description, Widgets::Hierarchy],
|
||||||
test_case: [Widgets::Description],
|
test_case: [Widgets::Description],
|
||||||
requirement: [Widgets::Description],
|
requirement: [Widgets::Description],
|
||||||
task: [Widgets::Assignees, Widgets::Description, Widgets::Hierarchy, Widgets::Weight]
|
task: [Widgets::Assignees, Widgets::Description, Widgets::Hierarchy]
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
cache_markdown_field :description, pipeline: :single_line
|
cache_markdown_field :description, pipeline: :single_line
|
||||||
|
@ -83,3 +83,5 @@ module WorkItems
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
WorkItems::Type.prepend_mod
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module WorkItems
|
|
||||||
module Widgets
|
|
||||||
class Weight < Base
|
|
||||||
delegate :weight, to: :work_item
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -3,6 +3,8 @@
|
||||||
class WorkItemPolicy < IssuePolicy
|
class WorkItemPolicy < IssuePolicy
|
||||||
condition(:is_member_and_author) { is_project_member? & is_author? }
|
condition(:is_member_and_author) { is_project_member? & is_author? }
|
||||||
|
|
||||||
|
rule { can?(:admin_issue) }.enable :admin_work_item
|
||||||
|
|
||||||
rule { can?(:destroy_issue) | is_member_and_author }.enable :delete_work_item
|
rule { can?(:destroy_issue) | is_member_and_author }.enable :delete_work_item
|
||||||
|
|
||||||
rule { can?(:update_issue) }.enable :update_work_item
|
rule { can?(:update_issue) }.enable :update_work_item
|
||||||
|
|
|
@ -11,6 +11,12 @@ module WorkItems
|
||||||
@widget = widget
|
@widget = widget
|
||||||
@current_user = current_user
|
@current_user = current_user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def can_admin_work_item?
|
||||||
|
can?(current_user, :admin_work_item, widget.work_item)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module WorkItems
|
|
||||||
module Widgets
|
|
||||||
module WeightService
|
|
||||||
class UpdateService < WorkItems::Widgets::BaseService
|
|
||||||
def update(params: {})
|
|
||||||
return unless params.present? && params[:weight]
|
|
||||||
|
|
||||||
widget.work_item.weight = params[:weight]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -51,7 +51,7 @@
|
||||||
.profile-header{ class: [('with-no-profile-tabs' if profile_tabs.empty?)] }
|
.profile-header{ class: [('with-no-profile-tabs' if profile_tabs.empty?)] }
|
||||||
.avatar-holder
|
.avatar-holder
|
||||||
= link_to avatar_icon_for_user(@user, 400, current_user: current_user), target: '_blank', rel: 'noopener noreferrer' do
|
= link_to avatar_icon_for_user(@user, 400, current_user: current_user), target: '_blank', rel: 'noopener noreferrer' do
|
||||||
= image_tag avatar_icon_for_user(@user, 90, current_user: current_user), class: "avatar s90", alt: '', itemprop: 'image'
|
= render Pajamas::AvatarComponent.new(@user, alt: "", size: 96, avatar_options: { itemprop: "image" })
|
||||||
|
|
||||||
- if @user.blocked? || !@user.confirmed?
|
- if @user.blocked? || !@user.confirmed?
|
||||||
.user-info
|
.user-info
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddStartDateToIssuesTable < Gitlab::Database::Migration[2.0]
|
||||||
|
enable_lock_retries!
|
||||||
|
|
||||||
|
def change
|
||||||
|
add_column :issues, :start_date, :date
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1 @@
|
||||||
|
d9ce6e056d66e6c1fb9dc6ac6340cc74cf2572edefce1a2a2cefe0556ee5db41
|
|
@ -16594,6 +16594,7 @@ CREATE TABLE issues (
|
||||||
upvotes_count integer DEFAULT 0 NOT NULL,
|
upvotes_count integer DEFAULT 0 NOT NULL,
|
||||||
work_item_type_id bigint,
|
work_item_type_id bigint,
|
||||||
namespace_id bigint,
|
namespace_id bigint,
|
||||||
|
start_date date,
|
||||||
CONSTRAINT check_fba63f706d CHECK ((lock_version IS NOT NULL))
|
CONSTRAINT check_fba63f706d CHECK ((lock_version IS NOT NULL))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,7 @@ disk at:
|
||||||
|
|
||||||
- `/var/log/gitlab/gitlab-rails` for Omnibus GitLab installations.
|
- `/var/log/gitlab/gitlab-rails` for Omnibus GitLab installations.
|
||||||
- `/home/git/gitlab/log` for installations from source.
|
- `/home/git/gitlab/log` for installations from source.
|
||||||
|
- `/var/log/gitlab` in the Sidekiq pod for GitLab Helm chart installations.
|
||||||
|
|
||||||
If periodic repository checks cause false alarms, you can clear all repository check states:
|
If periodic repository checks cause false alarms, you can clear all repository check states:
|
||||||
|
|
||||||
|
@ -65,8 +66,9 @@ If periodic repository checks cause false alarms, you can clear all repository c
|
||||||
You can run [`git fsck`](https://git-scm.com/docs/git-fsck) using the command line on repositories
|
You can run [`git fsck`](https://git-scm.com/docs/git-fsck) using the command line on repositories
|
||||||
on [Gitaly servers](gitaly/index.md). To locate the repositories:
|
on [Gitaly servers](gitaly/index.md). To locate the repositories:
|
||||||
|
|
||||||
1. Go to the storage location for repositories. For Omnibus GitLab installations, repositories are
|
1. Go to the storage location for repositories:
|
||||||
stored by default in the `/var/opt/gitlab/git-data/repositories` directory.
|
- For Omnibus GitLab installations, repositories are stored in the `/var/opt/gitlab/git-data/repositories` directory by default.
|
||||||
|
- For GitLab Helm chart installations, repositories are stored in the `/home/git/repositories` directory inside the Gitaly pod by default.
|
||||||
1. [Identify the subdirectory that contains the repository](repository_storage_types.md#from-project-name-to-hashed-path)
|
1. [Identify the subdirectory that contains the repository](repository_storage_types.md#from-project-name-to-hashed-path)
|
||||||
that you need to check.
|
that you need to check.
|
||||||
|
|
||||||
|
|
|
@ -22183,7 +22183,6 @@ A time-frame defined as a closed inclusive range of two dates.
|
||||||
| <a id="workitemupdatedtaskinputid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. |
|
| <a id="workitemupdatedtaskinputid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. |
|
||||||
| <a id="workitemupdatedtaskinputstateevent"></a>`stateEvent` | [`WorkItemStateEvent`](#workitemstateevent) | Close or reopen a work item. |
|
| <a id="workitemupdatedtaskinputstateevent"></a>`stateEvent` | [`WorkItemStateEvent`](#workitemstateevent) | Close or reopen a work item. |
|
||||||
| <a id="workitemupdatedtaskinputtitle"></a>`title` | [`String`](#string) | Title of the work item. |
|
| <a id="workitemupdatedtaskinputtitle"></a>`title` | [`String`](#string) | Title of the work item. |
|
||||||
| <a id="workitemupdatedtaskinputweightwidget"></a>`weightWidget` | [`WorkItemWidgetWeightInput`](#workitemwidgetweightinput) | Input for weight widget. |
|
|
||||||
|
|
||||||
### `WorkItemWidgetDescriptionInput`
|
### `WorkItemWidgetDescriptionInput`
|
||||||
|
|
||||||
|
|
|
@ -255,7 +255,7 @@ It also displays the following information:
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
|:-------------------|:------------|
|
|:-------------------|:------------|
|
||||||
| Users in License | The number of users you've paid for in the current license loaded on the system. The number does not change unless you [add seats](#add-seats-to-a-subscription) during your current subscription period. |
|
| Users in License | The number of users you've paid for in the current license loaded on the system. The number does not change unless you [add seats](#add-seats-to-a-subscription) during your current subscription period. |
|
||||||
| Billable users | The daily count of billable users on your system. The count may change as you block or add users to your instance. |
|
| Billable users | The daily count of billable users on your system. The count may change as you block, deactivate, or add users to your instance. |
|
||||||
| Maximum users | The highest number of billable users on your system during the term of the loaded license. |
|
| Maximum users | The highest number of billable users on your system during the term of the loaded license. |
|
||||||
| Users over license | Calculated as `Maximum users` - `Users in License` for the current license term. This number incurs a retroactive charge that must be paid before renewal. |
|
| Users over license | Calculated as `Maximum users` - `Users in License` for the current license term. This number incurs a retroactive charge that must be paid before renewal. |
|
||||||
|
|
||||||
|
@ -312,7 +312,7 @@ the contact person who manages your subscription.
|
||||||
|
|
||||||
It's important to regularly review your user accounts, because:
|
It's important to regularly review your user accounts, because:
|
||||||
|
|
||||||
- Stale user accounts that are not blocked count as billable users. You may pay more than you should
|
- Stale user accounts may count as billable users. You may pay more than you should
|
||||||
if you renew for too many users.
|
if you renew for too many users.
|
||||||
- Stale user accounts can be a security risk. A regular review helps reduce this risk.
|
- Stale user accounts can be a security risk. A regular review helps reduce this risk.
|
||||||
|
|
||||||
|
@ -329,7 +329,7 @@ To view the number of _users over license_ go to the **Admin Area**.
|
||||||
|
|
||||||
You purchase a license for 10 users.
|
You purchase a license for 10 users.
|
||||||
|
|
||||||
| Event | Billable members | Maximum users |
|
| Event | Billable users | Maximum users |
|
||||||
|:---------------------------------------------------|:-----------------|:--------------|
|
|:---------------------------------------------------|:-----------------|:--------------|
|
||||||
| Ten users occupy all 10 seats. | 10 | 10 |
|
| Ten users occupy all 10 seats. | 10 | 10 |
|
||||||
| Two new users join. | 12 | 12 |
|
| Two new users join. | 12 | 12 |
|
||||||
|
|
|
@ -17,6 +17,7 @@ You can set the weight of an issue during its creation, by changing the
|
||||||
value in the dropdown menu. You can set it to a non-negative integer
|
value in the dropdown menu. You can set it to a non-negative integer
|
||||||
value from 0, 1, 2, and so on.
|
value from 0, 1, 2, and so on.
|
||||||
You can remove weight from an issue as well.
|
You can remove weight from an issue as well.
|
||||||
|
A user with a Reporter role (or above) can set the weight.
|
||||||
|
|
||||||
This value appears on the right sidebar of an individual issue, as well as
|
This value appears on the right sidebar of an individual issue, as well as
|
||||||
in the issues page next to a weight icon (**{weight}**).
|
in the issues page next to a weight icon (**{weight}**).
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Gitlab
|
||||||
|
# Used to run small workloads concurrently to other threads in the current process.
|
||||||
|
# This may be necessary when accessing process state, which cannot be done via
|
||||||
|
# Sidekiq jobs.
|
||||||
|
#
|
||||||
|
# Since the given task is put on its own thread, use instances sparingly and only
|
||||||
|
# for fast computations since they will compete with other threads such as Puma
|
||||||
|
# or Sidekiq workers for CPU time and memory.
|
||||||
|
#
|
||||||
|
# Good examples:
|
||||||
|
# - Polling and updating process counters
|
||||||
|
# - Observing process or thread state
|
||||||
|
# - Enforcing process limits at the application level
|
||||||
|
#
|
||||||
|
# Bad examples:
|
||||||
|
# - Running database queries
|
||||||
|
# - Running CPU bound work loads
|
||||||
|
#
|
||||||
|
# As a guideline, aim to yield frequently if tasks execute logic in loops by
|
||||||
|
# making each iteration cheap. If life-cycle callbacks like start and stop
|
||||||
|
# aren't necessary and the task does not loop, consider just using Thread.new.
|
||||||
|
#
|
||||||
|
# rubocop: disable Gitlab/NamespacedClass
|
||||||
|
class BackgroundTask
|
||||||
|
AlreadyStartedError = Class.new(StandardError)
|
||||||
|
|
||||||
|
attr_reader :name
|
||||||
|
|
||||||
|
def running?
|
||||||
|
@state == :running
|
||||||
|
end
|
||||||
|
|
||||||
|
# Possible options:
|
||||||
|
# - name [String] used to identify the task in thread listings and logs (defaults to 'background_task')
|
||||||
|
# - synchronous [Boolean] if true, turns `start` into a blocking call
|
||||||
|
def initialize(task, **options)
|
||||||
|
@task = task
|
||||||
|
@synchronous = options[:synchronous]
|
||||||
|
@name = options[:name] || self.class.name.demodulize.underscore
|
||||||
|
# We use a monitor, not a Mutex, because monitors allow for re-entrant locking.
|
||||||
|
@mutex = ::Monitor.new
|
||||||
|
@state = :idle
|
||||||
|
end
|
||||||
|
|
||||||
|
def start
|
||||||
|
@mutex.synchronize do
|
||||||
|
raise AlreadyStartedError, "background task #{name} already running on #{@thread}" if running?
|
||||||
|
|
||||||
|
start_task = @task.respond_to?(:start) ? @task.start : true
|
||||||
|
|
||||||
|
if start_task
|
||||||
|
@state = :running
|
||||||
|
|
||||||
|
at_exit { stop }
|
||||||
|
|
||||||
|
@thread = Thread.new do
|
||||||
|
Thread.current.name = name
|
||||||
|
@task.call
|
||||||
|
end
|
||||||
|
|
||||||
|
@thread.join if @synchronous
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def stop
|
||||||
|
@mutex.synchronize do
|
||||||
|
break unless running?
|
||||||
|
|
||||||
|
if @thread
|
||||||
|
# If thread is not in a stopped state, interrupt it because it may be sleeping.
|
||||||
|
# This is so we process a stop signal ASAP.
|
||||||
|
@thread.wakeup if @thread.alive?
|
||||||
|
begin
|
||||||
|
# Propagate stop event if supported.
|
||||||
|
@task.stop if @task.respond_to?(:stop)
|
||||||
|
|
||||||
|
# join will rethrow any error raised on the background thread
|
||||||
|
@thread.join unless Thread.current == @thread
|
||||||
|
rescue Exception => ex # rubocop:disable Lint/RescueException
|
||||||
|
Gitlab::ErrorTracking.track_exception(ex, extra: { reported_by: name })
|
||||||
|
end
|
||||||
|
@thread = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
@state = :stopped
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# rubocop: enable Gitlab/NamespacedClass
|
||||||
|
end
|
|
@ -1,6 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Gitlab
|
module Gitlab
|
||||||
|
# DEPRECATED. Use Gitlab::BackgroundTask for new code instead.
|
||||||
class Daemon
|
class Daemon
|
||||||
# Options:
|
# Options:
|
||||||
# - recreate: We usually only allow a single instance per process to exist;
|
# - recreate: We usually only allow a single instance per process to exist;
|
||||||
|
|
|
@ -6,7 +6,7 @@ namespace :gitlab do
|
||||||
|
|
||||||
desc "GitLab | DB | Install prevent write triggers on all databases"
|
desc "GitLab | DB | Install prevent write triggers on all databases"
|
||||||
task lock_writes: [:environment, 'gitlab:db:validate_config'] do
|
task lock_writes: [:environment, 'gitlab:db:validate_config'] do
|
||||||
Gitlab::Database::EachDatabase.each_database_connection do |connection, database_name|
|
Gitlab::Database::EachDatabase.each_database_connection(include_shared: false) do |connection, database_name|
|
||||||
create_write_trigger_function(connection)
|
create_write_trigger_function(connection)
|
||||||
|
|
||||||
schemas_for_connection = Gitlab::Database.gitlab_schemas_for_connection(connection)
|
schemas_for_connection = Gitlab::Database.gitlab_schemas_for_connection(connection)
|
||||||
|
|
|
@ -45899,6 +45899,9 @@ msgstr ""
|
||||||
msgid "example.com"
|
msgid "example.com"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "exceeds maximum length (100 usernames)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "exceeds the %{max_value_length} character limit"
|
msgid "exceeds the %{max_value_length} character limit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
require "spec_helper"
|
||||||
|
|
||||||
|
RSpec.describe Pajamas::AvatarComponent, type: :component do
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
let_it_be(:project) { create(:project) }
|
||||||
|
let_it_be(:group) { create(:group) }
|
||||||
|
|
||||||
|
let(:options) { {} }
|
||||||
|
|
||||||
|
before do
|
||||||
|
render_inline(described_class.new(record, **options))
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "avatar shape" do
|
||||||
|
context "for a User" do
|
||||||
|
let(:record) { user }
|
||||||
|
|
||||||
|
it "has a circle shape" do
|
||||||
|
expect(page).to have_css ".gl-avatar.gl-avatar-circle"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "for a Project" do
|
||||||
|
let(:record) { project }
|
||||||
|
|
||||||
|
it "has default shape (rect)" do
|
||||||
|
expect(page).to have_css ".gl-avatar"
|
||||||
|
expect(page).not_to have_css ".gl-avatar-circle"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "for a Group" do
|
||||||
|
let(:record) { group }
|
||||||
|
|
||||||
|
it "has default shape (rect)" do
|
||||||
|
expect(page).to have_css ".gl-avatar"
|
||||||
|
expect(page).not_to have_css ".gl-avatar-circle"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "avatar image" do
|
||||||
|
context "when it has an uploaded image" do
|
||||||
|
let(:record) { project }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(record).to receive(:avatar_url).and_return "/example.png"
|
||||||
|
render_inline(described_class.new(record, **options))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "uses the avatar_url as image src" do
|
||||||
|
expect(page).to have_css "img.gl-avatar[src='/example.png?width=64']"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "uses a srcset for higher resolution on retina displays" do
|
||||||
|
expect(page).to have_css "img.gl-avatar[srcset='/example.png?width=64 1x, /example.png?width=128 2x']"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "uses lazy loading" do
|
||||||
|
expect(page).to have_css "img.gl-avatar[loading='lazy']"
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with size option" do
|
||||||
|
let(:options) { { size: 16 } }
|
||||||
|
|
||||||
|
it "uses that size as param for image src and srcset" do
|
||||||
|
expect(page).to have_css(
|
||||||
|
"img.gl-avatar[src='/example.png?width=16'][srcset='/example.png?width=16 1x, /example.png?width=32 2x']"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when a project or group has no uploaded image" do
|
||||||
|
let(:record) { project }
|
||||||
|
|
||||||
|
it "uses an identicon with the record's initial" do
|
||||||
|
expect(page).to have_css "div.gl-avatar.gl-avatar-identicon", text: record.name[0].upcase
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when the record has no id" do
|
||||||
|
let(:record) { build :group }
|
||||||
|
|
||||||
|
it "uses an identicon with default background color" do
|
||||||
|
expect(page).to have_css "div.gl-avatar.gl-avatar-identicon-bg1"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when a user has no uploaded image" do
|
||||||
|
let(:record) { user }
|
||||||
|
|
||||||
|
it "uses a gravatar" do
|
||||||
|
expect(rendered_component).to match /gravatar\.com/
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "options" do
|
||||||
|
let(:record) { user }
|
||||||
|
|
||||||
|
describe "alt" do
|
||||||
|
context "with a value" do
|
||||||
|
let(:options) { { alt: "Profile picture" } }
|
||||||
|
|
||||||
|
it "uses given value as alt text" do
|
||||||
|
expect(page).to have_css ".gl-avatar[alt='Profile picture']"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "without a value" do
|
||||||
|
it "uses the record's name as alt text" do
|
||||||
|
expect(page).to have_css ".gl-avatar[alt='#{record.name}']"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "class" do
|
||||||
|
let(:options) { { class: 'gl-m-4' } }
|
||||||
|
|
||||||
|
it 'has the correct custom class' do
|
||||||
|
expect(page).to have_css '.gl-avatar.gl-m-4'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "size" do
|
||||||
|
let(:options) { { size: 96 } }
|
||||||
|
|
||||||
|
it 'has the correct size class' do
|
||||||
|
expect(page).to have_css '.gl-avatar.gl-avatar-s96'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -17,7 +17,7 @@ RSpec.describe 'User uploads avatar to profile' do
|
||||||
|
|
||||||
visit user_path(user)
|
visit user_path(user)
|
||||||
|
|
||||||
expect(page).to have_selector(%Q(img[data-src$="/uploads/-/system/user/avatar/#{user.id}/dk.png?width=90"]))
|
expect(page).to have_selector(%Q(img[src$="/uploads/-/system/user/avatar/#{user.id}/dk.png?width=96"]))
|
||||||
|
|
||||||
# Cheating here to verify something that isn't user-facing, but is important
|
# Cheating here to verify something that isn't user-facing, but is important
|
||||||
expect(user.reload.avatar.file).to exist
|
expect(user.reload.avatar.file).to exist
|
||||||
|
|
|
@ -0,0 +1,209 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'fast_spec_helper'
|
||||||
|
|
||||||
|
# We need to capture task state from a closure, which requires instance variables.
|
||||||
|
# rubocop: disable RSpec/InstanceVariable
|
||||||
|
RSpec.describe Gitlab::BackgroundTask do
|
||||||
|
let(:options) { {} }
|
||||||
|
let(:task) do
|
||||||
|
proc do
|
||||||
|
@task_run = true
|
||||||
|
@task_thread = Thread.current
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
subject(:background_task) { described_class.new(task, **options) }
|
||||||
|
|
||||||
|
def expect_condition
|
||||||
|
Timeout.timeout(3) do
|
||||||
|
sleep 0.1 until yield
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when stopped' do
|
||||||
|
it 'is not running' do
|
||||||
|
expect(background_task).not_to be_running
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#start' do
|
||||||
|
it 'runs the given task on a background thread' do
|
||||||
|
test_thread = Thread.current
|
||||||
|
|
||||||
|
background_task.start
|
||||||
|
|
||||||
|
expect_condition { @task_run == true }
|
||||||
|
expect_condition { @task_thread != test_thread }
|
||||||
|
expect(background_task).to be_running
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns self' do
|
||||||
|
expect(background_task.start).to be(background_task)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when installing exit handler' do
|
||||||
|
it 'stops a running background task' do
|
||||||
|
expect(background_task).to receive(:at_exit).and_yield
|
||||||
|
|
||||||
|
background_task.start
|
||||||
|
|
||||||
|
expect(background_task).not_to be_running
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when task responds to start' do
|
||||||
|
let(:task_class) do
|
||||||
|
Struct.new(:started, :start_retval, :run) do
|
||||||
|
def start
|
||||||
|
self.started = true
|
||||||
|
self.start_retval
|
||||||
|
end
|
||||||
|
|
||||||
|
def call
|
||||||
|
self.run = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:task) { task_class.new }
|
||||||
|
|
||||||
|
it 'calls start' do
|
||||||
|
background_task.start
|
||||||
|
|
||||||
|
expect_condition { task.started == true }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when start returns true' do
|
||||||
|
it 'runs the task' do
|
||||||
|
task.start_retval = true
|
||||||
|
|
||||||
|
background_task.start
|
||||||
|
|
||||||
|
expect_condition { task.run == true }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when start returns false' do
|
||||||
|
it 'does not run the task' do
|
||||||
|
task.start_retval = false
|
||||||
|
|
||||||
|
background_task.start
|
||||||
|
|
||||||
|
expect_condition { task.run.nil? }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when synchronous is set to true' do
|
||||||
|
let(:options) { { synchronous: true } }
|
||||||
|
|
||||||
|
it 'calls join on the thread' do
|
||||||
|
# Thread has to be run in a block, expect_next_instance_of does not support this.
|
||||||
|
allow_any_instance_of(Thread).to receive(:join) # rubocop:disable RSpec/AnyInstanceOf
|
||||||
|
|
||||||
|
background_task.start
|
||||||
|
|
||||||
|
expect_condition { @task_run == true }
|
||||||
|
expect(@task_thread).to have_received(:join)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#stop' do
|
||||||
|
it 'is a no-op' do
|
||||||
|
expect { background_task.stop }.not_to change { subject.running? }
|
||||||
|
expect_condition { @task_run.nil? }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when running' do
|
||||||
|
before do
|
||||||
|
background_task.start
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#start' do
|
||||||
|
it 'raises an error' do
|
||||||
|
expect { background_task.start }.to raise_error(described_class::AlreadyStartedError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#stop' do
|
||||||
|
it 'stops running' do
|
||||||
|
expect { background_task.stop }.to change { subject.running? }.from(true).to(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when task responds to stop' do
|
||||||
|
let(:task_class) do
|
||||||
|
Struct.new(:stopped, :call) do
|
||||||
|
def stop
|
||||||
|
self.stopped = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:task) { task_class.new }
|
||||||
|
|
||||||
|
it 'calls stop' do
|
||||||
|
background_task.stop
|
||||||
|
|
||||||
|
expect_condition { task.stopped == true }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when task stop raises an error' do
|
||||||
|
let(:error) { RuntimeError.new('task error') }
|
||||||
|
let(:options) { { name: 'test_background_task' } }
|
||||||
|
|
||||||
|
let(:task_class) do
|
||||||
|
Struct.new(:call, :error, keyword_init: true) do
|
||||||
|
def stop
|
||||||
|
raise error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:task) { task_class.new(error: error) }
|
||||||
|
|
||||||
|
it 'stops gracefully' do
|
||||||
|
expect { background_task.stop }.not_to raise_error
|
||||||
|
expect(background_task).not_to be_running
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'reports the error' do
|
||||||
|
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
|
||||||
|
error, { extra: { reported_by: 'test_background_task' } }
|
||||||
|
)
|
||||||
|
|
||||||
|
background_task.stop
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when task run raises exception' do
|
||||||
|
let(:error) { RuntimeError.new('task error') }
|
||||||
|
let(:options) { { name: 'test_background_task' } }
|
||||||
|
let(:task) do
|
||||||
|
proc do
|
||||||
|
@task_run = true
|
||||||
|
raise error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'stops gracefully' do
|
||||||
|
expect_condition { @task_run == true }
|
||||||
|
expect { background_task.stop }.not_to raise_error
|
||||||
|
expect(background_task).not_to be_running
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'reports the error' do
|
||||||
|
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
|
||||||
|
error, { extra: { reported_by: 'test_background_task' } }
|
||||||
|
)
|
||||||
|
|
||||||
|
background_task.stop
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# rubocop: enable RSpec/InstanceVariable
|
|
@ -3383,6 +3383,13 @@ RSpec.describe Group do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#work_items_mvc_2_feature_flag_enabled?' do
|
||||||
|
it_behaves_like 'checks self and root ancestor feature flag' do
|
||||||
|
let(:feature_flag) { :work_items_mvc_2 }
|
||||||
|
let(:feature_flag_method) { :work_items_mvc_2_feature_flag_enabled? }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'group shares' do
|
describe 'group shares' do
|
||||||
let!(:sub_group) { create(:group, parent: group) }
|
let!(:sub_group) { create(:group, parent: group) }
|
||||||
let!(:sub_sub_group) { create(:group, parent: sub_group) }
|
let!(:sub_sub_group) { create(:group, parent: sub_group) }
|
||||||
|
|
|
@ -69,7 +69,57 @@ RSpec.describe Issue do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'validations' do
|
describe 'validations' do
|
||||||
subject { issue.valid? }
|
subject(:valid?) { issue.valid? }
|
||||||
|
|
||||||
|
describe 'due_date_after_start_date' do
|
||||||
|
let(:today) { Date.today }
|
||||||
|
|
||||||
|
context 'when both values are not present' do
|
||||||
|
let(:issue) { build(:issue) }
|
||||||
|
|
||||||
|
it { is_expected.to be_truthy }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when start date is present and due date is not' do
|
||||||
|
let(:issue) { build(:work_item, start_date: today) }
|
||||||
|
|
||||||
|
it { is_expected.to be_truthy }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when due date is present and start date is not' do
|
||||||
|
let(:issue) { build(:work_item, due_date: today) }
|
||||||
|
|
||||||
|
it { is_expected.to be_truthy }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when both date values are present' do
|
||||||
|
context 'when due date is greater than start date' do
|
||||||
|
let(:issue) { build(:work_item, start_date: today, due_date: 1.week.from_now) }
|
||||||
|
|
||||||
|
it { is_expected.to be_truthy }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when due date is equal to start date' do
|
||||||
|
let(:issue) { build(:work_item, start_date: today, due_date: today) }
|
||||||
|
|
||||||
|
it { is_expected.to be_truthy }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when due date is before start date' do
|
||||||
|
let(:issue) { build(:work_item, due_date: today, start_date: 1.week.from_now) }
|
||||||
|
|
||||||
|
it { is_expected.to be_falsey }
|
||||||
|
|
||||||
|
it 'adds an error message' do
|
||||||
|
valid?
|
||||||
|
|
||||||
|
expect(issue.errors.full_messages).to contain_exactly(
|
||||||
|
'Due date must be greater than or equal to start date'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'issue_type' do
|
describe 'issue_type' do
|
||||||
let(:issue) { build(:issue, issue_type: issue_type) }
|
let(:issue) { build(:issue, issue_type: issue_type) }
|
||||||
|
|
|
@ -8239,58 +8239,42 @@ RSpec.describe Project, factory_default: :keep do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#work_items_feature_flag_enabled?' do
|
describe '#work_items_feature_flag_enabled?' do
|
||||||
shared_examples 'project checking work_items feature flag' do
|
let_it_be(:group_project) { create(:project, :in_subgroup) }
|
||||||
context 'when work_items FF is disabled globally' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(work_items: false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it { is_expected.to be_falsey }
|
it_behaves_like 'checks parent group feature flag' do
|
||||||
|
let(:feature_flag_method) { :work_items_feature_flag_enabled? }
|
||||||
|
let(:feature_flag) { :work_items }
|
||||||
|
let(:subject_project) { group_project }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when feature flag is enabled for the project' do
|
||||||
|
subject { subject_project.work_items_feature_flag_enabled? }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_feature_flags(work_items: subject_project)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when work_items FF is enabled for the project' do
|
context 'when project belongs to a group' do
|
||||||
before do
|
let(:subject_project) { group_project }
|
||||||
stub_feature_flags(work_items: project)
|
|
||||||
end
|
|
||||||
|
|
||||||
it { is_expected.to be_truthy }
|
it { is_expected.to be_truthy }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when work_items FF is enabled globally' do
|
context 'when project does not belong to a group' do
|
||||||
|
let(:subject_project) { create(:project, namespace: create(:namespace)) }
|
||||||
|
|
||||||
it { is_expected.to be_truthy }
|
it { is_expected.to be_truthy }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
subject { project.work_items_feature_flag_enabled? }
|
describe '#work_items_mvc_2_feature_flag_enabled?' do
|
||||||
|
let_it_be(:group_project) { create(:project, :in_subgroup) }
|
||||||
|
|
||||||
context 'when a project does not belong to a group' do
|
it_behaves_like 'checks parent group feature flag' do
|
||||||
let_it_be(:project) { create(:project, namespace: namespace) }
|
let(:feature_flag_method) { :work_items_mvc_2_feature_flag_enabled? }
|
||||||
|
let(:feature_flag) { :work_items_mvc_2 }
|
||||||
it_behaves_like 'project checking work_items feature flag'
|
let(:subject_project) { group_project }
|
||||||
end
|
|
||||||
|
|
||||||
context 'when project belongs to a group' do
|
|
||||||
let_it_be(:root_group) { create(:group) }
|
|
||||||
let_it_be(:group) { create(:group, parent: root_group) }
|
|
||||||
let_it_be(:project) { create(:project, group: group) }
|
|
||||||
|
|
||||||
it_behaves_like 'project checking work_items feature flag'
|
|
||||||
|
|
||||||
context 'when work_items FF is enabled for the root group' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(work_items: root_group)
|
|
||||||
end
|
|
||||||
|
|
||||||
it { is_expected.to be_truthy }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when work_items FF is enabled for the group' do
|
|
||||||
before do
|
|
||||||
stub_feature_flags(work_items: group)
|
|
||||||
end
|
|
||||||
|
|
||||||
it { is_expected.to be_truthy }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -40,10 +40,9 @@ RSpec.describe WorkItem do
|
||||||
subject { build(:work_item).widgets }
|
subject { build(:work_item).widgets }
|
||||||
|
|
||||||
it 'returns instances of supported widgets' do
|
it 'returns instances of supported widgets' do
|
||||||
is_expected.to match_array([instance_of(WorkItems::Widgets::Description),
|
is_expected.to include(instance_of(WorkItems::Widgets::Description),
|
||||||
instance_of(WorkItems::Widgets::Hierarchy),
|
instance_of(WorkItems::Widgets::Hierarchy),
|
||||||
instance_of(WorkItems::Widgets::Assignees),
|
instance_of(WorkItems::Widgets::Assignees))
|
||||||
instance_of(WorkItems::Widgets::Weight)])
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -64,10 +64,9 @@ RSpec.describe WorkItems::Type do
|
||||||
subject { described_class.available_widgets }
|
subject { described_class.available_widgets }
|
||||||
|
|
||||||
it 'returns list of all possible widgets' do
|
it 'returns list of all possible widgets' do
|
||||||
is_expected.to match_array([::WorkItems::Widgets::Description,
|
is_expected.to include(::WorkItems::Widgets::Description,
|
||||||
::WorkItems::Widgets::Hierarchy,
|
::WorkItems::Widgets::Hierarchy,
|
||||||
::WorkItems::Widgets::Assignees,
|
::WorkItems::Widgets::Assignees)
|
||||||
::WorkItems::Widgets::Weight])
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,27 @@ RSpec.describe WorkItemPolicy do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'admin_work_item' do
|
||||||
|
context 'when user is reporter' do
|
||||||
|
let(:current_user) { reporter }
|
||||||
|
|
||||||
|
it { is_expected.to be_allowed(:admin_work_item) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is guest' do
|
||||||
|
let(:current_user) { guest }
|
||||||
|
|
||||||
|
it { is_expected.to be_disallowed(:admin_work_item) }
|
||||||
|
|
||||||
|
context 'when guest authored the work item' do
|
||||||
|
let(:work_item_subject) { authored_work_item }
|
||||||
|
let(:current_user) { guest_author }
|
||||||
|
|
||||||
|
it { is_expected.to be_disallowed(:admin_work_item) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'update_work_item' do
|
describe 'update_work_item' do
|
||||||
context 'when user is reporter' do
|
context 'when user is reporter' do
|
||||||
let(:current_user) { reporter }
|
let(:current_user) { reporter }
|
||||||
|
|
|
@ -128,30 +128,6 @@ RSpec.describe 'Update a work item' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with weight widget input' do
|
|
||||||
let(:fields) do
|
|
||||||
<<~FIELDS
|
|
||||||
workItem {
|
|
||||||
widgets {
|
|
||||||
type
|
|
||||||
... on WorkItemWidgetWeight {
|
|
||||||
weight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
errors
|
|
||||||
FIELDS
|
|
||||||
end
|
|
||||||
|
|
||||||
it_behaves_like 'update work item weight widget' do
|
|
||||||
let(:new_weight) { 2 }
|
|
||||||
|
|
||||||
let(:input) do
|
|
||||||
{ 'weightWidget' => { 'weight' => new_weight } }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with hierarchy widget input' do
|
context 'with hierarchy widget input' do
|
||||||
let(:widgets_response) { mutation_response['workItem']['widgets'] }
|
let(:widgets_response) { mutation_response['workItem']['widgets'] }
|
||||||
let(:fields) do
|
let(:fields) do
|
||||||
|
|
|
@ -8,7 +8,7 @@ RSpec.describe 'Query.work_item(id)' do
|
||||||
let_it_be(:developer) { create(:user) }
|
let_it_be(:developer) { create(:user) }
|
||||||
let_it_be(:guest) { create(:user) }
|
let_it_be(:guest) { create(:user) }
|
||||||
let_it_be(:project) { create(:project, :private) }
|
let_it_be(:project) { create(:project, :private) }
|
||||||
let_it_be(:work_item) { create(:work_item, project: project, description: '- List item', weight: 1) }
|
let_it_be(:work_item) { create(:work_item, project: project, description: '- List item') }
|
||||||
let_it_be(:child_item1) { create(:work_item, :task, project: project) }
|
let_it_be(:child_item1) { create(:work_item, :task, project: project) }
|
||||||
let_it_be(:child_item2) { create(:work_item, :task, confidential: true, project: project) }
|
let_it_be(:child_item2) { create(:work_item, :task, confidential: true, project: project) }
|
||||||
let_it_be(:child_link1) { create(:parent_link, work_item_parent: work_item, work_item: child_item1) }
|
let_it_be(:child_link1) { create(:parent_link, work_item_parent: work_item, work_item: child_item1) }
|
||||||
|
@ -163,32 +163,6 @@ RSpec.describe 'Query.work_item(id)' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'weight widget' do
|
|
||||||
let(:work_item_fields) do
|
|
||||||
<<~GRAPHQL
|
|
||||||
id
|
|
||||||
widgets {
|
|
||||||
type
|
|
||||||
... on WorkItemWidgetWeight {
|
|
||||||
weight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
GRAPHQL
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns widget information' do
|
|
||||||
expect(work_item_data).to include(
|
|
||||||
'id' => work_item.to_gid.to_s,
|
|
||||||
'widgets' => include(
|
|
||||||
hash_including(
|
|
||||||
'type' => 'WEIGHT',
|
|
||||||
'weight' => work_item.weight
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'assignees widget' do
|
describe 'assignees widget' do
|
||||||
let(:assignees) { create_list(:user, 2) }
|
let(:assignees) { create_list(:user, 2) }
|
||||||
let(:work_item) { create(:work_item, project: project, assignees: assignees) }
|
let(:work_item) { create(:work_item, project: project, assignees: assignees) }
|
||||||
|
|
|
@ -4,6 +4,7 @@ require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Users::CreateService do
|
RSpec.describe Users::CreateService do
|
||||||
describe '#execute' do
|
describe '#execute' do
|
||||||
|
let(:password) { User.random_password }
|
||||||
let(:admin_user) { create(:admin) }
|
let(:admin_user) { create(:admin) }
|
||||||
|
|
||||||
context 'with an admin user' do
|
context 'with an admin user' do
|
||||||
|
@ -12,7 +13,7 @@ RSpec.describe Users::CreateService do
|
||||||
|
|
||||||
context 'when required parameters are provided' do
|
context 'when required parameters are provided' do
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{ name: 'John Doe', username: 'jduser', email: email, password: 'mydummypass' }
|
{ name: 'John Doe', username: 'jduser', email: email, password: password }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns a persisted user' do
|
it 'returns a persisted user' do
|
||||||
|
@ -82,13 +83,13 @@ RSpec.describe Users::CreateService do
|
||||||
|
|
||||||
context 'when force_random_password parameter is true' do
|
context 'when force_random_password parameter is true' do
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{ name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', force_random_password: true }
|
{ name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: password, force_random_password: true }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'generates random password' do
|
it 'generates random password' do
|
||||||
user = service.execute
|
user = service.execute
|
||||||
|
|
||||||
expect(user.password).not_to eq 'mydummypass'
|
expect(user.password).not_to eq password
|
||||||
expect(user.password).to be_present
|
expect(user.password).to be_present
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -99,7 +100,7 @@ RSpec.describe Users::CreateService do
|
||||||
name: 'John Doe',
|
name: 'John Doe',
|
||||||
username: 'jduser',
|
username: 'jduser',
|
||||||
email: 'jd@example.com',
|
email: 'jd@example.com',
|
||||||
password: 'mydummypass',
|
password: password,
|
||||||
password_automatically_set: true
|
password_automatically_set: true
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
@ -121,7 +122,7 @@ RSpec.describe Users::CreateService do
|
||||||
|
|
||||||
context 'when skip_confirmation parameter is true' do
|
context 'when skip_confirmation parameter is true' do
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{ name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', skip_confirmation: true }
|
{ name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: password, skip_confirmation: true }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'confirms the user' do
|
it 'confirms the user' do
|
||||||
|
@ -131,7 +132,7 @@ RSpec.describe Users::CreateService do
|
||||||
|
|
||||||
context 'when reset_password parameter is true' do
|
context 'when reset_password parameter is true' do
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{ name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', reset_password: true }
|
{ name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: password, reset_password: true }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'resets password even if a password parameter is given' do
|
it 'resets password even if a password parameter is given' do
|
||||||
|
@ -152,7 +153,7 @@ RSpec.describe Users::CreateService do
|
||||||
|
|
||||||
context 'with nil user' do
|
context 'with nil user' do
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{ name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', skip_confirmation: true }
|
{ name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: password, skip_confirmation: true }
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:service) { described_class.new(nil, params) }
|
let(:service) { described_class.new(nil, params) }
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Users::UpdateService do
|
RSpec.describe Users::UpdateService do
|
||||||
let(:password) { 'longsecret987!' }
|
let(:password) { User.random_password }
|
||||||
let(:user) { create(:user, password: password, password_confirmation: password) }
|
let(:user) { create(:user, password: password, password_confirmation: password) }
|
||||||
|
|
||||||
describe '#execute' do
|
describe '#execute' do
|
||||||
|
|
|
@ -84,8 +84,7 @@ RSpec.describe WorkItems::UpdateService do
|
||||||
let(:widget_params) do
|
let(:widget_params) do
|
||||||
{
|
{
|
||||||
hierarchy_widget: { parent: parent },
|
hierarchy_widget: { parent: parent },
|
||||||
description_widget: { description: 'foo' },
|
description_widget: { description: 'foo' }
|
||||||
weight_widget: { weight: 1 }
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -104,7 +103,6 @@ RSpec.describe WorkItems::UpdateService do
|
||||||
let(:supported_widgets) do
|
let(:supported_widgets) do
|
||||||
[
|
[
|
||||||
{ klass: WorkItems::Widgets::DescriptionService::UpdateService, callback: :update, params: { description: 'foo' } },
|
{ klass: WorkItems::Widgets::DescriptionService::UpdateService, callback: :update, params: { description: 'foo' } },
|
||||||
{ klass: WorkItems::Widgets::WeightService::UpdateService, callback: :update, params: { weight: 1 } },
|
|
||||||
{ klass: WorkItems::Widgets::HierarchyService::UpdateService, callback: :before_update_in_transaction, params: { parent: parent } }
|
{ klass: WorkItems::Widgets::HierarchyService::UpdateService, callback: :before_update_in_transaction, params: { parent: parent } }
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'spec_helper'
|
|
||||||
|
|
||||||
RSpec.describe WorkItems::Widgets::WeightService::UpdateService do
|
|
||||||
let_it_be(:user) { create(:user) }
|
|
||||||
let_it_be(:project) { create(:project) }
|
|
||||||
let_it_be_with_reload(:work_item) { create(:work_item, project: project, weight: 1) }
|
|
||||||
|
|
||||||
let(:widget) { work_item.widgets.find {|widget| widget.is_a?(WorkItems::Widgets::Weight) } }
|
|
||||||
|
|
||||||
describe '#update' do
|
|
||||||
subject { described_class.new(widget: widget, current_user: user).update(params: params) } # rubocop:disable Rails/SaveBang
|
|
||||||
|
|
||||||
context 'when weight param is present' do
|
|
||||||
let(:params) { { weight: 2 } }
|
|
||||||
|
|
||||||
it 'correctly sets work item weight value' do
|
|
||||||
subject
|
|
||||||
|
|
||||||
expect(work_item.weight).to eq(2)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when weight param is not present' do
|
|
||||||
let(:params) { {} }
|
|
||||||
|
|
||||||
it 'does not change work item weight value', :aggregate_failures do
|
|
||||||
expect { subject }
|
|
||||||
.to not_change { work_item.weight }
|
|
||||||
|
|
||||||
expect(work_item.weight).to eq(1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,34 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
RSpec.shared_examples 'update work item weight widget' do
|
|
||||||
it 'updates the weight widget' do
|
|
||||||
expect do
|
|
||||||
post_graphql_mutation(mutation, current_user: current_user)
|
|
||||||
work_item.reload
|
|
||||||
end.to change(work_item, :weight).from(nil).to(new_weight)
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:success)
|
|
||||||
expect(mutation_response['workItem']['widgets']).to include(
|
|
||||||
{
|
|
||||||
'weight' => new_weight,
|
|
||||||
'type' => 'WEIGHT'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the updated work item is not valid' do
|
|
||||||
it 'returns validation errors without the work item' do
|
|
||||||
errors = ActiveModel::Errors.new(work_item).tap { |e| e.add(:weight, 'error message') }
|
|
||||||
|
|
||||||
allow_next_found_instance_of(::WorkItem) do |instance|
|
|
||||||
allow(instance).to receive(:valid?).and_return(false)
|
|
||||||
allow(instance).to receive(:errors).and_return(errors)
|
|
||||||
end
|
|
||||||
|
|
||||||
post_graphql_mutation(mutation, current_user: current_user)
|
|
||||||
|
|
||||||
expect(mutation_response['workItem']).to be_nil
|
|
||||||
expect(mutation_response['errors']).to match_array(['Weight error message'])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -25,3 +25,38 @@ RSpec.shared_examples 'returns true if project is inactive' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
RSpec.shared_examples 'checks parent group feature flag' do
|
||||||
|
let(:group) { subject_project.group }
|
||||||
|
let(:root_group) { group.parent }
|
||||||
|
|
||||||
|
subject { subject_project.public_send(feature_flag_method) }
|
||||||
|
|
||||||
|
context 'when feature flag is disabled globally' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(feature_flag => false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to be_falsey }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when feature flag is enabled globally' do
|
||||||
|
it { is_expected.to be_truthy }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when feature flag is enabled for the root group' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(feature_flag => root_group)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to be_truthy }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when feature flag is enabled for the group' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(feature_flag => group)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to be_truthy }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -133,6 +133,23 @@ RSpec.describe 'gitlab:db:lock_writes', :silence_stdout, :reestablished_active_r
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'multiple shared databases' do
|
||||||
|
before do
|
||||||
|
allow(::Gitlab::Database).to receive(:db_config_share_with).and_return(nil)
|
||||||
|
ci_db_config = Ci::ApplicationRecord.connection_db_config
|
||||||
|
allow(::Gitlab::Database).to receive(:db_config_share_with).with(ci_db_config).and_return('main')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not lock any tables if the ci database is shared with main database' do
|
||||||
|
run_rake_task('gitlab:db:lock_writes')
|
||||||
|
|
||||||
|
expect do
|
||||||
|
ApplicationRecord.connection.execute("delete from ci_builds")
|
||||||
|
Ci::ApplicationRecord.connection.execute("delete from users")
|
||||||
|
end.not_to raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when unlocking writes' do
|
context 'when unlocking writes' do
|
||||||
before do
|
before do
|
||||||
run_rake_task('gitlab:db:lock_writes')
|
run_rake_task('gitlab:db:lock_writes')
|
||||||
|
|
Binary file not shown.
Loading…
Reference in New Issue