Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-04-27 21:09:41 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-04-27 21:09:41 +0300
commitf569792df8a25caa1bed9c448c8c4c3f837f5164 (patch)
tree8c2ed7dae5ba132a97c0321a7649174e5832d637
parentc2908ec6a0d7b62996cdb8da0350705bdad691bf (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop.yml19
-rw-r--r--Gemfile4
-rw-r--r--Gemfile.lock55
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_list.vue59
-rw-r--r--app/assets/javascripts/alert_management/list.js7
-rw-r--r--app/assets/javascripts/boards/models/issue.js26
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js28
-rw-r--r--app/assets/javascripts/users_select.js (renamed from app/assets/javascripts/users_select/index.js)29
-rw-r--r--app/assets/javascripts/users_select/constants.js18
-rw-r--r--app/assets/javascripts/users_select/utils.js27
-rw-r--r--app/controllers/application_controller.rb2
-rw-r--r--app/graphql/types/jira_import_type.rb5
-rw-r--r--app/helpers/explore_helper.rb2
-rw-r--r--app/helpers/projects/alert_management_helper.rb6
-rw-r--r--app/models/jira_import_state.rb2
-rw-r--r--app/views/dashboard/snippets/index.html.haml2
-rw-r--r--app/views/projects/alert_management/index.html.haml2
-rw-r--r--changelogs/unreleased/213808-add-scheduled-at-field-to-jira-imports.yml5
-rw-r--r--changelogs/unreleased/Remove-update-function-logic-from-issue-js.yml5
-rw-r--r--changelogs/unreleased/sh-clean-up-public-visiblity-level-check.yml5
-rw-r--r--changelogs/unreleased/sh-update-grape-gem.yml5
-rw-r--r--changelogs/unreleased/user-can-enable-alert-management.yml5
-rw-r--r--db/migrate/20200423101529_add_scheduled_at_to_jira_imports.rb9
-rw-r--r--db/structure.sql4
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md1
-rw-r--r--doc/api/README.md4
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql58
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json175
-rw-r--r--doc/api/graphql/reference/index.md13
-rw-r--r--doc/development/api_styleguide.md8
-rw-r--r--doc/development/ee_features.md12
-rw-r--r--doc/development/geo/framework.md4
-rw-r--r--doc/development/logging.md2
-rw-r--r--doc/user/application_security/dependency_scanning/index.md3
-rw-r--r--doc/user/project/issues/design_management.md1
-rw-r--r--lib/api/access_requests.rb2
-rw-r--r--lib/api/admin/sidekiq.rb2
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/api_guard.rb11
-rw-r--r--lib/api/appearance.rb2
-rw-r--r--lib/api/applications.rb2
-rw-r--r--lib/api/avatar.rb2
-rw-r--r--lib/api/award_emoji.rb2
-rw-r--r--lib/api/badges.rb2
-rw-r--r--lib/api/boards.rb2
-rw-r--r--lib/api/branches.rb2
-rw-r--r--lib/api/broadcast_messages.rb2
-rw-r--r--lib/api/commit_statuses.rb2
-rw-r--r--lib/api/commits.rb2
-rw-r--r--lib/api/container_registry_event.rb2
-rw-r--r--lib/api/deploy_keys.rb2
-rw-r--r--lib/api/deploy_tokens.rb6
-rw-r--r--lib/api/deployments.rb2
-rw-r--r--lib/api/discussions.rb2
-rw-r--r--lib/api/environments.rb2
-rw-r--r--lib/api/error_tracking.rb2
-rw-r--r--lib/api/events.rb2
-rw-r--r--lib/api/features.rb2
-rw-r--r--lib/api/files.rb2
-rw-r--r--lib/api/group_boards.rb2
-rw-r--r--lib/api/group_clusters.rb2
-rw-r--r--lib/api/group_container_repositories.rb2
-rw-r--r--lib/api/group_export.rb2
-rw-r--r--lib/api/group_import.rb2
-rw-r--r--lib/api/group_labels.rb2
-rw-r--r--lib/api/group_milestones.rb2
-rw-r--r--lib/api/group_variables.rb2
-rw-r--r--lib/api/groups.rb4
-rw-r--r--lib/api/helpers/merge_requests_helpers.rb2
-rw-r--r--lib/api/helpers/projects_helpers.rb2
-rw-r--r--lib/api/import_github.rb2
-rw-r--r--lib/api/internal/base.rb2
-rw-r--r--lib/api/internal/pages.rb2
-rw-r--r--lib/api/issues.rb10
-rw-r--r--lib/api/job_artifacts.rb2
-rw-r--r--lib/api/jobs.rb2
-rw-r--r--lib/api/keys.rb2
-rw-r--r--lib/api/labels.rb2
-rw-r--r--lib/api/lint.rb2
-rw-r--r--lib/api/lsif_data.rb2
-rw-r--r--lib/api/markdown.rb2
-rw-r--r--lib/api/members.rb6
-rw-r--r--lib/api/merge_request_diffs.rb2
-rw-r--r--lib/api/merge_requests.rb8
-rw-r--r--lib/api/metrics/dashboard/annotations.rb2
-rw-r--r--lib/api/milestone_responses.rb2
-rw-r--r--lib/api/namespaces.rb2
-rw-r--r--lib/api/notes.rb2
-rw-r--r--lib/api/notification_settings.rb2
-rw-r--r--lib/api/pages.rb2
-rw-r--r--lib/api/pages_domains.rb2
-rw-r--r--lib/api/pagination_params.rb2
-rw-r--r--lib/api/pipeline_schedules.rb2
-rw-r--r--lib/api/pipelines.rb2
-rw-r--r--lib/api/project_clusters.rb2
-rw-r--r--lib/api/project_container_repositories.rb2
-rw-r--r--lib/api/project_events.rb2
-rw-r--r--lib/api/project_export.rb2
-rw-r--r--lib/api/project_hooks.rb2
-rw-r--r--lib/api/project_import.rb2
-rw-r--r--lib/api/project_milestones.rb2
-rw-r--r--lib/api/project_snapshots.rb2
-rw-r--r--lib/api/project_snippets.rb2
-rw-r--r--lib/api/project_statistics.rb2
-rw-r--r--lib/api/project_templates.rb2
-rw-r--r--lib/api/projects.rb4
-rw-r--r--lib/api/protected_branches.rb2
-rw-r--r--lib/api/protected_tags.rb2
-rw-r--r--lib/api/release/links.rb2
-rw-r--r--lib/api/releases.rb2
-rw-r--r--lib/api/remote_mirrors.rb2
-rw-r--r--lib/api/repositories.rb4
-rw-r--r--lib/api/resource_label_events.rb2
-rw-r--r--lib/api/runner.rb4
-rw-r--r--lib/api/runners.rb12
-rw-r--r--lib/api/search.rb2
-rw-r--r--lib/api/services.rb2
-rw-r--r--lib/api/settings.rb11
-rw-r--r--lib/api/sidekiq_metrics.rb2
-rw-r--r--lib/api/snippets.rb2
-rw-r--r--lib/api/statistics.rb2
-rw-r--r--lib/api/submodules.rb2
-rw-r--r--lib/api/subscriptions.rb2
-rw-r--r--lib/api/suggestions.rb2
-rw-r--r--lib/api/system_hooks.rb2
-rw-r--r--lib/api/tags.rb2
-rw-r--r--lib/api/templates.rb2
-rw-r--r--lib/api/terraform/state.rb2
-rw-r--r--lib/api/todos.rb2
-rw-r--r--lib/api/triggers.rb2
-rw-r--r--lib/api/user_counts.rb2
-rw-r--r--lib/api/users.rb2
-rw-r--r--lib/api/validations/types/comma_separated_to_array.rb2
-rw-r--r--lib/api/validations/types/comma_separated_to_integer_array.rb15
-rw-r--r--lib/api/validations/types/labels_list.rb24
-rw-r--r--lib/api/validations/types/safe_file.rb15
-rw-r--r--lib/api/validations/types/workhorse_file.rb13
-rw-r--r--lib/api/variables.rb2
-rw-r--r--lib/api/version.rb2
-rw-r--r--lib/api/wikis.rb4
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb15
-rw-r--r--lib/gitlab/ci/pipeline/chain/sequence.rb3
-rw-r--r--lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Terraform.gitlab-ci.yml2
-rw-r--r--lib/gitlab/visibility_level.rb12
-rw-r--r--locale/gitlab.pot19
-rw-r--r--package.json4
-rw-r--r--rubocop/cop/api/grape_api_instance.rb42
-rw-r--r--rubocop/cop/api/grape_array_missing_coerce.rb83
-rw-r--r--rubocop/cop/gitlab/json.rb36
-rwxr-xr-xscripts/frontend/webpack_dev_server.js68
-rw-r--r--spec/factories/identities.rb2
-rw-r--r--spec/frontend/alert_management/components/alert_management_list_spec.js11
-rw-r--r--spec/frontend/boards/boards_store_spec.js61
-rw-r--r--spec/frontend/boards/issue_spec.js22
-rw-r--r--spec/frontend/deploy_keys/components/action_btn_spec.js54
-rw-r--r--spec/frontend/deploy_keys/components/app_spec.js142
-rw-r--r--spec/frontend/deploy_keys/components/key_spec.js161
-rw-r--r--spec/frontend/deploy_keys/components/keys_panel_spec.js63
-rw-r--r--spec/frontend/dirty_submit/dirty_submit_collection_spec.js22
-rw-r--r--spec/frontend/dirty_submit/dirty_submit_factory_spec.js (renamed from spec/javascripts/dirty_submit/dirty_submit_factory_spec.js)0
-rw-r--r--spec/frontend/dirty_submit/dirty_submit_form_spec.js (renamed from spec/javascripts/dirty_submit/dirty_submit_form_spec.js)65
-rw-r--r--spec/frontend/dirty_submit/helper.js (renamed from spec/javascripts/dirty_submit/helper.js)5
-rw-r--r--spec/frontend/pipelines/mock_data.js423
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js659
-rw-r--r--spec/frontend/users_select/utils_spec.js33
-rw-r--r--spec/graphql/types/jira_import_type_spec.rb2
-rw-r--r--spec/helpers/explore_helper_spec.rb19
-rw-r--r--spec/helpers/projects/alert_management_helper_spec.rb17
-rw-r--r--spec/javascripts/deploy_keys/components/action_btn_spec.js72
-rw-r--r--spec/javascripts/deploy_keys/components/app_spec.js155
-rw-r--r--spec/javascripts/deploy_keys/components/key_spec.js157
-rw-r--r--spec/javascripts/deploy_keys/components/keys_panel_spec.js63
-rw-r--r--spec/javascripts/dirty_submit/dirty_submit_collection_spec.js29
-rw-r--r--spec/javascripts/pipelines/pipelines_spec.js783
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb10
-rw-r--r--spec/lib/gitlab/kubernetes/helm/base_command_spec.rb80
-rw-r--r--spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb35
-rw-r--r--spec/lib/gitlab/kubernetes/helm/init_command_spec.rb73
-rw-r--r--spec/lib/gitlab/kubernetes/helm/install_command_spec.rb84
-rw-r--r--spec/lib/gitlab/kubernetes/helm/patch_command_spec.rb68
-rw-r--r--spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb33
-rw-r--r--spec/lib/gitlab/visibility_level_spec.rb24
-rw-r--r--spec/models/jira_import_state_spec.rb1
-rw-r--r--spec/requests/api/graphql/project/jira_import_spec.rb1
-rw-r--r--spec/requests/api/settings_spec.rb10
-rw-r--r--spec/rubocop/cop/api/grape_api_instance_spec.rb31
-rw-r--r--spec/rubocop/cop/api/grape_array_missing_coerce_spec.rb64
-rw-r--r--spec/rubocop/cop/code_reuse/worker_spec.rb2
-rw-r--r--spec/rubocop/cop/gitlab/json_spec.rb39
-rw-r--r--spec/support/shared_examples/helm_commands_shared_examples.rb131
-rw-r--r--spec/support/shared_examples/lib/gitlab/helm_generated_script_shared_examples.rb15
192 files changed, 2891 insertions, 2057 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 5de4cb7874f..bd19b80e8b5 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -211,6 +211,13 @@ Gitlab/HTTParty:
- 'spec/**/*'
- 'ee/spec/**/*'
+Gitlab/Json:
+ Enabled: false
+ Exclude:
+ - 'db/**/*'
+ - 'qa/**/*'
+ - 'scripts/**/*'
+
GitlabSecurity/PublicSend:
Enabled: true
Exclude:
@@ -277,6 +284,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 031252846e0..88c1ed19a30 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'
@@ -82,7 +82,7 @@ gem 'gitlab_omniauth-ldap', '~> 2.1.1', require: 'omniauth-ldap'
gem 'net-ldap'
# API
-gem 'grape', '~> 1.1.0'
+gem 'grape', '~> 1.3.2'
gem 'grape-entity', '~> 0.7.1'
gem 'rack-cors', '~> 1.0.6', require: 'rack/cors'
diff --git a/Gemfile.lock b/Gemfile.lock
index cba3e863e24..9f60cdcd4ad 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)
@@ -165,8 +161,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)
@@ -222,8 +216,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)
@@ -250,6 +242,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.7)
gpgme (2.0.20)
mini_portile2 (~> 2.3)
- grape (1.1.0)
+ grape (1.3.2)
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
@@ -645,9 +659,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)
@@ -961,6 +976,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)
@@ -1119,11 +1135,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)
@@ -1254,9 +1265,9 @@ DEPENDENCIES
google-api-client (~> 0.23)
google-protobuf (~> 3.8.0)
gpgme (~> 2.0.19)
- grape (~> 1.1.0)
+ grape (~> 1.3.2)
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/javascripts/alert_management/components/alert_management_list.vue b/app/assets/javascripts/alert_management/components/alert_management_list.vue
index 626d3017319..bd60dd45261 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_list.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_list.vue
@@ -1,5 +1,5 @@
<script>
-import { GlEmptyState, GlButton, GlLoadingIcon, GlTable, GlAlert } from '@gitlab/ui';
+import { GlEmptyState, GlDeprecatedButton, GlLoadingIcon, GlTable, GlAlert } from '@gitlab/ui';
import { s__ } from '~/locale';
import getAlerts from '../graphql/queries/getAlerts.query.graphql';
@@ -49,26 +49,28 @@ export default {
],
components: {
GlEmptyState,
- GlButton,
GlLoadingIcon,
GlTable,
GlAlert,
+ GlDeprecatedButton,
},
props: {
indexPath: {
type: String,
required: true,
},
- // TODO: Handle alertManagementEnabled depending on resolution - https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30024.
alertManagementEnabled: {
type: Boolean,
- required: false,
- default: true,
+ required: true,
},
enableAlertManagementPath: {
type: String,
required: true,
},
+ userCanEnableAlertManagement: {
+ type: Boolean,
+ required: true,
+ },
emptyAlertSvgPath: {
type: String,
required: true,
@@ -137,29 +139,28 @@ export default {
</template>
</gl-table>
</div>
- <template v-else>
- <gl-empty-state
- :title="s__('AlertManagement|Surface alerts in GitLab')"
- :svg-path="emptyAlertSvgPath"
- >
- <template #description>
- <div class="d-block">
- <span>{{
- s__(
- 'AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents.',
- )
- }}</span>
- <a href="/help/user/project/operations/alert_management.html">
- {{ s__('AlertManagement|More information') }}
- </a>
- </div>
- <div class="d-block center pt-4">
- <gl-button category="primary" variant="success" :href="enableAlertManagementPath">
- {{ s__('AlertManagement|Authorize external service') }}
- </gl-button>
- </div>
- </template>
- </gl-empty-state>
- </template>
+ <gl-empty-state v-else :title="__('Surface alerts in GitLab')" :svg-path="emptyAlertSvgPath">
+ <template #description>
+ <div class="d-block">
+ <span>{{
+ s__(
+ 'AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents.',
+ )
+ }}</span>
+ <a href="/help/user/project/operations/alert_management.html" target="_blank">
+ {{ s__('AlertManagement|More information') }}
+ </a>
+ </div>
+ <div v-if="userCanEnableAlertManagement" class="d-block center pt-4">
+ <gl-deprecated-button
+ category="primary"
+ variant="success"
+ :href="enableAlertManagementPath"
+ >
+ {{ s__('AlertManagement|Authorize external service') }}
+ </gl-deprecated-button>
+ </div>
+ </template>
+ </gl-empty-state>
</div>
</template>
diff --git a/app/assets/javascripts/alert_management/list.js b/app/assets/javascripts/alert_management/list.js
index ce408a8bdaa..3de4ebd96f8 100644
--- a/app/assets/javascripts/alert_management/list.js
+++ b/app/assets/javascripts/alert_management/list.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
+import { parseBoolean } from '~/lib/utils/common_utils';
import AlertManagementList from './components/alert_management_list.vue';
Vue.use(VueApollo);
@@ -10,6 +11,10 @@ export default () => {
const domEl = document.querySelector(selector);
const { indexPath, enableAlertManagementPath, emptyAlertSvgPath } = domEl.dataset;
+ let { alertManagementEnabled, userCanEnableAlertManagement } = domEl.dataset;
+
+ alertManagementEnabled = parseBoolean(alertManagementEnabled);
+ userCanEnableAlertManagement = parseBoolean(userCanEnableAlertManagement);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
@@ -27,6 +32,8 @@ export default () => {
indexPath,
enableAlertManagementPath,
emptyAlertSvgPath,
+ alertManagementEnabled,
+ userCanEnableAlertManagement,
},
});
},
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index d099c4b930c..af1a910149e 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -99,31 +99,7 @@ class ListIssue {
}
update() {
- const data = {
- issue: {
- milestone_id: this.milestone ? this.milestone.id : null,
- due_date: this.dueDate,
- assignee_ids: this.assignees.length > 0 ? this.assignees.map(u => u.id) : [0],
- label_ids: this.labels.map(label => label.id),
- },
- };
-
- if (!data.issue.label_ids.length) {
- data.issue.label_ids = [''];
- }
-
- const projectPath = this.project ? this.project.path : '';
- return axios.patch(`${this.path}.json`, data).then(({ data: body = {} } = {}) => {
- /**
- * Since post implementation of Scoped labels, server can reject
- * same key-ed labels. To keep the UI and server Model consistent,
- * we're just assigning labels that server echo's back to us when we
- * PATCH the said object.
- */
- if (body) {
- this.labels = convertObjectPropsToCamelCase(body.labels, { deep: true });
- }
- });
+ return boardsStore.updateIssue(this);
}
}
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index e5447080e37..4b4c7550553 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -6,7 +6,11 @@ import { sortBy } from 'lodash';
import Vue from 'vue';
import Cookies from 'js-cookie';
import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
-import { getUrlParamsArray, parseBoolean } from '~/lib/utils/common_utils';
+import {
+ getUrlParamsArray,
+ parseBoolean,
+ convertObjectPropsToCamelCase,
+} from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
@@ -632,6 +636,28 @@ const boardsStore = {
issue.assignees = obj.assignees.map(a => new ListAssignee(a));
}
},
+ updateIssue(issue) {
+ const data = {
+ issue: {
+ milestone_id: issue.milestone ? issue.milestone.id : null,
+ due_date: issue.dueDate,
+ assignee_ids: issue.assignees.length > 0 ? issue.assignees.map(({ id }) => id) : [0],
+ label_ids: issue.labels.length > 0 ? issue.labels.map(({ id }) => id) : [''],
+ },
+ };
+
+ return axios.patch(`${issue.path}.json`, data).then(({ data: body = {} } = {}) => {
+ /**
+ * Since post implementation of Scoped labels, server can reject
+ * same key-ed labels. To keep the UI and server Model consistent,
+ * we're just assigning labels that server echo's back to us when we
+ * PATCH the said object.
+ */
+ if (body) {
+ issue.labels = convertObjectPropsToCamelCase(body.labels, { deep: true });
+ }
+ });
+ },
};
BoardsStoreEE.initEESpecific(boardsStore);
diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select.js
index 577c21fed5d..ebbe8549656 100644
--- a/app/assets/javascripts/users_select/index.js
+++ b/app/assets/javascripts/users_select.js
@@ -4,15 +4,10 @@
import $ from 'jquery';
import { escape, template, uniqBy } from 'lodash';
-import axios from '../lib/utils/axios_utils';
-import { s__, __, sprintf } from '../locale';
-import ModalStore from '../boards/stores/modal_store';
-import { parseBoolean } from '../lib/utils/common_utils';
-import {
- AJAX_USERS_SELECT_OPTIONS_MAP,
- AJAX_USERS_SELECT_PARAMS_MAP,
-} from 'ee_else_ce/users_select/constants';
-import { getAjaxUsersSelectOptions, getAjaxUsersSelectParams } from './utils';
+import axios from './lib/utils/axios_utils';
+import { s__, __, sprintf } from './locale';
+import ModalStore from './boards/stores/modal_store';
+import { parseBoolean } from './lib/utils/common_utils';
// TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
@@ -563,8 +558,13 @@ function UsersSelect(currentUser, els, options = {}) {
import(/* webpackChunkName: 'select2' */ 'select2/select2')
.then(() => {
$('.ajax-users-select').each((i, select) => {
- const options = getAjaxUsersSelectOptions($(select), AJAX_USERS_SELECT_OPTIONS_MAP);
+ const options = {};
options.skipLdap = $(select).hasClass('skip_ldap');
+ options.projectId = $(select).data('projectId');
+ options.groupId = $(select).data('groupId');
+ options.showCurrentUser = $(select).data('currentUser');
+ options.authorId = $(select).data('authorId');
+ options.skipUsers = $(select).data('skipUsers');
const showNullUser = $(select).data('nullUser');
const showAnyUser = $(select).data('anyUser');
const showEmailUser = $(select).data('emailUser');
@@ -705,7 +705,14 @@ UsersSelect.prototype.users = function(query, options, callback) {
const params = {
search: query,
active: true,
- ...getAjaxUsersSelectParams(options, AJAX_USERS_SELECT_PARAMS_MAP),
+ project_id: options.projectId || null,
+ group_id: options.groupId || null,
+ skip_ldap: options.skipLdap || null,
+ todo_filter: options.todoFilter || null,
+ todo_state_filter: options.todoStateFilter || null,
+ current_user: options.showCurrentUser || null,
+ author_id: options.authorId || null,
+ skip_users: options.skipUsers || null,
};
if (options.issuableType === 'merge_request') {
diff --git a/app/assets/javascripts/users_select/constants.js b/app/assets/javascripts/users_select/constants.js
deleted file mode 100644
index 64df1e1748c..00000000000
--- a/app/assets/javascripts/users_select/constants.js
+++ /dev/null
@@ -1,18 +0,0 @@
-export const AJAX_USERS_SELECT_OPTIONS_MAP = {
- projectId: 'projectId',
- groupId: 'groupId',
- showCurrentUser: 'currentUser',
- authorId: 'authorId',
- skipUsers: 'skipUsers',
-};
-
-export const AJAX_USERS_SELECT_PARAMS_MAP = {
- project_id: 'projectId',
- group_id: 'groupId',
- skip_ldap: 'skipLdap',
- todo_filter: 'todoFilter',
- todo_state_filter: 'todoStateFilter',
- current_user: 'showCurrentUser',
- author_id: 'authorId',
- skip_users: 'skipUsers',
-};
diff --git a/app/assets/javascripts/users_select/utils.js b/app/assets/javascripts/users_select/utils.js
deleted file mode 100644
index b46fd15fb77..00000000000
--- a/app/assets/javascripts/users_select/utils.js
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * Get options from data attributes on passed `$select`.
- * @param {jQuery} $select
- * @param {Object} optionsMap e.g. { optionKeyName: 'dataAttributeName' }
- */
-export const getAjaxUsersSelectOptions = ($select, optionsMap) => {
- return Object.keys(optionsMap).reduce((accumulator, optionKey) => {
- const dataKey = optionsMap[optionKey];
- accumulator[optionKey] = $select.data(dataKey);
-
- return accumulator;
- }, {});
-};
-
-/**
- * Get query parameters used for users request from passed `options` parameter
- * @param {Object} options e.g. { currentUserId: 1, fooBar: 'baz' }
- * @param {Object} paramsMap e.g. { user_id: 'currentUserId', foo_bar: 'fooBar' }
- */
-export const getAjaxUsersSelectParams = (options, paramsMap) => {
- return Object.keys(paramsMap).reduce((accumulator, paramKey) => {
- const optionKey = paramsMap[paramKey];
- accumulator[paramKey] = options[optionKey] || null;
-
- return accumulator;
- }, {});
-};
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 26ef6117e1c..b5695322eb6 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -497,7 +497,7 @@ class ApplicationController < ActionController::Base
end
def public_visibility_restricted?
- Gitlab::CurrentSettings.restricted_visibility_levels.include? Gitlab::VisibilityLevel::PUBLIC
+ Gitlab::VisibilityLevel.public_visibility_restricted?
end
def set_usage_stats_consent_flag
diff --git a/app/graphql/types/jira_import_type.rb b/app/graphql/types/jira_import_type.rb
index ccd463370b6..4a124566ffb 100644
--- a/app/graphql/types/jira_import_type.rb
+++ b/app/graphql/types/jira_import_type.rb
@@ -7,9 +7,10 @@ module Types
class JiraImportType < BaseObject
graphql_name 'JiraImport'
- field :scheduled_at, Types::TimeType, null: true,
- method: :created_at,
+ field :created_at, Types::TimeType, null: true,
description: 'Timestamp of when the Jira import was created'
+ field :scheduled_at, Types::TimeType, null: true,
+ description: 'Timestamp of when the Jira import was scheduled'
field :scheduled_by, Types::UserType, null: true,
description: 'User that started the Jira import'
field :jira_project_key, GraphQL::STRING_TYPE, null: false,
diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb
index b66c7a69b71..026dbd60ac6 100644
--- a/app/helpers/explore_helper.rb
+++ b/app/helpers/explore_helper.rb
@@ -52,7 +52,7 @@ module ExploreHelper
end
def public_visibility_restricted?
- Gitlab::CurrentSettings.restricted_visibility_levels&.include? Gitlab::VisibilityLevel::PUBLIC
+ Gitlab::VisibilityLevel.public_visibility_restricted?
end
private
diff --git a/app/helpers/projects/alert_management_helper.rb b/app/helpers/projects/alert_management_helper.rb
index 1b0400fbaa5..6aadc18ac3a 100644
--- a/app/helpers/projects/alert_management_helper.rb
+++ b/app/helpers/projects/alert_management_helper.rb
@@ -1,12 +1,14 @@
# frozen_string_literal: true
module Projects::AlertManagementHelper
- def alert_management_data(project)
+ def alert_management_data(current_user, project)
{
'index-path' => project_alert_management_index_path(project,
format: :json),
'enable-alert-management-path' => project_settings_operations_path(project),
- 'empty-alert-svg-path' => image_path('illustrations/alert-management-empty-state.svg')
+ 'empty-alert-svg-path' => image_path('illustrations/alert-management-empty-state.svg'),
+ 'user-can-enable-alert-management' => 'false',
+ 'alert-management-enabled' => Feature.enabled?(:alert_management_minimal, project).to_s
}
end
end
diff --git a/app/models/jira_import_state.rb b/app/models/jira_import_state.rb
index bde2795e7b8..71bb25470f5 100644
--- a/app/models/jira_import_state.rb
+++ b/app/models/jira_import_state.rb
@@ -46,7 +46,7 @@ class JiraImportState < ApplicationRecord
after_transition initial: :scheduled do |state, _|
state.run_after_commit do
job_id = Gitlab::JiraImport::Stage::StartImportWorker.perform_async(project.id)
- state.update(jid: job_id) if job_id
+ state.update(jid: job_id, scheduled_at: Time.now) if job_id
end
end
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index 05214346496..2f0cc76f2e0 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -6,8 +6,6 @@
= render 'dashboard/snippets_head'
- if current_user.snippets.exists?
= render partial: 'snippets/snippets_scope_menu', locals: { include_private: true, counts: @snippet_counts }
-
-- if current_user.snippets.exists?
= render partial: 'shared/snippets/list', locals: { link_project: true }
- else
= render 'shared/empty_states/snippets', button_path: button_path
diff --git a/app/views/projects/alert_management/index.html.haml b/app/views/projects/alert_management/index.html.haml
index dab6aec0446..415820ac3ad 100644
--- a/app/views/projects/alert_management/index.html.haml
+++ b/app/views/projects/alert_management/index.html.haml
@@ -1,3 +1,3 @@
- page_title _('Alerts')
-#js-alert_management{ data: alert_management_data(@project) }
+#js-alert_management{ data: alert_management_data(@current_user, @project) }
diff --git a/changelogs/unreleased/213808-add-scheduled-at-field-to-jira-imports.yml b/changelogs/unreleased/213808-add-scheduled-at-field-to-jira-imports.yml
new file mode 100644
index 00000000000..29de6311997
--- /dev/null
+++ b/changelogs/unreleased/213808-add-scheduled-at-field-to-jira-imports.yml
@@ -0,0 +1,5 @@
+---
+title: Add scheduled_at field to jira_imports table
+merge_request: 30284
+author:
+type: added
diff --git a/changelogs/unreleased/Remove-update-function-logic-from-issue-js.yml b/changelogs/unreleased/Remove-update-function-logic-from-issue-js.yml
new file mode 100644
index 00000000000..a9e80606601
--- /dev/null
+++ b/changelogs/unreleased/Remove-update-function-logic-from-issue-js.yml
@@ -0,0 +1,5 @@
+---
+title: Moves updateIssue from issue model to board store
+merge_request: 21414
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/sh-clean-up-public-visiblity-level-check.yml b/changelogs/unreleased/sh-clean-up-public-visiblity-level-check.yml
new file mode 100644
index 00000000000..88f737a49a1
--- /dev/null
+++ b/changelogs/unreleased/sh-clean-up-public-visiblity-level-check.yml
@@ -0,0 +1,5 @@
+---
+title: Fix second 500 error with NULL restricted visibility levels
+merge_request: 30414
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-update-grape-gem.yml b/changelogs/unreleased/sh-update-grape-gem.yml
new file mode 100644
index 00000000000..4aec45c9429
--- /dev/null
+++ b/changelogs/unreleased/sh-update-grape-gem.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade Grape v1.1.0 to v1.3.2
+merge_request: 27276
+author:
+type: other
diff --git a/changelogs/unreleased/user-can-enable-alert-management.yml b/changelogs/unreleased/user-can-enable-alert-management.yml
new file mode 100644
index 00000000000..5012a078b26
--- /dev/null
+++ b/changelogs/unreleased/user-can-enable-alert-management.yml
@@ -0,0 +1,5 @@
+---
+title: Alert management can user enable
+merge_request: 30024
+author:
+type: changed
diff --git a/db/migrate/20200423101529_add_scheduled_at_to_jira_imports.rb b/db/migrate/20200423101529_add_scheduled_at_to_jira_imports.rb
new file mode 100644
index 00000000000..f0d9393b6f9
--- /dev/null
+++ b/db/migrate/20200423101529_add_scheduled_at_to_jira_imports.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddScheduledAtToJiraImports < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ add_column :jira_imports, :scheduled_at, :datetime_with_timezone
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index fca564dde8c..2ba55f2338e 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -3433,7 +3433,8 @@ CREATE TABLE public.jira_imports (
status smallint DEFAULT 0 NOT NULL,
jid character varying(255),
jira_project_key character varying(255) NOT NULL,
- jira_project_name character varying(255) NOT NULL
+ jira_project_name character varying(255) NOT NULL,
+ scheduled_at timestamp with time zone
);
CREATE SEQUENCE public.jira_imports_id_seq
@@ -13521,6 +13522,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200423080334
20200423080607
20200423081409
+20200423101529
20200424050250
20200427064130
\.
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 7921a10301d..fff7762bd99 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -36,6 +36,7 @@ The following metrics are available:
| `gitlab_cache_misses_total` | Counter | 10.2 | Cache read miss | controller, action |
| `gitlab_cache_operation_duration_seconds` | Histogram | 10.2 | Cache access time | |
| `gitlab_cache_operations_total` | Counter | 12.2 | Cache operations by controller/action | controller, action, operation |
+| `gitlab_ci_pipeline_creation_duration_seconds` | Histogram | 13.0 | Time in seconds it takes to create a CI/CD pipeline | |
| `job_waiter_started_total` | Counter | 12.9 | Number of batches of jobs started where a web request is waiting for the jobs to complete | worker |
| `job_waiter_timeouts_total` | Counter | 12.9 | Number of batches of jobs that timed out where a web request is waiting for the jobs to complete | worker |
| `gitlab_database_transaction_seconds` | Histogram | 12.1 | Time spent in database transactions, in seconds | |
diff --git a/doc/api/README.md b/doc/api/README.md
index 99d74aab83f..e08006a7f7f 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -417,7 +417,7 @@ The response header includes a link to the next page. For example:
```http
HTTP/1.1 200 OK
...
-Link: <https://gitlab.example.com/api/v4/projects?pagination=keyset&per_page=50&order_by=id&sort=asc&id_after=42>; rel="next"
+Links: <https://gitlab.example.com/api/v4/projects?pagination=keyset&per_page=50&order_by=id&sort=asc&id_after=42>; rel="next"
Status: 200 OK
...
```
@@ -425,7 +425,7 @@ Status: 200 OK
The link to the next page contains an additional filter `id_after=42` which excludes records we have retrieved already.
Note the type of filter depends on the `order_by` option used and we may have more than one additional filter.
-When the end of the collection has been reached and there are no additional records to retrieve, the `Link` header is absent and the resulting array is empty.
+When the end of the collection has been reached and there are no additional records to retrieve, the `Links` header is absent and the resulting array is empty.
We recommend using only the given link to retrieve the next page instead of building your own URL. Apart from the headers shown,
we don't expose additional pagination headers.
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 3522a5a50eb..9f5f0278f91 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -411,6 +411,56 @@ type BoardListEdge {
node: BoardList
}
+"""
+Autogenerated input type of BoardListUpdateLimitMetrics
+"""
+input BoardListUpdateLimitMetricsInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The new limit metric type for the list.
+ """
+ limitMetric: ListLimitMetric
+
+ """
+ The global ID of the list.
+ """
+ listId: ID!
+
+ """
+ The new maximum issue count limit.
+ """
+ maxIssueCount: Int
+
+ """
+ The new maximum issue weight limit.
+ """
+ maxIssueWeight: Int
+}
+
+"""
+Autogenerated return type of BoardListUpdateLimitMetrics
+"""
+type BoardListUpdateLimitMetricsPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Reasons why the mutation failed.
+ """
+ errors: [String!]!
+
+ """
+ The updated list
+ """
+ list: BoardList
+}
+
type Commit {
"""
Author of the commit
@@ -4460,12 +4510,17 @@ scalar JSON
type JiraImport {
"""
+ Timestamp of when the Jira import was created
+ """
+ createdAt: Time
+
+ """
Project key for the imported Jira project
"""
jiraProjectKey: String!
"""
- Timestamp of when the Jira import was created
+ Timestamp of when the Jira import was scheduled
"""
scheduledAt: Time
@@ -5689,6 +5744,7 @@ enum MoveType {
type Mutation {
addAwardEmoji(input: AddAwardEmojiInput!): AddAwardEmojiPayload
adminSidekiqQueuesDeleteJobs(input: AdminSidekiqQueuesDeleteJobsInput!): AdminSidekiqQueuesDeleteJobsPayload
+ boardListUpdateLimitMetrics(input: BoardListUpdateLimitMetricsInput!): BoardListUpdateLimitMetricsPayload
createDiffNote(input: CreateDiffNoteInput!): CreateDiffNotePayload
createEpic(input: CreateEpicInput!): CreateEpicPayload
createImageDiffNote(input: CreateImageDiffNoteInput!): CreateImageDiffNotePayload
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 4c8fb099faa..140519d403e 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -1263,6 +1263,138 @@
"possibleTypes": null
},
{
+ "kind": "INPUT_OBJECT",
+ "name": "BoardListUpdateLimitMetricsInput",
+ "description": "Autogenerated input type of BoardListUpdateLimitMetrics",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "listId",
+ "description": "The global ID of the list.",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "limitMetric",
+ "description": "The new limit metric type for the list.",
+ "type": {
+ "kind": "ENUM",
+ "name": "ListLimitMetric",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "maxIssueCount",
+ "description": "The new maximum issue count limit.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "maxIssueWeight",
+ "description": "The new maximum issue weight limit.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "BoardListUpdateLimitMetricsPayload",
+ "description": "Autogenerated return type of BoardListUpdateLimitMetrics",
+ "fields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Reasons why the mutation failed.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "list",
+ "description": "The updated list",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "BoardList",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
"kind": "SCALAR",
"name": "Boolean",
"description": "Represents `true` or `false` values.",
@@ -12636,6 +12768,20 @@
"description": null,
"fields": [
{
+ "name": "createdAt",
+ "description": "Timestamp of when the Jira import was created",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "jiraProjectKey",
"description": "Project key for the imported Jira project",
"args": [
@@ -12655,7 +12801,7 @@
},
{
"name": "scheduledAt",
- "description": "Timestamp of when the Jira import was created",
+ "description": "Timestamp of when the Jira import was scheduled",
"args": [
],
@@ -16353,6 +16499,33 @@
"deprecationReason": null
},
{
+ "name": "boardListUpdateLimitMetrics",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "BoardListUpdateLimitMetricsInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "BoardListUpdateLimitMetricsPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "createDiffNote",
"description": null,
"args": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index af126f48c50..6205c97d77c 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -97,6 +97,16 @@ Represents a list for an issue board
| `position` | Int | Position of list within the board |
| `title` | String! | Title of the list |
+## BoardListUpdateLimitMetricsPayload
+
+Autogenerated return type of BoardListUpdateLimitMetrics
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
+| `list` | BoardList | The updated list |
+
## Commit
| Name | Type | Description |
@@ -667,8 +677,9 @@ Autogenerated return type of IssueSetWeight
| Name | Type | Description |
| --- | ---- | ---------- |
+| `createdAt` | Time | Timestamp of when the Jira import was created |
| `jiraProjectKey` | String! | Project key for the imported Jira project |
-| `scheduledAt` | Time | Timestamp of when the Jira import was created |
+| `scheduledAt` | Time | Timestamp of when the Jira import was scheduled |
| `scheduledBy` | User | User that started the Jira import |
## JiraImportStartPayload
diff --git a/doc/development/api_styleguide.md b/doc/development/api_styleguide.md
index 25c8cbd3fde..a9ce1bc066e 100644
--- a/doc/development/api_styleguide.md
+++ b/doc/development/api_styleguide.md
@@ -98,6 +98,14 @@ 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.
+
## 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 c9fd1b75606..1d03e93ab79 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -513,12 +513,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
-paramters 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
@@ -579,7 +579,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!
@@ -623,7 +623,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)
@@ -692,7 +692,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[
@@ -708,7 +708,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/geo/framework.md b/doc/development/geo/framework.md
index 989949a7ca1..83809d1fd3d 100644
--- a/doc/development/geo/framework.md
+++ b/doc/development/geo/framework.md
@@ -270,7 +270,7 @@ For example, to add support for files referenced by a `Widget` model with a
```ruby
# frozen_string_literal: true
- class CreateWidgetRegistry < ActiveRecord::Migration[5.2]
+ class CreateWidgetRegistry < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
@@ -334,7 +334,7 @@ For example, to add support for files referenced by a `Widget` model with a
end
```
-1. Create `ee/spec/models/geo/widget_registry.rb`:
+1. Create `ee/spec/models/geo/widget_registry_spec.rb`:
```ruby
# frozen_string_literal: true
diff --git a/doc/development/logging.md b/doc/development/logging.md
index ef2d2d7022d..ba2e879a04e 100644
--- a/doc/development/logging.md
+++ b/doc/development/logging.md
@@ -360,4 +360,4 @@ end
project. See [this example](https://gitlab.com/gitlab-cookbooks/gitlab_fluentd/-/merge_requests/51/diffs).
1. Be sure to update the [GitLab CE/EE documentation](../administration/logs.md) and the [GitLab.com
- runbooks](https://gitlab.com/gitlab-com/runbooks/blob/master/howto/logging.md).
+ runbooks](https://gitlab.com/gitlab-com/runbooks/blob/master/docs/logging/README.md).
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index c6bd402f57c..ad13fe0c6b4 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -491,6 +491,7 @@ For every language and package manager, add the following to the variables secti
```yaml
GEMNASIUM_DB_REMOTE_URL: "gitlab.example.com/gemnasium-db.git"
+GIT_SSL_NO_VERIFY: "true"
```
See the following sections for additional instructions on specific languages and package managers.
@@ -520,7 +521,7 @@ When using self-signed certificates, add the following job section to the `.gitl
```yaml
gemnasium-maven-dependency_scanning:
variables:
- MAVEN_CLI_OPTS: "-Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true -Dmaven.wagon.http.ssl.ignore.validity.dates=true"
+ MAVEN_CLI_OPTS: "-s settings.xml -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true -Dmaven.wagon.http.ssl.ignore.validity.dates=true"
```
#### Java (Gradle) projects
diff --git a/doc/user/project/issues/design_management.md b/doc/user/project/issues/design_management.md
index 1078d0410ed..21e36cf9cad 100644
--- a/doc/user/project/issues/design_management.md
+++ b/doc/user/project/issues/design_management.md
@@ -86,6 +86,7 @@ Copy-and-pasting has some limitations:
- You can paste only one image at a time. When copy/pasting multiple files, only the first one will be uploaded.
- All images will be converted to `png` format under the hood, so when you want to copy/paste `gif` file, it will result in broken animation.
+- If you are pasting a screenshot from the clipboard, it will be renamed to `design_<timestamp>.png`
- Copy/pasting designs is not supported on Internet Explorer.
Designs with the same filename as an existing uploaded design will create a new version
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/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 de9a3120d90..6019a8991f3 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")
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index 9dd2de5c7ba..cb83d22a07f 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -148,7 +148,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 a775102e87d..8a46ebf4ef4 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 999bf1627c1..4c8e4b7a116 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/commit_statuses.rb b/lib/api/commit_statuses.rb
index b4c5d7869a2..a34ac5b0169 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 e86bcc19b2b..def479ba99b 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 f3a08ae970a..c088c71cde7 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
@@ -54,7 +54,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", or "write_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}`'
@@ -117,7 +117,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", or "write_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 0dd1850e526..4e71e4c50de 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 69b751e9bdb..181c2fd4a6f 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 76ab9a2190b..1e2f0e011ed 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/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 7f95b411b36..d924d717c85 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 8ca5dfa082e..b5933ca4b94 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
before do
not_found! unless Feature.enabled?(:group_import_export, user_group, default_enabled: true)
diff --git a/lib/api/group_import.rb b/lib/api/group_import.rb
index ed52506de14..a20523fd55f 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
MAXIMUM_FILE_SIZE = 50.megabytes.freeze
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..05dc417e3b1 100644
--- a/lib/api/group_milestones.rb
+++ b/lib/api/group_milestones.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class GroupMilestones < Grape::API
+ class GroupMilestones < Grape::API::Instance
include MilestoneResponses
include PaginationParams
diff --git a/lib/api/group_variables.rb b/lib/api/group_variables.rb
index 916f89649a5..7cf7584bf4c 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 d375c35e8c0..7f42d8ed0d9 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/merge_requests_helpers.rb b/lib/api/helpers/merge_requests_helpers.rb
index 73711a7e0ba..00430fa08c6 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 :created_after, type: DateTime, desc: 'Return merge requests created after the specified time'
diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb
index 14c83114f32..b80d662f117 100644
--- a/lib/api/helpers/projects_helpers.rb
+++ b/lib/api/helpers/projects_helpers.rb
@@ -44,7 +44,7 @@ module API
optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
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 564a00701c4..eab8ba25410 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 f27afd0055f..9ef1561f423 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
@@ -11,9 +11,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'
@@ -63,10 +63,10 @@ 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 :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 920938ad453..321c14de1b9 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 59f0dbe8a9b..7a7dfcedecb 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/lsif_data.rb b/lib/api/lsif_data.rb
index a673ccb4af0..338d6c533a4 100644
--- a/lib/api/lsif_data.rb
+++ b/lib/api/lsif_data.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class LsifData < Grape::API
+ class LsifData < Grape::API::Instance
MAX_FILE_SIZE = 10.megabytes
before 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 37d4ca29b68..2254a0b7898 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 d45786cdd3d..b7bc936fe2f 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
@@ -177,9 +177,9 @@ 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 :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'
@@ -194,7 +194,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
diff --git a/lib/api/metrics/dashboard/annotations.rb b/lib/api/metrics/dashboard/annotations.rb
index 432fa3ac0c9..d71a4e9d736 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/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 edc99590cdb..46058f45bcb 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 06f8920b37c..f881d5b63e6 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 555fd98b451..ed15bd92f1b 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 9fd9d13a20c..797c6097b04 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 0e83686cab2..9be192a80e0 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..71388fd500a 100644
--- a/lib/api/project_milestones.rb
+++ b/lib/api/project_milestones.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class ProjectMilestones < Grape::API
+ class ProjectMilestones < Grape::API::Instance
include PaginationParams
include MilestoneResponses
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 f5ca2f4d5a1..c3e4c806a59 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 119902a189c..3eded5606c1 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 ee0731a331f..7c98a749bf7 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
@@ -520,7 +520,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 f72230c084c..16154aac7b5 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 95b3e90323c..ae11561205e 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
diff --git a/lib/api/remote_mirrors.rb b/lib/api/remote_mirrors.rb
index 7e484eb8885..ef83d8de151 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 0b2df85f61f..37f134dcffa 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
before { authorize! :download_code, user_project }
@@ -139,7 +139,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 f7f7c881f4a..60bcee094ad 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/runner.rb b/lib/api/runner.rb
index 9095aba7340..aac26175715 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Runner < Grape::API
+ class Runner < Grape::API::Instance
helpers ::API::Helpers::Runner
resource :runners do
@@ -18,7 +18,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/runners.rb b/lib/api/runners.rb
index 43ee1dd1f71..f1adc9e5aff 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Runners < Grape::API
+ class Runners < Grape::API::Instance
include PaginationParams
before { authenticate! }
@@ -17,7 +17,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
@@ -40,7 +40,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
@@ -75,7 +75,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,
@@ -145,7 +145,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
@@ -208,7 +208,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/search.rb b/lib/api/search.rb
index ed52a4fc8f2..e685f2c4afe 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 09644d42e8f..0849a0ff945 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'
@@ -121,12 +122,12 @@ 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 :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 b89de93af1b..905b1c4d52b 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 d008d1b9e97..cf951ba497e 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
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 5141d1fd499..7192c33a41f 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 02b8bb55274..8a054adf3b8 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 e1829403941..8590487cf71 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 c986414c223..c46c3a45514 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 192b06b8a1b..8740915caef 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 a2146406690..e13b5d4f1c5 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 do
def commit_params(attrs)
# In order to avoid service disruption this can work with an old workhorse without the acceleration
@@ -117,7 +117,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/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
index fa46114615c..73187401903 100644
--- a/lib/gitlab/ci/pipeline/chain/command.rb
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -76,6 +76,21 @@ module Gitlab
def parent_pipeline
bridge&.parent_pipeline
end
+
+ def duration_histogram
+ strong_memoize(:duration_histogram) do
+ name = :gitlab_ci_pipeline_creation_duration_seconds
+ comment = 'Pipeline creation duration'
+ labels = {}
+ buckets = [0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0, 20.0, 50.0, 240.0]
+
+ Gitlab::Metrics.histogram(name, comment, labels, buckets)
+ end
+ end
+
+ def observe_creation_duration(duration)
+ duration_histogram.observe({}, duration.seconds)
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/sequence.rb b/lib/gitlab/ci/pipeline/chain/sequence.rb
index 99780409085..a7c671e76d3 100644
--- a/lib/gitlab/ci/pipeline/chain/sequence.rb
+++ b/lib/gitlab/ci/pipeline/chain/sequence.rb
@@ -10,6 +10,7 @@ module Gitlab
@command = command
@sequence = sequence
@completed = []
+ @start = Time.now
end
def build!
@@ -24,6 +25,8 @@ module Gitlab
@pipeline.tap do
yield @pipeline, self if block_given?
+
+ @command.observe_creation_duration(Time.now - @start)
end
end
diff --git a/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml
index a7945e31a5d..479ca363ed3 100644
--- a/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml
@@ -230,7 +230,7 @@ analyzers/gemnasium-python:
license-management:
extends: .download_images
variables:
- SECURE_BINARIES_ANALYZER_VERSION: "${CI_SERVER_VERSION_MAJOR}-${CI_SERVER_VERSION_MINOR}-stable"
+ SECURE_BINARIES_ANALYZER_VERSION: "3"
# TODO: license-management is > 1GB, disabling for now
SECURE_BINARIES_SAVE_ARTIFACTS: "false"
only:
diff --git a/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml b/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml
index 83483108fde..a0832718214 100644
--- a/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml
@@ -19,7 +19,7 @@ cache:
- .terraform
before_script:
- - alias convert_report="jq -r '([.resource_changes[].change.actions?]|flatten)|{\"create\":(map(select(.==\"create\"))|length),\"update\":(map(select(.==\"update\"))|length),\"delete\":(map(select(.==\"delete\"))|length)}'"
+ - alias convert_report="jq -r '([.resource_changes[]?.change.actions?]|flatten)|{\"create\":(map(select(.==\"create\"))|length),\"update\":(map(select(.==\"update\"))|length),\"delete\":(map(select(.==\"delete\"))|length)}'"
- terraform --version
- terraform init
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index 082d93aa354..a22740ab9b7 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -82,15 +82,23 @@ module Gitlab
end
def non_restricted_level?(level)
+ !restricted_level?(level)
+ end
+
+ def restricted_level?(level)
restricted_levels = Gitlab::CurrentSettings.restricted_visibility_levels
if restricted_levels.nil?
- true
+ false
else
- !restricted_levels.include?(level)
+ restricted_levels.include?(level)
end
end
+ def public_visibility_restricted?
+ restricted_level?(PUBLIC)
+ end
+
def valid_level?(level)
options.value?(level)
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 87736edd036..edf43da4b0c 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1716,9 +1716,6 @@ msgstr ""
msgid "AlertManagement|Status"
msgstr ""
-msgid "AlertManagement|Surface alerts in GitLab"
-msgstr ""
-
msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
msgstr ""
@@ -9521,10 +9518,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9566,6 +9563,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9581,6 +9581,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9641,6 +9644,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -20129,6 +20135,9 @@ msgstr ""
msgid "Support page URL"
msgstr ""
+msgid "Surface alerts in GitLab"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr ""
diff --git a/package.json b/package.json
index 5b630d0c5f4..df168d4290f 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"check-dependencies": "scripts/frontend/check_dependencies.sh",
"block-dependencies": "node scripts/frontend/block_dependencies.js",
"clean": "rm -rf public/assets tmp/cache/*-loader",
- "dev-server": "NODE_OPTIONS=\"--max-old-space-size=3584\" nodemon -w 'config/webpack.config.js' --exec 'webpack-dev-server --config config/webpack.config.js'",
+ "dev-server": "NODE_OPTIONS=\"--max-old-space-size=3584\" node scripts/frontend/webpack_dev_server.js",
"eslint": "eslint --cache --max-warnings 0 --report-unused-disable-directives --ext .js,.vue .",
"eslint-fix": "eslint --cache --max-warnings 0 --report-unused-disable-directives --ext .js,.vue --fix .",
"eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html --no-inline-config .",
@@ -213,4 +213,4 @@
"node": ">=10.13.0",
"yarn": "^1.10.0"
}
-}
+} \ No newline at end of file
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/rubocop/cop/gitlab/json.rb b/rubocop/cop/gitlab/json.rb
new file mode 100644
index 00000000000..8c9027223aa
--- /dev/null
+++ b/rubocop/cop/gitlab/json.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ module Gitlab
+ class Json < RuboCop::Cop::Cop
+ MSG_SEND = <<~EOL.freeze
+ Avoid calling `JSON` directly. Instead, use the `Gitlab::Json`
+ wrapper. This allows us to alter the JSON parser being used.
+ EOL
+
+ def_node_matcher :json_node?, <<~PATTERN
+ (send (const nil? :JSON)...)
+ PATTERN
+
+ def on_send(node)
+ add_offense(node, location: :expression, message: MSG_SEND) if json_node?(node)
+ end
+
+ def autocorrect(node)
+ autocorrect_json_node(node)
+ end
+
+ def autocorrect_json_node(node)
+ _, method_name, *arg_nodes = *node
+
+ replacement = "Gitlab::Json.#{method_name}(#{arg_nodes.map(&:source).join(', ')})"
+
+ lambda do |corrector|
+ corrector.replace(node.source_range, replacement)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/scripts/frontend/webpack_dev_server.js b/scripts/frontend/webpack_dev_server.js
new file mode 100755
index 00000000000..8026a8d47e2
--- /dev/null
+++ b/scripts/frontend/webpack_dev_server.js
@@ -0,0 +1,68 @@
+const nodemon = require('nodemon');
+
+const DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost';
+const DEV_SERVER_PORT = process.env.DEV_SERVER_PORT || '3808';
+const STATIC_MODE = process.env.DEV_SERVER_STATIC && process.env.DEV_SERVER_STATIC != 'false';
+const DLL_MODE = process.env.WEBPACK_VENDOR_DLL && process.env.WEBPACK_VENDOR_DLL != 'false';
+
+const baseConfig = {
+ ignoreRoot: ['.git', 'node_modules/*/'],
+ noUpdateNotifier: true,
+ signal: 'SIGTERM',
+ delay: 1000,
+};
+
+// run webpack in compile-once mode and watch for changes
+if (STATIC_MODE) {
+ nodemon({
+ exec: `rm -rf public/assets/webpack ; yarn run webpack && exec ruby -run -e httpd public/ -p ${DEV_SERVER_PORT}`,
+ watch: [
+ 'config/webpack.config.js',
+ 'app/assets/javascripts',
+ 'ee/app/assets/javascripts',
+ // ensure we refresh when running yarn install
+ 'node_modules/.yarn-integrity',
+ ],
+ ext: 'js,json,vue',
+ ...baseConfig,
+ });
+}
+
+// run webpack through webpack-dev-server, optionally compiling a DLL to reduce memory
+else {
+ let watch = ['config/webpack.config.js'];
+
+ // if utilizing the vendor DLL, we need to restart the process when dependency changes occur
+ if (DLL_MODE) {
+ watch.push(
+ 'config/webpack.vendor.config.js',
+ // ensure we refresh when running yarn install
+ 'node_modules/.yarn-integrity',
+ 'package.json',
+ 'yarn.lock',
+ );
+ }
+ nodemon({
+ exec: 'webpack-dev-server --config config/webpack.config.js',
+ watch,
+ ...baseConfig,
+ });
+}
+
+// print useful messages for nodemon events
+nodemon
+ .on('start', function() {
+ console.log(`Starting webpack webserver on http://${DEV_SERVER_HOST}:${DEV_SERVER_PORT}`);
+ if (STATIC_MODE) {
+ console.log('You are starting webpack in compile-once mode');
+ console.log('The JavaScript assets are recompiled only if they change');
+ console.log('If you change them often, you might want to unset DEV_SERVER_STATIC');
+ }
+ })
+ .on('quit', function() {
+ console.log('Shutting down webpack process');
+ process.exit();
+ })
+ .on('restart', function(files) {
+ console.log('Restarting webpack process due to: ', files);
+ });
diff --git a/spec/factories/identities.rb b/spec/factories/identities.rb
index fda4bfa589b..a2615ce30c3 100644
--- a/spec/factories/identities.rb
+++ b/spec/factories/identities.rb
@@ -3,6 +3,6 @@
FactoryBot.define do
factory :identity do
provider { 'ldapmain' }
- sequence(:extern_uid) { |n| "my-ldap-id-#{n}" }
+ extern_uid { 'my-ldap-id' }
end
end
diff --git a/spec/frontend/alert_management/components/alert_management_list_spec.js b/spec/frontend/alert_management/components/alert_management_list_spec.js
index 9753300d035..c18c2ec0d53 100644
--- a/spec/frontend/alert_management/components/alert_management_list_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_list_spec.js
@@ -12,7 +12,10 @@ describe('AlertManagementList', () => {
function mountComponent({
stubs = {},
- props = { alertManagementEnabled: false },
+ props = {
+ alertManagementEnabled: false,
+ userCanEnableAlertManagement: false,
+ },
data = {},
loading = false,
} = {}) {
@@ -62,7 +65,7 @@ describe('AlertManagementList', () => {
it('loading state', () => {
mountComponent({
stubs: { GlTable },
- props: { alertManagementEnabled: true },
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: null },
loading: true,
});
@@ -73,7 +76,7 @@ describe('AlertManagementList', () => {
it('error state', () => {
mountComponent({
stubs: { GlTable },
- props: { alertManagementEnabled: true },
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: null, errored: true },
loading: false,
});
@@ -86,7 +89,7 @@ describe('AlertManagementList', () => {
it('empty state', () => {
mountComponent({
stubs: { GlTable },
- props: { alertManagementEnabled: true },
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: [], errored: false },
loading: false,
});
diff --git a/spec/frontend/boards/boards_store_spec.js b/spec/frontend/boards/boards_store_spec.js
index 5c5315fd465..05a44138275 100644
--- a/spec/frontend/boards/boards_store_spec.js
+++ b/spec/frontend/boards/boards_store_spec.js
@@ -1040,5 +1040,66 @@ describe('boardsStore', () => {
});
});
});
+
+ describe('updateIssue', () => {
+ let issue;
+ let patchSpy;
+
+ beforeEach(() => {
+ issue = new ListIssue({
+ title: 'Testing',
+ id: 1,
+ iid: 1,
+ confidential: false,
+ labels: [
+ {
+ id: 1,
+ title: 'test',
+ color: 'red',
+ description: 'testing',
+ },
+ ],
+ assignees: [
+ {
+ id: 1,
+ name: 'name',
+ username: 'username',
+ avatar_url: 'http://avatar_url',
+ },
+ ],
+ real_path: 'path/to/issue',
+ });
+
+ patchSpy = jest.fn().mockReturnValue([200, { labels: [] }]);
+ axiosMock.onPatch(`path/to/issue.json`).reply(({ data }) => patchSpy(JSON.parse(data)));
+ });
+
+ it('passes assignee ids when there are assignees', () => {
+ boardsStore.updateIssue(issue);
+ return boardsStore.updateIssue(issue).then(() => {
+ expect(patchSpy).toHaveBeenCalledWith({
+ issue: {
+ milestone_id: null,
+ assignee_ids: [1],
+ label_ids: [1],
+ },
+ });
+ });
+ });
+
+ it('passes assignee ids of [0] when there are no assignees', () => {
+ issue.removeAllAssignees();
+
+ return boardsStore.updateIssue(issue).then(() => {
+ expect(patchSpy).toHaveBeenCalledWith({
+ issue: {
+ milestone_id: null,
+ assignee_ids: [0],
+ label_ids: [1],
+ },
+ });
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/boards/issue_spec.js b/spec/frontend/boards/issue_spec.js
index ff72edaa695..412f20684f5 100644
--- a/spec/frontend/boards/issue_spec.js
+++ b/spec/frontend/boards/issue_spec.js
@@ -1,6 +1,5 @@
/* global ListIssue */
-import axios from '~/lib/utils/axios_utils';
import '~/boards/models/label';
import '~/boards/models/assignee';
import '~/boards/models/issue';
@@ -173,25 +172,12 @@ describe('Issue model', () => {
});
describe('update', () => {
- it('passes assignee ids when there are assignees', done => {
- jest.spyOn(axios, 'patch').mockImplementation((url, data) => {
- expect(data.issue.assignee_ids).toEqual([1]);
- done();
- return Promise.resolve();
- });
-
- issue.update('url');
- });
+ it('passes update to boardsStore', () => {
+ jest.spyOn(boardsStore, 'updateIssue').mockImplementation();
- it('passes assignee ids of [0] when there are no assignees', done => {
- jest.spyOn(axios, 'patch').mockImplementation((url, data) => {
- expect(data.issue.assignee_ids).toEqual([0]);
- done();
- return Promise.resolve();
- });
+ issue.update();
- issue.removeAllAssignees();
- issue.update('url');
+ expect(boardsStore.updateIssue).toHaveBeenCalledWith(issue);
});
});
});
diff --git a/spec/frontend/deploy_keys/components/action_btn_spec.js b/spec/frontend/deploy_keys/components/action_btn_spec.js
new file mode 100644
index 00000000000..b8211b02464
--- /dev/null
+++ b/spec/frontend/deploy_keys/components/action_btn_spec.js
@@ -0,0 +1,54 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import eventHub from '~/deploy_keys/eventhub';
+import actionBtn from '~/deploy_keys/components/action_btn.vue';
+
+describe('Deploy keys action btn', () => {
+ const data = getJSONFixture('deploy_keys/keys.json');
+ const deployKey = data.enabled_keys[0];
+ let wrapper;
+
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+
+ beforeEach(() => {
+ wrapper = shallowMount(actionBtn, {
+ propsData: {
+ deployKey,
+ type: 'enable',
+ },
+ slots: {
+ default: 'Enable',
+ },
+ });
+ });
+
+ it('renders the default slot', () => {
+ expect(wrapper.text()).toBe('Enable');
+ });
+
+ it('sends eventHub event with btn type', () => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+
+ wrapper.trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('enable.key', deployKey, expect.anything());
+ });
+ });
+
+ it('shows loading spinner after click', () => {
+ wrapper.trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+
+ it('disables button after click', () => {
+ wrapper.trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.attributes('disabled')).toBe('disabled');
+ });
+ });
+});
diff --git a/spec/frontend/deploy_keys/components/app_spec.js b/spec/frontend/deploy_keys/components/app_spec.js
new file mode 100644
index 00000000000..291502c9ed7
--- /dev/null
+++ b/spec/frontend/deploy_keys/components/app_spec.js
@@ -0,0 +1,142 @@
+import { mount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'spec/test_constants';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
+import eventHub from '~/deploy_keys/eventhub';
+import deployKeysApp from '~/deploy_keys/components/app.vue';
+
+const TEST_ENDPOINT = `${TEST_HOST}/dummy/`;
+
+describe('Deploy keys app component', () => {
+ const data = getJSONFixture('deploy_keys/keys.json');
+ let wrapper;
+ let mock;
+
+ const mountComponent = () => {
+ wrapper = mount(deployKeysApp, {
+ propsData: {
+ endpoint: TEST_ENDPOINT,
+ projectId: '8',
+ },
+ });
+
+ return waitForPromises();
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet(TEST_ENDPOINT).reply(200, data);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ const findLoadingIcon = () => wrapper.find('.gl-spinner');
+ const findKeyPanels = () => wrapper.findAll('.deploy-keys .nav-links li');
+
+ it('renders loading icon while waiting for request', () => {
+ mock.onGet(TEST_ENDPOINT).reply(() => new Promise());
+
+ mountComponent();
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+
+ it('renders keys panels', () => {
+ return mountComponent().then(() => {
+ expect(findKeyPanels().length).toBe(3);
+ });
+ });
+
+ it.each`
+ selector | label | count
+ ${'.js-deployKeys-tab-enabled_keys'} | ${'Enabled deploy keys'} | ${1}
+ ${'.js-deployKeys-tab-available_project_keys'} | ${'Privately accessible deploy keys'} | ${0}
+ ${'.js-deployKeys-tab-public_keys'} | ${'Publicly accessible deploy keys'} | ${1}
+ `('$selector title is $label with keys count equal to $count', ({ selector, label, count }) => {
+ return mountComponent().then(() => {
+ const element = wrapper.find(selector);
+ expect(element.exists()).toBe(true);
+ expect(element.text().trim()).toContain(label);
+
+ expect(
+ element
+ .find('.badge')
+ .text()
+ .trim(),
+ ).toBe(count.toString());
+ });
+ });
+
+ it('does not render key panels when keys object is empty', () => {
+ mock.onGet(TEST_ENDPOINT).reply(200, []);
+
+ return mountComponent().then(() => {
+ expect(findKeyPanels().length).toBe(0);
+ });
+ });
+
+ it('re-fetches deploy keys when enabling a key', () => {
+ const key = data.public_keys[0];
+ return mountComponent()
+ .then(() => {
+ jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {});
+ jest.spyOn(wrapper.vm.service, 'enableKey').mockImplementation(() => Promise.resolve());
+
+ eventHub.$emit('enable.key', key);
+
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ expect(wrapper.vm.service.enableKey).toHaveBeenCalledWith(key.id);
+ expect(wrapper.vm.service.getKeys).toHaveBeenCalled();
+ });
+ });
+
+ it('re-fetches deploy keys when disabling a key', () => {
+ const key = data.public_keys[0];
+ return mountComponent()
+ .then(() => {
+ jest.spyOn(window, 'confirm').mockReturnValue(true);
+ jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {});
+ jest.spyOn(wrapper.vm.service, 'disableKey').mockImplementation(() => Promise.resolve());
+
+ eventHub.$emit('disable.key', key);
+
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ expect(wrapper.vm.service.disableKey).toHaveBeenCalledWith(key.id);
+ expect(wrapper.vm.service.getKeys).toHaveBeenCalled();
+ });
+ });
+
+ it('calls disableKey when removing a key', () => {
+ const key = data.public_keys[0];
+ return mountComponent()
+ .then(() => {
+ jest.spyOn(window, 'confirm').mockReturnValue(true);
+ jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {});
+ jest.spyOn(wrapper.vm.service, 'disableKey').mockImplementation(() => Promise.resolve());
+
+ eventHub.$emit('remove.key', key);
+
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ expect(wrapper.vm.service.disableKey).toHaveBeenCalledWith(key.id);
+ expect(wrapper.vm.service.getKeys).toHaveBeenCalled();
+ });
+ });
+
+ it('hasKeys returns true when there are keys', () => {
+ return mountComponent().then(() => {
+ expect(wrapper.vm.hasKeys).toEqual(3);
+ });
+ });
+});
diff --git a/spec/frontend/deploy_keys/components/key_spec.js b/spec/frontend/deploy_keys/components/key_spec.js
new file mode 100644
index 00000000000..7d942d969bb
--- /dev/null
+++ b/spec/frontend/deploy_keys/components/key_spec.js
@@ -0,0 +1,161 @@
+import { mount } from '@vue/test-utils';
+import DeployKeysStore from '~/deploy_keys/store';
+import key from '~/deploy_keys/components/key.vue';
+import { getTimeago } from '~/lib/utils/datetime_utility';
+
+describe('Deploy keys key', () => {
+ let wrapper;
+ let store;
+
+ const data = getJSONFixture('deploy_keys/keys.json');
+
+ const findTextAndTrim = selector =>
+ wrapper
+ .find(selector)
+ .text()
+ .trim();
+
+ const createComponent = propsData => {
+ wrapper = mount(key, {
+ propsData: {
+ store,
+ endpoint: 'https://test.host/dummy/endpoint',
+ ...propsData,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ store = new DeployKeysStore();
+ store.keys = data;
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('enabled key', () => {
+ const deployKey = data.enabled_keys[0];
+
+ it('renders the keys title', () => {
+ createComponent({ deployKey });
+
+ expect(findTextAndTrim('.title')).toContain('My title');
+ });
+
+ it('renders human friendly formatted created date', () => {
+ createComponent({ deployKey });
+
+ expect(findTextAndTrim('.key-created-at')).toBe(
+ `${getTimeago().format(deployKey.created_at)}`,
+ );
+ });
+
+ it('shows pencil button for editing', () => {
+ createComponent({ deployKey });
+
+ expect(wrapper.find('.btn .ic-pencil')).toExist();
+ });
+
+ it('shows disable button when the project is not deletable', () => {
+ createComponent({ deployKey });
+
+ expect(wrapper.find('.btn .ic-cancel')).toExist();
+ });
+
+ it('shows remove button when the project is deletable', () => {
+ createComponent({
+ deployKey: { ...deployKey, destroyed_when_orphaned: true, almost_orphaned: true },
+ });
+ expect(wrapper.find('.btn .ic-remove')).toExist();
+ });
+ });
+
+ describe('deploy key labels', () => {
+ const deployKey = data.enabled_keys[0];
+ const deployKeysProjects = [...deployKey.deploy_keys_projects];
+ it('shows write access title when key has write access', () => {
+ deployKeysProjects[0] = { ...deployKeysProjects[0], can_push: true };
+ createComponent({ deployKey: { ...deployKey, deploy_keys_projects: deployKeysProjects } });
+
+ expect(wrapper.find('.deploy-project-label').attributes('data-original-title')).toBe(
+ 'Write access allowed',
+ );
+ });
+
+ it('does not show write access title when key has write access', () => {
+ deployKeysProjects[0] = { ...deployKeysProjects[0], can_push: false };
+ createComponent({ deployKey: { ...deployKey, deploy_keys_projects: deployKeysProjects } });
+
+ expect(wrapper.find('.deploy-project-label').attributes('data-original-title')).toBe(
+ 'Read access only',
+ );
+ });
+
+ it('shows expandable button if more than two projects', () => {
+ createComponent({ deployKey });
+ const labels = wrapper.findAll('.deploy-project-label');
+
+ expect(labels.length).toBe(2);
+ expect(labels.at(1).text()).toContain('others');
+ expect(labels.at(1).attributes('data-original-title')).toContain('Expand');
+ });
+
+ it('expands all project labels after click', () => {
+ createComponent({ deployKey });
+ const { length } = deployKey.deploy_keys_projects;
+ wrapper
+ .findAll('.deploy-project-label')
+ .at(1)
+ .trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ const labels = wrapper.findAll('.deploy-project-label');
+
+ expect(labels.length).toBe(length);
+ expect(labels.at(1).text()).not.toContain(`+${length} others`);
+ expect(labels.at(1).attributes('data-original-title')).not.toContain('Expand');
+ });
+ });
+
+ it('shows two projects', () => {
+ createComponent({
+ deployKey: { ...deployKey, deploy_keys_projects: [...deployKeysProjects].slice(0, 2) },
+ });
+
+ const labels = wrapper.findAll('.deploy-project-label');
+
+ expect(labels.length).toBe(2);
+ expect(labels.at(1).text()).toContain(deployKey.deploy_keys_projects[1].project.full_name);
+ });
+ });
+
+ describe('public keys', () => {
+ const deployKey = data.public_keys[0];
+
+ it('renders deploy keys without any enabled projects', () => {
+ createComponent({ deployKey: { ...deployKey, deploy_keys_projects: [] } });
+
+ expect(findTextAndTrim('.deploy-project-list')).toBe('None');
+ });
+
+ it('shows enable button', () => {
+ createComponent({ deployKey });
+ expect(findTextAndTrim('.btn')).toBe('Enable');
+ });
+
+ it('shows pencil button for editing', () => {
+ createComponent({ deployKey });
+ expect(wrapper.find('.btn .ic-pencil')).toExist();
+ });
+
+ it('shows disable button when key is enabled', () => {
+ store.keys.enabled_keys.push(deployKey);
+
+ createComponent({ deployKey });
+
+ expect(wrapper.find('.btn .ic-cancel')).toExist();
+ });
+ });
+});
diff --git a/spec/frontend/deploy_keys/components/keys_panel_spec.js b/spec/frontend/deploy_keys/components/keys_panel_spec.js
new file mode 100644
index 00000000000..53c8ba073bc
--- /dev/null
+++ b/spec/frontend/deploy_keys/components/keys_panel_spec.js
@@ -0,0 +1,63 @@
+import { mount } from '@vue/test-utils';
+import DeployKeysStore from '~/deploy_keys/store';
+import deployKeysPanel from '~/deploy_keys/components/keys_panel.vue';
+
+describe('Deploy keys panel', () => {
+ const data = getJSONFixture('deploy_keys/keys.json');
+ let wrapper;
+
+ const findTableRowHeader = () => wrapper.find('.table-row-header');
+
+ const mountComponent = props => {
+ const store = new DeployKeysStore();
+ store.keys = data;
+ wrapper = mount(deployKeysPanel, {
+ propsData: {
+ title: 'test',
+ keys: data.enabled_keys,
+ showHelpBox: true,
+ store,
+ endpoint: 'https://test.host/dummy/endpoint',
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders list of keys', () => {
+ mountComponent();
+ expect(wrapper.findAll('.deploy-key').length).toBe(wrapper.vm.keys.length);
+ });
+
+ it('renders table header', () => {
+ mountComponent();
+ const tableHeader = findTableRowHeader();
+
+ expect(tableHeader).toExist();
+ expect(tableHeader.text()).toContain('Deploy key');
+ expect(tableHeader.text()).toContain('Project usage');
+ expect(tableHeader.text()).toContain('Created');
+ });
+
+ it('renders help box if keys are empty', () => {
+ mountComponent({ keys: [] });
+
+ expect(wrapper.find('.settings-message').exists()).toBe(true);
+
+ expect(
+ wrapper
+ .find('.settings-message')
+ .text()
+ .trim(),
+ ).toBe('No deploy keys found. Create one with the form above.');
+ });
+
+ it('renders no table header if keys are empty', () => {
+ mountComponent({ keys: [] });
+ expect(findTableRowHeader().exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/dirty_submit/dirty_submit_collection_spec.js b/spec/frontend/dirty_submit/dirty_submit_collection_spec.js
new file mode 100644
index 00000000000..170d581be23
--- /dev/null
+++ b/spec/frontend/dirty_submit/dirty_submit_collection_spec.js
@@ -0,0 +1,22 @@
+import DirtySubmitCollection from '~/dirty_submit/dirty_submit_collection';
+import { setInputValue, createForm } from './helper';
+
+jest.mock('lodash/throttle', () => jest.fn(fn => fn));
+
+describe('DirtySubmitCollection', () => {
+ const testElementsCollection = [createForm(), createForm()];
+ const forms = testElementsCollection.map(testElements => testElements.form);
+
+ new DirtySubmitCollection(forms); // eslint-disable-line no-new
+
+ it.each(testElementsCollection)('disables submits until there are changes', testElements => {
+ const { input, submit } = testElements;
+ const originalValue = input.value;
+
+ expect(submit.disabled).toBe(true);
+ setInputValue(input, `${originalValue} changes`);
+ expect(submit.disabled).toBe(false);
+ setInputValue(input, originalValue);
+ expect(submit.disabled).toBe(true);
+ });
+});
diff --git a/spec/javascripts/dirty_submit/dirty_submit_factory_spec.js b/spec/frontend/dirty_submit/dirty_submit_factory_spec.js
index 40843a68582..40843a68582 100644
--- a/spec/javascripts/dirty_submit/dirty_submit_factory_spec.js
+++ b/spec/frontend/dirty_submit/dirty_submit_factory_spec.js
diff --git a/spec/javascripts/dirty_submit/dirty_submit_form_spec.js b/spec/frontend/dirty_submit/dirty_submit_form_spec.js
index 42f806fa1bf..d7f690df1f3 100644
--- a/spec/javascripts/dirty_submit/dirty_submit_form_spec.js
+++ b/spec/frontend/dirty_submit/dirty_submit_form_spec.js
@@ -1,95 +1,78 @@
-import { range as rge } from 'lodash';
+import { range as rge, throttle } from 'lodash';
import DirtySubmitForm from '~/dirty_submit/dirty_submit_form';
import { getInputValue, setInputValue, createForm } from './helper';
+jest.mock('lodash/throttle', () => jest.fn(fn => fn));
+const lodash = jest.requireActual('lodash');
+
function expectToToggleDisableOnDirtyUpdate(submit, input) {
const originalValue = getInputValue(input);
expect(submit.disabled).toBe(true);
- return setInputValue(input, `${originalValue} changes`)
- .then(() => expect(submit.disabled).toBe(false))
- .then(() => setInputValue(input, originalValue))
- .then(() => expect(submit.disabled).toBe(true));
+ setInputValue(input, `${originalValue} changes`);
+ expect(submit.disabled).toBe(false);
+ setInputValue(input, originalValue);
+ expect(submit.disabled).toBe(true);
}
describe('DirtySubmitForm', () => {
- const originalThrottleDuration = DirtySubmitForm.THROTTLE_DURATION;
-
describe('submit button tests', () => {
- beforeEach(() => {
- DirtySubmitForm.THROTTLE_DURATION = 0;
- });
-
- afterEach(() => {
- DirtySubmitForm.THROTTLE_DURATION = originalThrottleDuration;
- });
-
- it('disables submit until there are changes', done => {
+ it('disables submit until there are changes', () => {
const { form, input, submit } = createForm();
new DirtySubmitForm(form); // eslint-disable-line no-new
- return expectToToggleDisableOnDirtyUpdate(submit, input)
- .then(done)
- .catch(done.fail);
+ expectToToggleDisableOnDirtyUpdate(submit, input);
});
- it('disables submit until there are changes when initializing with a falsy value', done => {
+ it('disables submit until there are changes when initializing with a falsy value', () => {
const { form, input, submit } = createForm();
input.value = '';
new DirtySubmitForm(form); // eslint-disable-line no-new
- return expectToToggleDisableOnDirtyUpdate(submit, input)
- .then(done)
- .catch(done.fail);
+ expectToToggleDisableOnDirtyUpdate(submit, input);
});
- it('disables submit until there are changes for radio inputs', done => {
+ it('disables submit until there are changes for radio inputs', () => {
const { form, input, submit } = createForm('radio');
new DirtySubmitForm(form); // eslint-disable-line no-new
- return expectToToggleDisableOnDirtyUpdate(submit, input)
- .then(done)
- .catch(done.fail);
+ expectToToggleDisableOnDirtyUpdate(submit, input);
});
- it('disables submit until there are changes for checkbox inputs', done => {
+ it('disables submit until there are changes for checkbox inputs', () => {
const { form, input, submit } = createForm('checkbox');
new DirtySubmitForm(form); // eslint-disable-line no-new
- return expectToToggleDisableOnDirtyUpdate(submit, input)
- .then(done)
- .catch(done.fail);
+ expectToToggleDisableOnDirtyUpdate(submit, input);
});
});
describe('throttling tests', () => {
beforeEach(() => {
- jasmine.clock().install();
- jasmine.clock().mockDate();
- DirtySubmitForm.THROTTLE_DURATION = 100;
+ throttle.mockImplementation(lodash.throttle);
+ jest.useFakeTimers();
});
afterEach(() => {
- jasmine.clock().uninstall();
- DirtySubmitForm.THROTTLE_DURATION = originalThrottleDuration;
+ throttle.mockReset();
});
it('throttles updates when rapid changes are made to a single form element', () => {
const { form, input } = createForm();
- const updateDirtyInputSpy = spyOn(new DirtySubmitForm(form), 'updateDirtyInput');
+ const updateDirtyInputSpy = jest.spyOn(new DirtySubmitForm(form), 'updateDirtyInput');
rge(10).forEach(i => {
setInputValue(input, `change ${i}`, false);
});
- jasmine.clock().tick(101);
+ jest.runOnlyPendingTimers();
- expect(updateDirtyInputSpy).toHaveBeenCalledTimes(2);
+ expect(updateDirtyInputSpy).toHaveBeenCalledTimes(1);
});
it('does not throttle updates when rapid changes are made to different form elements', () => {
@@ -99,14 +82,14 @@ describe('DirtySubmitForm', () => {
form.innerHTML += `<input type="text" name="input-${i}" class="js-input-${i}"/>`;
});
- const updateDirtyInputSpy = spyOn(new DirtySubmitForm(form), 'updateDirtyInput');
+ const updateDirtyInputSpy = jest.spyOn(new DirtySubmitForm(form), 'updateDirtyInput');
range.forEach(i => {
const input = form.querySelector(`.js-input-${i}`);
setInputValue(input, `change`, false);
});
- jasmine.clock().tick(101);
+ jest.runOnlyPendingTimers();
expect(updateDirtyInputSpy).toHaveBeenCalledTimes(range.length);
});
diff --git a/spec/javascripts/dirty_submit/helper.js b/spec/frontend/dirty_submit/helper.js
index b51783cb915..c02512b7671 100644
--- a/spec/javascripts/dirty_submit/helper.js
+++ b/spec/frontend/dirty_submit/helper.js
@@ -1,6 +1,3 @@
-import DirtySubmitForm from '~/dirty_submit/dirty_submit_form';
-import setTimeoutPromiseHelper from '../helpers/set_timeout_promise_helper';
-
function isCheckableType(type) {
return /^(radio|checkbox)$/.test(type);
}
@@ -22,8 +19,6 @@ export function setInputValue(element, value) {
bubbles: true,
}),
);
-
- return setTimeoutPromiseHelper(DirtySubmitForm.THROTTLE_DURATION);
}
export function getInputValue(input) {
diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js
new file mode 100644
index 00000000000..f876987cd88
--- /dev/null
+++ b/spec/frontend/pipelines/mock_data.js
@@ -0,0 +1,423 @@
+export const pipelineWithStages = {
+ id: 20333396,
+ user: {
+ id: 128633,
+ name: 'Rémy Coutable',
+ username: 'rymai',
+ state: 'active',
+ avatar_url:
+ 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon',
+ web_url: 'https://gitlab.com/rymai',
+ path: '/rymai',
+ },
+ active: true,
+ coverage: '58.24',
+ source: 'push',
+ created_at: '2018-04-11T14:04:53.881Z',
+ updated_at: '2018-04-11T14:05:00.792Z',
+ path: '/gitlab-org/gitlab/pipelines/20333396',
+ flags: {
+ latest: true,
+ stuck: false,
+ auto_devops: false,
+ yaml_errors: false,
+ retryable: false,
+ cancelable: true,
+ failure_reason: false,
+ },
+ details: {
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab/pipelines/20333396',
+ favicon:
+ 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_running-2eb56be2871937954b2ba6d6f4ee9fdf7e5e1c146ac45f7be98119ccaca1aca9.ico',
+ },
+ duration: null,
+ finished_at: null,
+ stages: [
+ {
+ name: 'build',
+ title: 'build: skipped',
+ status: {
+ icon: 'status_skipped',
+ text: 'skipped',
+ label: 'skipped',
+ group: 'skipped',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab/pipelines/20333396#build',
+ favicon:
+ 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_skipped-a2eee568a5bffdb494050c7b62dde241de9189280836288ac8923d369f16222d.ico',
+ },
+ path: '/gitlab-org/gitlab/pipelines/20333396#build',
+ dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=build',
+ },
+ {
+ name: 'prepare',
+ title: 'prepare: passed',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab/pipelines/20333396#prepare',
+ favicon:
+ 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_success-26f59841becbef8c6fe414e9e74471d8bfd6a91b5855c19fe7f5923a40a7da47.ico',
+ },
+ path: '/gitlab-org/gitlab/pipelines/20333396#prepare',
+ dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=prepare',
+ },
+ {
+ name: 'test',
+ title: 'test: running',
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab/pipelines/20333396#test',
+ favicon:
+ 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_running-2eb56be2871937954b2ba6d6f4ee9fdf7e5e1c146ac45f7be98119ccaca1aca9.ico',
+ },
+ path: '/gitlab-org/gitlab/pipelines/20333396#test',
+ dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=test',
+ },
+ {
+ name: 'post-test',
+ title: 'post-test: created',
+ status: {
+ icon: 'status_created',
+ text: 'created',
+ label: 'created',
+ group: 'created',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab/pipelines/20333396#post-test',
+ favicon:
+ 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico',
+ },
+ path: '/gitlab-org/gitlab/pipelines/20333396#post-test',
+ dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=post-test',
+ },
+ {
+ name: 'pages',
+ title: 'pages: created',
+ status: {
+ icon: 'status_created',
+ text: 'created',
+ label: 'created',
+ group: 'created',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab/pipelines/20333396#pages',
+ favicon:
+ 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico',
+ },
+ path: '/gitlab-org/gitlab/pipelines/20333396#pages',
+ dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=pages',
+ },
+ {
+ name: 'post-cleanup',
+ title: 'post-cleanup: created',
+ status: {
+ icon: 'status_created',
+ text: 'created',
+ label: 'created',
+ group: 'created',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab/pipelines/20333396#post-cleanup',
+ favicon:
+ 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico',
+ },
+ path: '/gitlab-org/gitlab/pipelines/20333396#post-cleanup',
+ dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=post-cleanup',
+ },
+ ],
+ artifacts: [
+ {
+ name: 'gitlab:assets:compile',
+ expired: false,
+ expire_at: '2018-05-12T14:22:54.730Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411438/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411438/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411438/artifacts/browse',
+ },
+ {
+ name: 'rspec-mysql 12 28',
+ expired: false,
+ expire_at: '2018-05-12T14:22:45.136Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411397/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411397/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411397/artifacts/browse',
+ },
+ {
+ name: 'rspec-mysql 6 28',
+ expired: false,
+ expire_at: '2018-05-12T14:22:41.523Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411391/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411391/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411391/artifacts/browse',
+ },
+ {
+ name: 'rspec-pg geo 0 1',
+ expired: false,
+ expire_at: '2018-05-12T14:22:13.287Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411353/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411353/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411353/artifacts/browse',
+ },
+ {
+ name: 'rspec-mysql 0 28',
+ expired: false,
+ expire_at: '2018-05-12T14:22:06.834Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411385/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411385/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411385/artifacts/browse',
+ },
+ {
+ name: 'spinach-mysql 0 2',
+ expired: false,
+ expire_at: '2018-05-12T14:21:51.409Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411423/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411423/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411423/artifacts/browse',
+ },
+ {
+ name: 'karma',
+ expired: false,
+ expire_at: '2018-05-12T14:21:20.934Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411440/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411440/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411440/artifacts/browse',
+ },
+ {
+ name: 'spinach-pg 0 2',
+ expired: false,
+ expire_at: '2018-05-12T14:20:01.028Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411419/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411419/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411419/artifacts/browse',
+ },
+ {
+ name: 'spinach-pg 1 2',
+ expired: false,
+ expire_at: '2018-05-12T14:19:04.336Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411421/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411421/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411421/artifacts/browse',
+ },
+ {
+ name: 'sast',
+ expired: null,
+ expire_at: null,
+ path: '/gitlab-org/gitlab/-/jobs/62411442/artifacts/download',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411442/artifacts/browse',
+ },
+ {
+ name: 'code_quality',
+ expired: false,
+ expire_at: '2018-04-18T14:16:24.484Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411441/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411441/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411441/artifacts/browse',
+ },
+ {
+ name: 'cache gems',
+ expired: null,
+ expire_at: null,
+ path: '/gitlab-org/gitlab/-/jobs/62411447/artifacts/download',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411447/artifacts/browse',
+ },
+ {
+ name: 'dependency_scanning',
+ expired: null,
+ expire_at: null,
+ path: '/gitlab-org/gitlab/-/jobs/62411443/artifacts/download',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411443/artifacts/browse',
+ },
+ {
+ name: 'compile-assets',
+ expired: false,
+ expire_at: '2018-04-18T14:12:07.638Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411334/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411334/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411334/artifacts/browse',
+ },
+ {
+ name: 'setup-test-env',
+ expired: false,
+ expire_at: '2018-04-18T14:10:27.024Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411336/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411336/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411336/artifacts/browse',
+ },
+ {
+ name: 'retrieve-tests-metadata',
+ expired: false,
+ expire_at: '2018-05-12T14:06:35.926Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411333/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411333/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411333/artifacts/browse',
+ },
+ ],
+ manual_actions: [
+ {
+ name: 'package-and-qa',
+ path: '/gitlab-org/gitlab/-/jobs/62411330/play',
+ playable: true,
+ },
+ {
+ name: 'review-docs-deploy',
+ path: '/gitlab-org/gitlab/-/jobs/62411332/play',
+ playable: true,
+ },
+ ],
+ },
+ ref: {
+ name: 'master',
+ path: '/gitlab-org/gitlab/commits/master',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: 'e6a2885c503825792cb8a84a8731295e361bd059',
+ short_id: 'e6a2885c',
+ title: "Merge branch 'ce-to-ee-2018-04-11' into 'master'",
+ created_at: '2018-04-11T14:04:39.000Z',
+ parent_ids: [
+ '5d9b5118f6055f72cff1a82b88133609912f2c1d',
+ '6fdc6ee76a8062fe41b1a33f7c503334a6ebdc02',
+ ],
+ message:
+ "Merge branch 'ce-to-ee-2018-04-11' into 'master'\n\nCE upstream - 2018-04-11 12:26 UTC\n\nSee merge request gitlab-org/gitlab-ee!5326",
+ author_name: 'Rémy Coutable',
+ author_email: 'remy@rymai.me',
+ authored_date: '2018-04-11T14:04:39.000Z',
+ committer_name: 'Rémy Coutable',
+ committer_email: 'remy@rymai.me',
+ committed_date: '2018-04-11T14:04:39.000Z',
+ author: {
+ id: 128633,
+ name: 'Rémy Coutable',
+ username: 'rymai',
+ state: 'active',
+ avatar_url:
+ 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon',
+ web_url: 'https://gitlab.com/rymai',
+ path: '/rymai',
+ },
+ author_gravatar_url:
+ 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon',
+ commit_url:
+ 'https://gitlab.com/gitlab-org/gitlab/commit/e6a2885c503825792cb8a84a8731295e361bd059',
+ commit_path: '/gitlab-org/gitlab/commit/e6a2885c503825792cb8a84a8731295e361bd059',
+ },
+ cancel_path: '/gitlab-org/gitlab/pipelines/20333396/cancel',
+ triggered_by: null,
+ triggered: [],
+};
+
+export const stageReply = {
+ name: 'deploy',
+ title: 'deploy: running',
+ latest_statuses: [
+ {
+ id: 928,
+ name: 'stop staging',
+ started: false,
+ build_path: '/twitter/flight/-/jobs/928',
+ cancel_path: '/twitter/flight/-/jobs/928/cancel',
+ playable: false,
+ created_at: '2018-04-04T20:02:02.728Z',
+ updated_at: '2018-04-04T20:02:02.766Z',
+ status: {
+ icon: 'status_pending',
+ text: 'pending',
+ label: 'pending',
+ group: 'pending',
+ tooltip: 'pending',
+ has_details: true,
+ details_path: '/twitter/flight/-/jobs/928',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_pending-db32e1faf94b9f89530ac519790920d1f18ea8f6af6cd2e0a26cd6840cacf101.ico',
+ action: {
+ icon: 'cancel',
+ title: 'Cancel',
+ path: '/twitter/flight/-/jobs/928/cancel',
+ method: 'post',
+ },
+ },
+ },
+ {
+ id: 926,
+ name: 'production',
+ started: false,
+ build_path: '/twitter/flight/-/jobs/926',
+ retry_path: '/twitter/flight/-/jobs/926/retry',
+ play_path: '/twitter/flight/-/jobs/926/play',
+ playable: true,
+ created_at: '2018-04-04T20:00:57.202Z',
+ updated_at: '2018-04-04T20:11:13.110Z',
+ status: {
+ icon: 'status_canceled',
+ text: 'canceled',
+ label: 'manual play action',
+ group: 'canceled',
+ tooltip: 'canceled',
+ has_details: true,
+ details_path: '/twitter/flight/-/jobs/926',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_canceled-5491840b9b6feafba0bc599cbd49ee9580321dc809683856cf1b0d51532b1af6.ico',
+ action: {
+ icon: 'play',
+ title: 'Play',
+ path: '/twitter/flight/-/jobs/926/play',
+ method: 'post',
+ },
+ },
+ },
+ {
+ id: 217,
+ name: 'staging',
+ started: '2018-03-07T08:41:46.234Z',
+ build_path: '/twitter/flight/-/jobs/217',
+ retry_path: '/twitter/flight/-/jobs/217/retry',
+ playable: false,
+ created_at: '2018-03-07T14:41:58.093Z',
+ updated_at: '2018-03-07T14:41:58.093Z',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/twitter/flight/-/jobs/217',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/twitter/flight/-/jobs/217/retry',
+ method: 'post',
+ },
+ },
+ },
+ ],
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ tooltip: 'running',
+ has_details: true,
+ details_path: '/twitter/flight/pipelines/13#deploy',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
+ },
+ path: '/twitter/flight/pipelines/13#deploy',
+ dropdown_path: '/twitter/flight/pipelines/13/stage.json?stage=deploy',
+};
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
new file mode 100644
index 00000000000..40cd0ad9047
--- /dev/null
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -0,0 +1,659 @@
+import { mount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import PipelinesComponent from '~/pipelines/components/pipelines.vue';
+import Store from '~/pipelines/stores/pipelines_store';
+import { pipelineWithStages, stageReply } from './mock_data';
+
+describe('Pipelines', () => {
+ const jsonFixtureName = 'pipelines/pipelines.json';
+
+ preloadFixtures(jsonFixtureName);
+
+ let pipelines;
+ let wrapper;
+ let mock;
+
+ const paths = {
+ endpoint: 'twitter/flight/pipelines.json',
+ autoDevopsPath: '/help/topics/autodevops/index.md',
+ helpPagePath: '/help/ci/quick_start/README',
+ emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
+ errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
+ noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
+ ciLintPath: '/ci/lint',
+ resetCachePath: '/twitter/flight/settings/ci_cd/reset_cache',
+ newPipelinePath: '/twitter/flight/pipelines/new',
+ };
+
+ const noPermissions = {
+ endpoint: 'twitter/flight/pipelines.json',
+ autoDevopsPath: '/help/topics/autodevops/index.md',
+ helpPagePath: '/help/ci/quick_start/README',
+ emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
+ errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
+ noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
+ };
+
+ const defaultProps = {
+ hasGitlabCi: true,
+ canCreatePipeline: true,
+ ...paths,
+ };
+
+ const createComponent = (props = defaultProps, methods) => {
+ wrapper = mount(PipelinesComponent, {
+ propsData: {
+ store: new Store(),
+ ...props,
+ },
+ methods: {
+ ...methods,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ pipelines = getJSONFixture(jsonFixtureName);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ describe('With permission', () => {
+ describe('With pipelines in main tab', () => {
+ beforeEach(() => {
+ mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
+ createComponent();
+ return waitForPromises();
+ });
+
+ it('renders tabs', () => {
+ expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All');
+ });
+
+ it('renders Run Pipeline link', () => {
+ expect(wrapper.find('.js-run-pipeline').attributes('href')).toBe(paths.newPipelinePath);
+ });
+
+ it('renders CI Lint link', () => {
+ expect(wrapper.find('.js-ci-lint').attributes('href')).toBe(paths.ciLintPath);
+ });
+
+ it('renders Clear Runner Cache button', () => {
+ expect(wrapper.find('.js-clear-cache').text()).toBe('Clear Runner Caches');
+ });
+
+ it('renders pipelines table', () => {
+ expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength(
+ pipelines.pipelines.length + 1,
+ );
+ });
+ });
+
+ describe('Without pipelines on main tab with CI', () => {
+ beforeEach(() => {
+ mock.onGet('twitter/flight/pipelines.json').reply(200, {
+ pipelines: [],
+ count: {
+ all: 0,
+ pending: 0,
+ running: 0,
+ finished: 0,
+ },
+ });
+
+ createComponent();
+
+ return waitForPromises();
+ });
+
+ it('renders tabs', () => {
+ expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All');
+ });
+
+ it('renders Run Pipeline link', () => {
+ expect(wrapper.find('.js-run-pipeline').attributes('href')).toEqual(paths.newPipelinePath);
+ });
+
+ it('renders CI Lint link', () => {
+ expect(wrapper.find('.js-ci-lint').attributes('href')).toEqual(paths.ciLintPath);
+ });
+
+ it('renders Clear Runner Cache button', () => {
+ expect(wrapper.find('.js-clear-cache').text()).toEqual('Clear Runner Caches');
+ });
+
+ it('renders tab empty state', () => {
+ expect(wrapper.find('.empty-state h4').text()).toEqual('There are currently no pipelines.');
+ });
+ });
+
+ describe('Without pipelines nor CI', () => {
+ beforeEach(() => {
+ mock.onGet('twitter/flight/pipelines.json').reply(200, {
+ pipelines: [],
+ count: {
+ all: 0,
+ pending: 0,
+ running: 0,
+ finished: 0,
+ },
+ });
+
+ createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
+
+ return waitForPromises();
+ });
+
+ it('renders empty state', () => {
+ expect(wrapper.find('.js-empty-state h4').text()).toEqual('Build with confidence');
+
+ expect(wrapper.find('.js-get-started-pipelines').attributes('href')).toEqual(
+ paths.helpPagePath,
+ );
+ });
+
+ it('does not render tabs nor buttons', () => {
+ expect(wrapper.find('.js-pipelines-tab-all').exists()).toBeFalsy();
+ expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy();
+ expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy();
+ expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy();
+ });
+ });
+
+ describe('When API returns error', () => {
+ beforeEach(() => {
+ mock.onGet('twitter/flight/pipelines.json').reply(500, {});
+ createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
+
+ return waitForPromises();
+ });
+
+ it('renders tabs', () => {
+ expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All');
+ });
+
+ it('renders buttons', () => {
+ expect(wrapper.find('.js-run-pipeline').attributes('href')).toEqual(paths.newPipelinePath);
+
+ expect(wrapper.find('.js-ci-lint').attributes('href')).toEqual(paths.ciLintPath);
+ expect(wrapper.find('.js-clear-cache').text()).toEqual('Clear Runner Caches');
+ });
+
+ it('renders error state', () => {
+ expect(wrapper.find('.empty-state').text()).toContain(
+ 'There was an error fetching the pipelines.',
+ );
+ });
+ });
+ });
+
+ describe('Without permission', () => {
+ describe('With pipelines in main tab', () => {
+ beforeEach(() => {
+ mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
+
+ createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions });
+
+ return waitForPromises();
+ });
+
+ it('renders tabs', () => {
+ expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All');
+ });
+
+ it('does not render buttons', () => {
+ expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy();
+ expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy();
+ expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy();
+ });
+
+ it('renders pipelines table', () => {
+ expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength(
+ pipelines.pipelines.length + 1,
+ );
+ });
+ });
+
+ describe('Without pipelines on main tab with CI', () => {
+ beforeEach(() => {
+ mock.onGet('twitter/flight/pipelines.json').reply(200, {
+ pipelines: [],
+ count: {
+ all: 0,
+ pending: 0,
+ running: 0,
+ finished: 0,
+ },
+ });
+
+ createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions });
+
+ return waitForPromises();
+ });
+
+ it('renders tabs', () => {
+ expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All');
+ });
+
+ it('does not render buttons', () => {
+ expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy();
+ expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy();
+ expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy();
+ });
+
+ it('renders tab empty state', () => {
+ expect(wrapper.find('.empty-state h4').text()).toEqual('There are currently no pipelines.');
+ });
+ });
+
+ describe('Without pipelines nor CI', () => {
+ beforeEach(() => {
+ mock.onGet('twitter/flight/pipelines.json').reply(200, {
+ pipelines: [],
+ count: {
+ all: 0,
+ pending: 0,
+ running: 0,
+ finished: 0,
+ },
+ });
+
+ createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions });
+
+ return waitForPromises();
+ });
+
+ it('renders empty state without button to set CI', () => {
+ expect(wrapper.find('.js-empty-state').text()).toEqual(
+ 'This project is not currently set up to run pipelines.',
+ );
+
+ expect(wrapper.find('.js-get-started-pipelines').exists()).toBeFalsy();
+ });
+
+ it('does not render tabs or buttons', () => {
+ expect(wrapper.find('.js-pipelines-tab-all').exists()).toBeFalsy();
+ expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy();
+ expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy();
+ expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy();
+ });
+ });
+
+ describe('When API returns error', () => {
+ beforeEach(() => {
+ mock.onGet('twitter/flight/pipelines.json').reply(500, {});
+
+ createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...noPermissions });
+
+ return waitForPromises();
+ });
+
+ it('renders tabs', () => {
+ expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All');
+ });
+
+ it('does not renders buttons', () => {
+ expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy();
+ expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy();
+ expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy();
+ });
+
+ it('renders error state', () => {
+ expect(wrapper.find('.empty-state').text()).toContain(
+ 'There was an error fetching the pipelines.',
+ );
+ });
+ });
+ });
+
+ describe('successful request', () => {
+ describe('with pipelines', () => {
+ beforeEach(() => {
+ mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
+
+ createComponent();
+ return waitForPromises();
+ });
+
+ it('should render table', () => {
+ expect(wrapper.find('.table-holder').exists()).toBe(true);
+ expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength(
+ pipelines.pipelines.length + 1,
+ );
+ });
+
+ it('should render navigation tabs', () => {
+ expect(wrapper.find('.js-pipelines-tab-pending').text()).toContain('Pending');
+
+ expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All');
+
+ expect(wrapper.find('.js-pipelines-tab-running').text()).toContain('Running');
+
+ expect(wrapper.find('.js-pipelines-tab-finished').text()).toContain('Finished');
+
+ expect(wrapper.find('.js-pipelines-tab-branches').text()).toContain('Branches');
+
+ expect(wrapper.find('.js-pipelines-tab-tags').text()).toContain('Tags');
+ });
+
+ it('should make an API request when using tabs', () => {
+ const updateContentMock = jest.fn(() => {});
+ createComponent(
+ { hasGitlabCi: true, canCreatePipeline: true, ...paths },
+ {
+ updateContent: updateContentMock,
+ },
+ );
+
+ return waitForPromises().then(() => {
+ wrapper.find('.js-pipelines-tab-finished').trigger('click');
+
+ expect(updateContentMock).toHaveBeenCalledWith({ scope: 'finished', page: '1' });
+ });
+ });
+
+ describe('with pagination', () => {
+ it('should make an API request when using pagination', () => {
+ const updateContentMock = jest.fn(() => {});
+ createComponent(
+ { hasGitlabCi: true, canCreatePipeline: true, ...paths },
+ {
+ updateContent: updateContentMock,
+ },
+ );
+
+ return waitForPromises()
+ .then(() => {
+ // Mock pagination
+ wrapper.vm.store.state.pageInfo = {
+ page: 1,
+ total: 10,
+ perPage: 2,
+ nextPage: 2,
+ totalPages: 5,
+ };
+
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ wrapper.find('.next-page-item').trigger('click');
+
+ expect(updateContentMock).toHaveBeenCalledWith({ scope: 'all', page: '2' });
+ });
+ });
+ });
+ });
+ });
+
+ describe('methods', () => {
+ beforeEach(() => {
+ jest.spyOn(window.history, 'pushState').mockImplementation(() => null);
+ });
+
+ describe('onChangeTab', () => {
+ it('should set page to 1', () => {
+ const updateContentMock = jest.fn(() => {});
+ createComponent(
+ { hasGitlabCi: true, canCreatePipeline: true, ...paths },
+ {
+ updateContent: updateContentMock,
+ },
+ );
+
+ wrapper.vm.onChangeTab('running');
+
+ expect(updateContentMock).toHaveBeenCalledWith({ scope: 'running', page: '1' });
+ });
+ });
+
+ describe('onChangePage', () => {
+ it('should update page and keep scope', () => {
+ const updateContentMock = jest.fn(() => {});
+ createComponent(
+ { hasGitlabCi: true, canCreatePipeline: true, ...paths },
+ {
+ updateContent: updateContentMock,
+ },
+ );
+
+ wrapper.vm.onChangePage(4);
+
+ expect(updateContentMock).toHaveBeenCalledWith({ scope: wrapper.vm.scope, page: '4' });
+ });
+ });
+ });
+
+ describe('computed properties', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('tabs', () => {
+ it('returns default tabs', () => {
+ expect(wrapper.vm.tabs).toEqual([
+ { name: 'All', scope: 'all', count: undefined, isActive: true },
+ { name: 'Pending', scope: 'pending', count: undefined, isActive: false },
+ { name: 'Running', scope: 'running', count: undefined, isActive: false },
+ { name: 'Finished', scope: 'finished', count: undefined, isActive: false },
+ { name: 'Branches', scope: 'branches', isActive: false },
+ { name: 'Tags', scope: 'tags', isActive: false },
+ ]);
+ });
+ });
+
+ describe('emptyTabMessage', () => {
+ it('returns message with scope', () => {
+ wrapper.vm.scope = 'pending';
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.emptyTabMessage).toEqual('There are currently no pending pipelines.');
+ });
+ });
+
+ it('returns message without scope when scope is `all`', () => {
+ expect(wrapper.vm.emptyTabMessage).toEqual('There are currently no pipelines.');
+ });
+ });
+
+ describe('stateToRender', () => {
+ it('returns loading state when the app is loading', () => {
+ expect(wrapper.vm.stateToRender).toEqual('loading');
+ });
+
+ it('returns error state when app has error', () => {
+ wrapper.vm.hasError = true;
+ wrapper.vm.isLoading = false;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.stateToRender).toEqual('error');
+ });
+ });
+
+ it('returns table list when app has pipelines', () => {
+ wrapper.vm.isLoading = false;
+ wrapper.vm.hasError = false;
+ wrapper.vm.state.pipelines = pipelines.pipelines;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.stateToRender).toEqual('tableList');
+ });
+ });
+
+ it('returns empty tab when app does not have pipelines but project has pipelines', () => {
+ wrapper.vm.state.count.all = 10;
+ wrapper.vm.isLoading = false;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.stateToRender).toEqual('emptyTab');
+ });
+ });
+
+ it('returns empty tab when project has CI', () => {
+ wrapper.vm.isLoading = false;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.stateToRender).toEqual('emptyTab');
+ });
+ });
+
+ it('returns empty state when project does not have pipelines nor CI', () => {
+ createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
+
+ wrapper.vm.isLoading = false;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.stateToRender).toEqual('emptyState');
+ });
+ });
+ });
+
+ describe('shouldRenderTabs', () => {
+ it('returns true when state is loading & has already made the first request', () => {
+ wrapper.vm.isLoading = true;
+ wrapper.vm.hasMadeRequest = true;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.shouldRenderTabs).toEqual(true);
+ });
+ });
+
+ it('returns true when state is tableList & has already made the first request', () => {
+ wrapper.vm.isLoading = false;
+ wrapper.vm.state.pipelines = pipelines.pipelines;
+ wrapper.vm.hasMadeRequest = true;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.shouldRenderTabs).toEqual(true);
+ });
+ });
+
+ it('returns true when state is error & has already made the first request', () => {
+ wrapper.vm.isLoading = false;
+ wrapper.vm.hasError = true;
+ wrapper.vm.hasMadeRequest = true;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.shouldRenderTabs).toEqual(true);
+ });
+ });
+
+ it('returns true when state is empty tab & has already made the first request', () => {
+ wrapper.vm.isLoading = false;
+ wrapper.vm.state.count.all = 10;
+ wrapper.vm.hasMadeRequest = true;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.shouldRenderTabs).toEqual(true);
+ });
+ });
+
+ it('returns false when has not made first request', () => {
+ wrapper.vm.hasMadeRequest = false;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.shouldRenderTabs).toEqual(false);
+ });
+ });
+
+ it('returns false when state is empty state', () => {
+ createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
+
+ wrapper.vm.isLoading = false;
+ wrapper.vm.hasMadeRequest = true;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.shouldRenderTabs).toEqual(false);
+ });
+ });
+ });
+
+ describe('shouldRenderButtons', () => {
+ it('returns true when it has paths & has made the first request', () => {
+ wrapper.vm.hasMadeRequest = true;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.shouldRenderButtons).toEqual(true);
+ });
+ });
+
+ it('returns false when it has not made the first request', () => {
+ wrapper.vm.hasMadeRequest = false;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.shouldRenderButtons).toEqual(false);
+ });
+ });
+ });
+ });
+
+ describe('updates results when a staged is clicked', () => {
+ beforeEach(() => {
+ const copyPipeline = Object.assign({}, pipelineWithStages);
+ copyPipeline.id += 1;
+ mock
+ .onGet('twitter/flight/pipelines.json')
+ .reply(
+ 200,
+ {
+ pipelines: [pipelineWithStages],
+ count: {
+ all: 1,
+ finished: 1,
+ pending: 0,
+ running: 0,
+ },
+ },
+ {
+ 'POLL-INTERVAL': 100,
+ },
+ )
+ .onGet(pipelineWithStages.details.stages[0].dropdown_path)
+ .reply(200, stageReply);
+
+ createComponent();
+ });
+
+ describe('when a request is being made', () => {
+ it('stops polling, cancels the request, & restarts polling', () => {
+ const stopMock = jest.spyOn(wrapper.vm.poll, 'stop');
+ const restartMock = jest.spyOn(wrapper.vm.poll, 'restart');
+ const cancelMock = jest.spyOn(wrapper.vm.service.cancelationSource, 'cancel');
+ mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
+
+ return waitForPromises()
+ .then(() => {
+ wrapper.vm.isMakingRequest = true;
+ wrapper.find('.js-builds-dropdown-button').trigger('click');
+ })
+ .then(() => {
+ expect(cancelMock).toHaveBeenCalled();
+ expect(stopMock).toHaveBeenCalled();
+ expect(restartMock).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('when no request is being made', () => {
+ it('stops polling & restarts polling', () => {
+ const stopMock = jest.spyOn(wrapper.vm.poll, 'stop');
+ const restartMock = jest.spyOn(wrapper.vm.poll, 'restart');
+ mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
+
+ return waitForPromises()
+ .then(() => {
+ wrapper.find('.js-builds-dropdown-button').trigger('click');
+ expect(stopMock).toHaveBeenCalled();
+ })
+ .then(() => {
+ expect(restartMock).toHaveBeenCalled();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/users_select/utils_spec.js b/spec/frontend/users_select/utils_spec.js
deleted file mode 100644
index a09935d8a04..00000000000
--- a/spec/frontend/users_select/utils_spec.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import $ from 'jquery';
-import { getAjaxUsersSelectOptions, getAjaxUsersSelectParams } from '~/users_select/utils';
-
-const options = {
- fooBar: 'baz',
- activeUserId: 1,
-};
-
-describe('getAjaxUsersSelectOptions', () => {
- it('returns options built from select data attributes', () => {
- const $select = $('<select />', { 'data-foo-bar': 'baz', 'data-user-id': 1 });
-
- expect(
- getAjaxUsersSelectOptions($select, { fooBar: 'fooBar', activeUserId: 'user-id' }),
- ).toEqual(options);
- });
-});
-
-describe('getAjaxUsersSelectParams', () => {
- it('returns query parameters built from provided options', () => {
- expect(
- getAjaxUsersSelectParams(options, {
- foo_bar: 'fooBar',
- active_user_id: 'activeUserId',
- non_existent_key: 'nonExistentKey',
- }),
- ).toEqual({
- foo_bar: 'baz',
- active_user_id: 1,
- non_existent_key: null,
- });
- });
-});
diff --git a/spec/graphql/types/jira_import_type_spec.rb b/spec/graphql/types/jira_import_type_spec.rb
index 8448a120682..e73568c9710 100644
--- a/spec/graphql/types/jira_import_type_spec.rb
+++ b/spec/graphql/types/jira_import_type_spec.rb
@@ -6,6 +6,6 @@ describe GitlabSchema.types['JiraImport'] do
it { expect(described_class.graphql_name).to eq('JiraImport') }
it 'has the expected fields' do
- expect(described_class).to have_graphql_fields(:jira_project_key, :scheduled_at, :scheduled_by)
+ expect(described_class).to have_graphql_fields(:jira_project_key, :createdAt, :scheduled_at, :scheduled_by)
end
end
diff --git a/spec/helpers/explore_helper_spec.rb b/spec/helpers/explore_helper_spec.rb
index f8240dd3a4c..1a6af3be055 100644
--- a/spec/helpers/explore_helper_spec.rb
+++ b/spec/helpers/explore_helper_spec.rb
@@ -19,23 +19,10 @@ describe ExploreHelper do
end
describe '#public_visibility_restricted?' do
- using RSpec::Parameterized::TableSyntax
+ it 'delegates to Gitlab::VisibilityLevel' do
+ expect(Gitlab::VisibilityLevel).to receive(:public_visibility_restricted?).and_call_original
- where(:visibility_levels, :expected_status) do
- nil | nil
- [Gitlab::VisibilityLevel::PRIVATE] | false
- [Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::INTERNAL] | false
- [Gitlab::VisibilityLevel::PUBLIC] | true
- end
-
- with_them do
- before do
- stub_application_setting(restricted_visibility_levels: visibility_levels)
- end
-
- it 'returns the expected status' do
- expect(helper.public_visibility_restricted?).to eq(expected_status)
- end
+ helper.public_visibility_restricted?
end
end
end
diff --git a/spec/helpers/projects/alert_management_helper_spec.rb b/spec/helpers/projects/alert_management_helper_spec.rb
index ee180cef692..9246d1deff6 100644
--- a/spec/helpers/projects/alert_management_helper_spec.rb
+++ b/spec/helpers/projects/alert_management_helper_spec.rb
@@ -5,21 +5,32 @@ require 'spec_helper'
describe Projects::AlertManagementHelper do
include Gitlab::Routing.url_helpers
- let(:project) { create(:project) }
+ let_it_be(:project, reload: true) { create(:project) }
+ let_it_be(:current_user) { create(:user) }
describe '#alert_management_data' do
+ let(:user_can_enable_alert_management) { false }
let(:setting_path) { project_settings_operations_path(project) }
let(:index_path) do
project_alert_management_index_path(project, format: :json)
end
+ before do
+ allow(helper)
+ .to receive(:can?)
+ .with(current_user, :admin_operations, project)
+ .and_return(user_can_enable_alert_management)
+ end
+
context 'without alert_managements_setting' do
it 'returns frontend configuration' do
- expect(alert_management_data(project)).to eq(
+ expect(alert_management_data(current_user, project)).to eq(
'index-path' => index_path,
'enable-alert-management-path' => setting_path,
- "empty-alert-svg-path" => "/images/illustrations/alert-management-empty-state.svg"
+ "empty-alert-svg-path" => "/images/illustrations/alert-management-empty-state.svg",
+ 'user-can-enable-alert-management' => 'false',
+ 'alert-management-enabled' => 'true'
)
end
end
diff --git a/spec/javascripts/deploy_keys/components/action_btn_spec.js b/spec/javascripts/deploy_keys/components/action_btn_spec.js
deleted file mode 100644
index 5bf72cc0018..00000000000
--- a/spec/javascripts/deploy_keys/components/action_btn_spec.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import Vue from 'vue';
-import eventHub from '~/deploy_keys/eventhub';
-import actionBtn from '~/deploy_keys/components/action_btn.vue';
-
-describe('Deploy keys action btn', () => {
- const data = getJSONFixture('deploy_keys/keys.json');
- const deployKey = data.enabled_keys[0];
- let vm;
-
- beforeEach(done => {
- const ActionBtnComponent = Vue.extend({
- components: {
- actionBtn,
- },
- data() {
- return {
- deployKey,
- };
- },
- template: `
- <action-btn
- :deploy-key="deployKey"
- type="enable">
- Enable
- </action-btn>`,
- });
-
- vm = new ActionBtnComponent().$mount();
-
- Vue.nextTick()
- .then(done)
- .catch(done.fail);
- });
-
- it('renders the default slot', () => {
- expect(vm.$el.textContent.trim()).toBe('Enable');
- });
-
- it('sends eventHub event with btn type', done => {
- spyOn(eventHub, '$emit');
-
- vm.$el.click();
-
- Vue.nextTick(() => {
- expect(eventHub.$emit).toHaveBeenCalledWith('enable.key', deployKey, jasmine.anything());
-
- done();
- });
- });
-
- it('shows loading spinner after click', done => {
- vm.$el.click();
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.fa')).toBeDefined();
-
- done();
- });
- });
-
- it('disables button after click', done => {
- vm.$el.click();
-
- Vue.nextTick(() => {
- expect(vm.$el.classList.contains('disabled')).toBeTruthy();
-
- expect(vm.$el.getAttribute('disabled')).toBe('disabled');
-
- done();
- });
- });
-});
diff --git a/spec/javascripts/deploy_keys/components/app_spec.js b/spec/javascripts/deploy_keys/components/app_spec.js
deleted file mode 100644
index c9a9814d122..00000000000
--- a/spec/javascripts/deploy_keys/components/app_spec.js
+++ /dev/null
@@ -1,155 +0,0 @@
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import { TEST_HOST } from 'spec/test_constants';
-import axios from '~/lib/utils/axios_utils';
-import eventHub from '~/deploy_keys/eventhub';
-import deployKeysApp from '~/deploy_keys/components/app.vue';
-
-describe('Deploy keys app component', () => {
- const data = getJSONFixture('deploy_keys/keys.json');
- let vm;
- let mock;
-
- beforeEach(done => {
- // set up axios mock before component
- mock = new MockAdapter(axios);
- mock.onGet(`${TEST_HOST}/dummy/`).replyOnce(200, data);
-
- const Component = Vue.extend(deployKeysApp);
-
- vm = new Component({
- propsData: {
- endpoint: `${TEST_HOST}/dummy`,
- projectId: '8',
- },
- }).$mount();
-
- setTimeout(done);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('renders loading icon', done => {
- vm.store.keys = {};
- vm.isLoading = false;
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').length).toBe(0);
-
- expect(vm.$el.querySelector('.fa-spinner')).toBeDefined();
-
- done();
- });
- });
-
- it('renders keys panels', () => {
- expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').length).toBe(3);
- });
-
- it('renders the titles with keys count', () => {
- const textContent = selector => {
- const element = vm.$el.querySelector(`${selector}`);
-
- expect(element).not.toBeNull();
- return element.textContent.trim();
- };
-
- expect(textContent('.js-deployKeys-tab-enabled_keys')).toContain('Enabled deploy keys');
- expect(textContent('.js-deployKeys-tab-available_project_keys')).toContain(
- 'Privately accessible deploy keys',
- );
-
- expect(textContent('.js-deployKeys-tab-public_keys')).toContain(
- 'Publicly accessible deploy keys',
- );
-
- expect(textContent('.js-deployKeys-tab-enabled_keys .badge')).toBe(
- `${vm.store.keys.enabled_keys.length}`,
- );
-
- expect(textContent('.js-deployKeys-tab-available_project_keys .badge')).toBe(
- `${vm.store.keys.available_project_keys.length}`,
- );
-
- expect(textContent('.js-deployKeys-tab-public_keys .badge')).toBe(
- `${vm.store.keys.public_keys.length}`,
- );
- });
-
- it('does not render key panels when keys object is empty', done => {
- vm.store.keys = {};
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').length).toBe(0);
-
- done();
- });
- });
-
- it('re-fetches deploy keys when enabling a key', done => {
- const key = data.public_keys[0];
-
- spyOn(vm.service, 'getKeys');
- spyOn(vm.service, 'enableKey').and.callFake(() => Promise.resolve());
-
- eventHub.$emit('enable.key', key);
-
- Vue.nextTick(() => {
- expect(vm.service.enableKey).toHaveBeenCalledWith(key.id);
- expect(vm.service.getKeys).toHaveBeenCalled();
- done();
- });
- });
-
- it('re-fetches deploy keys when disabling a key', done => {
- const key = data.public_keys[0];
-
- spyOn(window, 'confirm').and.returnValue(true);
- spyOn(vm.service, 'getKeys');
- spyOn(vm.service, 'disableKey').and.callFake(() => Promise.resolve());
-
- eventHub.$emit('disable.key', key);
-
- Vue.nextTick(() => {
- expect(vm.service.disableKey).toHaveBeenCalledWith(key.id);
- expect(vm.service.getKeys).toHaveBeenCalled();
- done();
- });
- });
-
- it('calls disableKey when removing a key', done => {
- const key = data.public_keys[0];
-
- spyOn(window, 'confirm').and.returnValue(true);
- spyOn(vm.service, 'getKeys');
- spyOn(vm.service, 'disableKey').and.callFake(() => Promise.resolve());
-
- eventHub.$emit('remove.key', key);
-
- Vue.nextTick(() => {
- expect(vm.service.disableKey).toHaveBeenCalledWith(key.id);
- expect(vm.service.getKeys).toHaveBeenCalled();
- done();
- });
- });
-
- it('hasKeys returns true when there are keys', () => {
- expect(vm.hasKeys).toEqual(3);
- });
-
- it('resets disable button loading state', done => {
- spyOn(window, 'confirm').and.returnValue(false);
-
- const btn = vm.$el.querySelector('.btn-warning');
-
- btn.click();
-
- Vue.nextTick(() => {
- expect(btn.querySelector('.btn-warning')).not.toExist();
-
- done();
- });
- });
-});
diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js
deleted file mode 100644
index 7117dc4a9ee..00000000000
--- a/spec/javascripts/deploy_keys/components/key_spec.js
+++ /dev/null
@@ -1,157 +0,0 @@
-import Vue from 'vue';
-import DeployKeysStore from '~/deploy_keys/store';
-import key from '~/deploy_keys/components/key.vue';
-import { getTimeago } from '~/lib/utils/datetime_utility';
-
-describe('Deploy keys key', () => {
- let vm;
- const KeyComponent = Vue.extend(key);
- const data = getJSONFixture('deploy_keys/keys.json');
- const createComponent = deployKey => {
- const store = new DeployKeysStore();
- store.keys = data;
-
- vm = new KeyComponent({
- propsData: {
- deployKey,
- store,
- endpoint: 'https://test.host/dummy/endpoint',
- },
- }).$mount();
- };
-
- describe('enabled key', () => {
- const deployKey = data.enabled_keys[0];
-
- beforeEach(done => {
- createComponent(deployKey);
-
- setTimeout(done);
- });
-
- it('renders the keys title', () => {
- expect(vm.$el.querySelector('.title').textContent.trim()).toContain('My title');
- });
-
- it('renders human friendly formatted created date', () => {
- expect(vm.$el.querySelector('.key-created-at').textContent.trim()).toBe(
- `${getTimeago().format(deployKey.created_at)}`,
- );
- });
-
- it('shows pencil button for editing', () => {
- expect(vm.$el.querySelector('.btn .ic-pencil')).toExist();
- });
-
- it('shows disable button when the project is not deletable', () => {
- expect(vm.$el.querySelector('.btn .ic-cancel')).toExist();
- });
-
- it('shows remove button when the project is deletable', done => {
- vm.deployKey.destroyed_when_orphaned = true;
- vm.deployKey.almost_orphaned = true;
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.btn .ic-remove')).toExist();
- done();
- });
- });
- });
-
- describe('deploy key labels', () => {
- it('shows write access title when key has write access', done => {
- vm.deployKey.deploy_keys_projects[0].can_push = true;
-
- Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.deploy-project-label').getAttribute('data-original-title'),
- ).toBe('Write access allowed');
- done();
- });
- });
-
- it('does not show write access title when key has write access', done => {
- vm.deployKey.deploy_keys_projects[0].can_push = false;
-
- Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.deploy-project-label').getAttribute('data-original-title'),
- ).toBe('Read access only');
- done();
- });
- });
-
- it('shows expandable button if more than two projects', () => {
- const labels = vm.$el.querySelectorAll('.deploy-project-label');
-
- expect(labels.length).toBe(2);
- expect(labels[1].textContent).toContain('others');
- expect(labels[1].getAttribute('data-original-title')).toContain('Expand');
- });
-
- it('expands all project labels after click', done => {
- const { length } = vm.deployKey.deploy_keys_projects;
- vm.$el.querySelectorAll('.deploy-project-label')[1].click();
-
- Vue.nextTick(() => {
- const labels = vm.$el.querySelectorAll('.deploy-project-label');
-
- expect(labels.length).toBe(length);
- expect(labels[1].textContent).not.toContain(`+${length} others`);
- expect(labels[1].getAttribute('data-original-title')).not.toContain('Expand');
- done();
- });
- });
-
- it('shows two projects', done => {
- vm.deployKey.deploy_keys_projects = [...vm.deployKey.deploy_keys_projects].slice(0, 2);
-
- Vue.nextTick(() => {
- const labels = vm.$el.querySelectorAll('.deploy-project-label');
-
- expect(labels.length).toBe(2);
- expect(labels[1].textContent).toContain(
- vm.deployKey.deploy_keys_projects[1].project.full_name,
- );
- done();
- });
- });
- });
-
- describe('public keys', () => {
- const deployKey = data.public_keys[0];
-
- beforeEach(done => {
- createComponent(deployKey);
-
- setTimeout(done);
- });
-
- it('renders deploy keys without any enabled projects', done => {
- vm.deployKey.deploy_keys_projects = [];
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.deploy-project-list').textContent.trim()).toBe('None');
-
- done();
- });
- });
-
- it('shows enable button', () => {
- expect(vm.$el.querySelectorAll('.btn')[0].textContent.trim()).toBe('Enable');
- });
-
- it('shows pencil button for editing', () => {
- expect(vm.$el.querySelector('.btn .ic-pencil')).toExist();
- });
-
- it('shows disable button when key is enabled', done => {
- vm.store.keys.enabled_keys.push(deployKey);
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.btn .ic-cancel')).toExist();
-
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/deploy_keys/components/keys_panel_spec.js b/spec/javascripts/deploy_keys/components/keys_panel_spec.js
deleted file mode 100644
index f71f5ccf082..00000000000
--- a/spec/javascripts/deploy_keys/components/keys_panel_spec.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import Vue from 'vue';
-import DeployKeysStore from '~/deploy_keys/store';
-import deployKeysPanel from '~/deploy_keys/components/keys_panel.vue';
-
-describe('Deploy keys panel', () => {
- const data = getJSONFixture('deploy_keys/keys.json');
- let vm;
-
- beforeEach(done => {
- const DeployKeysPanelComponent = Vue.extend(deployKeysPanel);
- const store = new DeployKeysStore();
- store.keys = data;
-
- vm = new DeployKeysPanelComponent({
- propsData: {
- title: 'test',
- keys: data.enabled_keys,
- showHelpBox: true,
- store,
- endpoint: 'https://test.host/dummy/endpoint',
- },
- }).$mount();
-
- setTimeout(done);
- });
-
- it('renders list of keys', () => {
- expect(vm.$el.querySelectorAll('.deploy-key').length).toBe(vm.keys.length);
- });
-
- it('renders table header', () => {
- const tableHeader = vm.$el.querySelector('.table-row-header');
-
- expect(tableHeader).toExist();
- expect(tableHeader.textContent).toContain('Deploy key');
- expect(tableHeader.textContent).toContain('Project usage');
- expect(tableHeader.textContent).toContain('Created');
- });
-
- it('renders help box if keys are empty', done => {
- vm.keys = [];
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.settings-message')).toBeDefined();
-
- expect(vm.$el.querySelector('.settings-message').textContent.trim()).toBe(
- 'No deploy keys found. Create one with the form above.',
- );
-
- done();
- });
- });
-
- it('renders no table header if keys are empty', done => {
- vm.keys = [];
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.table-row-header')).not.toExist();
-
- done();
- });
- });
-});
diff --git a/spec/javascripts/dirty_submit/dirty_submit_collection_spec.js b/spec/javascripts/dirty_submit/dirty_submit_collection_spec.js
deleted file mode 100644
index 47be0b3ce9d..00000000000
--- a/spec/javascripts/dirty_submit/dirty_submit_collection_spec.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import DirtySubmitCollection from '~/dirty_submit/dirty_submit_collection';
-import { setInputValue, createForm } from './helper';
-
-describe('DirtySubmitCollection', () => {
- it('disables submits until there are changes', done => {
- const testElementsCollection = [createForm(), createForm()];
- const forms = testElementsCollection.map(testElements => testElements.form);
-
- new DirtySubmitCollection(forms); // eslint-disable-line no-new
-
- testElementsCollection.forEach(testElements => {
- const { input, submit } = testElements;
- const originalValue = input.value;
-
- expect(submit.disabled).toBe(true);
-
- return setInputValue(input, `${originalValue} changes`)
- .then(() => {
- expect(submit.disabled).toBe(false);
- })
- .then(() => setInputValue(input, originalValue))
- .then(() => {
- expect(submit.disabled).toBe(true);
- })
- .then(done)
- .catch(done.fail);
- });
- });
-});
diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js
deleted file mode 100644
index 5cd91413c5f..00000000000
--- a/spec/javascripts/pipelines/pipelines_spec.js
+++ /dev/null
@@ -1,783 +0,0 @@
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import axios from '~/lib/utils/axios_utils';
-import pipelinesComp from '~/pipelines/components/pipelines.vue';
-import Store from '~/pipelines/stores/pipelines_store';
-import { pipelineWithStages, stageReply } from './mock_data';
-
-describe('Pipelines', () => {
- const jsonFixtureName = 'pipelines/pipelines.json';
-
- preloadFixtures(jsonFixtureName);
-
- let PipelinesComponent;
- let pipelines;
- let vm;
- let mock;
-
- const paths = {
- endpoint: 'twitter/flight/pipelines.json',
- autoDevopsPath: '/help/topics/autodevops/index.md',
- helpPagePath: '/help/ci/quick_start/README',
- emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
- errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
- noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
- ciLintPath: '/ci/lint',
- resetCachePath: '/twitter/flight/settings/ci_cd/reset_cache',
- newPipelinePath: '/twitter/flight/pipelines/new',
- };
-
- const noPermissions = {
- endpoint: 'twitter/flight/pipelines.json',
- autoDevopsPath: '/help/topics/autodevops/index.md',
- helpPagePath: '/help/ci/quick_start/README',
- emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
- errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
- noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
- };
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
-
- pipelines = getJSONFixture(jsonFixtureName);
-
- PipelinesComponent = Vue.extend(pipelinesComp);
- });
-
- afterEach(() => {
- vm.$destroy();
- mock.restore();
- });
-
- describe('With permission', () => {
- describe('With pipelines in main tab', () => {
- beforeEach(done => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
-
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: true,
- canCreatePipeline: true,
- ...paths,
- });
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders tabs', () => {
- expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
- });
-
- it('renders Run Pipeline link', () => {
- expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(
- paths.newPipelinePath,
- );
- });
-
- it('renders CI Lint link', () => {
- expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath);
- });
-
- it('renders Clear Runner Cache button', () => {
- expect(vm.$el.querySelector('.js-clear-cache').textContent.trim()).toEqual(
- 'Clear Runner Caches',
- );
- });
-
- it('renders pipelines table', () => {
- expect(vm.$el.querySelectorAll('.gl-responsive-table-row').length).toEqual(
- pipelines.pipelines.length + 1,
- );
- });
- });
-
- describe('Without pipelines on main tab with CI', () => {
- beforeEach(done => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, {
- pipelines: [],
- count: {
- all: 0,
- pending: 0,
- running: 0,
- finished: 0,
- },
- });
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: true,
- canCreatePipeline: true,
- ...paths,
- });
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders tabs', () => {
- expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
- });
-
- it('renders Run Pipeline link', () => {
- expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(
- paths.newPipelinePath,
- );
- });
-
- it('renders CI Lint link', () => {
- expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath);
- });
-
- it('renders Clear Runner Cache button', () => {
- expect(vm.$el.querySelector('.js-clear-cache').textContent.trim()).toEqual(
- 'Clear Runner Caches',
- );
- });
-
- it('renders tab empty state', () => {
- expect(vm.$el.querySelector('.empty-state h4').textContent.trim()).toEqual(
- 'There are currently no pipelines.',
- );
- });
- });
-
- describe('Without pipelines nor CI', () => {
- beforeEach(done => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, {
- pipelines: [],
- count: {
- all: 0,
- pending: 0,
- running: 0,
- finished: 0,
- },
- });
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: false,
- canCreatePipeline: true,
- ...paths,
- });
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders empty state', () => {
- expect(vm.$el.querySelector('.js-empty-state h4').textContent.trim()).toEqual(
- 'Build with confidence',
- );
-
- expect(vm.$el.querySelector('.js-get-started-pipelines').getAttribute('href')).toEqual(
- paths.helpPagePath,
- );
- });
-
- it('does not render tabs nor buttons', () => {
- expect(vm.$el.querySelector('.js-pipelines-tab-all')).toBeNull();
- expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
- expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
- expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
- });
- });
-
- describe('When API returns error', () => {
- beforeEach(done => {
- mock.onGet('twitter/flight/pipelines.json').reply(500, {});
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: false,
- canCreatePipeline: true,
- ...paths,
- });
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders tabs', () => {
- expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
- });
-
- it('renders buttons', () => {
- expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(
- paths.newPipelinePath,
- );
-
- expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath);
- expect(vm.$el.querySelector('.js-clear-cache').textContent.trim()).toEqual(
- 'Clear Runner Caches',
- );
- });
-
- it('renders error state', () => {
- expect(vm.$el.querySelector('.empty-state').textContent.trim()).toContain(
- 'There was an error fetching the pipelines.',
- );
- });
- });
- });
-
- describe('Without permission', () => {
- describe('With pipelines in main tab', () => {
- beforeEach(done => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
-
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: false,
- canCreatePipeline: false,
- ...noPermissions,
- });
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders tabs', () => {
- expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
- });
-
- it('does not render buttons', () => {
- expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
- expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
- expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
- });
-
- it('renders pipelines table', () => {
- expect(vm.$el.querySelectorAll('.gl-responsive-table-row').length).toEqual(
- pipelines.pipelines.length + 1,
- );
- });
- });
-
- describe('Without pipelines on main tab with CI', () => {
- beforeEach(done => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, {
- pipelines: [],
- count: {
- all: 0,
- pending: 0,
- running: 0,
- finished: 0,
- },
- });
-
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: true,
- canCreatePipeline: false,
- ...noPermissions,
- });
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders tabs', () => {
- expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
- });
-
- it('does not render buttons', () => {
- expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
- expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
- expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
- });
-
- it('renders tab empty state', () => {
- expect(vm.$el.querySelector('.empty-state h4').textContent.trim()).toEqual(
- 'There are currently no pipelines.',
- );
- });
- });
-
- describe('Without pipelines nor CI', () => {
- beforeEach(done => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, {
- pipelines: [],
- count: {
- all: 0,
- pending: 0,
- running: 0,
- finished: 0,
- },
- });
-
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: false,
- canCreatePipeline: false,
- ...noPermissions,
- });
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders empty state without button to set CI', () => {
- expect(vm.$el.querySelector('.js-empty-state').textContent.trim()).toEqual(
- 'This project is not currently set up to run pipelines.',
- );
-
- expect(vm.$el.querySelector('.js-get-started-pipelines')).toBeNull();
- });
-
- it('does not render tabs or buttons', () => {
- expect(vm.$el.querySelector('.js-pipelines-tab-all')).toBeNull();
- expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
- expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
- expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
- });
- });
-
- describe('When API returns error', () => {
- beforeEach(done => {
- mock.onGet('twitter/flight/pipelines.json').reply(500, {});
-
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: false,
- canCreatePipeline: true,
- ...noPermissions,
- });
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders tabs', () => {
- expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
- });
-
- it('does not renders buttons', () => {
- expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
- expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
- expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
- });
-
- it('renders error state', () => {
- expect(vm.$el.querySelector('.empty-state').textContent.trim()).toContain(
- 'There was an error fetching the pipelines.',
- );
- });
- });
- });
-
- describe('successful request', () => {
- describe('with pipelines', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
-
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: true,
- canCreatePipeline: true,
- ...paths,
- });
- });
-
- it('should render table', done => {
- setTimeout(() => {
- expect(vm.$el.querySelector('.table-holder')).toBeDefined();
- expect(vm.$el.querySelectorAll('.gl-responsive-table-row').length).toEqual(
- pipelines.pipelines.length + 1,
- );
- done();
- });
- });
-
- it('should render navigation tabs', done => {
- setTimeout(() => {
- expect(vm.$el.querySelector('.js-pipelines-tab-pending').textContent.trim()).toContain(
- 'Pending',
- );
-
- expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
-
- expect(vm.$el.querySelector('.js-pipelines-tab-running').textContent.trim()).toContain(
- 'Running',
- );
-
- expect(vm.$el.querySelector('.js-pipelines-tab-finished').textContent.trim()).toContain(
- 'Finished',
- );
-
- expect(vm.$el.querySelector('.js-pipelines-tab-branches').textContent.trim()).toContain(
- 'Branches',
- );
-
- expect(vm.$el.querySelector('.js-pipelines-tab-tags').textContent.trim()).toContain(
- 'Tags',
- );
- done();
- });
- });
-
- it('should make an API request when using tabs', done => {
- setTimeout(() => {
- spyOn(vm, 'updateContent');
- vm.$el.querySelector('.js-pipelines-tab-finished').click();
-
- expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'finished', page: '1' });
- done();
- });
- });
-
- describe('with pagination', () => {
- it('should make an API request when using pagination', done => {
- setTimeout(() => {
- spyOn(vm, 'updateContent');
- // Mock pagination
- vm.store.state.pageInfo = {
- page: 1,
- total: 10,
- perPage: 2,
- nextPage: 2,
- totalPages: 5,
- };
-
- vm.$nextTick(() => {
- vm.$el.querySelector('.next-page-item').click();
-
- expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' });
-
- done();
- });
- });
- });
- });
- });
- });
-
- describe('methods', () => {
- beforeEach(() => {
- spyOn(window.history, 'pushState').and.stub();
- });
-
- describe('updateContent', () => {
- it('should set given parameters', () => {
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: true,
- canCreatePipeline: true,
- ...paths,
- });
- vm.updateContent({ scope: 'finished', page: '4' });
-
- expect(vm.page).toEqual('4');
- expect(vm.scope).toEqual('finished');
- expect(vm.requestData.scope).toEqual('finished');
- expect(vm.requestData.page).toEqual('4');
- });
- });
-
- describe('onChangeTab', () => {
- it('should set page to 1', () => {
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: true,
- canCreatePipeline: true,
- ...paths,
- });
- spyOn(vm, 'updateContent');
-
- vm.onChangeTab('running');
-
- expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' });
- });
- });
-
- describe('onChangePage', () => {
- it('should update page and keep scope', () => {
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: true,
- canCreatePipeline: true,
- ...paths,
- });
- spyOn(vm, 'updateContent');
-
- vm.onChangePage(4);
-
- expect(vm.updateContent).toHaveBeenCalledWith({ scope: vm.scope, page: '4' });
- });
- });
- });
-
- describe('computed properties', () => {
- beforeEach(() => {
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: true,
- canCreatePipeline: true,
- ...paths,
- });
- });
-
- describe('tabs', () => {
- it('returns default tabs', () => {
- expect(vm.tabs).toEqual([
- { name: 'All', scope: 'all', count: undefined, isActive: true },
- { name: 'Pending', scope: 'pending', count: undefined, isActive: false },
- { name: 'Running', scope: 'running', count: undefined, isActive: false },
- { name: 'Finished', scope: 'finished', count: undefined, isActive: false },
- { name: 'Branches', scope: 'branches', isActive: false },
- { name: 'Tags', scope: 'tags', isActive: false },
- ]);
- });
- });
-
- describe('emptyTabMessage', () => {
- it('returns message with scope', done => {
- vm.scope = 'pending';
-
- vm.$nextTick(() => {
- expect(vm.emptyTabMessage).toEqual('There are currently no pending pipelines.');
- done();
- });
- });
-
- it('returns message without scope when scope is `all`', () => {
- expect(vm.emptyTabMessage).toEqual('There are currently no pipelines.');
- });
- });
-
- describe('stateToRender', () => {
- it('returns loading state when the app is loading', () => {
- expect(vm.stateToRender).toEqual('loading');
- });
-
- it('returns error state when app has error', done => {
- vm.hasError = true;
- vm.isLoading = false;
-
- vm.$nextTick(() => {
- expect(vm.stateToRender).toEqual('error');
- done();
- });
- });
-
- it('returns table list when app has pipelines', done => {
- vm.isLoading = false;
- vm.hasError = false;
- vm.state.pipelines = pipelines.pipelines;
-
- vm.$nextTick(() => {
- expect(vm.stateToRender).toEqual('tableList');
-
- done();
- });
- });
-
- it('returns empty tab when app does not have pipelines but project has pipelines', done => {
- vm.state.count.all = 10;
- vm.isLoading = false;
-
- vm.$nextTick(() => {
- expect(vm.stateToRender).toEqual('emptyTab');
-
- done();
- });
- });
-
- it('returns empty tab when project has CI', done => {
- vm.isLoading = false;
- vm.$nextTick(() => {
- expect(vm.stateToRender).toEqual('emptyTab');
-
- done();
- });
- });
-
- it('returns empty state when project does not have pipelines nor CI', done => {
- vm.isLoading = false;
- vm.hasGitlabCi = false;
- vm.$nextTick(() => {
- expect(vm.stateToRender).toEqual('emptyState');
-
- done();
- });
- });
- });
-
- describe('shouldRenderTabs', () => {
- it('returns true when state is loading & has already made the first request', done => {
- vm.isLoading = true;
- vm.hasMadeRequest = true;
-
- vm.$nextTick(() => {
- expect(vm.shouldRenderTabs).toEqual(true);
-
- done();
- });
- });
-
- it('returns true when state is tableList & has already made the first request', done => {
- vm.isLoading = false;
- vm.state.pipelines = pipelines.pipelines;
- vm.hasMadeRequest = true;
-
- vm.$nextTick(() => {
- expect(vm.shouldRenderTabs).toEqual(true);
-
- done();
- });
- });
-
- it('returns true when state is error & has already made the first request', done => {
- vm.isLoading = false;
- vm.hasError = true;
- vm.hasMadeRequest = true;
-
- vm.$nextTick(() => {
- expect(vm.shouldRenderTabs).toEqual(true);
-
- done();
- });
- });
-
- it('returns true when state is empty tab & has already made the first request', done => {
- vm.isLoading = false;
- vm.state.count.all = 10;
- vm.hasMadeRequest = true;
-
- vm.$nextTick(() => {
- expect(vm.shouldRenderTabs).toEqual(true);
-
- done();
- });
- });
-
- it('returns false when has not made first request', done => {
- vm.hasMadeRequest = false;
-
- vm.$nextTick(() => {
- expect(vm.shouldRenderTabs).toEqual(false);
-
- done();
- });
- });
-
- it('returns false when state is empty state', done => {
- vm.isLoading = false;
- vm.hasMadeRequest = true;
- vm.hasGitlabCi = false;
-
- vm.$nextTick(() => {
- expect(vm.shouldRenderTabs).toEqual(false);
-
- done();
- });
- });
- });
-
- describe('shouldRenderButtons', () => {
- it('returns true when it has paths & has made the first request', done => {
- vm.hasMadeRequest = true;
-
- vm.$nextTick(() => {
- expect(vm.shouldRenderButtons).toEqual(true);
-
- done();
- });
- });
-
- it('returns false when it has not made the first request', done => {
- vm.hasMadeRequest = false;
-
- vm.$nextTick(() => {
- expect(vm.shouldRenderButtons).toEqual(false);
-
- done();
- });
- });
- });
- });
-
- describe('updates results when a staged is clicked', () => {
- beforeEach(() => {
- const copyPipeline = Object.assign({}, pipelineWithStages);
- copyPipeline.id += 1;
- mock
- .onGet('twitter/flight/pipelines.json')
- .reply(
- 200,
- {
- pipelines: [pipelineWithStages],
- count: {
- all: 1,
- finished: 1,
- pending: 0,
- running: 0,
- },
- },
- {
- 'POLL-INTERVAL': 100,
- },
- )
- .onGet(pipelineWithStages.details.stages[0].dropdown_path)
- .reply(200, stageReply);
-
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: true,
- canCreatePipeline: true,
- ...paths,
- });
- });
-
- describe('when a request is being made', () => {
- it('stops polling, cancels the request, & restarts polling', done => {
- spyOn(vm.poll, 'stop');
- spyOn(vm.poll, 'restart');
- spyOn(vm.service.cancelationSource, 'cancel').and.callThrough();
-
- setTimeout(() => {
- vm.isMakingRequest = true;
- return vm
- .$nextTick()
- .then(() => {
- vm.$el.querySelector('.js-builds-dropdown-button').click();
- })
- .then(() => {
- expect(vm.service.cancelationSource.cancel).toHaveBeenCalled();
- expect(vm.poll.stop).toHaveBeenCalled();
-
- setTimeout(() => {
- expect(vm.poll.restart).toHaveBeenCalled();
- done();
- }, 0);
- })
- .catch(done.fail);
- }, 0);
- });
- });
-
- describe('when no request is being made', () => {
- it('stops polling & restarts polling', done => {
- spyOn(vm.poll, 'stop');
- spyOn(vm.poll, 'restart');
-
- setTimeout(() => {
- vm.$el.querySelector('.js-builds-dropdown-button').click();
-
- expect(vm.poll.stop).toHaveBeenCalled();
-
- setTimeout(() => {
- expect(vm.poll.restart).toHaveBeenCalled();
- done();
- }, 0);
- }, 0);
- });
- });
- });
-});
diff --git a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
index 9033b71b19f..f82e49f9323 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
@@ -5,11 +5,13 @@ require 'spec_helper'
describe Gitlab::Ci::Pipeline::Chain::Sequence do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
+
let(:pipeline) { build_stubbed(:ci_pipeline) }
let(:command) { Gitlab::Ci::Pipeline::Chain::Command.new }
let(:first_step) { spy('first step') }
let(:second_step) { spy('second step') }
let(:sequence) { [first_step, second_step] }
+ let(:histogram) { spy('prometheus metric') }
subject do
described_class.new(pipeline, command, sequence)
@@ -52,5 +54,13 @@ describe Gitlab::Ci::Pipeline::Chain::Sequence do
it 'returns a pipeline object' do
expect(subject.build!).to eq pipeline
end
+
+ it 'adds sequence duration to duration histogram' do
+ allow(command).to receive(:duration_histogram).and_return(histogram)
+
+ subject.build!
+
+ expect(histogram).to have_received(:observe)
+ end
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb
index a11a9d08503..2a4a911cf38 100644
--- a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb
@@ -3,6 +3,10 @@
require 'spec_helper'
describe Gitlab::Kubernetes::Helm::BaseCommand do
+ subject(:base_command) do
+ test_class.new(rbac)
+ end
+
let(:application) { create(:clusters_applications_helm) }
let(:rbac) { false }
@@ -30,87 +34,17 @@ describe Gitlab::Kubernetes::Helm::BaseCommand do
end
end
- let(:base_command) do
- test_class.new(rbac)
- end
-
- subject { base_command }
-
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) { '' }
end
- describe '#pod_resource' do
- subject { base_command.pod_resource }
-
- it 'returns a kubeclient resoure with pod content for application' do
- is_expected.to be_an_instance_of ::Kubeclient::Resource
- end
-
- context 'when rbac is true' do
- let(:rbac) { true }
-
- it 'also returns a kubeclient resource' do
- is_expected.to be_an_instance_of ::Kubeclient::Resource
- end
- end
- end
-
describe '#pod_name' do
subject { base_command.pod_name }
it { is_expected.to eq('install-test-class-name') }
end
- describe '#service_account_resource' do
- let(:resource) do
- Kubeclient::Resource.new(metadata: { name: 'tiller', namespace: 'gitlab-managed-apps' })
- end
-
- subject { base_command.service_account_resource }
-
- context 'rbac is enabled' do
- let(:rbac) { true }
-
- it 'generates a Kubeclient resource for the tiller ServiceAccount' do
- is_expected.to eq(resource)
- end
- end
-
- context 'rbac is not enabled' do
- let(:rbac) { false }
-
- it 'generates nothing' do
- is_expected.to be_nil
- end
- end
- end
-
- describe '#cluster_role_binding_resource' do
- let(:resource) do
- Kubeclient::Resource.new(
- metadata: { name: 'tiller-admin' },
- roleRef: { apiGroup: 'rbac.authorization.k8s.io', kind: 'ClusterRole', name: 'cluster-admin' },
- subjects: [{ kind: 'ServiceAccount', name: 'tiller', namespace: 'gitlab-managed-apps' }]
- )
- end
-
- subject { base_command.cluster_role_binding_resource }
-
- context 'rbac is enabled' do
- let(:rbac) { true }
-
- it 'generates a Kubeclient resource for the ClusterRoleBinding for tiller' do
- is_expected.to eq(resource)
- end
- end
-
- context 'rbac is not enabled' do
- let(:rbac) { false }
-
- it 'generates nothing' do
- is_expected.to be_nil
- end
- end
+ it_behaves_like 'helm command' do
+ let(:command) { base_command }
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb
index 82e15864687..e1ca56b0ba6 100644
--- a/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb
@@ -3,14 +3,13 @@
require 'spec_helper'
describe Gitlab::Kubernetes::Helm::DeleteCommand do
+ subject(:delete_command) { described_class.new(name: app_name, rbac: rbac, files: files) }
+
let(:app_name) { 'app-name' }
let(:rbac) { true }
let(:files) { {} }
- let(:delete_command) { described_class.new(name: app_name, rbac: rbac, files: files) }
-
- subject { delete_command }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
export HELM_HOST="localhost:44134"
@@ -26,7 +25,7 @@ describe Gitlab::Kubernetes::Helm::DeleteCommand do
stub_feature_flags(managed_apps_local_tiller: false)
end
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
helm init --upgrade
@@ -48,7 +47,7 @@ describe Gitlab::Kubernetes::Helm::DeleteCommand do
EOS
end
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
helm init --upgrade
@@ -67,29 +66,13 @@ describe Gitlab::Kubernetes::Helm::DeleteCommand do
end
end
- describe '#pod_resource' do
- subject { delete_command.pod_resource }
-
- context 'rbac is enabled' do
- let(:rbac) { true }
-
- it 'generates a pod that uses the tiller serviceAccountName' do
- expect(subject.spec.serviceAccountName).to eq('tiller')
- end
- end
-
- context 'rbac is not enabled' do
- let(:rbac) { false }
-
- it 'generates a pod that uses the default serviceAccountName' do
- expect(subject.spec.serviceAcccountName).to be_nil
- end
- end
- end
-
describe '#pod_name' do
subject { delete_command.pod_name }
it { is_expected.to eq('uninstall-app-name') }
end
+
+ it_behaves_like 'helm command' do
+ let(:command) { delete_command }
+ end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
index 13021a08f9f..05d9b63d12b 100644
--- a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
@@ -3,25 +3,24 @@
require 'spec_helper'
describe Gitlab::Kubernetes::Helm::InitCommand do
+ subject(:init_command) { described_class.new(name: application.name, files: files, rbac: rbac) }
+
let(:application) { create(:clusters_applications_helm) }
let(:rbac) { false }
let(:files) { {} }
- let(:init_command) { described_class.new(name: application.name, files: files, rbac: rbac) }
- let(:commands) do
- <<~EOS
- helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem
- EOS
+ it_behaves_like 'helm command generator' do
+ let(:commands) do
+ <<~EOS
+ helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem
+ EOS
+ end
end
- subject { init_command }
-
- it_behaves_like 'helm commands'
-
context 'on a rbac-enabled cluster' do
let(:rbac) { true }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem --service-account tiller
@@ -30,57 +29,7 @@ describe Gitlab::Kubernetes::Helm::InitCommand do
end
end
- describe '#rbac?' do
- subject { init_command.rbac? }
-
- context 'rbac is enabled' do
- let(:rbac) { true }
-
- it { is_expected.to be_truthy }
- end
-
- context 'rbac is not enabled' do
- let(:rbac) { false }
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#config_map_resource' do
- let(:metadata) do
- {
- name: 'values-content-configuration-helm',
- namespace: 'gitlab-managed-apps',
- labels: { name: 'values-content-configuration-helm' }
- }
- end
-
- let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: files) }
-
- subject { init_command.config_map_resource }
-
- it 'returns a KubeClient resource with config map content for the application' do
- is_expected.to eq(resource)
- end
- end
-
- describe '#pod_resource' do
- subject { init_command.pod_resource }
-
- context 'rbac is enabled' do
- let(:rbac) { true }
-
- it 'generates a pod that uses the tiller serviceAccountName' do
- expect(subject.spec.serviceAccountName).to eq('tiller')
- end
- end
-
- context 'rbac is not enabled' do
- let(:rbac) { false }
-
- it 'generates a pod that uses the default serviceAccountName' do
- expect(subject.spec.serviceAcccountName).to be_nil
- end
- end
+ it_behaves_like 'helm command' do
+ let(:command) { init_command }
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
index a5ed8f57bf3..abd29e97505 100644
--- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
@@ -3,14 +3,7 @@
require 'spec_helper'
describe Gitlab::Kubernetes::Helm::InstallCommand do
- let(:files) { { 'ca.pem': 'some file content' } }
- let(:repository) { 'https://repository.example.com' }
- let(:rbac) { false }
- let(:version) { '1.2.3' }
- let(:preinstall) { nil }
- let(:postinstall) { nil }
-
- let(:install_command) do
+ subject(:install_command) do
described_class.new(
name: 'app-name',
chart: 'chart-name',
@@ -23,9 +16,14 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
)
end
- subject { install_command }
+ let(:files) { { 'ca.pem': 'some file content' } }
+ let(:repository) { 'https://repository.example.com' }
+ let(:rbac) { false }
+ let(:version) { '1.2.3' }
+ let(:preinstall) { nil }
+ let(:postinstall) { nil }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
export HELM_HOST="localhost:44134"
@@ -66,7 +64,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
EOS
end
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
helm init --upgrade
@@ -97,7 +95,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
context 'when rbac is true' do
let(:rbac) { true }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
export HELM_HOST="localhost:44134"
@@ -128,7 +126,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
context 'when there is a pre-install script' do
let(:preinstall) { ['/bin/date', '/bin/true'] }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
export HELM_HOST="localhost:44134"
@@ -161,7 +159,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
context 'when there is a post-install script' do
let(:postinstall) { ['/bin/date', "/bin/false\n"] }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
export HELM_HOST="localhost:44134"
@@ -194,7 +192,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
context 'when there is no ca.pem file' do
let(:files) { { 'file.txt': 'some content' } }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
export HELM_HOST="localhost:44134"
@@ -225,7 +223,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
context 'when there is no version' do
let(:version) { nil }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
export HELM_HOST="localhost:44134"
@@ -252,57 +250,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
end
end
- describe '#rbac?' do
- subject { install_command.rbac? }
-
- context 'rbac is enabled' do
- let(:rbac) { true }
-
- it { is_expected.to be_truthy }
- end
-
- context 'rbac is not enabled' do
- let(:rbac) { false }
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#pod_resource' do
- subject { install_command.pod_resource }
-
- context 'rbac is enabled' do
- let(:rbac) { true }
-
- it 'generates a pod that uses the tiller serviceAccountName' do
- expect(subject.spec.serviceAccountName).to eq('tiller')
- end
- end
-
- context 'rbac is not enabled' do
- let(:rbac) { false }
-
- it 'generates a pod that uses the default serviceAccountName' do
- expect(subject.spec.serviceAcccountName).to be_nil
- end
- end
- end
-
- describe '#config_map_resource' do
- let(:metadata) do
- {
- name: "values-content-configuration-app-name",
- namespace: 'gitlab-managed-apps',
- labels: { name: "values-content-configuration-app-name" }
- }
- end
-
- let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: files) }
-
- subject { install_command.config_map_resource }
-
- it 'returns a KubeClient resource with config map content for the application' do
- is_expected.to eq(resource)
- end
+ it_behaves_like 'helm command' do
+ let(:command) { install_command }
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/patch_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/patch_command_spec.rb
index e69570f5371..eee842fa7d6 100644
--- a/spec/lib/gitlab/kubernetes/helm/patch_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/patch_command_spec.rb
@@ -33,7 +33,7 @@ describe Gitlab::Kubernetes::Helm::PatchCommand do
EOS
end
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
helm init --upgrade
@@ -57,7 +57,7 @@ describe Gitlab::Kubernetes::Helm::PatchCommand do
end
end
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
export HELM_HOST="localhost:44134"
@@ -83,7 +83,7 @@ describe Gitlab::Kubernetes::Helm::PatchCommand do
context 'when rbac is true' do
let(:rbac) { true }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
export HELM_HOST="localhost:44134"
@@ -110,7 +110,7 @@ describe Gitlab::Kubernetes::Helm::PatchCommand do
context 'when there is no ca.pem file' do
let(:files) { { 'file.txt': 'some content' } }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
export HELM_HOST="localhost:44134"
@@ -134,69 +134,19 @@ describe Gitlab::Kubernetes::Helm::PatchCommand do
end
end
- describe '#pod_name' do
- subject { patch_command.pod_name }
-
- it { is_expected.to eq 'install-app-name' }
- end
-
context 'when there is no version' do
let(:version) { nil }
it { expect { patch_command }.to raise_error(ArgumentError, 'version is required') }
end
- describe '#rbac?' do
- subject { patch_command.rbac? }
-
- context 'rbac is enabled' do
- let(:rbac) { true }
-
- it { is_expected.to be_truthy }
- end
-
- context 'rbac is not enabled' do
- let(:rbac) { false }
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#pod_resource' do
- subject { patch_command.pod_resource }
-
- context 'rbac is enabled' do
- let(:rbac) { true }
-
- it 'generates a pod that uses the tiller serviceAccountName' do
- expect(subject.spec.serviceAccountName).to eq('tiller')
- end
- end
-
- context 'rbac is not enabled' do
- let(:rbac) { false }
+ describe '#pod_name' do
+ subject { patch_command.pod_name }
- it 'generates a pod that uses the default serviceAccountName' do
- expect(subject.spec.serviceAcccountName).to be_nil
- end
- end
+ it { is_expected.to eq 'install-app-name' }
end
- describe '#config_map_resource' do
- let(:metadata) do
- {
- name: "values-content-configuration-app-name",
- namespace: 'gitlab-managed-apps',
- labels: { name: "values-content-configuration-app-name" }
- }
- end
-
- let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: files) }
-
- subject { patch_command.config_map_resource }
-
- it 'returns a KubeClient resource with config map content for the application' do
- is_expected.to eq(resource)
- end
+ it_behaves_like 'helm command' do
+ let(:command) { patch_command }
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb
index 2a89b04723d..981bb4e4abf 100644
--- a/spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb
@@ -3,14 +3,13 @@
require 'spec_helper'
describe Gitlab::Kubernetes::Helm::ResetCommand do
+ subject(:reset_command) { described_class.new(name: name, rbac: rbac, files: files) }
+
let(:rbac) { true }
let(:name) { 'helm' }
let(:files) { {} }
- let(:reset_command) { described_class.new(name: name, rbac: rbac, files: files) }
-
- subject { reset_command }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
helm reset
@@ -23,7 +22,7 @@ describe Gitlab::Kubernetes::Helm::ResetCommand do
context 'when there is a ca.pem file' do
let(:files) { { 'ca.pem': 'some file content' } }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS1.squish + "\n" + <<~EOS2
helm reset
@@ -39,29 +38,13 @@ describe Gitlab::Kubernetes::Helm::ResetCommand do
end
end
- describe '#pod_resource' do
- subject { reset_command.pod_resource }
-
- context 'rbac is enabled' do
- let(:rbac) { true }
-
- it 'generates a pod that uses the tiller serviceAccountName' do
- expect(subject.spec.serviceAccountName).to eq('tiller')
- end
- end
-
- context 'rbac is not enabled' do
- let(:rbac) { false }
-
- it 'generates a pod that uses the default serviceAccountName' do
- expect(subject.spec.serviceAcccountName).to be_nil
- end
- end
- end
-
describe '#pod_name' do
subject { reset_command.pod_name }
it { is_expected.to eq('uninstall-helm') }
end
+
+ it_behaves_like 'helm command' do
+ let(:command) { reset_command }
+ end
end
diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb
index 16a05af2216..a249b3a235e 100644
--- a/spec/lib/gitlab/visibility_level_spec.rb
+++ b/spec/lib/gitlab/visibility_level_spec.rb
@@ -96,6 +96,30 @@ describe Gitlab::VisibilityLevel do
end
end
+ describe '.restricted_level?, .non_restricted_level?, and .public_level_restricted?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:visibility_levels, :expected_status) do
+ nil | false
+ [Gitlab::VisibilityLevel::PRIVATE] | false
+ [Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::INTERNAL] | false
+ [Gitlab::VisibilityLevel::PUBLIC] | true
+ [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL] | true
+ end
+
+ with_them do
+ before do
+ stub_application_setting(restricted_visibility_levels: visibility_levels)
+ end
+
+ it 'returns the expected status' do
+ expect(described_class.restricted_level?(Gitlab::VisibilityLevel::PUBLIC)).to eq(expected_status)
+ expect(described_class.non_restricted_level?(Gitlab::VisibilityLevel::PUBLIC)).to eq(!expected_status)
+ expect(described_class.public_visibility_restricted?).to eq(expected_status)
+ end
+ end
+ end
+
describe '#visibility_level_decreased?' do
let(:project) { create(:project, :internal) }
diff --git a/spec/models/jira_import_state_spec.rb b/spec/models/jira_import_state_spec.rb
index 4d91bf25b5e..99f9e035205 100644
--- a/spec/models/jira_import_state_spec.rb
+++ b/spec/models/jira_import_state_spec.rb
@@ -124,6 +124,7 @@ describe JiraImportState do
jira_import.schedule
expect(jira_import.jid).to eq('some-job-id')
+ expect(jira_import.scheduled_at).to be_within(1.second).of(Time.now)
end
end
diff --git a/spec/requests/api/graphql/project/jira_import_spec.rb b/spec/requests/api/graphql/project/jira_import_spec.rb
index 2e631fb56ba..e063068eb1a 100644
--- a/spec/requests/api/graphql/project/jira_import_spec.rb
+++ b/spec/requests/api/graphql/project/jira_import_spec.rb
@@ -18,6 +18,7 @@ describe 'query Jira import data' do
jiraImports {
nodes {
jiraProjectKey
+ createdAt
scheduledAt
scheduledBy {
username
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 07e7a48d8c4..95d64ee8124 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -60,14 +60,14 @@ 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,
@@ -89,7 +89,9 @@ describe API::Settings, 'Settings' do
push_event_hooks_limit: 2,
push_event_activities_limit: 2,
snippet_size_limit: 5,
- issues_create_limit: 300
+ issues_create_limit: 300,
+ disabled_oauth_sign_in_sources: 'unknown',
+ import_sources: 'github,bitbucket'
}
expect(response).to have_gitlab_http_status(:ok)
@@ -127,6 +129,8 @@ describe API::Settings, 'Settings' do
expect(json_response['push_event_activities_limit']).to eq(2)
expect(json_response['snippet_size_limit']).to eq(5)
expect(json_response['issues_create_limit']).to eq(300)
+ 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..0199377f104
--- /dev/null
+++ b/spec/rubocop/cop/api/grape_api_instance_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rubocop'
+require_relative '../../../support/helpers/expect_offense'
+require_relative '../../../../rubocop/cop/api/grape_api_instance'
+
+describe RuboCop::Cop::API::GrapeAPIInstance do
+ include CopHelper
+ include ExpectOffense
+
+ subject(:cop) { described_class.new }
+
+ it 'adds an offense when inheriting from Grape::API' do
+ inspect_source(<<~CODE.strip_indent)
+ 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.strip_indent)
+ 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..8252e07837d
--- /dev/null
+++ b/spec/rubocop/cop/api/grape_array_missing_coerce_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rubocop'
+require_relative '../../../support/helpers/expect_offense'
+require_relative '../../../../rubocop/cop/api/grape_array_missing_coerce'
+
+describe RuboCop::Cop::API::GrapeArrayMissingCoerce do
+ include CopHelper
+ include ExpectOffense
+
+ subject(:cop) { described_class.new }
+
+ it 'adds an offense with a required parameter' do
+ inspect_source(<<~CODE.strip_indent)
+ 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.strip_indent)
+ 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.strip_indent)
+ 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.strip_indent)
+ 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 97acaeb7643..9005b5a0611 100644
--- a/spec/rubocop/cop/code_reuse/worker_spec.rb
+++ b/spec/rubocop/cop/code_reuse/worker_spec.rb
@@ -31,7 +31,7 @@ describe RuboCop::Cop::CodeReuse::Worker 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/rubocop/cop/gitlab/json_spec.rb b/spec/rubocop/cop/gitlab/json_spec.rb
new file mode 100644
index 00000000000..d64f60c8583
--- /dev/null
+++ b/spec/rubocop/cop/gitlab/json_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../../rubocop/cop/gitlab/json'
+
+describe RuboCop::Cop::Gitlab::Json do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ shared_examples('registering call offense') do |options|
+ let(:offending_lines) { options[:offending_lines] }
+
+ it 'registers an offense when the class calls JSON' do
+ inspect_source(source)
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(offending_lines.size)
+ expect(cop.offenses.map(&:line)).to eq(offending_lines)
+ end
+ end
+ end
+
+ context 'when JSON is called' do
+ it_behaves_like 'registering call offense', offending_lines: [3] do
+ let(:source) do
+ <<~RUBY
+ class Foo
+ def bar
+ JSON.parse('{ "foo": "bar" }')
+ end
+ end
+ RUBY
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/helm_commands_shared_examples.rb b/spec/support/shared_examples/helm_commands_shared_examples.rb
new file mode 100644
index 00000000000..f0624fbf29f
--- /dev/null
+++ b/spec/support/shared_examples/helm_commands_shared_examples.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+shared_examples 'helm command generator' do
+ describe '#generate_script' do
+ let(:helm_setup) do
+ <<~EOS
+ set -xeo pipefail
+ EOS
+ end
+
+ it 'returns appropriate command' do
+ expect(subject.generate_script.strip).to eq((helm_setup + commands).strip)
+ end
+ end
+end
+
+shared_examples 'helm command' do
+ describe '#rbac?' do
+ subject { command.rbac? }
+
+ context 'rbac is enabled' do
+ let(:rbac) { true }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'rbac is not enabled' do
+ let(:rbac) { false }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#pod_resource' do
+ subject { command.pod_resource }
+
+ context 'rbac is enabled' do
+ let(:rbac) { true }
+
+ it { is_expected.to be_an_instance_of ::Kubeclient::Resource }
+
+ it 'generates a pod that uses the tiller serviceAccountName' do
+ expect(subject.spec.serviceAccountName).to eq('tiller')
+ end
+ end
+
+ context 'rbac is not enabled' do
+ let(:rbac) { false }
+
+ it { is_expected.to be_an_instance_of ::Kubeclient::Resource }
+
+ it 'generates a pod that uses the default serviceAccountName' do
+ expect(subject.spec.serviceAcccountName).to be_nil
+ end
+ end
+ end
+
+ describe '#config_map_resource' do
+ subject { command.config_map_resource }
+
+ let(:metadata) do
+ {
+ name: "values-content-configuration-#{command.name}",
+ namespace: 'gitlab-managed-apps',
+ labels: { name: "values-content-configuration-#{command.name}" }
+ }
+ end
+
+ let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: command.files) }
+
+ it 'returns a KubeClient resource with config map content for the application' do
+ is_expected.to eq(resource)
+ end
+ end
+
+ describe '#service_account_resource' do
+ let(:resource) do
+ Kubeclient::Resource.new(metadata: { name: 'tiller', namespace: 'gitlab-managed-apps' })
+ end
+
+ subject { command.service_account_resource }
+
+ context 'rbac is enabled' do
+ let(:rbac) { true }
+
+ it 'generates a Kubeclient resource for the tiller ServiceAccount' do
+ is_expected.to eq(resource)
+ end
+ end
+
+ context 'rbac is not enabled' do
+ let(:rbac) { false }
+
+ it 'generates nothing' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#cluster_role_binding_resource' do
+ let(:resource) do
+ Kubeclient::Resource.new(
+ metadata: { name: 'tiller-admin' },
+ roleRef: { apiGroup: 'rbac.authorization.k8s.io', kind: 'ClusterRole', name: 'cluster-admin' },
+ subjects: [{ kind: 'ServiceAccount', name: 'tiller', namespace: 'gitlab-managed-apps' }]
+ )
+ end
+
+ subject(:cluster_role_binding_resource) { command.cluster_role_binding_resource }
+
+ context 'rbac is enabled' do
+ let(:rbac) { true }
+
+ it 'generates a Kubeclient resource for the ClusterRoleBinding for tiller' do
+ is_expected.to eq(resource)
+ end
+
+ it 'binds the account in #service_account_resource' do
+ expect(cluster_role_binding_resource.subjects.first.name).to eq(command.service_account_resource.metadata.name)
+ end
+ end
+
+ context 'rbac is not enabled' do
+ let(:rbac) { false }
+
+ it 'generates nothing' do
+ is_expected.to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/helm_generated_script_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/helm_generated_script_shared_examples.rb
deleted file mode 100644
index bbf8a946f8b..00000000000
--- a/spec/support/shared_examples/lib/gitlab/helm_generated_script_shared_examples.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'helm commands' do
- describe '#generate_script' do
- let(:helm_setup) do
- <<~EOS
- set -xeo pipefail
- EOS
- end
-
- it 'returns appropriate command' do
- expect(subject.generate_script.strip).to eq((helm_setup + commands).strip)
- end
- end
-end