Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-10-26 21:09:20 +00:00
parent 51ba1dfa3b
commit f825fd1d88
64 changed files with 880 additions and 119 deletions

View File

@ -503,7 +503,7 @@ gem 'ssh_data', '~> 1.3'
gem 'spamcheck', '~> 1.0.0'
# Gitaly GRPC protocol definitions
gem 'gitaly', '~> 15.4.0-rc2'
gem 'gitaly', '~> 15.5.0'
# KAS GRPC protocol definitions
gem 'kas-grpc', '~> 0.0.2'

View File

@ -198,7 +198,7 @@
{"name":"gettext_i18n_rails","version":"1.8.0","platform":"ruby","checksum":"95e5cf8440b1e08705b27f2bccb56143272c5a7a0dabcf54ea1bd701140a496f"},
{"name":"gettext_i18n_rails_js","version":"1.3.0","platform":"ruby","checksum":"5d10afe4be3639bff78c50a56768c20f39aecdabc580c08aa45573911c2bd687"},
{"name":"git","version":"1.11.0","platform":"ruby","checksum":"7e95ba4da8298a0373ef1a6862aa22007d761f3c8274b675aa787966fecea0f1"},
{"name":"gitaly","version":"15.4.0.pre.rc2","platform":"ruby","checksum":"48764528a730605a46f00cf86c7cfcb92d25f4f3d8cb9e09557baac3e9f3f8e3"},
{"name":"gitaly","version":"15.5.0","platform":"ruby","checksum":"d85dd4890a1f0fd95f935c848bcedf03f19b78872f20f04b9811e602bea4ef42"},
{"name":"github-markup","version":"1.7.0","platform":"ruby","checksum":"97eb27c70662d9cc1d5997cd6c99832026fae5d4913b5dce1ce6c9f65078e69d"},
{"name":"gitlab","version":"4.16.1","platform":"ruby","checksum":"13fd7059cbdad5a1a21b15fa2cf9070b97d92e27f8c688581fe3d84dc038074f"},
{"name":"gitlab-chronic","version":"0.10.5","platform":"ruby","checksum":"f80f18dc699b708870a80685243331290bc10cfeedb6b99c92219722f729c875"},

View File

@ -547,7 +547,7 @@ GEM
rails (>= 3.2.0)
git (1.11.0)
rchardet (~> 1.8)
gitaly (15.4.0.pre.rc2)
gitaly (15.5.0)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab (4.16.1)
@ -1626,7 +1626,7 @@ DEPENDENCIES
gettext (~> 3.3)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly (~> 15.4.0.pre.rc2)
gitaly (~> 15.5.0)
github-markup (~> 1.7.0)
gitlab-chronic (~> 0.10.5)
gitlab-dangerfiles (~> 3.6.1)

View File

@ -143,8 +143,9 @@
"WorkItemWidgetHierarchy",
"WorkItemWidgetIteration",
"WorkItemWidgetLabels",
"WorkItemWidgetMilestone",
"WorkItemWidgetStartAndDueDate",
"WorkItemWidgetStatus",
"WorkItemWidgetWeight"
]
}
}

View File

@ -44,6 +44,7 @@ export default {
'signupEnabled',
'requireAdminApprovalAfterUserSignup',
'sendUserConfirmationEmail',
'emailConfirmationSetting',
'minimumPasswordLength',
'minimumPasswordLengthMin',
'minimumPasswordLengthMax',
@ -66,6 +67,7 @@ export default {
signupEnabled: this.signupEnabled,
requireAdminApproval: this.requireAdminApprovalAfterUserSignup,
sendConfirmationEmail: this.sendUserConfirmationEmail,
emailConfirmationSetting: this.emailConfirmationSetting,
minimumPasswordLength: this.minimumPasswordLength,
minimumPasswordLengthMin: this.minimumPasswordLengthMin,
minimumPasswordLengthMax: this.minimumPasswordLengthMax,
@ -199,6 +201,15 @@ export default {
signupEnabledLabel: s__('ApplicationSettings|Sign-up enabled'),
requireAdminApprovalLabel: s__('ApplicationSettings|Require admin approval for new sign-ups'),
sendConfirmationEmailLabel: s__('ApplicationSettings|Send confirmation email on sign-up'),
emailConfirmationSettingsLabel: s__('ApplicationSettings|Email confirmation settings'),
emailConfirmationSettingsOffLabel: s__('ApplicationSettings|Off'),
emailConfirmationSettingsOffHelpText: s__(
'ApplicationSettings|New users can sign up without confirming their email address.',
),
emailConfirmationSettingsHardLabel: s__('ApplicationSettings|Hard'),
emailConfirmationSettingsHardHelpText: s__(
'ApplicationSettings|Send a confirmation email during sign up. New users must confirm their email address before they can log in.',
),
minimumPasswordLengthLabel: s__(
'ApplicationSettings|Minimum password length (number of characters)',
),
@ -276,6 +287,24 @@ export default {
:label="$options.i18n.sendConfirmationEmailLabel"
/>
<gl-form-group :label="$options.i18n.emailConfirmationSettingsLabel">
<gl-form-radio-group
v-model="form.emailConfirmationSetting"
name="application_setting[email_confirmation_setting]"
>
<gl-form-radio value="hard">
{{ $options.i18n.emailConfirmationSettingsHardLabel }}
<template #help> {{ $options.i18n.emailConfirmationSettingsHardHelpText }} </template>
</gl-form-radio>
<gl-form-radio value="off">
{{ $options.i18n.emailConfirmationSettingsOffLabel }}
<template #help> {{ $options.i18n.emailConfirmationSettingsOffHelpText }} </template>
</gl-form-radio>
</gl-form-radio-group>
</gl-form-group>
<gl-form-group
:label="$options.i18n.userCapLabel"
:description="$options.i18n.userCapDescription"

View File

@ -440,7 +440,7 @@ class ProjectsController < Projects::ApplicationController
def operations_feature_attributes
if Feature.enabled?(:split_operations_visibility_permissions, project)
%i[
environments_access_level feature_flags_access_level monitor_access_level
environments_access_level feature_flags_access_level monitor_access_level infrastructure_access_level
]
else
%i[operations_access_level]

View File

@ -33,6 +33,9 @@ module Mutations
argument :labels_widget, ::Types::WorkItems::Widgets::LabelsUpdateInputType,
required: false,
description: 'Input for labels widget.'
argument :milestone_widget, ::Types::WorkItems::Widgets::MilestoneInputType,
required: false,
description: 'Input for milestone widget.'
end
end
end

View File

@ -22,6 +22,9 @@ module Mutations
argument :hierarchy_widget, ::Types::WorkItems::Widgets::HierarchyCreateInputType,
required: false,
description: 'Input for hierarchy widget.'
argument :milestone_widget, ::Types::WorkItems::Widgets::MilestoneInputType,
required: false,
description: 'Input for milestone widget.'
argument :project_path, GraphQL::Types::ID,
required: true,
description: 'Full path of the project the work item is associated with.'

View File

@ -55,7 +55,8 @@ module Resolvers
last_edited_by: :last_edited_by,
assignees: :assignees,
parent: :work_item_parent,
labels: :labels
labels: :labels,
milestone: :milestone
}
end

View File

@ -16,7 +16,8 @@ module Types
::Types::WorkItems::Widgets::HierarchyType,
::Types::WorkItems::Widgets::LabelsType,
::Types::WorkItems::Widgets::AssigneesType,
::Types::WorkItems::Widgets::StartAndDueDateType
::Types::WorkItems::Widgets::StartAndDueDateType,
::Types::WorkItems::Widgets::MilestoneType
].freeze
def self.ce_orphan_types
@ -38,6 +39,8 @@ module Types
::Types::WorkItems::Widgets::LabelsType
when ::WorkItems::Widgets::StartAndDueDate
::Types::WorkItems::Widgets::StartAndDueDateType
when ::WorkItems::Widgets::Milestone
::Types::WorkItems::Widgets::MilestoneType
else
raise "Unknown GraphQL type for widget #{object}"
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module Types
module WorkItems
module Widgets
class MilestoneInputType < BaseInputObject
graphql_name 'WorkItemWidgetMilestoneInput'
argument :milestone_id,
Types::GlobalIDType[::Milestone],
required: :nullable,
prepare: ->(id, _) { id.model_id unless id.nil? },
description: 'Milestone to assign to the work item.'
end
end
end
end

View File

@ -0,0 +1,23 @@
# 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 MilestoneType < BaseObject
graphql_name 'WorkItemWidgetMilestone'
description 'Represents a milestone widget'
implements Types::WorkItems::WidgetInterface
field :milestone,
::Types::MilestoneType,
null: true,
description: 'Milestone of the work item.'
end
# rubocop:enable Graphql/AuthorizeTypes
end
end
end

View File

@ -241,6 +241,7 @@ module ApplicationSettingsHelper
:eks_access_key_id,
:eks_secret_access_key,
:email_author_in_body,
:email_confirmation_setting,
:enabled_git_access_protocol,
:enforce_terms,
:error_tracking_enabled,
@ -544,6 +545,7 @@ module ApplicationSettingsHelper
signup_enabled: @application_setting[:signup_enabled].to_s,
require_admin_approval_after_user_signup: @application_setting[:require_admin_approval_after_user_signup].to_s,
send_user_confirmation_email: @application_setting[:send_user_confirmation_email].to_s,
email_confirmation_setting: @application_setting[:email_confirmation_setting].to_s,
minimum_password_length: @application_setting[:minimum_password_length],
minimum_password_length_min: ApplicationSetting::DEFAULT_MINIMUM_PASSWORD_LENGTH,
minimum_password_length_max: Devise.password_length.max,

View File

@ -20,6 +20,7 @@ class ApplicationSetting < ApplicationRecord
'Admin Area > Settings > General > Kroki'
enum whats_new_variant: { all_tiers: 0, current_tier: 1, disabled: 2 }, _prefix: true
enum email_confirmation_setting: { off: 0, soft: 1, hard: 2 }
add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption) ? :optional : :required }
add_authentication_token_field :health_check_access_token

View File

@ -110,6 +110,10 @@ module ProjectFeaturesCompatibility
write_feature_attribute_string(:releases_access_level, value)
end
def infrastructure_access_level=(value)
write_feature_attribute_string(:infrastructure_access_level, value)
end
# TODO: Remove this method after we drop support for project create/edit APIs to set the
# container_registry_enabled attribute. They can instead set the container_registry_access_level
# attribute.

View File

@ -4,11 +4,6 @@ class NamespaceSetting < ApplicationRecord
include CascadingNamespaceSettingAttribute
include Sanitizable
include ChronicDurationAttribute
include IgnorableColumns
ignore_columns %i[exclude_from_free_user_cap include_for_free_user_cap_preview],
remove_with: '15.5',
remove_after: '2022-09-23'
cascading_attr :delayed_project_removal

View File

@ -451,7 +451,7 @@ class Project < ApplicationRecord
:metrics_dashboard_access_level, :analytics_access_level,
:operations_access_level, :security_and_compliance_access_level,
:container_registry_access_level, :environments_access_level, :feature_flags_access_level,
:monitor_access_level, :releases_access_level,
:monitor_access_level, :releases_access_level, :infrastructure_access_level,
to: :project_feature, allow_nil: true
delegate :show_default_award_emojis, :show_default_award_emojis=,

View File

@ -25,6 +25,7 @@ class ProjectFeature < ApplicationRecord
environments
feature_flags
releases
infrastructure
].freeze
EXPORTABLE_FEATURES = (FEATURES - [:security_and_compliance, :pages]).freeze

View File

@ -190,7 +190,7 @@ class Wiki
end
def empty?
!repository_exists? || list_page_paths.empty?
!repository_exists? || list_page_paths(limit: 1).empty?
end
def exists?
@ -207,9 +207,9 @@ class Wiki
#
# Returns an Array of GitLab WikiPage instances or an
# empty Array if this Wiki has no pages.
def list_pages(limit: 0, direction: DIRECTION_ASC, load_content: false)
def list_pages(direction: DIRECTION_ASC, load_content: false, limit: 0, offset: 0)
create_wiki_repository unless repository_exists?
list_pages_with_repository_rpcs(limit: limit, direction: direction, load_content: load_content)
list_pages_with_repository_rpcs(direction: direction, load_content: load_content, limit: limit, offset: offset)
end
def sidebar_entries(limit: Gitlab::WikiPages::MAX_SIDEBAR_PAGES, **options)
@ -457,7 +457,7 @@ class Wiki
escaped_path = RE2::Regexp.escape(sluggified_title(title))
path_regexp = Gitlab::EncodingHelper.encode_utf8_no_detect("(?i)^#{escaped_path}\\.(#{file_extension_regexp})$")
matched_files = repository.search_files_by_regexp(path_regexp, version)
matched_files = repository.search_files_by_regexp(path_regexp, version, limit: 1)
return if matched_files.blank?
Gitlab::EncodingHelper.encode_utf8_no_detect(matched_files.first)
@ -509,15 +509,15 @@ class Wiki
path.sub(/\.[^.]+\z/, "")
end
def list_page_paths
def list_page_paths(limit: 0, offset: 0)
return [] if repository.empty?
path_regexp = Gitlab::EncodingHelper.encode_utf8_no_detect("(?i)\\.(#{file_extension_regexp})$")
repository.search_files_by_regexp(path_regexp, default_branch)
repository.search_files_by_regexp(path_regexp, default_branch, limit: limit, offset: offset)
end
def list_pages_with_repository_rpcs(limit:, direction:, load_content:)
paths = list_page_paths
def list_pages_with_repository_rpcs(direction:, load_content:, limit:, offset:)
paths = list_page_paths(limit: limit, offset: offset)
return [] if paths.empty?
pages = paths.map do |path|

View File

@ -21,11 +21,13 @@ module WorkItems
}.freeze
WIDGETS_FOR_TYPE = {
issue: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::StartAndDueDate],
issue: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::StartAndDueDate,
Widgets::Milestone],
incident: [Widgets::Description, Widgets::Hierarchy],
test_case: [Widgets::Description],
requirement: [Widgets::Description],
task: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::StartAndDueDate]
task: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::StartAndDueDate,
Widgets::Milestone]
}.freeze
WI_TYPES_WITH_CREATED_HEADER = %w[issue incident].freeze

View File

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

View File

@ -213,6 +213,7 @@ class ProjectPolicy < BasePolicy
environments
feature_flags
releases
infrastructure
]
features.each do |f|
@ -409,6 +410,14 @@ class ProjectPolicy < BasePolicy
prevent(*create_read_update_admin_destroy(:alert_management_alert))
end
rule { split_operations_visibility_permissions & infrastructure_disabled }.policy do
prevent(*create_read_update_admin_destroy(:terraform_state))
prevent(*create_read_update_admin_destroy(:cluster))
prevent(:read_pod_logs)
prevent(:read_prometheus)
prevent(:admin_project_google_cloud)
end
rule { can?(:metrics_dashboard) }.policy do
enable :read_prometheus
enable :read_deployment

View File

@ -30,6 +30,13 @@ module WorkItems
error(e.message, :unprocessable_entity)
end
def before_create(work_item)
execute_widgets(work_item: work_item, callback: :before_create_callback,
widget_params: @widget_params)
super
end
def transaction_create(work_item)
super.tap do |save_result|
if save_result

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
module WorkItems
module Widgets
module MilestoneService
class BaseService < WorkItems::Widgets::BaseService
private
def handle_milestone_change(params:)
return unless params.present? && params.key?(:milestone_id)
unless has_permission?(:set_work_item_metadata)
params.delete(:milestone_id)
return
end
if params[:milestone_id].nil?
work_item.milestone = nil
return
end
project = work_item.project
milestone = MilestonesFinder.new({
project_ids: [project.id],
group_ids: project.group&.self_and_ancestors&.select(:id),
ids: [params[:milestone_id]]
}).execute.first
if milestone
work_item.milestone = milestone
else
params.delete(:milestone_id)
end
end
end
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module WorkItems
module Widgets
module MilestoneService
class CreateService < WorkItems::Widgets::MilestoneService::BaseService
def before_create_callback(params:)
handle_milestone_change(params: params)
end
end
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module WorkItems
module Widgets
module MilestoneService
class UpdateService < WorkItems::Widgets::MilestoneService::BaseService
def before_update_callback(params:)
handle_milestone_change(params: params)
end
end
end
end
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddEmailConfirmationSettingToApplicationSettings < Gitlab::Database::Migration[2.0]
def change
add_column :application_settings, :email_confirmation_setting, :integer, limit: 2, default: 2
end
end

View File

@ -0,0 +1 @@
001b43cc0006b8f936310171ff2d12993eece1378f64945e6835728f540815ba

View File

@ -11493,6 +11493,7 @@ CREATE TABLE application_settings (
password_expires_in_days integer DEFAULT 90 NOT NULL,
password_expires_notice_before_days integer DEFAULT 7 NOT NULL,
product_analytics_enabled boolean DEFAULT false NOT NULL,
email_confirmation_setting smallint DEFAULT 2,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_container_registry_pre_import_tags_rate_positive CHECK ((container_registry_pre_import_tags_rate >= (0)::numeric)),
CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)),

View File

@ -5774,6 +5774,7 @@ Input type: `WorkItemCreateInput`
| <a id="mutationworkitemcreateconfidential"></a>`confidential` | [`Boolean`](#boolean) | Sets the work item confidentiality. |
| <a id="mutationworkitemcreatedescription"></a>`description` | [`String`](#string) | Description of the work item. |
| <a id="mutationworkitemcreatehierarchywidget"></a>`hierarchyWidget` | [`WorkItemWidgetHierarchyCreateInput`](#workitemwidgethierarchycreateinput) | Input for hierarchy widget. |
| <a id="mutationworkitemcreatemilestonewidget"></a>`milestoneWidget` | [`WorkItemWidgetMilestoneInput`](#workitemwidgetmilestoneinput) | Input for milestone widget. |
| <a id="mutationworkitemcreateprojectpath"></a>`projectPath` | [`ID!`](#id) | Full path of the project the work item is associated with. |
| <a id="mutationworkitemcreatetitle"></a>`title` | [`String!`](#string) | Title of the work item. |
| <a id="mutationworkitemcreateworkitemtypeid"></a>`workItemTypeId` | [`WorkItemsTypeID!`](#workitemstypeid) | Global ID of a work item type. |
@ -5887,6 +5888,7 @@ Input type: `WorkItemUpdateInput`
| <a id="mutationworkitemupdateid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. |
| <a id="mutationworkitemupdateiterationwidget"></a>`iterationWidget` | [`WorkItemWidgetIterationInput`](#workitemwidgetiterationinput) | Input for iteration widget. |
| <a id="mutationworkitemupdatelabelswidget"></a>`labelsWidget` | [`WorkItemWidgetLabelsUpdateInput`](#workitemwidgetlabelsupdateinput) | Input for labels widget. |
| <a id="mutationworkitemupdatemilestonewidget"></a>`milestoneWidget` | [`WorkItemWidgetMilestoneInput`](#workitemwidgetmilestoneinput) | Input for milestone widget. |
| <a id="mutationworkitemupdatestartandduedatewidget"></a>`startAndDueDateWidget` | [`WorkItemWidgetStartAndDueDateUpdateInput`](#workitemwidgetstartandduedateupdateinput) | Input for start and due date widget. |
| <a id="mutationworkitemupdatestateevent"></a>`stateEvent` | [`WorkItemStateEvent`](#workitemstateevent) | Close or reopen a work item. |
| <a id="mutationworkitemupdatestatuswidget"></a>`statusWidget` | [`StatusInput`](#statusinput) | Input for status widget. |
@ -19968,6 +19970,17 @@ Represents the labels widget.
| <a id="workitemwidgetlabelslabels"></a>`labels` | [`LabelConnection`](#labelconnection) | Labels assigned to the work item. (see [Connections](#connections)) |
| <a id="workitemwidgetlabelstype"></a>`type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. |
### `WorkItemWidgetMilestone`
Represents a milestone widget.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="workitemwidgetmilestonemilestone"></a>`milestone` | [`Milestone`](#milestone) | Milestone of the work item. |
| <a id="workitemwidgetmilestonetype"></a>`type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. |
### `WorkItemWidgetStartAndDueDate`
Represents a start and due date widget.
@ -22044,6 +22057,7 @@ Type of a work item widget.
| <a id="workitemwidgettypehierarchy"></a>`HIERARCHY` | Hierarchy widget. |
| <a id="workitemwidgettypeiteration"></a>`ITERATION` | Iteration widget. |
| <a id="workitemwidgettypelabels"></a>`LABELS` | Labels widget. |
| <a id="workitemwidgettypemilestone"></a>`MILESTONE` | Milestone widget. |
| <a id="workitemwidgettypestart_and_due_date"></a>`START_AND_DUE_DATE` | Start And Due Date widget. |
| <a id="workitemwidgettypestatus"></a>`STATUS` | Status widget. |
| <a id="workitemwidgettypeweight"></a>`WEIGHT` | Weight widget. |
@ -23314,6 +23328,7 @@ Implementations:
- [`WorkItemWidgetHierarchy`](#workitemwidgethierarchy)
- [`WorkItemWidgetIteration`](#workitemwidgetiteration)
- [`WorkItemWidgetLabels`](#workitemwidgetlabels)
- [`WorkItemWidgetMilestone`](#workitemwidgetmilestone)
- [`WorkItemWidgetStartAndDueDate`](#workitemwidgetstartandduedate)
- [`WorkItemWidgetStatus`](#workitemwidgetstatus)
- [`WorkItemWidgetWeight`](#workitemwidgetweight)
@ -23847,6 +23862,7 @@ A time-frame defined as a closed inclusive range of two dates.
| <a id="workitemupdatedtaskinputhierarchywidget"></a>`hierarchyWidget` | [`WorkItemWidgetHierarchyUpdateInput`](#workitemwidgethierarchyupdateinput) | Input for hierarchy widget. |
| <a id="workitemupdatedtaskinputid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. |
| <a id="workitemupdatedtaskinputlabelswidget"></a>`labelsWidget` | [`WorkItemWidgetLabelsUpdateInput`](#workitemwidgetlabelsupdateinput) | Input for labels widget. |
| <a id="workitemupdatedtaskinputmilestonewidget"></a>`milestoneWidget` | [`WorkItemWidgetMilestoneInput`](#workitemwidgetmilestoneinput) | Input for milestone widget. |
| <a id="workitemupdatedtaskinputstartandduedatewidget"></a>`startAndDueDateWidget` | [`WorkItemWidgetStartAndDueDateUpdateInput`](#workitemwidgetstartandduedateupdateinput) | Input for start and due date widget. |
| <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. |
@ -23901,6 +23917,14 @@ A time-frame defined as a closed inclusive range of two dates.
| <a id="workitemwidgetlabelsupdateinputaddlabelids"></a>`addLabelIds` | [`[LabelID!]`](#labelid) | Global IDs of labels to be added to the work item. |
| <a id="workitemwidgetlabelsupdateinputremovelabelids"></a>`removeLabelIds` | [`[LabelID!]`](#labelid) | Global IDs of labels to be removed from the work item. |
### `WorkItemWidgetMilestoneInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="workitemwidgetmilestoneinputmilestoneid"></a>`milestoneId` | [`MilestoneID`](#milestoneid) | Milestone to assign to the work item. |
### `WorkItemWidgetStartAndDueDateUpdateInput`
#### Arguments

View File

@ -1054,19 +1054,19 @@ module Gitlab
end
end
def search_files_by_name(query, ref)
def search_files_by_name(query, ref, limit: 0, offset: 0)
safe_query = query.sub(%r{^/*}, "")
ref ||= root_ref
return [] if empty? || safe_query.blank?
gitaly_repository_client.search_files_by_name(ref, safe_query).map do |file|
gitaly_repository_client.search_files_by_name(ref, safe_query, limit: limit, offset: offset).map do |file|
Gitlab::EncodingHelper.encode_utf8(file)
end
end
def search_files_by_regexp(filter, ref = 'HEAD')
gitaly_repository_client.search_files_by_regexp(ref, filter).map do |file|
def search_files_by_regexp(filter, ref = 'HEAD', limit: 0, offset: 0)
gitaly_repository_client.search_files_by_regexp(ref, filter, limit: limit, offset: offset).map do |file|
Gitlab::EncodingHelper.encode_utf8(file)
end
end

View File

@ -303,8 +303,8 @@ module Gitlab
GitalyClient.call(@storage, :repository_service, :get_raw_changes, request, timeout: GitalyClient.fast_timeout)
end
def search_files_by_name(ref, query)
request = Gitaly::SearchFilesByNameRequest.new(repository: @gitaly_repo, ref: ref, query: query)
def search_files_by_name(ref, query, limit: 0, offset: 0)
request = Gitaly::SearchFilesByNameRequest.new(repository: @gitaly_repo, ref: ref, query: query, limit: limit, offset: offset)
GitalyClient.call(@storage, :repository_service, :search_files_by_name, request, timeout: GitalyClient.fast_timeout).flat_map(&:files)
end
@ -314,8 +314,8 @@ module Gitlab
search_results_from_response(response, options)
end
def search_files_by_regexp(ref, filter)
request = Gitaly::SearchFilesByNameRequest.new(repository: @gitaly_repo, ref: ref, query: '.', filter: filter)
def search_files_by_regexp(ref, filter, limit: 0, offset: 0)
request = Gitaly::SearchFilesByNameRequest.new(repository: @gitaly_repo, ref: ref, query: '.', filter: filter, limit: limit, offset: offset)
GitalyClient.call(@storage, :repository_service, :search_files_by_name, request, timeout: GitalyClient.fast_timeout).flat_map(&:files)
end

View File

@ -302,6 +302,7 @@ included_attributes:
- :environments_access_level
- :feature_flags_access_level
- :releases_access_level
- :infrastructure_access_level
prometheus_metrics:
- :created_at
- :updated_at
@ -717,6 +718,7 @@ included_attributes:
- :environments_access_level
- :feature_flags_access_level
- :releases_access_level
- :infrastructure_access_level
- :allow_merge_on_skipped_pipeline
- :auto_devops_deploy_strategy
- :auto_devops_enabled

View File

@ -20,6 +20,10 @@ module Sidebars
# Push Rules are the only group setting that can also be edited by maintainers.
# Create an empty sub-menu here and EE adds Repository menu item (with only Push Rules).
return true
elsif Gitlab.ee? && can?(context.current_user, :read_billing, context.group)
# Billing is the only group setting that is visible to auditors.
# Create an empty sub-menu here and EE adds Settings menu item (with only Billing).
return true
end
false

View File

@ -6,7 +6,7 @@ module Sidebars
class InfrastructureMenu < ::Sidebars::Menu
override :configure_menu_items
def configure_menu_items
return false unless context.project.feature_available?(:operations, context.current_user)
return false unless feature_enabled?
add_item(kubernetes_menu_item)
add_item(terraform_menu_item)
@ -34,6 +34,14 @@ module Sidebars
private
def feature_enabled?
if ::Feature.enabled?(:split_operations_visibility_permissions, context.project)
context.project.feature_available?(:infrastructure, context.current_user)
else
context.project.feature_available?(:operations, context.current_user)
end
end
def kubernetes_menu_item
unless can?(context.current_user, :read_cluster, context.project)
return ::Sidebars::NilMenuItem.new(item_id: :kubernetes)

View File

@ -4717,6 +4717,9 @@ msgstr ""
msgid "ApplicationSettings|Domain denylist"
msgstr ""
msgid "ApplicationSettings|Email confirmation settings"
msgstr ""
msgid "ApplicationSettings|Email restrictions"
msgstr ""
@ -4735,9 +4738,18 @@ msgstr ""
msgid "ApplicationSettings|Enter denylist manually"
msgstr ""
msgid "ApplicationSettings|Hard"
msgstr ""
msgid "ApplicationSettings|Minimum password length (number of characters)"
msgstr ""
msgid "ApplicationSettings|New users can sign up without confirming their email address."
msgstr ""
msgid "ApplicationSettings|Off"
msgstr ""
msgid "ApplicationSettings|Only users with e-mail addresses that match these domain(s) can sign up. Wildcards allowed. Use separate lines for multiple entries. Example: domain.com, *.domain.com"
msgstr ""
@ -4765,6 +4777,9 @@ msgstr ""
msgid "ApplicationSettings|See %{linkStart}password policy guidelines%{linkEnd}."
msgstr ""
msgid "ApplicationSettings|Send a confirmation email during sign up. New users must confirm their email address before they can log in."
msgstr ""
msgid "ApplicationSettings|Send confirmation email on sign-up"
msgstr ""

View File

@ -2,7 +2,7 @@
source 'https://rubygems.org'
gem 'gitlab-qa', '~> 8', '>= 8.8.0', require: 'gitlab/qa'
gem 'gitlab-qa', '~> 8', '>= 8.9.0', require: 'gitlab/qa'
gem 'activesupport', '~> 6.1.4.7' # This should stay in sync with the root's Gemfile
gem 'allure-rspec', '~> 2.18.0'
gem 'capybara', '~> 3.37.1'

View File

@ -100,7 +100,7 @@ GEM
gitlab (4.18.0)
httparty (~> 0.18)
terminal-table (>= 1.5.1)
gitlab-qa (8.8.0)
gitlab-qa (8.9.0)
activesupport (~> 6.1)
gitlab (~> 4.18.0)
http (~> 5.0)
@ -314,7 +314,7 @@ DEPENDENCIES
faraday-retry (~> 2.0)
fog-core (= 2.1.0)
fog-google (~> 1.19)
gitlab-qa (~> 8, >= 8.8.0)
gitlab-qa (~> 8, >= 8.9.0)
influxdb-client (~> 2.7)
knapsack (~> 4.0)
nokogiri (~> 1.13, >= 1.13.9)

View File

@ -27,6 +27,7 @@ module RuboCop
environments
feature_flags
releases
infrastructure
].freeze
EE_FEATURES = %i[requirements].freeze
ALL_FEATURES = (FEATURES + EE_FEATURES).freeze

View File

@ -921,6 +921,7 @@ RSpec.describe ProjectsController do
feature_flags_access_level
releases_access_level
monitor_access_level
infrastructure_access_level
]
end

View File

@ -41,6 +41,7 @@ FactoryBot.define do
environments_access_level { ProjectFeature::ENABLED }
feature_flags_access_level { ProjectFeature::ENABLED }
releases_access_level { ProjectFeature::ENABLED }
infrastructure_access_level { ProjectFeature::ENABLED }
# we can't assign the delegated `#ci_cd_settings` attributes directly, as the
# `#ci_cd_settings` relation needs to be created first

View File

@ -205,6 +205,22 @@ RSpec.describe 'Admin updates settings' do
expect(page).to have_content "Application settings saved successfully"
end
end
context 'Email confirmation settings' do
it "is set to 'hard' by default" do
expect(current_settings.email_confirmation_setting).to eq('hard')
end
it 'changes the setting', :js do
page.within('.as-signup') do
choose 'Off'
click_button 'Save changes'
end
expect(current_settings.email_confirmation_setting).to eq('off')
expect(page).to have_content "Application settings saved successfully"
end
end
end
it 'change Sign-in restrictions' do

View File

@ -4,6 +4,7 @@ export const rawMockData = {
signupEnabled: 'true',
requireAdminApprovalAfterUserSignup: 'true',
sendUserConfirmationEmail: 'true',
emailConfirmationSetting: 'hard',
minimumPasswordLength: '8',
minimumPasswordLengthMin: '3',
minimumPasswordLengthMax: '10',
@ -30,6 +31,7 @@ export const mockData = {
signupEnabled: true,
requireAdminApprovalAfterUserSignup: true,
sendUserConfirmationEmail: true,
emailConfirmationSetting: 'hard',
minimumPasswordLength: '8',
minimumPasswordLengthMin: '3',
minimumPasswordLengthMax: '10',

View File

@ -2,8 +2,6 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::Migrations::Runner, :reestablished_active_record_base do
include Database::MultipleDatabases
let(:base_result_dir) { Pathname.new(Dir.mktmpdir) }
let(:migration_runs) { [] } # This list gets populated as the runner tries to run migrations

View File

@ -592,6 +592,7 @@ ProjectFeature:
- feature_flags_access_level
- releases_access_level
- monitor_access_level
- infrastructure_access_level
- created_at
- updated_at
ProtectedBranch::MergeAccessLevel:

View File

@ -23,6 +23,52 @@ RSpec.describe Sidebars::Projects::Menus::InfrastructureMenu do
expect(subject.render?).to be true
end
end
describe 'behavior based on access level setting' do
using RSpec::Parameterized::TableSyntax
let_it_be(:project) { create(:project) }
let(:enabled) { Featurable::PRIVATE }
let(:disabled) { Featurable::DISABLED }
where(:operations_access_level, :infrastructure_access_level, :render) do
ref(:disabled) | ref(:enabled) | true
ref(:disabled) | ref(:disabled) | false
ref(:enabled) | ref(:enabled) | true
ref(:enabled) | ref(:disabled) | false
end
with_them do
it 'renders based on the infrastructure access level' do
project.project_feature.update!(operations_access_level: operations_access_level)
project.project_feature.update!(infrastructure_access_level: infrastructure_access_level)
expect(subject.render?).to be render
end
end
context 'when `split_operations_visibility_permissions` feature flag is disabled' do
before do
stub_feature_flags(split_operations_visibility_permissions: false)
end
where(:operations_access_level, :infrastructure_access_level, :render) do
ref(:disabled) | ref(:enabled) | false
ref(:disabled) | ref(:disabled) | false
ref(:enabled) | ref(:enabled) | true
ref(:enabled) | ref(:disabled) | true
end
with_them do
it 'renders based on the operations access level' do
project.project_feature.update!(operations_access_level: operations_access_level)
project.project_feature.update!(infrastructure_access_level: infrastructure_access_level)
expect(subject.render?).to be render
end
end
end
end
end
describe '#link' do

View File

@ -8,7 +8,7 @@ RSpec.describe ProjectFeaturesCompatibility do
let(:features) do
features_enabled + %w(
repository pages operations container_registry package_registry environments feature_flags releases
monitor
monitor infrastructure
)
end

View File

@ -862,6 +862,7 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to delegate_method(:environments_access_level).to(:project_feature) }
it { is_expected.to delegate_method(:feature_flags_access_level).to(:project_feature) }
it { is_expected.to delegate_method(:releases_access_level).to(:project_feature) }
it { is_expected.to delegate_method(:infrastructure_access_level).to(:project_feature) }
it { is_expected.to delegate_method(:maven_package_requests_forwarding).to(:namespace) }
it { is_expected.to delegate_method(:pypi_package_requests_forwarding).to(:namespace) }
it { is_expected.to delegate_method(:npm_package_requests_forwarding).to(:namespace) }

View File

@ -69,7 +69,8 @@ RSpec.describe WorkItems::Type do
::WorkItems::Widgets::Hierarchy,
::WorkItems::Widgets::Labels,
::WorkItems::Widgets::Assignees,
::WorkItems::Widgets::StartAndDueDate
::WorkItems::Widgets::StartAndDueDate,
::WorkItems::Widgets::Milestone
)
end
end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WorkItems::Widgets::Milestone do
let_it_be(:project) { create(:project) }
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:work_item) { create(:work_item, :issue, project: project, milestone: milestone) }
describe '.type' do
subject { described_class.type }
it { is_expected.to eq(:milestone) }
end
describe '#type' do
subject { described_class.new(work_item).type }
it { is_expected.to eq(:milestone) }
end
describe '#milestone' do
subject { described_class.new(work_item).milestone }
it { is_expected.to eq(work_item.milestone) }
end
end

View File

@ -2299,6 +2299,74 @@ RSpec.describe ProjectPolicy do
end
end
describe 'infrastructure feature' do
using RSpec::Parameterized::TableSyntax
let(:guest_permissions) { [] }
let(:developer_permissions) do
guest_permissions + [:read_terraform_state, :read_pod_logs, :read_prometheus]
end
let(:maintainer_permissions) do
developer_permissions + [:create_cluster, :read_cluster, :update_cluster, :admin_cluster, :admin_terraform_state, :admin_project_google_cloud]
end
where(:project_visibility, :access_level, :role, :allowed) do
:public | ProjectFeature::ENABLED | :maintainer | true
:public | ProjectFeature::ENABLED | :developer | true
:public | ProjectFeature::ENABLED | :guest | true
:public | ProjectFeature::ENABLED | :anonymous | true
:public | ProjectFeature::PRIVATE | :maintainer | true
:public | ProjectFeature::PRIVATE | :developer | true
:public | ProjectFeature::PRIVATE | :guest | true
:public | ProjectFeature::PRIVATE | :anonymous | false
:public | ProjectFeature::DISABLED | :maintainer | false
:public | ProjectFeature::DISABLED | :developer | false
:public | ProjectFeature::DISABLED | :guest | false
:public | ProjectFeature::DISABLED | :anonymous | false
:internal | ProjectFeature::ENABLED | :maintainer | true
:internal | ProjectFeature::ENABLED | :developer | true
:internal | ProjectFeature::ENABLED | :guest | true
:internal | ProjectFeature::ENABLED | :anonymous | false
:internal | ProjectFeature::PRIVATE | :maintainer | true
:internal | ProjectFeature::PRIVATE | :developer | true
:internal | ProjectFeature::PRIVATE | :guest | true
:internal | ProjectFeature::PRIVATE | :anonymous | false
:internal | ProjectFeature::DISABLED | :maintainer | false
:internal | ProjectFeature::DISABLED | :developer | false
:internal | ProjectFeature::DISABLED | :guest | false
:internal | ProjectFeature::DISABLED | :anonymous | false
:private | ProjectFeature::ENABLED | :maintainer | true
:private | ProjectFeature::ENABLED | :developer | true
:private | ProjectFeature::ENABLED | :guest | true
:private | ProjectFeature::ENABLED | :anonymous | false
:private | ProjectFeature::PRIVATE | :maintainer | true
:private | ProjectFeature::PRIVATE | :developer | true
:private | ProjectFeature::PRIVATE | :guest | true
:private | ProjectFeature::PRIVATE | :anonymous | false
:private | ProjectFeature::DISABLED | :maintainer | false
:private | ProjectFeature::DISABLED | :developer | false
:private | ProjectFeature::DISABLED | :guest | false
:private | ProjectFeature::DISABLED | :anonymous | false
end
with_them do
let(:current_user) { user_subject(role) }
let(:project) { project_subject(project_visibility) }
it 'allows/disallows the abilities based on the infrastructure access level' do
project.project_feature.update!(infrastructure_access_level: access_level)
if allowed
expect_allowed(*permissions_abilities(role))
else
expect_disallowed(*permissions_abilities(role))
end
end
end
end
describe 'access_security_and_compliance' do
context 'when the "Security & Compliance" is enabled' do
before do

View File

@ -154,6 +154,68 @@ RSpec.describe 'Create a work item' do
end
end
context 'with milestone widget input' do
let(:widgets_response) { mutation_response['workItem']['widgets'] }
let(:fields) do
<<~FIELDS
workItem {
widgets {
type
... on WorkItemWidgetMilestone {
milestone {
id
}
}
}
}
errors
FIELDS
end
let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path), fields) }
context 'when setting milestone on work item creation' do
let_it_be(:project_milestone) { create(:milestone, project: project) }
let_it_be(:group_milestone) { create(:milestone, project: project) }
let(:input) do
{
title: 'some WI',
workItemTypeId: WorkItems::Type.default_by_type(:task).to_global_id.to_s,
milestoneWidget: { 'milestoneId' => milestone.to_global_id.to_s }
}
end
shared_examples "work item's milestone is set" do
it "sets the work item's milestone" do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.to change(WorkItem, :count).by(1)
expect(response).to have_gitlab_http_status(:success)
expect(widgets_response).to include(
{
'type' => 'MILESTONE',
'milestone' => { 'id' => milestone.to_global_id.to_s }
}
)
end
end
context 'when assigning a project milestone' do
it_behaves_like "work item's milestone is set" do
let(:milestone) { project_milestone }
end
end
context 'when assigning a group milestone' do
it_behaves_like "work item's milestone is set" do
let(:milestone) { group_milestone }
end
end
end
end
context 'when the work_items feature flag is disabled' do
before do
stub_feature_flags(work_items: false)

View File

@ -5,8 +5,11 @@ require 'spec_helper'
RSpec.describe 'Update a work item' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
let_it_be(:reporter) { create(:user).tap { |user| project.add_reporter(user) } }
let_it_be(:guest) { create(:user).tap { |user| project.add_guest(user) } }
let_it_be(:work_item, refind: true) { create(:work_item, project: project) }
let(:work_item_event) { 'CLOSE' }
@ -543,6 +546,91 @@ RSpec.describe 'Update a work item' do
end
end
context 'when updating milestone' do
let_it_be(:project_milestone) { create(:milestone, project: project) }
let_it_be(:group_milestone) { create(:milestone, project: project) }
let(:input) { { 'milestoneWidget' => { 'milestoneId' => new_milestone&.to_global_id&.to_s } } }
let(:fields) do
<<~FIELDS
workItem {
widgets {
type
... on WorkItemWidgetMilestone {
milestone {
id
}
}
}
}
errors
FIELDS
end
shared_examples "work item's milestone is updated" do
it "updates the work item's milestone" do
expect do
post_graphql_mutation(mutation, current_user: current_user)
work_item.reload
end.to change(work_item, :milestone).from(old_milestone).to(new_milestone)
expect(response).to have_gitlab_http_status(:success)
end
end
shared_examples "work item's milestone is not updated" do
it "ignores the update request" do
expect do
post_graphql_mutation(mutation, current_user: current_user)
work_item.reload
end.to not_change(work_item, :milestone)
expect(response).to have_gitlab_http_status(:success)
end
end
context 'when user cannot set work item metadata' do
let(:current_user) { guest }
let(:old_milestone) { nil }
it_behaves_like "work item's milestone is not updated" do
let(:new_milestone) { project_milestone }
end
end
context 'when user can set work item metadata' do
let(:current_user) { reporter }
context 'when assigning a project milestone' do
it_behaves_like "work item's milestone is updated" do
let(:old_milestone) { nil }
let(:new_milestone) { project_milestone }
end
end
context 'when assigning a group milestone' do
it_behaves_like "work item's milestone is updated" do
let(:old_milestone) { nil }
let(:new_milestone) { group_milestone }
end
end
context "when unsetting the work item's milestone" do
it_behaves_like "work item's milestone is updated" do
let(:old_milestone) { group_milestone }
let(:new_milestone) { nil }
before do
work_item.update!(milestone: old_milestone)
end
end
end
end
end
context 'when unsupported widget input is sent' do
let_it_be(:test_case) { create(:work_item_type, :default, :test_case, name: 'some_test_case_name') }
let_it_be(:work_item) { create(:work_item, work_item_type: test_case, project: project) }

View File

@ -10,6 +10,8 @@ RSpec.describe 'getting a work item list for a project' do
let_it_be(:current_user) { create(:user) }
let_it_be(:label1) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) }
let_it_be(:milestone1) { create(:milestone, project: project) }
let_it_be(:milestone2) { create(:milestone, project: project) }
let_it_be(:item1) { create(:work_item, project: project, discussion_locked: true, title: 'item1', labels: [label1]) }
let_it_be(:item2) do
@ -19,7 +21,8 @@ RSpec.describe 'getting a work item list for a project' do
title: 'item2',
last_edited_by: current_user,
last_edited_at: 1.day.ago,
labels: [label2]
labels: [label2],
milestone: milestone1
)
end
@ -55,7 +58,8 @@ RSpec.describe 'getting a work item list for a project' do
:last_edited_by_user,
last_edited_at: 1.week.ago,
project: project,
labels: [label1, label2]
labels: [label1, label2],
milestone: milestone2
)
expect_graphql_errors_to_be_empty
@ -94,6 +98,11 @@ RSpec.describe 'getting a work item list for a project' do
labels { nodes { id } }
allowsScopedLabels
}
... on WorkItemWidgetMilestone {
milestone {
id
}
}
}
}
GRAPHQL

View File

@ -298,6 +298,40 @@ RSpec.describe 'Query.work_item(id)' do
)
end
end
describe 'milestone widget' do
let_it_be(:milestone) { create(:milestone, project: project) }
let(:work_item) { create(:work_item, project: project, milestone: milestone) }
let(:work_item_fields) do
<<~GRAPHQL
id
widgets {
type
... on WorkItemWidgetMilestone {
milestone {
id
}
}
}
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' => 'MILESTONE',
'milestone' => {
'id' => work_item.milestone.to_gid.to_s
}
)
)
)
end
end
end
context 'when an Issue Global ID is provided' do

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WorkItems::Widgets::MilestoneService::CreateService do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :private, group: group) }
let_it_be(:project_milestone) { create(:milestone, project: project) }
let_it_be(:group_milestone) { create(:milestone, group: group) }
let_it_be(:guest) { create(:user) }
let(:current_user) { guest }
let(:work_item) { build(:work_item, project: project, updated_at: 1.day.ago) }
let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::Milestone) } }
let(:service) { described_class.new(widget: widget, current_user: current_user) }
before do
project.add_guest(guest)
end
describe '#before_create_callback' do
it_behaves_like "setting work item's milestone" do
subject(:execute_callback) do
service.before_create_callback(params: params)
end
end
end
end

View File

@ -0,0 +1,58 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WorkItems::Widgets::MilestoneService::UpdateService do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :private, group: group) }
let_it_be(:project_milestone) { create(:milestone, project: project) }
let_it_be(:group_milestone) { create(:milestone, group: group) }
let_it_be(:reporter) { create(:user) }
let_it_be(:guest) { create(:user) }
let(:work_item) { create(:work_item, project: project, updated_at: 1.day.ago) }
let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::Milestone) } }
let(:service) { described_class.new(widget: widget, current_user: current_user) }
before do
project.add_reporter(reporter)
project.add_guest(guest)
end
describe '#before_update_callback' do
context 'when current user is not allowed to set work item metadata' do
let(:current_user) { guest }
let(:params) { { milestone_id: group_milestone.id } }
it "does not set the work item's milestone" do
expect { service.before_update_callback(params: params) }
.to not_change(work_item, :milestone)
end
end
context "when current user is allowed to set work item metadata" do
let(:current_user) { reporter }
it_behaves_like "setting work item's milestone" do
subject(:execute_callback) do
service.before_update_callback(params: params)
end
end
context 'when unsetting a milestone' do
let(:params) { { milestone_id: nil } }
before do
work_item.update!(milestone: project_milestone)
end
it "sets the work item's milestone" do
expect { service.before_update_callback(params: params) }
.to change(work_item, :milestone)
.from(project_milestone)
.to(nil)
end
end
end
end
end

View File

@ -145,7 +145,7 @@ RSpec.configure do |config|
config.include NextInstanceOf
config.include TestEnv
config.include FileReadHelpers
config.include Database::MultipleDatabases
config.include Database::MultipleDatabasesHelpers
config.include Database::WithoutCheckConstraint
config.include Devise::Test::ControllerHelpers, type: :controller
config.include Devise::Test::ControllerHelpers, type: :view

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module Database
module MultipleDatabases
module MultipleDatabasesHelpers
def skip_if_multiple_databases_not_setup
skip 'Skipping because multiple databases not set up' unless Gitlab::Database.has_config?(:ci)
end
@ -52,17 +52,17 @@ module Database
#
# rubocop:disable Database/MultipleDatabases
def with_reestablished_active_record_base(reconnect: true)
connection_classes = ActiveRecord::Base.connection_handler.connection_pool_names.map(&:constantize).to_h do |klass|
[klass, klass.connection_db_config]
end
connection_classes = ActiveRecord::Base
.connection_handler
.connection_pool_names
.map(&:constantize)
.index_with(&:connection_db_config)
original_handler = ActiveRecord::Base.connection_handler
new_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
ActiveRecord::Base.connection_handler = new_handler
if reconnect
connection_classes.each { |klass, db_config| klass.establish_connection(db_config) }
end
connection_classes.each { |klass, db_config| klass.establish_connection(db_config) } if reconnect
yield
ensure
@ -95,9 +95,12 @@ module Database
module ActiveRecordBaseEstablishConnection
def establish_connection(*args)
# rubocop:disable Database/MultipleDatabases
if connected? && connection&.transaction_open? && ActiveRecord::Base.connection_handler == ActiveRecord::Base.default_connection_handler
raise "Cannot re-establish '#{self}.establish_connection' within an open transaction (#{connection&.open_transactions.to_i}). " \
"Use `with_reestablished_active_record_base` instead or add `:reestablished_active_record_base` to rspec context."
if connected? &&
connection&.transaction_open? &&
ActiveRecord::Base.connection_handler == ActiveRecord::Base.default_connection_handler
raise "Cannot re-establish '#{self}.establish_connection' within an open transaction " \
"(#{connection&.open_transactions.to_i}). Use `with_reestablished_active_record_base` " \
"instead or add `:reestablished_active_record_base` to rspec context."
end
# rubocop:enable Database/MultipleDatabases
@ -106,56 +109,4 @@ module Database
end
end
RSpec.configure do |config|
# Ensure database versions are memoized to prevent query counts from
# being affected by version checks. Note that
# Gitlab::Database.check_postgres_version_and_print_warning is called
# at startup, but that generates its own
# `Gitlab::Database::Reflection` so the result is not memoized by
# callers of `ApplicationRecord.database.version`, such as
# `Gitlab::Database::AsWithMaterialized.materialized_supported?`.
# TODO This can be removed once https://gitlab.com/gitlab-org/gitlab/-/issues/325639 is completed.
[ApplicationRecord, ::Ci::ApplicationRecord].each { |record| record.database.version }
config.around(:each, :reestablished_active_record_base) do |example|
with_reestablished_active_record_base(reconnect: example.metadata.fetch(:reconnect, true)) do
example.run
end
end
config.around(:each, :add_ci_connection) do |example|
with_added_ci_connection do
example.run
end
end
config.append_after(:context, :migration) do
recreate_databases_and_seed_if_needed || ensure_schema_and_empty_tables
end
config.around(:each, :migration) do |example|
self.class.use_transactional_tests = false
migration_schema = example.metadata[:migration]
migration_schema = :gitlab_main if migration_schema == true
base_model = Gitlab::Database.schemas_to_base_models.fetch(migration_schema).first
# Migration require an `ActiveRecord::Base` to point to desired database
if base_model != ActiveRecord::Base
with_reestablished_active_record_base do
reconfigure_db_connection(
model: ActiveRecord::Base,
config_model: base_model
)
example.run
end
else
example.run
end
self.class.use_transactional_tests = true
end
end
ActiveRecord::Base.singleton_class.prepend(::Database::ActiveRecordBaseEstablishConnection) # rubocop:disable Database/MultipleDatabases

View File

@ -16,14 +16,42 @@ RSpec.configure do |config|
schema_migrate_down!
end
config.after(:context, :migration) do
Gitlab::CurrentSettings.clear_in_memory_application_settings!
end
config.append_after(:context, :migration) do
recreate_databases_and_seed_if_needed || ensure_schema_and_empty_tables
end
config.around(:each, :migration) do |example|
self.class.use_transactional_tests = false
migration_schema = example.metadata[:migration]
migration_schema = :gitlab_main if migration_schema == true
base_model = Gitlab::Database.schemas_to_base_models.fetch(migration_schema).first
# Migration require an `ActiveRecord::Base` to point to desired database
if base_model != ActiveRecord::Base
with_reestablished_active_record_base do
reconfigure_db_connection(
model: ActiveRecord::Base,
config_model: base_model
)
example.run
end
else
example.run
end
self.class.use_transactional_tests = true
end
# Each example may call `migrate!`, so we must ensure we are migrated down every time
config.before(:each, :migration) do
use_fake_application_settings
schema_migrate_down!
end
config.after(:context, :migration) do
Gitlab::CurrentSettings.clear_in_memory_application_settings!
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
RSpec.configure do |config|
# Ensure database versions are memoized to prevent query counts from
# being affected by version checks. Note that
# Gitlab::Database.check_postgres_version_and_print_warning is called
# at startup, but that generates its own
# `Gitlab::Database::Reflection` so the result is not memoized by
# callers of `ApplicationRecord.database.version`, such as
# `Gitlab::Database::AsWithMaterialized.materialized_supported?`.
# TODO This can be removed once https://gitlab.com/gitlab-org/gitlab/-/issues/325639 is completed.
[ApplicationRecord, ::Ci::ApplicationRecord].each { |record| record.database.version }
config.around(:each, :reestablished_active_record_base) do |example|
with_reestablished_active_record_base(reconnect: example.metadata.fetch(:reconnect, true)) do
example.run
end
end
config.around(:each, :add_ci_connection) do |example|
with_added_ci_connection do
example.run
end
end
end

View File

@ -161,9 +161,10 @@ RSpec.shared_examples 'wiki model' do
let(:wiki_pages) { subject.list_pages }
before do
subject.create_page('index', 'This is an index')
# The order is intentional
subject.create_page('index2', 'This is an index2')
subject.create_page('an index3', 'This is an index3')
subject.create_page('index', 'This is an index')
subject.create_page('index3', 'This is an index3')
end
it 'returns an array of WikiPage instances' do
@ -183,13 +184,47 @@ RSpec.shared_examples 'wiki model' do
context 'with limit option' do
it 'returns limited set of pages' do
expect(subject.list_pages(limit: 1).count).to eq(1)
expect(
subject.list_pages(limit: 1).map(&:title)
).to eql(%w[index])
end
it 'returns all set of pages if limit is more than the total pages' do
expect(subject.list_pages(limit: 4).count).to eq(3)
end
it 'returns all set of pages if limit is 0' do
expect(subject.list_pages(limit: 0).count).to eq(3)
end
end
context 'with offset option' do
it 'returns offset-ed set of pages' do
expect(
subject.list_pages(offset: 1).map(&:title)
).to eq(%w[index2 index3])
expect(
subject.list_pages(offset: 2).map(&:title)
).to eq(["index3"])
expect(subject.list_pages(offset: 3).count).to eq(0)
expect(subject.list_pages(offset: 4).count).to eq(0)
end
it 'returns all set of pages if offset is 0' do
expect(subject.list_pages(offset: 0).count).to eq(3)
end
it 'can combines with limit' do
expect(
subject.list_pages(offset: 1, limit: 1).map(&:title)
).to eq(["index2"])
end
end
context 'with sorting options' do
it 'returns pages sorted by title by default' do
pages = ['an index3', 'index', 'index2']
pages = %w[index index2 index3]
expect(subject.list_pages.map(&:title)).to eq(pages)
expect(subject.list_pages(direction: 'desc').map(&:title)).to eq(pages.reverse)
@ -200,9 +235,9 @@ RSpec.shared_examples 'wiki model' do
let(:pages) { subject.list_pages(load_content: true) }
it 'loads WikiPage content' do
expect(pages.first.content).to eq('This is an index3')
expect(pages.second.content).to eq('This is an index')
expect(pages.third.content).to eq('This is an index2')
expect(pages.first.content).to eq('This is an index')
expect(pages.second.content).to eq('This is an index2')
expect(pages.third.content).to eq('This is an index3')
end
end
end

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
RSpec.shared_examples "setting work item's milestone" do
context "when 'milestone' param does not exist" do
let(:params) { {} }
it "does not set the work item's milestone" do
expect { execute_callback }.to not_change(work_item, :milestone)
end
end
context "when 'milestone' is not in the work item's project's hierarchy" do
let(:another_group_milestone) { create(:milestone, group: create(:group)) }
let(:params) { { milestone_id: another_group_milestone.id } }
it "does not set the work item's milestone" do
expect { execute_callback }.to not_change(work_item, :milestone)
end
end
context 'when assigning a group milestone' do
let(:params) { { milestone_id: group_milestone.id } }
it "sets the work item's milestone" do
expect { execute_callback }
.to change(work_item, :milestone)
.from(nil)
.to(group_milestone)
end
end
context 'when assigning a project milestone' do
let(:params) { { milestone_id: project_milestone.id } }
it "sets the work item's milestone" do
expect { execute_callback }
.to change(work_item, :milestone)
.from(nil)
.to(project_milestone)
end
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Database::MultipleDatabases' do
RSpec.describe 'Database::MultipleDatabasesHelpers' do
let(:query) do
<<~SQL
WITH cte AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported} (SELECT 1) SELECT 1;
@ -72,7 +72,8 @@ RSpec.describe 'Database::MultipleDatabases' do
context 'when reconnect is false' do
it 'does raise exception' do
with_reestablished_active_record_base(reconnect: false) do
expect { ApplicationRecord.connection.execute("SELECT 1") }.to raise_error(ActiveRecord::ConnectionNotEstablished)
expect { ApplicationRecord.connection.execute("SELECT 1") }
.to raise_error(ActiveRecord::ConnectionNotEstablished)
end
end
end