diff options
40 files changed, 518 insertions, 113 deletions
diff --git a/.rubocop_todo/rspec/feature_category.yml b/.rubocop_todo/rspec/feature_category.yml index c66b1c9a563..c5ee969c61f 100644 --- a/.rubocop_todo/rspec/feature_category.yml +++ b/.rubocop_todo/rspec/feature_category.yml @@ -3368,7 +3368,6 @@ RSpec/FeatureCategory: - 'spec/lib/gitlab/etag_caching/router/rails_spec.rb' - 'spec/lib/gitlab/etag_caching/router_spec.rb' - 'spec/lib/gitlab/etag_caching/store_spec.rb' - - 'spec/lib/gitlab/event_store/event_spec.rb' - 'spec/lib/gitlab/event_store/store_spec.rb' - 'spec/lib/gitlab/exception_log_formatter_spec.rb' - 'spec/lib/gitlab/exceptions_app_spec.rb' diff --git a/app/controllers/concerns/onboarding/redirectable.rb b/app/controllers/concerns/onboarding/redirectable.rb index 7e669db9199..15c1847ebe4 100644 --- a/app/controllers/concerns/onboarding/redirectable.rb +++ b/app/controllers/concerns/onboarding/redirectable.rb @@ -9,7 +9,7 @@ module Onboarding def after_sign_up_path if onboarding_status.single_invite? flash[:notice] = helpers.invite_accepted_notice(onboarding_status.last_invited_member) - onboarding_status.last_invited_member_source.activity_path + polymorphic_path(onboarding_status.last_invited_member_source) else # Invites will come here if there is more than 1. path_for_signed_in_user @@ -17,13 +17,13 @@ module Onboarding end def path_for_signed_in_user - stored_location_for(:user) || last_member_activity_path + stored_location_for(:user) || last_member_source_path end - def last_member_activity_path + def last_member_source_path return dashboard_projects_path unless onboarding_status.last_invited_member_source.present? - onboarding_status.last_invited_member_source.activity_path + polymorphic_path(onboarding_status.last_invited_member_source) end end end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index c058329680a..fcd87f46f67 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -125,14 +125,14 @@ class InvitesController < ApplicationController name: member.source.full_name, url: project_url(member.source), title: _("project"), - path: member.source.activity_path + path: project_path(member.source) } when Group { name: member.source.name, url: group_url(member.source), title: _("group"), - path: member.source.activity_path + path: group_path(member.source) } end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 595d79abcf2..a8a09bd6ac6 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -100,7 +100,7 @@ class SessionsController < Devise::SessionsController def after_pending_invitations_hook member = resource.members.last - store_location_for(:user, member.source.activity_path) if member + store_location_for(:user, polymorphic_path(member.source)) if member end def captcha_enabled? diff --git a/app/models/group.rb b/app/models/group.rb index 7092d5217e1..215d6624fd3 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -790,10 +790,6 @@ class Group < Namespace end strong_memoize_attr :has_project_with_service_desk_enabled? - def activity_path - Gitlab::Routing.url_helpers.activity_group_path(self) - end - # rubocop: disable CodeReuse/ServiceClass def open_issues_count(current_user = nil) Groups::OpenIssuesCountService.new(self, current_user).count diff --git a/app/models/project.rb b/app/models/project.rb index 5788885498c..e9403728e54 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2992,10 +2992,6 @@ class Project < ApplicationRecord Projects::GitGarbageCollectWorker end - def activity_path - Gitlab::Routing.url_helpers.activity_project_path(self) - end - def ci_forward_deployment_enabled? return false unless ci_cd_settings diff --git a/app/views/groups/_import_group_from_another_instance_panel.html.haml b/app/views/groups/_import_group_from_another_instance_panel.html.haml index 6c5a27e68c4..9cf52115f4f 100644 --- a/app/views/groups/_import_group_from_another_instance_panel.html.haml +++ b/app/views/groups/_import_group_from_another_instance_panel.html.haml @@ -27,16 +27,19 @@ - docs_link = link_to('', help_page_path('user/group/import/index', anchor: 'migrated-group-items'), target: '_blank', rel: 'noopener noreferrer') = safe_format(s_('GroupsNew|Not all group items are migrated. %{docs_link_start}What items are migrated%{docs_link_end}?'), tag_pair(docs_link, :docs_link_start, :docs_link_end)) - %p.gl-mt-3 - = s_('GroupsNew|Provide credentials for the source instance to import from. You can provide this instance as a source to move groups in this instance.') - .form-group.gl-display-flex.gl-flex-direction-column - = f.label :bulk_import_gitlab_url, s_('GroupsNew|GitLab source instance URL'), for: 'import_gitlab_url' + %p.gl-mt-5.gl-mb-3 + - url_link = link_to('', help_page_path('user/group/import/index', anchor: 'connect-the-source-gitlab-instance'), target: '_blank', rel: 'noopener noreferrer') + = safe_format(s_('GroupsNew|Provide credentials for the %{url_link_start}source instance%{url_link_end} to import from. You can provide this instance as a source to move groups within this instance.'), tag_pair(url_link, :url_link_start, :url_link_end)) + .form-group.gl-form-group.gl-display-flex.gl-flex-direction-column + = f.label :bulk_import_gitlab_url, s_('GroupsNew|GitLab source instance base URL'), for: 'import_gitlab_url' = f.text_field :bulk_import_gitlab_url, disabled: bulk_imports_disabled, placeholder: 'https://gitlab.example.com', class: 'gl-form-input col-xs-12 col-sm-8', required: true, title: s_('GroupsNew|Enter the URL for the source instance.'), id: 'import_gitlab_url', data: { testid: 'import-gitlab-url' } - .form-group.gl-display-flex.gl-flex-direction-column + %small.form-text.text-gl-muted + = s_('Import|Must only contain the base URL of the source GitLab instance.') + .form-group.gl-form-group.gl-display-flex.gl-flex-direction-column = f.label :bulk_import_gitlab_access_token, s_('GroupsNew|Personal access token'), for: 'import_gitlab_token', class: 'col-form-label' .gl-font-weight-normal - pat_link = link_to('', help_page_path('user/profile/personal_access_tokens'), target: '_blank') diff --git a/config/feature_flags/development/duo_chat_absolute_doc_links.yml b/config/feature_flags/development/duo_chat_absolute_doc_links.yml deleted file mode 100644 index 56c894beb78..00000000000 --- a/config/feature_flags/development/duo_chat_absolute_doc_links.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: duo_chat_absolute_doc_links -introduced_by_url: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136240' -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/431338 -milestone: '16.6' -type: development -group: group::ai framework -default_enabled: false diff --git a/config/feature_flags/development/highlight_js_worker.yml b/config/feature_flags/development/highlight_js_worker.yml index ee74cbb7004..7086ace38e6 100644 --- a/config/feature_flags/development/highlight_js_worker.yml +++ b/config/feature_flags/development/highlight_js_worker.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/415755 milestone: '16.2' type: development group: group::source code -default_enabled: false +default_enabled: true diff --git a/db/post_migrate/20231128103624_add_unique_id_partition_id_index_to_ci_job_artifact.rb b/db/post_migrate/20231128103624_add_unique_id_partition_id_index_to_ci_job_artifact.rb new file mode 100644 index 00000000000..504864f75c9 --- /dev/null +++ b/db/post_migrate/20231128103624_add_unique_id_partition_id_index_to_ci_job_artifact.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddUniqueIdPartitionIdIndexToCiJobArtifact < Gitlab::Database::Migration[2.2] + milestone '16.7' + TABLE_NAME = :ci_job_artifacts + INDEX_NAME = :index_ci_job_artifacts_on_id_partition_id_unique + COLUMNS = %i[id partition_id] + + def up + prepare_async_index(TABLE_NAME, COLUMNS, unique: true, name: INDEX_NAME) + end + + def down + unprepare_async_index(TABLE_NAME, COLUMNS, name: INDEX_NAME) + end +end diff --git a/db/post_migrate/20231128104044_add_unique_job_id_filte_type_partition_id_index_to_ci_job_artifact.rb b/db/post_migrate/20231128104044_add_unique_job_id_filte_type_partition_id_index_to_ci_job_artifact.rb new file mode 100644 index 00000000000..c667dce511f --- /dev/null +++ b/db/post_migrate/20231128104044_add_unique_job_id_filte_type_partition_id_index_to_ci_job_artifact.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddUniqueJobIdFilteTypePartitionIdIndexToCiJobArtifact < Gitlab::Database::Migration[2.2] + milestone '16.7' + + TABLE_NAME = :ci_job_artifacts + INDEX_NAME = :idx_ci_job_artifacts_on_job_id_file_type_and_partition_id_uniq + COLUMNS = %i[job_id file_type partition_id] + + def up + prepare_async_index(TABLE_NAME, COLUMNS, unique: true, name: INDEX_NAME) + end + + def down + unprepare_async_index(TABLE_NAME, COLUMNS, name: INDEX_NAME) + end +end diff --git a/db/post_migrate/20231128111550_add_async_indexes_with_partition_id_for_ci_pipeline_variables.rb b/db/post_migrate/20231128111550_add_async_indexes_with_partition_id_for_ci_pipeline_variables.rb new file mode 100644 index 00000000000..9539bbcca31 --- /dev/null +++ b/db/post_migrate/20231128111550_add_async_indexes_with_partition_id_for_ci_pipeline_variables.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddAsyncIndexesWithPartitionIdForCiPipelineVariables < Gitlab::Database::Migration[2.2] + milestone '16.7' + + TABLE_NAME = :ci_pipeline_variables + PK_INDEX_NAME = :index_ci_pipeline_variables_on_id_partition_id_unique + UNIQUE_INDEX_NAME = :index_pipeline_variables_on_pipeline_id_key_partition_id_unique + + def up + prepare_async_index TABLE_NAME, %i[id partition_id], name: PK_INDEX_NAME, unique: true + prepare_async_index TABLE_NAME, %i[pipeline_id key partition_id], name: UNIQUE_INDEX_NAME, unique: true + end + + def down + unprepare_async_index TABLE_NAME, %i[id partition_id], name: PK_INDEX_NAME, unique: true + unprepare_async_index TABLE_NAME, %i[pipeline_id key partition_id], name: UNIQUE_INDEX_NAME, unique: true + end +end diff --git a/db/schema_migrations/20231128103624 b/db/schema_migrations/20231128103624 new file mode 100644 index 00000000000..1502eda2f77 --- /dev/null +++ b/db/schema_migrations/20231128103624 @@ -0,0 +1 @@ +9cfcd48c86956f9f1a0429ab4a2b9f772b7cd6f2e7ac325bb8b1acbbe6ba4ed6
\ No newline at end of file diff --git a/db/schema_migrations/20231128104044 b/db/schema_migrations/20231128104044 new file mode 100644 index 00000000000..8af740468a6 --- /dev/null +++ b/db/schema_migrations/20231128104044 @@ -0,0 +1 @@ +856be6ee89a0e0c4042539ffff10aa410dbfb59bff43527482af5817a20e20cc
\ No newline at end of file diff --git a/db/schema_migrations/20231128111550 b/db/schema_migrations/20231128111550 new file mode 100644 index 00000000000..e906f189a3a --- /dev/null +++ b/db/schema_migrations/20231128111550 @@ -0,0 +1 @@ +a076623d4d7c2f475f1c712802288ef8bdd0c830798dd27d7397da63065b6639
\ No newline at end of file diff --git a/doc/administration/object_storage.md b/doc/administration/object_storage.md index 0b12d41eb4e..da0aa426f5e 100644 --- a/doc/administration/object_storage.md +++ b/doc/administration/object_storage.md @@ -157,7 +157,7 @@ For the storage-specific form, because it does not require a shared folder. For configuring object storage in GitLab 13.1 and earlier, _or_ for storage types not -For storage types not supported by the consolidated form, refer to the following guides: +supported by the consolidated form, refer to the following guides: | Object storage type | Supported by consolidated form? | |---------------------|------------------------------------------| diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 4a51ce36bef..714b9b8395f 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -72,6 +72,23 @@ four standard [pagination arguments](#connection-pagination-arguments): | ---- | ---- | ----------- | | <a id="queryabusereportlabelssearchterm"></a>`searchTerm` | [`String`](#string) | Search term to find labels with. | +### `Query.addOnPurchase` + +Retrieve the active add-on purchase. This query can be used in GitLab SaaS and self-managed environments. + +WARNING: +**Introduced** in 16.7. +This feature is an Experiment. It can be changed or removed at any time. + +Returns [`AddOnPurchase`](#addonpurchase). + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="queryaddonpurchaseaddontype"></a>`addOnType` | [`GitlabSubscriptionsAddOnType!`](#gitlabsubscriptionsaddontype) | Type of add-on for the add-on purchase. | +| <a id="queryaddonpurchasenamespaceid"></a>`namespaceId` | [`NamespaceID`](#namespaceid) | ID of namespace that the add-on was purchased for. | + ### `Query.aiMessages` Find GitLab Duo Chat messages. diff --git a/doc/ci/runners/saas/macos_saas_runner.md b/doc/ci/runners/saas/macos_saas_runner.md index b503fea4f2f..8978d45a12f 100644 --- a/doc/ci/runners/saas/macos_saas_runner.md +++ b/doc/ci/runners/saas/macos_saas_runner.md @@ -36,11 +36,11 @@ GitLab SaaS provides a set of VM images for macOS. You can execute your build in one of the following images, which you specify in your `.gitlab-ci.yml` file. Each image runs a specific version of macOS and Xcode. -| VM image | Status | -|----------------------------|--------| -| `macos-12-xcode-14` | `GA` | -| `macos-13-xcode-14` | `GA` | -| `macos-14-xcode-15` | `Beta` | +| VM image | Status | | +|----------------------------|--------|--------------| +| `macos-12-xcode-14` | `GA` | | +| `macos-13-xcode-14` | `GA` | [Preinstalled Software](https://gitlab.com/gitlab-org/ci-cd/shared-runners/images/job-images/-/blob/main/toolchain/macos-13.yml) | +| `macos-14-xcode-15` | `Beta` | [Preinstalled Software](https://gitlab.com/gitlab-org/ci-cd/shared-runners/images/job-images/-/blob/main/toolchain/macos-14.yml) | If no image is specified, the macOS runner uses `macos-13-xcode-14`. @@ -48,7 +48,7 @@ If no image is specified, the macOS runner uses `macos-13-xcode-14`. macOS and Xcode follow a yearly release cadence, during which GitLab increments its versions synchronously. GitLab typically supports multiple versions of preinstalled tools. For more information, see the [full list of preinstalled software](https://gitlab.com/gitlab-org/ci-cd/shared-runners/images/job-images/-/tree/main/toolchain). -When Apple releases a new macOS version, GitLab releases a new `stable` image based on the OS in the next release, +When Apple releases a new macOS version, GitLab releases a new `stable` image based on the OS in the next release, which is in Beta. With the release of the first patch to macOS, the `stable` image becomes Generally Available (GA). As only two GA images are supported at a time, the prior OS version becomes deprecated and is deleted after three months in accordance with the [supported image lifecycle](../index.md#supported-image-lifecycle). diff --git a/doc/development/audit_event_guide/index.md b/doc/development/audit_event_guide/index.md index c540680c2f8..87896120cce 100644 --- a/doc/development/audit_event_guide/index.md +++ b/doc/development/audit_event_guide/index.md @@ -199,7 +199,7 @@ In addition to recording to the database, we also write these events to > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/367847) in GitLab 15.4. -All new audit events must have a type definition stored in `config/audit_events/types/` that contains a single source of truth for every auditable event in GitLab. +All new audit events must have a type definition stored in `config/audit_events/types/` or `ee/config/audit_events/types/` that contains a single source of truth for every auditable event in GitLab. ### Add a new audit event type diff --git a/doc/development/permissions/custom_roles.md b/doc/development/permissions/custom_roles.md index 52c561038b7..9d02c0c6f39 100644 --- a/doc/development/permissions/custom_roles.md +++ b/doc/development/permissions/custom_roles.md @@ -182,6 +182,7 @@ security dashboard. To add a new ability to a custom role: +- Generate YAML file by running `./ee/bin/custom-ability` generator - Add a new column to `member_roles` table, for example in [this change in merge request 114734](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114734/diffs#diff-content-5c53d6f1c29a272a87eecea3f62d017ab6635275). - Add the ability to the `MemberRole` model, `ALL_CUSTOMIZABLE_PERMISSIONS` hash, for example in [this change in merge request 121534](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121534/diffs#ce5ec769500a53ce2b603467d9984fc2b33ca71d_8_8). There are following possible keys in the `ALL_CUSTOMIZABLE_PERMISSIONS` hash: @@ -200,6 +201,33 @@ Examples of merge requests adding new abilities to custom roles: You should make sure a new custom roles ability is under a feature flag. +## Custom abilities definition + +All new custom abilities must have a type definition stored in `ee/config/custom_abilities` that contains a single source of truth for every ability that is part of custom roles feature. + +### Add a new custom ability definition + +To add a new custom ability: + +1. Create the YAML definition. You can either: + - Use the `ee/bin/custom-ability` CLI to create the YAML definition automatically. + - Perform manual steps to create a new file in `ee/config/custom_abilities/` with the filename matching the name of the ability name. +1. Add contents to the file that conform to the [schema](#schema) defined in `ee/config/custom_abilities/types/type_schema.json`. + +### Schema + +| Field | Required | Description | +| ----- | -------- |--------------| +| `name` | yes | Unique, lowercase and underscored name describing the custom ability. Must match the filename. | +| `description` | yes | Human-readable description of the custom ability. | +| `feature_category` | yes | Name of the feature category. For example, `vulnerability_management`. | +| `introduced_by_issue` | yes | Issue URL that proposed the addition of this custom ability. | +| `introduced_by_mr` | yes | MR URL that added this custom ability. | +| `milestone` | yes | Milestone in which this custom ability was added. | +| `group_ability` | yes | Indicate whether this ability is checked on group level. | +| `project_ability` | yes | Indicate whether this ability is checked on project level. | +| `skip_seat_consumption` | yes | Indicate wheter this ability should be skiped when counting licensed users. | + ### Privilege escalation consideration A base role typically has permissions that allow creation or management of artifacts corresponding to the base role when interacting with that artifact. For example, when a `Developer` creates an access token for a project, it is created with `Developer` access encoded into that credential. It is important to keep in mind that as new custom permissions are created, there might be a risk of elevated privileges when interacting with GitLab artifacts, and appropriate safeguards or base role checks should be added. diff --git a/doc/user/group/import/index.md b/doc/user/group/import/index.md index 3367c364676..d120f626944 100644 --- a/doc/user/group/import/index.md +++ b/doc/user/group/import/index.md @@ -201,7 +201,7 @@ Create the group you want to import to and connect the source GitLab instance: - A new subgroup. On existing group's page, either: - Select **New subgroup**. - On the left sidebar, at the top, select **Create new** (**{plus}**) and **New subgroup**. Then select the **import an existing group** link. -1. Enter the URL of a GitLab instance running GitLab 14.0 or later. +1. Enter the base URL of a GitLab instance running GitLab 14.0 or later. 1. Enter the [personal access token](../../../user/profile/personal_access_tokens.md) for your source GitLab instance. 1. Select **Connect instance**. diff --git a/lib/bulk_imports/clients/http.rb b/lib/bulk_imports/clients/http.rb index 6c2aa41c346..e22e37d66af 100644 --- a/lib/bulk_imports/clients/http.rb +++ b/lib/bulk_imports/clients/http.rb @@ -81,7 +81,7 @@ module BulkImports return true if response['scopes']&.include?('api') - raise ::BulkImports::Error.scope_validation_failure + raise ::BulkImports::Error.scope_or_url_validation_failure end def validate_instance_version! @@ -110,7 +110,7 @@ module BulkImports rescue BulkImports::NetworkError => e case e&.response&.code when 401, 403 - raise ::BulkImports::Error.scope_validation_failure + raise ::BulkImports::Error.scope_or_url_validation_failure when 404 raise ::BulkImports::Error.invalid_url else diff --git a/lib/bulk_imports/error.rb b/lib/bulk_imports/error.rb index c40b4bc7f34..57a55abafa6 100644 --- a/lib/bulk_imports/error.rb +++ b/lib/bulk_imports/error.rb @@ -3,35 +3,37 @@ module BulkImports class Error < StandardError def self.unsupported_gitlab_version - self.new("Unsupported GitLab version. Minimum supported version is #{BulkImport::MIN_MAJOR_VERSION}.") + self.new(format(s_("BulkImport|Unsupported GitLab version. Minimum supported version is '%{version}'."), + version: BulkImport::MIN_MAJOR_VERSION)) end - def self.scope_validation_failure - self.new("Personal access token does not have the required " \ - "'api' scope or is no longer valid.") + def self.scope_or_url_validation_failure + self.new(s_("BulkImport|Check that the source instance base URL and the personal access " \ + "token meet the necessary requirements.")) end def self.invalid_url - self.new("Invalid source URL. Enter only the base URL of the source GitLab instance.") + self.new(s_("BulkImport|Invalid source URL. Enter only the base URL of the source GitLab instance.")) end def self.destination_namespace_validation_failure(destination_namespace) - self.new("Import failed. Destination '#{destination_namespace}' is invalid, or you don't have permission.") + self.new(format(s_("BulkImport|Import failed. Destination '%{destination}' is invalid, " \ + "or you don't have permission."), destination: destination_namespace)) end def self.destination_slug_validation_failure - self.new("Import failed. Destination URL " \ - "#{Gitlab::Regex.oci_repository_path_regex_message}") + self.new(format(s_("BulkImport|Import failed. Destination URL %{url}"), + url: Gitlab::Regex.oci_repository_path_regex_message)) end def self.destination_full_path_validation_failure(full_path) - self.new("Import failed. '#{full_path}' already exists. Change the destination and try again.") + self.new(format(s_("BulkImport|Import failed. '%{path}' already exists. Change the destination and try again."), + path: full_path)) end def self.setting_not_enabled - self.new("Group import disabled on source or destination instance. " \ - "Ask an administrator to enable it on both instances and try again." - ) + self.new(s_("BulkImport|Group import disabled on source or destination instance. " \ + "Ask an administrator to enable it on both instances and try again.")) end end end diff --git a/lib/gitlab/event_store/event.rb b/lib/gitlab/event_store/event.rb index ee0c329b8e8..ba82ae6dd6a 100644 --- a/lib/gitlab/event_store/event.rb +++ b/lib/gitlab/event_store/event.rb @@ -29,8 +29,13 @@ module Gitlab class Event attr_reader :data + class << self + attr_accessor :json_schema_valid + end + def initialize(data:) - validate_schema!(data) + validate_schema! + validate_data!(data) @data = data end @@ -40,7 +45,17 @@ module Gitlab private - def validate_schema!(data) + def validate_schema! + if self.class.json_schema_valid.nil? + self.class.json_schema_valid = JSONSchemer.schema(self.class.json_schema).valid?(schema) + end + + return if self.class.json_schema_valid == true + + raise Gitlab::EventStore::InvalidEvent, "Schema for event #{self.class} is invalid" + end + + def validate_data!(data) unless data.is_a?(Hash) raise Gitlab::EventStore::InvalidEvent, "Event data must be a Hash" end @@ -49,6 +64,10 @@ module Gitlab raise Gitlab::EventStore::InvalidEvent, "Data for event #{self.class} does not match the defined schema: #{schema}" end end + + def self.json_schema + @json_schema ||= Gitlab::Json.parse(File.read(File.join(__dir__, 'json_schema_draft07.json'))) + end end end end diff --git a/lib/gitlab/event_store/json_schema_draft07.json b/lib/gitlab/event_store/json_schema_draft07.json new file mode 100644 index 00000000000..aea0a29c4dc --- /dev/null +++ b/lib/gitlab/event_store/json_schema_draft07.json @@ -0,0 +1,250 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://json-schema.org/draft-07/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#" + } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { + "$ref": "#/definitions/nonNegativeInteger" + }, + { + "default": 0 + } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true, + "default": [ + + ] + } + }, + "type": [ + "object", + "boolean" + ], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "$comment": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { + "$ref": "#/definitions/nonNegativeInteger" + }, + "minLength": { + "$ref": "#/definitions/nonNegativeIntegerDefault0" + }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { + "$ref": "#" + }, + "items": { + "anyOf": [ + { + "$ref": "#" + }, + { + "$ref": "#/definitions/schemaArray" + } + ], + "default": true + }, + "maxItems": { + "$ref": "#/definitions/nonNegativeInteger" + }, + "minItems": { + "$ref": "#/definitions/nonNegativeIntegerDefault0" + }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { + "$ref": "#" + }, + "maxProperties": { + "$ref": "#/definitions/nonNegativeInteger" + }, + "minProperties": { + "$ref": "#/definitions/nonNegativeIntegerDefault0" + }, + "required": { + "$ref": "#/definitions/stringArray" + }, + "additionalProperties": { + "$ref": "#" + }, + "definitions": { + "type": "object", + "additionalProperties": { + "$ref": "#" + }, + "default": { + } + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#" + }, + "default": { + } + }, + "patternProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#" + }, + "propertyNames": { + "format": "regex" + }, + "default": { + } + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#" + }, + { + "$ref": "#/definitions/stringArray" + } + ] + } + }, + "propertyNames": { + "$ref": "#" + }, + "const": true, + "enum": { + "type": "array", + "items": true, + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { + "$ref": "#/definitions/simpleTypes" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/simpleTypes" + }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { + "type": "string" + }, + "contentMediaType": { + "type": "string" + }, + "contentEncoding": { + "type": "string" + }, + "if": { + "$ref": "#" + }, + "then": { + "$ref": "#" + }, + "else": { + "$ref": "#" + }, + "allOf": { + "$ref": "#/definitions/schemaArray" + }, + "anyOf": { + "$ref": "#/definitions/schemaArray" + }, + "oneOf": { + "$ref": "#/definitions/schemaArray" + }, + "not": { + "$ref": "#" + } + }, + "default": true +} diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7ae8c2737b3..f1e69d63ddd 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9020,6 +9020,9 @@ msgstr "" msgid "BulkImport|Be aware of %{linkStart}visibility rules%{linkEnd} when importing groups." msgstr "" +msgid "BulkImport|Check that the source instance base URL and the personal access token meet the necessary requirements." +msgstr "" + msgid "BulkImport|Destination" msgstr "" @@ -9035,9 +9038,21 @@ msgstr "" msgid "BulkImport|GitLab Migration history" msgstr "" +msgid "BulkImport|Group import disabled on source or destination instance. Ask an administrator to enable it on both instances and try again." +msgstr "" + msgid "BulkImport|History" msgstr "" +msgid "BulkImport|Import failed. '%{path}' already exists. Change the destination and try again." +msgstr "" + +msgid "BulkImport|Import failed. Destination '%{destination}' is invalid, or you don't have permission." +msgstr "" + +msgid "BulkImport|Import failed. Destination URL %{url}" +msgstr "" + msgid "BulkImport|Import failed: Destination cannot be a subgroup of the source group. Change the destination and try again." msgstr "" @@ -9059,6 +9074,9 @@ msgstr "" msgid "BulkImport|Importing the group failed." msgstr "" +msgid "BulkImport|Invalid source URL. Enter only the base URL of the source GitLab instance." +msgstr "" + msgid "BulkImport|Last imported to %{link}" msgstr "" @@ -9116,6 +9134,9 @@ msgstr "" msgid "BulkImport|Template / File-based import / GitLab Migration" msgstr "" +msgid "BulkImport|Unsupported GitLab version. Minimum supported version is '%{version}'." +msgstr "" + msgid "BulkImport|Update of import statuses with realtime changes failed" msgstr "" @@ -23555,7 +23576,7 @@ msgstr "" msgid "GroupsNew|Enter the URL for the source instance." msgstr "" -msgid "GroupsNew|GitLab source instance URL" +msgid "GroupsNew|GitLab source instance base URL" msgstr "" msgid "GroupsNew|Groups" @@ -23597,7 +23618,7 @@ msgstr "" msgid "GroupsNew|Please fill in your personal access token." msgstr "" -msgid "GroupsNew|Provide credentials for the source instance to import from. You can provide this instance as a source to move groups in this instance." +msgid "GroupsNew|Provide credentials for the %{url_link_start}source instance%{url_link_end} to import from. You can provide this instance as a source to move groups within this instance." msgstr "" msgid "GroupsNew|Remember to enable it also on the instance you are migrating from." @@ -24901,6 +24922,9 @@ msgstr "" msgid "Import|Maximum size of decompressed archive." msgstr "" +msgid "Import|Must only contain the base URL of the source GitLab instance." +msgstr "" + msgid "Import|No import details" msgstr "" @@ -56317,6 +56341,9 @@ msgstr "" msgid "Your public email will be displayed on your public profile." msgstr "" +msgid "Your push to this repository has been rejected because it would exceed the namespace storage limit of %{size_limit}. Reduce your namespace storage or purchase additional storage.To manage storage, or purchase additional storage, see %{manage_storage_url}. To learn more about restricted actions, see %{restricted_actions_url}" +msgstr "" + msgid "Your request for access could not be processed: %{error_message}" msgstr "" diff --git a/spec/features/groups/import_export/connect_instance_spec.rb b/spec/features/groups/import_export/connect_instance_spec.rb index f6548c035f0..f3c89cce633 100644 --- a/spec/features/groups/import_export/connect_instance_spec.rb +++ b/spec/features/groups/import_export/connect_instance_spec.rb @@ -89,7 +89,7 @@ RSpec.describe 'Import/Export - Connect to another instance', :js, feature_categ end it 'renders fields and button disabled' do - expect(page).to have_field('GitLab source instance URL', disabled: true) + expect(page).to have_field('GitLab source instance base URL', disabled: true) expect(page).to have_field('Personal access token', disabled: true) expect(page).to have_button('Connect instance', disabled: true) end diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb index bf4aef66c03..015544e1a11 100644 --- a/spec/features/invites_spec.rb +++ b/spec/features/invites_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_category: :acquisition do let_it_be(:owner) { create(:user, name: 'John Doe') } - # private will ensure we really have access to the group when we land on the activity page + # private will ensure we really have access to the group when we land on the group page let_it_be(:group) { create(:group, :private, name: 'Owned') } let_it_be(:project) { create(:project, :repository, namespace: group) } @@ -60,12 +60,12 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_cate visit invite_path(group_invite.raw_invite_token, invite_type: Emails::Members::INITIAL_INVITE) end - it 'sign in, grants access and redirects to group activity page' do + it 'sign in, grants access and redirects to group page' do click_link 'Sign in' gitlab_sign_in(user, remember: true, visit: false) - expect_to_be_on_group_activity_page(group) + expect_to_be_on_group_page(group) end end @@ -126,8 +126,8 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_cate end end - def expect_to_be_on_group_activity_page(group) - expect(page).to have_current_path(activity_group_path(group)) + def expect_to_be_on_group_page(group) + expect(page).to have_current_path(group_path(group)) end end end @@ -169,10 +169,10 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_cate end context 'when the user signs up for an account with the invitation email address' do - it 'redirects to the most recent membership activity page with all invitations automatically accepted' do + it 'redirects to the most recent membership group page with all invitations automatically accepted' do fill_in_sign_up_form(new_user) - expect(page).to have_current_path(activity_group_path(group), ignore_query: true) + expect(page).to have_current_path(group_path(group), ignore_query: true) expect(page).to have_content('You have been granted Owner access to group Owned.') end end @@ -215,10 +215,10 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_cate end context 'when the user signs up for an account with the invitation email address' do - it 'redirects to the most recent membership activity page with all invitations automatically accepted' do + it 'redirects to the most recent membership group page with all invitations automatically accepted' do fill_in_sign_up_form(new_user) - expect(page).to have_current_path(activity_group_path(group), ignore_query: true) + expect(page).to have_current_path(group_path(group), ignore_query: true) end end @@ -271,7 +271,7 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_cate fill_in_sign_up_form(new_user, 'Register') - expect(page).to have_current_path(activity_group_path(group)) + expect(page).to have_current_path(group_path(group)) expect(page).to have_content('You have been granted Owner access to group Owned.') end end @@ -295,16 +295,16 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures, feature_cate gitlab_sign_in(user) end - it 'does not accept the pending invitation and does not redirect to the groups activity path' do - expect(page).not_to have_current_path(activity_group_path(group), ignore_query: true) + it 'does not accept the pending invitation and does not redirect to the group path' do + expect(page).not_to have_current_path(group_path(group), ignore_query: true) expect(group.reload.users).not_to include(user) end context 'when the secondary email address is confirmed' do let(:secondary_email) { create(:email, :confirmed, user: user) } - it 'accepts the pending invitation and redirects to the groups activity path' do - expect(page).to have_current_path(activity_group_path(group), ignore_query: true) + it 'accepts the pending invitation and redirects to the group path' do + expect(page).to have_current_path(group_path(group), ignore_query: true) expect(group.reload.users).to include(user) end end diff --git a/spec/features/registrations/oauth_registration_spec.rb b/spec/features/registrations/oauth_registration_spec.rb index 98300cbeaaa..eb21d285bd0 100644 --- a/spec/features/registrations/oauth_registration_spec.rb +++ b/spec/features/registrations/oauth_registration_spec.rb @@ -105,12 +105,12 @@ RSpec.describe 'OAuth Registration', :js, :allow_forgery_protection, feature_cat mock_auth_hash(provider, uid, invite_email, additional_info: additional_info) end - it 'redirects to the activity page with all the projects/groups invitations accepted' do + it 'redirects to the group page with all the projects/groups invitations accepted' do visit invite_path(group_invite.raw_invite_token, extra_params) click_link_or_button "oauth-login-#{provider}" expect(page).to have_content('You have been granted Owner access to group Owned.') - expect(page).to have_current_path(activity_group_path(group), ignore_query: true) + expect(page).to have_current_path(group_path(group), ignore_query: true) end end end diff --git a/spec/lib/bulk_imports/clients/http_spec.rb b/spec/lib/bulk_imports/clients/http_spec.rb index 08d0509b54f..2eceefe3091 100644 --- a/spec/lib/bulk_imports/clients/http_spec.rb +++ b/spec/lib/bulk_imports/clients/http_spec.rb @@ -250,9 +250,9 @@ RSpec.describe BulkImports::Clients::HTTP, feature_category: :importers do stub_request(:get, 'http://gitlab.example/api/v4/metadata?private_token=token') .to_return(status: 401, body: "", headers: { 'Content-Type' => 'application/json' }) - expect { subject.instance_version }.to raise_exception(BulkImports::Error, - "Personal access token does not have the required 'api' scope or " \ - "is no longer valid.") + expect { subject.instance_version } + .to raise_exception(BulkImports::Error, + "Check that the source instance base URL and the personal access token meet the necessary requirements.") end end @@ -262,9 +262,9 @@ RSpec.describe BulkImports::Clients::HTTP, feature_category: :importers do stub_request(:get, 'http://gitlab.example/api/v4/metadata?private_token=token') .to_return(status: 403, body: "", headers: { 'Content-Type' => 'application/json' }) - expect { subject.instance_version }.to raise_exception(BulkImports::Error, - "Personal access token does not have the required 'api' scope or " \ - "is no longer valid.") + expect { subject.instance_version } + .to raise_exception(BulkImports::Error, + "Check that the source instance base URL and the personal access token meet the necessary requirements.") end end diff --git a/spec/lib/gitlab/event_store/event_spec.rb b/spec/lib/gitlab/event_store/event_spec.rb index 97f6870a5ec..edcb0e5dd1a 100644 --- a/spec/lib/gitlab/event_store/event_spec.rb +++ b/spec/lib/gitlab/event_store/event_spec.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' +require 'json_schemer' +require 'oj' -RSpec.describe Gitlab::EventStore::Event do +RSpec.describe Gitlab::EventStore::Event, feature_category: :shared do let(:event_class) { stub_const('TestEvent', Class.new(described_class)) } let(:event) { event_class.new(data: data) } let(:data) { { project_id: 123, project_path: 'org/the-project' } } @@ -42,6 +44,14 @@ RSpec.describe Gitlab::EventStore::Event do it 'initializes the event correctly' do expect(event.data).to eq(data) end + + it 'validates schema' do + expect(event_class.json_schema_valid).to eq(nil) + + event + + expect(event_class.json_schema_valid).to eq(true) + end end context 'when some properties are missing' do @@ -59,6 +69,31 @@ RSpec.describe Gitlab::EventStore::Event do expect { event }.to raise_error(Gitlab::EventStore::InvalidEvent, 'Event data must be a Hash') end end + + context 'when schema is invalid' do + before do + event_class.class_eval do + def schema + { + 'required' => ['project_id'], + 'type' => 'object', + 'properties' => { + 'project_id' => { 'type' => 'int' }, + 'project_path' => { 'type' => 'string ' } + } + } + end + end + end + + it 'raises an error' do + expect(event_class.json_schema_valid).to eq(nil) + + expect { event }.to raise_error(Gitlab::EventStore::InvalidEvent, 'Schema for event TestEvent is invalid') + + expect(event_class.json_schema_valid).to eq(false) + end + end end end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 7a31067732f..0236ae9dc9e 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -3014,14 +3014,6 @@ RSpec.describe Group, feature_category: :groups_and_projects do end end - describe '#activity_path' do - it 'returns the group activity_path' do - expected_path = "/groups/#{group.name}/-/activity" - - expect(group.activity_path).to eq(expected_path) - end - end - context 'with export' do let(:group) { create(:group, :with_export) } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 64ed8811e00..81c8473cee0 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -7955,14 +7955,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr end end end - - describe '#activity_path' do - it 'returns the project activity_path' do - expected_path = "/#{project.full_path}/activity" - - expect(project.activity_path).to eq(expected_path) - end - end end describe '#default_branch_or_main' do diff --git a/spec/requests/sessions_spec.rb b/spec/requests/sessions_spec.rb index 3428e607305..12939acdd91 100644 --- a/spec/requests/sessions_spec.rb +++ b/spec/requests/sessions_spec.rb @@ -41,7 +41,7 @@ RSpec.describe 'Sessions', feature_category: :system_access do post user_session_path(user: { login: user.username, password: user.password }) - expect(response).to redirect_to(activity_group_path(member.source)) + expect(response).to redirect_to(group_path(member.source)) end end diff --git a/spec/services/bulk_imports/create_service_spec.rb b/spec/services/bulk_imports/create_service_spec.rb index 20872623802..024e7a0aa44 100644 --- a/spec/services/bulk_imports/create_service_spec.rb +++ b/spec/services/bulk_imports/create_service_spec.rb @@ -123,7 +123,8 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do ) allow_next_instance_of(BulkImports::Clients::HTTP) do |client| - allow(client).to receive(:validate_import_scopes!).and_raise(BulkImports::Error.scope_validation_failure) + allow(client).to receive(:validate_import_scopes!) + .and_raise(BulkImports::Error.scope_or_url_validation_failure) end result = subject.execute @@ -132,8 +133,7 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do expect(result).to be_error expect(result.message) .to eq( - "Personal access token does not " \ - "have the required 'api' scope or is no longer valid." + "Check that the source instance base URL and the personal access token meet the necessary requirements." ) end end @@ -546,7 +546,8 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do expect(result).to be_a(ServiceResponse) expect(result).to be_error expect(result.message) - .to eq("Import failed. Destination 'destination-namespace' is invalid, or you don't have permission.") + .to eq("Import failed. Destination 'destination-namespace' is invalid, " \ + "or you don't have permission.") end end @@ -571,7 +572,8 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do expect(result).to be_a(ServiceResponse) expect(result).to be_error expect(result.message) - .to eq("Import failed. Destination '#{parent_group.path}' is invalid, or you don't have permission.") + .to eq("Import failed. Destination '#{parent_group.path}' is invalid, " \ + "or you don't have permission.") end end @@ -596,7 +598,8 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do expect(result).to be_a(ServiceResponse) expect(result).to be_error expect(result.message) - .to eq("Import failed. Destination '#{parent_group.path}' is invalid, or you don't have permission.") + .to eq("Import failed. Destination '#{parent_group.path}' is invalid, " \ + "or you don't have permission.") end end end diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index 25c75ae7244..199f5e3fd9a 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -42,7 +42,7 @@ RSpec.describe MergeRequests::CloseService, feature_category: :code_review_workf .with(@merge_request, 'close') end - it 'sends email to user2 about assign of new merge_request', :sidekiq_might_not_need_inline do + it 'sends email to user2 about assign of new merge_request', :sidekiq_inline do email = ActionMailer::Base.deliveries.last expect(email.to.first).to eq(user2.email) expect(email.subject).to include(merge_request.title) diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index 51b1bed1dd3..bf52800b77e 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -52,7 +52,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state, f end.to change { project.open_merge_requests_count }.from(0).to(1) end - it 'creates exactly 1 create MR event', :sidekiq_might_not_need_inline do + it 'creates exactly 1 create MR event', :sidekiq_inline do attributes = { action: :created, target_id: merge_request.id, diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb index e173cd382f2..f3ac55059bc 100644 --- a/spec/services/merge_requests/reopen_service_spec.rb +++ b/spec/services/merge_requests/reopen_service_spec.rb @@ -44,7 +44,7 @@ RSpec.describe MergeRequests::ReopenService, feature_category: :code_review_work expect(Integrations::GroupMentionWorker).not_to receive(:perform_async) end - it 'sends email to user2 about reopen of merge_request', :sidekiq_might_not_need_inline do + it 'sends email to user2 about reopen of merge_request', :sidekiq_inline do email = ActionMailer::Base.deliveries.last expect(email.to.first).to eq(user2.email) expect(email.subject).to include(merge_request.title) diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index 4935a7a3803..49e2eaacb6b 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -6027,7 +6027,6 @@ - './spec/lib/gitlab/etag_caching/router/rails_spec.rb' - './spec/lib/gitlab/etag_caching/router_spec.rb' - './spec/lib/gitlab/etag_caching/store_spec.rb' -- './spec/lib/gitlab/event_store/event_spec.rb' - './spec/lib/gitlab/event_store/store_spec.rb' - './spec/lib/gitlab/exception_log_formatter_spec.rb' - './spec/lib/gitlab/exceptions_app_spec.rb' diff --git a/spec/support/shared_examples/controllers/concerns/onboarding/redirectable_shared_examples.rb b/spec/support/shared_examples/controllers/concerns/onboarding/redirectable_shared_examples.rb index b448ea16128..efb05709924 100644 --- a/spec/support/shared_examples/controllers/concerns/onboarding/redirectable_shared_examples.rb +++ b/spec/support/shared_examples/controllers/concerns/onboarding/redirectable_shared_examples.rb @@ -6,20 +6,20 @@ RSpec.shared_examples Onboarding::Redirectable do context 'when the new user already has any accepted group membership' do let!(:single_member) { create(:group_member, invite_email: email) } - it 'redirects to activity group path with a flash message' do + it 'redirects to the group path with a flash message' do post_create - expect(response).to redirect_to activity_group_path(single_member.source) + expect(response).to redirect_to group_path(single_member.source) expect(controller).to set_flash[:notice].to(/You have been granted/) end context 'when the new user already has more than 1 accepted group membership' do let!(:last_member) { create(:group_member, invite_email: email) } - it 'redirects to the last member activity group path without a flash message' do + it 'redirects to the last member group path without a flash message' do post_create - expect(response).to redirect_to activity_group_path(last_member.source) + expect(response).to redirect_to group_path(last_member.source) expect(controller).not_to set_flash[:notice].to(/You have been granted/) end end |