From dce8d0c6a7fdc7cca4df3b00b11d68000d117e2f Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 13 Oct 2023 00:10:17 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- Gemfile | 2 +- Gemfile.checksum | 2 +- Gemfile.lock | 4 +- .../super_sidebar/components/user_menu.vue | 9 +- .../components/user_menu_profile_item.vue | 83 +++++++++++++ .../super_sidebar/components/user_name_group.vue | 91 --------------- .../components/action_buttons.vue | 8 -- .../states/mr_widget_auto_merge_enabled.vue | 1 - .../components/states/mr_widget_merged.vue | 8 +- .../components/states/mr_widget_rebase.vue | 2 - .../components/states/sha_mismatch.vue | 2 +- .../components/states/squash_before_merge.vue | 2 +- .../components/widget/action_buttons.vue | 7 -- .../vue_merge_request_widget/mr_widget_options.vue | 2 +- .../components/markdown/apply_suggestion.vue | 6 +- .../vue_shared/components/markdown/header.vue | 4 +- .../components/markdown/suggestion_diff_header.vue | 6 +- app/models/packages/protection/rule.rb | 10 +- .../development/on_demand_scans_runner_tags.yml | 8 -- .../prohibited_tag_name_encoding_check.yml | 8 ++ ...vel_to_smallint_in_packages_protection_rules.rb | 13 +++ db/schema_migrations/20231010101246 | 1 + db/structure.sql | 2 +- .../audit_event_streaming/graphql_api.md | 22 +++- doc/user/application_security/dast/proxy-based.md | 1 - .../repository/managing_large_repositories.md | 46 +++++++- lib/api/entities/bulk_import.rb | 1 + lib/api/entities/bulk_imports/entity.rb | 1 + lib/gitlab/checks/tag_check.rb | 13 ++- lib/gitlab/encoding_helper.rb | 4 +- qa/qa/page/main/menu.rb | 2 +- qa/qa/page/merge_request/show.rb | 76 ++++++------ .../user_can_bulk_delete_artifacts_spec.rb | 4 +- spec/factories/packages/protection/rules.rb | 2 +- .../frontend/design_management/pages/index_spec.js | 2 +- .../components/user_menu_profile_item_spec.js | 125 ++++++++++++++++++++ .../super_sidebar/components/user_menu_spec.js | 8 +- .../components/user_name_group_spec.js | 130 --------------------- .../work_item_links/work_item_links_spec.js | 1 - spec/lib/api/entities/bulk_import_spec.rb | 5 +- spec/lib/api/entities/bulk_imports/entity_spec.rb | 5 +- spec/lib/gitlab/checks/tag_check_spec.rb | 30 +++++ spec/lib/gitlab/encoding_helper_spec.rb | 37 +++++- spec/models/packages/protection/rule_spec.rb | 21 ++-- 44 files changed, 466 insertions(+), 351 deletions(-) create mode 100644 app/assets/javascripts/super_sidebar/components/user_menu_profile_item.vue delete mode 100644 app/assets/javascripts/super_sidebar/components/user_name_group.vue delete mode 100644 config/feature_flags/development/on_demand_scans_runner_tags.yml create mode 100644 config/feature_flags/development/prohibited_tag_name_encoding_check.yml create mode 100644 db/migrate/20231010101246_change_push_protected_up_to_access_level_to_smallint_in_packages_protection_rules.rb create mode 100644 db/schema_migrations/20231010101246 create mode 100644 spec/frontend/super_sidebar/components/user_menu_profile_item_spec.js delete mode 100644 spec/frontend/super_sidebar/components/user_name_group_spec.js diff --git a/Gemfile b/Gemfile index 15e1aff2edf..dd902996341 100644 --- a/Gemfile +++ b/Gemfile @@ -530,7 +530,7 @@ gem 'ssh_data', '~> 1.3' # rubocop:todo Gemfile/MissingFeatureCategory gem 'spamcheck', '~> 1.3.0' # rubocop:todo Gemfile/MissingFeatureCategory # Gitaly GRPC protocol definitions -gem 'gitaly', '~> 16.3.0-rc1' # rubocop:todo Gemfile/MissingFeatureCategory +gem 'gitaly', '~> 16.5.0.pre.rc1' # rubocop:todo Gemfile/MissingFeatureCategory # KAS GRPC protocol definitions gem 'kas-grpc', '~> 0.2.0' # rubocop:todo Gemfile/MissingFeatureCategory diff --git a/Gemfile.checksum b/Gemfile.checksum index 8d7326c7821..8397f016bac 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -203,7 +203,7 @@ {"name":"gettext_i18n_rails","version":"1.11.0","platform":"ruby","checksum":"e19c7e4a256c500f7f38396dca44a282b9838ae278f57c362993a54964b22bbe"}, {"name":"gettext_i18n_rails_js","version":"1.3.0","platform":"ruby","checksum":"5d10afe4be3639bff78c50a56768c20f39aecdabc580c08aa45573911c2bd687"}, {"name":"git","version":"1.18.0","platform":"ruby","checksum":"c9b80462e4565cd3d7a9ba8440c41d2c52244b17b0dad0bfddb46de70630c465"}, -{"name":"gitaly","version":"16.3.0.pre.rc1","platform":"ruby","checksum":"55d9cc414a4f3859588f3770bd88d7c67c0f5454a1178b018b7a6f6913674c43"}, +{"name":"gitaly","version":"16.5.0.pre.rc1","platform":"ruby","checksum":"ed17515ad04d4663a0efc15c8f2887b705f006133e8b10cc9321460eb0a38353"}, {"name":"gitlab","version":"4.19.0","platform":"ruby","checksum":"3f645e3e195dbc24f0834fbf83e8ccfb2056d8e9712b01a640aad418a6949679"}, {"name":"gitlab-chronic","version":"0.10.5","platform":"ruby","checksum":"f80f18dc699b708870a80685243331290bc10cfeedb6b99c92219722f729c875"}, {"name":"gitlab-dangerfiles","version":"4.1.0","platform":"ruby","checksum":"ecf2262fcc038c1e77f7ea014f5fa8657e02ae37fde5034a2d7f67fd6e804d8d"}, diff --git a/Gemfile.lock b/Gemfile.lock index 51101a25079..77e936d8bc0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -639,7 +639,7 @@ GEM git (1.18.0) addressable (~> 2.8) rchardet (~> 1.8) - gitaly (16.3.0.pre.rc1) + gitaly (16.5.0.pre.rc1) grpc (~> 1.0) gitlab (4.19.0) httparty (~> 0.20) @@ -1816,7 +1816,7 @@ DEPENDENCIES gettext (~> 3.3) gettext_i18n_rails (~> 1.11.0) gettext_i18n_rails_js (~> 1.3) - gitaly (~> 16.3.0.pre.rc1) + gitaly (~> 16.5.0.pre.rc1) gitlab-chronic (~> 0.10.5) gitlab-dangerfiles (~> 4.1.0) gitlab-experiment (~> 0.8.0) diff --git a/app/assets/javascripts/super_sidebar/components/user_menu.vue b/app/assets/javascripts/super_sidebar/components/user_menu.vue index ed6c41e85c6..891e883b6c0 100644 --- a/app/assets/javascripts/super_sidebar/components/user_menu.vue +++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue @@ -12,7 +12,7 @@ import NewNavToggle from '~/nav/components/new_nav_toggle.vue'; import Tracking from '~/tracking'; import PersistentUserCallout from '~/persistent_user_callout'; import { USER_MENU_TRACKING_DEFAULTS, DROPDOWN_Y_OFFSET, IMPERSONATING_OFFSET } from '../constants'; -import UserNameGroup from './user_name_group.vue'; +import UserMenuProfileItem from './user_menu_profile_item.vue'; // Left offset required for the dropdown to be aligned with the super sidebar const DROPDOWN_X_OFFSET_BASE = -211; @@ -40,7 +40,7 @@ export default { GlDisclosureDropdownItem, GlButton, NewNavToggle, - UserNameGroup, + UserMenuProfileItem, }, directives: { SafeHtml, @@ -247,7 +247,10 @@ export default { - + + + + +import { GlBadge, GlDisclosureDropdownItem, GlTooltip } from '@gitlab/ui'; +import SafeHtml from '~/vue_shared/directives/safe_html'; +import { s__ } from '~/locale'; +import { USER_MENU_TRACKING_DEFAULTS } from '../constants'; + +export default { + i18n: { + user: { + busy: s__('UserProfile|Busy'), + }, + }, + components: { + GlBadge, + GlDisclosureDropdownItem, + GlTooltip, + }, + directives: { + SafeHtml, + }, + props: { + user: { + required: true, + type: Object, + }, + }, + computed: { + menuItem() { + const item = { + text: this.user.name, + }; + if (this.user.has_link_to_profile) { + item.href = this.user.link_to_profile; + + item.extraAttrs = { + ...USER_MENU_TRACKING_DEFAULTS, + 'data-track-label': 'user_profile', + 'data-testid': 'user-profile-link', + }; + } + + return item; + }, + }, +}; + + + diff --git a/app/assets/javascripts/super_sidebar/components/user_name_group.vue b/app/assets/javascripts/super_sidebar/components/user_name_group.vue deleted file mode 100644 index 29cc340116b..00000000000 --- a/app/assets/javascripts/super_sidebar/components/user_name_group.vue +++ /dev/null @@ -1,91 +0,0 @@ - - - diff --git a/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue b/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue index c49c1316b1b..619f141144d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue @@ -83,12 +83,6 @@ export default { return btn.tooltipText; }, - actionButtonQaSelector(btn) { - if (btn.dataQaSelector) { - return btn.dataQaSelector; - } - return 'mr_widget_extension_actions_button'; - }, }, }; @@ -105,7 +99,6 @@ export default { :target="btn.target" :class="[{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }, btn.class]" :data-clipboard-text="btn.dataClipboardText" - :data-qa-selector="actionButtonQaSelector(btn)" :data-method="btn.dataMethod" :icon="btn.icon" :data-testid="btn.testId || 'extension-actions-button'" @@ -159,7 +152,6 @@ export default { :target="btn.target" :class="[{ 'gl-mr-1': index !== tertiaryButtons.length - 1 }, btn.class]" :data-clipboard-text="btn.dataClipboardText" - :data-qa-selector="actionButtonQaSelector(btn)" :data-method="btn.dataMethod" :icon="btn.icon" :data-testid="btn.testId || 'extension-actions-button'" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue index 6299f0fcbb8..ec72b74daa2 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue @@ -75,7 +75,6 @@ export default { actions.push({ text: this.cancelButtonText, loading: this.isCancellingAutoMerge, - dataQaSelector: 'cancel_auto_merge_button', class: 'js-cancel-auto-merge', testId: 'cancelAutomaticMergeButton', onClick: () => this.cancelAutomaticMerge(), diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue index 4d906f29cb0..4454718a647 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -67,7 +67,7 @@ export default { actions.push({ text: this.revertLabel, tooltipText: this.revertTitle, - dataQaSelector: 'revert_button', + testId: 'revert-button', onClick: () => this.openRevertModal(), }); } else if (this.mr.revertInForkPath) { @@ -75,7 +75,7 @@ export default { text: this.revertLabel, tooltipText: this.revertTitle, href: this.mr.revertInForkPath, - dataQaSelector: 'revert_button', + testId: 'revert-button', dataMethod: 'post', }); } @@ -84,7 +84,7 @@ export default { actions.push({ text: this.cherryPickLabel, tooltipText: this.cherryPickTitle, - dataQaSelector: 'cherry_pick_button', + testId: 'cherry-pick-button', onClick: () => this.openCherryPickModal(), }); } else if (this.mr.cherryPickInForkPath) { @@ -92,7 +92,7 @@ export default { text: this.cherryPickLabel, tooltipText: this.cherryPickTitle, href: this.mr.cherryPickInForkPath, - dataQaSelector: 'cherry_pick_button', + testId: 'cherry-pick-button', dataMethod: 'post', }); } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index 415f58ea8e6..a4afdee4d49 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -230,7 +230,6 @@ export default { v-if="!rebasingError" class="gl-w-100 gl-md-w-auto gl-flex-grow-1 gl-ml-0! gl-text-body! gl-md-mr-3" data-testid="rebase-message" - data-qa-selector="no_fast_forward_message_content" > @@ -247,7 +246,6 @@ export default { :loading="isMakingRequest" variant="confirm" size="small" - data-qa-selector="mr_rebase_button" data-testid="standard-rebase-button" class="gl-align-self-start" @click="tryRebase" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue index 9da754d01fc..00383418f2d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue @@ -32,7 +32,7 @@ export default { > diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue index 97ef96fe382..f1bd5bb25bb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue @@ -46,7 +46,7 @@ export default { :disabled="isDisabled" name="squash" class="js-squash-checkbox gl-mr-2" - data-qa-selector="squash_checkbox" + data-testid="squash-checkbox" :title="tooltipTitle" @change="(checked) => $emit('input', checked)" > diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue index 55eeb9d0e63..5b7657f15d9 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue @@ -80,12 +80,6 @@ export default { return btn.tooltipText; }, - actionButtonQaSelector(btn) { - if (btn.dataQaSelector) { - return btn.dataQaSelector; - } - return 'mr_widget_extension_actions_button'; - }, }, }; @@ -121,7 +115,6 @@ export default { :target="btn.target" :class="[{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }, btn.class]" :data-clipboard-text="btn.dataClipboardText" - :data-qa-selector="actionButtonQaSelector(btn)" :data-method="btn.dataMethod" :icon="btn.icon || btn.iconName" :data-testid="btn.testId || 'extension-actions-button'" diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 175a0b0563f..b55b59fe581 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -576,7 +576,7 @@ export default { -
+
@@ -93,7 +93,7 @@ export default { class="gl-w-auto! gl-mt-3 gl-align-self-end" category="primary" variant="confirm" - data-qa-selector="commit_with_custom_message_button" + data-testid="commit-with-custom-message-button" @click="onApply" > {{ __('Apply') }} diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index a569b4ea9a7..741bdfd211b 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -281,7 +281,7 @@ export default { :tag-content="lineContent" tracking-property="codeSuggestion" icon="doc-code" - data-qa-selector="suggestion_button" + data-testid="suggestion-button" class="js-suggestion-btn" @click="handleSuggestDismissed" /> @@ -305,7 +305,7 @@ export default { variant="confirm" category="primary" size="small" - data-qa-selector="dismiss_suggestion_popover_button" + data-testid="dismiss-suggestion-popover-button" @click="handleSuggestDismissed" > {{ __('Got it') }} diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue index 8a0ca8ebac1..a822e2a6151 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue @@ -144,13 +144,13 @@ export default {
- + {{ __('Applied') }}
{{ applyingSuggestionsMessage }} @@ -169,7 +169,7 @@ export default {
- Auditing for DAST profile management [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217872) in GitLab 14.1. > - Scheduled on-demand DAST scans [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/328749) in GitLab 14.3 [with a flag](../../../administration/feature_flags.md) named `dast_on_demand_scans_scheduler`. Disabled by default. > - Scheduled on-demand DAST scans [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/328749) in GitLab 14.5. Feature flag `dast_on_demand_scans_scheduler` removed. -> - Runner tags selection [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345430) in GitLab 15.9 [with a flag](../../../administration/feature_flags.md) named `on_demand_scans_runner_tags. Disabled by default. > - Runner tags selection [enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111499) in GitLab 16.3. WARNING: diff --git a/doc/user/project/repository/managing_large_repositories.md b/doc/user/project/repository/managing_large_repositories.md index 83ba0da9865..721150f958b 100644 --- a/doc/user/project/repository/managing_large_repositories.md +++ b/doc/user/project/repository/managing_large_repositories.md @@ -263,12 +263,46 @@ following sections for information on solving: ### Large number of references -A reference in Git (a branch or tag) is used to refer to a commit. If you are -curious, you can go to any `.git` directory and look under the `refs` directory. - -A large number of references can cause performance problems because, with more -references, object walks that Git does are larger for various operations such as -clones, pushes, and housekeeping tasks. +[References in Git](https://git-scm.com/book/en/v2/Git-Internals-Git-References) +are branch and tag names that point to a particular commit. You can use the `git +for-each-ref` command to list all references present in a repository. A large +number of references in a repository can have detrimental impact on the command's +performance. To understand why, we need to understand how Git stores references +and uses them. + +In general, Git stores all references as loose files in the `.git/refs` folder of +the repository. As the number of references grows, the seek time to find a +particular reference in the folder also increases. Therefore, every time Git has +to parse a reference, there is an increased latency due to the added seek time +of the file system. + +To resolve this issue, Git uses [pack-refs](https://git-scm.com/docs/git-pack-refs). In short, instead of storing each +reference in a single file, Git creates a single `.git/packed-refs` file that +contains all the references for that repository. This file reduces storage space +while also increasing performance because seeking within a single file is faster +than seeking a file within a directory. However, creating and updating new references +is still done through loose files and are not added to the `packed-refs` file. To +recreate the `packed-refs` file, run `git pack-refs`. + +Gitaly runs `git pack-refs` during [housekeeping](../../../administration/housekeeping.md#heuristical-housekeeping) +to move loose references into `packed-refs` files. While this is very beneficial +for most repositories, write-heavy repositories still have the problem that: + +- Creating or updating references creates new loose files. +- Deleting references involves modifying the existing `packed-refs` file + altogether to remove the existing reference. + +These problems still cause the same performance issues. + +In addition, fetches and clones from repositories includes the transfer +of missing objects from the server to the client. When there are numerous +references, Git iterates over all references and walks the internal graph +structure for each reference to find the missing objects to transfer to +the client. Iteration and walking are CPU-intensive operations that increase +the latency of these commands. + +In repositories with a lot of activity, this often causes a domino effect because +every operation is slower and each operation stalls subsequent operations. #### Mitigation strategies diff --git a/lib/api/entities/bulk_import.rb b/lib/api/entities/bulk_import.rb index 75989cb4180..18f71048595 100644 --- a/lib/api/entities/bulk_import.rb +++ b/lib/api/entities/bulk_import.rb @@ -10,6 +10,7 @@ module API expose :source_type, documentation: { type: 'string', example: 'gitlab' } expose :created_at, documentation: { type: 'dateTime', example: '2012-05-28T04:42:42-07:00' } expose :updated_at, documentation: { type: 'dateTime', example: '2012-05-28T04:42:42-07:00' } + expose :has_failures, documentation: { type: 'boolean', example: false } end end end diff --git a/lib/api/entities/bulk_imports/entity.rb b/lib/api/entities/bulk_imports/entity.rb index 176d10b2580..7e9b9973e15 100644 --- a/lib/api/entities/bulk_imports/entity.rb +++ b/lib/api/entities/bulk_imports/entity.rb @@ -24,6 +24,7 @@ module API expose :updated_at, documentation: { type: 'dateTime', example: '2012-05-28T04:42:42-07:00' } expose :failures, using: EntityFailure, documentation: { is_array: true } expose :migrate_projects, documentation: { type: 'boolean', example: true } + expose :has_failures, documentation: { type: 'boolean', example: false } end end end diff --git a/lib/gitlab/checks/tag_check.rb b/lib/gitlab/checks/tag_check.rb index 4505bcb5411..d5addab74b8 100644 --- a/lib/gitlab/checks/tag_check.rb +++ b/lib/gitlab/checks/tag_check.rb @@ -11,7 +11,8 @@ module Gitlab delete_protected_tag_non_web: 'You can only delete protected tags using the web interface.', create_protected_tag: 'You are not allowed to create this tag as it is protected.', default_branch_collision: 'You cannot use default branch name to create a tag', - prohibited_tag_name: 'You cannot create a tag with a prohibited pattern.' + prohibited_tag_name: 'You cannot create a tag with a prohibited pattern.', + prohibited_tag_name_encoding: 'Tag names must be valid when converted to UTF-8 encoding' }.freeze LOG_MESSAGES = { @@ -46,6 +47,16 @@ module Gitlab if tag_name.start_with?("refs/tags/") # rubocop: disable Style/GuardClause raise GitAccess::ForbiddenError, ERROR_MESSAGES[:prohibited_tag_name] end + + # rubocop: disable Style/GuardClause + # rubocop: disable Style/SoleNestedConditional + if Feature.enabled?(:prohibited_tag_name_encoding_check, project) + unless Gitlab::EncodingHelper.force_encode_utf8(tag_name).valid_encoding? + raise GitAccess::ForbiddenError, ERROR_MESSAGES[:prohibited_tag_name_encoding] + end + end + # rubocop: enable Style/SoleNestedConditional + # rubocop: enable Style/GuardClause end def protected_tag_checks diff --git a/lib/gitlab/encoding_helper.rb b/lib/gitlab/encoding_helper.rb index 99240f2ad48..b080cb197d4 100644 --- a/lib/gitlab/encoding_helper.rb +++ b/lib/gitlab/encoding_helper.rb @@ -152,8 +152,6 @@ module Gitlab message.delete_prefix(BOM_UTF8) end - private - def force_encode_utf8(message) raise ArgumentError unless message.respond_to?(:force_encoding) return message if message.encoding == Encoding::UTF_8 && message.valid_encoding? @@ -163,6 +161,8 @@ module Gitlab message.force_encoding("UTF-8") end + private + # Escapes \x80 - \xFF characters not supported by UTF-8 def escape_chars(char) bytes = char.bytes diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb index 98d00664854..2413166e120 100644 --- a/qa/qa/page/main/menu.rb +++ b/qa/qa/page/main/menu.rb @@ -21,7 +21,7 @@ module QA element :edit_profile_link end - view 'app/assets/javascripts/super_sidebar/components/user_name_group.vue' do + view 'app/assets/javascripts/super_sidebar/components/user_menu_profile_item.vue' do element 'user-profile-link' end diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index 3782ae0c9d0..a51c65a18c6 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -71,13 +71,13 @@ module QA end view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue' do - element :cherry_pick_button - element :revert_button + element 'cherry-pick-button' + element 'revert-button' end view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue' do - element :mr_rebase_button - element :no_fast_forward_message_content + element 'standard-rebase-button' + element 'rebase-message' end view 'app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue' do @@ -88,33 +88,33 @@ module QA end view 'app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue' do - element :head_mismatch_content + element 'head-mismatch-content' end view 'app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue' do - element :squash_checkbox + element 'squash-checkbox' end view 'app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue' do - element :mr_widget_content + element 'mr-widget-content' element 'pipeline-container' end view 'app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue' do - element :apply_suggestion_dropdown - element :commit_message_field - element :commit_with_custom_message_button + element 'apply-suggestion-dropdown' + element 'commit-message-field' + element 'commit-with-custom-message-button' end view 'app/assets/javascripts/vue_shared/components/markdown/header.vue' do - element :suggestion_button - element :dismiss_suggestion_popover_button + element 'suggestion-button' + element 'dismiss-suggestion-popover-button' end view 'app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue' do - element :add_suggestion_batch_button - element :applied_badge - element :applying_badge + element 'add-suggestion-batch-button' + element 'applied-badge' + element 'applying-badge' end view 'app/views/projects/merge_requests/_description.html.haml' do @@ -132,10 +132,6 @@ module QA element 'diffs-tab', required: true end - view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue' do - element :cancel_auto_merge_button - end - view 'app/views/shared/_broadcast_message.html.haml' do element 'broadcast-notification-container' element 'close-button' @@ -195,7 +191,9 @@ module QA all_elements('left-line-number', minimum: 1).first.hover click_element('left-comment-button') - click_element(:dismiss_suggestion_popover_button) if has_element?(:dismiss_suggestion_popover_button, wait: 1) + + click_element('dismiss-suggestion-popover-button') if has_element?('dismiss-suggestion-popover-button', + wait: 1) fill_element('reply-field', text) end @@ -227,7 +225,7 @@ module QA end def fast_forward_not_possible? - has_element?(:no_fast_forward_message_content) + has_element?('rebase-message') end def has_file?(file_name) @@ -290,15 +288,15 @@ module QA def mark_to_squash # Refresh page if commit arrived after loading the MR page wait_until(reload: true, message: 'Wait for MR to be unblocked') do - has_no_element?(:head_mismatch_content, wait: 1) + has_no_element?('head-mismatch-content', wait: 1) end # The squash checkbox is enabled via JS wait_until(reload: false) do - !find_element(:squash_checkbox, visible: false).disabled? + !find_element('squash-checkbox', visible: false).disabled? end - check_element(:squash_checkbox, true) + check_element('squash-checkbox', true) end def merge! @@ -338,7 +336,7 @@ module QA # Waits up 10 seconds and returns false if the Revert button is not enabled def revertible? - has_element?(:revert_button, disabled: false, wait: 10) + has_element?('revert-button', disabled: false, wait: 10) end # Waits up 60 seconds and raises an error if unable to merge. @@ -357,7 +355,7 @@ module QA break true unless find_element('merge-button').disabled? # If the widget shows "Merge blocked: new changes were just added" we can refresh the page and check again - next false if has_element?(:head_mismatch_content, wait: 1) + next false if has_element?('head-mismatch-content', wait: 1) # Stop waiting if we're in a transient test. By this point we're in an unexpected state and should let the # test fail so we can investigate. If we're not in a transient test we keep trying until we reach timeout. @@ -372,15 +370,15 @@ module QA def rebase! # The rebase button is disabled on load wait_until do - has_element?(:mr_rebase_button) + has_element?('standard-rebase-button') end # The rebase button is enabled via JS wait_until(reload: false) do - !find_element(:mr_rebase_button).disabled? + !find_element('standard-rebase-button').disabled? end - click_element(:mr_rebase_button) + click_element('standard-rebase-button') end def merge_immediately! @@ -448,7 +446,7 @@ module QA def add_suggestion_to_diff(suggestion, line) find("a[data-linenumber='#{line}']").hover click_element('left-comment-button') - click_element(:suggestion_button) + click_element('suggestion-button') initial_content = find_element('reply-field').value fill_element('reply-field', '') fill_element('reply-field', initial_content.gsub(/(```suggestion:-0\+0\n).*(\n```)/, "\\1#{suggestion}\\2")) @@ -457,24 +455,24 @@ module QA end def apply_suggestion_with_message(message) - all_elements(:apply_suggestion_dropdown, minimum: 1).first.click - fill_element(:commit_message_field, message) - click_element(:commit_with_custom_message_button) + all_elements('apply-suggestion-dropdown', minimum: 1).first.click + fill_element('commit-message-field', message) + click_element('commit-with-custom-message-button') end def add_suggestion_to_batch - all_elements(:add_suggestion_batch_button, minimum: 1).first.click + all_elements('add-suggestion-batch-button', minimum: 1).first.click end def has_suggestions_applied?(count = 1) wait_until(reload: false) do - has_no_element?(:applying_badge) + has_no_element?('applying-badge') end - all_elements(:applied_badge, count: count) + all_elements('applied-badge', count: count) end def cherry_pick! - click_element(:cherry_pick_button, Page::Component::CommitModal) + click_element('cherry-pick-button', Page::Component::CommitModal) click_element(:submit_commit_button) end @@ -482,13 +480,13 @@ module QA # reload page when the revert modal occasionally doesn't appear in ee:large-setup job # https://gitlab.com/gitlab-org/gitlab/-/issues/386623 (transient issue) retry_on_exception(reload: true) do - click_element(:revert_button, Page::Component::CommitModal) + click_element('revert-button', Page::Component::CommitModal) end click_element(:submit_commit_button) end def mr_widget_text - find_element(:mr_widget_content).text + find_element('mr-widget-content').text end def has_fork_icon? diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_project_artifacts/user_can_bulk_delete_artifacts_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_project_artifacts/user_can_bulk_delete_artifacts_spec.rb index 17c83a32d52..27969759adf 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/ci_project_artifacts/user_can_bulk_delete_artifacts_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_project_artifacts/user_can_bulk_delete_artifacts_spec.rb @@ -23,7 +23,7 @@ module QA after do Parallel.each((0..(total_runners_count - 1)), in_threads: 3) do |i| - runners[i].remove_via_api! + runners[i]&.remove_via_api! end end @@ -46,7 +46,7 @@ module QA def launch_runners Parallel.each((1..total_runners_count), in_threads: 3) do |i| - runners << create(:project_runner, project: project, name: "executor-#{i}", tags: [executor]) + runners << create(:project_runner, project: project, name: "#{executor}-#{i}", tags: [executor]) end end diff --git a/spec/factories/packages/protection/rules.rb b/spec/factories/packages/protection/rules.rb index 3038fb847e7..f65a9d3e64d 100644 --- a/spec/factories/packages/protection/rules.rb +++ b/spec/factories/packages/protection/rules.rb @@ -5,6 +5,6 @@ FactoryBot.define do project package_name_pattern { '@my_scope/my_package' } package_type { :npm } - push_protected_up_to_access_level { Gitlab::Access::DEVELOPER } + push_protected_up_to_access_level { :developer } end end diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js index 961ea27f0f4..9b5e812c021 100644 --- a/spec/frontend/design_management/pages/index_spec.js +++ b/spec/frontend/design_management/pages/index_spec.js @@ -191,7 +191,7 @@ describe('Design management index page', () => { [moveDesignMutation, moveDesignHandler], ]; - fakeApollo = createMockApollo(requestHandlers, {}, { addTypename: true }); + fakeApollo = createMockApollo(requestHandlers, {}); wrapper = shallowMountExtended(Index, { apolloProvider: fakeApollo, router, diff --git a/spec/frontend/super_sidebar/components/user_menu_profile_item_spec.js b/spec/frontend/super_sidebar/components/user_menu_profile_item_spec.js new file mode 100644 index 00000000000..9cf55154a59 --- /dev/null +++ b/spec/frontend/super_sidebar/components/user_menu_profile_item_spec.js @@ -0,0 +1,125 @@ +import { GlDisclosureDropdownItem, GlTooltip } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import UserMenuProfileItem from '~/super_sidebar/components/user_menu_profile_item.vue'; +import { userMenuMockData, userMenuMockStatus } from '../mock_data'; + +describe('UserMenuProfileItem component', () => { + let wrapper; + + const findGlDisclosureDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem); + const findGlTooltip = () => wrapper.findComponent(GlTooltip); + const findUserStatus = () => wrapper.findByTestId('user-menu-status'); + + const GlEmoji = { template: '' }; + + const createWrapper = (userDataChanges = {}) => { + wrapper = shallowMountExtended(UserMenuProfileItem, { + propsData: { + user: { + ...userMenuMockData, + ...userDataChanges, + }, + }, + stubs: { + GlEmoji, + GlDisclosureDropdownItem, + }, + }); + }; + + beforeEach(() => { + createWrapper(); + }); + + it('renders menu item', () => { + expect(findGlDisclosureDropdownItem().exists()).toBe(true); + }); + + it('passes the item to the disclosure dropdown item', () => { + expect(findGlDisclosureDropdownItem().props('item')).toEqual( + expect.objectContaining({ + text: userMenuMockData.name, + href: userMenuMockData.link_to_profile, + }), + ); + }); + + it("renders user's name", () => { + expect(findGlDisclosureDropdownItem().text()).toContain(userMenuMockData.name); + }); + + it("renders user's username", () => { + expect(findGlDisclosureDropdownItem().text()).toContain(userMenuMockData.username); + }); + + describe('Busy status', () => { + it('should not render "Busy" when user is NOT busy', () => { + expect(findGlDisclosureDropdownItem().text()).not.toContain('Busy'); + }); + it('should render "Busy" when user is busy', () => { + createWrapper({ status: { customized: true, busy: true } }); + + expect(findGlDisclosureDropdownItem().text()).toContain('Busy'); + }); + }); + + describe('User status', () => { + describe('when not customized', () => { + it('should not render it', () => { + expect(findUserStatus().exists()).toBe(false); + }); + }); + + describe('when customized', () => { + beforeEach(() => { + createWrapper({ status: { ...userMenuMockStatus, customized: true } }); + }); + + it('should render it', () => { + expect(findUserStatus().exists()).toBe(true); + }); + + it('should render status emoji', () => { + expect(findUserStatus().findComponent(GlEmoji).attributes('data-name')).toBe( + userMenuMockData.status.emoji, + ); + }); + + it('should render status message', () => { + expect(findUserStatus().html()).toContain(userMenuMockData.status.message_html); + }); + + it("sets the tooltip's target to the status container", () => { + expect(findGlTooltip().props('target')?.()).toBe(findUserStatus().element); + }); + + describe('Tooltip', () => { + it('renders the tooltip when message has some text', () => { + createWrapper({ + status: { ...userMenuMockStatus, customized: true, message_html: 'Has text' }, + }); + expect(findGlTooltip().exists()).toBe(true); + }); + + it('does not render the tooltip when message is empty', () => { + createWrapper({ + status: { ...userMenuMockStatus, customized: true, message_html: '' }, + }); + expect(findGlTooltip().exists()).toBe(false); + }); + }); + }); + }); + + describe('Tracking', () => { + it('sets the tracking attributes', () => { + expect(findGlDisclosureDropdownItem().find('a').attributes()).toEqual( + expect.objectContaining({ + 'data-track-property': 'nav_user_menu', + 'data-track-action': 'click_link', + 'data-track-label': 'user_profile', + }), + ); + }); + }); +}); diff --git a/spec/frontend/super_sidebar/components/user_menu_spec.js b/spec/frontend/super_sidebar/components/user_menu_spec.js index d41a414f69e..79a31492f3f 100644 --- a/spec/frontend/super_sidebar/components/user_menu_spec.js +++ b/spec/frontend/super_sidebar/components/user_menu_spec.js @@ -2,7 +2,7 @@ import { GlAvatar, GlDisclosureDropdown } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import { stubComponent } from 'helpers/stub_component'; import UserMenu from '~/super_sidebar/components/user_menu.vue'; -import UserNameGroup from '~/super_sidebar/components/user_name_group.vue'; +import UserMenuProfileItem from '~/super_sidebar/components/user_menu_profile_item.vue'; import NewNavToggle from '~/nav/components/new_nav_toggle.vue'; import invalidUrl from '~/lib/utils/invalid_url'; import { mockTracking } from 'helpers/tracking_helper'; @@ -86,9 +86,9 @@ describe('UserMenu component', () => { describe('User Menu Group', () => { it('renders and passes data to it', () => { createWrapper(); - const userNameGroup = wrapper.findComponent(UserNameGroup); - expect(userNameGroup.exists()).toBe(true); - expect(userNameGroup.props('user')).toEqual(userMenuMockData); + const userMenuProfileItem = wrapper.findComponent(UserMenuProfileItem); + expect(userMenuProfileItem.exists()).toBe(true); + expect(userMenuProfileItem.props('user')).toEqual(userMenuMockData); }); }); diff --git a/spec/frontend/super_sidebar/components/user_name_group_spec.js b/spec/frontend/super_sidebar/components/user_name_group_spec.js deleted file mode 100644 index a31ad93d143..00000000000 --- a/spec/frontend/super_sidebar/components/user_name_group_spec.js +++ /dev/null @@ -1,130 +0,0 @@ -import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem, GlTooltip } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import UserNameGroup from '~/super_sidebar/components/user_name_group.vue'; -import { userMenuMockData, userMenuMockStatus } from '../mock_data'; - -describe('UserNameGroup component', () => { - let wrapper; - - const findGlDisclosureDropdownGroup = () => wrapper.findComponent(GlDisclosureDropdownGroup); - const findGlDisclosureDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem); - const findGlTooltip = () => wrapper.findComponent(GlTooltip); - const findUserStatus = () => wrapper.findByTestId('user-menu-status'); - - const GlEmoji = { template: '' }; - - const createWrapper = (userDataChanges = {}) => { - wrapper = shallowMountExtended(UserNameGroup, { - propsData: { - user: { - ...userMenuMockData, - ...userDataChanges, - }, - }, - stubs: { - GlEmoji, - GlDisclosureDropdownItem, - }, - }); - }; - - beforeEach(() => { - createWrapper(); - }); - - it('renders the menu item in a separate group', () => { - expect(findGlDisclosureDropdownGroup().exists()).toBe(true); - }); - - it('renders menu item', () => { - expect(findGlDisclosureDropdownItem().exists()).toBe(true); - }); - - it('passes the item to the disclosure dropdown item', () => { - expect(findGlDisclosureDropdownItem().props('item')).toEqual( - expect.objectContaining({ - text: userMenuMockData.name, - href: userMenuMockData.link_to_profile, - }), - ); - }); - - it("renders user's name", () => { - expect(findGlDisclosureDropdownItem().text()).toContain(userMenuMockData.name); - }); - - it("renders user's username", () => { - expect(findGlDisclosureDropdownItem().text()).toContain(userMenuMockData.username); - }); - - describe('Busy status', () => { - it('should not render "Busy" when user is NOT busy', () => { - expect(findGlDisclosureDropdownItem().text()).not.toContain('Busy'); - }); - it('should render "Busy" when user is busy', () => { - createWrapper({ status: { customized: true, busy: true } }); - - expect(findGlDisclosureDropdownItem().text()).toContain('Busy'); - }); - }); - - describe('User status', () => { - describe('when not customized', () => { - it('should not render it', () => { - expect(findUserStatus().exists()).toBe(false); - }); - }); - - describe('when customized', () => { - beforeEach(() => { - createWrapper({ status: { ...userMenuMockStatus, customized: true } }); - }); - - it('should render it', () => { - expect(findUserStatus().exists()).toBe(true); - }); - - it('should render status emoji', () => { - expect(findUserStatus().findComponent(GlEmoji).attributes('data-name')).toBe( - userMenuMockData.status.emoji, - ); - }); - - it('should render status message', () => { - expect(findUserStatus().html()).toContain(userMenuMockData.status.message_html); - }); - - it("sets the tooltip's target to the status container", () => { - expect(findGlTooltip().props('target')?.()).toBe(findUserStatus().element); - }); - - describe('Tooltip', () => { - it('renders the tooltip when message has some text', () => { - createWrapper({ - status: { ...userMenuMockStatus, customized: true, message_html: 'Has text' }, - }); - expect(findGlTooltip().exists()).toBe(true); - }); - - it('does not render the tooltip when message is empty', () => { - createWrapper({ - status: { ...userMenuMockStatus, customized: true, message_html: '' }, - }); - expect(findGlTooltip().exists()).toBe(false); - }); - }); - }); - }); - - describe('Tracking', () => { - it('sets the tracking attributes', () => { - expect(findGlDisclosureDropdownItem().find('a').attributes()).toEqual( - expect.objectContaining({ - 'data-track-property': 'nav_user_menu', - 'data-track-action': 'click_link', - 'data-track-label': 'user_profile', - }), - ); - }); - }); -}); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js index 111fb5d8458..0b88b3ff5b4 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js @@ -54,7 +54,6 @@ describe('WorkItemLinks', () => { [issueDetailsQuery, issueDetailsQueryHandler], ], resolvers, - { addTypename: true }, ); wrapper = shallowMountExtended(WorkItemLinks, { diff --git a/spec/lib/api/entities/bulk_import_spec.rb b/spec/lib/api/entities/bulk_import_spec.rb index 2db6862b079..cfa293463ad 100644 --- a/spec/lib/api/entities/bulk_import_spec.rb +++ b/spec/lib/api/entities/bulk_import_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Entities::BulkImport do +RSpec.describe API::Entities::BulkImport, feature_category: :importers do let_it_be(:import) { create(:bulk_import) } subject { described_class.new(import).as_json } @@ -13,7 +13,8 @@ RSpec.describe API::Entities::BulkImport do :status, :source_type, :created_at, - :updated_at + :updated_at, + :has_failures ) end end diff --git a/spec/lib/api/entities/bulk_imports/entity_spec.rb b/spec/lib/api/entities/bulk_imports/entity_spec.rb index ba8a2ddffcb..791cd3a20e2 100644 --- a/spec/lib/api/entities/bulk_imports/entity_spec.rb +++ b/spec/lib/api/entities/bulk_imports/entity_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe API::Entities::BulkImports::Entity do +RSpec.describe API::Entities::BulkImports::Entity, feature_category: :importers do let_it_be(:entity) { create(:bulk_import_entity) } subject { described_class.new(entity).as_json } @@ -22,7 +22,8 @@ RSpec.describe API::Entities::BulkImports::Entity do :created_at, :updated_at, :failures, - :migrate_projects + :migrate_projects, + :has_failures ) end end diff --git a/spec/lib/gitlab/checks/tag_check_spec.rb b/spec/lib/gitlab/checks/tag_check_spec.rb index 60d3eb4bfb3..b5aafde006f 100644 --- a/spec/lib/gitlab/checks/tag_check_spec.rb +++ b/spec/lib/gitlab/checks/tag_check_spec.rb @@ -41,6 +41,36 @@ RSpec.describe Gitlab::Checks::TagCheck, feature_category: :source_code_manageme expect { subject.validate! }.not_to raise_error end end + + it "prohibits tag names that include characters incompatible with UTF-8" do + allow(subject).to receive(:tag_name).and_return("v6.0.0-\xCE.BETA") + + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "Tag names must be valid when converted to UTF-8 encoding") + end + + it "doesn't prohibit UTF-8 compatible characters" do + allow(subject).to receive(:tag_name).and_return("v6.0.0-Ü.BETA") + + expect { subject.validate! }.not_to raise_error + end + + context "when prohibited_tag_name_encoding_check feature flag is disabled" do + before do + stub_feature_flags(prohibited_tag_name_encoding_check: false) + end + + it "doesn't prohibit tag names that include characters incompatible with UTF-8" do + allow(subject).to receive(:tag_name).and_return("v6.0.0-\xCE.BETA") + + expect { subject.validate! }.not_to raise_error + end + + it "doesn't prohibit UTF-8 compatible characters" do + allow(subject).to receive(:tag_name).and_return("v6.0.0-Ü.BETA") + + expect { subject.validate! }.not_to raise_error + end + end end context 'with protected tag' do diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb index bc72d1a67d6..1b7c11dfef6 100644 --- a/spec/lib/gitlab/encoding_helper_spec.rb +++ b/spec/lib/gitlab/encoding_helper_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -RSpec.describe Gitlab::EncodingHelper do +RSpec.describe Gitlab::EncodingHelper, feature_category: :shared do using RSpec::Parameterized::TableSyntax let(:ext_class) { Class.new { extend Gitlab::EncodingHelper } } @@ -291,4 +291,39 @@ RSpec.describe Gitlab::EncodingHelper do expect(described_class.strip_bom("BOM at the end\xEF\xBB\xBF")).to eq("BOM at the end\xEF\xBB\xBF") end end + + # This cop's alternative to .dup doesn't work in this context for some reason. + # rubocop: disable Performance/UnfreezeString + describe "#force_encode_utf8" do + let(:stringish) do + Class.new(String) do + undef :force_encoding + end + end + + it "raises an ArgumentError if the argument can't force encoding" do + expect { described_class.force_encode_utf8(stringish.new("foo")) }.to raise_error(ArgumentError) + end + + it "returns the message if already UTF-8 and valid encoding" do + string = "føø".dup + + expect(string).not_to receive(:force_encoding).and_call_original + expect(described_class.force_encode_utf8(string)).to eq("føø") + end + + it "forcibly encodes a string to UTF-8" do + string = "føø".dup.force_encoding("ISO-8859-1") + + expect(string).to receive(:force_encoding).with("UTF-8").and_call_original + expect(described_class.force_encode_utf8(string)).to eq("føø") + end + + it "forcibly encodes a frozen string to UTF-8" do + string = "bår".dup.force_encoding("ISO-8859-1").freeze + + expect(described_class.force_encode_utf8(string)).to eq("bår") + end + end + # rubocop: enable Performance/UnfreezeString end diff --git a/spec/models/packages/protection/rule_spec.rb b/spec/models/packages/protection/rule_spec.rb index b368687e6d8..d59c374b442 100644 --- a/spec/models/packages/protection/rule_spec.rb +++ b/spec/models/packages/protection/rule_spec.rb @@ -10,9 +10,19 @@ RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :pack end describe 'enums' do - describe '#package_type' do - it { is_expected.to define_enum_for(:package_type).with_values(npm: Packages::Package.package_types[:npm]) } - end + it { is_expected.to define_enum_for(:package_type).with_values(npm: Packages::Package.package_types[:npm]) } + + it { + is_expected.to( + define_enum_for(:push_protected_up_to_access_level) + .with_values( + developer: Gitlab::Access::DEVELOPER, + maintainer: Gitlab::Access::MAINTAINER, + owner: Gitlab::Access::OWNER + ) + .with_prefix(:push_protected_up_to) + ) + } end describe 'validations' do @@ -30,11 +40,6 @@ RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :pack describe '#push_protected_up_to_access_level' do it { is_expected.to validate_presence_of(:push_protected_up_to_access_level) } - - it { - is_expected.to validate_inclusion_of(:push_protected_up_to_access_level).in_array([Gitlab::Access::DEVELOPER, - Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER]) - } end end end -- cgit v1.2.3