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

View File

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

View File

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

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(: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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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?)] } .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

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

View File

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

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="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`

View File

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

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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