Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-07-25 09:09:05 +00:00
parent 5b9a8005ea
commit 012ed4e4f6
52 changed files with 766 additions and 266 deletions

View File

@ -1 +1 @@
f8e688fbf64938cf8563f765c040af39f33e0790
4da75e5814680fe0d657bb734099527c74b76905

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.'

View File

@ -51,3 +51,5 @@ module Mutations
end
end
end
Mutations::WorkItems::Update.prepend_mod

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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))

View File

@ -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)

View File

@ -40,3 +40,5 @@ class WorkItem < Issue
Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter.track_work_item_created_action(author: author)
end
end
WorkItem.prepend_mod

View File

@ -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

View File

@ -1,9 +0,0 @@
# frozen_string_literal: true
module WorkItems
module Widgets
class Weight < Base
delegate :weight, to: :work_item
end
end
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
d9ce6e056d66e6c1fb9dc6ac6340cc74cf2572edefce1a2a2cefe0556ee5db41

View File

@ -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))
);

View File

@ -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.

View File

@ -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`

View File

@ -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 |

View File

@ -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}**).

View File

@ -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

View File

@ -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;

View File

@ -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)

View File

@ -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 ""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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) }

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.