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
|
||||
push_frontend_feature_flag(:incident_timeline, @project)
|
||||
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)
|
||||
end
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
before_action only: :show do
|
||||
push_frontend_feature_flag(:issue_assignees_widget, 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)
|
||||
end
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
class Projects::WorkItemsController < Projects::ApplicationController
|
||||
before_action do
|
||||
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)
|
||||
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(: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_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(:work_items_hierarchy, @project)
|
||||
end
|
||||
|
|
|
@ -18,9 +18,6 @@ module Mutations
|
|||
argument :description_widget, ::Types::WorkItems::Widgets::DescriptionInputType,
|
||||
required: false,
|
||||
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,
|
||||
required: false,
|
||||
description: 'Input for hierarchy widget.'
|
||||
|
|
|
@ -51,3 +51,5 @@ module Mutations
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
Mutations::WorkItems::Update.prepend_mod
|
||||
|
|
|
@ -10,6 +10,16 @@ module Types
|
|||
field :type, ::Types::WorkItems::WidgetTypeEnum, null: true,
|
||||
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)
|
||||
case object
|
||||
when ::WorkItems::Widgets::Description
|
||||
|
@ -18,17 +28,14 @@ module Types
|
|||
::Types::WorkItems::Widgets::HierarchyType
|
||||
when ::WorkItems::Widgets::Assignees
|
||||
::Types::WorkItems::Widgets::AssigneesType
|
||||
when ::WorkItems::Widgets::Weight
|
||||
::Types::WorkItems::Widgets::WeightType
|
||||
else
|
||||
raise "Unknown GraphQL type for widget #{object}"
|
||||
end
|
||||
end
|
||||
|
||||
orphan_types ::Types::WorkItems::Widgets::DescriptionType,
|
||||
::Types::WorkItems::Widgets::HierarchyType,
|
||||
::Types::WorkItems::Widgets::AssigneesType,
|
||||
::Types::WorkItems::Widgets::WeightType
|
||||
orphan_types(*ORPHAN_TYPES)
|
||||
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)
|
||||
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?`
|
||||
# NOTE: We still want to keep this after removing `Namespace#feature_available?`.
|
||||
override :feature_available?
|
||||
|
|
|
@ -100,6 +100,8 @@ class Issue < ApplicationRecord
|
|||
validates :issue_type, presence: true
|
||||
validates :namespace, presence: true, if: -> { project.present? }
|
||||
|
||||
validate :due_date_after_start_date
|
||||
|
||||
enum issue_type: WorkItems::Type.base_types
|
||||
|
||||
alias_method :issuing_parent, :project
|
||||
|
@ -660,6 +662,14 @@ class Issue < ApplicationRecord
|
|||
|
||||
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
|
||||
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))
|
||||
|
|
|
@ -2983,6 +2983,10 @@ class Project < ApplicationRecord
|
|||
group&.work_items_feature_flag_enabled? || Feature.enabled?(:work_items, self)
|
||||
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
|
||||
return unless Gitlab.com?
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
WorkItem.prepend_mod
|
||||
|
|
|
@ -21,11 +21,11 @@ module WorkItems
|
|||
}.freeze
|
||||
|
||||
WIDGETS_FOR_TYPE = {
|
||||
issue: [Widgets::Assignees, Widgets::Description, Widgets::Hierarchy, Widgets::Weight],
|
||||
issue: [Widgets::Assignees, Widgets::Description, Widgets::Hierarchy],
|
||||
incident: [Widgets::Description, Widgets::Hierarchy],
|
||||
test_case: [Widgets::Description],
|
||||
requirement: [Widgets::Description],
|
||||
task: [Widgets::Assignees, Widgets::Description, Widgets::Hierarchy, Widgets::Weight]
|
||||
task: [Widgets::Assignees, Widgets::Description, Widgets::Hierarchy]
|
||||
}.freeze
|
||||
|
||||
cache_markdown_field :description, pipeline: :single_line
|
||||
|
@ -83,3 +83,5 @@ module WorkItems
|
|||
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
|
||||
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?(:update_issue) }.enable :update_work_item
|
||||
|
|
|
@ -11,6 +11,12 @@ module WorkItems
|
|||
@widget = widget
|
||||
@current_user = current_user
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def can_admin_work_item?
|
||||
can?(current_user, :admin_work_item, widget.work_item)
|
||||
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?)] }
|
||||
.avatar-holder
|
||||
= 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?
|
||||
.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,
|
||||
work_item_type_id bigint,
|
||||
namespace_id bigint,
|
||||
start_date date,
|
||||
CONSTRAINT check_fba63f706d CHECK ((lock_version IS NOT NULL))
|
||||
);
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ disk at:
|
|||
|
||||
- `/var/log/gitlab/gitlab-rails` for Omnibus GitLab installations.
|
||||
- `/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:
|
||||
|
||||
|
@ -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
|
||||
on [Gitaly servers](gitaly/index.md). To locate the repositories:
|
||||
|
||||
1. Go to the storage location for repositories. For Omnibus GitLab installations, repositories are
|
||||
stored by default in the `/var/opt/gitlab/git-data/repositories` directory.
|
||||
1. Go to the storage location for repositories:
|
||||
- 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)
|
||||
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="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="workitemupdatedtaskinputweightwidget"></a>`weightWidget` | [`WorkItemWidgetWeightInput`](#workitemwidgetweightinput) | Input for weight widget. |
|
||||
|
||||
### `WorkItemWidgetDescriptionInput`
|
||||
|
||||
|
|
|
@ -255,7 +255,7 @@ It also displays the following information:
|
|||
| 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. |
|
||||
| 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. |
|
||||
| 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:
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
| Event | Billable members | Maximum users |
|
||||
| Event | Billable users | Maximum users |
|
||||
|:---------------------------------------------------|:-----------------|:--------------|
|
||||
| Ten users occupy all 10 seats. | 10 | 10 |
|
||||
| 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 from 0, 1, 2, and so on.
|
||||
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
|
||||
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
|
||||
|
||||
module Gitlab
|
||||
# DEPRECATED. Use Gitlab::BackgroundTask for new code instead.
|
||||
class Daemon
|
||||
# Options:
|
||||
# - 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"
|
||||
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)
|
||||
|
||||
schemas_for_connection = Gitlab::Database.gitlab_schemas_for_connection(connection)
|
||||
|
|
|
@ -45899,6 +45899,9 @@ msgstr ""
|
|||
msgid "example.com"
|
||||
msgstr ""
|
||||
|
||||
msgid "exceeds maximum length (100 usernames)"
|
||||
msgstr ""
|
||||
|
||||
msgid "exceeds the %{max_value_length} character limit"
|
||||
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)
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
let!(:sub_group) { create(:group, parent: group) }
|
||||
let!(:sub_sub_group) { create(:group, parent: sub_group) }
|
||||
|
|
|
@ -69,7 +69,57 @@ RSpec.describe Issue do
|
|||
end
|
||||
|
||||
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
|
||||
let(:issue) { build(:issue, issue_type: issue_type) }
|
||||
|
|
|
@ -8239,58 +8239,42 @@ RSpec.describe Project, factory_default: :keep do
|
|||
end
|
||||
|
||||
describe '#work_items_feature_flag_enabled?' do
|
||||
shared_examples 'project checking work_items feature flag' do
|
||||
context 'when work_items FF is disabled globally' do
|
||||
before do
|
||||
stub_feature_flags(work_items: false)
|
||||
end
|
||||
let_it_be(:group_project) { create(:project, :in_subgroup) }
|
||||
|
||||
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
|
||||
|
||||
context 'when work_items FF is enabled for the project' do
|
||||
before do
|
||||
stub_feature_flags(work_items: project)
|
||||
end
|
||||
context 'when project belongs to a group' do
|
||||
let(:subject_project) { group_project }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
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 }
|
||||
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
|
||||
let_it_be(:project) { create(:project, namespace: namespace) }
|
||||
|
||||
it_behaves_like 'project checking work_items feature flag'
|
||||
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
|
||||
it_behaves_like 'checks parent group feature flag' do
|
||||
let(:feature_flag_method) { :work_items_mvc_2_feature_flag_enabled? }
|
||||
let(:feature_flag) { :work_items_mvc_2 }
|
||||
let(:subject_project) { group_project }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -40,10 +40,9 @@ RSpec.describe WorkItem do
|
|||
subject { build(:work_item).widgets }
|
||||
|
||||
it 'returns instances of supported widgets' do
|
||||
is_expected.to match_array([instance_of(WorkItems::Widgets::Description),
|
||||
instance_of(WorkItems::Widgets::Hierarchy),
|
||||
instance_of(WorkItems::Widgets::Assignees),
|
||||
instance_of(WorkItems::Widgets::Weight)])
|
||||
is_expected.to include(instance_of(WorkItems::Widgets::Description),
|
||||
instance_of(WorkItems::Widgets::Hierarchy),
|
||||
instance_of(WorkItems::Widgets::Assignees))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -64,10 +64,9 @@ RSpec.describe WorkItems::Type do
|
|||
subject { described_class.available_widgets }
|
||||
|
||||
it 'returns list of all possible widgets' do
|
||||
is_expected.to match_array([::WorkItems::Widgets::Description,
|
||||
::WorkItems::Widgets::Hierarchy,
|
||||
::WorkItems::Widgets::Assignees,
|
||||
::WorkItems::Widgets::Weight])
|
||||
is_expected.to include(::WorkItems::Widgets::Description,
|
||||
::WorkItems::Widgets::Hierarchy,
|
||||
::WorkItems::Widgets::Assignees)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -63,6 +63,27 @@ RSpec.describe WorkItemPolicy do
|
|||
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
|
||||
context 'when user is reporter' do
|
||||
let(:current_user) { reporter }
|
||||
|
|
|
@ -128,30 +128,6 @@ RSpec.describe 'Update a work item' do
|
|||
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
|
||||
let(:widgets_response) { mutation_response['workItem']['widgets'] }
|
||||
let(:fields) do
|
||||
|
|
|
@ -8,7 +8,7 @@ RSpec.describe 'Query.work_item(id)' do
|
|||
let_it_be(:developer) { create(:user) }
|
||||
let_it_be(:guest) { create(:user) }
|
||||
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_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) }
|
||||
|
@ -163,32 +163,6 @@ RSpec.describe 'Query.work_item(id)' do
|
|||
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
|
||||
let(:assignees) { create_list(:user, 2) }
|
||||
let(:work_item) { create(:work_item, project: project, assignees: assignees) }
|
||||
|
|
|
@ -4,6 +4,7 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Users::CreateService do
|
||||
describe '#execute' do
|
||||
let(:password) { User.random_password }
|
||||
let(:admin_user) { create(:admin) }
|
||||
|
||||
context 'with an admin user' do
|
||||
|
@ -12,7 +13,7 @@ RSpec.describe Users::CreateService do
|
|||
|
||||
context 'when required parameters are provided' do
|
||||
let(:params) do
|
||||
{ name: 'John Doe', username: 'jduser', email: email, password: 'mydummypass' }
|
||||
{ name: 'John Doe', username: 'jduser', email: email, password: password }
|
||||
end
|
||||
|
||||
it 'returns a persisted user' do
|
||||
|
@ -82,13 +83,13 @@ RSpec.describe Users::CreateService do
|
|||
|
||||
context 'when force_random_password parameter is true' 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
|
||||
|
||||
it 'generates random password' do
|
||||
user = service.execute
|
||||
|
||||
expect(user.password).not_to eq 'mydummypass'
|
||||
expect(user.password).not_to eq password
|
||||
expect(user.password).to be_present
|
||||
end
|
||||
end
|
||||
|
@ -99,7 +100,7 @@ RSpec.describe Users::CreateService do
|
|||
name: 'John Doe',
|
||||
username: 'jduser',
|
||||
email: 'jd@example.com',
|
||||
password: 'mydummypass',
|
||||
password: password,
|
||||
password_automatically_set: true
|
||||
}
|
||||
end
|
||||
|
@ -121,7 +122,7 @@ RSpec.describe Users::CreateService do
|
|||
|
||||
context 'when skip_confirmation parameter is true' 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
|
||||
|
||||
it 'confirms the user' do
|
||||
|
@ -131,7 +132,7 @@ RSpec.describe Users::CreateService do
|
|||
|
||||
context 'when reset_password parameter is true' 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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
let(:service) { described_class.new(nil, params) }
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Users::UpdateService do
|
||||
let(:password) { 'longsecret987!' }
|
||||
let(:password) { User.random_password }
|
||||
let(:user) { create(:user, password: password, password_confirmation: password) }
|
||||
|
||||
describe '#execute' do
|
||||
|
|
|
@ -84,8 +84,7 @@ RSpec.describe WorkItems::UpdateService do
|
|||
let(:widget_params) do
|
||||
{
|
||||
hierarchy_widget: { parent: parent },
|
||||
description_widget: { description: 'foo' },
|
||||
weight_widget: { weight: 1 }
|
||||
description_widget: { description: 'foo' }
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -104,7 +103,6 @@ RSpec.describe WorkItems::UpdateService do
|
|||
let(:supported_widgets) do
|
||||
[
|
||||
{ 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 } }
|
||||
]
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
before do
|
||||
run_rake_task('gitlab:db:lock_writes')
|
||||
|
|
Binary file not shown.
Loading…
Reference in New Issue