diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 0c2ea8e4ad0..c5e5b0e121f 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -f8e688fbf64938cf8563f765c040af39f33e0790 +4da75e5814680fe0d657bb734099527c74b76905 diff --git a/app/components/pajamas/avatar_component.html.haml b/app/components/pajamas/avatar_component.html.haml new file mode 100644 index 00000000000..502f673fe2c --- /dev/null +++ b/app/components/pajamas/avatar_component.html.haml @@ -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 diff --git a/app/components/pajamas/avatar_component.rb b/app/components/pajamas/avatar_component.rb new file mode 100644 index 00000000000..09d4776557d --- /dev/null +++ b/app/components/pajamas/avatar_component.rb @@ -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 diff --git a/app/controllers/projects/incidents_controller.rb b/app/controllers/projects/incidents_controller.rb index f9fa8046962..36b52533e78 100644 --- a/app/controllers/projects/incidents_controller.rb +++ b/app/controllers/projects/incidents_controller.rb @@ -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 diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 48f883bcd4b..9cafd442913 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -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 diff --git a/app/controllers/projects/work_items_controller.rb b/app/controllers/projects/work_items_controller.rb index ba23af41bb0..b794785f285 100644 --- a/app/controllers/projects/work_items_controller.rb +++ b/app/controllers/projects/work_items_controller.rb @@ -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 diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 37e472050a0..4de46c9579e 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -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 diff --git a/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb b/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb index cbe1cfb4099..c50f1ce006e 100644 --- a/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb +++ b/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb @@ -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.' diff --git a/app/graphql/mutations/work_items/update.rb b/app/graphql/mutations/work_items/update.rb index 5d8c574877a..b4ed0a1a3ca 100644 --- a/app/graphql/mutations/work_items/update.rb +++ b/app/graphql/mutations/work_items/update.rb @@ -51,3 +51,5 @@ module Mutations end end end + +Mutations::WorkItems::Update.prepend_mod diff --git a/app/graphql/types/work_items/widget_interface.rb b/app/graphql/types/work_items/widget_interface.rb index 1b752393296..3f4758a6334 100644 --- a/app/graphql/types/work_items/widget_interface.rb +++ b/app/graphql/types/work_items/widget_interface.rb @@ -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 diff --git a/app/graphql/types/work_items/widgets/weight_input_type.rb b/app/graphql/types/work_items/widgets/weight_input_type.rb deleted file mode 100644 index a01c63222a5..00000000000 --- a/app/graphql/types/work_items/widgets/weight_input_type.rb +++ /dev/null @@ -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 diff --git a/app/graphql/types/work_items/widgets/weight_type.rb b/app/graphql/types/work_items/widgets/weight_type.rb deleted file mode 100644 index c8eaf560268..00000000000 --- a/app/graphql/types/work_items/widgets/weight_type.rb +++ /dev/null @@ -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 diff --git a/app/models/group.rb b/app/models/group.rb index 6d8f8bd7613..122fb48a34b 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -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? diff --git a/app/models/issue.rb b/app/models/issue.rb index cae42115bef..8a44d7c3c9b 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -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)) diff --git a/app/models/project.rb b/app/models/project.rb index 46e25564eab..c8d7afdd46f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -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) diff --git a/app/models/work_item.rb b/app/models/work_item.rb index d29df0c31fc..965851d431a 100644 --- a/app/models/work_item.rb +++ b/app/models/work_item.rb @@ -40,3 +40,5 @@ class WorkItem < Issue Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter.track_work_item_created_action(author: author) end end + +WorkItem.prepend_mod diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb index e38d0ae153a..7c4da00479c 100644 --- a/app/models/work_items/type.rb +++ b/app/models/work_items/type.rb @@ -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 diff --git a/app/models/work_items/widgets/weight.rb b/app/models/work_items/widgets/weight.rb deleted file mode 100644 index f589378f307..00000000000 --- a/app/models/work_items/widgets/weight.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -module WorkItems - module Widgets - class Weight < Base - delegate :weight, to: :work_item - end - end -end diff --git a/app/policies/work_item_policy.rb b/app/policies/work_item_policy.rb index 2f3561f1135..2fd84761a9b 100644 --- a/app/policies/work_item_policy.rb +++ b/app/policies/work_item_policy.rb @@ -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 diff --git a/app/services/work_items/widgets/base_service.rb b/app/services/work_items/widgets/base_service.rb index 037733bbed5..c695651361e 100644 --- a/app/services/work_items/widgets/base_service.rb +++ b/app/services/work_items/widgets/base_service.rb @@ -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 diff --git a/app/services/work_items/widgets/weight_service/update_service.rb b/app/services/work_items/widgets/weight_service/update_service.rb deleted file mode 100644 index cd62a25358f..00000000000 --- a/app/services/work_items/widgets/weight_service/update_service.rb +++ /dev/null @@ -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 diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 361beda4d02..e2f7a88569a 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -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 diff --git a/db/migrate/20220720210446_add_start_date_to_issues_table.rb b/db/migrate/20220720210446_add_start_date_to_issues_table.rb new file mode 100644 index 00000000000..315d80fa654 --- /dev/null +++ b/db/migrate/20220720210446_add_start_date_to_issues_table.rb @@ -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 diff --git a/db/schema_migrations/20220720210446 b/db/schema_migrations/20220720210446 new file mode 100644 index 00000000000..143d05d70f3 --- /dev/null +++ b/db/schema_migrations/20220720210446 @@ -0,0 +1 @@ +d9ce6e056d66e6c1fb9dc6ac6340cc74cf2572edefce1a2a2cefe0556ee5db41 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index fa24855931e..b0feeea8e00 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -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)) ); diff --git a/doc/administration/repository_checks.md b/doc/administration/repository_checks.md index 8cfefdb9b56..b83947b224b 100644 --- a/doc/administration/repository_checks.md +++ b/doc/administration/repository_checks.md @@ -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. diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index fa317490c56..72bfbe41518 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -22183,7 +22183,6 @@ A time-frame defined as a closed inclusive range of two dates. | `id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. | | `stateEvent` | [`WorkItemStateEvent`](#workitemstateevent) | Close or reopen a work item. | | `title` | [`String`](#string) | Title of the work item. | -| `weightWidget` | [`WorkItemWidgetWeightInput`](#workitemwidgetweightinput) | Input for weight widget. | ### `WorkItemWidgetDescriptionInput` diff --git a/doc/subscriptions/self_managed/index.md b/doc/subscriptions/self_managed/index.md index c1a11ce7d12..99001cf7451 100644 --- a/doc/subscriptions/self_managed/index.md +++ b/doc/subscriptions/self_managed/index.md @@ -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 | diff --git a/doc/user/project/issues/issue_weight.md b/doc/user/project/issues/issue_weight.md index 756fe9699f1..fcc53a239dc 100644 --- a/doc/user/project/issues/issue_weight.md +++ b/doc/user/project/issues/issue_weight.md @@ -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}**). diff --git a/lib/gitlab/background_task.rb b/lib/gitlab/background_task.rb new file mode 100644 index 00000000000..1f03e32844c --- /dev/null +++ b/lib/gitlab/background_task.rb @@ -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 diff --git a/lib/gitlab/daemon.rb b/lib/gitlab/daemon.rb index 04d13778499..49828e54d7e 100644 --- a/lib/gitlab/daemon.rb +++ b/lib/gitlab/daemon.rb @@ -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; diff --git a/lib/tasks/gitlab/db/lock_writes.rake b/lib/tasks/gitlab/db/lock_writes.rake index 3a083036781..fad949eb718 100644 --- a/lib/tasks/gitlab/db/lock_writes.rake +++ b/lib/tasks/gitlab/db/lock_writes.rake @@ -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) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1b1cd638336..4a906868b5a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/spec/components/pajamas/avatar_component_spec.rb b/spec/components/pajamas/avatar_component_spec.rb new file mode 100644 index 00000000000..3b4e4e49fc2 --- /dev/null +++ b/spec/components/pajamas/avatar_component_spec.rb @@ -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 diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb index 900cd72c17f..cbd2d30d726 100644 --- a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb +++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb @@ -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 diff --git a/spec/lib/gitlab/background_task_spec.rb b/spec/lib/gitlab/background_task_spec.rb new file mode 100644 index 00000000000..102556b6b2f --- /dev/null +++ b/spec/lib/gitlab/background_task_spec.rb @@ -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 diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index e8e805b2678..4531ca713f0 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -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) } diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 89c440dc49c..946d26ef367 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -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) } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 2171ee752fd..cef61fbb0a2 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -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 diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb index f33c8e0a186..777ade511b0 100644 --- a/spec/models/work_item_spec.rb +++ b/spec/models/work_item_spec.rb @@ -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 diff --git a/spec/models/work_items/type_spec.rb b/spec/models/work_items/type_spec.rb index e91617effc0..057bf045f60 100644 --- a/spec/models/work_items/type_spec.rb +++ b/spec/models/work_items/type_spec.rb @@ -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 diff --git a/spec/policies/work_item_policy_spec.rb b/spec/policies/work_item_policy_spec.rb index f8ec7d9f9bc..f3e8bd6a08b 100644 --- a/spec/policies/work_item_policy_spec.rb +++ b/spec/policies/work_item_policy_spec.rb @@ -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 } diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb index 77f7b9bacef..46fd3c2b6c2 100644 --- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb @@ -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 diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb index f17d2ebbb7e..a4e6a3a9791 100644 --- a/spec/requests/api/graphql/work_item_spec.rb +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -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) } diff --git a/spec/services/users/create_service_spec.rb b/spec/services/users/create_service_spec.rb index 74340bac055..f3c9701c556 100644 --- a/spec/services/users/create_service_spec.rb +++ b/spec/services/users/create_service_spec.rb @@ -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) } diff --git a/spec/services/users/update_service_spec.rb b/spec/services/users/update_service_spec.rb index 52c7b54ed72..411cd7316d8 100644 --- a/spec/services/users/update_service_spec.rb +++ b/spec/services/users/update_service_spec.rb @@ -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 diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb index 5fee5926823..9db4139c6d5 100644 --- a/spec/services/work_items/update_service_spec.rb +++ b/spec/services/work_items/update_service_spec.rb @@ -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 diff --git a/spec/services/work_items/widgets/weight_service/update_service_spec.rb b/spec/services/work_items/widgets/weight_service/update_service_spec.rb deleted file mode 100644 index 97e17f1c526..00000000000 --- a/spec/services/work_items/widgets/weight_service/update_service_spec.rb +++ /dev/null @@ -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 diff --git a/spec/support/shared_examples/graphql/mutations/work_items/update_weight_widget_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/work_items/update_weight_widget_shared_examples.rb deleted file mode 100644 index 3c32b7e0310..00000000000 --- a/spec/support/shared_examples/graphql/mutations/work_items/update_weight_widget_shared_examples.rb +++ /dev/null @@ -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 diff --git a/spec/support/shared_examples/models/project_shared_examples.rb b/spec/support/shared_examples/models/project_shared_examples.rb index 475ac1da04b..0b880f00a22 100644 --- a/spec/support/shared_examples/models/project_shared_examples.rb +++ b/spec/support/shared_examples/models/project_shared_examples.rb @@ -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 diff --git a/spec/tasks/gitlab/db/lock_writes_rake_spec.rb b/spec/tasks/gitlab/db/lock_writes_rake_spec.rb index 034c520887e..341e1a3f56c 100644 --- a/spec/tasks/gitlab/db/lock_writes_rake_spec.rb +++ b/spec/tasks/gitlab/db/lock_writes_rake_spec.rb @@ -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') diff --git a/vendor/project_templates/hugo.tar.gz b/vendor/project_templates/hugo.tar.gz index 1f756a696e3..3d037bbf1df 100644 Binary files a/vendor/project_templates/hugo.tar.gz and b/vendor/project_templates/hugo.tar.gz differ