From 777f6da99ae8dd4111bb880893cee9c8cfefa132 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 29 Jun 2020 21:09:07 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitlab/ci/rules.gitlab-ci.yml | 1 + .rubocop.yml | 12 +++ Gemfile | 4 +- Gemfile.lock | 55 ++++++---- app/assets/stylesheets/framework/common.scss | 1 - app/controllers/projects/refs_controller.rb | 11 -- app/graphql/mutations/jira_import/start.rb | 9 +- .../types/jira_users_mapping_input_type.rb | 18 ++++ .../jira_import/start_import_service.rb | 20 +++- app/views/projects/refs/logs_tree.js.haml | 23 ---- .../tree/_tree_commit_column.html.haml | 3 - .../unreleased/216145-graphql-import.yml | 5 + changelogs/unreleased/sh-update-grape-gem.yml | 5 + doc/administration/logs.md | 95 +++++++++++----- .../graphql/reference/gitlab_schema.graphql | 17 +++ doc/api/graphql/reference/gitlab_schema.json | 53 +++++++++ doc/development/api_styleguide.md | 40 +++++++ doc/development/ee_features.md | 12 +-- doc/development/permissions.md | 11 +- .../img/pipeline_security_dashboard_v12_6.png | Bin 59799 -> 0 bytes .../img/pipeline_security_dashboard_v13_2.png | Bin 0 -> 79103 bytes .../security_dashboard/index.md | 2 +- doc/user/group/subgroups/index.md | 3 + doc/user/project/import/gemnasium.md | 2 +- lib/api/access_requests.rb | 2 +- lib/api/admin/ci/variables.rb | 2 +- lib/api/admin/sidekiq.rb | 2 +- lib/api/api.rb | 4 +- lib/api/api_guard.rb | 11 +- lib/api/appearance.rb | 2 +- lib/api/applications.rb | 2 +- lib/api/avatar.rb | 2 +- lib/api/award_emoji.rb | 2 +- lib/api/badges.rb | 2 +- lib/api/boards.rb | 2 +- lib/api/branches.rb | 2 +- lib/api/broadcast_messages.rb | 2 +- lib/api/ci/runner.rb | 4 +- lib/api/ci/runners.rb | 12 +-- lib/api/commit_statuses.rb | 2 +- lib/api/commits.rb | 2 +- lib/api/container_registry_event.rb | 2 +- lib/api/deploy_keys.rb | 2 +- lib/api/deploy_tokens.rb | 6 +- lib/api/deployments.rb | 2 +- lib/api/discussions.rb | 2 +- lib/api/environments.rb | 2 +- lib/api/error_tracking.rb | 2 +- lib/api/events.rb | 2 +- lib/api/features.rb | 2 +- lib/api/files.rb | 2 +- lib/api/freeze_periods.rb | 2 +- lib/api/group_boards.rb | 2 +- lib/api/group_clusters.rb | 2 +- lib/api/group_container_repositories.rb | 2 +- lib/api/group_export.rb | 2 +- lib/api/group_import.rb | 2 +- lib/api/group_labels.rb | 2 +- lib/api/group_milestones.rb | 6 +- lib/api/group_variables.rb | 2 +- lib/api/groups.rb | 4 +- lib/api/helpers/common_helpers.rb | 20 ++++ lib/api/helpers/merge_requests_helpers.rb | 2 +- lib/api/helpers/projects_helpers.rb | 2 +- lib/api/import_github.rb | 2 +- lib/api/internal/base.rb | 2 +- lib/api/internal/pages.rb | 2 +- lib/api/issues.rb | 14 +-- lib/api/job_artifacts.rb | 2 +- lib/api/jobs.rb | 2 +- lib/api/keys.rb | 2 +- lib/api/labels.rb | 2 +- lib/api/lint.rb | 2 +- lib/api/markdown.rb | 2 +- lib/api/members.rb | 6 +- lib/api/merge_request_diffs.rb | 2 +- lib/api/merge_requests.rb | 16 +-- lib/api/metrics/dashboard/annotations.rb | 2 +- lib/api/metrics/user_starred_dashboards.rb | 2 +- lib/api/milestone_responses.rb | 2 +- lib/api/namespaces.rb | 2 +- lib/api/notes.rb | 2 +- lib/api/notification_settings.rb | 2 +- lib/api/pages.rb | 2 +- lib/api/pages_domains.rb | 2 +- lib/api/pagination_params.rb | 2 +- lib/api/pipeline_schedules.rb | 2 +- lib/api/pipelines.rb | 2 +- lib/api/project_clusters.rb | 2 +- lib/api/project_container_repositories.rb | 2 +- lib/api/project_events.rb | 2 +- lib/api/project_export.rb | 2 +- lib/api/project_hooks.rb | 2 +- lib/api/project_import.rb | 2 +- lib/api/project_milestones.rb | 6 +- lib/api/project_repository_storage_moves.rb | 2 +- lib/api/project_snapshots.rb | 2 +- lib/api/project_snippets.rb | 2 +- lib/api/project_statistics.rb | 2 +- lib/api/project_templates.rb | 2 +- lib/api/projects.rb | 4 +- lib/api/protected_branches.rb | 2 +- lib/api/protected_tags.rb | 2 +- lib/api/release/links.rb | 2 +- lib/api/releases.rb | 4 +- lib/api/remote_mirrors.rb | 2 +- lib/api/repositories.rb | 4 +- lib/api/resource_label_events.rb | 2 +- lib/api/resource_milestone_events.rb | 2 +- lib/api/search.rb | 2 +- lib/api/services.rb | 2 +- lib/api/settings.rb | 11 +- lib/api/sidekiq_metrics.rb | 2 +- lib/api/snippets.rb | 2 +- lib/api/statistics.rb | 2 +- lib/api/submodules.rb | 2 +- lib/api/subscriptions.rb | 2 +- lib/api/suggestions.rb | 4 +- lib/api/system_hooks.rb | 2 +- lib/api/tags.rb | 2 +- lib/api/templates.rb | 2 +- lib/api/terraform/state.rb | 2 +- lib/api/todos.rb | 2 +- lib/api/triggers.rb | 2 +- lib/api/user_counts.rb | 2 +- lib/api/users.rb | 2 +- .../types/comma_separated_to_array.rb | 2 +- .../types/comma_separated_to_integer_array.rb | 15 +++ lib/api/validations/types/labels_list.rb | 24 ----- lib/api/validations/types/safe_file.rb | 15 --- lib/api/validations/types/workhorse_file.rb | 13 ++- lib/api/variables.rb | 2 +- lib/api/version.rb | 2 +- lib/api/wikis.rb | 4 +- .../table_management_helpers.rb | 76 ++++++------- locale/gitlab.pot | 3 + rubocop/cop/api/grape_api_instance.rb | 42 ++++++++ rubocop/cop/api/grape_array_missing_coerce.rb | 83 ++++++++++++++ .../projects/refs_controller_spec.rb | 9 +- spec/lib/api/helpers/common_helpers_spec.rb | 51 +++++++++ .../table_management_helpers_spec.rb | 102 +++++++++--------- spec/requests/api/applications_spec.rb | 9 +- .../mutations/jira_import/start_spec.rb | 3 +- spec/requests/api/settings_spec.rb | 10 +- .../cop/api/grape_api_instance_spec.rb | 29 +++++ .../api/grape_array_missing_coerce_spec.rb | 62 +++++++++++ spec/rubocop/cop/code_reuse/worker_spec.rb | 2 +- .../jira_import/start_import_service_spec.rb | 88 ++++++++++----- 148 files changed, 929 insertions(+), 425 deletions(-) create mode 100644 app/graphql/types/jira_users_mapping_input_type.rb delete mode 100644 app/views/projects/refs/logs_tree.js.haml delete mode 100644 app/views/projects/tree/_tree_commit_column.html.haml create mode 100644 changelogs/unreleased/216145-graphql-import.yml create mode 100644 changelogs/unreleased/sh-update-grape-gem.yml delete mode 100644 doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v12_6.png create mode 100644 doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v13_2.png create mode 100644 lib/api/validations/types/comma_separated_to_integer_array.rb delete mode 100644 lib/api/validations/types/labels_list.rb delete mode 100644 lib/api/validations/types/safe_file.rb create mode 100644 rubocop/cop/api/grape_api_instance.rb create mode 100644 rubocop/cop/api/grape_array_missing_coerce.rb create mode 100644 spec/lib/api/helpers/common_helpers_spec.rb create mode 100644 spec/rubocop/cop/api/grape_api_instance_spec.rb create mode 100644 spec/rubocop/cop/api/grape_array_missing_coerce_spec.rb diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 43b55245e51..86e3011e8f4 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -119,6 +119,7 @@ .db-patterns: &db-patterns - "{,ee/}{db}/**/*" + - "{,ee}/spec/{db,migrations}/**/*" .backstage-patterns: &backstage-patterns - "Dangerfile" diff --git a/.rubocop.yml b/.rubocop.yml index 1967cbfb982..89e6214fe47 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -308,6 +308,18 @@ Gitlab/Union: - 'spec/**/*' - 'ee/spec/**/*' +API/GrapeAPIInstance: + Enabled: true + Include: + - 'lib/**/api/**/*.rb' + - 'ee/**/api/**/*.rb' + +API/GrapeArrayMissingCoerce: + Enabled: true + Include: + - 'lib/**/api/**/*.rb' + - 'ee/**/api/**/*.rb' + Cop/SidekiqOptionsQueue: Enabled: true Exclude: diff --git a/Gemfile b/Gemfile index c33975966cb..099cf5477dc 100644 --- a/Gemfile +++ b/Gemfile @@ -19,7 +19,7 @@ gem 'default_value_for', '~> 3.3.0' gem 'pg', '~> 1.1' gem 'rugged', '~> 0.28' -gem 'grape-path-helpers', '~> 1.2' +gem 'grape-path-helpers', '~> 1.3' gem 'faraday', '~> 0.12' gem 'marginalia', '~> 1.8.0' @@ -81,7 +81,7 @@ gem 'gitlab_omniauth-ldap', '~> 2.1.1', require: 'omniauth-ldap' gem 'net-ldap' # API -gem 'grape', '~> 1.1.0' +gem 'grape', '~> 1.3.3' gem 'grape-entity', '~> 0.7.1' gem 'rack-cors', '~> 1.0.6', require: 'rack/cors' diff --git a/Gemfile.lock b/Gemfile.lock index be2d98cb063..ae35fa30e6e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -103,10 +103,6 @@ GEM aws-sdk-core (= 2.11.374) aws-sigv4 (1.1.0) aws-eventstream (~> 1.0, >= 1.0.2) - axiom-types (0.1.1) - descendants_tracker (~> 0.0.4) - ice_nine (~> 0.11.0) - thread_safe (~> 0.3, >= 0.3.1) babosa (1.0.2) base32 (0.3.2) batch-loader (1.4.0) @@ -164,8 +160,6 @@ GEM nap open4 (~> 1.3) coderay (1.1.2) - coercible (1.0.0) - descendants_tracker (~> 0.0.1) colored2 (3.1.2) commonmarker (0.20.1) ruby-enum (~> 0.5) @@ -221,8 +215,6 @@ GEM ruby-statistics (>= 2.1) thor (>= 0.19, < 2) unicode_plot (>= 0.0.4, < 1.0.0) - descendants_tracker (0.0.4) - thread_safe (~> 0.3, >= 0.3.1) device_detector (1.0.0) devise (4.7.1) bcrypt (~> 3.0) @@ -249,6 +241,28 @@ GEM doorkeeper-openid_connect (1.6.3) doorkeeper (>= 5.0, < 5.2) json-jwt (~> 1.6) + dry-configurable (0.11.5) + concurrent-ruby (~> 1.0) + dry-core (~> 0.4, >= 0.4.7) + dry-equalizer (~> 0.2) + dry-container (0.7.2) + concurrent-ruby (~> 1.0) + dry-configurable (~> 0.1, >= 0.1.3) + dry-core (0.4.9) + concurrent-ruby (~> 1.0) + dry-equalizer (0.3.0) + dry-inflector (0.2.0) + dry-logic (1.0.6) + concurrent-ruby (~> 1.0) + dry-core (~> 0.2) + dry-equalizer (~> 0.2) + dry-types (1.4.0) + concurrent-ruby (~> 1.0) + dry-container (~> 0.3) + dry-core (~> 0.4, >= 0.4.4) + dry-equalizer (~> 0.3) + dry-inflector (~> 0.1, >= 0.1.2) + dry-logic (~> 1.0, >= 1.0.2) ed25519 (1.2.4) elasticsearch (6.8.0) elasticsearch-api (= 6.8.0) @@ -439,19 +453,19 @@ GEM signet (~> 0.14) gpgme (2.0.20) mini_portile2 (~> 2.3) - grape (1.1.0) + grape (1.3.3) activesupport builder + dry-types (>= 1.1) mustermann-grape (~> 1.0.0) rack (>= 1.3.0) rack-accept - virtus (>= 1.0.0) grape-entity (0.7.1) activesupport (>= 4.0) multi_json (>= 1.3.2) - grape-path-helpers (1.2.0) + grape-path-helpers (1.3.0) activesupport - grape (~> 1.0) + grape (~> 1.3) rake (~> 12) grape_logging (1.8.3) grape @@ -642,9 +656,10 @@ GEM multi_xml (0.6.0) multipart-post (2.1.1) murmurhash3 (0.1.6) - mustermann (1.0.3) - mustermann-grape (1.0.0) - mustermann (~> 1.0.0) + mustermann (1.1.1) + ruby2_keywords (~> 0.0.1) + mustermann-grape (1.0.1) + mustermann (>= 1.0.0) nakayoshi_fork (0.0.4) nap (1.1.0) nenv (0.3.0) @@ -959,6 +974,7 @@ GEM ruby-saml (1.7.2) nokogiri (>= 1.5.10) ruby-statistics (2.1.2) + ruby2_keywords (0.0.2) ruby_dep (1.5.0) ruby_parser (3.13.1) sexp_processor (~> 4.9) @@ -1122,11 +1138,6 @@ GEM activerecord (>= 3.0) activesupport (>= 3.0) version_sorter (2.2.4) - virtus (1.0.5) - axiom-types (~> 0.1) - coercible (~> 1.0) - descendants_tracker (~> 0.0, >= 0.0.3) - equalizer (~> 0.0, >= 0.0.9) vmstat (2.3.0) warden (1.2.8) rack (>= 2.0.6) @@ -1257,9 +1268,9 @@ DEPENDENCIES google-api-client (~> 0.33) google-protobuf (~> 3.8.0) gpgme (~> 2.0.19) - grape (~> 1.1.0) + grape (~> 1.3.3) grape-entity (~> 0.7.1) - grape-path-helpers (~> 1.2) + grape-path-helpers (~> 1.3) grape_logging (~> 1.7) graphiql-rails (~> 1.4.10) graphql (~> 1.10.5) diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index cfeb4e71c56..068612c4c92 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -411,7 +411,6 @@ img.emoji { .append-right-15 { margin-right: 15px; } .append-right-default { margin-right: $gl-padding; } .append-right-20 { margin-right: 20px; } -.append-right-48 { margin-right: 48px; } .append-bottom-5 { margin-bottom: 5px; } .append-bottom-10 { margin-bottom: 10px; } .append-bottom-15 { margin-bottom: 15px; } diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index a2581e72257..0d1583e05e5 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -57,22 +57,11 @@ class Projects::RefsController < Projects::ApplicationController render json: logs end - - # Deprecated due to https://gitlab.com/gitlab-org/gitlab/-/issues/36863 - # Will be removed soon https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29895 - format.js do - @logs, _ = tree_summary.summarize - @more_log_url = more_url(tree_summary.next_offset) if tree_summary.more? - end end end private - def more_url(offset) - logs_file_project_ref_path(@project, @ref, @path, offset: offset) - end - def validate_ref_id return not_found! if params[:id].present? && params[:id] !~ Gitlab::PathRegex.git_reference_regex end diff --git a/app/graphql/mutations/jira_import/start.rb b/app/graphql/mutations/jira_import/start.rb index 3df26d33711..eda28059272 100644 --- a/app/graphql/mutations/jira_import/start.rb +++ b/app/graphql/mutations/jira_import/start.rb @@ -21,12 +21,17 @@ module Mutations argument :jira_project_name, GraphQL::STRING_TYPE, required: false, description: 'Project name of the importer Jira project' + argument :users_mapping, + [Types::JiraUsersMappingInputType], + required: false, + description: 'The mapping of Jira to GitLab users' - def resolve(project_path:, jira_project_key:) + def resolve(project_path:, jira_project_key:, users_mapping:) project = authorized_find!(full_path: project_path) + mapping = users_mapping.to_ary.map { |map| map.to_hash } service_response = ::JiraImport::StartImportService - .new(context[:current_user], project, jira_project_key) + .new(context[:current_user], project, jira_project_key, mapping) .execute jira_import = service_response.success? ? service_response.payload[:import_data] : nil diff --git a/app/graphql/types/jira_users_mapping_input_type.rb b/app/graphql/types/jira_users_mapping_input_type.rb new file mode 100644 index 00000000000..61cf1474493 --- /dev/null +++ b/app/graphql/types/jira_users_mapping_input_type.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Types + # rubocop: disable Graphql/AuthorizeTypes + class JiraUsersMappingInputType < BaseInputObject + graphql_name 'JiraUsersMappingInputType' + + argument :jira_account_id, + GraphQL::STRING_TYPE, + required: true, + description: 'Jira account id of the user' + argument :gitlab_id, + GraphQL::INT_TYPE, + required: false, + description: 'Id of the GitLab user' + end + # rubocop: enable Graphql/AuthorizeTypes +end diff --git a/app/services/jira_import/start_import_service.rb b/app/services/jira_import/start_import_service.rb index a06cc6df719..f85f686c61a 100644 --- a/app/services/jira_import/start_import_service.rb +++ b/app/services/jira_import/start_import_service.rb @@ -2,23 +2,39 @@ module JiraImport class StartImportService - attr_reader :user, :project, :jira_project_key + attr_reader :user, :project, :jira_project_key, :users_mapping - def initialize(user, project, jira_project_key) + def initialize(user, project, jira_project_key, users_mapping) @user = user @project = project @jira_project_key = jira_project_key + @users_mapping = users_mapping end def execute validation_response = validate return validation_response if validation_response&.error? + store_users_mapping create_and_schedule_import end private + def store_users_mapping + return if users_mapping.blank? + + mapping = users_mapping.map do |map| + next if !map[:jira_account_id] || !map[:gitlab_id] + + [map[:jira_account_id], map[:gitlab_id]] + end.compact.to_h + + return if mapping.blank? + + Gitlab::JiraImport.cache_users_mapping(project.id, mapping) + end + def create_and_schedule_import jira_import = build_jira_import project.import_type = 'jira' diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml deleted file mode 100644 index 506bf54b3f8..00000000000 --- a/app/views/projects/refs/logs_tree.js.haml +++ /dev/null @@ -1,23 +0,0 @@ -- @logs.each do |content_data| - - file_name = content_data[:file_name] - - commit = content_data[:commit] - - next unless commit - - :plain - var row = $("table.table_#{@hex_path} tr.file_#{hexdigest(file_name)}"); - row.find("td.tree-time-ago").html('#{escape_javascript time_ago_with_tooltip(commit.committed_date)}'); - row.find("td.tree-commit").html('#{escape_javascript render("projects/tree/tree_commit_column", commit: commit)}'); - - = render_if_exists 'projects/refs/logs_tree_lock_label', lock_label: content_data[:lock_label] - -- if @more_log_url - :plain - if($('#tree-slider').length) { - // Load more commit logs for each file in tree - // if we still on the same page - var url = "#{escape_javascript(@more_log_url)}"; - gl.utils.ajaxGet(url); - } - -:plain - gl.utils.localTimeAgo($('.js-timeago', 'table.table_#{@hex_path} tbody')); diff --git a/app/views/projects/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml deleted file mode 100644 index 065fef606d5..00000000000 --- a/app/views/projects/tree/_tree_commit_column.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -- full_title = markdown_field(commit, :full_title) -%span.str-truncated - = link_to_html full_title, project_commit_path(@project, commit.id), title: full_title, class: 'tree-commit-link' diff --git a/changelogs/unreleased/216145-graphql-import.yml b/changelogs/unreleased/216145-graphql-import.yml new file mode 100644 index 00000000000..a107b95cd4a --- /dev/null +++ b/changelogs/unreleased/216145-graphql-import.yml @@ -0,0 +1,5 @@ +--- +title: Add Jira users mapping to start Jira import mutation +merge_request: 34609 +author: +type: added diff --git a/changelogs/unreleased/sh-update-grape-gem.yml b/changelogs/unreleased/sh-update-grape-gem.yml new file mode 100644 index 00000000000..fbed6ad665b --- /dev/null +++ b/changelogs/unreleased/sh-update-grape-gem.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade Grape v1.1.0 to v1.3.3 +merge_request: 33450 +author: +type: other diff --git a/doc/administration/logs.md b/doc/administration/logs.md index 0ec6ea7986e..140f6cd6f0a 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -26,7 +26,7 @@ GitLab, thanks to [Lograge](https://github.com/roidrage/lograge/). Note that requests from the API are logged to a separate file in `api_json.log`. Each line contains a JSON line that can be ingested by services like Elasticsearch and Splunk. -Line breaks have been added to this example for legibility: +Line breaks were added to examples for legibility: ```json { @@ -79,7 +79,7 @@ seconds: User clone and fetch activity using HTTP transport appears in this log as `action: git_upload_pack`. In addition, the log contains the originating IP address, -(`remote_ip`),the user's ID (`user_id`), and username (`username`). +(`remote_ip`), the user's ID (`user_id`), and username (`username`). Some endpoints such as `/search` may make requests to Elasticsearch if using [Advanced Global Search](../user/search/advanced_global_search.md). These will @@ -227,7 +227,7 @@ It helps you see requests made directly to the API. For example: } ``` -This entry shows an access to an internal endpoint to check whether an +This entry shows an internal endpoint accessed to check whether an associated SSH key can download the project in question via a `git fetch` or `git clone`. In this example, we see: @@ -320,7 +320,7 @@ packages or in `/home/git/gitlab/log/kubernetes.log` for installations from source. It logs information related to the Kubernetes Integration including errors -during installing cluster applications on your GitLab managed Kubernetes +during installing cluster applications on your managed Kubernetes clusters. Each line contains a JSON line that can be ingested by services like Elasticsearch and Splunk. @@ -362,7 +362,7 @@ After 12.2, this file was renamed from `githost.log` to `git_json.log` and stored in JSON format. GitLab has to interact with Git repositories, but in some rare cases -something can go wrong, and in this case you will know what exactly +something can go wrong, and in this case you may need know what exactly happened. This log file contains all failed requests from GitLab to Git repositories. In the majority of cases this file will be useful for developers only. For example: @@ -473,8 +473,8 @@ This file lives in `/var/log/gitlab/gitlab-rails/sidekiq_client.log` for Omnibus GitLab packages or in `/home/git/gitlab/log/sidekiq_client.log` for installations from source. -This file contains logging information about jobs before they are start -being processed by Sidekiq, for example before being enqueued. +This file contains logging information about jobs before Sidekiq starts +processing them, such as before being enqueued. This log file follows the same structure as [`sidekiq.log`](#sidekiqlog), so it will be structured as JSON if @@ -571,32 +571,45 @@ User clone/fetch activity using SSH transport appears in this log as `executing ## Gitaly Logs -This file lives in `/var/log/gitlab/gitaly/current` and is produced by [runit](http://smarden.org/runit/). `runit` is packaged with Omnibus and a brief explanation of its purpose is available [in the omnibus documentation](https://docs.gitlab.com/omnibus/architecture/#runit). [Log files are rotated](http://smarden.org/runit/svlogd.8.html), renamed in Unix timestamp format and `gzip`-compressed (e.g. `@1584057562.s`). +This file lives in `/var/log/gitlab/gitaly/current` and is produced by [runit](http://smarden.org/runit/). `runit` is packaged with Omnibus GitLab and a brief explanation of its purpose is available [in the Omnibus GitLab documentation](https://docs.gitlab.com/omnibus/architecture/#runit). [Log files are rotated](http://smarden.org/runit/svlogd.8.html), renamed in Unix timestamp format, and `gzip`-compressed (like `@1584057562.s`). ### `grpc.log` This file lives in `/var/log/gitlab/gitlab-rails/grpc.log` for Omnibus GitLab packages. Native [gRPC](https://grpc.io/) logging used by Gitaly. -## `puma_stderr.log` & `puma_stdout.log` +## Puma Logs -This file lives in `/var/log/gitlab/puma/puma_stderr.log` and `/var/log/gitlab/puma/puma_stdout.log` for -Omnibus GitLab packages or in `/home/git/gitlab/log/puma_stderr.log` and `/home/git/gitlab/log/puma_stdout.log` -for installations from source. +### `puma_stdout.log` -## `unicorn_stderr.log` & `unicorn_stdout.log` +This file lives in `/var/log/gitlab/puma/puma_stdout.log` for +Omnibus GitLab packages, and `/home/git/gitlab/log/puma_stdout.log` for +installations from source. + +### `puma_stderr.log` + +This file lives in `/var/log/gitlab/puma/puma_stderr.log` for +Omnibus GitLab packages, or in `/home/git/gitlab/log/puma_stderr.log` for +installations from source. + +## Unicorn Logs NOTE: **Note:** Starting with GitLab 13.0, Puma is the default web server used in GitLab all-in-one package based installations as well as GitLab Helm chart deployments. -This file lives in `/var/log/gitlab/unicorn/unicorn_stderr.log` and `/var/log/gitlab/unicorn/unicorn_stdout.log` for -Omnibus GitLab packages or in `/home/git/gitlab/log/unicorn_stderr.log` and `/home/git/gitlab/log/unicorn_stdout.log` +### `unicorn_stdout.log` + +This file lives in `/var/log/gitlab/unicorn/unicorn_stdout.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/unicorn_stdout.log` for for installations from source. -Unicorn is a high-performance forking Web server which is used for -serving the GitLab application. You can look at this log if, for -example, your application does not respond. This log contains all -information about the state of Unicorn processes at any given time. +### `unicorn_stderr.log` + +This file lives in `/var/log/gitlab/unicorn/unicorn_stderr.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/unicorn_stderr.log` for +for installations from source. + +These logs contain all information about the state of Unicorn processes at any given time. ```plaintext I, [2015-02-13T06:14:46.680381 #9047] INFO -- : Refreshing Gem list @@ -657,7 +670,7 @@ This log records: - [Protected paths](../user/admin_area/settings/protected_paths.md) abusive requests. NOTE: **Note:** -From [%12.3](https://gitlab.com/gitlab-org/gitlab/-/issues/29239), user ID and username are also +In GitLab versions [12.3](https://gitlab.com/gitlab-org/gitlab/-/issues/29239) and greater, user ID and username are also recorded on this log, if available. ## `graphql_json.log` @@ -686,7 +699,7 @@ installations from source. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/19186) in GitLab 12.6. -This file lives in `/var/log/gitlab/mailroom/mail_room_json.log` for +This file lives in `/var/log/gitlab/mailroom/current` for Omnibus GitLab packages or in `/home/git/gitlab/log/mail_room_json.log` for installations from source. @@ -793,8 +806,8 @@ This file lives in `/var/log/gitlab/gitlab-rails/service_measurement.log` for Omnibus GitLab packages or in `/home/git/gitlab/log/service_measurement.log` for installations from source. -It contain only a single structured log with measurements for each service execution. -It will contain measurement such as: number of SQL calls, `execution_time`, `gc_stats`, `memory_usage`, etc... +It contains only a single structured log with measurements for each service execution. +It will contain measurements such as the number of SQL calls, `execution_time`, `gc_stats`, and `memory usage`. For example: @@ -870,22 +883,46 @@ For example: } ``` +## Mattermost Logs + +For Omnibus GitLab installations, Mattermost logs reside in `/var/log/gitlab/mattermost/mattermost.log`. + ## Workhorse Logs -For Omnibus installations, Workhorse logs reside in `/var/log/gitlab/gitlab-workhorse/current`. +For Omnibus GitLab installations, Workhorse logs reside in `/var/log/gitlab/gitlab-workhorse/`. ## PostgreSQL Logs -For Omnibus installations, PostgreSQL logs reside in `/var/log/gitlab/postgresql/current`. +For Omnibus GitLab installations, PostgreSQL logs reside in `/var/log/gitlab/postgresql/`. ## Prometheus Logs -For Omnibus installations, Prometheus logs reside in `/var/log/gitlab/prometheus/current`. +For Omnibus GitLab installations, Prometheus logs reside in `/var/log/gitlab/prometheus/`. ## Redis Logs -For Omnibus installations, Redis logs reside in `/var/log/gitlab/redis/current`. +For Omnibus GitLab installations, Redis logs reside in `/var/log/gitlab/redis/`. -## Mattermost Logs +## Alertmanager Logs -For Omnibus installations, Mattermost logs reside in `/var/log/gitlab/mattermost/mattermost.log`. +For Omnibus GitLab installations, Alertmanager logs reside in `/var/log/gitlab/alertmanager/`. + +## Crond Logs + +For Omnibus GitLab installations, crond logs reside in `/var/log/gitlab/crond/`. + +## Grafana Logs + +For Omnibus GitLab installations, Grafana logs reside in `/var/log/gitlab/grafana/`. + +## LogRotate Logs + +For Omnibus GitLab installations, logrotate logs reside in `/var/log/gitlab/logrotate/`. + +## GitLab Monitor Logs + +For Omnibus GitLab installations, GitLab Monitor logs reside in `/var/log/gitlab/gitlab-monitor/`. + +## GitLab Exporter + +For Omnibus GitLab installations, GitLab Exporter logs reside in `/var/log/gitlab/gitlab-exporter/`. diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index d0a03534c6b..9310b5081a4 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -6368,6 +6368,11 @@ input JiraImportStartInput { The project to import the Jira project into """ projectPath: ID! + + """ + The mapping of Jira to GitLab users + """ + usersMapping: [JiraUsersMappingInputType!] } """ @@ -6546,6 +6551,18 @@ type JiraUser { jiraEmail: String } +input JiraUsersMappingInputType { + """ + Id of the GitLab user + """ + gitlabId: Int + + """ + Jira account id of the user + """ + jiraAccountId: String! +} + type Label { """ Background color of the label diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 7e53d48594e..203cfc1c65c 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -17605,6 +17605,24 @@ }, "defaultValue": null }, + { + "name": "usersMapping", + "description": "The mapping of Jira to GitLab users", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "JiraUsersMappingInputType", + "ofType": null + } + } + }, + "defaultValue": null + }, { "name": "clientMutationId", "description": "A unique identifier for the client performing the mutation.", @@ -18167,6 +18185,41 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "JiraUsersMappingInputType", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "jiraAccountId", + "description": "Jira account id of the user", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "gitlabId", + "description": "Id of the GitLab user", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "Label", diff --git a/doc/development/api_styleguide.md b/doc/development/api_styleguide.md index 6a044004926..327f919d7f4 100644 --- a/doc/development/api_styleguide.md +++ b/doc/development/api_styleguide.md @@ -98,6 +98,46 @@ For instance: Model.create(foo: params[:foo]) ``` +## Array types + +With Grape v1.3+, Array types must be defined with a `coerce_with` +block, or parameters will fail to validate when passed a string from an +API request. See the [Grape upgrading +documentation](https://github.com/ruby-grape/grape/blob/master/UPGRADING.md#ensure-that-array-types-have-explicit-coercions) +for more details. + +### Automatic coercion of nil inputs + +Prior to Grape v1.3.3, Array parameters with `nil` values would +automatically be coerced to an empty Array. However, due to [this pull +request in v1.3.3](https://github.com/ruby-grape/grape/pull/2040), this +is no longer the case. For example, suppose you define a PUT `/test` +request that has an optional parameter: + +```ruby +optional :user_ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The user ids for this rule' +``` + +Normally, a request to PUT `/test?user_ids` would cause Grape to pass +`params` of `{ user_ids: nil }`. + +This may introduce errors with endpoints that expect a blank array and +do not handle `nil` inputs properly. To preserve the previous behavior, +there is a helper method `coerce_nil_params_to_array!` that is used +in the `before` block of all API calls: + +```ruby +before do + coerce_nil_params_to_array! +end +``` + +With this change, a request to PUT `/test?user_ids` will cause Grape to +pass `params` to be `{ user_ids: [] }`. + +There is [an open issue in the Grape tracker](https://github.com/ruby-grape/grape/issues/2068) +to make this easier. + ## Using HTTP status helpers For non-200 HTTP responses, use the provided helpers in `lib/api/helpers.rb` to ensure correct behavior (`not_found!`, `no_content!` etc.). These will `throw` inside Grape and abort the execution of your endpoint. diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index e22e96b6f06..e2cbcd6cc22 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -512,12 +512,12 @@ do that, so we'll follow regular object-oriented practices that we define the interface first here. For example, suppose we have a few more optional parameters for EE. We can move the -parameters out of the `Grape::API` class to a helper module, so we can inject it +parameters out of the `Grape::API::Instance` class to a helper module, so we can inject it before it would be used in the class. ```ruby module API - class Projects < Grape::API + class Projects < Grape::API::Instance helpers Helpers::ProjectsHelpers end end @@ -578,7 +578,7 @@ class definition to make it easy and clear: ```ruby module API - class JobArtifacts < Grape::API + class JobArtifacts < Grape::API::Instance # EE::API::JobArtifacts would override the following helpers helpers do def authorize_download_artifacts! @@ -622,7 +622,7 @@ route. Something like this: ```ruby module API - class MergeRequests < Grape::API + class MergeRequests < Grape::API::Instance helpers do # EE::API::MergeRequests would override the following helpers def update_merge_request_ee(merge_request) @@ -691,7 +691,7 @@ least argument. We would approach this as follows: ```ruby # api/merge_requests/parameters.rb module API - class MergeRequests < Grape::API + class MergeRequests < Grape::API::Instance module Parameters def self.update_params_at_least_one_of %i[ @@ -707,7 +707,7 @@ API::MergeRequests::Parameters.prepend_if_ee('EE::API::MergeRequests::Parameters # api/merge_requests.rb module API - class MergeRequests < Grape::API + class MergeRequests < Grape::API::Instance params do at_least_one_of(*Parameters.update_params_at_least_one_of) end diff --git a/doc/development/permissions.md b/doc/development/permissions.md index 06a4a03de38..e0364342dc6 100644 --- a/doc/development/permissions.md +++ b/doc/development/permissions.md @@ -13,9 +13,16 @@ Groups and projects can have the following visibility levels: - internal (`10`) - an entity is visible to logged in users - private (`0`) - an entity is visible only to the approved members of the entity +By default, subgroups can **not** have higher visibility levels. +For example, if you create a new private group, it can not include a public subgroup. + The visibility level of a group can be changed only if all subgroups and -sub-projects have the same or lower visibility level. (e.g., a group can be set -to internal only if all subgroups and projects are internal or private). +sub-projects have the same or lower visibility level. For example, a group can be set +to internal only if all subgroups and projects are internal or private. + +CAUTION: **Warning:** +If you migrate an existing group to a lower visibility level, that action does not migrate subgroups +in the same way. This is a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/22406). Visibility levels can be found in the `Gitlab::VisibilityLevel` module. diff --git a/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v12_6.png b/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v12_6.png deleted file mode 100644 index 670c90d10a3f9b791f4f73b56c8d211586d9f341..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59799 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 diff --git a/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v13_2.png b/doc/user/application_security/security_dashboard/img/pipeline_security_dashboard_v13_2.png new file mode 100644 index 0000000000000000000000000000000000000000..d935af962126a1aae7abb5023c74e6deb543fc29 GIT binary patch literal 79103 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 z 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 IdGo;k1**V=E&u=k literal 0 HcmV?d00001 diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md index 1594ed77f74..c12ce677de3 100644 --- a/doc/user/application_security/security_dashboard/index.md +++ b/doc/user/application_security/security_dashboard/index.md @@ -45,7 +45,7 @@ At the pipeline level, the Security section displays the vulnerabilities present Visit the page for any pipeline which has run any of the [supported reports](#supported-reports). Click the **Security** tab to view the Security findings. -![Pipeline Security Dashboard](img/pipeline_security_dashboard_v12_6.png) +![Pipeline Security Dashboard](img/pipeline_security_dashboard_v13_2.png) NOTE: **Note:** A pipeline consists of multiple jobs, including SAST and DAST scanning. If any job fails to finish for any reason, the security dashboard will not show SAST scanner output. For example, if the SAST job finishes but the DAST job fails, the security dashboard will not show SAST results. The analyzer will output an [exit code](../../../development/integrations/secure.md#exit-code) on failure. diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index 842e2082be4..8494fb4fa6a 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -19,6 +19,9 @@ By using subgroups you can do the following: - **Make it easier to manage people and control visibility.** Give people different [permissions](../../permissions.md#group-members-permissions) depending on their group [membership](#membership). +For more information on allowed permissions in groups and projects, see +[visibility levels](../../../development/permissions.md#general-permissions). + ## Overview A group can have many subgroups inside it, and at the same time a group can have diff --git a/doc/user/project/import/gemnasium.md b/doc/user/project/import/gemnasium.md index 0d6e059f1cf..3838289aec4 100644 --- a/doc/user/project/import/gemnasium.md +++ b/doc/user/project/import/gemnasium.md @@ -105,7 +105,7 @@ back to both GitLab and GitHub when completed. 1. The result of the job will be visible directly from the pipeline view: - ![Security Dashboard](../../application_security/security_dashboard/img/pipeline_security_dashboard_v12_6.png) + ![Security Dashboard](../../application_security/security_dashboard/img/pipeline_security_dashboard_v13_2.png) NOTE: **Note:** If you don't commit very often to your project, you may want to use diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb index ee8dc822098..5305b25538f 100644 --- a/lib/api/access_requests.rb +++ b/lib/api/access_requests.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class AccessRequests < Grape::API + class AccessRequests < Grape::API::Instance include PaginationParams before { authenticate! } diff --git a/lib/api/admin/ci/variables.rb b/lib/api/admin/ci/variables.rb index df731148bac..6b0ff5e9395 100644 --- a/lib/api/admin/ci/variables.rb +++ b/lib/api/admin/ci/variables.rb @@ -3,7 +3,7 @@ module API module Admin module Ci - class Variables < Grape::API + class Variables < Grape::API::Instance include PaginationParams before { authenticated_as_admin! } diff --git a/lib/api/admin/sidekiq.rb b/lib/api/admin/sidekiq.rb index a700bea0fd7..f4c84f2eee8 100644 --- a/lib/api/admin/sidekiq.rb +++ b/lib/api/admin/sidekiq.rb @@ -2,7 +2,7 @@ module API module Admin - class Sidekiq < Grape::API + class Sidekiq < Grape::API::Instance before { authenticated_as_admin! } namespace 'admin' do diff --git a/lib/api/api.rb b/lib/api/api.rb index 1cdb500e6ac..e5bc49de32e 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class API < Grape::API + class API < Grape::API::Instance include APIGuard LOG_FILENAME = Rails.root.join("log", "api_json.log") @@ -46,6 +46,8 @@ module API end before do + coerce_nil_params_to_array! + Gitlab::ApplicationContext.push( user: -> { @current_user }, project: -> { @project }, diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index c6557fce541..1e56b1f3bfc 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -153,7 +153,16 @@ module API { scope: e.scopes }) end - response.finish + finished_response = nil + response.finish do |rack_response| + # Grape expects a Rack::Response + # (https://github.com/ruby-grape/grape/commit/c117bff7d22971675f4b34367d3a98bc31c8fc02), + # and we need to retrieve it here: + # https://github.com/nov/rack-oauth2/blob/40c9a99fd80486ccb8de0e4869ae384547c0d703/lib/rack/oauth2/server/abstract/error.rb#L28 + finished_response = rack_response + end + + finished_response end end end diff --git a/lib/api/appearance.rb b/lib/api/appearance.rb index 71a35bb4493..f98004af480 100644 --- a/lib/api/appearance.rb +++ b/lib/api/appearance.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Appearance < Grape::API + class Appearance < Grape::API::Instance before { authenticated_as_admin! } helpers do diff --git a/lib/api/applications.rb b/lib/api/applications.rb index 70e6b8395d7..4e8d68c8d09 100644 --- a/lib/api/applications.rb +++ b/lib/api/applications.rb @@ -2,7 +2,7 @@ module API # External applications API - class Applications < Grape::API + class Applications < Grape::API::Instance before { authenticated_as_admin! } resource :applications do diff --git a/lib/api/avatar.rb b/lib/api/avatar.rb index 0f14d003065..9501e777fff 100644 --- a/lib/api/avatar.rb +++ b/lib/api/avatar.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Avatar < Grape::API + class Avatar < Grape::API::Instance resource :avatar do desc 'Return avatar url for a user' do success Entities::Avatar diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb index 8e3b3ff8ce5..0a3df3ed96e 100644 --- a/lib/api/award_emoji.rb +++ b/lib/api/award_emoji.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class AwardEmoji < Grape::API + class AwardEmoji < Grape::API::Instance include PaginationParams before { authenticate! } diff --git a/lib/api/badges.rb b/lib/api/badges.rb index d2152fad07b..f6cd3f83ff3 100644 --- a/lib/api/badges.rb +++ b/lib/api/badges.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Badges < Grape::API + class Badges < Grape::API::Instance include PaginationParams before { authenticate_non_get! } diff --git a/lib/api/boards.rb b/lib/api/boards.rb index 87818903705..1f5086127a8 100644 --- a/lib/api/boards.rb +++ b/lib/api/boards.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Boards < Grape::API + class Boards < Grape::API::Instance include BoardsResponses include PaginationParams diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 081e8ffe4f0..bbed50e96ea 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -3,7 +3,7 @@ require 'mime/types' module API - class Branches < Grape::API + class Branches < Grape::API::Instance include PaginationParams BRANCH_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(branch: API::NO_SLASH_URL_PART_REGEX) diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb index 42e7dc751f0..dcf950d7a03 100644 --- a/lib/api/broadcast_messages.rb +++ b/lib/api/broadcast_messages.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class BroadcastMessages < Grape::API + class BroadcastMessages < Grape::API::Instance include PaginationParams resource :broadcast_messages do diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb index 3116af24ff5..b216406695b 100644 --- a/lib/api/ci/runner.rb +++ b/lib/api/ci/runner.rb @@ -2,7 +2,7 @@ module API module Ci - class Runner < Grape::API + class Runner < Grape::API::Instance helpers ::API::Helpers::Runner resource :runners do @@ -19,7 +19,7 @@ module API optional :access_level, type: String, values: ::Ci::Runner.access_levels.keys, desc: 'The access_level of the runner' optional :run_untagged, type: Boolean, desc: 'Should Runner handle untagged jobs' - optional :tag_list, type: Array[String], desc: %q(List of Runner's tags) + optional :tag_list, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: %q(List of Runner's tags) optional :maximum_timeout, type: Integer, desc: 'Maximum timeout set when this Runner will handle the job' end post '/' do diff --git a/lib/api/ci/runners.rb b/lib/api/ci/runners.rb index a4ef54534ba..2c156a71160 100644 --- a/lib/api/ci/runners.rb +++ b/lib/api/ci/runners.rb @@ -2,7 +2,7 @@ module API module Ci - class Runners < Grape::API + class Runners < Grape::API::Instance include PaginationParams before { authenticate! } @@ -18,7 +18,7 @@ module API desc: 'The type of the runners to show' optional :status, type: String, values: ::Ci::Runner::AVAILABLE_STATUSES, desc: 'The status of the runners to show' - optional :tag_list, type: Array[String], desc: 'The tags of the runners to show' + optional :tag_list, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The tags of the runners to show' use :pagination end get do @@ -41,7 +41,7 @@ module API desc: 'The type of the runners to show' optional :status, type: String, values: ::Ci::Runner::AVAILABLE_STATUSES, desc: 'The status of the runners to show' - optional :tag_list, type: Array[String], desc: 'The tags of the runners to show' + optional :tag_list, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The tags of the runners to show' use :pagination end get 'all' do @@ -76,7 +76,7 @@ module API requires :id, type: Integer, desc: 'The ID of the runner' optional :description, type: String, desc: 'The description of the runner' optional :active, type: Boolean, desc: 'The state of a runner' - optional :tag_list, type: Array[String], desc: 'The list of tags for a runner' + optional :tag_list, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The list of tags for a runner' optional :run_untagged, type: Boolean, desc: 'Flag indicating the runner can execute untagged jobs' optional :locked, type: Boolean, desc: 'Flag indicating the runner is locked' optional :access_level, type: String, values: ::Ci::Runner.access_levels.keys, @@ -146,7 +146,7 @@ module API desc: 'The type of the runners to show' optional :status, type: String, values: ::Ci::Runner::AVAILABLE_STATUSES, desc: 'The status of the runners to show' - optional :tag_list, type: Array[String], desc: 'The tags of the runners to show' + optional :tag_list, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The tags of the runners to show' use :pagination end get ':id/runners' do @@ -209,7 +209,7 @@ module API desc: 'The type of the runners to show' optional :status, type: String, values: ::Ci::Runner::AVAILABLE_STATUSES, desc: 'The status of the runners to show' - optional :tag_list, type: Array[String], desc: 'The tags of the runners to show' + optional :tag_list, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The tags of the runners to show' use :pagination end get ':id/runners' do diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index fcd317b2daa..140351c9e5c 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -3,7 +3,7 @@ require 'mime/types' module API - class CommitStatuses < Grape::API + class CommitStatuses < Grape::API::Instance params do requires :id, type: String, desc: 'The ID of a project' end diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 086a1b7c402..1a0fe393753 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -3,7 +3,7 @@ require 'mime/types' module API - class Commits < Grape::API + class Commits < Grape::API::Instance include PaginationParams before do diff --git a/lib/api/container_registry_event.rb b/lib/api/container_registry_event.rb index 6d93cc65336..0b7c35cadbd 100644 --- a/lib/api/container_registry_event.rb +++ b/lib/api/container_registry_event.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ContainerRegistryEvent < Grape::API + class ContainerRegistryEvent < Grape::API::Instance DOCKER_DISTRIBUTION_EVENTS_V1_JSON = 'application/vnd.docker.distribution.events.v1+json' before { authenticate_registry_notification! } diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 3259b615369..ad37b7578ad 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class DeployKeys < Grape::API + class DeployKeys < Grape::API::Instance include PaginationParams before { authenticate! } diff --git a/lib/api/deploy_tokens.rb b/lib/api/deploy_tokens.rb index 0fbbd96cf02..96aa2445f56 100644 --- a/lib/api/deploy_tokens.rb +++ b/lib/api/deploy_tokens.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class DeployTokens < Grape::API + class DeployTokens < Grape::API::Instance include PaginationParams helpers do @@ -56,7 +56,7 @@ module API params do requires :name, type: String, desc: "New deploy token's name" - requires :scopes, type: Array[String], values: ::DeployToken::AVAILABLE_SCOPES.map(&:to_s), + requires :scopes, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, values: ::DeployToken::AVAILABLE_SCOPES.map(&:to_s), desc: 'Indicates the deploy token scopes. Must be at least one of "read_repository", "read_registry", "write_registry", "read_package_registry", or "write_package_registry".' optional :expires_at, type: DateTime, desc: 'Expiration date for the deploy token. Does not expire if no value is provided.' optional :username, type: String, desc: 'Username for deploy token. Default is `gitlab+deploy-token-{n}`' @@ -119,7 +119,7 @@ module API params do requires :name, type: String, desc: 'The name of the deploy token' - requires :scopes, type: Array[String], values: ::DeployToken::AVAILABLE_SCOPES.map(&:to_s), + requires :scopes, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, values: ::DeployToken::AVAILABLE_SCOPES.map(&:to_s), desc: 'Indicates the deploy token scopes. Must be at least one of "read_repository", "read_registry", "write_registry", "read_package_registry", or "write_package_registry".' optional :expires_at, type: DateTime, desc: 'Expiration date for the deploy token. Does not expire if no value is provided.' optional :username, type: String, desc: 'Username for deploy token. Default is `gitlab+deploy-token-{n}`' diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb index cb1dca11e87..87144fd31cc 100644 --- a/lib/api/deployments.rb +++ b/lib/api/deployments.rb @@ -2,7 +2,7 @@ module API # Deployments RESTful API endpoints - class Deployments < Grape::API + class Deployments < Grape::API::Instance include PaginationParams before { authenticate! } diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb index 7b453ada41c..29a13e4420d 100644 --- a/lib/api/discussions.rb +++ b/lib/api/discussions.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Discussions < Grape::API + class Discussions < Grape::API::Instance include PaginationParams helpers ::API::Helpers::NotesHelpers helpers ::RendersNotes diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 28019ce7796..b825904e2c5 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -2,7 +2,7 @@ module API # Environments RESTfull API endpoints - class Environments < Grape::API + class Environments < Grape::API::Instance include PaginationParams before { authenticate! } diff --git a/lib/api/error_tracking.rb b/lib/api/error_tracking.rb index 14888037f53..64ec6f0a57a 100644 --- a/lib/api/error_tracking.rb +++ b/lib/api/error_tracking.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ErrorTracking < Grape::API + class ErrorTracking < Grape::API::Instance before { authenticate! } params do diff --git a/lib/api/events.rb b/lib/api/events.rb index e4c017fab42..0b79431a76d 100644 --- a/lib/api/events.rb +++ b/lib/api/events.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Events < Grape::API + class Events < Grape::API::Instance include PaginationParams include APIGuard helpers ::API::Helpers::EventsHelpers diff --git a/lib/api/features.rb b/lib/api/features.rb index 3fb3fc92e42..9d011d658f6 100644 --- a/lib/api/features.rb +++ b/lib/api/features.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Features < Grape::API + class Features < Grape::API::Instance before { authenticated_as_admin! } helpers do diff --git a/lib/api/files.rb b/lib/api/files.rb index c525897b8fe..748bdfa894d 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Files < Grape::API + class Files < Grape::API::Instance include APIGuard FILE_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(file_path: API::NO_SLASH_URL_PART_REGEX) diff --git a/lib/api/freeze_periods.rb b/lib/api/freeze_periods.rb index 9c7e5a5832d..b8254ee9ab4 100644 --- a/lib/api/freeze_periods.rb +++ b/lib/api/freeze_periods.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class FreezePeriods < Grape::API + class FreezePeriods < Grape::API::Instance include PaginationParams before { authenticate! } diff --git a/lib/api/group_boards.rb b/lib/api/group_boards.rb index 88d04e70e11..7efc12121d2 100644 --- a/lib/api/group_boards.rb +++ b/lib/api/group_boards.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class GroupBoards < Grape::API + class GroupBoards < Grape::API::Instance include BoardsResponses include PaginationParams diff --git a/lib/api/group_clusters.rb b/lib/api/group_clusters.rb index 2c12c6387fb..c6d10f22bb4 100644 --- a/lib/api/group_clusters.rb +++ b/lib/api/group_clusters.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class GroupClusters < Grape::API + class GroupClusters < Grape::API::Instance include PaginationParams before { authenticate! } diff --git a/lib/api/group_container_repositories.rb b/lib/api/group_container_repositories.rb index d34317b5271..25b3059f63b 100644 --- a/lib/api/group_container_repositories.rb +++ b/lib/api/group_container_repositories.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class GroupContainerRepositories < Grape::API + class GroupContainerRepositories < Grape::API::Instance include PaginationParams before { authorize_read_group_container_images! } diff --git a/lib/api/group_export.rb b/lib/api/group_export.rb index d3010b6d147..dc14813eefc 100644 --- a/lib/api/group_export.rb +++ b/lib/api/group_export.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class GroupExport < Grape::API + class GroupExport < Grape::API::Instance helpers Helpers::RateLimiter before do diff --git a/lib/api/group_import.rb b/lib/api/group_import.rb index afcbc24d3ce..b82d9fc519a 100644 --- a/lib/api/group_import.rb +++ b/lib/api/group_import.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class GroupImport < Grape::API + class GroupImport < Grape::API::Instance helpers Helpers::FileUploadHelpers helpers do diff --git a/lib/api/group_labels.rb b/lib/api/group_labels.rb index 7585293031f..56f2b769464 100644 --- a/lib/api/group_labels.rb +++ b/lib/api/group_labels.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class GroupLabels < Grape::API + class GroupLabels < Grape::API::Instance include PaginationParams helpers ::API::Helpers::LabelHelpers diff --git a/lib/api/group_milestones.rb b/lib/api/group_milestones.rb index 9e9f5101285..82f5df79356 100644 --- a/lib/api/group_milestones.rb +++ b/lib/api/group_milestones.rb @@ -1,13 +1,11 @@ # frozen_string_literal: true module API - class GroupMilestones < Grape::API + class GroupMilestones < Grape::API::Instance include MilestoneResponses include PaginationParams - before do - authenticate! - end + before { authenticate! } params do requires :id, type: String, desc: 'The ID of a group' diff --git a/lib/api/group_variables.rb b/lib/api/group_variables.rb index 95aa587d0c7..d3ca1c79e73 100644 --- a/lib/api/group_variables.rb +++ b/lib/api/group_variables.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class GroupVariables < Grape::API + class GroupVariables < Grape::API::Instance include PaginationParams before { authenticate! } diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 6e07bb46721..4671e82ab66 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Groups < Grape::API + class Groups < Grape::API::Instance include PaginationParams include Helpers::CustomAttributes @@ -16,7 +16,7 @@ module API params :group_list_params do use :statistics_params - optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list' + optional :skip_groups, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'Array of group ids to exclude from list' optional :all_available, type: Boolean, desc: 'Show all group that you have access to' optional :search, type: String, desc: 'Search for a specific group' optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user' diff --git a/lib/api/helpers/common_helpers.rb b/lib/api/helpers/common_helpers.rb index 32a15381f27..a44fd4b0a5b 100644 --- a/lib/api/helpers/common_helpers.rb +++ b/lib/api/helpers/common_helpers.rb @@ -12,6 +12,26 @@ module API end end end + + # Grape v1.3.3 no longer automatically coerces an Array + # type to an empty array if the value is nil. + def coerce_nil_params_to_array! + keys_to_coerce = params_with_array_types + + params.each do |key, val| + params[key] = Array(val) if val.nil? && keys_to_coerce.include?(key) + end + end + + def params_with_array_types + options[:route_options][:params].map do |key, val| + param_type = val[:type] + # Search for parameters with Array types (e.g. "[String]", "[Integer]", etc.) + if param_type =~ %r(\[\w*\]) + key + end + end.compact.to_set + end end end end diff --git a/lib/api/helpers/merge_requests_helpers.rb b/lib/api/helpers/merge_requests_helpers.rb index 9dab2a88f0b..939f29a04b3 100644 --- a/lib/api/helpers/merge_requests_helpers.rb +++ b/lib/api/helpers/merge_requests_helpers.rb @@ -24,7 +24,7 @@ module API optional :milestone, type: String, desc: 'Return merge requests for a specific milestone' optional :labels, type: Array[String], - coerce_with: Validations::Types::LabelsList.coerce, + coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names' optional :with_labels_details, type: Boolean, desc: 'Return titles of labels and other details', default: false optional :with_merge_status_recheck, type: Boolean, desc: 'Request that stale merge statuses be rechecked asynchronously', default: false diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index 92030415d02..d541d3d42db 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -48,7 +48,7 @@ module API optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed' optional :allow_merge_on_skipped_pipeline, type: Boolean, desc: 'Allow to merge if pipeline is skipped' optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved' - optional :tag_list, type: Array[String], desc: 'The list of tags for a project' + optional :tag_list, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The list of tags for a project' # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab/issues/14960 optional :avatar, type: File, desc: 'Avatar image for project' # rubocop:disable Scalability/FileUploads optional :printing_merge_request_link_enabled, type: Boolean, desc: 'Show link to create/view merge request when pushing from the command line' diff --git a/lib/api/import_github.rb b/lib/api/import_github.rb index 21d4928193e..986827e80be 100644 --- a/lib/api/import_github.rb +++ b/lib/api/import_github.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ImportGithub < Grape::API + class ImportGithub < Grape::API::Instance rescue_from Octokit::Unauthorized, with: :provider_unauthorized helpers do diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb index 83615c7639b..6d4a4fc9c8b 100644 --- a/lib/api/internal/base.rb +++ b/lib/api/internal/base.rb @@ -3,7 +3,7 @@ module API # Internal access API module Internal - class Base < Grape::API + class Base < Grape::API::Instance before { authenticate_by_gitlab_shell_token! } before do diff --git a/lib/api/internal/pages.rb b/lib/api/internal/pages.rb index 6c8da414e4d..5f8d23f15fa 100644 --- a/lib/api/internal/pages.rb +++ b/lib/api/internal/pages.rb @@ -3,7 +3,7 @@ module API # Pages Internal API module Internal - class Pages < Grape::API + class Pages < Grape::API::Instance before do authenticate_gitlab_pages_request! end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 2374ac11f4a..93b0fbc5223 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Issues < Grape::API + class Issues < Grape::API::Instance include PaginationParams helpers Helpers::IssuesHelpers helpers Helpers::RateLimiter @@ -10,9 +10,9 @@ module API helpers do params :negatable_issue_filter_params do - optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names' + optional :labels, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names' optional :milestone, type: String, desc: 'Milestone title' - optional :iids, type: Array[Integer], desc: 'The IID array of issues' + optional :iids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The IID array of issues' optional :search, type: String, desc: 'Search issues for text present in the title, description, or any combination of these' optional :in, type: String, desc: '`title`, `description`, or a string joining them with comma' @@ -62,12 +62,12 @@ module API params :issue_params do optional :description, type: String, desc: 'The description of an issue' - optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue' + optional :assignee_ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The array of user IDs to assign issue' optional :assignee_id, type: Integer, desc: '[Deprecated] The ID of a user to assign issue' optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue' - optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names' - optional :add_labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names' - optional :remove_labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names' + optional :labels, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names' + optional :add_labels, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names' + optional :remove_labels, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names' optional :due_date, type: String, desc: 'Date string in the format YEAR-MONTH-DAY' optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential' optional :discussion_locked, type: Boolean, desc: " Boolean parameter indicating if the issue's discussion is locked" diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb index 6a82256cc96..61c279a76e9 100644 --- a/lib/api/job_artifacts.rb +++ b/lib/api/job_artifacts.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class JobArtifacts < Grape::API + class JobArtifacts < Grape::API::Instance before { authenticate_non_get! } # EE::API::JobArtifacts would override the following helpers diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb index 9432b992d74..bcc00429dd6 100644 --- a/lib/api/jobs.rb +++ b/lib/api/jobs.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Jobs < Grape::API + class Jobs < Grape::API::Instance include PaginationParams before { authenticate! } diff --git a/lib/api/keys.rb b/lib/api/keys.rb index b730e027063..c014641ca04 100644 --- a/lib/api/keys.rb +++ b/lib/api/keys.rb @@ -2,7 +2,7 @@ module API # Keys API - class Keys < Grape::API + class Keys < Grape::API::Instance before { authenticate! } resource :keys do diff --git a/lib/api/labels.rb b/lib/api/labels.rb index 2b283d82e4a..edf4a8ca14e 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Labels < Grape::API + class Labels < Grape::API::Instance include PaginationParams helpers ::API::Helpers::LabelHelpers diff --git a/lib/api/lint.rb b/lib/api/lint.rb index a7672021db0..f7796b1e969 100644 --- a/lib/api/lint.rb +++ b/lib/api/lint.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Lint < Grape::API + class Lint < Grape::API::Instance namespace :ci do desc 'Validation of .gitlab-ci.yml content' params do diff --git a/lib/api/markdown.rb b/lib/api/markdown.rb index de77bef43ce..a0822271cca 100644 --- a/lib/api/markdown.rb +++ b/lib/api/markdown.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Markdown < Grape::API + class Markdown < Grape::API::Instance params do requires :text, type: String, desc: "The markdown text to render" optional :gfm, type: Boolean, desc: "Render text using GitLab Flavored Markdown" diff --git a/lib/api/members.rb b/lib/api/members.rb index 341d4837468..3de7ae13d5e 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Members < Grape::API + class Members < Grape::API::Instance include PaginationParams before { authenticate! } @@ -18,7 +18,7 @@ module API end params do optional :query, type: String, desc: 'A query string to search for members' - optional :user_ids, type: Array[Integer], desc: 'Array of user ids to look up for membership' + optional :user_ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'Array of user ids to look up for membership' optional :show_seat_info, type: Boolean, desc: 'Show seat information for members' use :optional_filter_params_ee use :pagination @@ -37,7 +37,7 @@ module API end params do optional :query, type: String, desc: 'A query string to search for members' - optional :user_ids, type: Array[Integer], desc: 'Array of user ids to look up for membership' + optional :user_ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'Array of user ids to look up for membership' optional :show_seat_info, type: Boolean, desc: 'Show seat information for members' use :pagination end diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb index 6ad30aa56e0..3e43fe8b257 100644 --- a/lib/api/merge_request_diffs.rb +++ b/lib/api/merge_request_diffs.rb @@ -2,7 +2,7 @@ module API # MergeRequestDiff API - class MergeRequestDiffs < Grape::API + class MergeRequestDiffs < Grape::API::Instance include PaginationParams before { authenticate! } diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 773a451d3a8..82f103ff0ae 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class MergeRequests < Grape::API + class MergeRequests < Grape::API::Instance include PaginationParams CONTEXT_COMMITS_POST_LIMIT = 20 @@ -179,11 +179,11 @@ module API params :optional_params do optional :description, type: String, desc: 'The description of the merge request' optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request' - optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue' + optional :assignee_ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The array of user IDs to assign issue' optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request' - optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names' - optional :add_labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names' - optional :remove_labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names' + optional :labels, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names' + optional :add_labels, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names' + optional :remove_labels, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names' optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging' optional :allow_collaboration, type: Boolean, desc: 'Allow commits from members who can merge to the target branch' optional :allow_maintainer_to_push, type: Boolean, as: :allow_collaboration, desc: '[deprecated] See allow_collaboration' @@ -198,7 +198,7 @@ module API end params do use :merge_requests_params - optional :iids, type: Array[Integer], desc: 'The IID array of merge requests' + optional :iids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The IID array of merge requests' end get ":id/merge_requests" do authorize! :read_merge_request, user_project @@ -315,7 +315,7 @@ module API end params do - requires :commits, type: Array, allow_blank: false, desc: 'List of context commits sha' + requires :commits, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, allow_blank: false, desc: 'List of context commits sha' end desc 'create context commits of merge request' do success Entities::Commit @@ -345,7 +345,7 @@ module API end params do - requires :commits, type: Array, allow_blank: false, desc: 'List of context commits sha' + requires :commits, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, allow_blank: false, desc: 'List of context commits sha' end desc 'remove context commits of merge request' delete ':id/merge_requests/:merge_request_iid/context_commits' do diff --git a/lib/api/metrics/dashboard/annotations.rb b/lib/api/metrics/dashboard/annotations.rb index c8ec4d29657..e07762ac6d3 100644 --- a/lib/api/metrics/dashboard/annotations.rb +++ b/lib/api/metrics/dashboard/annotations.rb @@ -3,7 +3,7 @@ module API module Metrics module Dashboard - class Annotations < Grape::API + class Annotations < Grape::API::Instance desc 'Create a new monitoring dashboard annotation' do success Entities::Metrics::Dashboard::Annotation end diff --git a/lib/api/metrics/user_starred_dashboards.rb b/lib/api/metrics/user_starred_dashboards.rb index 85fc0f33ed8..263d2394276 100644 --- a/lib/api/metrics/user_starred_dashboards.rb +++ b/lib/api/metrics/user_starred_dashboards.rb @@ -2,7 +2,7 @@ module API module Metrics - class UserStarredDashboards < Grape::API + class UserStarredDashboards < Grape::API::Instance resource :projects do desc 'Marks selected metrics dashboard as starred' do success Entities::Metrics::UserStarredDashboard diff --git a/lib/api/milestone_responses.rb b/lib/api/milestone_responses.rb index 62e159ab003..8ff885983bc 100644 --- a/lib/api/milestone_responses.rb +++ b/lib/api/milestone_responses.rb @@ -15,7 +15,7 @@ module API params :list_params do optional :state, type: String, values: %w[active closed all], default: 'all', desc: 'Return "active", "closed", or "all" milestones' - optional :iids, type: Array[Integer], desc: 'The IIDs of the milestones' + optional :iids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The IIDs of the milestones' optional :title, type: String, desc: 'The title of the milestones' optional :search, type: String, desc: 'The search criteria for the title or description of the milestone' use :pagination diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb index e40a5dde7ce..e1f279df045 100644 --- a/lib/api/namespaces.rb +++ b/lib/api/namespaces.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Namespaces < Grape::API + class Namespaces < Grape::API::Instance include PaginationParams before { authenticate! } diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 3eafc1ead77..4fb7bffb3d5 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Notes < Grape::API + class Notes < Grape::API::Instance include PaginationParams helpers ::API::Helpers::NotesHelpers diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb index 8cb46bd3ad6..f8b621c1c38 100644 --- a/lib/api/notification_settings.rb +++ b/lib/api/notification_settings.rb @@ -2,7 +2,7 @@ module API # notification_settings API - class NotificationSettings < Grape::API + class NotificationSettings < Grape::API::Instance before { authenticate! } helpers ::API::Helpers::MembersHelpers diff --git a/lib/api/pages.rb b/lib/api/pages.rb index ee7fe669519..79a6b527581 100644 --- a/lib/api/pages.rb +++ b/lib/api/pages.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Pages < Grape::API + class Pages < Grape::API::Instance before do require_pages_config_enabled! authenticated_with_can_read_all_resources! diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb index 4c3d2d131ac..7d27b575efa 100644 --- a/lib/api/pages_domains.rb +++ b/lib/api/pages_domains.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class PagesDomains < Grape::API + class PagesDomains < Grape::API::Instance include PaginationParams PAGES_DOMAINS_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(domain: API::NO_SLASH_URL_PART_REGEX) diff --git a/lib/api/pagination_params.rb b/lib/api/pagination_params.rb index ae03595eb25..a232b58d3f7 100644 --- a/lib/api/pagination_params.rb +++ b/lib/api/pagination_params.rb @@ -4,7 +4,7 @@ module API # Concern for declare pagination params. # # @example - # class CustomApiResource < Grape::API + # class CustomApiResource < Grape::API::Instance # include PaginationParams # # params do diff --git a/lib/api/pipeline_schedules.rb b/lib/api/pipeline_schedules.rb index 7486518cd4a..9af16f61967 100644 --- a/lib/api/pipeline_schedules.rb +++ b/lib/api/pipeline_schedules.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class PipelineSchedules < Grape::API + class PipelineSchedules < Grape::API::Instance include PaginationParams before { authenticate! } diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb index b27ff2d24f8..1aac7b7deb4 100644 --- a/lib/api/pipelines.rb +++ b/lib/api/pipelines.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Pipelines < Grape::API + class Pipelines < Grape::API::Instance include PaginationParams before { authenticate_non_get! } diff --git a/lib/api/project_clusters.rb b/lib/api/project_clusters.rb index 299301aabc4..e1dfb647fa0 100644 --- a/lib/api/project_clusters.rb +++ b/lib/api/project_clusters.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ProjectClusters < Grape::API + class ProjectClusters < Grape::API::Instance include PaginationParams before { authenticate! } diff --git a/lib/api/project_container_repositories.rb b/lib/api/project_container_repositories.rb index 2a0099018d9..8f2a62bc5a4 100644 --- a/lib/api/project_container_repositories.rb +++ b/lib/api/project_container_repositories.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ProjectContainerRepositories < Grape::API + class ProjectContainerRepositories < Grape::API::Instance include PaginationParams REPOSITORY_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge( diff --git a/lib/api/project_events.rb b/lib/api/project_events.rb index 734311e1142..726e693826e 100644 --- a/lib/api/project_events.rb +++ b/lib/api/project_events.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ProjectEvents < Grape::API + class ProjectEvents < Grape::API::Instance include PaginationParams include APIGuard helpers ::API::Helpers::EventsHelpers diff --git a/lib/api/project_export.rb b/lib/api/project_export.rb index 4b35f245b8c..d11c47f8d78 100644 --- a/lib/api/project_export.rb +++ b/lib/api/project_export.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ProjectExport < Grape::API + class ProjectExport < Grape::API::Instance helpers Helpers::RateLimiter before do diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index 0e7576c9243..7cea44e6304 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ProjectHooks < Grape::API + class ProjectHooks < Grape::API::Instance include PaginationParams before { authenticate! } diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index 17d08d14a20..9f43c3c7993 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ProjectImport < Grape::API + class ProjectImport < Grape::API::Instance include PaginationParams MAXIMUM_FILE_SIZE = 50.megabytes diff --git a/lib/api/project_milestones.rb b/lib/api/project_milestones.rb index 8643854a655..2f8dd1085dc 100644 --- a/lib/api/project_milestones.rb +++ b/lib/api/project_milestones.rb @@ -1,13 +1,11 @@ # frozen_string_literal: true module API - class ProjectMilestones < Grape::API + class ProjectMilestones < Grape::API::Instance include PaginationParams include MilestoneResponses - before do - authenticate! - end + before { authenticate! } params do requires :id, type: String, desc: 'The ID of a project' diff --git a/lib/api/project_repository_storage_moves.rb b/lib/api/project_repository_storage_moves.rb index 5de623102fb..c318907542b 100644 --- a/lib/api/project_repository_storage_moves.rb +++ b/lib/api/project_repository_storage_moves.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ProjectRepositoryStorageMoves < Grape::API + class ProjectRepositoryStorageMoves < Grape::API::Instance include PaginationParams before { authenticated_as_admin! } diff --git a/lib/api/project_snapshots.rb b/lib/api/project_snapshots.rb index 175fbb2ce92..360000861fc 100644 --- a/lib/api/project_snapshots.rb +++ b/lib/api/project_snapshots.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ProjectSnapshots < Grape::API + class ProjectSnapshots < Grape::API::Instance helpers ::API::Helpers::ProjectSnapshotsHelpers before { authorize_read_git_snapshot! } diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index 68f4a0dcb65..d93454f949d 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ProjectSnippets < Grape::API + class ProjectSnippets < Grape::API::Instance include PaginationParams before { authenticate! } diff --git a/lib/api/project_statistics.rb b/lib/api/project_statistics.rb index 14ee0f75513..2196801096f 100644 --- a/lib/api/project_statistics.rb +++ b/lib/api/project_statistics.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ProjectStatistics < Grape::API + class ProjectStatistics < Grape::API::Instance before do authenticate! authorize! :daily_statistics, user_project diff --git a/lib/api/project_templates.rb b/lib/api/project_templates.rb index cfcc7f5212d..f0fe4d85c8f 100644 --- a/lib/api/project_templates.rb +++ b/lib/api/project_templates.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ProjectTemplates < Grape::API + class ProjectTemplates < Grape::API::Instance include PaginationParams TEMPLATE_TYPES = %w[dockerfiles gitignores gitlab_ci_ymls licenses].freeze diff --git a/lib/api/projects.rb b/lib/api/projects.rb index f4582d36f4b..d24dab63bd9 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -3,7 +3,7 @@ require_dependency 'declarative_policy' module API - class Projects < Grape::API + class Projects < Grape::API::Instance include PaginationParams include Helpers::CustomAttributes @@ -546,7 +546,7 @@ module API end params do optional :search, type: String, desc: 'Return list of users matching the search criteria' - optional :skip_users, type: Array[Integer], desc: 'Filter out users with the specified IDs' + optional :skip_users, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'Filter out users with the specified IDs' use :pagination end get ':id/users' do diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb index 1fd86d1e720..b0a7f898eec 100644 --- a/lib/api/protected_branches.rb +++ b/lib/api/protected_branches.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ProtectedBranches < Grape::API + class ProtectedBranches < Grape::API::Instance include PaginationParams BRANCH_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(name: API::NO_SLASH_URL_PART_REGEX) diff --git a/lib/api/protected_tags.rb b/lib/api/protected_tags.rb index ee13473c848..aaa31cb7cc6 100644 --- a/lib/api/protected_tags.rb +++ b/lib/api/protected_tags.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ProtectedTags < Grape::API + class ProtectedTags < Grape::API::Instance include PaginationParams TAG_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(name: API::NO_SLASH_URL_PART_REGEX) diff --git a/lib/api/release/links.rb b/lib/api/release/links.rb index 07c27f39539..7e1815480a5 100644 --- a/lib/api/release/links.rb +++ b/lib/api/release/links.rb @@ -2,7 +2,7 @@ module API module Release - class Links < Grape::API + class Links < Grape::API::Instance include PaginationParams RELEASE_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS diff --git a/lib/api/releases.rb b/lib/api/releases.rb index a5bb1a44f1f..30c5e06053e 100644 --- a/lib/api/releases.rb +++ b/lib/api/releases.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Releases < Grape::API + class Releases < Grape::API::Instance include PaginationParams RELEASE_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS @@ -54,7 +54,7 @@ module API requires :url, type: String end end - optional :milestones, type: Array, desc: 'The titles of the related milestones', default: [] + optional :milestones, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The titles of the related milestones', default: [] optional :released_at, type: DateTime, desc: 'The date when the release will be/was ready. Defaults to the current time.' end route_setting :authentication, job_token_allowed: true diff --git a/lib/api/remote_mirrors.rb b/lib/api/remote_mirrors.rb index 0808541d3c7..d1def05808b 100644 --- a/lib/api/remote_mirrors.rb +++ b/lib/api/remote_mirrors.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class RemoteMirrors < Grape::API + class RemoteMirrors < Grape::API::Instance include PaginationParams before do diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index bf4f08ce390..81702f8f02a 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -3,7 +3,7 @@ require 'mime/types' module API - class Repositories < Grape::API + class Repositories < Grape::API::Instance include PaginationParams helpers ::API::Helpers::HeadersHelpers @@ -143,7 +143,7 @@ module API success Entities::Commit end params do - requires :refs, type: Array[String] + requires :refs, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce end get ':id/repository/merge_base' do refs = params[:refs] diff --git a/lib/api/resource_label_events.rb b/lib/api/resource_label_events.rb index 1fa6898b92c..a8d3419528c 100644 --- a/lib/api/resource_label_events.rb +++ b/lib/api/resource_label_events.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ResourceLabelEvents < Grape::API + class ResourceLabelEvents < Grape::API::Instance include PaginationParams helpers ::API::Helpers::NotesHelpers diff --git a/lib/api/resource_milestone_events.rb b/lib/api/resource_milestone_events.rb index e466e236371..a8f221f8740 100644 --- a/lib/api/resource_milestone_events.rb +++ b/lib/api/resource_milestone_events.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ResourceMilestoneEvents < Grape::API + class ResourceMilestoneEvents < Grape::API::Instance include PaginationParams helpers ::API::Helpers::NotesHelpers diff --git a/lib/api/search.rb b/lib/api/search.rb index ac00d3682a0..092da6571c8 100644 --- a/lib/api/search.rb +++ b/lib/api/search.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Search < Grape::API + class Search < Grape::API::Instance include PaginationParams before { authenticate! } diff --git a/lib/api/services.rb b/lib/api/services.rb index 5fd5c6bd9b0..9ee1822339c 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true module API - class Services < Grape::API + class Services < Grape::API::Instance services = Helpers::ServicesHelpers.services service_classes = Helpers::ServicesHelpers.service_classes diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 0bf5eed26b4..3463e29041b 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Settings < Grape::API + class Settings < Grape::API::Instance before { authenticated_as_admin! } helpers Helpers::SettingsHelpers @@ -49,7 +49,7 @@ module API optional :default_project_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default project visibility' optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects' optional :default_snippet_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default snippet visibility' - optional :disabled_oauth_sign_in_sources, type: Array[String], desc: 'Disable certain OAuth sign-in sources' + optional :disabled_oauth_sign_in_sources, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Disable certain OAuth sign-in sources' optional :domain_blacklist_enabled, type: Boolean, desc: 'Enable domain blacklist for sign ups' optional :domain_blacklist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com' optional :domain_whitelist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com' @@ -79,7 +79,8 @@ module API requires :housekeeping_incremental_repack_period, type: Integer, desc: "Number of Git pushes after which an incremental 'git repack' is run." end optional :html_emails_enabled, type: Boolean, desc: 'By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format.' - optional :import_sources, type: Array[String], values: %w[github bitbucket bitbucket_server gitlab google_code fogbugz git gitlab_project gitea manifest phabricator], + optional :import_sources, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, + values: %w[github bitbucket bitbucket_server gitlab google_code fogbugz git gitlab_project gitea manifest phabricator], desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com' optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size for each job's artifacts" optional :max_attachment_size, type: Integer, desc: 'Maximum attachment size in MB' @@ -113,13 +114,13 @@ module API requires :recaptcha_private_key, type: String, desc: 'Generate private key at http://www.google.com/recaptcha' end optional :repository_checks_enabled, type: Boolean, desc: "GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues." - optional :repository_storages, type: Array[String], desc: 'Storage paths for new projects' + optional :repository_storages, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Storage paths for new projects' optional :repository_storages_weighted, type: Hash, desc: 'Storage paths for new projects with a weighted value between 0 and 100' optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users to set up Two-factor authentication' given require_two_factor_authentication: ->(val) { val } do requires :two_factor_grace_period, type: Integer, desc: 'Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication' end - optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for groups, projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.' + optional :restricted_visibility_levels, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Selected levels cannot be used by non-admin users for groups, projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.' optional :send_user_confirmation_email, type: Boolean, desc: 'Send confirmation email on sign-up' optional :session_expire_delay, type: Integer, desc: 'Session duration in minutes. GitLab restart is required to apply changes.' optional :shared_runners_enabled, type: Boolean, desc: 'Enable shared runners for new projects' diff --git a/lib/api/sidekiq_metrics.rb b/lib/api/sidekiq_metrics.rb index 693c20cb73a..de1373144e3 100644 --- a/lib/api/sidekiq_metrics.rb +++ b/lib/api/sidekiq_metrics.rb @@ -3,7 +3,7 @@ require 'sidekiq/api' module API - class SidekiqMetrics < Grape::API + class SidekiqMetrics < Grape::API::Instance before { authenticated_as_admin! } helpers do diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index be58b832f97..3e6ccf7c0cf 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -2,7 +2,7 @@ module API # Snippets API - class Snippets < Grape::API + class Snippets < Grape::API::Instance include PaginationParams before { authenticate! } diff --git a/lib/api/statistics.rb b/lib/api/statistics.rb index d2dce34dfa5..3869fd3ac76 100644 --- a/lib/api/statistics.rb +++ b/lib/api/statistics.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Statistics < Grape::API + class Statistics < Grape::API::Instance before { authenticated_as_admin! } COUNTED_ITEMS = [Project, User, Group, ForkNetworkMember, ForkNetwork, Issue, diff --git a/lib/api/submodules.rb b/lib/api/submodules.rb index 72d7d994102..34d21d3d7d8 100644 --- a/lib/api/submodules.rb +++ b/lib/api/submodules.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Submodules < Grape::API + class Submodules < Grape::API::Instance before { authenticate! } helpers do diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb index dfb54446ddf..533663fb087 100644 --- a/lib/api/subscriptions.rb +++ b/lib/api/subscriptions.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Subscriptions < Grape::API + class Subscriptions < Grape::API::Instance helpers ::API::Helpers::LabelHelpers before { authenticate! } diff --git a/lib/api/suggestions.rb b/lib/api/suggestions.rb index 05aaa8a6f41..38e96c080f2 100644 --- a/lib/api/suggestions.rb +++ b/lib/api/suggestions.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Suggestions < Grape::API + class Suggestions < Grape::API::Instance before { authenticate! } resource :suggestions do @@ -25,7 +25,7 @@ module API success Entities::Suggestion end params do - requires :ids, type: Array[String], desc: "An array of suggestion ID's" + requires :ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: "An array of suggestion ID's" end put 'batch_apply' do ids = params[:ids] diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb index 51fae0e54aa..d8e0a425625 100644 --- a/lib/api/system_hooks.rb +++ b/lib/api/system_hooks.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class SystemHooks < Grape::API + class SystemHooks < Grape::API::Instance include PaginationParams before do diff --git a/lib/api/tags.rb b/lib/api/tags.rb index 796b1450602..c1fbd3ca7c6 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Tags < Grape::API + class Tags < Grape::API::Instance include PaginationParams TAG_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(tag_name: API::NO_SLASH_URL_PART_REGEX) diff --git a/lib/api/templates.rb b/lib/api/templates.rb index 51f357d9477..80a97aae429 100644 --- a/lib/api/templates.rb +++ b/lib/api/templates.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Templates < Grape::API + class Templates < Grape::API::Instance include PaginationParams GLOBAL_TEMPLATE_TYPES = { diff --git a/lib/api/terraform/state.rb b/lib/api/terraform/state.rb index e7c9627c753..6b9809c76a4 100644 --- a/lib/api/terraform/state.rb +++ b/lib/api/terraform/state.rb @@ -4,7 +4,7 @@ require_dependency 'api/validations/validators/limit' module API module Terraform - class State < Grape::API + class State < Grape::API::Instance include ::Gitlab::Utils::StrongMemoize default_format :json diff --git a/lib/api/todos.rb b/lib/api/todos.rb index e36ddf21277..4a73e3e0e94 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Todos < Grape::API + class Todos < Grape::API::Instance include PaginationParams before { authenticate! } diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index 65e9118e3ef..de67a149274 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Triggers < Grape::API + class Triggers < Grape::API::Instance include PaginationParams HTTP_GITLAB_EVENT_HEADER = "HTTP_#{WebHookService::GITLAB_EVENT_HEADER}".underscore.upcase diff --git a/lib/api/user_counts.rb b/lib/api/user_counts.rb index 8df4b381bbf..90127ecbc73 100644 --- a/lib/api/user_counts.rb +++ b/lib/api/user_counts.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class UserCounts < Grape::API + class UserCounts < Grape::API::Instance resource :user_counts do desc 'Return the user specific counts' do detail 'Open MR Count' diff --git a/lib/api/users.rb b/lib/api/users.rb index 3f89f1c253a..3124acd511f 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Users < Grape::API + class Users < Grape::API::Instance include PaginationParams include APIGuard include Helpers::CustomAttributes diff --git a/lib/api/validations/types/comma_separated_to_array.rb b/lib/api/validations/types/comma_separated_to_array.rb index b551878abd1..409eb67a3d3 100644 --- a/lib/api/validations/types/comma_separated_to_array.rb +++ b/lib/api/validations/types/comma_separated_to_array.rb @@ -10,7 +10,7 @@ module API when String value.split(',').map(&:strip) when Array - value.map { |v| v.to_s.split(',').map(&:strip) }.flatten + value.flat_map { |v| v.to_s.split(',').map(&:strip) } else [] end diff --git a/lib/api/validations/types/comma_separated_to_integer_array.rb b/lib/api/validations/types/comma_separated_to_integer_array.rb new file mode 100644 index 00000000000..b8ab08b3fd4 --- /dev/null +++ b/lib/api/validations/types/comma_separated_to_integer_array.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module API + module Validations + module Types + class CommaSeparatedToIntegerArray < CommaSeparatedToArray + def self.coerce + lambda do |value| + super.call(value).map(&:to_i) + end + end + end + end + end +end diff --git a/lib/api/validations/types/labels_list.rb b/lib/api/validations/types/labels_list.rb deleted file mode 100644 index 60277b99106..00000000000 --- a/lib/api/validations/types/labels_list.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module API - module Validations - module Types - class LabelsList - def self.coerce - lambda do |value| - case value - when String - value.split(',').map(&:strip) - when Array - value.flat_map { |v| v.to_s.split(',').map(&:strip) } - when LabelsList - value - else - [] - end - end - end - end - end - end -end diff --git a/lib/api/validations/types/safe_file.rb b/lib/api/validations/types/safe_file.rb deleted file mode 100644 index 53b5790bfa2..00000000000 --- a/lib/api/validations/types/safe_file.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -# This module overrides the Grape type validator defined in -# https://github.com/ruby-grape/grape/blob/master/lib/grape/validations/types/file.rb -module API - module Validations - module Types - class SafeFile < ::Grape::Validations::Types::File - def value_coerced?(value) - super && value[:tempfile].is_a?(Tempfile) - end - end - end - end -end diff --git a/lib/api/validations/types/workhorse_file.rb b/lib/api/validations/types/workhorse_file.rb index 18d111f6556..e65e94fc8db 100644 --- a/lib/api/validations/types/workhorse_file.rb +++ b/lib/api/validations/types/workhorse_file.rb @@ -3,15 +3,14 @@ module API module Validations module Types - class WorkhorseFile < Virtus::Attribute - def coerce(input) - # Processing of multipart file objects - # is already taken care of by Gitlab::Middleware::Multipart. - # Nothing to do here. - input + class WorkhorseFile + def self.parse(value) + raise "#{value.class} is not an UploadedFile type" unless parsed?(value) + + value end - def value_coerced?(value) + def self.parsed?(value) value.is_a?(::UploadedFile) end end diff --git a/lib/api/variables.rb b/lib/api/variables.rb index f3def756fe8..2a051c2adae 100644 --- a/lib/api/variables.rb +++ b/lib/api/variables.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Variables < Grape::API + class Variables < Grape::API::Instance include PaginationParams before { authenticate! } diff --git a/lib/api/version.rb b/lib/api/version.rb index 2d8c90260fa..6a480fc2bd9 100644 --- a/lib/api/version.rb +++ b/lib/api/version.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Version < Grape::API + class Version < Grape::API::Instance helpers ::API::Helpers::GraphqlHelpers include APIGuard diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb index 006dc257fe1..713136e0887 100644 --- a/lib/api/wikis.rb +++ b/lib/api/wikis.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Wikis < Grape::API + class Wikis < Grape::API::Instance helpers ::API::Helpers::WikisHelpers helpers do @@ -112,7 +112,7 @@ module API success Entities::WikiAttachment end params do - requires :file, types: [::API::Validations::Types::SafeFile, ::API::Validations::Types::WorkhorseFile], desc: 'The attachment file to be uploaded' + requires :file, types: [Rack::Multipart::UploadedFile, ::API::Validations::Types::WorkhorseFile], desc: 'The attachment file to be uploaded' optional :branch, type: String, desc: 'The name of the branch' end post ":id/wikis/attachments" do diff --git a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb index a25a5e2335b..ae20d23a15c 100644 --- a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb +++ b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb @@ -51,12 +51,12 @@ module Gitlab partition_column = find_column_definition(table_name, column_name) raise "partition column #{column_name} does not exist on #{table_name}" if partition_column.nil? - new_table_name = partitioned_table_name(table_name) + partitioned_table_name = make_partitioned_table_name(table_name) - create_range_partitioned_copy(new_table_name, table_name, partition_column, primary_key) - create_daterange_partitions(new_table_name, partition_column.name, min_date, max_date) - create_trigger_to_sync_tables(table_name, new_table_name, primary_key) - enqueue_background_migration(table_name, new_table_name, primary_key) + create_range_partitioned_copy(table_name, partitioned_table_name, partition_column, primary_key) + create_daterange_partitions(partitioned_table_name, partition_column.name, min_date, max_date) + create_trigger_to_sync_tables(table_name, partitioned_table_name, primary_key) + enqueue_background_migration(table_name, partitioned_table_name, primary_key) end # Clean up a partitioned copy of an existing table. This deletes the partitioned table and all partitions. @@ -70,15 +70,15 @@ module Gitlab assert_not_in_transaction_block(scope: ERROR_SCOPE) with_lock_retries do - trigger_name = sync_trigger_name(table_name) + trigger_name = make_sync_trigger_name(table_name) drop_trigger(table_name, trigger_name) end - function_name = sync_function_name(table_name) + function_name = make_sync_function_name(table_name) drop_function(function_name) - part_table_name = partitioned_table_name(table_name) - drop_table(part_table_name) + partitioned_table_name = make_partitioned_table_name(table_name) + drop_table(partitioned_table_name) end def create_hash_partitions(table_name, number_of_partitions) @@ -107,15 +107,15 @@ module Gitlab "for more information please contact the database team" end - def partitioned_table_name(table) + def make_partitioned_table_name(table) tmp_table_name("#{table}_part") end - def sync_function_name(table) + def make_sync_function_name(table) object_name(table, 'table_sync_function') end - def sync_trigger_name(table) + def make_sync_trigger_name(table) object_name(table, 'table_sync_trigger') end @@ -123,11 +123,11 @@ module Gitlab connection.columns(table).find { |c| c.name == column.to_s } end - def create_range_partitioned_copy(table_name, template_table_name, partition_column, primary_key) - if table_exists?(table_name) + def create_range_partitioned_copy(source_table_name, partitioned_table_name, partition_column, primary_key) + if table_exists?(partitioned_table_name) # rubocop:disable Gitlab/RailsLogger Rails.logger.warn "Partitioned table not created because it already exists" \ - " (this may be due to an aborted migration or similar): table_name: #{table_name} " + " (this may be due to an aborted migration or similar): table_name: #{partitioned_table_name} " # rubocop:enable Gitlab/RailsLogger return end @@ -135,20 +135,20 @@ module Gitlab tmp_column_name = object_name(partition_column.name, 'partition_key') transaction do execute(<<~SQL) - CREATE TABLE #{table_name} ( - LIKE #{template_table_name} INCLUDING ALL EXCLUDING INDEXES, + CREATE TABLE #{partitioned_table_name} ( + LIKE #{source_table_name} INCLUDING ALL EXCLUDING INDEXES, #{tmp_column_name} #{partition_column.sql_type} NOT NULL, PRIMARY KEY (#{[primary_key, tmp_column_name].join(", ")}) ) PARTITION BY RANGE (#{tmp_column_name}) SQL - remove_column(table_name, partition_column.name) - rename_column(table_name, tmp_column_name, partition_column.name) - change_column_default(table_name, primary_key, nil) + remove_column(partitioned_table_name, partition_column.name) + rename_column(partitioned_table_name, tmp_column_name, partition_column.name) + change_column_default(partitioned_table_name, primary_key, nil) - if column_of_type?(table_name, primary_key, :integer) + if column_of_type?(partitioned_table_name, primary_key, :integer) # Default to int8 primary keys to prevent overflow - change_column(table_name, primary_key, :bigint) + change_column(partitioned_table_name, primary_key, :bigint) end end end @@ -191,19 +191,19 @@ module Gitlab create_range_partition(partition_name, table_name, lower_bound, upper_bound) end - def create_trigger_to_sync_tables(source_table, target_table, unique_key) - function_name = sync_function_name(source_table) - trigger_name = sync_trigger_name(source_table) + def create_trigger_to_sync_tables(source_table_name, partitioned_table_name, unique_key) + function_name = make_sync_function_name(source_table_name) + trigger_name = make_sync_trigger_name(source_table_name) with_lock_retries do - create_sync_function(function_name, target_table, unique_key) - create_comment('FUNCTION', function_name, "Partitioning migration: table sync for #{source_table} table") + create_sync_function(function_name, partitioned_table_name, unique_key) + create_comment('FUNCTION', function_name, "Partitioning migration: table sync for #{source_table_name} table") - create_sync_trigger(source_table, trigger_name, function_name) + create_sync_trigger(source_table_name, trigger_name, function_name) end end - def create_sync_function(name, target_table, unique_key) + def create_sync_function(name, partitioned_table_name, unique_key) if function_exists?(name) # rubocop:disable Gitlab/RailsLogger Rails.logger.warn "Partitioning sync function not created because it already exists" \ @@ -213,20 +213,20 @@ module Gitlab end delimiter = ",\n " - column_names = connection.columns(target_table).map(&:name) + column_names = connection.columns(partitioned_table_name).map(&:name) set_statements = build_set_statements(column_names, unique_key) insert_values = column_names.map { |name| "NEW.#{name}" } create_trigger_function(name, replace: false) do <<~SQL IF (TG_OP = 'DELETE') THEN - DELETE FROM #{target_table} where #{unique_key} = OLD.#{unique_key}; + DELETE FROM #{partitioned_table_name} where #{unique_key} = OLD.#{unique_key}; ELSIF (TG_OP = 'UPDATE') THEN - UPDATE #{target_table} + UPDATE #{partitioned_table_name} SET #{set_statements.join(delimiter)} - WHERE #{target_table}.#{unique_key} = NEW.#{unique_key}; + WHERE #{partitioned_table_name}.#{unique_key} = NEW.#{unique_key}; ELSIF (TG_OP = 'INSERT') THEN - INSERT INTO #{target_table} (#{column_names.join(delimiter)}) + INSERT INTO #{partitioned_table_name} (#{column_names.join(delimiter)}) VALUES (#{insert_values.join(delimiter)}); END IF; RETURN NULL; @@ -250,15 +250,15 @@ module Gitlab create_trigger(table_name, trigger_name, function_name, fires: 'AFTER INSERT OR UPDATE OR DELETE') end - def enqueue_background_migration(source_table, partitioned_table, source_key) - model_class = define_batchable_model(source_table) + def enqueue_background_migration(source_table_name, partitioned_table_name, source_key) + source_model = define_batchable_model(source_table_name) queue_background_migration_jobs_by_range_at_intervals( - model_class, + source_model, MIGRATION_CLASS_NAME, BATCH_INTERVAL, batch_size: BATCH_SIZE, - other_job_arguments: [source_table.to_s, partitioned_table, source_key]) + other_job_arguments: [source_table_name.to_s, partitioned_table_name, source_key]) end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d9e4037ff16..e0f55c3f51f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -19141,6 +19141,9 @@ msgstr "" msgid "Reports|Failure" msgstr "" +msgid "Reports|Identifier" +msgstr "" + msgid "Reports|Metrics reports are loading" msgstr "" diff --git a/rubocop/cop/api/grape_api_instance.rb b/rubocop/cop/api/grape_api_instance.rb new file mode 100644 index 00000000000..de11b9ef3f6 --- /dev/null +++ b/rubocop/cop/api/grape_api_instance.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module API + class GrapeAPIInstance < RuboCop::Cop::Cop + # This cop checks that APIs subclass Grape::API::Instance with Grape v1.3+. + # + # @example + # + # # bad + # module API + # class Projects < Grape::API + # end + # end + # + # # good + # module API + # class Projects < Grape::API::Instance + # end + # end + MSG = 'Inherit from Grape::API::Instance instead of Grape::API. ' \ + 'For more details check the https://gitlab.com/gitlab-org/gitlab/-/issues/215230.' + + def_node_matcher :grape_api_definition, <<~PATTERN + (class + (const _ _) + (const + (const nil? :Grape) :API) + ... + ) + PATTERN + + def on_class(node) + grape_api_definition(node) do + add_offense(node.children[1]) + end + end + end + end + end +end diff --git a/rubocop/cop/api/grape_array_missing_coerce.rb b/rubocop/cop/api/grape_array_missing_coerce.rb new file mode 100644 index 00000000000..3d7a6a72d81 --- /dev/null +++ b/rubocop/cop/api/grape_array_missing_coerce.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module API + class GrapeArrayMissingCoerce < RuboCop::Cop::Cop + # This cop checks that Grape API parameters using an Array type + # implement a coerce_with method: + # + # https://github.com/ruby-grape/grape/blob/master/UPGRADING.md#ensure-that-array-types-have-explicit-coercions + # + # @example + # + # # bad + # requires :values, type: Array[String] + # + # # good + # requires :values, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce + # + # end + MSG = 'This Grape parameter defines an Array but is missing a coerce_with definition. ' \ + 'For more details, see https://github.com/ruby-grape/grape/blob/master/UPGRADING.md#ensure-that-array-types-have-explicit-coercions' + + def_node_matcher :grape_api_instance?, <<~PATTERN + (class + (const _ _) + (const + (const + (const nil? :Grape) :API) :Instance) + ... + ) + PATTERN + + def_node_matcher :grape_api_param_block?, <<~PATTERN + (send _ {:requires :optional} + (sym _) + $_) + PATTERN + + def_node_matcher :grape_type_def?, <<~PATTERN + (sym :type) + PATTERN + + def_node_matcher :grape_array_type?, <<~PATTERN + (send + (const nil? :Array) :[] + (const nil? _)) + PATTERN + + def_node_matcher :grape_coerce_with?, <<~PATTERN + (sym :coerce_with) + PATTERN + + def on_class(node) + @grape_api ||= grape_api_instance?(node) + end + + def on_send(node) + return unless @grape_api + + match = grape_api_param_block?(node) + + return unless match.is_a?(RuboCop::AST::HashNode) + + is_array_type = false + has_coerce_method = false + + match.each_pair do |first, second| + has_coerce_method ||= grape_coerce_with?(first) + + if grape_type_def?(first) && grape_array_type?(second) + is_array_type = true + end + end + + if is_array_type && !has_coerce_method + add_offense(node) + end + end + end + end + end +end diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb index a6a4aff7ce9..d10351feb9e 100644 --- a/spec/controllers/projects/refs_controller_spec.rb +++ b/spec/controllers/projects/refs_controller_spec.rb @@ -41,19 +41,12 @@ RSpec.describe Projects::RefsController do expect { xhr_get }.not_to raise_error end - it 'renders 404 for non-JS requests' do + it 'renders 404 for HTML requests' do xhr_get expect(response).to be_not_found end - it 'renders JS' do - expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original - - xhr_get(:js) - expect(response).to be_successful - end - context 'when json is requested' do it 'renders JSON' do expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original diff --git a/spec/lib/api/helpers/common_helpers_spec.rb b/spec/lib/api/helpers/common_helpers_spec.rb new file mode 100644 index 00000000000..5162d2f1000 --- /dev/null +++ b/spec/lib/api/helpers/common_helpers_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Helpers::CommonHelpers do + include Rack::Test::Methods + + subject do + Class.new(Grape::API) do + helpers API::Helpers::CommonHelpers + + before do + coerce_nil_params_to_array! + end + + params do + requires :id, type: String + optional :array, type: Array, coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce + optional :array_of_strings, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce + optional :array_of_ints, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce + end + get ":id" do + params.to_json + end + end + end + + def app + subject + end + + describe '.coerce_nil_params_to_array!' do + let(:json_response) { Gitlab::Json.parse(last_response.body) } + + it 'converts all nil parameters to empty arrays' do + get '/test?array=&array_of_strings=&array_of_ints=' + + expect(json_response['array']).to eq([]) + expect(json_response['array_of_strings']).to eq([]) + expect(json_response['array_of_ints']).to eq([]) + end + + it 'leaves non-nil parameters alone' do + get '/test?array=&array_of_strings=test,me&array_of_ints=1,2' + + expect(json_response['array']).to eq([]) + expect(json_response['array_of_strings']).to eq(%w(test me)) + expect(json_response['array_of_ints']).to eq([1, 2]) + end + end +end diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb index a257ca19385..2b3d7e76ae6 100644 --- a/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end let_it_be(:connection) { ActiveRecord::Base.connection } - let(:template_table) { :audit_events } + let(:source_table) { :audit_events } let(:partitioned_table) { '_test_migration_partitioned_table' } let(:function_name) { '_test_migration_function_name' } let(:trigger_name) { '_test_migration_trigger_name' } @@ -22,9 +22,9 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe before do allow(migration).to receive(:puts) allow(migration).to receive(:transaction_open?).and_return(false) - allow(migration).to receive(:partitioned_table_name).and_return(partitioned_table) - allow(migration).to receive(:sync_function_name).and_return(function_name) - allow(migration).to receive(:sync_trigger_name).and_return(trigger_name) + allow(migration).to receive(:make_partitioned_table_name).and_return(partitioned_table) + allow(migration).to receive(:make_sync_function_name).and_return(function_name) + allow(migration).to receive(:make_sync_trigger_name).and_return(trigger_name) allow(migration).to receive(:assert_table_is_allowed) end @@ -38,14 +38,14 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end context 'when the table is not allowed' do - let(:template_table) { :this_table_is_not_allowed } + let(:source_table) { :this_table_is_not_allowed } it 'raises an error' do - expect(migration).to receive(:assert_table_is_allowed).with(template_table).and_call_original + expect(migration).to receive(:assert_table_is_allowed).with(source_table).and_call_original expect do - migration.partition_table_by_date template_table, partition_column, min_date: min_date, max_date: max_date - end.to raise_error(/#{template_table} is not allowed for use/) + migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date + end.to raise_error(/#{source_table} is not allowed for use/) end end @@ -54,7 +54,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe expect(migration).to receive(:transaction_open?).and_return(true) expect do - migration.partition_table_by_date template_table, partition_column, min_date: min_date, max_date: max_date + migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date end.to raise_error(/can not be run inside a transaction/) end end @@ -64,7 +64,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe it 'raises an error' do expect do - migration.partition_table_by_date template_table, partition_column, min_date: min_date, max_date: max_date + migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date end.to raise_error(/max_date #{max_date} must be greater than min_date #{min_date}/) end end @@ -74,24 +74,24 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe it 'raises an error' do expect do - migration.partition_table_by_date template_table, partition_column, min_date: min_date, max_date: max_date + migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date end.to raise_error(/max_date #{max_date} must be greater than min_date #{min_date}/) end end context 'when the given table does not have a primary key' do - let(:template_table) { :_partitioning_migration_helper_test_table } + let(:source_table) { :_partitioning_migration_helper_test_table } let(:partition_column) { :some_field } it 'raises an error' do - migration.create_table template_table, id: false do |t| + migration.create_table source_table, id: false do |t| t.integer :id t.datetime partition_column end expect do - migration.partition_table_by_date template_table, partition_column, min_date: min_date, max_date: max_date - end.to raise_error(/primary key not defined for #{template_table}/) + migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date + end.to raise_error(/primary key not defined for #{source_table}/) end end @@ -100,14 +100,14 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe it 'raises an error' do expect do - migration.partition_table_by_date template_table, partition_column, min_date: min_date, max_date: max_date + migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date end.to raise_error(/partition column #{partition_column} does not exist/) end end describe 'constructing the partitioned table' do it 'creates a table partitioned by the proper column' do - migration.partition_table_by_date template_table, partition_column, min_date: min_date, max_date: max_date + migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date expect(connection.table_exists?(partitioned_table)).to be(true) expect(connection.primary_key(partitioned_table)).to eq(new_primary_key) @@ -116,7 +116,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end it 'changes the primary key datatype to bigint' do - migration.partition_table_by_date template_table, partition_column, min_date: min_date, max_date: max_date + migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key } @@ -131,13 +131,13 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end end - let(:template_table) { :another_example } + let(:source_table) { :another_example } let(:old_primary_key) { 'identifier' } it 'does not change the primary key datatype' do - migration.partition_table_by_date template_table, partition_column, min_date: min_date, max_date: max_date + migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date - original_pk_column = connection.columns(template_table).find { |c| c.name == old_primary_key } + original_pk_column = connection.columns(source_table).find { |c| c.name == old_primary_key } pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key } expect(pk_column).not_to be_nil @@ -146,7 +146,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end it 'removes the default from the primary key column' do - migration.partition_table_by_date template_table, partition_column, min_date: min_date, max_date: max_date + migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date pk_column = connection.columns(partitioned_table).find { |c| c.name == old_primary_key } @@ -154,16 +154,16 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end it 'creates the partitioned table with the same non-key columns' do - migration.partition_table_by_date template_table, partition_column, min_date: min_date, max_date: max_date + migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date copied_columns = filter_columns_by_name(connection.columns(partitioned_table), new_primary_key) - original_columns = filter_columns_by_name(connection.columns(template_table), new_primary_key) + original_columns = filter_columns_by_name(connection.columns(source_table), new_primary_key) expect(copied_columns).to match_array(original_columns) end it 'creates a partition spanning over each month in the range given' do - migration.partition_table_by_date template_table, partition_column, min_date: min_date, max_date: max_date + migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date expect_range_partitions_for(partitioned_table, { '000000' => ['MINVALUE', "'2019-12-01 00:00:00'"], @@ -175,7 +175,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end context 'when min_date is not given' do - let(:template_table) { :todos } + let(:source_table) { :todos } context 'with records present already' do before do @@ -183,7 +183,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end it 'creates a partition spanning over each month from the first record' do - migration.partition_table_by_date template_table, partition_column, max_date: max_date + migration.partition_table_by_date source_table, partition_column, max_date: max_date expect_range_partitions_for(partitioned_table, { '000000' => ['MINVALUE', "'2019-11-01 00:00:00'"], @@ -198,7 +198,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe context 'without data' do it 'creates the catchall partition plus two actual partition' do - migration.partition_table_by_date template_table, partition_column, max_date: max_date + migration.partition_table_by_date source_table, partition_column, max_date: max_date expect_range_partitions_for(partitioned_table, { '000000' => ['MINVALUE', "'2020-02-01 00:00:00'"], @@ -214,7 +214,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe today = Date.new(2020, 5, 8) Timecop.freeze(today) do - migration.partition_table_by_date template_table, partition_column, min_date: min_date + migration.partition_table_by_date source_table, partition_column, min_date: min_date expect_range_partitions_for(partitioned_table, { '000000' => ['MINVALUE', "'2019-12-01 00:00:00'"], @@ -234,7 +234,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe it 'creates partitions for the current and next month' do current_date = Date.new(2020, 05, 22) Timecop.freeze(current_date.to_time) do - migration.partition_table_by_date template_table, partition_column + migration.partition_table_by_date source_table, partition_column expect_range_partitions_for(partitioned_table, { '000000' => ['MINVALUE', "'2020-05-01 00:00:00'"], @@ -247,7 +247,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end describe 'keeping data in sync with the partitioned table' do - let(:template_table) { :todos } + let(:source_table) { :todos } let(:model) { Class.new(ActiveRecord::Base) } let(:timestamp) { Time.utc(2019, 12, 1, 12).round } @@ -258,16 +258,16 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe it 'creates a trigger function on the original table' do expect_function_not_to_exist(function_name) - expect_trigger_not_to_exist(template_table, trigger_name) + expect_trigger_not_to_exist(source_table, trigger_name) - migration.partition_table_by_date template_table, partition_column, min_date: min_date, max_date: max_date + migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date expect_function_to_exist(function_name) - expect_valid_function_trigger(template_table, trigger_name, function_name, after: %w[delete insert update]) + expect_valid_function_trigger(source_table, trigger_name, function_name, after: %w[delete insert update]) end it 'syncs inserts to the partitioned tables' do - migration.partition_table_by_date template_table, partition_column, min_date: min_date, max_date: max_date + migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date expect(model.count).to eq(0) @@ -280,7 +280,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end it 'syncs updates to the partitioned tables' do - migration.partition_table_by_date template_table, partition_column, min_date: min_date, max_date: max_date + migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date first_todo = create(:todo, :pending, commit_id: nil, created_at: timestamp, updated_at: timestamp) second_todo = create(:todo, created_at: timestamp, updated_at: timestamp) @@ -301,7 +301,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end it 'syncs deletes to the partitioned tables' do - migration.partition_table_by_date template_table, partition_column, min_date: min_date, max_date: max_date + migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date first_todo = create(:todo, created_at: timestamp, updated_at: timestamp) second_todo = create(:todo, created_at: timestamp, updated_at: timestamp) @@ -317,7 +317,7 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end describe 'copying historic data to the partitioned table' do - let(:template_table) { 'todos' } + let(:source_table) { 'todos' } let(:migration_class) { '::Gitlab::Database::PartitioningMigrationHelpers::BackfillPartitionedTable' } let(:sub_batch_size) { described_class::SUB_BATCH_SIZE } let(:pause_seconds) { described_class::PAUSE_SECONDS } @@ -333,14 +333,14 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe it 'enqueues jobs to copy each batch of data' do Sidekiq::Testing.fake! do - migration.partition_table_by_date template_table, partition_column, min_date: min_date, max_date: max_date + migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date expect(BackgroundMigrationWorker.jobs.size).to eq(2) - first_job_arguments = [first_id, second_id, template_table, partitioned_table, 'id'] + first_job_arguments = [first_id, second_id, source_table, partitioned_table, 'id'] expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([migration_class, first_job_arguments]) - second_job_arguments = [third_id, third_id, template_table, partitioned_table, 'id'] + second_job_arguments = [third_id, third_id, source_table, partitioned_table, 'id'] expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([migration_class, second_job_arguments]) end end @@ -353,37 +353,37 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::TableManagementHe end context 'when the table is not allowed' do - let(:template_table) { :this_table_is_not_allowed } + let(:source_table) { :this_table_is_not_allowed } it 'raises an error' do - expect(migration).to receive(:assert_table_is_allowed).with(template_table).and_call_original + expect(migration).to receive(:assert_table_is_allowed).with(source_table).and_call_original expect do - migration.drop_partitioned_table_for template_table - end.to raise_error(/#{template_table} is not allowed for use/) + migration.drop_partitioned_table_for source_table + end.to raise_error(/#{source_table} is not allowed for use/) end end it 'drops the trigger syncing to the partitioned table' do - migration.partition_table_by_date template_table, partition_column, min_date: min_date, max_date: max_date + migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date expect_function_to_exist(function_name) - expect_valid_function_trigger(template_table, trigger_name, function_name, after: %w[delete insert update]) + expect_valid_function_trigger(source_table, trigger_name, function_name, after: %w[delete insert update]) - migration.drop_partitioned_table_for template_table + migration.drop_partitioned_table_for source_table expect_function_not_to_exist(function_name) - expect_trigger_not_to_exist(template_table, trigger_name) + expect_trigger_not_to_exist(source_table, trigger_name) end it 'drops the partitioned copy and all partitions' do - migration.partition_table_by_date template_table, partition_column, min_date: min_date, max_date: max_date + migration.partition_table_by_date source_table, partition_column, min_date: min_date, max_date: max_date expected_tables.each do |table| expect(connection.table_exists?(table)).to be(true) end - migration.drop_partitioned_table_for template_table + migration.drop_partitioned_table_for source_table expected_tables.each do |table| expect(connection.table_exists?(table)).to be(false) diff --git a/spec/requests/api/applications_spec.rb b/spec/requests/api/applications_spec.rb index 5b2d835f89a..63fbf6e32dd 100644 --- a/spec/requests/api/applications_spec.rb +++ b/spec/requests/api/applications_spec.rb @@ -74,14 +74,15 @@ RSpec.describe API::Applications, :api do expect(json_response['error']).to eq('scopes is missing') end - it 'does not allow creating an application with confidential set to nil' do + it 'defaults to creating an application with confidential' do expect do post api('/applications', admin_user), params: { name: 'application_name', redirect_uri: 'http://application.url', scopes: '', confidential: nil } - end.not_to change { Doorkeeper::Application.count } + end.to change { Doorkeeper::Application.count }.by(1) - expect(response).to have_gitlab_http_status(:bad_request) + expect(response).to have_gitlab_http_status(:created) expect(json_response).to be_a Hash - expect(json_response['message']['confidential'].first).to eq('is not included in the list') + expect(json_response['callback_url']).to eq('http://application.url') + expect(json_response['confidential']).to be true end end diff --git a/spec/requests/api/graphql/mutations/jira_import/start_spec.rb b/spec/requests/api/graphql/mutations/jira_import/start_spec.rb index 050073f8b98..e7124512ef1 100644 --- a/spec/requests/api/graphql/mutations/jira_import/start_spec.rb +++ b/spec/requests/api/graphql/mutations/jira_import/start_spec.rb @@ -14,7 +14,8 @@ RSpec.describe 'Starting a Jira Import' do let(:mutation) do variables = { jira_project_key: jira_project_key, - project_path: project_path + project_path: project_path, + users_mapping: [{ jiraAccountId: 'abc', gitlabId: 5 }] } graphql_mutation(:jira_import_start, variables) diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 1df4d7ea9f6..602aacb6ced 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -62,14 +62,14 @@ RSpec.describe API::Settings, 'Settings' do default_projects_limit: 3, default_project_creation: 2, password_authentication_enabled_for_web: false, - repository_storages: ['custom'], + repository_storages: 'custom', plantuml_enabled: true, plantuml_url: 'http://plantuml.example.com', sourcegraph_enabled: true, sourcegraph_url: 'https://sourcegraph.com', sourcegraph_public_only: false, default_snippet_visibility: 'internal', - restricted_visibility_levels: ['public'], + restricted_visibility_levels: 'public', default_artifacts_expire_in: '2 days', help_page_text: 'custom help text', help_page_hide_commercial_content: true, @@ -94,7 +94,9 @@ RSpec.describe API::Settings, 'Settings' do issues_create_limit: 300, raw_blob_request_limit: 300, spam_check_endpoint_enabled: true, - spam_check_endpoint_url: 'https://example.com/spam_check' + spam_check_endpoint_url: 'https://example.com/spam_check', + disabled_oauth_sign_in_sources: 'unknown', + import_sources: 'github,bitbucket' } expect(response).to have_gitlab_http_status(:ok) @@ -135,6 +137,8 @@ RSpec.describe API::Settings, 'Settings' do expect(json_response['raw_blob_request_limit']).to eq(300) expect(json_response['spam_check_endpoint_enabled']).to be_truthy expect(json_response['spam_check_endpoint_url']).to eq('https://example.com/spam_check') + expect(json_response['disabled_oauth_sign_in_sources']).to eq([]) + expect(json_response['import_sources']).to match_array(%w(github bitbucket)) end end diff --git a/spec/rubocop/cop/api/grape_api_instance_spec.rb b/spec/rubocop/cop/api/grape_api_instance_spec.rb new file mode 100644 index 00000000000..74f175cb707 --- /dev/null +++ b/spec/rubocop/cop/api/grape_api_instance_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'rubocop' +require_relative '../../../../rubocop/cop/api/grape_api_instance' + +RSpec.describe RuboCop::Cop::API::GrapeAPIInstance do + include CopHelper + + subject(:cop) { described_class.new } + + it 'adds an offense when inheriting from Grape::API' do + inspect_source(<<~CODE) + class SomeAPI < Grape::API + end + CODE + + expect(cop.offenses.size).to eq(1) + end + + it 'does not add an offense when inheriting from Grape::API::Instance' do + inspect_source(<<~CODE) + class SomeAPI < Grape::API::Instance + end + CODE + + expect(cop.offenses.size).to be_zero + end +end diff --git a/spec/rubocop/cop/api/grape_array_missing_coerce_spec.rb b/spec/rubocop/cop/api/grape_array_missing_coerce_spec.rb new file mode 100644 index 00000000000..c7bb8255398 --- /dev/null +++ b/spec/rubocop/cop/api/grape_array_missing_coerce_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'rubocop' +require_relative '../../../../rubocop/cop/api/grape_array_missing_coerce' + +RSpec.describe RuboCop::Cop::API::GrapeArrayMissingCoerce do + include CopHelper + + subject(:cop) { described_class.new } + + it 'adds an offense with a required parameter' do + inspect_source(<<~CODE) + class SomeAPI < Grape::API::Instance + params do + requires :values, type: Array[String] + end + end + CODE + + expect(cop.offenses.size).to eq(1) + end + + it 'adds an offense with an optional parameter' do + inspect_source(<<~CODE) + class SomeAPI < Grape::API::Instance + params do + optional :values, type: Array[String] + end + end + CODE + + expect(cop.offenses.size).to eq(1) + end + + it 'does not add an offense' do + inspect_source(<<~CODE) + class SomeAPI < Grape::API::Instance + params do + requires :values, type: Array[String], coerce_with: ->(val) { val.split(',').map(&:strip) } + requires :milestone, type: String, desc: 'Milestone title' + optional :assignee_id, types: [Integer, String], integer_none_any: true, + desc: 'Return issues which are assigned to the user with the given ID' + end + end + CODE + + expect(cop.offenses.size).to be_zero + end + + it 'does not add an offense for unrelated classes' do + inspect_source(<<~CODE) + class SomeClass + params do + requires :values, type: Array[String] + end + end + CODE + + expect(cop.offenses.size).to be_zero + end +end diff --git a/spec/rubocop/cop/code_reuse/worker_spec.rb b/spec/rubocop/cop/code_reuse/worker_spec.rb index bd1246ceb07..1f502e554c4 100644 --- a/spec/rubocop/cop/code_reuse/worker_spec.rb +++ b/spec/rubocop/cop/code_reuse/worker_spec.rb @@ -31,7 +31,7 @@ RSpec.describe RuboCop::Cop::CodeReuse::Worker, type: :rubocop do .and_return(true) expect_offense(<<~SOURCE) - class Foo < Grape::API + class Foo < Grape::API::Instance resource :projects do get '/' do FooWorker.perform_async diff --git a/spec/services/jira_import/start_import_service_spec.rb b/spec/services/jira_import/start_import_service_spec.rb index 0437dd19e34..a10928355ef 100644 --- a/spec/services/jira_import/start_import_service_spec.rb +++ b/spec/services/jira_import/start_import_service_spec.rb @@ -8,8 +8,15 @@ RSpec.describe JiraImport::StartImportService do let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) { create(:project) } let(:key) { 'KEY' } + let(:mapping) do + [ + { jira_account_id: 'abc', gitlab_id: 12 }, + { jira_account_id: 'def', gitlab_id: nil }, + { jira_account_id: nil, gitlab_id: 1 } + ] + end - subject { described_class.new(user, project, key).execute } + subject { described_class.new(user, project, key, mapping).execute } context 'when an error is returned from the project validation' do before do @@ -37,7 +44,7 @@ RSpec.describe JiraImport::StartImportService do context 'when correct data provided' do let(:fake_key) { 'some-key' } - subject { described_class.new(user, project, fake_key).execute } + subject { described_class.new(user, project, fake_key, mapping).execute } context 'when import is already running' do let_it_be(:jira_import_state) { create(:jira_import_state, :started, project: project) } @@ -62,35 +69,68 @@ RSpec.describe JiraImport::StartImportService do end context 'when everything is ok' do - it 'returns success response' do - expect(subject).to be_a(ServiceResponse) - expect(subject).to be_success + context 'with complete mapping' do + before do + expect(Gitlab::JiraImport).to receive(:cache_users_mapping).with(project.id, { 'abc' => 12 }) + end + + it 'returns success response' do + expect(subject).to be_a(ServiceResponse) + expect(subject).to be_success + end + + it 'schedules Jira import' do + subject + + expect(project.latest_jira_import).to be_scheduled + end + + it 'creates Jira import data', :aggregate_failures do + jira_import = subject.payload[:import_data] + + expect(jira_import.jira_project_xid).to eq(0) + expect(jira_import.jira_project_name).to eq(fake_key) + expect(jira_import.jira_project_key).to eq(fake_key) + expect(jira_import.user).to eq(user) + end + + it 'creates Jira import label' do + expect { subject }.to change { Label.count }.by(1) + end + + it 'creates Jira label title with correct number' do + jira_import = subject.payload[:import_data] + label_title = "jira-import::#{jira_import.jira_project_key}-1" + + expect(jira_import.label.title).to eq(label_title) + end end - it 'schedules Jira import' do - subject + context 'when mapping is nil' do + let(:mapping) { nil } - expect(project.latest_jira_import).to be_scheduled + it 'returns success response' do + expect(Gitlab::JiraImport).not_to receive(:cache_users_mapping) + + expect(subject).to be_a(ServiceResponse) + expect(subject).to be_success + end end - it 'creates Jira import data', :aggregate_failures do - jira_import = subject.payload[:import_data] + context 'when no mapping value is complete' do + let(:mapping) do + [ + { jira_account_id: 'def', gitlab_id: nil }, + { jira_account_id: nil, gitlab_id: 1 } + ] + end - expect(jira_import.jira_project_xid).to eq(0) - expect(jira_import.jira_project_name).to eq(fake_key) - expect(jira_import.jira_project_key).to eq(fake_key) - expect(jira_import.user).to eq(user) - end + it 'returns success response' do + expect(Gitlab::JiraImport).not_to receive(:cache_users_mapping) - it 'creates Jira import label' do - expect { subject }.to change { Label.count }.by(1) - end - - it 'creates Jira label title with correct number' do - jira_import = subject.payload[:import_data] - label_title = "jira-import::#{jira_import.jira_project_key}-1" - - expect(jira_import.label.title).to eq(label_title) + expect(subject).to be_a(ServiceResponse) + expect(subject).to be_success + end end end