diff options
74 files changed, 557 insertions, 205 deletions
diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml index 97b36b84172..a8e2f98e174 100644 --- a/.rubocop_manual_todo.yml +++ b/.rubocop_manual_todo.yml @@ -181,16 +181,6 @@ Rails/SaveBang: - 'spec/features/dashboard/issuables_counter_spec.rb' - 'spec/features/dashboard/project_member_activity_index_spec.rb' - 'spec/features/dashboard/projects_spec.rb' - - 'spec/features/issues/bulk_assignment_labels_spec.rb' - - 'spec/features/issues/gfm_autocomplete_spec.rb' - - 'spec/features/issues/issue_sidebar_spec.rb' - - 'spec/features/issues/note_polling_spec.rb' - - 'spec/features/issues/user_creates_branch_and_merge_request_spec.rb' - - 'spec/features/issues/user_creates_confidential_merge_request_spec.rb' - - 'spec/features/issues/user_edits_issue_spec.rb' - - 'spec/features/issues/user_filters_issues_spec.rb' - - 'spec/features/issues/user_sees_live_update_spec.rb' - - 'spec/features/issues/user_sorts_issues_spec.rb' - 'spec/frontend/fixtures/issues.rb' - 'spec/frontend/fixtures/merge_requests.rb' - 'spec/graphql/mutations/merge_requests/set_locked_spec.rb' @@ -302,23 +292,6 @@ Rails/SaveBang: - 'spec/models/user_status_spec.rb' - 'spec/models/wiki_page/meta_spec.rb' - 'spec/models/wiki_page_spec.rb' - - 'spec/requests/api/ci/runner_spec.rb' - - 'spec/requests/api/commit_statuses_spec.rb' - - 'spec/requests/api/conan_packages_spec.rb' - - 'spec/requests/api/deployments_spec.rb' - - 'spec/requests/api/environments_spec.rb' - - 'spec/requests/api/go_proxy_spec.rb' - - 'spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb' - - 'spec/requests/api/graphql/user_query_spec.rb' - - 'spec/requests/api/graphql_spec.rb' - - 'spec/requests/api/group_import_spec.rb' - - 'spec/requests/api/group_milestones_spec.rb' - - 'spec/requests/api/internal/base_spec.rb' - - 'spec/requests/api/issues/get_group_issues_spec.rb' - - 'spec/requests/api/issues/post_projects_issues_spec.rb' - - 'spec/requests/api/jobs_spec.rb' - - 'spec/requests/api/labels_spec.rb' - - 'spec/requests/api/project_import_spec.rb' Rails/TimeZone: Enabled: true @@ -1102,8 +1075,6 @@ RSpec/EmptyLineAfterFinalLetItBe: - spec/models/ci/stage_spec.rb - spec/models/clusters/kubernetes_namespace_spec.rb - spec/models/commit_spec.rb - - spec/models/concerns/batch_destroy_dependent_associations_spec.rb - - spec/models/concerns/featurable_spec.rb - spec/models/deploy_token_spec.rb - spec/models/deployment_spec.rb - spec/models/diff_viewer/server_side_spec.rb @@ -3335,26 +3306,6 @@ Style/HashTransformation: - 'ee/lib/gitlab/custom_file_templates.rb' - 'ee/spec/elastic_integration/global_search_spec.rb' - 'ee/spec/lib/ee/gitlab/application_context_spec.rb' - - 'lib/api/helpers/packages/conan/api_helpers.rb' - - 'lib/api/projects.rb' - - 'lib/atlassian/jira_connect/client.rb' - - 'lib/banzai/filter/repository_link_filter.rb' - - 'lib/gitlab/ci/config/entry/product/variables.rb' - - 'lib/gitlab/ci/config/entry/variables.rb' - - 'lib/gitlab/ci/variables/collection.rb' - - 'lib/gitlab/ci/variables/helpers.rb' - - 'lib/gitlab/git/commit.rb' - - 'lib/gitlab/import_sources.rb' - - 'lib/gitlab/language_detection.rb' - - 'lib/gitlab/metrics/samplers/database_sampler.rb' - - 'lib/gitlab/metrics/subscribers/active_record.rb' - - 'lib/gitlab/phabricator_import/project_creator.rb' - - 'lib/gitlab/prometheus_client.rb' - - 'lib/gitlab/repository_hash_cache.rb' - - 'lib/gitlab/static_site_editor/config/file_config.rb' - - 'lib/gitlab/template/base_template.rb' - - 'lib/gitlab/usage_data_counters/base_counter.rb' - - 'lib/gitlab/usage_data_counters/note_counter.rb' - 'spec/lib/atlassian/jira_connect/serializers/pull_request_entity_spec.rb' - 'spec/lib/gitlab/ci/status/composite_spec.rb' - 'spec/lib/gitlab/conflict/file_spec.rb' diff --git a/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue index 0bcc85157f1..91062c222f4 100644 --- a/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue +++ b/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue @@ -42,6 +42,7 @@ export default { :member-id="member.id" :message="message" :title="s__('Member|Revoke invite')" + is-invite /> </div> </action-button-group> diff --git a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue index 3777f1dbb97..9954da3e0d4 100644 --- a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue +++ b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue @@ -36,6 +36,11 @@ export default { required: false, default: false, }, + isInvite: { + type: Boolean, + required: false, + default: false, + }, }, computed: { ...mapState(['memberPath']), @@ -57,6 +62,7 @@ export default { :data-member-path="computedMemberPath" :data-member-type="memberType" :data-is-access-request="isAccessRequest" + :data-is-invite="isInvite" :data-message="message" data-qa-selector="delete_member_button" /> diff --git a/app/assets/javascripts/vue_shared/components/remove_member_modal.vue b/app/assets/javascripts/vue_shared/components/remove_member_modal.vue index 2a19537f820..9968240b5b1 100644 --- a/app/assets/javascripts/vue_shared/components/remove_member_modal.vue +++ b/app/assets/javascripts/vue_shared/components/remove_member_modal.vue @@ -2,7 +2,7 @@ import { GlFormCheckbox, GlModal } from '@gitlab/ui'; import { parseBoolean } from '~/lib/utils/common_utils'; import csrf from '~/lib/utils/csrf'; -import { __ } from '~/locale'; +import { s__, __ } from '~/locale'; export default { actionCancel: { @@ -22,11 +22,20 @@ export default { isAccessRequest() { return parseBoolean(this.modalData.isAccessRequest); }, + isInvite() { + return parseBoolean(this.modalData.isInvite); + }, isGroupMember() { return this.modalData.memberType === 'GroupMember'; }, actionText() { - return this.isAccessRequest ? __('Deny access request') : __('Remove member'); + if (this.isAccessRequest) { + return __('Deny access request'); + } else if (this.isInvite) { + return s__('Member|Revoke invite'); + } + + return __('Remove member'); }, actionPrimary() { return { @@ -36,6 +45,9 @@ export default { }, }; }, + showUnassignIssuablesCheckbox() { + return !this.isAccessRequest && !this.isInvite; + }, }, mounted() { document.addEventListener('click', this.handleClick); @@ -76,7 +88,7 @@ export default { <gl-form-checkbox v-if="isGroupMember" name="remove_sub_memberships"> {{ __('Also remove direct user membership from subgroups and projects') }} </gl-form-checkbox> - <gl-form-checkbox v-if="!isAccessRequest" name="unassign_issuables"> + <gl-form-checkbox v-if="showUnassignIssuablesCheckbox" name="unassign_issuables"> {{ __('Also unassign this user from related issues and merge requests') }} </gl-form-checkbox> </form> diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb index 2d766a14291..94a256fed3d 100644 --- a/app/graphql/types/ci/job_type.rb +++ b/app/graphql/types/ci/job_type.rb @@ -26,6 +26,8 @@ module Types description: 'Whether this job is allowed to fail.' field :duration, GraphQL::INT_TYPE, null: true, description: 'Duration of the job in seconds.' + field :tags, [GraphQL::STRING_TYPE], null: true, + description: 'Tags for the current job.' # Life-cycle timestamps: field :created_at, Types::TimeType, null: false, @@ -68,6 +70,10 @@ module Types Gitlab::Graphql::Loaders::BatchModelLoader.new(::Ci::Pipeline, object.pipeline_id).find end + def tags + object.tags.map(&:name) if object.is_a?(::Ci::Build) + end + def detailed_status object.detailed_status(context[:current_user]) end diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb index 8c9c861e1fe..ea71930062c 100644 --- a/app/uploaders/object_storage.rb +++ b/app/uploaders/object_storage.rb @@ -187,7 +187,7 @@ module ObjectStorage hash[:TempPath] = workhorse_local_upload_path end - hash[:FeatureFlagExtractBase] = Feature.enabled?(:workhorse_extract_filename_base) + hash[:FeatureFlagExtractBase] = Feature.enabled?(:workhorse_extract_filename_base, default_enabled: :yaml) hash[:MaximumSize] = maximum_size if maximum_size.present? end end diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index cec8948aaa4..1c7a9ffe0bb 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -8,7 +8,7 @@ .content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed .files-changed-inner - .inline-parallel-buttons.d-none.d-md-block + .inline-parallel-buttons.gl-display-none.gl-md-display-flex - if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? } = link_to _('Expand all'), url_for(safe_params.merge(expanded: 1, format: nil)), class: 'gl-button btn btn-default' - if show_whitespace_toggle @@ -20,7 +20,7 @@ = diff_compare_whitespace_link(diffs.project, params[:from], params[:to], class: 'd-none d-sm-inline-block') - elsif current_controller?(:wikis) = toggle_whitespace_link(url_for(params_with_whitespace), class: 'd-none d-sm-inline-block') - .btn-group + .btn-group.gl-ml-3 = inline_diff_btn = parallel_diff_btn = render 'projects/diffs/stats', diff_files: diff_files diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 9197b177b7b..35e2fe1b398 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -14,10 +14,10 @@ = submodule_diff_compare_link(diff_file) - unless diff_file.submodule? - .file-actions.d-none.d-sm-block + .file-actions.gl-display-none.gl-sm-display-flex - if diff_file.blob&.readable_text? - %span.has-tooltip{ title: _("Toggle comments for this file") } - = link_to '#', class: 'js-toggle-diff-comments btn gl-button btn-default selected', disabled: @diff_notes_disabled do + %span.has-tooltip.gl-mr-3{ title: _("Toggle comments for this file") } + = link_to '#', class: 'js-toggle-diff-comments btn gl-button btn-default btn-icon selected', disabled: @diff_notes_disabled do = sprite_icon('comment') \ - if editable_diff?(diff_file) diff --git a/app/views/shared/notes/_comment_button.html.haml b/app/views/shared/notes/_comment_button.html.haml index 3e81b1fcb13..d415c64b929 100644 --- a/app/views/shared/notes/_comment_button.html.haml +++ b/app/views/shared/notes/_comment_button.html.haml @@ -1,15 +1,15 @@ - noteable_name = @note.noteable.human_class_name .float-left.btn-group.gl-mr-3.droplab-dropdown.comment-type-dropdown.js-comment-type-dropdown - %input.btn.gl-button.btn-success.js-comment-button.js-comment-submit-button{ type: 'submit', value: _('Comment'), data: { qa_selector: 'comment_button' } } + %input.btn.gl-button.btn-confirm.js-comment-button.js-comment-submit-button{ type: 'submit', value: _('Comment'), data: { qa_selector: 'comment_button' } } - if @note.can_be_discussion_note? - = button_tag type: 'button', class: 'gl-button btn dropdown-toggle btn-success js-note-new-discussion js-disable-on-submit', data: { 'dropdown-trigger' => '#resolvable-comment-menu' }, 'aria-label' => _('Open comment type dropdown') do + = button_tag type: 'button', class: 'gl-button btn dropdown-toggle btn-confirm btn-icon js-note-new-discussion js-disable-on-submit', data: { 'dropdown-trigger' => '#resolvable-comment-menu' }, 'aria-label' => _('Open comment type dropdown') do = sprite_icon('chevron-down') %ul#resolvable-comment-menu.dropdown-menu.dropdown-open-top{ data: { dropdown: true } } %li#comment.droplab-item-selected{ data: { value: '', 'submit-text' => _('Comment'), 'close-text' => _("Comment & close %{noteable_name}") % { noteable_name: noteable_name }, 'reopen-text' => _("Comment & reopen %{noteable_name}") % { noteable_name: noteable_name } } } - %button{ type: 'button' } + %button.btn.gl-button.btn-default-tertiary = sprite_icon('check', css_class: 'icon') .description %strong= _("Comment") @@ -19,7 +19,7 @@ %li.divider.droplab-item-ignore %li#discussion{ data: { value: 'DiscussionNote', 'submit-text' => _('Start thread'), 'close-text' => _("Start thread & close %{noteable_name}") % { noteable_name: noteable_name }, 'reopen-text' => _("Start thread & reopen %{noteable_name}") % { noteable_name: noteable_name } } } - %button{ type: 'button' } + %button.btn.gl-button.btn-default-tertiary = sprite_icon('check', css_class: 'icon') .description %strong= _("Start thread") diff --git a/changelogs/unreleased/259794-members-view-update-revoke-invite-modal-header-and-button.yml b/changelogs/unreleased/259794-members-view-update-revoke-invite-modal-header-and-button.yml new file mode 100644 index 00000000000..4aa3e7ee55d --- /dev/null +++ b/changelogs/unreleased/259794-members-view-update-revoke-invite-modal-header-and-button.yml @@ -0,0 +1,5 @@ +--- +title: Update title on revoke member invite modal and hide unneeded related issues and merge requests checkbox +merge_request: 57755 +author: +type: changed diff --git a/changelogs/unreleased/273284-update-commit-page-buttons.yml b/changelogs/unreleased/273284-update-commit-page-buttons.yml new file mode 100644 index 00000000000..95257d0f6d3 --- /dev/null +++ b/changelogs/unreleased/273284-update-commit-page-buttons.yml @@ -0,0 +1,5 @@ +--- +title: Update buttons and spacing on commit page +merge_request: 56793 +author: +type: changed diff --git a/changelogs/unreleased/issue-220040-fix-rails-savebang-requests-api-module.yml b/changelogs/unreleased/issue-220040-fix-rails-savebang-requests-api-module.yml new file mode 100644 index 00000000000..316bc1d5cc0 --- /dev/null +++ b/changelogs/unreleased/issue-220040-fix-rails-savebang-requests-api-module.yml @@ -0,0 +1,5 @@ +--- +title: Fix Rails/SaveBang Rubocop offenses for requests/api module +merge_request: 57887 +author: Huzaifa Iftikhar @huzaifaiftikhar +type: fixed diff --git a/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-models-concerns.yml b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-models-concerns.yml new file mode 100644 index 00000000000..27eb4d38a23 --- /dev/null +++ b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-models-concerns.yml @@ -0,0 +1,5 @@ +--- +title: Fix EmptyLineAfterFinalLetItBe offenses in spec/models/concerns +merge_request: 58367 +author: Huzaifa Iftikhar @huzaifaiftikhar +type: fixed diff --git a/changelogs/unreleased/jivanvl-add-tags-job-type-graphql.yml b/changelogs/unreleased/jivanvl-add-tags-job-type-graphql.yml new file mode 100644 index 00000000000..fbfed90706d --- /dev/null +++ b/changelogs/unreleased/jivanvl-add-tags-job-type-graphql.yml @@ -0,0 +1,5 @@ +--- +title: Add tags field to jobType in the CI namespace +merge_request: 57631 +author: +type: changed diff --git a/changelogs/unreleased/pl-rubocop-style-hashtransformation-lib-directory.yml b/changelogs/unreleased/pl-rubocop-style-hashtransformation-lib-directory.yml new file mode 100644 index 00000000000..e2ee3eade14 --- /dev/null +++ b/changelogs/unreleased/pl-rubocop-style-hashtransformation-lib-directory.yml @@ -0,0 +1,5 @@ +--- +title: Fix cop offenses for Style/HashTransformation in lib directory +merge_request: 56583 +author: Karthik Sivadas @karthik.sivadas +type: other diff --git a/changelogs/unreleased/rails-save-bang-features-issues.yml b/changelogs/unreleased/rails-save-bang-features-issues.yml new file mode 100644 index 00000000000..6942bd383dd --- /dev/null +++ b/changelogs/unreleased/rails-save-bang-features-issues.yml @@ -0,0 +1,5 @@ +--- +title: Fix Rails/SaveBang rubocop offenses in spec/features/issues +merge_request: 57900 +author: Abdul Wadood @abdulwd +type: fixed diff --git a/changelogs/unreleased/sh-enable-workhorse-extract-filename-base-default.yml b/changelogs/unreleased/sh-enable-workhorse-extract-filename-base-default.yml new file mode 100644 index 00000000000..cd2d5e8410c --- /dev/null +++ b/changelogs/unreleased/sh-enable-workhorse-extract-filename-base-default.yml @@ -0,0 +1,5 @@ +--- +title: Set workhorse_extract_filename_base feature flag to default +merge_request: 58504 +author: +type: changed diff --git a/config/feature_flags/development/workhorse_extract_filename_base.yml b/config/feature_flags/development/workhorse_extract_filename_base.yml index ee0442bb016..a80f6c45f28 100644 --- a/config/feature_flags/development/workhorse_extract_filename_base.yml +++ b/config/feature_flags/development/workhorse_extract_filename_base.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/326379 milestone: '13.11' type: development group: group::source code -default_enabled: false +default_enabled: true diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md index f886e19b13d..a168584e754 100644 --- a/doc/administration/environment_variables.md +++ b/doc/administration/environment_variables.md @@ -21,6 +21,9 @@ You can use the following environment variables to override certain values: |--------------------------------------------|---------|---------------------------------------------------------------------------------------------------------| | `DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development`. | | `ENABLE_BOOTSNAP` | string | Enables Bootsnap for speeding up initial Rails boot (`1` to enable). | +| `EXTERNAL_VALIDATION_SERVICE_TIMEOUT` | integer | Timeout, in seconds, for an [external CI/CD pipeline validation service](external_pipeline_validation.md). Default is `5`. | +| `EXTERNAL_VALIDATION_SERVICE_URL` | string | URL to an [external CI/CD pipeline validation service](external_pipeline_validation.md). | +| `EXTERNAL_VALIDATION_SERVICE_TOKEN` | string | The `X-Gitlab-Token` for authentication with an [external CI/CD pipeline validation service](external_pipeline_validation.md). | | `GITLAB_CDN_HOST` | string | Sets the base URL for a CDN to serve static assets (for example, `//mycdnsubdomain.fictional-cdn.com`). | | `GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the **From** field in emails sent by GitLab. | | `GITLAB_EMAIL_FROM` | string | The email address used in the **From** field in emails sent by GitLab. | @@ -29,8 +32,8 @@ You can use the following environment variables to override certain values: | `GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`). | | `GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation. | | `GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN` | string | Sets the initial registration token used for runners. | -| `GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the [unicorn-worker-killer](operations/unicorn.md#unicorn-worker-killer). | -| `GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the [unicorn-worker-killer](operations/unicorn.md#unicorn-worker-killer). | +| `GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the [unicorn-worker-killer](operations/unicorn.md#unicorn-worker-killer). | +| `GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the [unicorn-worker-killer](operations/unicorn.md#unicorn-worker-killer). | | `RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging`, or `test`. | | `UNSTRUCTURED_RAILS_LOG` | string | Enables the unstructured log in addition to JSON logs (defaults to `true`). | diff --git a/doc/administration/external_pipeline_validation.md b/doc/administration/external_pipeline_validation.md index 482ca320736..f8329b24d6c 100644 --- a/doc/administration/external_pipeline_validation.md +++ b/doc/administration/external_pipeline_validation.md @@ -7,7 +7,7 @@ type: reference, howto # External Pipeline Validation -You can use an external service for validating a pipeline before it's created. +You can use an external service to validate a pipeline before it's created. WARNING: This is an experimental feature and subject to change without notice. @@ -19,19 +19,17 @@ data as payload. GitLab then invalidates the pipeline based on the response code. If there's an error or the request times out, the pipeline is not invalidated. -Response Code Legend: +Response codes: -- `200` - Accepted -- `406` - Not Accepted -- Other Codes - Accepted and Logged +- `200`: Accepted +- `4XX`: Not accepted +- All other codes: accepted and logged ## Configuration -To configure external pipeline validation: - -1. Set the `EXTERNAL_VALIDATION_SERVICE_URL` environment variable to the external - service URL. -1. Enable the `ci_external_validation_service` feature flag. +To configure external pipeline validation, add the +[`EXTERNAL_VALIDATION_SERVICE_URL` environment variable](environment_variables.md) +and set it to the external service URL. By default, requests to the external service time out after five seconds. To override the default, set the `EXTERNAL_VALIDATION_SERVICE_TIMEOUT` environment variable to the @@ -131,3 +129,6 @@ required number of seconds. } } ``` + +The `namespace` field is only available in [GitLab Premium](https://about.gitlab.com/pricing/) +and higher. diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index fe85d43fd14..303b8c5b574 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1235,6 +1235,7 @@ An edge in a connection. | `stage` | [`CiStage`](#cistage) | Stage of the job. | | `startedAt` | [`Time`](#time) | When the job was started. | | `status` | [`CiJobStatus`](#cijobstatus) | Status of the job. | +| `tags` | [`[String!]`](#string) | Tags for the current job. | ### `CiJobArtifact` diff --git a/doc/development/cicd/index.md b/doc/development/cicd/index.md index eb2224d710a..242446590bb 100644 --- a/doc/development/cicd/index.md +++ b/doc/development/cicd/index.md @@ -182,3 +182,17 @@ Watch a walkthrough of this feature in details in the video below. <figure class="video-container"> <iframe src="https://www.youtube.com/embed/NmdWRGT8kZg" frameborder="0" allowfullscreen="true"> </iframe> </figure> + +## External pipeline validation service + +The [external CI/CD pipeline validation service](../../administration/external_pipeline_validation.md) +is available for use on self-managed GitLab instances, but is not in use on GitLab.com. +It is configured with [environment variables](../../administration/environment_variables.md) +on the instance. + +To enable the feature on GitLab.com, enable the `ci_external_validation_service` +[feature flag](../feature_flags/index.md). The valid "Not accepted" response code +for GitLab.com is `406` only. + +For more details, see the linked issues and MRs in the +[feature flag rollout issue](https://gitlab.com/gitlab-org/gitlab/-/issues/325982). diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md index 78f8e320269..1842bf5e7f8 100644 --- a/doc/development/testing_guide/frontend_testing.md +++ b/doc/development/testing_guide/frontend_testing.md @@ -212,8 +212,8 @@ When it comes to querying DOM elements in your tests, it is best to uniquely and the element. Preferentially, this is done by targeting what the user actually sees using [DOM Testing Library](https://testing-library.com/docs/dom-testing-library/intro/). -When selecting by text it is best to use [`getByRole` or `findByRole`](https://testing-library.com/docs/queries/byrole/) -as these enforce accessibility best practices as well. The examples below demonstrate the order of preference. +When selecting by text it is best to use the [`byRole`](https://testing-library.com/docs/queries/byrole) query +as it helps enforce accessibility best practices. `findByRole` and the other [DOM Testing Library queries](https://testing-library.com/docs/queries/about) are available when using [`shallowMountExtended` or `mountExtended`](#shallowmountextended-and-mountextended). When writing Vue component unit tests, it can be wise to query children by component, so that the unit test can focus on comprehensive value coverage rather than dealing with the complexity of a child component's behavior. @@ -223,25 +223,27 @@ possible selectors include: - A semantic attribute like `name` (also verifies that `name` was setup properly) - A `data-testid` attribute ([recommended by maintainers of `@vue/test-utils`](https://github.com/vuejs/vue-test-utils/issues/1498#issuecomment-610133465)) - optionally combined with [`findByTestId`](#extendedwrapper-and-findbytestid) + optionally combined with [`shallowMountExtended` or `mountExtended`](#shallowmountextended-and-mountextended) - a Vue `ref` (if using `@vue/test-utils`) ```javascript -import { getByRole, getByText } from '@testing-library/dom' +import { shallowMountExtended } from 'helpers/vue_test_utils_helper' + +const wrapper = shallowMountExtended(ExampleComponent); // In this example, `wrapper` is a `@vue/test-utils` wrapper returned from `mount` or `shallowMount`. it('exists', () => { // Best (especially for integration tests) - getByRole(wrapper.element, 'link', { name: /Click Me/i }) - getByRole(wrapper.element, 'link', { name: 'Click Me' }) - getByText(wrapper.element, 'Click Me') - getByText(wrapper.element, /Click Me/i) + wrapper.findByRole('link', { name: /Click Me/i }) + wrapper.findByRole('link', { name: 'Click Me' }) + wrapper.findByText('Click Me') + wrapper.findByText(/Click Me/i) // Good (especially for unit tests) wrapper.find(FooComponent); wrapper.find('input[name=foo]'); wrapper.find('[data-testid="my-foo-id"]'); - wrapper.findByTestId('my-foo-id'); // with the extendedWrapper utility – check below + wrapper.findByTestId('my-foo-id'); // with shallowMountExtended or mountExtended – check below wrapper.find({ ref: 'foo'}); // Bad @@ -1138,23 +1140,40 @@ These are very useful if you don't have a handle to the request's Promise, for e Both functions run `callback` on the next tick after the requests finish (using `setImmediate()`), to allow any `.then()` or `.catch()` handlers to run. -### `extendedWrapper` and `findByTestId` +### `shallowMountExtended` and `mountExtended` -Using `data-testid` is one of the [recommended ways to query DOM elements](#how-to-query-dom-elements). -You can use the `extendedWrapper` utility on the `wrapper` returned by `shalowMount`/`mount`. -By doing so, the `wrapper` provides you with the ability to perform a `findByTestId`, -which is a shortcut to the more verbose `wrapper.find('[data-testid="my-test-id"]');` +The `shallowMountExtended` and `mountExtended` utilities provide you with the ability to perform +any of the available [DOM Testing Library queries](https://testing-library.com/docs/queries/about) +by prefixing them with `find` or `findAll`. ```javascript -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; describe('FooComponent', () => { - const wrapper = extendedWrapper(shallowMount({ - template: `<div data-testid="my-test-id"></div>`, - })); + const wrapper = shallowMountExtended({ + template: ` + <div data-testid="gitlab-frontend-stack"> + <p>GitLab frontend stack</p> + <div role="tablist"> + <button role="tab" aria-selected="true">Vue.js</button> + <button role="tab" aria-selected="false">GraphQL</button> + <button role="tab" aria-selected="false">SCSS</button> + </div> + </div> + `, + }); + + it('finds elements with `findByTestId`', () => { + expect(wrapper.findByTestId('gitlab-frontend-stack').exists()).toBe(true); + }); + + it('finds elements with `findByText`', () => { + expect(wrapper.findByText('GitLab frontend stack').exists()).toBe(true); + expect(wrapper.findByText('TypeScript').exists()).toBe(false); + }); - it('exists', () => { - expect(wrapper.findByTestId('my-test-id').exists()).toBe(true); + it('finds elements with `findAllByRole`', () => { + expect(wrapper.findAllByRole('tab').length).toBe(3); }); }); ``` diff --git a/lib/api/helpers/packages/conan/api_helpers.rb b/lib/api/helpers/packages/conan/api_helpers.rb index 24ebeb007d3..5499b6fe653 100644 --- a/lib/api/helpers/packages/conan/api_helpers.rb +++ b/lib/api/helpers/packages/conan/api_helpers.rb @@ -32,19 +32,15 @@ module API end def recipe_upload_urls - { upload_urls: Hash[ - file_names.select(&method(:recipe_file?)).map do |file_name| - [file_name, build_recipe_file_upload_url(file_name)] - end - ] } + { upload_urls: file_names.select(&method(:recipe_file?)).to_h do |file_name| + [file_name, build_recipe_file_upload_url(file_name)] + end } end def package_upload_urls - { upload_urls: Hash[ - file_names.select(&method(:package_file?)).map do |file_name| - [file_name, build_package_file_upload_url(file_name)] - end - ] } + { upload_urls: file_names.select(&method(:package_file?)).to_h do |file_name| + [file_name, build_package_file_upload_url(file_name)] + end } end def recipe_file?(file_name) diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 880ece9048a..7674e67cd8f 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -485,7 +485,7 @@ module API get ':id/languages', feature_category: :source_code_management do ::Projects::RepositoryLanguagesService .new(user_project, current_user) - .execute.map { |lang| [lang.name, lang.share] }.to_h + .execute.to_h { |lang| [lang.name, lang.share] } end desc 'Delete a project' diff --git a/lib/atlassian/jira_connect/client.rb b/lib/atlassian/jira_connect/client.rb index 6f87b7b7d3c..ea83076c49b 100644 --- a/lib/atlassian/jira_connect/client.rb +++ b/lib/atlassian/jira_connect/client.rb @@ -141,9 +141,9 @@ module Atlassian def user_notes_count(merge_requests) return unless merge_requests - Note.count_for_collection(merge_requests.map(&:id), 'MergeRequest').map do |count_group| + Note.count_for_collection(merge_requests.map(&:id), 'MergeRequest').to_h do |count_group| [count_group.noteable_id, count_group.count] - end.to_h + end end def jwt_token(http_method, uri) diff --git a/lib/banzai/filter/repository_link_filter.rb b/lib/banzai/filter/repository_link_filter.rb index 66b9aac3e7e..04bbcabd93f 100644 --- a/lib/banzai/filter/repository_link_filter.rb +++ b/lib/banzai/filter/repository_link_filter.rb @@ -60,7 +60,7 @@ module Banzai def get_uri_types(paths) return {} if paths.empty? - uri_types = Hash[paths.collect { |name| [name, nil] }] + uri_types = paths.to_h { |name| [name, nil] } get_blob_types(paths).each do |name, type| if type == :blob diff --git a/lib/gitlab/ci/config/entry/product/variables.rb b/lib/gitlab/ci/config/entry/product/variables.rb index aa34cfb3acc..e869e0bbb31 100644 --- a/lib/gitlab/ci/config/entry/product/variables.rb +++ b/lib/gitlab/ci/config/entry/product/variables.rb @@ -25,8 +25,7 @@ module Gitlab def value @config - .map { |key, value| [key.to_s, Array(value).map(&:to_s)] } - .to_h + .to_h { |key, value| [key.to_s, Array(value).map(&:to_s)] } end end end diff --git a/lib/gitlab/ci/config/entry/variables.rb b/lib/gitlab/ci/config/entry/variables.rb index dc164d752be..efb469ee32a 100644 --- a/lib/gitlab/ci/config/entry/variables.rb +++ b/lib/gitlab/ci/config/entry/variables.rb @@ -18,7 +18,7 @@ module Gitlab end def value - Hash[@config.map { |key, value| [key.to_s, expand_value(value)[:value]] }] + @config.to_h { |key, value| [key.to_s, expand_value(value)[:value]] } end def self.default(**) @@ -26,7 +26,7 @@ module Gitlab end def value_with_data - Hash[@config.map { |key, value| [key.to_s, expand_value(value)] }] + @config.to_h { |key, value| [key.to_s, expand_value(value)] } end def use_value_data? diff --git a/lib/gitlab/ci/variables/helpers.rb b/lib/gitlab/ci/variables/helpers.rb index 2c3457e0265..3a62f01e2e3 100644 --- a/lib/gitlab/ci/variables/helpers.rb +++ b/lib/gitlab/ci/variables/helpers.rb @@ -23,7 +23,7 @@ module Gitlab def transform_from_yaml_variables(vars) return vars.stringify_keys if vars.is_a?(Hash) - vars.to_a.map { |var| [var[:key].to_s, var[:value]] }.to_h + vars.to_a.to_h { |var| [var[:key].to_s, var[:value]] } end def inherit_yaml_variables(from:, to:, inheritance:) diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index ff99803d8de..51baed32935 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -390,7 +390,7 @@ module Gitlab @committer_name = commit.committer.name.dup @committer_email = commit.committer.email.dup @parent_ids = Array(commit.parent_ids) - @trailers = Hash[commit.trailers.map { |t| [t.key, t.value] }] + @trailers = commit.trailers.to_h { |t| [t.key, t.value] } end # Gitaly provides a UNIX timestamp in author.date.seconds, and a timezone diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb index 88753e80391..95c002edf0a 100644 --- a/lib/gitlab/import_sources.rb +++ b/lib/gitlab/import_sources.rb @@ -28,7 +28,7 @@ module Gitlab prepend_if_ee('EE::Gitlab::ImportSources') # rubocop: disable Cop/InjectEnterpriseEditionModule def options - Hash[import_table.map { |importer| [importer.title, importer.name] }] + import_table.to_h { |importer| [importer.title, importer.name] } end def values diff --git a/lib/gitlab/language_detection.rb b/lib/gitlab/language_detection.rb index 7600e60b904..1e5edb79f10 100644 --- a/lib/gitlab/language_detection.rb +++ b/lib/gitlab/language_detection.rb @@ -20,7 +20,7 @@ module Gitlab # Newly detected languages, returned in a structure accepted by # Gitlab::Database.bulk_insert def insertions(programming_languages) - lang_to_id = programming_languages.map { |p| [p.name, p.id] }.to_h + lang_to_id = programming_languages.to_h { |p| [p.name, p.id] } (languages - previous_language_names).map do |new_lang| { @@ -63,8 +63,7 @@ module Gitlab @repository .languages .first(MAX_LANGUAGES) - .map { |l| [l[:label], l] } - .to_h + .to_h { |l| [l[:label], l] } end end end diff --git a/lib/gitlab/metrics/samplers/database_sampler.rb b/lib/gitlab/metrics/samplers/database_sampler.rb index 60ae22df607..c0336a4d0fb 100644 --- a/lib/gitlab/metrics/samplers/database_sampler.rb +++ b/lib/gitlab/metrics/samplers/database_sampler.rb @@ -32,9 +32,9 @@ module Gitlab private def init_metrics - METRIC_DESCRIPTIONS.map do |name, description| + METRIC_DESCRIPTIONS.to_h do |name, description| [name, ::Gitlab::Metrics.gauge(:"#{METRIC_PREFIX}#{name}", description)] - end.to_h + end end def host_stats diff --git a/lib/gitlab/phabricator_import/project_creator.rb b/lib/gitlab/phabricator_import/project_creator.rb index 5ace31386e0..c842798ca74 100644 --- a/lib/gitlab/phabricator_import/project_creator.rb +++ b/lib/gitlab/phabricator_import/project_creator.rb @@ -55,12 +55,13 @@ module Gitlab end def project_feature_attributes - @project_features_attributes ||= begin - # everything disabled except for issues - ProjectFeature::FEATURES.map do |feature| - [ProjectFeature.access_level_attribute(feature), ProjectFeature::DISABLED] - end.to_h.merge(ProjectFeature.access_level_attribute(:issues) => ProjectFeature::ENABLED) - end + @project_features_attributes ||= + begin + # everything disabled except for issues + ProjectFeature::FEATURES.to_h do |feature| + [ProjectFeature.access_level_attribute(feature), ProjectFeature::DISABLED] + end.merge(ProjectFeature.access_level_attribute(:issues) => ProjectFeature::ENABLED) + end end def import_data diff --git a/lib/gitlab/prometheus_client.rb b/lib/gitlab/prometheus_client.rb index 965349ad711..0fcf63d03fc 100644 --- a/lib/gitlab/prometheus_client.rb +++ b/lib/gitlab/prometheus_client.rb @@ -140,7 +140,7 @@ module Gitlab end def mapped_options - options.keys.map { |k| [gitlab_http_key(k), options[k]] }.to_h + options.keys.to_h { |k| [gitlab_http_key(k), options[k]] } end def http_options diff --git a/lib/gitlab/repository_hash_cache.rb b/lib/gitlab/repository_hash_cache.rb index d479d3115a6..430f3e8d162 100644 --- a/lib/gitlab/repository_hash_cache.rb +++ b/lib/gitlab/repository_hash_cache.rb @@ -148,7 +148,7 @@ module Gitlab # @param hash [Hash] # @return [Hash] the stringified hash def standardize_hash(hash) - hash.map { |k, v| [k.to_s, v.to_s] }.to_h + hash.to_h { |k, v| [k.to_s, v.to_s] } end # Record metrics in Prometheus. diff --git a/lib/gitlab/static_site_editor/config/file_config.rb b/lib/gitlab/static_site_editor/config/file_config.rb index 315c603c1dd..4180f6ccf00 100644 --- a/lib/gitlab/static_site_editor/config/file_config.rb +++ b/lib/gitlab/static_site_editor/config/file_config.rb @@ -28,7 +28,7 @@ module Gitlab def to_hash_with_defaults # NOTE: The current approach of simply mapping all the descendents' keys and values ('config') # into a flat hash may need to be enhanced as we add more complex, non-scalar entries. - @global.descendants.map { |descendant| [descendant.key, descendant.config] }.to_h + @global.descendants.to_h { |descendant| [descendant.key, descendant.config] } end private diff --git a/lib/gitlab/template/base_template.rb b/lib/gitlab/template/base_template.rb index dc006877129..31e11f73fe7 100644 --- a/lib/gitlab/template/base_template.rb +++ b/lib/gitlab/template/base_template.rb @@ -130,10 +130,10 @@ module Gitlab return [] if project && !project.repository.exists? if categories.any? - categories.keys.map do |category| + categories.keys.to_h do |category| files = self.by_category(category, project) [category, files.map { |t| { key: t.key, name: t.name, content: t.content } }] - end.to_h + end else files = self.all(project) files.map { |t| { key: t.key, name: t.name, content: t.content } } diff --git a/lib/gitlab/usage_data_counters/base_counter.rb b/lib/gitlab/usage_data_counters/base_counter.rb index d28fd17a989..4ab310a2519 100644 --- a/lib/gitlab/usage_data_counters/base_counter.rb +++ b/lib/gitlab/usage_data_counters/base_counter.rb @@ -22,11 +22,11 @@ module Gitlab::UsageDataCounters end def totals - known_events.map { |event| [counter_key(event), read(event)] }.to_h + known_events.to_h { |event| [counter_key(event), read(event)] } end def fallback_totals - known_events.map { |event| [counter_key(event), -1] }.to_h + known_events.to_h { |event| [counter_key(event), -1] } end def fetch_supported_event(event_name) diff --git a/lib/gitlab/usage_data_counters/note_counter.rb b/lib/gitlab/usage_data_counters/note_counter.rb index 7a76180cb08..aae2d144c5b 100644 --- a/lib/gitlab/usage_data_counters/note_counter.rb +++ b/lib/gitlab/usage_data_counters/note_counter.rb @@ -24,13 +24,13 @@ module Gitlab::UsageDataCounters end def totals - COUNTABLE_TYPES.map do |countable_type| + COUNTABLE_TYPES.to_h do |countable_type| [counter_key(countable_type), read(:create, countable_type)] - end.to_h + end end def fallback_totals - COUNTABLE_TYPES.map { |counter_key| [counter_key(counter_key), -1] }.to_h + COUNTABLE_TYPES.to_h { |counter_key| [counter_key(counter_key), -1] } end private diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb index aa61aff3b05..80bf964e2ee 100644 --- a/spec/features/issues/bulk_assignment_labels_spec.rb +++ b/spec/features/issues/bulk_assignment_labels_spec.rb @@ -295,8 +295,8 @@ RSpec.describe 'Issues > Labels bulk assignment' do before do issue1.milestone = milestone issue2.milestone = milestone - issue1.save - issue2.save + issue1.save! + issue2.save! issue1.labels << bug issue2.labels << feature diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index ca44978d223..bbf2a889880 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -171,7 +171,7 @@ RSpec.describe 'Issue Sidebar' do context 'editing issue labels', :js do before do - issue.update(labels: [label]) + issue.update!(labels: [label]) page.within('.block.labels') do click_on 'Edit' end diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb index bc4c67fdd79..5e02d5ad038 100644 --- a/spec/features/issues/note_polling_spec.rb +++ b/spec/features/issues/note_polling_spec.rb @@ -103,7 +103,7 @@ RSpec.describe 'Issue notes polling', :js do end def update_note(note, new_text) - note.update(note: new_text) + note.update!(note: new_text) wait_for_requests end diff --git a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb index e225a45481d..6e8b3e4fb7c 100644 --- a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb +++ b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb @@ -150,7 +150,7 @@ RSpec.describe 'User creates branch and merge request on issue page', :js do context 'when merge requests are disabled' do before do - project.project_feature.update(merge_requests_access_level: 0) + project.project_feature.update!(merge_requests_access_level: 0) visit project_issue_path(project, issue) end diff --git a/spec/features/issues/user_creates_confidential_merge_request_spec.rb b/spec/features/issues/user_creates_confidential_merge_request_spec.rb index ea96165d7b7..6b4526cd624 100644 --- a/spec/features/issues/user_creates_confidential_merge_request_spec.rb +++ b/spec/features/issues/user_creates_confidential_merge_request_spec.rb @@ -38,7 +38,7 @@ RSpec.describe 'User creates confidential merge request on issue page', :js do let(:forked_project) { fork_project(project, user, repository: true) } before do - forked_project.update(visibility: Gitlab::VisibilityLevel::PRIVATE) + forked_project.update!(visibility: Gitlab::VisibilityLevel::PRIVATE) visit_confidential_issue end diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb index b8e35c9afa3..4b0886ee85e 100644 --- a/spec/features/issues/user_edits_issue_spec.rb +++ b/spec/features/issues/user_edits_issue_spec.rb @@ -78,7 +78,7 @@ RSpec.describe "Issues > User edits issue", :js do end it 'warns about version conflict' do - issue.update(title: "New title") + issue.update!(title: "New title") fill_in 'issue_title', with: 'bug 345' fill_in 'issue_description', with: 'bug description' @@ -307,7 +307,7 @@ RSpec.describe "Issues > User edits issue", :js do before do project.add_guest(guest) issue.milestone = milestone - issue.save + issue.save! end it 'shows milestone text' do diff --git a/spec/features/issues/user_filters_issues_spec.rb b/spec/features/issues/user_filters_issues_spec.rb index 1b246181523..5d05df6aaf0 100644 --- a/spec/features/issues/user_filters_issues_spec.rb +++ b/spec/features/issues/user_filters_issues_spec.rb @@ -18,7 +18,7 @@ RSpec.describe 'User filters issues', :js do @issue = Issue.find_by(title: 'foobar') @issue.milestone = create(:milestone, project: project) @issue.assignees = [] - @issue.save + @issue.save! end let(:issue) { @issue } diff --git a/spec/features/issues/user_sees_live_update_spec.rb b/spec/features/issues/user_sees_live_update_spec.rb index 79c6978cbc0..7e4880f209e 100644 --- a/spec/features/issues/user_sees_live_update_spec.rb +++ b/spec/features/issues/user_sees_live_update_spec.rb @@ -18,7 +18,7 @@ RSpec.describe 'Issues > User sees live update', :js do expect(page).to have_text("new title") - issue.update(title: "updated title") + issue.update!(title: "updated title") wait_for_requests expect(page).to have_text("updated title") diff --git a/spec/features/issues/user_sorts_issues_spec.rb b/spec/features/issues/user_sorts_issues_spec.rb index f0bb055c6f2..c161e1deb83 100644 --- a/spec/features/issues/user_sorts_issues_spec.rb +++ b/spec/features/issues/user_sorts_issues_spec.rb @@ -77,7 +77,7 @@ RSpec.describe "User sorts issues" do it 'sorts by most recently updated', :js do issue3.updated_at = Time.now + 100 - issue3.save + issue3.save! visit project_issues_path(project, sort: sort_value_recently_updated) expect(first_issue).to include('baz') @@ -85,8 +85,8 @@ RSpec.describe "User sorts issues" do describe 'sorting by due date', :js do before do - issue1.update(due_date: 1.day.from_now) - issue2.update(due_date: 6.days.from_now) + issue1.update!(due_date: 1.day.from_now) + issue2.update!(due_date: 6.days.from_now) end it 'sorts by due date' do @@ -96,7 +96,7 @@ RSpec.describe "User sorts issues" do end it 'sorts by due date by excluding nil due dates' do - issue2.update(due_date: nil) + issue2.update!(due_date: nil) visit project_issues_path(project, sort: sort_value_due_date) @@ -111,7 +111,7 @@ RSpec.describe "User sorts issues" do end it 'sorts by least recently due date by excluding nil due dates' do - issue2.update(due_date: nil) + issue2.update!(due_date: nil) visit project_issues_path(project, label_names: [label.name], sort: sort_value_due_date_later) @@ -122,8 +122,8 @@ RSpec.describe "User sorts issues" do describe 'filtering by due date', :js do before do - issue1.update(due_date: 1.day.from_now) - issue2.update(due_date: 6.days.from_now) + issue1.update!(due_date: 1.day.from_now) + issue2.update!(due_date: 6.days.from_now) end it 'filters by none' do @@ -147,9 +147,9 @@ RSpec.describe "User sorts issues" do end it 'filters by due this week' do - issue1.update(due_date: Date.today.beginning_of_week + 2.days) - issue2.update(due_date: Date.today.end_of_week) - issue3.update(due_date: Date.today - 8.days) + issue1.update!(due_date: Date.today.beginning_of_week + 2.days) + issue2.update!(due_date: Date.today.end_of_week) + issue3.update!(due_date: Date.today - 8.days) visit project_issues_path(project, due_date: Issue::DueThisWeek.name) @@ -161,9 +161,9 @@ RSpec.describe "User sorts issues" do end it 'filters by due this month' do - issue1.update(due_date: Date.today.beginning_of_month + 2.days) - issue2.update(due_date: Date.today.end_of_month) - issue3.update(due_date: Date.today - 50.days) + issue1.update!(due_date: Date.today.beginning_of_month + 2.days) + issue2.update!(due_date: Date.today.end_of_month) + issue3.update!(due_date: Date.today - 50.days) visit project_issues_path(project, due_date: Issue::DueThisMonth.name) @@ -175,9 +175,9 @@ RSpec.describe "User sorts issues" do end it 'filters by overdue' do - issue1.update(due_date: Date.today + 2.days) - issue2.update(due_date: Date.today + 20.days) - issue3.update(due_date: Date.yesterday) + issue1.update!(due_date: Date.today + 2.days) + issue2.update!(due_date: Date.today + 20.days) + issue3.update!(due_date: Date.yesterday) visit project_issues_path(project, due_date: Issue::Overdue.name) @@ -189,9 +189,9 @@ RSpec.describe "User sorts issues" do end it 'filters by due next month and previous two weeks' do - issue1.update(due_date: Date.today - 4.weeks) - issue2.update(due_date: (Date.today + 2.months).beginning_of_month) - issue3.update(due_date: Date.yesterday) + issue1.update!(due_date: Date.today - 4.weeks) + issue2.update!(due_date: (Date.today + 2.months).beginning_of_month) + issue3.update!(due_date: Date.yesterday) visit project_issues_path(project, due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name) @@ -206,9 +206,9 @@ RSpec.describe "User sorts issues" do describe 'sorting by milestone', :js do before do issue1.milestone = newer_due_milestone - issue1.save + issue1.save! issue2.milestone = later_due_milestone - issue2.save + issue2.save! end it 'sorts by milestone' do @@ -224,9 +224,9 @@ RSpec.describe "User sorts issues" do before do issue1.assignees << user2 - issue1.save + issue1.save! issue2.assignees << user2 - issue2.save + issue2.save! end it 'sorts with a filter applied' do diff --git a/spec/frontend/__helpers__/vue_test_utils_helper.js b/spec/frontend/__helpers__/vue_test_utils_helper.js index d6132ef84ac..a94cee84f74 100644 --- a/spec/frontend/__helpers__/vue_test_utils_helper.js +++ b/spec/frontend/__helpers__/vue_test_utils_helper.js @@ -1,4 +1,6 @@ -import { isArray } from 'lodash'; +import * as testingLibrary from '@testing-library/dom'; +import { createWrapper, WrapperArray, mount, shallowMount } from '@vue/test-utils'; +import { isArray, upperFirst } from 'lodash'; const vNodeContainsText = (vnode, text) => (vnode.text && vnode.text.includes(text)) || @@ -37,6 +39,17 @@ export const waitForMutation = (store, expectedMutationType) => }); export const extendedWrapper = (wrapper) => { + // https://testing-library.com/docs/queries/about + const AVAILABLE_QUERIES = [ + 'byRole', + 'byLabelText', + 'byPlaceholderText', + 'byText', + 'byDisplayValue', + 'byAltText', + 'byTitle', + ]; + if (isArray(wrapper) || !wrapper?.find) { // eslint-disable-next-line no-console console.warn( @@ -56,5 +69,63 @@ export const extendedWrapper = (wrapper) => { return this.findAll(`[data-testid="${id}"]`); }, }, + // `findBy` + ...AVAILABLE_QUERIES.reduce((accumulator, query) => { + return { + ...accumulator, + [`find${upperFirst(query)}`]: { + value(text, options = {}) { + const elements = testingLibrary[`queryAll${upperFirst(query)}`]( + wrapper.element, + text, + options, + ); + + // Return VTU `ErrorWrapper` if element is not found + // https://github.com/vuejs/vue-test-utils/blob/dev/packages/test-utils/src/error-wrapper.js + // VTU does not expose `ErrorWrapper` so, as of now, this is the best way to + // create an `ErrorWrapper` + if (!elements.length) { + const emptyElement = document.createElement('div'); + + return createWrapper(emptyElement).find('testing-library-element-not-found'); + } + + return createWrapper(elements[0], this.options || {}); + }, + }, + }; + }, {}), + // `findAllBy` + ...AVAILABLE_QUERIES.reduce((accumulator, query) => { + return { + ...accumulator, + [`findAll${upperFirst(query)}`]: { + value(text, options = {}) { + const elements = testingLibrary[`queryAll${upperFirst(query)}`]( + wrapper.element, + text, + options, + ); + + const wrappers = elements.map((element) => { + const elementWrapper = createWrapper(element, this.options || {}); + elementWrapper.selector = text; + + return elementWrapper; + }); + + const wrapperArray = new WrapperArray(wrappers); + wrapperArray.selector = text; + + return wrapperArray; + }, + }, + }; + }, {}), }); }; + +export const shallowMountExtended = (...args) => extendedWrapper(shallowMount(...args)); + +export const mountExtended = (...args) => extendedWrapper(mount(...args)); diff --git a/spec/frontend/__helpers__/vue_test_utils_helper_spec.js b/spec/frontend/__helpers__/vue_test_utils_helper_spec.js index d4f8e36c169..dfe5a483223 100644 --- a/spec/frontend/__helpers__/vue_test_utils_helper_spec.js +++ b/spec/frontend/__helpers__/vue_test_utils_helper_spec.js @@ -1,7 +1,27 @@ -import { shallowMount } from '@vue/test-utils'; -import { extendedWrapper, shallowWrapperContainsSlotText } from './vue_test_utils_helper'; +import * as testingLibrary from '@testing-library/dom'; +import * as vtu from '@vue/test-utils'; +import { + shallowMount, + Wrapper as VTUWrapper, + WrapperArray as VTUWrapperArray, +} from '@vue/test-utils'; +import { + extendedWrapper, + shallowMountExtended, + mountExtended, + shallowWrapperContainsSlotText, +} from './vue_test_utils_helper'; + +jest.mock('@testing-library/dom', () => ({ + __esModule: true, + ...jest.requireActual('@testing-library/dom'), +})); describe('Vue test utils helpers', () => { + afterAll(() => { + jest.unmock('@testing-library/dom'); + }); + describe('shallowWrapperContainsSlotText', () => { const mockText = 'text'; const mockSlot = `<div>${mockText}</div>`; @@ -84,7 +104,7 @@ describe('Vue test utils helpers', () => { ); }); - it('should find the component by test id', () => { + it('should find the element by test id', () => { expect(mockComponent.findByTestId(testId).exists()).toBe(true); }); }); @@ -105,5 +125,187 @@ describe('Vue test utils helpers', () => { expect(mockComponent.findAllByTestId(testId)).toHaveLength(2); }); }); + + describe.each` + findMethod | expectedQuery + ${'findByRole'} | ${'queryAllByRole'} + ${'findByLabelText'} | ${'queryAllByLabelText'} + ${'findByPlaceholderText'} | ${'queryAllByPlaceholderText'} + ${'findByText'} | ${'queryAllByText'} + ${'findByDisplayValue'} | ${'queryAllByDisplayValue'} + ${'findByAltText'} | ${'queryAllByAltText'} + `('$findMethod', ({ findMethod, expectedQuery }) => { + const text = 'foo bar'; + const options = { selector: 'div' }; + const mockDiv = document.createElement('div'); + + let wrapper; + beforeEach(() => { + wrapper = extendedWrapper( + shallowMount({ + template: `<div>foo bar</div>`, + }), + ); + }); + + it(`calls Testing Library \`${expectedQuery}\` function with correct parameters`, () => { + jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => [mockDiv]); + + wrapper[findMethod](text, options); + + expect(testingLibrary[expectedQuery]).toHaveBeenLastCalledWith( + wrapper.element, + text, + options, + ); + }); + + describe('when element is found', () => { + beforeEach(() => { + jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => [mockDiv]); + jest.spyOn(vtu, 'createWrapper'); + }); + + it('returns a VTU wrapper', () => { + const result = wrapper[findMethod](text, options); + + expect(vtu.createWrapper).toHaveBeenCalledWith(mockDiv, wrapper.options); + expect(result).toBeInstanceOf(VTUWrapper); + }); + }); + + describe('when multiple elements are found', () => { + beforeEach(() => { + const mockSpan = document.createElement('span'); + jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => [mockDiv, mockSpan]); + jest.spyOn(vtu, 'createWrapper'); + }); + + it('returns the first element as a VTU wrapper', () => { + const result = wrapper[findMethod](text, options); + + expect(vtu.createWrapper).toHaveBeenCalledWith(mockDiv, wrapper.options); + expect(result).toBeInstanceOf(VTUWrapper); + }); + }); + + describe('when element is not found', () => { + beforeEach(() => { + jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => []); + }); + + it('returns a VTU error wrapper', () => { + expect(wrapper[findMethod](text, options).exists()).toBe(false); + }); + }); + }); + + describe.each` + findMethod | expectedQuery + ${'findAllByRole'} | ${'queryAllByRole'} + ${'findAllByLabelText'} | ${'queryAllByLabelText'} + ${'findAllByPlaceholderText'} | ${'queryAllByPlaceholderText'} + ${'findAllByText'} | ${'queryAllByText'} + ${'findAllByDisplayValue'} | ${'queryAllByDisplayValue'} + ${'findAllByAltText'} | ${'queryAllByAltText'} + `('$findMethod', ({ findMethod, expectedQuery }) => { + const text = 'foo bar'; + const options = { selector: 'div' }; + const mockElements = [ + document.createElement('li'), + document.createElement('li'), + document.createElement('li'), + ]; + + let wrapper; + beforeEach(() => { + wrapper = extendedWrapper( + shallowMount({ + template: ` + <ul> + <li>foo</li> + <li>bar</li> + <li>baz</li> + </ul> + `, + }), + ); + }); + + it(`calls Testing Library \`${expectedQuery}\` function with correct parameters`, () => { + jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => mockElements); + + wrapper[findMethod](text, options); + + expect(testingLibrary[expectedQuery]).toHaveBeenLastCalledWith( + wrapper.element, + text, + options, + ); + }); + + describe('when elements are found', () => { + beforeEach(() => { + jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => mockElements); + }); + + it('returns a VTU wrapper array', () => { + const result = wrapper[findMethod](text, options); + + expect(result).toBeInstanceOf(VTUWrapperArray); + expect( + result.wrappers.every( + (resultWrapper) => + resultWrapper instanceof VTUWrapper && resultWrapper.options === wrapper.options, + ), + ).toBe(true); + expect(result.length).toBe(3); + }); + }); + + describe('when elements are not found', () => { + beforeEach(() => { + jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => []); + }); + + it('returns an empty VTU wrapper array', () => { + const result = wrapper[findMethod](text, options); + + expect(result).toBeInstanceOf(VTUWrapperArray); + expect(result.length).toBe(0); + }); + }); + }); + }); + + describe.each` + mountExtendedFunction | expectedMountFunction + ${shallowMountExtended} | ${'shallowMount'} + ${mountExtended} | ${'mount'} + `('$mountExtendedFunction', ({ mountExtendedFunction, expectedMountFunction }) => { + const FakeComponent = jest.fn(); + const options = { + propsData: { + foo: 'bar', + }, + }; + + beforeEach(() => { + const mockWrapper = { find: jest.fn() }; + jest.spyOn(vtu, expectedMountFunction).mockImplementation(() => mockWrapper); + }); + + it(`calls \`${expectedMountFunction}\` with passed arguments`, () => { + mountExtendedFunction(FakeComponent, options); + + expect(vtu[expectedMountFunction]).toHaveBeenCalledWith(FakeComponent, options); + }); + + it('returns extended wrapper', () => { + const result = mountExtendedFunction(FakeComponent, options); + + expect(result).toHaveProperty('find'); + expect(result).toHaveProperty('findByTestId'); + }); }); }); diff --git a/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js index f86237dc160..f1471f625f8 100644 --- a/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js +++ b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js @@ -42,6 +42,7 @@ describe('AccessRequestActionButtons', () => { memberId: member.id, title: 'Deny access', isAccessRequest: true, + isInvite: false, icon: 'close', }); }); diff --git a/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js index 55866f90baa..e7a99a96da6 100644 --- a/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js +++ b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js @@ -43,6 +43,7 @@ describe('InviteActionButtons', () => { message: `Are you sure you want to revoke the invitation for ${member.invite.email} to join "${member.source.fullName}"`, title: 'Revoke invite', isAccessRequest: false, + isInvite: true, icon: 'remove', }); }); diff --git a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js index 0d66f343fda..952ce679a2f 100644 --- a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js +++ b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js @@ -28,6 +28,7 @@ describe('RemoveMemberButton', () => { message: 'Are you sure you want to remove John Smith?', title: 'Remove member', isAccessRequest: true, + isInvite: true, ...propsData, }, directives: { @@ -48,6 +49,7 @@ describe('RemoveMemberButton', () => { 'data-member-type': 'GroupMember', 'data-message': 'Are you sure you want to remove John Smith?', 'data-is-access-request': 'true', + 'data-is-invite': 'true', 'aria-label': 'Remove member', title: 'Remove member', icon: 'remove', diff --git a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js index f43779b8970..2f13c436fd9 100644 --- a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js +++ b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js @@ -43,6 +43,7 @@ describe('UserActionButtons', () => { message: `Are you sure you want to remove ${member.user.name} from "${member.source.fullName}"`, title: 'Remove member', isAccessRequest: false, + isInvite: false, icon: 'remove', }); }); diff --git a/spec/frontend/vue_shared/components/remove_member_modal_spec.js b/spec/frontend/vue_shared/components/remove_member_modal_spec.js index 1ef3cc348bd..7a04de13615 100644 --- a/spec/frontend/vue_shared/components/remove_member_modal_spec.js +++ b/spec/frontend/vue_shared/components/remove_member_modal_spec.js @@ -15,16 +15,18 @@ describe('RemoveMemberModal', () => { }); describe.each` - state | memberType | isAccessRequest | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message - ${'removing a group member'} | ${'GroupMember'} | ${'false'} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} - ${'removing a project member'} | ${'ProjectMember'} | ${'false'} | ${'Remove member'} | ${false} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} - ${'denying an access request'} | ${'ProjectMember'} | ${'true'} | ${'Deny access request'} | ${false} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"} + state | memberType | isAccessRequest | isInvite | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message + ${'removing a group member'} | ${'GroupMember'} | ${'false'} | ${'false'} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} + ${'removing a project member'} | ${'ProjectMember'} | ${'false'} | ${'false'} | ${'Remove member'} | ${false} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} + ${'denying an access request'} | ${'ProjectMember'} | ${'true'} | ${'false'} | ${'Deny access request'} | ${false} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"} + ${'revoking invite'} | ${'ProjectMember'} | ${'false'} | ${'true'} | ${'Revoke invite'} | ${false} | ${false} | ${'Are you sure you want to revoke the invitation for foo@bar.com to join the Gitlab Org / Gitlab Test project?'} `( 'when $state', ({ actionText, memberType, isAccessRequest, + isInvite, message, removeSubMembershipsCheckboxExpected, unassignIssuablesCheckboxExpected, @@ -35,6 +37,7 @@ describe('RemoveMemberModal', () => { return { modalData: { isAccessRequest, + isInvite, message, memberPath, memberType, diff --git a/spec/graphql/resolvers/ci/jobs_resolver_spec.rb b/spec/graphql/resolvers/ci/jobs_resolver_spec.rb index c44f6b623d7..1b69bf7f63a 100644 --- a/spec/graphql/resolvers/ci/jobs_resolver_spec.rb +++ b/spec/graphql/resolvers/ci/jobs_resolver_spec.rb @@ -13,6 +13,7 @@ RSpec.describe Resolvers::Ci::JobsResolver do create(:ci_build, :sast, name: 'DAST job', pipeline: pipeline) create(:ci_build, :dast, name: 'SAST job', pipeline: pipeline) create(:ci_build, :container_scanning, name: 'Container scanning job', pipeline: pipeline) + create(:ci_build, name: 'Job with tags', pipeline: pipeline, tag_list: ['review']) end describe '#resolve' do @@ -24,7 +25,8 @@ RSpec.describe Resolvers::Ci::JobsResolver do have_attributes(name: 'Normal job'), have_attributes(name: 'DAST job'), have_attributes(name: 'SAST job'), - have_attributes(name: 'Container scanning job') + have_attributes(name: 'Container scanning job'), + have_attributes(name: 'Job with tags') ) end end @@ -43,5 +45,18 @@ RSpec.describe Resolvers::Ci::JobsResolver do ) end end + + context 'when a job has tags' do + it "returns jobs with tags when applicable" do + jobs = resolve(described_class, obj: pipeline) + expect(jobs).to contain_exactly( + have_attributes(tag_list: []), + have_attributes(tag_list: []), + have_attributes(tag_list: []), + have_attributes(tag_list: []), + have_attributes(tag_list: ['review']) + ) + end + end end end diff --git a/spec/graphql/types/ci/job_type_spec.rb b/spec/graphql/types/ci/job_type_spec.rb index 75a178bc8f6..787e2174070 100644 --- a/spec/graphql/types/ci/job_type_spec.rb +++ b/spec/graphql/types/ci/job_type_spec.rb @@ -33,6 +33,7 @@ RSpec.describe Types::Ci::JobType do stage started_at status + tags ] expect(described_class).to have_graphql_fields(*expected_fields) diff --git a/spec/models/concerns/batch_destroy_dependent_associations_spec.rb b/spec/models/concerns/batch_destroy_dependent_associations_spec.rb index a8fcb714c64..993afd47a57 100644 --- a/spec/models/concerns/batch_destroy_dependent_associations_spec.rb +++ b/spec/models/concerns/batch_destroy_dependent_associations_spec.rb @@ -26,6 +26,7 @@ RSpec.describe BatchDestroyDependentAssociations do let_it_be(:project) { create(:project) } let_it_be(:build) { create(:ci_build, project: project) } let_it_be(:notification_setting) { create(:notification_setting, project: project) } + let!(:todos) { create(:todo, project: project) } it 'destroys multiple builds' do diff --git a/spec/models/concerns/featurable_spec.rb b/spec/models/concerns/featurable_spec.rb index b550d22f686..bcff5ce383e 100644 --- a/spec/models/concerns/featurable_spec.rb +++ b/spec/models/concerns/featurable_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Featurable do let_it_be(:user) { create(:user) } + let(:project) { create(:project) } let(:feature_class) { subject.class } let(:features) { feature_class::FEATURES } diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index 10fa15d468f..ac125e81acd 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -14,8 +14,8 @@ RSpec.describe API::CommitStatuses do let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" } context 'ci commit exists' do - let!(:master) { project.ci_pipelines.create(source: :push, sha: commit.id, ref: 'master', protected: false) } - let!(:develop) { project.ci_pipelines.create(source: :push, sha: commit.id, ref: 'develop', protected: false) } + let!(:master) { project.ci_pipelines.create!(source: :push, sha: commit.id, ref: 'master', protected: false) } + let!(:develop) { project.ci_pipelines.create!(source: :push, sha: commit.id, ref: 'develop', protected: false) } context "reporter user" do let(:statuses_id) { json_response.map { |status| status['id'] } } @@ -270,8 +270,8 @@ RSpec.describe API::CommitStatuses do end context 'when a pipeline id is specified' do - let!(:first_pipeline) { project.ci_pipelines.create(source: :push, sha: commit.id, ref: 'master', status: 'created') } - let!(:other_pipeline) { project.ci_pipelines.create(source: :push, sha: commit.id, ref: 'master', status: 'created') } + let!(:first_pipeline) { project.ci_pipelines.create!(source: :push, sha: commit.id, ref: 'master', status: 'created') } + let!(:other_pipeline) { project.ci_pipelines.create!(source: :push, sha: commit.id, ref: 'master', status: 'created') } subject do post api(post_url, developer), params: { diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb index d6a0423e83c..c89c59a2151 100644 --- a/spec/requests/api/deployments_spec.rb +++ b/spec/requests/api/deployments_spec.rb @@ -345,7 +345,7 @@ RSpec.describe API::Deployments do context 'as a maintainer' do it 'returns a 403 when updating a deployment with a build' do - deploy.update(deployable: build) + deploy.update!(deployable: build) put( api("/projects/#{project.id}/deployments/#{deploy.id}", user), @@ -394,7 +394,7 @@ RSpec.describe API::Deployments do end it 'returns a 403 when updating a deployment with a build' do - deploy.update(deployable: build) + deploy.update!(deployable: build) put( api("/projects/#{project.id}/deployments/#{deploy.id}", developer), diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 303e510883d..aa1a4643593 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -214,7 +214,7 @@ RSpec.describe API::Environments do context 'as a maintainer' do context 'with a stoppable environment' do before do - environment.update(state: :available) + environment.update!(state: :available) post api("/projects/#{project.id}/environments/#{environment.id}/stop", user) end diff --git a/spec/requests/api/go_proxy_spec.rb b/spec/requests/api/go_proxy_spec.rb index d45e24241b2..e678b6cf1c8 100644 --- a/spec/requests/api/go_proxy_spec.rb +++ b/spec/requests/api/go_proxy_spec.rb @@ -363,7 +363,7 @@ RSpec.describe API::GoProxy do let(:module_name) { base } before do - project.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) end describe 'GET /projects/:id/packages/go/*module_name/@v/list' do @@ -412,7 +412,7 @@ RSpec.describe API::GoProxy do let(:module_name) { base } before do - project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) end describe 'GET /projects/:id/packages/go/*module_name/@v/list' do diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb index 34d347c76fd..0d0cc66c52a 100644 --- a/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb +++ b/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb @@ -52,7 +52,7 @@ RSpec.describe 'Setting labels of a merge request' do end it 'sets the merge request labels, removing existing ones' do - merge_request.update(labels: [label2]) + merge_request.update!(labels: [label2]) post_graphql_mutation(mutation, current_user: current_user) diff --git a/spec/requests/api/group_import_spec.rb b/spec/requests/api/group_import_spec.rb index bb7436502ed..f632e49bf3a 100644 --- a/spec/requests/api/group_import_spec.rb +++ b/spec/requests/api/group_import_spec.rb @@ -218,12 +218,14 @@ RSpec.describe API::GroupImport do stub_uploads_object_storage(ImportExportUploader, direct_upload: true) end + # rubocop:disable Rails/SaveBang let(:tmp_object) do fog_connection.directories.new(key: 'uploads').files.create( key: "tmp/uploads/#{file_name}", body: file_upload ) end + # rubocop:enable Rails/SaveBang let(:fog_file) { fog_to_uploaded_file(tmp_object) } let(:params) do diff --git a/spec/requests/api/group_milestones_spec.rb b/spec/requests/api/group_milestones_spec.rb index 7d387079b3a..e3e0164e5a7 100644 --- a/spec/requests/api/group_milestones_spec.rb +++ b/spec/requests/api/group_milestones_spec.rb @@ -20,7 +20,7 @@ RSpec.describe API::GroupMilestones do let_it_be(:params) { { include_parent_milestones: true } } before_all do - group.update(parent: ancestor_group) + group.update!(parent: ancestor_group) end shared_examples 'listing all milestones' do @@ -83,9 +83,9 @@ RSpec.describe API::GroupMilestones do end def setup_for_group - context_group.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + context_group.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) context_group.add_developer(user) - public_project.update(namespace: context_group) + public_project.update!(namespace: context_group) context_group.reload end end diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index 8b060fa2217..6bedd43e5c4 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -887,7 +887,7 @@ RSpec.describe API::Internal::Base do context 'project does not exist' do context 'git pull' do it 'returns a 200 response with status: false' do - project.destroy + project.destroy! pull(key, project) diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb index 3870c78deee..cebde747210 100644 --- a/spec/requests/api/issues/get_group_issues_spec.rb +++ b/spec/requests/api/issues/get_group_issues_spec.rb @@ -754,7 +754,7 @@ RSpec.describe API::Issues do let(:parent_group) { create(:group) } before do - group.update(parent_id: parent_group.id) + group.update!(parent_id: parent_group.id) group_closed_issue.reload end diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb index 5b3e2363669..7f1db620d4f 100644 --- a/spec/requests/api/issues/post_projects_issues_spec.rb +++ b/spec/requests/api/issues/post_projects_issues_spec.rb @@ -111,7 +111,7 @@ RSpec.describe API::Issues do let(:not_member) { create(:user) } before do - project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE) + project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE) end it 'renders 403' do diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb index fe00b654d3b..4f73917763f 100644 --- a/spec/requests/api/jobs_spec.rb +++ b/spec/requests/api/jobs_spec.rb @@ -215,7 +215,7 @@ RSpec.describe API::Jobs do first_build = create(:ci_build, :trace_artifact, :artifacts, :test_reports, pipeline: pipeline) first_build.runner = create(:ci_runner) first_build.user = create(:user) - first_build.save + first_build.save! control_count = ActiveRecord::QueryRecorder.new { go }.count @@ -223,7 +223,7 @@ RSpec.describe API::Jobs do second_build = create(:ci_build, :trace_artifact, :artifacts, :test_reports, pipeline: second_pipeline) second_build.runner = create(:ci_runner) second_build.user = create(:user) - second_build.save + second_build.save! expect { go }.not_to exceed_query_limit(control_count) end @@ -684,7 +684,7 @@ RSpec.describe API::Jobs do context 'with regular branch' do before do pipeline.reload - pipeline.update(ref: 'master', + pipeline.update!(ref: 'master', sha: project.commit('master').sha) get_for_ref('master') @@ -696,7 +696,7 @@ RSpec.describe API::Jobs do context 'with branch name containing slash' do before do pipeline.reload - pipeline.update(ref: 'improve/awesome', + pipeline.update!(ref: 'improve/awesome', sha: project.commit('improve/awesome').sha) end @@ -732,7 +732,7 @@ RSpec.describe API::Jobs do stub_artifacts_object_storage job.success - project.update(visibility_level: visibility_level, + project.update!(visibility_level: visibility_level, public_builds: public_builds) get_artifact_file(artifact) @@ -826,7 +826,7 @@ RSpec.describe API::Jobs do context 'with branch name containing slash' do before do pipeline.reload - pipeline.update(ref: 'improve/awesome', + pipeline.update!(ref: 'improve/awesome', sha: project.commit('improve/awesome').sha) end diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index e3fffd3e3fd..26377c40b73 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -119,7 +119,7 @@ RSpec.describe API::Labels do expect(label).not_to be_nil - label.priorities.create(project: label.project, priority: 1) + label.priorities.create!(project: label.project, priority: 1) label.save! request_params = { @@ -139,7 +139,7 @@ RSpec.describe API::Labels do expect(label).not_to be_nil label_id = spec_params[:name] || spec_params[:label_id] - label.priorities.create(project: label.project, priority: 1) + label.priorities.create!(project: label.project, priority: 1) label.save! request_params = { @@ -383,7 +383,7 @@ RSpec.describe API::Labels do it 'returns 409 if label already exists in group' do group = create(:group) group_label = create(:group_label, group: group) - project.update(group: group) + project.update!(group: group) post api("/projects/#{project.id}/labels", user), params: { diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index a049d7d7515..f6cdf370e5c 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -235,12 +235,14 @@ RSpec.describe API::ProjectImport do stub_uploads_object_storage(ImportExportUploader, direct_upload: true) end + # rubocop:disable Rails/SaveBang let(:tmp_object) do fog_connection.directories.new(key: 'uploads').files.create( key: "tmp/uploads/#{file_name}", body: fixture_file_upload(file) ) end + # rubocop:enable Rails/SaveBang let(:file_upload) { fog_to_uploaded_file(tmp_object) } @@ -285,7 +287,7 @@ RSpec.describe API::ProjectImport do it 'returns the import status and the error if failed' do project = create(:project, :import_failed) project.add_maintainer(user) - project.import_state.update(last_error: 'error') + project.import_state.update!(last_error: 'error') get api("/projects/#{project.id}/import", user) |