diff options
64 files changed, 880 insertions, 119 deletions
@@ -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' diff --git a/Gemfile.checksum b/Gemfile.checksum index 9455e6360e6..d3bd3b4045f 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -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"}, diff --git a/Gemfile.lock b/Gemfile.lock index 877afac4509..82232f1c4b1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json index 545c150e536..1a949adc6a2 100644 --- a/app/assets/javascripts/graphql_shared/possible_types.json +++ b/app/assets/javascripts/graphql_shared/possible_types.json @@ -143,8 +143,9 @@ "WorkItemWidgetHierarchy", "WorkItemWidgetIteration", "WorkItemWidgetLabels", + "WorkItemWidgetMilestone", "WorkItemWidgetStartAndDueDate", "WorkItemWidgetStatus", "WorkItemWidgetWeight" ] -} +}
\ No newline at end of file diff --git a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue index ccb449f96e1..95c1f575fe4 100644 --- a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue +++ b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue @@ -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" diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 56eb2a6de56..2d2a732e85c 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -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] 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 e42e59de78f..6738f268e92 100644 --- a/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb +++ b/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb @@ -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 diff --git a/app/graphql/mutations/work_items/create.rb b/app/graphql/mutations/work_items/create.rb index ece00e04ed9..a1af8064609 100644 --- a/app/graphql/mutations/work_items/create.rb +++ b/app/graphql/mutations/work_items/create.rb @@ -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.' diff --git a/app/graphql/resolvers/work_items_resolver.rb b/app/graphql/resolvers/work_items_resolver.rb index a4cbcc61ead..264c92a4443 100644 --- a/app/graphql/resolvers/work_items_resolver.rb +++ b/app/graphql/resolvers/work_items_resolver.rb @@ -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 diff --git a/app/graphql/types/work_items/widget_interface.rb b/app/graphql/types/work_items/widget_interface.rb index a3943361114..b85d0a23535 100644 --- a/app/graphql/types/work_items/widget_interface.rb +++ b/app/graphql/types/work_items/widget_interface.rb @@ -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 diff --git a/app/graphql/types/work_items/widgets/milestone_input_type.rb b/app/graphql/types/work_items/widgets/milestone_input_type.rb new file mode 100644 index 00000000000..996c782373f --- /dev/null +++ b/app/graphql/types/work_items/widgets/milestone_input_type.rb @@ -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 diff --git a/app/graphql/types/work_items/widgets/milestone_type.rb b/app/graphql/types/work_items/widgets/milestone_type.rb new file mode 100644 index 00000000000..73318e58a00 --- /dev/null +++ b/app/graphql/types/work_items/widgets/milestone_type.rb @@ -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 diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 37196764974..9678d8bf2fd 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -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, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 173a897e4a3..a4a881b219b 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -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 diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index 2976b6f02a7..d37f20e2e7c 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -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. diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb index 6a87fba57ac..3e6371b0c4d 100644 --- a/app/models/namespace_setting.rb +++ b/app/models/namespace_setting.rb @@ -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 diff --git a/app/models/project.rb b/app/models/project.rb index 7accc208fc6..211e00eb7c6 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -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=, diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index dad8aaf0625..11f4a3f3b6f 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -25,6 +25,7 @@ class ProjectFeature < ApplicationRecord environments feature_flags releases + infrastructure ].freeze EXPORTABLE_FEATURES = (FEATURES - [:security_and_compliance, :pages]).freeze diff --git a/app/models/wiki.rb b/app/models/wiki.rb index 7d695537632..eb98bb67948 100644 --- a/app/models/wiki.rb +++ b/app/models/wiki.rb @@ -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| diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb index 753fcbcb8f9..efffba62379 100644 --- a/app/models/work_items/type.rb +++ b/app/models/work_items/type.rb @@ -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 diff --git a/app/models/work_items/widgets/milestone.rb b/app/models/work_items/widgets/milestone.rb new file mode 100644 index 00000000000..a3e610110f1 --- /dev/null +++ b/app/models/work_items/widgets/milestone.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module WorkItems + module Widgets + class Milestone < Base + delegate :milestone, to: :work_item + end + end +end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 77bdf9d62fc..832b78bb9c3 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -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 diff --git a/app/services/work_items/create_service.rb b/app/services/work_items/create_service.rb index ebda043e873..87cc690d666 100644 --- a/app/services/work_items/create_service.rb +++ b/app/services/work_items/create_service.rb @@ -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 diff --git a/app/services/work_items/widgets/milestone_service/base_service.rb b/app/services/work_items/widgets/milestone_service/base_service.rb new file mode 100644 index 00000000000..f373e6daea3 --- /dev/null +++ b/app/services/work_items/widgets/milestone_service/base_service.rb @@ -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 diff --git a/app/services/work_items/widgets/milestone_service/create_service.rb b/app/services/work_items/widgets/milestone_service/create_service.rb new file mode 100644 index 00000000000..e8d6bfe503c --- /dev/null +++ b/app/services/work_items/widgets/milestone_service/create_service.rb @@ -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 diff --git a/app/services/work_items/widgets/milestone_service/update_service.rb b/app/services/work_items/widgets/milestone_service/update_service.rb new file mode 100644 index 00000000000..7ff0c2a5367 --- /dev/null +++ b/app/services/work_items/widgets/milestone_service/update_service.rb @@ -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 diff --git a/db/migrate/20221015000511_add_email_confirmation_setting_to_application_settings.rb b/db/migrate/20221015000511_add_email_confirmation_setting_to_application_settings.rb new file mode 100644 index 00000000000..42fa4c1baf5 --- /dev/null +++ b/db/migrate/20221015000511_add_email_confirmation_setting_to_application_settings.rb @@ -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 diff --git a/db/schema_migrations/20221015000511 b/db/schema_migrations/20221015000511 new file mode 100644 index 00000000000..16845f8859c --- /dev/null +++ b/db/schema_migrations/20221015000511 @@ -0,0 +1 @@ +001b43cc0006b8f936310171ff2d12993eece1378f64945e6835728f540815ba
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 653f4ffae7c..570417ccbb6 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -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)), diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 32b9c28b6e3..8555d8c6789 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -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 diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 9bbe17dcad1..a5eab9aae8c 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -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 diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index f11437552e1..8934067551c 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -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 diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml index b2a593397e4..2d9c8d1108e 100644 --- a/lib/gitlab/import_export/project/import_export.yml +++ b/lib/gitlab/import_export/project/import_export.yml @@ -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 diff --git a/lib/sidebars/groups/menus/settings_menu.rb b/lib/sidebars/groups/menus/settings_menu.rb index df170670aab..ede195a8e59 100644 --- a/lib/sidebars/groups/menus/settings_menu.rb +++ b/lib/sidebars/groups/menus/settings_menu.rb @@ -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 diff --git a/lib/sidebars/projects/menus/infrastructure_menu.rb b/lib/sidebars/projects/menus/infrastructure_menu.rb index 2181d89262b..a8ac3d10f83 100644 --- a/lib/sidebars/projects/menus/infrastructure_menu.rb +++ b/lib/sidebars/projects/menus/infrastructure_menu.rb @@ -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) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ed4ef44375e..4bbb0913375 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/qa/Gemfile b/qa/Gemfile index 92023bee998..61904642387 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -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' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 5e6b1531aff..f641e6e112b 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -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) diff --git a/rubocop/cop/gitlab/feature_available_usage.rb b/rubocop/cop/gitlab/feature_available_usage.rb index 4dba4baf1e7..fcf4992a19d 100644 --- a/rubocop/cop/gitlab/feature_available_usage.rb +++ b/rubocop/cop/gitlab/feature_available_usage.rb @@ -27,6 +27,7 @@ module RuboCop environments feature_flags releases + infrastructure ].freeze EE_FEATURES = %i[requirements].freeze ALL_FEATURES = (FEATURES + EE_FEATURES).freeze diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index b5797e374f3..446e5e38865 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -921,6 +921,7 @@ RSpec.describe ProjectsController do feature_flags_access_level releases_access_level monitor_access_level + infrastructure_access_level ] end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 61a3e5b7f79..3a25d179e84 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -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 diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 3474d4c3907..25ea8743b53 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -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 diff --git a/spec/frontend/admin/signup_restrictions/mock_data.js b/spec/frontend/admin/signup_restrictions/mock_data.js index 9e001e122a4..62e931d9163 100644 --- a/spec/frontend/admin/signup_restrictions/mock_data.js +++ b/spec/frontend/admin/signup_restrictions/mock_data.js @@ -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', diff --git a/spec/lib/gitlab/database/migrations/runner_spec.rb b/spec/lib/gitlab/database/migrations/runner_spec.rb index f364ebfa522..6283cb58b47 100644 --- a/spec/lib/gitlab/database/migrations/runner_spec.rb +++ b/spec/lib/gitlab/database/migrations/runner_spec.rb @@ -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 diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 1ee2fbcec29..64fed73303f 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -592,6 +592,7 @@ ProjectFeature: - feature_flags_access_level - releases_access_level - monitor_access_level +- infrastructure_access_level - created_at - updated_at ProtectedBranch::MergeAccessLevel: diff --git a/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb b/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb index 2da7d324708..64408ac3b88 100644 --- a/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb @@ -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 diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb index 89f34834aa4..f168bedc8eb 100644 --- a/spec/models/concerns/project_features_compatibility_spec.rb +++ b/spec/models/concerns/project_features_compatibility_spec.rb @@ -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 diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index bf98566fdcc..88d96a812e0 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -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) } diff --git a/spec/models/work_items/type_spec.rb b/spec/models/work_items/type_spec.rb index e41df7f0f61..aa18e00f63b 100644 --- a/spec/models/work_items/type_spec.rb +++ b/spec/models/work_items/type_spec.rb @@ -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 diff --git a/spec/models/work_items/widgets/milestone_spec.rb b/spec/models/work_items/widgets/milestone_spec.rb new file mode 100644 index 00000000000..7b2d661df29 --- /dev/null +++ b/spec/models/work_items/widgets/milestone_spec.rb @@ -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 diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 40ee2e662b2..bf361c00374 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -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 diff --git a/spec/requests/api/graphql/mutations/work_items/create_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_spec.rb index 8233821053f..2fd4c5e7b35 100644 --- a/spec/requests/api/graphql/mutations/work_items/create_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/create_spec.rb @@ -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) 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 6b0129c457f..a6d4f518f5b 100644 --- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb @@ -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) } diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb index e82f6ad24a2..3c41950d578 100644 --- a/spec/requests/api/graphql/project/work_items_spec.rb +++ b/spec/requests/api/graphql/project/work_items_spec.rb @@ -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 diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb index 2105e479ed2..08333824c05 100644 --- a/spec/requests/api/graphql/work_item_spec.rb +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -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 diff --git a/spec/services/work_items/widgets/milestone_service/create_service_spec.rb b/spec/services/work_items/widgets/milestone_service/create_service_spec.rb new file mode 100644 index 00000000000..3f90784b703 --- /dev/null +++ b/spec/services/work_items/widgets/milestone_service/create_service_spec.rb @@ -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 diff --git a/spec/services/work_items/widgets/milestone_service/update_service_spec.rb b/spec/services/work_items/widgets/milestone_service/update_service_spec.rb new file mode 100644 index 00000000000..f3a7fc156b9 --- /dev/null +++ b/spec/services/work_items/widgets/milestone_service/update_service_spec.rb @@ -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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8a1fa486bde..6761e100cfe 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -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 diff --git a/spec/support/database/multiple_databases.rb b/spec/support/helpers/database/multiple_databases_helpers.rb index b6341c2caec..16f5168ca29 100644 --- a/spec/support/database/multiple_databases.rb +++ b/spec/support/helpers/database/multiple_databases_helpers.rb @@ -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 diff --git a/spec/support/migration.rb b/spec/support/migration.rb index 490aa836d74..2a69630a29a 100644 --- a/spec/support/migration.rb +++ b/spec/support/migration.rb @@ -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 diff --git a/spec/support/multiple_databases.rb b/spec/support/multiple_databases.rb new file mode 100644 index 00000000000..616cf00269c --- /dev/null +++ b/spec/support/multiple_databases.rb @@ -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 diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb index 1fa8ccf4b55..be7e19492d7 100644 --- a/spec/support/shared_examples/models/wiki_shared_examples.rb +++ b/spec/support/shared_examples/models/wiki_shared_examples.rb @@ -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 diff --git a/spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb b/spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb new file mode 100644 index 00000000000..ac17915c15a --- /dev/null +++ b/spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb @@ -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 diff --git a/spec/support_specs/database/multiple_databases_spec.rb b/spec/support_specs/database/multiple_databases_helpers_spec.rb index 0b019462077..eb0e980d376 100644 --- a/spec/support_specs/database/multiple_databases_spec.rb +++ b/spec/support_specs/database/multiple_databases_helpers_spec.rb @@ -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 |