From f986ce9ffa56e25d0a3010c78d9481664742d766 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 23 Mar 2021 18:09:05 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .rubocop_todo.yml | 9 + Gemfile | 6 +- Gemfile.lock | 12 +- .../javascripts/pages/admin/services/edit/index.js | 6 +- .../javascripts/pipelines/components/dag/dag.vue | 4 + .../components/graph/stage_column_component.vue | 19 ++- .../pipelines/components/unwrapping_utils.js | 2 +- .../javascripts/releases/components/app_show.vue | 56 +++++- app/assets/javascripts/releases/mount_show.js | 28 +-- .../releases/stores/modules/detail/actions.js | 4 +- app/controllers/graphql_controller.rb | 5 +- app/controllers/projects/forks_controller.rb | 10 +- app/controllers/projects/pipelines_controller.rb | 1 + app/controllers/projects/releases_controller.rb | 1 - .../token_authenticatable_strategies/encrypted.rb | 8 +- app/serializers/fork_namespace_entity.rb | 2 +- app/validators/json_schema_validator.rb | 11 +- .../application_setting_kroki_formats.json | 1 + .../json_schemas/build_metadata_secrets.json | 1 + .../json_schemas/build_report_result_data.json | 1 + .../build_report_result_data_tests.json | 1 + app/validators/json_schemas/codeclimate.json | 1 + .../daily_build_group_report_result_data.json | 1 + app/validators/json_schemas/debian_fields.json | 1 + app/validators/json_schemas/git_trailers.json | 1 + ...http_integration_payload_attribute_mapping.json | 1 + .../sast_ui_schema.json | 1 + ...3431-set-traversal_ids-for-gitlab-org-group.yml | 5 + ...ure-flag-enable-individual-jira-issue-pages.yml | 5 - .../322592-clean-up-a-token_with_ivs-table.yml | 6 + changelogs/unreleased/change-json-validation.yml | 5 + ...a-properties-to-external-validation-payload.yml | 5 + ...friend-reorganize-release-detail-page-store.yml | 5 + .../development/dynamic_nonce_creation.yml | 8 - .../graphql_individual_release_page.yml | 8 - .../development/jira_issues_show_integration.yml | 8 - .../development/pipeline_filter_jobs.yml | 8 + ...t_traversal_ids_for_gitlab_org_group_staging.rb | 88 ++++++++++ db/schema_migrations/20210311045138 | 1 + doc/administration/external_pipeline_validation.md | 29 +++- doc/development/usage_ping/dictionary.md | 24 +++ .../jira-upload-app-success_v13_11.png | Bin 0 -> 11440 bytes .../integrations/jira-upload-app_v13_11.png | Bin 0 -> 20667 bytes doc/user/project/integrations/jira.md | 29 +--- doc/user/project/integrations/jira_integrations.md | 144 +++++++++++++--- jest.config.base.js | 12 +- jest.config.integration.js | 10 +- .../groups/pipelines/labels_pipeline.rb | 11 -- .../groups/pipelines/members_pipeline.rb | 11 -- .../groups/pipelines/milestones_pipeline.rb | 11 -- lib/bulk_imports/pipeline/runner.rb | 27 ++- lib/gitlab/ci/pipeline/chain/validate/external.rb | 34 ++-- lib/gitlab/ci/reports/codequality_reports.rb | 5 +- lib/gitlab/crypto_helper.rb | 6 +- .../graphql/query_analyzers/logger_analyzer.rb | 7 +- .../usage/metrics/names_suggestions/generator.rb | 130 ++++++++++++-- .../names_suggestions/relation_parsers/joins.rb | 74 ++++++++ .../known_events/epic_events.yml | 6 + locale/gitlab.pot | 2 +- spec/controllers/graphql_controller_spec.rb | 32 +++- spec/controllers/projects/forks_controller_spec.rb | 41 +++-- .../projects/releases/user_views_release_spec.rb | 43 ++--- spec/fixtures/api/schemas/external_validation.json | 15 +- spec/frontend/releases/components/app_show_spec.js | 189 ++++++++++++++++----- .../releases/stores/modules/detail/actions_spec.js | 2 +- .../groups/pipelines/labels_pipeline_spec.rb | 71 +++----- .../groups/pipelines/members_pipeline_spec.rb | 11 +- .../groups/pipelines/milestones_pipeline_spec.rb | 80 +++------ .../pipelines/subgroup_entities_pipeline_spec.rb | 18 +- spec/lib/bulk_imports/pipeline/runner_spec.rb | 46 +++-- .../ci/parsers/codequality/code_climate_spec.rb | 1 - .../ci/pipeline/chain/validate/external_spec.rb | 24 ++- .../gitlab/ci/reports/codequality_reports_spec.rb | 2 - spec/lib/gitlab/crypto_helper_spec.rb | 48 ------ .../query_analyzers/logger_analyzer_spec.rb | 47 ++--- .../metrics/names_suggestions/generator_spec.rb | 39 ++++- .../relation_parsers/joins_spec.rb | 40 +++++ .../encrypted_spec.rb | 4 - spec/requests/api/graphql/gitlab_schema_spec.rb | 1 + spec/requests/api/graphql_spec.rb | 1 + spec/support/helpers/graphql_helpers.rb | 7 +- spec/validators/json_schema_validator_spec.rb | 30 ---- 82 files changed, 1121 insertions(+), 589 deletions(-) create mode 100644 changelogs/unreleased/233431-set-traversal_ids-for-gitlab-org-group.yml delete mode 100644 changelogs/unreleased/299832-feature-flag-enable-individual-jira-issue-pages.yml create mode 100644 changelogs/unreleased/322592-clean-up-a-token_with_ivs-table.yml create mode 100644 changelogs/unreleased/change-json-validation.yml create mode 100644 changelogs/unreleased/ci-add-extra-properties-to-external-validation-payload.yml create mode 100644 changelogs/unreleased/nfriend-reorganize-release-detail-page-store.yml delete mode 100644 config/feature_flags/development/dynamic_nonce_creation.yml delete mode 100644 config/feature_flags/development/graphql_individual_release_page.yml delete mode 100644 config/feature_flags/development/jira_issues_show_integration.yml create mode 100644 config/feature_flags/development/pipeline_filter_jobs.yml create mode 100644 db/post_migrate/20210311045138_set_traversal_ids_for_gitlab_org_group_staging.rb create mode 100644 db/schema_migrations/20210311045138 create mode 100644 doc/user/project/integrations/jira-upload-app-success_v13_11.png create mode 100644 doc/user/project/integrations/jira-upload-app_v13_11.png create mode 100644 lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins.rb create mode 100644 spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins_spec.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 35778f056b4..d6982146426 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -323,6 +323,10 @@ Performance/Detect: Performance/MethodObjectAsBlock: Enabled: false +# Offense count: 42 +Performance/OpenStruct: + Enabled: false + # Offense count: 18 # Cop supports --auto-correct. # Configuration parameters: AutoCorrect. @@ -376,6 +380,11 @@ RSpec/EmptyExampleGroup: - 'ee/spec/services/personal_access_tokens/revoke_invalid_tokens_spec.rb' - 'spec/services/projects/prometheus/alerts/notify_service_spec.rb' +# Offense count: 1162 +# Cop supports --auto-correct. +RSpec/EmptyLineAfterFinalLetItBe: + Enabled: false + # Offense count: 1428 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle. diff --git a/Gemfile b/Gemfile index c8240c3cdc2..522122b13e4 100644 --- a/Gemfile +++ b/Gemfile @@ -377,7 +377,7 @@ group :development, :test do gem 'spring', '~> 2.1.0' gem 'spring-commands-rspec', '~> 1.0.4' - gem 'gitlab-styles', '~> 6.1.0', require: false + gem 'gitlab-styles', '~> 6.2.0', require: false gem 'haml_lint', '~> 0.36.0', require: false gem 'bundler-audit', '~> 0.7.0.1', require: false @@ -414,6 +414,7 @@ group :development, :test, :omnibus do end group :test do + gem 'json-schema', '~> 2.8.0' gem 'fuubar', '~> 2.2.0' gem 'rspec-retry', '~> 0.6.1' gem 'rspec_profiling', '~> 0.0.6' @@ -488,7 +489,7 @@ gem 'flipper', '~> 0.17.1' gem 'flipper-active_record', '~> 0.17.1' gem 'flipper-active_support_cache_store', '~> 0.17.1' gem 'unleash', '~> 0.1.5' -gem 'gitlab-experiment', '~> 0.5.0' +gem 'gitlab-experiment', '~> 0.5.1' # Structured logging gem 'lograge', '~> 0.5' @@ -520,7 +521,6 @@ gem 'valid_email', '~> 0.1' # JSON gem 'json', '~> 2.3.0' -gem 'json-schema', '~> 2.8.0' gem 'json_schemer', '~> 0.2.12' gem 'oj', '~> 3.10.6' gem 'multi_json', '~> 1.14.1' diff --git a/Gemfile.lock b/Gemfile.lock index aa08e41631d..5d63cb1cb2d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -438,9 +438,9 @@ GEM numerizer (~> 0.2) gitlab-dangerfiles (0.8.0) danger - gitlab-experiment (0.5.0) + gitlab-experiment (0.5.1) activesupport (>= 3.0) - scientist (~> 1.5, >= 1.5.0) + scientist (~> 1.6, >= 1.6.0) gitlab-fog-azure-rm (1.0.1) azure-storage-blob (~> 2.0) azure-storage-common (~> 2.0) @@ -472,7 +472,7 @@ GEM pry (~> 0.13.0) gitlab-sidekiq-fetcher (0.5.5) sidekiq (~> 5) - gitlab-styles (6.1.0) + gitlab-styles (6.2.0) rubocop (~> 0.91, >= 0.91.1) rubocop-gitlab-security (~> 0.1.1) rubocop-performance (~> 1.9.2) @@ -643,7 +643,7 @@ GEM activesupport (>= 4.2) aes_key_wrap bindata - json-schema (2.8.0) + json-schema (2.8.1) addressable (>= 2.4) json_schemer (0.2.12) ecma-re-validator (~> 0.2) @@ -1422,7 +1422,7 @@ DEPENDENCIES github-markup (~> 1.7.0) gitlab-chronic (~> 0.10.5) gitlab-dangerfiles (~> 0.8.0) - gitlab-experiment (~> 0.5.0) + gitlab-experiment (~> 0.5.1) gitlab-fog-azure-rm (~> 1.0.1) gitlab-fog-google (~> 1.13) gitlab-labkit (~> 0.16.2) @@ -1432,7 +1432,7 @@ DEPENDENCIES gitlab-net-dns (~> 0.9.1) gitlab-pry-byebug gitlab-sidekiq-fetcher (= 0.5.5) - gitlab-styles (~> 6.1.0) + gitlab-styles (~> 6.2.0) gitlab_chronic_duration (~> 0.10.6.2) gitlab_omniauth-ldap (~> 2.1.1) gon (~> 6.2) diff --git a/app/assets/javascripts/pages/admin/services/edit/index.js b/app/assets/javascripts/pages/admin/services/edit/index.js index 3d692ef4dcc..b8080ddff77 100644 --- a/app/assets/javascripts/pages/admin/services/edit/index.js +++ b/app/assets/javascripts/pages/admin/services/edit/index.js @@ -1,6 +1,4 @@ import IntegrationSettingsForm from '~/integrations/integration_settings_form'; -document.addEventListener('DOMContentLoaded', () => { - const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); - integrationSettingsForm.init(); -}); +const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); +integrationSettingsForm.init(); diff --git a/app/assets/javascripts/pipelines/components/dag/dag.vue b/app/assets/javascripts/pipelines/components/dag/dag.vue index e44dedfe2ee..16fb931ec2b 100644 --- a/app/assets/javascripts/pipelines/components/dag/dag.vue +++ b/app/assets/javascripts/pipelines/components/dag/dag.vue @@ -50,6 +50,10 @@ export default { }; }, update(data) { + if (!data?.project?.pipeline) { + return this.graphData; + } + const { stages: { nodes: stages }, } = data.project.pipeline; diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index 0a762563114..66467dbc994 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -1,5 +1,6 @@ diff --git a/app/assets/javascripts/releases/mount_show.js b/app/assets/javascripts/releases/mount_show.js index f3ed7d6c5ff..7272880197a 100644 --- a/app/assets/javascripts/releases/mount_show.js +++ b/app/assets/javascripts/releases/mount_show.js @@ -1,26 +1,28 @@ import Vue from 'vue'; -import Vuex from 'vuex'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; import ReleaseShowApp from './components/app_show.vue'; -import createStore from './stores'; -import createDetailModule from './stores/modules/detail'; -Vue.use(Vuex); +Vue.use(VueApollo); + +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), +}); export default () => { const el = document.getElementById('js-show-release-page'); - const store = createStore({ - modules: { - detail: createDetailModule(el.dataset), - }, - featureFlags: { - graphqlIndividualReleasePage: Boolean(gon.features?.graphqlIndividualReleasePage), - }, - }); + if (!el) return false; + + const { projectPath, tagName } = el.dataset; return new Vue({ el, - store, + apolloProvider, + provide: { + fullPath: projectPath, + tagName, + }, render: (h) => h(ReleaseShowApp), }); }; diff --git a/app/assets/javascripts/releases/stores/modules/detail/actions.js b/app/assets/javascripts/releases/stores/modules/detail/actions.js index 5fa002706c6..8dc2083dd2b 100644 --- a/app/assets/javascripts/releases/stores/modules/detail/actions.js +++ b/app/assets/javascripts/releases/stores/modules/detail/actions.js @@ -43,7 +43,7 @@ export const fetchRelease = ({ commit, state, rootState }) => { }) .catch((error) => { commit(types.RECEIVE_RELEASE_ERROR, error); - createFlash(s__('Release|Something went wrong while getting the release details')); + createFlash(s__('Release|Something went wrong while getting the release details.')); }); } @@ -54,7 +54,7 @@ export const fetchRelease = ({ commit, state, rootState }) => { }) .catch((error) => { commit(types.RECEIVE_RELEASE_ERROR, error); - createFlash(s__('Release|Something went wrong while getting the release details')); + createFlash(s__('Release|Something went wrong while getting the release details.')); }); }; diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 82005c548f2..a13ec1daddb 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -146,8 +146,7 @@ class GraphqlController < ApplicationController end def logs - RequestStore.store[:graphql_logs].to_h - .except(:duration_s, :query_string) - .merge(operation_name: params[:operationName]) + RequestStore.store[:graphql_logs].to_a + .map { |log| log.except(:duration_s, :query_string) } end end diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb index 005bc2a385b..b999110181b 100644 --- a/app/controllers/projects/forks_controller.rb +++ b/app/controllers/projects/forks_controller.rb @@ -44,13 +44,17 @@ class Projects::ForksController < Projects::ApplicationController def new respond_to do |format| format.html do - @own_namespace = current_user.namespace if fork_service.valid_fork_targets.include?(current_user.namespace) + @own_namespace = current_user.namespace if can_fork_to?(current_user.namespace) @project = project end format.json do namespaces = load_namespaces_with_associations - [project.namespace] + namespaces = [current_user.namespace] + namespaces if + Feature.enabled?(:fork_project_form, project, default_enabled: :yaml) && + can_fork_to?(current_user.namespace) + render json: { namespaces: ForkNamespaceSerializer.new.represent(namespaces, project: project, current_user: current_user, memberships: memberships_hash) } @@ -78,6 +82,10 @@ class Projects::ForksController < Projects::ApplicationController private + def can_fork_to?(namespace) + ForkTargetsFinder.new(@project, current_user).execute.id_in(current_user.namespace).any? + end + def load_forks forks = ForkProjectsFinder.new( project, diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 35d138fc27b..e1c2efc3760 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -15,6 +15,7 @@ class Projects::PipelinesController < Projects::ApplicationController before_action do push_frontend_feature_flag(:new_pipeline_form, project, default_enabled: :yaml) push_frontend_feature_flag(:pipeline_graph_layers_view, project, type: :development, default_enabled: :yaml) + push_frontend_feature_flag(:pipeline_filter_jobs, project, default_enabled: :yaml) push_frontend_feature_flag(:graphql_pipeline_details, project, type: :development, default_enabled: :yaml) push_frontend_feature_flag(:graphql_pipeline_details_users, current_user, type: :development, default_enabled: :yaml) push_frontend_feature_flag(:jira_for_vulnerabilities, project, type: :development, default_enabled: :yaml) diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb index 614bada09ed..26382856761 100644 --- a/app/controllers/projects/releases_controller.rb +++ b/app/controllers/projects/releases_controller.rb @@ -12,7 +12,6 @@ class Projects::ReleasesController < Projects::ApplicationController push_frontend_feature_flag(:graphql_release_data, project, default_enabled: true) push_frontend_feature_flag(:graphql_milestone_stats, project, default_enabled: true) push_frontend_feature_flag(:graphql_releases_page, project, default_enabled: true) - push_frontend_feature_flag(:graphql_individual_release_page, project, default_enabled: true) end before_action :authorize_update_release!, only: %i[edit update] before_action :authorize_create_release!, only: :new diff --git a/app/models/concerns/token_authenticatable_strategies/encrypted.rb b/app/models/concerns/token_authenticatable_strategies/encrypted.rb index 672402ee4d6..b59396a323c 100644 --- a/app/models/concerns/token_authenticatable_strategies/encrypted.rb +++ b/app/models/concerns/token_authenticatable_strategies/encrypted.rb @@ -85,18 +85,12 @@ module TokenAuthenticatableStrategies end def find_by_encrypted_token(token, unscoped) - nonce = Feature.enabled?(:dynamic_nonce_creation) ? find_hashed_iv(token) : Gitlab::CryptoHelper::AES256_GCM_IV_STATIC + nonce = Gitlab::CryptoHelper::AES256_GCM_IV_STATIC encrypted_value = Gitlab::CryptoHelper.aes256_gcm_encrypt(token, nonce: nonce) relation(unscoped).find_by(encrypted_field => encrypted_value) end - def find_hashed_iv(token) - token_record = TokenWithIv.find_by_plaintext_token(token) - - token_record&.iv || Gitlab::CryptoHelper::AES256_GCM_IV_STATIC - end - def insecure_strategy @insecure_strategy ||= TokenAuthenticatableStrategies::Insecure .new(klass, token_field, options) diff --git a/app/serializers/fork_namespace_entity.rb b/app/serializers/fork_namespace_entity.rb index abfaf4be811..fc238fa3958 100644 --- a/app/serializers/fork_namespace_entity.rb +++ b/app/serializers/fork_namespace_entity.rb @@ -23,7 +23,7 @@ class ForkNamespaceEntity < Grape::Entity end expose :relative_path do |namespace| - polymorphic_path(namespace) + group_path(namespace) end expose :markdown_description do |namespace| diff --git a/app/validators/json_schema_validator.rb b/app/validators/json_schema_validator.rb index 742839f5f5b..8dc6265f471 100644 --- a/app/validators/json_schema_validator.rb +++ b/app/validators/json_schema_validator.rb @@ -12,7 +12,6 @@ class JsonSchemaValidator < ActiveModel::EachValidator FILENAME_ALLOWED = /\A[a-z0-9_-]*\Z/.freeze FilenameError = Class.new(StandardError) - JSON_VALIDATOR_MAX_DRAFT_VERSION = 4 BASE_DIRECTORY = %w(app validators json_schemas).freeze def initialize(options) @@ -35,11 +34,11 @@ class JsonSchemaValidator < ActiveModel::EachValidator attr_reader :base_directory def valid_schema?(value) - if draft_version > JSON_VALIDATOR_MAX_DRAFT_VERSION - JSONSchemer.schema(Pathname.new(schema_path)).valid?(value) - else - JSON::Validator.validate(schema_path, value) - end + validator.valid?(value) + end + + def validator + @validator ||= JSONSchemer.schema(Pathname.new(schema_path)) end def schema_path diff --git a/app/validators/json_schemas/application_setting_kroki_formats.json b/app/validators/json_schemas/application_setting_kroki_formats.json index 460dc74069f..4dfa710abea 100644 --- a/app/validators/json_schemas/application_setting_kroki_formats.json +++ b/app/validators/json_schemas/application_setting_kroki_formats.json @@ -1,4 +1,5 @@ { + "$schema": "http://json-schema.org/draft-07/schema#", "description": "Kroki formats", "type": "object", "properties": { diff --git a/app/validators/json_schemas/build_metadata_secrets.json b/app/validators/json_schemas/build_metadata_secrets.json index e745a266777..799e7ab1642 100644 --- a/app/validators/json_schemas/build_metadata_secrets.json +++ b/app/validators/json_schemas/build_metadata_secrets.json @@ -1,4 +1,5 @@ { + "$schema": "http://json-schema.org/draft-07/schema#", "description": "CI builds metadata secrets", "type": "object", "patternProperties": { diff --git a/app/validators/json_schemas/build_report_result_data.json b/app/validators/json_schemas/build_report_result_data.json index 0fb4fd6d0b7..0a12c9c39a7 100644 --- a/app/validators/json_schemas/build_report_result_data.json +++ b/app/validators/json_schemas/build_report_result_data.json @@ -1,4 +1,5 @@ { + "$schema": "http://json-schema.org/draft-07/schema#", "description": "Build report result data", "type": "object", "properties": { diff --git a/app/validators/json_schemas/build_report_result_data_tests.json b/app/validators/json_schemas/build_report_result_data_tests.json index b38559e727f..610070fde5f 100644 --- a/app/validators/json_schemas/build_report_result_data_tests.json +++ b/app/validators/json_schemas/build_report_result_data_tests.json @@ -1,4 +1,5 @@ { + "$schema": "http://json-schema.org/draft-07/schema#", "description": "Build report result data tests", "type": "object", "properties": { diff --git a/app/validators/json_schemas/codeclimate.json b/app/validators/json_schemas/codeclimate.json index 56056c62c4e..dc43eab6290 100644 --- a/app/validators/json_schemas/codeclimate.json +++ b/app/validators/json_schemas/codeclimate.json @@ -1,4 +1,5 @@ { + "$schema": "http://json-schema.org/draft-07/schema#", "description": "Codequality used by codeclimate parser", "type": "object", "required": ["description", "fingerprint", "severity", "location"], diff --git a/app/validators/json_schemas/daily_build_group_report_result_data.json b/app/validators/json_schemas/daily_build_group_report_result_data.json index 2524ac63050..2b073506375 100644 --- a/app/validators/json_schemas/daily_build_group_report_result_data.json +++ b/app/validators/json_schemas/daily_build_group_report_result_data.json @@ -1,4 +1,5 @@ { + "$schema": "http://json-schema.org/draft-07/schema#", "description": "Daily build group report result data", "type": "object", "properties": { diff --git a/app/validators/json_schemas/debian_fields.json b/app/validators/json_schemas/debian_fields.json index b9f6ad2b31d..ae1a2726ea2 100644 --- a/app/validators/json_schemas/debian_fields.json +++ b/app/validators/json_schemas/debian_fields.json @@ -1,4 +1,5 @@ { + "$schema": "http://json-schema.org/draft-07/schema#", "description": "Debian fields", "type": "object", "patternProperties": { diff --git a/app/validators/json_schemas/git_trailers.json b/app/validators/json_schemas/git_trailers.json index 18ac97226a7..384eb280765 100644 --- a/app/validators/json_schemas/git_trailers.json +++ b/app/validators/json_schemas/git_trailers.json @@ -1,4 +1,5 @@ { + "$schema": "http://json-schema.org/draft-07/schema#", "description": "Git trailer key/value pairs", "type": "object", "patternProperties": { diff --git a/app/validators/json_schemas/http_integration_payload_attribute_mapping.json b/app/validators/json_schemas/http_integration_payload_attribute_mapping.json index a194daf5e45..7aebc959169 100644 --- a/app/validators/json_schemas/http_integration_payload_attribute_mapping.json +++ b/app/validators/json_schemas/http_integration_payload_attribute_mapping.json @@ -1,4 +1,5 @@ { + "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "patternProperties": { ".*": { diff --git a/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json b/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json index 08442565931..99961d7264b 100644 --- a/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json +++ b/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json @@ -1,4 +1,5 @@ { + "$schema": "http://json-schema.org/draft-07/schema#", "global": [ { "field" : "SECURE_ANALYZERS_PREFIX", diff --git a/changelogs/unreleased/233431-set-traversal_ids-for-gitlab-org-group.yml b/changelogs/unreleased/233431-set-traversal_ids-for-gitlab-org-group.yml new file mode 100644 index 00000000000..55d56116625 --- /dev/null +++ b/changelogs/unreleased/233431-set-traversal_ids-for-gitlab-org-group.yml @@ -0,0 +1,5 @@ +--- +title: Backfill traversal_ids for gitlab-org staging +merge_request: 56293 +author: +type: performance diff --git a/changelogs/unreleased/299832-feature-flag-enable-individual-jira-issue-pages.yml b/changelogs/unreleased/299832-feature-flag-enable-individual-jira-issue-pages.yml deleted file mode 100644 index 9146488d42d..00000000000 --- a/changelogs/unreleased/299832-feature-flag-enable-individual-jira-issue-pages.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Enable :jira_issues_show_integration feature flag by default -merge_request: 56182 -author: -type: added diff --git a/changelogs/unreleased/322592-clean-up-a-token_with_ivs-table.yml b/changelogs/unreleased/322592-clean-up-a-token_with_ivs-table.yml new file mode 100644 index 00000000000..962d42cb36d --- /dev/null +++ b/changelogs/unreleased/322592-clean-up-a-token_with_ivs-table.yml @@ -0,0 +1,6 @@ +--- +title: Remove referencing TokenWithIv model in the codebase and dynamic nonce creation + feature flag +merge_request: 55209 +author: +type: changed diff --git a/changelogs/unreleased/change-json-validation.yml b/changelogs/unreleased/change-json-validation.yml new file mode 100644 index 00000000000..a86233004ea --- /dev/null +++ b/changelogs/unreleased/change-json-validation.yml @@ -0,0 +1,5 @@ +--- +title: Stop using json-schema gem for production +merge_request: 56745 +author: +type: other diff --git a/changelogs/unreleased/ci-add-extra-properties-to-external-validation-payload.yml b/changelogs/unreleased/ci-add-extra-properties-to-external-validation-payload.yml new file mode 100644 index 00000000000..6461f6c7a3a --- /dev/null +++ b/changelogs/unreleased/ci-add-extra-properties-to-external-validation-payload.yml @@ -0,0 +1,5 @@ +--- +title: Add extra fields to the external pipeline validation payload +merge_request: 56969 +author: +type: changed diff --git a/changelogs/unreleased/nfriend-reorganize-release-detail-page-store.yml b/changelogs/unreleased/nfriend-reorganize-release-detail-page-store.yml new file mode 100644 index 00000000000..0b3fc4ccf1e --- /dev/null +++ b/changelogs/unreleased/nfriend-reorganize-release-detail-page-store.yml @@ -0,0 +1,5 @@ +--- +title: Remove graphql_individual_release_page feature flag +merge_request: 56882 +author: +type: removed diff --git a/config/feature_flags/development/dynamic_nonce_creation.yml b/config/feature_flags/development/dynamic_nonce_creation.yml deleted file mode 100644 index b135f288554..00000000000 --- a/config/feature_flags/development/dynamic_nonce_creation.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: dynamic_nonce_creation -introduced_by_url: -rollout_issue_url: -milestone: '13.9' -type: development -group: group::manage -default_enabled: false diff --git a/config/feature_flags/development/graphql_individual_release_page.yml b/config/feature_flags/development/graphql_individual_release_page.yml deleted file mode 100644 index 8cf13ca4854..00000000000 --- a/config/feature_flags/development/graphql_individual_release_page.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: graphql_individual_release_page -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44779 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/263522 -milestone: '13.5' -type: development -group: group::release -default_enabled: true diff --git a/config/feature_flags/development/jira_issues_show_integration.yml b/config/feature_flags/development/jira_issues_show_integration.yml deleted file mode 100644 index dd89ace22be..00000000000 --- a/config/feature_flags/development/jira_issues_show_integration.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: jira_issues_show_integration -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52446 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/299832 -milestone: '13.9' -type: development -group: group::ecosystem -default_enabled: false diff --git a/config/feature_flags/development/pipeline_filter_jobs.yml b/config/feature_flags/development/pipeline_filter_jobs.yml new file mode 100644 index 00000000000..6fb989a6815 --- /dev/null +++ b/config/feature_flags/development/pipeline_filter_jobs.yml @@ -0,0 +1,8 @@ +--- +name: pipeline_filter_jobs +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57142 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/325693 +milestone: '13.11' +type: development +group: group::pipeline authoring +default_enabled: false diff --git a/db/post_migrate/20210311045138_set_traversal_ids_for_gitlab_org_group_staging.rb b/db/post_migrate/20210311045138_set_traversal_ids_for_gitlab_org_group_staging.rb new file mode 100644 index 00000000000..bcf872ded54 --- /dev/null +++ b/db/post_migrate/20210311045138_set_traversal_ids_for_gitlab_org_group_staging.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +class SetTraversalIdsForGitlabOrgGroupStaging < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + return unless Gitlab.staging? + + # namespace ID 9970 is gitlab-org on staging. + with_lock_retries do + execute(<<~SQL) + UPDATE + namespaces + SET + traversal_ids = cte.traversal_ids + FROM + ( + WITH RECURSIVE cte(id, traversal_ids, cycle) AS ( + VALUES + (9970, ARRAY[9970], false) + UNION ALL + SELECT + n.id, + cte.traversal_ids || n.id, + n.id = ANY(cte.traversal_ids) + FROM + namespaces n, + cte + WHERE + n.parent_id = cte.id + AND NOT cycle + ) + SELECT + id, + traversal_ids + FROM + cte FOR + UPDATE + ) as cte + WHERE + namespaces.id = cte.id + AND namespaces.traversal_ids <> cte.traversal_ids + SQL + end + end + + def down + return unless Gitlab.staging? + + # namespace ID 9970 is gitlab-org on staging. + with_lock_retries do + execute(<<~SQL) + UPDATE + namespaces + SET + traversal_ids = '{}' + FROM + ( + WITH RECURSIVE cte(id, traversal_ids, cycle) AS ( + VALUES + (9970, ARRAY[9970], false) + UNION ALL + SELECT + n.id, + cte.traversal_ids || n.id, + n.id = ANY(cte.traversal_ids) + FROM + namespaces n, + cte + WHERE + n.parent_id = cte.id + AND NOT cycle + ) + SELECT + id, + traversal_ids + FROM + cte FOR + UPDATE + ) as cte + WHERE + namespaces.id = cte.id + SQL + end + end +end diff --git a/db/schema_migrations/20210311045138 b/db/schema_migrations/20210311045138 new file mode 100644 index 00000000000..3dcf40429f9 --- /dev/null +++ b/db/schema_migrations/20210311045138 @@ -0,0 +1 @@ +01bbe2af2bc6bdaa6bf1e2fe10557e3f9f969cc60a348f188fbfe126ea7ea97d \ No newline at end of file diff --git a/doc/administration/external_pipeline_validation.md b/doc/administration/external_pipeline_validation.md index 44cbb626f0c..c0b0556e0b1 100644 --- a/doc/administration/external_pipeline_validation.md +++ b/doc/administration/external_pipeline_validation.md @@ -38,18 +38,21 @@ Set the `EXTERNAL_VALIDATION_SERVICE_URL` to the external service URL and enable "project", "user", "pipeline", - "builds" + "builds", + "namespace" ], "properties" : { "project": { "type": "object", "required": [ "id", - "path" + "path", + "created_at" ], "properties": { "id": { "type": "integer" }, - "path": { "type": "string" } + "path": { "type": "string" }, + "created_at": { "type": ["string", "null"], "format": "date-time" } } }, "user": { @@ -57,12 +60,14 @@ Set the `EXTERNAL_VALIDATION_SERVICE_URL` to the external service URL and enable "required": [ "id", "username", - "email" + "email", + "created_at" ], "properties": { "id": { "type": "integer" }, "username": { "type": "string" }, - "email": { "type": "string" } + "email": { "type": "string" }, + "created_at": { "type": ["string", "null"], "format": "date-time" } } }, "pipeline": { @@ -103,8 +108,18 @@ Set the `EXTERNAL_VALIDATION_SERVICE_URL` to the external service URL and enable } } } + }, + "namespace": { + "type": "object", + "required": [ + "plan", + "trial" + ], + "properties": { + "plan": { "type": "string" }, + "trial": { "type": "boolean" } + } } - }, - "additionalProperties": false + } } ``` diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md index 02417041d2a..45a1c5c29fa 100644 --- a/doc/development/usage_ping/dictionary.md +++ b/doc/development/usage_ping/dictionary.md @@ -9836,6 +9836,30 @@ Status: `implemented` Tiers: `premium`, `ultimate` +### `redis_hll_counters.epics_usage.g_project_management_epic_issue_added_monthly` + +Count of MAU adding issues to epics + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210312144719_g_product_planning_epic_issue_added_monthly.yml) + +Group: `group::product planning` + +Status: `implemented` + +Tiers: `premium`, `ultimate` + +### `redis_hll_counters.epics_usage.g_project_management_epic_issue_added_weekly` + +Count of WAU adding issues to epics + +[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210312181918_g_product_planning_epic_issue_added_weekly.yml) + +Group: `group::product planning` + +Status: `implemented` + +Tiers: `premium`, `ultimate` + ### `redis_hll_counters.epics_usage.g_project_management_users_destroying_epic_notes_monthly` Counts of MAU destroying epic notes diff --git a/doc/user/project/integrations/jira-upload-app-success_v13_11.png b/doc/user/project/integrations/jira-upload-app-success_v13_11.png new file mode 100644 index 00000000000..c0d4c9744b6 Binary files /dev/null and b/doc/user/project/integrations/jira-upload-app-success_v13_11.png differ diff --git a/doc/user/project/integrations/jira-upload-app_v13_11.png b/doc/user/project/integrations/jira-upload-app_v13_11.png new file mode 100644 index 00000000000..88d1573f778 Binary files /dev/null and b/doc/user/project/integrations/jira-upload-app_v13_11.png differ diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md index 3acbc4e6fad..ad31e7ca784 100644 --- a/doc/user/project/integrations/jira.md +++ b/doc/user/project/integrations/jira.md @@ -261,14 +261,8 @@ Issues are grouped into tabs based on their [Jira status](https://confluence.atl #### View a Jira issue -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/299832) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.10. -> - It's [deployed behind a feature flag](../../feature_flags.md), disabled by default. -> - It's enabled on GitLab.com. -> - It's recommended for production use. -> - For GitLab self-managed instances, GitLab administrators can opt to [enable it](#enable-or-disable-jira-issue-detail-view). **(PREMIUM)** - -WARNING: -This feature might not be available to you. Check the **version history** note above for details. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/299832) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.10 behind a feature flag, disabled by default. +> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/299832) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.11. When viewing the [Jira issues list](#view-jira-issues), select an issue from the list to open it in GitLab: @@ -323,22 +317,3 @@ which may lead to a `401 unauthorized` error when testing your Jira integration. If CAPTCHA has been triggered, you can't use Jira's REST API to authenticate with the Jira site. You need to log in to your Jira instance and complete the CAPTCHA. - -## Enable or disable Jira issue detail view - -Jira issue detail view is under development but ready for production use. It is -deployed behind a feature flag that is **disabled by default**. -[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) -can enable it. - -To enable it: - -```ruby -Feature.enable(:jira_issues_show_integration) -``` - -To disable it: - -```ruby -Feature.disable(:jira_issues_show_integration) -``` diff --git a/doc/user/project/integrations/jira_integrations.md b/doc/user/project/integrations/jira_integrations.md index 1f895a9e2fa..6e8c0a07fde 100644 --- a/doc/user/project/integrations/jira_integrations.md +++ b/doc/user/project/integrations/jira_integrations.md @@ -6,29 +6,121 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Jira integrations **(FREE)** -If your organization uses [Jira](https://www.atlassian.com/software/jira) issues, -you can [migrate](../../../user/project/import/jira.md) your issues from Jira and work -exclusively in GitLab. - -However, if you'd like to continue to use Jira, you can integrate it with GitLab. - -There are two ways to use GitLab with Jira: - -- [Jira integration](jira.md). Connect a GitLab project - to a Jira instance. The Jira instance can be hosted by you or in [Atlassian cloud](https://www.atlassian.com/cloud). -- [Jira Development panel integration](../../../integration/jira_development_panel.md). - Connect all GitLab projects under a group or personal namespace. - -The integration you choose depends on the capabilities you require. -You can also install both at the same time. - -| Capability | Jira integration | Jira Development panel integration | -|-|-|-| -| Mention a Jira issue ID in GitLab and a link to the Jira issue is created. | Yes. | No. | -| Mention a Jira issue ID in GitLab and the Jira issue shows the GitLab issue or merge request. | Yes. A Jira comment with the GitLab issue or MR title links to GitLab. The first mention is also added to the Jira issue under **Web links**. | Yes, in the issue's Development panel. | -| Mention a Jira issue ID in a GitLab commit message and the Jira issue shows the commit message. | Yes. The entire commit message is displayed in the Jira issue as a comment and under **Web links**. Each message links back to the commit in GitLab. | Yes, in the issue's Development panel and optionally with a custom comment on the Jira issue using Jira Smart Commits. | -| Mention a Jira issue ID in a GitLab branch name and the Jira issue shows the branch name. | No. | Yes, in the issue's Development panel. | -| Add Jira time tracking to an issue. | No. | Yes. Time can be specified using Jira Smart Commits. | -| Use a Git commit or merge request to transition or close a Jira issue. | Yes. Only a single transition type, typically configured to close the issue by setting it to Done. | Yes. Transition to any state using Jira Smart Commits. | -| Display a list of Jira issues. | Yes. **(PREMIUM)** | No. | -| Create a Jira issue from a vulnerability or finding. **(ULTIMATE)** | Yes. | No. | +GitLab can be integrated with [Jira](https://www.atlassian.com/software/jira). + +[Issues](../issues/index.md) are a tool for discussing ideas, and planning and tracking work. +However, your organization may already use Jira for these purposes, with extensive, established data +and business processes they rely on. + +Although you can [migrate](../../../user/project/import/jira.md) your Jira issues and work +exclusively in GitLab, you can also continue to use Jira by using the GitLab Jira integrations. + +## Integration types + +There are two different Jira integrations that allow different types of cross-referencing between +GitLab activity and Jira issues, with additional features: + +- [Jira integration](jira.md), built in to GitLab. In a given GitLab project, it can be configured + to connect to any Jira instance, either hosted by you or hosted in + [Atlassian cloud](https://www.atlassian.com/cloud). +- [Jira development panel integration](../../../integration/jira_development_panel.md). Connects all + GitLab projects under a specified group or personal namespace. + +Jira development panel integration configuration depends on whether you are +using Jira on [Atlassian cloud](https://www.atlassian.com/cloud) or on your own server: + +- *If your Jira instance is hosted on Atlassian Cloud:* + - **GitLab.com (SaaS) customers**: Use the + [GitLab.com for Jira Cloud](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?hosting=cloud&tab=overview) + application installed from the [Atlassian Marketplace](https://marketplace.atlassian.com). + - **Self-managed installs**: Use the + [GitLab.com for Jira Cloud](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud?hosting=cloud&tab=overview), with + [this workaround process](#install-the-gitlab-jira-cloud-application-for-self-managed-instances). Read the + [relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/268278) for more information. +- *If your Jira instance is hosted on your own server:* + Use the [Jira DVCS connector](../../../integration/jira_development_panel.md). + +### Install the GitLab Jira Cloud application for self-managed instances **(FREE SELF)** + +If your GitLab instance is self-managed, you must follow some +extra steps to install the GitLab Jira Cloud application. + +Each Jira Cloud application must be installed from a single location. Jira fetches +a [manifest file](https://developer.atlassian.com/cloud/jira/platform/connect-app-descriptor/) +from the location you provide. The manifest file describes the application to the system. To support +self-managed GitLab instances with Jira Cloud, you can either: + +- [Install the application manually](#install-the-application-manually). +- [Create a Marketplace listing](#create-a-marketplace-listing). + +#### Install the application manually **(FREE SELF)** + +You can configure your Atlassian Cloud instance to allow you to install applications +from outside the Marketplace, which allows you to install the application: + +1. Sign in to your Jira instance as a user with administrator permissions. +1. Place your Jira instance into + [development mode](https://developer.atlassian.com/cloud/jira/platform/getting-started-with-connect/#step-2--enable-development-mode). +1. Sign in to your GitLab application as a user with [Administrator](../../permissions.md) permissions. +1. Install the GitLab application from your self-managed GitLab instance, as + described in the [Atlassian developer guides](https://developer.atlassian.com/cloud/jira/platform/getting-started-with-connect/#step-3--install-and-test-your-app)). +1. In your Jira instance, go to **Apps > Manage Apps** and click **Upload app**: + + ![Image showing button labeled "upload app"](jira-upload-app_v13_11.png) + +1. For **App descriptor URL**, provide full URL to your manifest file, modifying this + URL based on your instance configuration: `https://your.domain/your-path/-/jira_connect/app_descriptor.json` +1. Click **Upload**, and Jira fetches the content of your `app_descriptor` file and installs + it for you. +1. If the upload is successful, Jira displays a modal panel: **Installed and ready to go!** + Click **Get started** to configure the integration. + + ![Image showing success modal](jira-upload-app-success_v13_11.png) + +The **GitLab for Jira** app now displays under **Manage apps**. You can also +click **Get started** to open the configuration page rendered from your GitLab instance. + +NOTE: +If you make changes to the application descriptor, you must uninstall, then reinstall, the +application. + +#### Create a Marketplace listing **(FREE SELF)** + +If you prefer to not use development mode on your Jira instance, you can create +your own Marketplace listing for your instance, which enables your application +to be installed from the Atlassian Marketplace. + +For full instructions, review the Atlassian [guide to creating a marketplace listing](https://developer.atlassian.com/platform/marketplace/installing-cloud-apps/#creating-the-marketplace-listing). To create a +Marketplace listing, you must: + +1. Register as a Marketplace vendor. +1. List your application, using the application descriptor URL. + - Your manifest file is located at: `https://your.domain/your-path/-/jira_connect/app_descriptor.json` + - GitLab recommends you list your application as `private`, because public + applications can be viewed and installed by any user. +1. Generate test license tokens for your application. + +Review the +[official Atlassian documentation](https://developer.atlassian.com/platform/marketplace/installing-cloud-apps/#creating-the-marketplace-listing) +for details. + +NOTE: +DVCS means distributed version control system. + +## Feature comparison + +The integration to use depends on the capabilities you require. You can install both at the same +time. + +| Capability | Jira integration | Jira Development Panel integration | +|:----------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------| +| Mention of Jira issue ID in GitLab is automatically linked to that issue | Yes | No | +| Mention of Jira issue ID in GitLab issue/MR is reflected in the Jira issue | Yes, as a Jira comment with the GitLab issue/MR title and a link back to it. Its first mention also adds the GitLab page to the Jira issue under “Web links”. | Yes, in the issue's Development panel | +| Mention of Jira issue ID in GitLab commit message is reflected in the issue | Yes. The entire commit message is added to the Jira issue as a comment and under “Web links”, each with a link back to the commit in GitLab. | Yes, in the issue's Development panel and optionally with a custom comment on the Jira issue using Jira Smart Commits. | +| Mention of Jira issue ID in GitLab branch names is reflected in Jira issue | No | Yes, in the issue's Development panel | +| Pipeline status is shown in Jira issue | No | Yes, in the issue's Development panel when using Jira Cloud and the GitLab application. | +| Deployment status is shown in Jira issue | No | Yes, in the issue's Development panel when using Jira Cloud and the GitLab application. | +| Record Jira time tracking information against an issue | No | Yes. Time can be specified via Jira Smart Commits. | +| Transition or close a Jira issue with a Git commit or merge request | Yes. Only a single transition type, typically configured to close the issue by setting it to Done. | Yes. Transition to any state using Jira Smart Commits. | +| Display a list of Jira issues | Yes **(PREMIUM)** | No | +| Create a Jira issue from a vulnerability or finding **(ULTIMATE)** | Yes | No | diff --git a/jest.config.base.js b/jest.config.base.js index 745a179af6d..5b7ab4d9276 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -1,7 +1,12 @@ const IS_EE = require('./config/helpers/is_ee_env'); const isESLint = require('./config/helpers/is_eslint'); -module.exports = (path) => { +module.exports = (path, options = {}) => { + const { + moduleNameMapper: extModuleNameMapper = {}, + moduleNameMapperEE: extModuleNameMapperEE = {}, + } = options; + const reporters = ['default']; // To have consistent date time parsing both in local and CI environments we set @@ -45,8 +50,7 @@ module.exports = (path) => { 'emojis(/.*).json': '/fixtures/emojis$1.json', '^spec/test_constants$': '/spec/frontend/__helpers__/test_constants', '^jest/(.*)$': '/spec/frontend/$1', - '^test_helpers(/.*)$': '/spec/frontend_integration/test_helpers$1', - '^ee_else_ce_test_helpers(/.*)$': '/spec/frontend_integration/test_helpers$1', + ...extModuleNameMapper, }; const collectCoverageFrom = ['/app/assets/javascripts/**/*.{js,vue}']; @@ -57,9 +61,9 @@ module.exports = (path) => { '^ee(/.*)$': rootDirEE, '^ee_component(/.*)$': rootDirEE, '^ee_else_ce(/.*)$': rootDirEE, - '^ee_else_ce_test_helpers(/.*)$': '/ee/spec/frontend_integration/test_helpers$1', '^ee_jest/(.*)$': '/ee/spec/frontend/$1', [TEST_FIXTURES_PATTERN]: '/tmp/tests/frontend/fixtures-ee$1', + ...extModuleNameMapperEE, }); collectCoverageFrom.push(rootDirEE.replace('$1', '/**/*.{js,vue}')); diff --git a/jest.config.integration.js b/jest.config.integration.js index 573002c1a34..d85e14fe218 100644 --- a/jest.config.integration.js +++ b/jest.config.integration.js @@ -1,5 +1,13 @@ const baseConfig = require('./jest.config.base'); module.exports = { - ...baseConfig('spec/frontend_integration'), + ...baseConfig('spec/frontend_integration', { + moduleNameMapper: { + '^test_helpers(/.*)$': '/spec/frontend_integration/test_helpers$1', + '^ee_else_ce_test_helpers(/.*)$': '/spec/frontend_integration/test_helpers$1', + }, + moduleNameMapperEE: { + '^ee_else_ce_test_helpers(/.*)$': '/ee/spec/frontend_integration/test_helpers$1', + }, + }), }; diff --git a/lib/bulk_imports/groups/pipelines/labels_pipeline.rb b/lib/bulk_imports/groups/pipelines/labels_pipeline.rb index 61d3e6c700e..0dc4a968b84 100644 --- a/lib/bulk_imports/groups/pipelines/labels_pipeline.rb +++ b/lib/bulk_imports/groups/pipelines/labels_pipeline.rb @@ -14,17 +14,6 @@ module BulkImports def load(context, data) Labels::CreateService.new(data).execute(group: context.group) end - - def after_run(extracted_data) - tracker.update( - has_next_page: extracted_data.has_next_page?, - next_page: extracted_data.next_page - ) - - if extracted_data.has_next_page? - run - end - end end end end diff --git a/lib/bulk_imports/groups/pipelines/members_pipeline.rb b/lib/bulk_imports/groups/pipelines/members_pipeline.rb index d29bd74c5ae..5e4293d2c06 100644 --- a/lib/bulk_imports/groups/pipelines/members_pipeline.rb +++ b/lib/bulk_imports/groups/pipelines/members_pipeline.rb @@ -17,17 +17,6 @@ module BulkImports context.group.members.create!(data) end - - def after_run(extracted_data) - tracker.update( - has_next_page: extracted_data.has_next_page?, - next_page: extracted_data.next_page - ) - - if extracted_data.has_next_page? - run - end - end end end end diff --git a/lib/bulk_imports/groups/pipelines/milestones_pipeline.rb b/lib/bulk_imports/groups/pipelines/milestones_pipeline.rb index eb51424c14a..9b2be30735c 100644 --- a/lib/bulk_imports/groups/pipelines/milestones_pipeline.rb +++ b/lib/bulk_imports/groups/pipelines/milestones_pipeline.rb @@ -19,17 +19,6 @@ module BulkImports context.group.milestones.create!(data) end - def after_run(extracted_data) - tracker.update( - has_next_page: extracted_data.has_next_page?, - next_page: extracted_data.next_page - ) - - if extracted_data.has_next_page? - run - end - end - private def authorized? diff --git a/lib/bulk_imports/pipeline/runner.rb b/lib/bulk_imports/pipeline/runner.rb index 588d2c87209..b756fba3bee 100644 --- a/lib/bulk_imports/pipeline/runner.rb +++ b/lib/bulk_imports/pipeline/runner.rb @@ -14,19 +14,24 @@ module BulkImports extracted_data = extracted_data_from - extracted_data&.each do |entry| - transformers.each do |transformer| - entry = run_pipeline_step(:transformer, transformer.class.name) do - transformer.transform(context, entry) + if extracted_data + extracted_data.each do |entry| + transformers.each do |transformer| + entry = run_pipeline_step(:transformer, transformer.class.name) do + transformer.transform(context, entry) + end end - end - run_pipeline_step(:loader, loader.class.name) do - loader.load(context, entry) + run_pipeline_step(:loader, loader.class.name) do + loader.load(context, entry) + end end - end - if extracted_data && respond_to?(:after_run) + tracker.update!( + has_next_page: extracted_data.has_next_page?, + next_page: extracted_data.next_page + ) + run_pipeline_step(:after_run) do after_run(extracted_data) end @@ -65,6 +70,10 @@ module BulkImports end end + def after_run(extracted_data) + run if extracted_data.has_next_page? + end + def mark_as_failed warn(message: 'Pipeline failed') diff --git a/lib/gitlab/ci/pipeline/chain/validate/external.rb b/lib/gitlab/ci/pipeline/chain/validate/external.rb index b2fbe43aa77..41ff5fddba6 100644 --- a/lib/gitlab/ci/pipeline/chain/validate/external.rb +++ b/lib/gitlab/ci/pipeline/chain/validate/external.rb @@ -21,13 +21,13 @@ module Gitlab pipeline_authorized = validate_external log_message = pipeline_authorized ? 'authorized' : 'not authorized' - Gitlab::AppLogger.info(message: "Pipeline #{log_message}", project_id: @pipeline.project.id, user_id: @pipeline.user.id) + Gitlab::AppLogger.info(message: "Pipeline #{log_message}", project_id: project.id, user_id: current_user.id) error('External validation failed', drop_reason: :external_validation_failure) unless pipeline_authorized end def break? - @pipeline.errors.any? + pipeline.errors.any? end private @@ -71,7 +71,7 @@ module Gitlab def validate_service_request Gitlab::HTTP.post( validation_service_url, timeout: VALIDATION_REQUEST_TIMEOUT, - body: validation_service_payload(@pipeline, @command.yaml_processor_result.stages_attributes) + body: validation_service_payload.to_json ) end @@ -79,28 +79,30 @@ module Gitlab ENV['EXTERNAL_VALIDATION_SERVICE_URL'] end - def validation_service_payload(pipeline, stages_attributes) + def validation_service_payload { project: { - id: pipeline.project.id, - path: pipeline.project.full_path + id: project.id, + path: project.full_path, + created_at: project.created_at&.iso8601 }, user: { - id: pipeline.user.id, - username: pipeline.user.username, - email: pipeline.user.email + id: current_user.id, + username: current_user.username, + email: current_user.email, + created_at: current_user.created_at&.iso8601 }, pipeline: { sha: pipeline.sha, ref: pipeline.ref, type: pipeline.source }, - builds: builds_validation_payload(stages_attributes) - }.to_json + builds: builds_validation_payload + } end - def builds_validation_payload(stages_attributes) - stages_attributes.map { |stage| stage[:builds] }.flatten + def builds_validation_payload + stages_attributes.flat_map { |stage| stage[:builds] } .map(&method(:build_validation_payload)) end @@ -117,9 +119,15 @@ module Gitlab ].flatten.compact } end + + def stages_attributes + command.yaml_processor_result.stages_attributes + end end end end end end end + +Gitlab::Ci::Pipeline::Chain::Validate::External.prepend_if_ee('EE::Gitlab::Ci::Pipeline::Chain::Validate::External') diff --git a/lib/gitlab/ci/reports/codequality_reports.rb b/lib/gitlab/ci/reports/codequality_reports.rb index 060a1e2399b..ed7373a7d4b 100644 --- a/lib/gitlab/ci/reports/codequality_reports.rb +++ b/lib/gitlab/ci/reports/codequality_reports.rb @@ -32,9 +32,8 @@ module Gitlab private def valid_degradation?(degradation) - JSON::Validator.validate!(CODECLIMATE_SCHEMA_PATH, degradation) - rescue JSON::Schema::ValidationError => e - set_error_message("Invalid degradation format: #{e.message}") + JSONSchemer.schema(Pathname.new(CODECLIMATE_SCHEMA_PATH)).valid?(degradation) + rescue StandardError => _ false end end diff --git a/lib/gitlab/crypto_helper.rb b/lib/gitlab/crypto_helper.rb index 4428354642d..2b6a1c3c976 100644 --- a/lib/gitlab/crypto_helper.rb +++ b/lib/gitlab/crypto_helper.rb @@ -23,16 +23,12 @@ module Gitlab def aes256_gcm_decrypt(value) return unless value - nonce = Feature.enabled?(:dynamic_nonce_creation) ? dynamic_nonce(value) : AES256_GCM_IV_STATIC + nonce = AES256_GCM_IV_STATIC encrypted_token = Base64.decode64(value) decrypted_token = Encryptor.decrypt(AES256_GCM_OPTIONS.merge(value: encrypted_token, iv: nonce)) decrypted_token end - def dynamic_nonce(value) - TokenWithIv.find_nonce_by_hashed_token(value) || AES256_GCM_IV_STATIC - end - def aes256_gcm_encrypt_using_static_nonce(value) create_encrypted_token(value, AES256_GCM_IV_STATIC) end diff --git a/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb b/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb index 8acd27869a9..c6f22e0bd4f 100644 --- a/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb +++ b/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb @@ -12,6 +12,7 @@ module Gitlab def initial_value(query) variables = process_variables(query.provided_variables) default_initial_values(query).merge({ + operation_name: query.operation_name, query_string: query.query_string, variables: variables }) @@ -20,8 +21,8 @@ module Gitlab default_initial_values(query) end - def call(memo, visit_type, irep_node) - RequestStore.store[:graphql_logs] = memo + def call(memo, *) + memo end def final_value(memo) @@ -37,6 +38,8 @@ module Gitlab memo[:used_fields] = field_usages.first memo[:used_deprecated_fields] = field_usages.second + RequestStore.store[:graphql_logs] ||= [] + RequestStore.store[:graphql_logs] << memo GraphqlLogger.info(memo.except!(:time_started, :query)) rescue => e Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e) diff --git a/lib/gitlab/usage/metrics/names_suggestions/generator.rb b/lib/gitlab/usage/metrics/names_suggestions/generator.rb index 33f025770e0..84ca661b7a7 100644 --- a/lib/gitlab/usage/metrics/names_suggestions/generator.rb +++ b/lib/gitlab/usage/metrics/names_suggestions/generator.rb @@ -48,48 +48,144 @@ module Gitlab def name_suggestion(relation:, column: nil, prefix: nil, distinct: nil) parts = [prefix] - - if column - parts << parse_target(column) + arel_column = arelize_column(relation, column) + + # nil as column indicates that the counting would use fallback value of primary key. + # Because counting primary key from relation is the conceptual equal to counting all + # records from given relation, in order to keep name suggestion more condensed + # primary key column is skipped. + # eg: SELECT COUNT(id) FROM issues would translate as count_issues and not + # as count_id_from_issues since it does not add more information to the name suggestion + if arel_column != Arel::Table.new(relation.table_name)[relation.primary_key] + parts << arel_column.name parts << 'from' end - source = parse_source(relation) - constraints = parse_constraints(relation: relation, column: column, distinct: distinct) - - if constraints.include?(source) + arel = arel_query(relation: relation, column: arel_column, distinct: distinct) + constraints = parse_constraints(relation: relation, arel: arel) + + # In some cases due to performance reasons metrics are instrumented with joined relations + # where relation listed in FROM statement is not the one that includes counted attribute + # in such situations to make name suggestion more intuitive source should be inferred based + # on the relation that provide counted attribute + # EG: SELECT COUNT(deployments.environment_id) FROM clusters + # JOIN deployments ON deployments.cluster_id = cluster.id + # should be translated into: + # count_environment_id_from_deployments_with_clusters + # instead of + # count_environment_id_from_clusters_with_deployments + actual_source = parse_source(relation, arel_column) + + if constraints.include?(actual_source) parts << "" end - parts << source + parts << actual_source + parts += process_joined_relations(actual_source, arel, relation) parts.compact.join('_') end - def parse_constraints(relation:, column: nil, distinct: nil) + def parse_constraints(relation:, arel:) connection = relation.connection ::Gitlab::Usage::Metrics::NamesSuggestions::RelationParsers::Constraints .new(connection) - .accept(arel(relation: relation, column: column, distinct: distinct), collector(connection)) + .accept(arel, collector(connection)) .value end - def parse_target(column) - if column.is_a?(Arel::Attribute) - "#{column.relation.name}.#{column.name}" - else + # TODO: joins with `USING` keyword + def process_joined_relations(actual_source, arel, relation) + joins = parse_joins(connection: relation.connection, arel: arel) + return [] unless joins.any? + + sources = [relation.table_name, *joins.map { |join| join[:source] }] + joins = extract_joins_targets(joins, sources) + + relations = if actual_source != relation.table_name + build_relations_tree(joins + [{ source: relation.table_name }], actual_source) + else + # in case where counter attribute comes from joined relations, the relations + # diagram has to be built bottom up, thus source and target are reverted + build_relations_tree(joins + [{ source: relation.table_name }], actual_source, source_key: :target, target_key: :source) + end + + collect_join_parts(relations[actual_source]) + end + + def parse_joins(connection:, arel:) + ::Gitlab::Usage::Metrics::NamesSuggestions::RelationParsers::Joins + .new(connection) + .accept(arel) + end + + def extract_joins_targets(joins, sources) + joins.map do |join| + source_regex = /(#{join[:source]})\.(\w+_)*id/i + + tables_except_src = (sources - [join[:source]]).join('|') + target_regex = /(?#{tables_except_src})\.(\w+_)*id/i + + join_cond_regex = /(#{source_regex}\s+=\s+#{target_regex})|(#{target_regex}\s+=\s+#{source_regex})/i + matched = join_cond_regex.match(join[:constraints]) + + join[:target] = matched[:target] if matched + join + end + end + + def build_relations_tree(joins, parent, source_key: :source, target_key: :target) + return [] if joins.blank? + + tree = {} + tree[parent] = [] + + joins.each do |join| + if join[source_key] == parent + tree[parent] << build_relations_tree(joins - [join], join[target_key], source_key: source_key, target_key: target_key) + end + end + tree + end + + def collect_join_parts(joined_relations, parts = [], conjunctions = %w[with having including].cycle) + conjunction = conjunctions.next + joined_relations.each do |subtree| + subtree.each do |parent, children| + parts << "<#{conjunction}>" + parts << parent + collect_join_parts(children, parts, conjunctions) + end + end + parts + end + + def arelize_column(relation, column) + case column + when Arel::Attribute column + when NilClass + Arel::Table.new(relation.table_name)[relation.primary_key] + when String + if column.include?('.') + table, col = column.split('.') + Arel::Table.new(table)[col] + else + Arel::Table.new(relation.table_name)[column] + end + when Symbol + arelize_column(relation, column.to_s) end end - def parse_source(relation) - relation.table_name + def parse_source(relation, column) + column.relation.name || relation.table_name end def collector(connection) Arel::Collectors::SubstituteBinds.new(connection, Arel::Collectors::SQLString.new) end - def arel(relation:, column: nil, distinct: nil) + def arel_query(relation:, column: nil, distinct: nil) column ||= relation.primary_key if column.is_a?(Arel::Attribute) diff --git a/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins.rb b/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins.rb new file mode 100644 index 00000000000..d52e4903f3c --- /dev/null +++ b/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module Gitlab + module Usage + module Metrics + module NamesSuggestions + module RelationParsers + class Joins < ::Arel::Visitors::PostgreSQL + def accept(object) + object.source.right.map do |join| + visit(join, collector) + end + end + + private + + # rubocop:disable Naming/MethodName + def visit_Arel_Nodes_StringJoin(object, collector) + result = visit(object.left, collector) + source, constraints = result.value.split('ON') + { + source: source.split('JOIN').last&.strip, + constraints: constraints&.strip + }.compact + end + + def visit_Arel_Nodes_FullOuterJoin(object, _) + parse_join(object) + end + + def visit_Arel_Nodes_OuterJoin(object, _) + parse_join(object) + end + + def visit_Arel_Nodes_RightOuterJoin(object, _) + parse_join(object) + end + + def visit_Arel_Nodes_InnerJoin(object, _) + { + source: visit(object.left, collector).value, + constraints: object.right ? visit(object.right.expr, collector).value : nil + }.compact + end + # rubocop:enable Naming/MethodName + + def parse_join(object) + { + source: visit(object.left, collector).value, + constraints: visit(object.right.expr, collector).value + } + end + + def quote(value) + "#{value}" + end + + def quote_table_name(name) + "#{name}" + end + + def quote_column_name(name) + "#{name}" + end + + def collector + Arel::Collectors::SubstituteBinds.new(@connection, Arel::Collectors::SQLString.new) + end + end + end + end + end + end +end diff --git a/lib/gitlab/usage_data_counters/known_events/epic_events.yml b/lib/gitlab/usage_data_counters/known_events/epic_events.yml index 7cd2a72d8ca..79dfae98157 100644 --- a/lib/gitlab/usage_data_counters/known_events/epic_events.yml +++ b/lib/gitlab/usage_data_counters/known_events/epic_events.yml @@ -32,3 +32,9 @@ redis_slot: project_management aggregation: daily feature_flag: track_epics_activity + +- name: g_project_management_epic_issue_added + category: epics_usage + redis_slot: project_management + aggregation: daily + feature_flag: track_epics_activity diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 6b43f79455a..2e6d673e528 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -25348,7 +25348,7 @@ msgstr "" msgid "Release|Something went wrong while creating a new release" msgstr "" -msgid "Release|Something went wrong while getting the release details" +msgid "Release|Something went wrong while getting the release details." msgstr "" msgid "Release|Something went wrong while saving the release details" diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index f10fbf5ef2c..f2d86b1b166 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -175,22 +175,44 @@ RSpec.describe GraphqlController do end describe '#append_info_to_payload' do - let(:graphql_query) { graphql_query_for('project', { 'fullPath' => 'foo' }, %w(id name)) } - let(:mock_store) { { graphql_logs: { foo: :bar } } } + let(:query_1) { { query: graphql_query_for('project', { 'fullPath' => 'foo' }, %w(id name), 'getProject_1') } } + let(:query_2) { { query: graphql_query_for('project', { 'fullPath' => 'bar' }, %w(id), 'getProject_2') } } + let(:graphql_queries) { [query_1, query_2] } let(:log_payload) { {} } + let(:expected_logs) do + [ + { + operation_name: 'getProject_1', + complexity: 3, + depth: 2, + used_deprecated_fields: [], + used_fields: ['Project.id', 'Project.name', 'Query.project'], + variables: '{}' + }, + { + operation_name: 'getProject_2', + complexity: 2, + depth: 2, + used_deprecated_fields: [], + used_fields: ['Project.id', 'Query.project'], + variables: '{}' + } + ] + end before do - allow(RequestStore).to receive(:store).and_return(mock_store) + RequestStore.clear! + allow(controller).to receive(:append_info_to_payload).and_wrap_original do |method, *| method.call(log_payload) end end it 'appends metadata for logging' do - post :execute, params: { query: graphql_query, operationName: 'Foo' } + post :execute, params: { _json: graphql_queries } expect(controller).to have_received(:append_info_to_payload) - expect(log_payload.dig(:metadata, :graphql)).to eq({ operation_name: 'Foo', foo: :bar }) + expect(log_payload.dig(:metadata, :graphql)).to match_array(expected_logs) end end end diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb index 7da3d403b53..d80d7338d3b 100644 --- a/spec/controllers/projects/forks_controller_spec.rb +++ b/spec/controllers/projects/forks_controller_spec.rb @@ -153,8 +153,11 @@ RSpec.describe Projects::ForksController do end describe 'GET new' do - subject do + let(:format) { :html } + + subject(:do_request) do get :new, + format: format, params: { namespace_id: project.namespace, project_id: project @@ -166,24 +169,32 @@ RSpec.describe Projects::ForksController do sign_in(user) end - context 'when JSON requested' do - it 'responds with available groups' do - get :new, - format: :json, - params: { - namespace_id: project.namespace, - project_id: project - } + it 'responds with status 200' do + request - expect(json_response['namespaces'].length).to eq(1) - expect(json_response['namespaces'].first['id']).to eq(group.id) - end + expect(response).to have_gitlab_http_status(:ok) end - it 'responds with status 200' do - subject + context 'when JSON is requested' do + let(:format) { :json } - expect(response).to have_gitlab_http_status(:ok) + it 'responds with user namespace + groups' do + do_request + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['namespaces'].length).to eq(2) + expect(json_response['namespaces'][0]['id']).to eq(user.namespace.id) + expect(json_response['namespaces'][1]['id']).to eq(group.id) + end + + it 'responds with group only when fork_project_form feature flag is disabled' do + stub_feature_flags(fork_project_form: false) + do_request + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['namespaces'].length).to eq(1) + expect(json_response['namespaces'][0]['id']).to eq(group.id) + end end end diff --git a/spec/features/projects/releases/user_views_release_spec.rb b/spec/features/projects/releases/user_views_release_spec.rb index 186122536ce..4410f345e56 100644 --- a/spec/features/projects/releases/user_views_release_spec.rb +++ b/spec/features/projects/releases/user_views_release_spec.rb @@ -5,7 +5,6 @@ require 'spec_helper' RSpec.describe 'User views Release', :js do let(:project) { create(:project, :repository) } let(:user) { create(:user) } - let(:graphql_feature_flag) { true } let(:release) do create(:release, @@ -15,8 +14,6 @@ RSpec.describe 'User views Release', :js do end before do - stub_feature_flags(graphql_individual_release_page: graphql_feature_flag) - project.add_developer(user) sign_in(user) @@ -26,35 +23,23 @@ RSpec.describe 'User views Release', :js do it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet' - shared_examples 'release page' do - it 'renders the breadcrumbs' do - within('.breadcrumbs') do - expect(page).to have_content("#{project.creator.name} #{project.name} Releases #{release.name}") - - expect(page).to have_link(project.creator.name, href: user_path(project.creator)) - expect(page).to have_link(project.name, href: project_path(project)) - expect(page).to have_link('Releases', href: project_releases_path(project)) - expect(page).to have_link(release.name, href: project_release_path(project, release)) - end - end + it 'renders the breadcrumbs' do + within('.breadcrumbs') do + expect(page).to have_content("#{project.creator.name} #{project.name} Releases #{release.name}") - it 'renders the release details' do - within('.release-block') do - expect(page).to have_content(release.name) - expect(page).to have_content(release.tag) - expect(page).to have_content(release.commit.short_id) - expect(page).to have_content('Lorem ipsum dolor sit amet') - end + expect(page).to have_link(project.creator.name, href: user_path(project.creator)) + expect(page).to have_link(project.name, href: project_path(project)) + expect(page).to have_link('Releases', href: project_releases_path(project)) + expect(page).to have_link(release.name, href: project_release_path(project, release)) end end - describe 'when the graphql_individual_release_page feature flag is enabled' do - it_behaves_like 'release page' - end - - describe 'when the graphql_individual_release_page feature flag is disabled' do - let(:graphql_feature_flag) { false } - - it_behaves_like 'release page' + it 'renders the release details' do + within('.release-block') do + expect(page).to have_content(release.name) + expect(page).to have_content(release.tag) + expect(page).to have_content(release.commit.short_id) + expect(page).to have_content('Lorem ipsum dolor sit amet') + end end end diff --git a/spec/fixtures/api/schemas/external_validation.json b/spec/fixtures/api/schemas/external_validation.json index 1bd00a2e6fc..3ff71626cc0 100644 --- a/spec/fixtures/api/schemas/external_validation.json +++ b/spec/fixtures/api/schemas/external_validation.json @@ -11,11 +11,13 @@ "type": "object", "required": [ "id", - "path" + "path", + "created_at" ], "properties": { "id": { "type": "integer" }, - "path": { "type": "string" } + "path": { "type": "string" }, + "created_at": { "type": ["string", "null"], "format": "date-time" } } }, "user": { @@ -23,12 +25,14 @@ "required": [ "id", "username", - "email" + "email", + "created_at" ], "properties": { "id": { "type": "integer" }, "username": { "type": "string" }, - "email": { "type": "string" } + "email": { "type": "string" }, + "created_at": { "type": ["string", "null"], "format": "date-time" } } }, "pipeline": { @@ -70,6 +74,5 @@ } } } - }, - "additionalProperties": false + } } diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js index 5caea395f0a..425cb9d0059 100644 --- a/spec/frontend/releases/components/app_show_spec.js +++ b/spec/frontend/releases/components/app_show_spec.js @@ -1,63 +1,176 @@ import { shallowMount } from '@vue/test-utils'; -import Vuex from 'vuex'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; import { getJSONFixture } from 'helpers/fixtures'; -import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import createFlash from '~/flash'; import ReleaseShowApp from '~/releases/components/app_show.vue'; import ReleaseBlock from '~/releases/components/release_block.vue'; import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue'; +import oneReleaseQuery from '~/releases/queries/one_release.query.graphql'; -const originalRelease = getJSONFixture('api/releases/release.json'); +jest.mock('~/flash'); + +const oneReleaseQueryResponse = getJSONFixture( + 'graphql/releases/queries/one_release.query.graphql.json', +); + +Vue.use(VueApollo); + +const EXPECTED_ERROR_MESSAGE = 'Something went wrong while getting the release details.'; +const MOCK_FULL_PATH = 'project/full/path'; +const MOCK_TAG_NAME = 'test-tag-name'; describe('Release show component', () => { let wrapper; - let release; - let actions; - beforeEach(() => { - release = convertObjectPropsToCamelCase(originalRelease); - }); - - const factory = (state) => { - actions = { - fetchRelease: jest.fn(), - }; - - const store = new Vuex.Store({ - modules: { - detail: { - namespaced: true, - actions, - state, - }, + const createComponent = ({ apolloProvider }) => { + wrapper = shallowMount(ReleaseShowApp, { + provide: { + fullPath: MOCK_FULL_PATH, + tagName: MOCK_TAG_NAME, }, + apolloProvider, }); - - wrapper = shallowMount(ReleaseShowApp, { store }); }; + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + const findLoadingSkeleton = () => wrapper.find(ReleaseSkeletonLoader); const findReleaseBlock = () => wrapper.find(ReleaseBlock); - it('calls fetchRelease when the component is created', () => { - factory({ release }); - expect(actions.fetchRelease).toHaveBeenCalledTimes(1); + const expectLoadingIndicator = () => { + it('renders a loading indicator', () => { + expect(findLoadingSkeleton().exists()).toBe(true); + }); + }; + + const expectNoLoadingIndicator = () => { + it('does not render a loading indicator', () => { + expect(findLoadingSkeleton().exists()).toBe(false); + }); + }; + + const expectNoFlash = () => { + it('does not show a flash message', () => { + expect(createFlash).not.toHaveBeenCalled(); + }); + }; + + const expectFlashWithMessage = (message) => { + it(`shows a flash message that reads "${message}"`, () => { + expect(createFlash).toHaveBeenCalledTimes(1); + expect(createFlash).toHaveBeenCalledWith({ + message, + captureError: true, + error: expect.any(Error), + }); + }); + }; + + const expectReleaseBlock = () => { + it('renders a release block', () => { + expect(findReleaseBlock().exists()).toBe(true); + }); + }; + + const expectNoReleaseBlock = () => { + it('does not render a release block', () => { + expect(findReleaseBlock().exists()).toBe(false); + }); + }; + + describe('GraphQL query variables', () => { + const queryHandler = jest.fn().mockResolvedValueOnce(oneReleaseQueryResponse); + + beforeEach(() => { + const apolloProvider = createMockApollo([[oneReleaseQuery, queryHandler]]); + + createComponent({ apolloProvider }); + }); + + it('builds a GraphQL with the expected variables', () => { + expect(queryHandler).toHaveBeenCalledTimes(1); + expect(queryHandler).toHaveBeenCalledWith({ + fullPath: MOCK_FULL_PATH, + tagName: MOCK_TAG_NAME, + }); + }); }); - it('shows a loading skeleton and hides the release block while the API call is in progress', () => { - factory({ isFetchingRelease: true }); - expect(findLoadingSkeleton().exists()).toBe(true); - expect(findReleaseBlock().exists()).toBe(false); + describe('when the component is loading data', () => { + beforeEach(() => { + const apolloProvider = createMockApollo([ + [oneReleaseQuery, jest.fn().mockReturnValueOnce(new Promise(() => {}))], + ]); + + createComponent({ apolloProvider }); + }); + + expectLoadingIndicator(); + expectNoFlash(); + expectNoReleaseBlock(); }); - it('hides the loading skeleton and shows the release block when the API call finishes successfully', () => { - factory({ isFetchingRelease: false }); - expect(findLoadingSkeleton().exists()).toBe(false); - expect(findReleaseBlock().exists()).toBe(true); + describe('when the component has successfully loaded the release', () => { + beforeEach(() => { + const apolloProvider = createMockApollo([ + [oneReleaseQuery, jest.fn().mockResolvedValueOnce(oneReleaseQueryResponse)], + ]); + + createComponent({ apolloProvider }); + }); + + expectNoLoadingIndicator(); + expectNoFlash(); + expectReleaseBlock(); }); - it('hides both the loading skeleton and the release block when the API call fails', () => { - factory({ fetchError: new Error('Uh oh') }); - expect(findLoadingSkeleton().exists()).toBe(false); - expect(findReleaseBlock().exists()).toBe(false); + describe('when the request succeeded, but the returned "project" key was null', () => { + beforeEach(() => { + const apolloProvider = createMockApollo([ + [oneReleaseQuery, jest.fn().mockResolvedValueOnce({ data: { project: null } })], + ]); + + createComponent({ apolloProvider }); + }); + + expectNoLoadingIndicator(); + expectFlashWithMessage(EXPECTED_ERROR_MESSAGE); + expectNoReleaseBlock(); + }); + + describe('when the request succeeded, but the returned "project.release" key was null', () => { + beforeEach(() => { + const apolloProvider = createMockApollo([ + [ + oneReleaseQuery, + jest.fn().mockResolvedValueOnce({ data: { project: { release: null } } }), + ], + ]); + + createComponent({ apolloProvider }); + }); + + expectNoLoadingIndicator(); + expectFlashWithMessage(EXPECTED_ERROR_MESSAGE); + expectNoReleaseBlock(); + }); + + describe('when an error occurs while loading the release', () => { + beforeEach(() => { + const apolloProvider = createMockApollo([ + [oneReleaseQuery, jest.fn().mockRejectedValueOnce('An error occurred!')], + ]); + + createComponent({ apolloProvider }); + }); + + expectNoLoadingIndicator(); + expectFlashWithMessage(EXPECTED_ERROR_MESSAGE); + expectNoReleaseBlock(); }); }); diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js index 9c125fbb87b..0f81869e3f9 100644 --- a/spec/frontend/releases/stores/modules/detail/actions_spec.js +++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js @@ -163,7 +163,7 @@ describe('Release detail actions', () => { return actions.fetchRelease({ commit: jest.fn(), state, rootState: state }).then(() => { expect(createFlash).toHaveBeenCalledTimes(1); expect(createFlash).toHaveBeenCalledWith( - 'Something went wrong while getting the release details', + 'Something went wrong while getting the release details.', ); }); }); diff --git a/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb index 80ad5b69a61..eeed5c6d079 100644 --- a/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb +++ b/spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb @@ -5,7 +5,6 @@ require 'spec_helper' RSpec.describe BulkImports::Groups::Pipelines::LabelsPipeline do let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group) } - let_it_be(:cursor) { 'cursor' } let_it_be(:timestamp) { Time.new(2020, 01, 01).utc } let_it_be(:entity) do @@ -23,29 +22,10 @@ RSpec.describe BulkImports::Groups::Pipelines::LabelsPipeline do subject { described_class.new(context) } - def label_data(title) - { - 'title' => title, - 'description' => 'desc', - 'color' => '#428BCA', - 'created_at' => timestamp.to_s, - 'updated_at' => timestamp.to_s - } - end - - def extractor_data(title:, has_next_page:, cursor: nil) - page_info = { - 'end_cursor' => cursor, - 'has_next_page' => has_next_page - } - - BulkImports::Pipeline::ExtractedData.new(data: [label_data(title)], page_info: page_info) - end - describe '#run' do it 'imports a group labels' do - first_page = extractor_data(title: 'label1', has_next_page: true, cursor: cursor) - last_page = extractor_data(title: 'label2', has_next_page: false) + first_page = extracted_data(title: 'label1', has_next_page: true) + last_page = extracted_data(title: 'label2') allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor| allow(extractor) @@ -65,34 +45,6 @@ RSpec.describe BulkImports::Groups::Pipelines::LabelsPipeline do end end - describe '#after_run' do - context 'when extracted data has next page' do - it 'updates tracker information and runs pipeline again' do - data = extractor_data(title: 'label', has_next_page: true, cursor: cursor) - - expect(subject).to receive(:run) - - subject.after_run(data) - - expect(tracker.has_next_page).to eq(true) - expect(tracker.next_page).to eq(cursor) - end - end - - context 'when extracted data has no next page' do - it 'updates tracker information and does not run pipeline' do - data = extractor_data(title: 'label', has_next_page: false) - - expect(subject).not_to receive(:run) - - subject.after_run(data) - - expect(tracker.has_next_page).to eq(false) - expect(tracker.next_page).to be_nil - end - end - end - describe '#load' do it 'creates the label' do data = label_data('label') @@ -128,4 +80,23 @@ RSpec.describe BulkImports::Groups::Pipelines::LabelsPipeline do ) end end + + def label_data(title) + { + 'title' => title, + 'description' => 'desc', + 'color' => '#428BCA', + 'created_at' => timestamp.to_s, + 'updated_at' => timestamp.to_s + } + end + + def extracted_data(title:, has_next_page: false) + page_info = { + 'has_next_page' => has_next_page, + 'end_cursor' => has_next_page ? 'cursor' : nil + } + + BulkImports::Pipeline::ExtractedData.new(data: [label_data(title)], page_info: page_info) + end end diff --git a/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb index 5c82a028751..0af45ae17d6 100644 --- a/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb +++ b/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb @@ -8,7 +8,6 @@ RSpec.describe BulkImports::Groups::Pipelines::MembersPipeline do let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group) } - let_it_be(:cursor) { 'cursor' } let_it_be(:bulk_import) { create(:bulk_import, user: user) } let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) } let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } @@ -18,8 +17,8 @@ RSpec.describe BulkImports::Groups::Pipelines::MembersPipeline do describe '#run' do it 'maps existing users to the imported group' do - first_page = member_data(email: member_user1.email, has_next_page: true, cursor: cursor) - last_page = member_data(email: member_user2.email, has_next_page: false) + first_page = extracted_data(email: member_user1.email, has_next_page: true) + last_page = extracted_data(email: member_user2.email) allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor| allow(extractor) @@ -89,7 +88,7 @@ RSpec.describe BulkImports::Groups::Pipelines::MembersPipeline do end end - def member_data(email:, has_next_page:, cursor: nil) + def extracted_data(email:, has_next_page: false) data = { 'created_at' => '2020-01-01T00:00:00Z', 'updated_at' => '2020-01-01T00:00:00Z', @@ -103,8 +102,8 @@ RSpec.describe BulkImports::Groups::Pipelines::MembersPipeline do } page_info = { - 'end_cursor' => cursor, - 'has_next_page' => has_next_page + 'has_next_page' => has_next_page, + 'end_cursor' => has_next_page ? 'cursor' : nil } BulkImports::Pipeline::ExtractedData.new(data: data, page_info: page_info) diff --git a/spec/lib/bulk_imports/groups/pipelines/milestones_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/milestones_pipeline_spec.rb index 15a64a70ff3..3ce81026834 100644 --- a/spec/lib/bulk_imports/groups/pipelines/milestones_pipeline_spec.rb +++ b/spec/lib/bulk_imports/groups/pipelines/milestones_pipeline_spec.rb @@ -5,7 +5,6 @@ require 'spec_helper' RSpec.describe BulkImports::Groups::Pipelines::MilestonesPipeline do let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group) } - let_it_be(:cursor) { 'cursor' } let_it_be(:timestamp) { Time.new(2020, 01, 01).utc } let_it_be(:bulk_import) { create(:bulk_import, user: user) } @@ -25,35 +24,14 @@ RSpec.describe BulkImports::Groups::Pipelines::MilestonesPipeline do subject { described_class.new(context) } - def milestone_data(title) - { - 'title' => title, - 'description' => 'desc', - 'state' => 'closed', - 'start_date' => '2020-10-21', - 'due_date' => '2020-10-22', - 'created_at' => timestamp.to_s, - 'updated_at' => timestamp.to_s - } - end - - def extracted_data(title:, has_next_page:, cursor: nil) - page_info = { - 'end_cursor' => cursor, - 'has_next_page' => has_next_page - } - - BulkImports::Pipeline::ExtractedData.new(data: [milestone_data(title)], page_info: page_info) - end - before do group.add_owner(user) end describe '#run' do it 'imports group milestones' do - first_page = extracted_data(title: 'milestone1', has_next_page: true, cursor: cursor) - last_page = extracted_data(title: 'milestone2', has_next_page: false) + first_page = extracted_data(title: 'milestone1', has_next_page: true) + last_page = extracted_data(title: 'milestone2') allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor| allow(extractor) @@ -76,34 +54,6 @@ RSpec.describe BulkImports::Groups::Pipelines::MilestonesPipeline do end end - describe '#after_run' do - context 'when extracted data has next page' do - it 'updates tracker information and runs pipeline again' do - data = extracted_data(title: 'milestone', has_next_page: true, cursor: cursor) - - expect(subject).to receive(:run) - - subject.after_run(data) - - expect(tracker.has_next_page).to eq(true) - expect(tracker.next_page).to eq(cursor) - end - end - - context 'when extracted data has no next page' do - it 'updates tracker information and does not run pipeline' do - data = extracted_data(title: 'milestone', has_next_page: false) - - expect(subject).not_to receive(:run) - - subject.after_run(data) - - expect(tracker.has_next_page).to eq(false) - expect(tracker.next_page).to be_nil - end - end - end - describe '#load' do it 'creates the milestone' do data = milestone_data('milestone') @@ -117,7 +67,7 @@ RSpec.describe BulkImports::Groups::Pipelines::MilestonesPipeline do end it 'raises NotAllowedError' do - data = extracted_data(title: 'milestone', has_next_page: false) + data = extracted_data(title: 'milestone') expect { subject.load(context, data) }.to raise_error(::BulkImports::Pipeline::NotAllowedError) end @@ -145,4 +95,28 @@ RSpec.describe BulkImports::Groups::Pipelines::MilestonesPipeline do ) end end + + def milestone_data(title) + { + 'title' => title, + 'description' => 'desc', + 'state' => 'closed', + 'start_date' => '2020-10-21', + 'due_date' => '2020-10-22', + 'created_at' => timestamp.to_s, + 'updated_at' => timestamp.to_s + } + end + + def extracted_data(title:, has_next_page: false) + page_info = { + 'has_next_page' => has_next_page, + 'end_cursor' => has_next_page ? 'cursor' : nil + } + + BulkImports::Pipeline::ExtractedData.new( + data: milestone_data(title), + page_info: page_info + ) + end end diff --git a/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb index fd7265aea34..e4a41428dd2 100644 --- a/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb +++ b/spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb @@ -12,19 +12,17 @@ RSpec.describe BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline do subject { described_class.new(context) } - describe '#run' do - let(:subgroup_data) do - [ - { - "name" => "subgroup", - "full_path" => "parent/subgroup" - } - ] - end + let(:extracted_data) do + BulkImports::Pipeline::ExtractedData.new(data: { + 'name' => 'subgroup', + 'full_path' => 'parent/subgroup' + }) + end + describe '#run' do before do allow_next_instance_of(BulkImports::Groups::Extractors::SubgroupsExtractor) do |extractor| - allow(extractor).to receive(:extract).and_return(subgroup_data) + allow(extractor).to receive(:extract).and_return(extracted_data) end parent.add_owner(user) diff --git a/spec/lib/bulk_imports/pipeline/runner_spec.rb b/spec/lib/bulk_imports/pipeline/runner_spec.rb index 29fd1ee3ffc..9cadc06d613 100644 --- a/spec/lib/bulk_imports/pipeline/runner_spec.rb +++ b/spec/lib/bulk_imports/pipeline/runner_spec.rb @@ -38,8 +38,6 @@ RSpec.describe BulkImports::Pipeline::Runner do extractor BulkImports::Extractor transformer BulkImports::Transformer loader BulkImports::Loader - - def after_run(_); end end stub_const('BulkImports::MyPipeline', pipeline) @@ -54,8 +52,6 @@ RSpec.describe BulkImports::Pipeline::Runner do describe 'pipeline runner' do context 'when entity is not marked as failed' do it 'runs pipeline extractor, transformer, loader' do - extracted_data = BulkImports::Pipeline::ExtractedData.new(data: { foo: :bar }) - expect_next_instance_of(BulkImports::Extractor) do |extractor| expect(extractor) .to receive(:extract) @@ -133,6 +129,22 @@ RSpec.describe BulkImports::Pipeline::Runner do subject.run end + context 'when extracted data has multiple pages' do + it 'updates tracker information and runs pipeline again' do + first_page = extracted_data(has_next_page: true) + last_page = extracted_data + + expect_next_instance_of(BulkImports::Extractor) do |extractor| + expect(extractor) + .to receive(:extract) + .with(context) + .and_return(first_page, last_page) + end + + subject.run + end + end + context 'when exception is raised' do before do allow_next_instance_of(BulkImports::Extractor) do |extractor| @@ -218,14 +230,24 @@ RSpec.describe BulkImports::Pipeline::Runner do subject.run end end - end - def log_params(context, extra = {}) - { - bulk_import_id: context.bulk_import.id, - bulk_import_entity_id: context.entity.id, - bulk_import_entity_type: context.entity.source_type, - context_extra: context.extra - }.merge(extra) + def log_params(context, extra = {}) + { + bulk_import_id: context.bulk_import.id, + bulk_import_entity_id: context.entity.id, + bulk_import_entity_type: context.entity.source_type, + context_extra: context.extra + }.merge(extra) + end + + def extracted_data(has_next_page: false) + BulkImports::Pipeline::ExtractedData.new( + data: { foo: :bar }, + page_info: { + 'has_next_page' => has_next_page, + 'end_cursor' => has_next_page ? 'cursor' : nil + } + ) + end end end diff --git a/spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb b/spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb index c6b8cf2a985..6a08e8f0b7f 100644 --- a/spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb +++ b/spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb @@ -131,7 +131,6 @@ RSpec.describe Gitlab::Ci::Parsers::Codequality::CodeClimate do expect { parse }.not_to raise_error expect(codequality_report.degradations_count).to eq(0) - expect(codequality_report.error_message).to eq("Invalid degradation format: The property '#/' did not contain a required property of 'location'") end end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb index 21d636aa7f0..37893e54bca 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do - let(:project) { create(:project) } - let(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } let(:pipeline) { build(:ci_empty_pipeline, user: user, project: project) } let!(:step) { described_class.new(pipeline, command) } @@ -59,6 +59,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do allow(Gitlab).to receive(:com?).and_return(dot_com) end + it 'respects the defined payload schema' do + expect(::Gitlab::HTTP).to receive(:post) do |_url, params| + expect(params[:body]).to match_schema('/external_validation') + end + + perform! + end + shared_examples 'successful external authorization' do it 'does not drop the pipeline' do perform! @@ -224,16 +232,4 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do end end end - - describe '#validation_service_payload' do - subject(:validation_service_payload) { step.send(:validation_service_payload, pipeline, command.yaml_processor_result.stages_attributes) } - - it 'respects the defined schema' do - expect(validation_service_payload).to match_schema('/external_validation') - end - - it 'does not fire sql queries' do - expect { validation_service_payload }.not_to exceed_query_limit(1) - end - end end diff --git a/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb b/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb index ae9b2f2c62b..d6d8ace86c5 100644 --- a/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb +++ b/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb @@ -34,8 +34,6 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReports do it 'sets location as an error' do codequality_report.add_degradation(invalid_degradation) - - expect(codequality_report.error_message).to eq("Invalid degradation format: The property '#/' did not contain a required property of 'location'") end end end diff --git a/spec/lib/gitlab/crypto_helper_spec.rb b/spec/lib/gitlab/crypto_helper_spec.rb index 024564ea213..199a680921b 100644 --- a/spec/lib/gitlab/crypto_helper_spec.rb +++ b/spec/lib/gitlab/crypto_helper_spec.rb @@ -32,10 +32,6 @@ RSpec.describe Gitlab::CryptoHelper do end describe '.aes256_gcm_decrypt' do - before do - stub_feature_flags(dynamic_nonce_creation: false) - end - context 'when token was encrypted using static nonce' do let(:encrypted) { described_class.aes256_gcm_encrypt('some-value', nonce: described_class::AES256_GCM_IV_STATIC) } @@ -54,50 +50,6 @@ RSpec.describe Gitlab::CryptoHelper do it 'does not save hashed token with iv value in database' do expect { described_class.aes256_gcm_decrypt(encrypted) }.not_to change { TokenWithIv.count } end - - context 'with feature flag switched on' do - before do - stub_feature_flags(dynamic_nonce_creation: true) - end - - it 'correctly decrypts encrypted string' do - decrypted = described_class.aes256_gcm_decrypt(encrypted) - - expect(decrypted).to eq 'some-value' - end - end end - - context 'when token was encrypted using random nonce' do - let(:value) { 'random-value' } - - # for compatibility with tokens encrypted using dynamic nonce - let!(:encrypted) do - iv = create_nonce - encrypted_token = described_class.create_encrypted_token(value, iv) - TokenWithIv.create!(hashed_token: Digest::SHA256.digest(encrypted_token), hashed_plaintext_token: Digest::SHA256.digest(encrypted_token), iv: iv) - encrypted_token - end - - before do - stub_feature_flags(dynamic_nonce_creation: true) - end - - it 'correctly decrypts encrypted string' do - decrypted = described_class.aes256_gcm_decrypt(encrypted) - - expect(decrypted).to eq value - end - - it 'does not save hashed token with iv value in database' do - expect { described_class.aes256_gcm_decrypt(encrypted) }.not_to change { TokenWithIv.count } - end - end - end - - def create_nonce - cipher = OpenSSL::Cipher.new('aes-256-gcm') - cipher.encrypt # Required before '#random_iv' can be called - cipher.random_iv # Ensures that the IV is the correct length respective to the algorithm used. end end diff --git a/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb b/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb index 8450396284a..fc723138d88 100644 --- a/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb +++ b/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb @@ -3,43 +3,46 @@ require 'spec_helper' RSpec.describe Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer do - subject { described_class.new } - - describe '#initial_value' do - it 'filters out sensitive variables' do - doc = GraphQL.parse <<-GRAPHQL - mutation createNote($body: String!) { - createNote(input: {noteableId: "1", body: $body}) { - note { - id - } + let(:initial_value) { analyzer.initial_value(query) } + let(:analyzer) { described_class.new } + let(:query) { GraphQL::Query.new(GitlabSchema, document: document, context: {}, variables: { body: "some note" }) } + let(:document) do + GraphQL.parse <<-GRAPHQL + mutation createNote($body: String!) { + createNote(input: {noteableId: "1", body: $body}) { + note { + id } } - GRAPHQL + } + GRAPHQL + end - query = GraphQL::Query.new(GitlabSchema, document: doc, context: {}, variables: { body: "some note" }) + describe 'variables' do + subject { initial_value.fetch(:variables) } - expect(subject.initial_value(query)[:variables]).to eq('{:body=>"[FILTERED]"}') - end + it { is_expected.to eq('{:body=>"[FILTERED]"}') } end describe '#final_value' do let(:monotonic_time_before) { 42 } let(:monotonic_time_after) { 500 } let(:monotonic_time_duration) { monotonic_time_after - monotonic_time_before } + let(:memo) { initial_value } + + subject(:final_value) { analyzer.final_value(memo) } + + before do + RequestStore.store[:graphql_logs] = nil - it 'returns a duration in seconds' do allow(GraphQL::Analysis).to receive(:analyze_query).and_return([4, 2, [[], []]]) allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after) allow(Gitlab::GraphqlLogger).to receive(:info) + end - expected_duration = monotonic_time_duration - memo = subject.initial_value(spy('query')) - - subject.final_value(memo) - - expect(memo).to have_key(:duration_s) - expect(memo[:duration_s]).to eq(expected_duration) + it 'inserts duration in seconds to memo and sets request store' do + expect { final_value }.to change { memo[:duration_s] }.to(monotonic_time_duration) + .and change { RequestStore.store[:graphql_logs] }.to([memo]) end end end diff --git a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb index cd0413feab4..74cbeb85f16 100644 --- a/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb +++ b/spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb @@ -12,7 +12,7 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do describe '#generate' do shared_examples 'name suggestion' do it 'return correct name' do - expect(described_class.generate(key_path)).to eq name_suggestion + expect(described_class.generate(key_path)).to match name_suggestion end end @@ -20,7 +20,7 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do it_behaves_like 'name suggestion' do # corresponding metric is collected with count(Board) let(:key_path) { 'counts.boards' } - let(:name_suggestion) { 'count_boards' } + let(:name_suggestion) { /count_boards/ } end end @@ -28,7 +28,32 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do it_behaves_like 'name suggestion' do # corresponding metric is collected with distinct_count(ZoomMeeting, :issue_id) let(:key_path) { 'counts.issues_using_zoom_quick_actions' } - let(:name_suggestion) { 'count_distinct_issue_id_from_zoom_meetings' } + let(:name_suggestion) { /count_distinct_issue_id_from_zoom_meetings/ } + end + end + + context 'joined relations' do + context 'counted attribute comes from joined relation' do + it_behaves_like 'name suggestion' do + # corresponding metric is collected with: + # distinct_count( + # ::Clusters::Applications::Ingress.modsecurity_enabled.logging + # .joins(cluster: :deployments) + # .merge(::Clusters::Cluster.enabled) + # .merge(Deployment.success), + # ::Deployment.arel_table[:environment_id] + # ) + let(:key_path) { 'counts.ingress_modsecurity_logging' } + let(:name_suggestion) { /count_distinct_environment_id_from__deployments__clusters__clusters_applications_ingress/ } + end + end + + context 'counted attribute comes from source relation' do + it_behaves_like 'name suggestion' do + # corresponding metric is collected with count(Issue.with_alert_management_alerts.not_authored_by(::User.alert_bot), start: issue_minimum_id, finish: issue_maximum_id) + let(:key_path) { 'counts.issues_created_manually_from_alerts' } + let(:name_suggestion) { /count__issues__alert_management_alerts/ } + end end end @@ -36,7 +61,7 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do it_behaves_like 'name suggestion' do # corresponding metric is collected with sum(JiraImportState.finished, :imported_issues_count) let(:key_path) { 'counts.jira_imports_total_imported_issues_count' } - let(:name_suggestion) { "sum_imported_issues_count_from__jira_imports" } + let(:name_suggestion) { /sum_imported_issues_count_from__jira_imports/ } end end @@ -44,7 +69,7 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do it_behaves_like 'name suggestion' do # corresponding metric is collected with add(data[:personal_snippets], data[:project_snippets]) let(:key_path) { 'counts.snippets' } - let(:name_suggestion) { "add_count__snippets_and_count__snippets" } + let(:name_suggestion) { /add_count__snippets_and_count__snippets/ } end end @@ -52,7 +77,7 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do it_behaves_like 'name suggestion' do # corresponding metric is collected with redis_usage_data { unique_visit_service.unique_visits_for(targets: :analytics) } let(:key_path) { 'analytics_unique_visits.analytics_unique_visits_for_any_target' } - let(:name_suggestion) { '' } + let(:name_suggestion) { // } end end @@ -60,7 +85,7 @@ RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::Generator do it_behaves_like 'name suggestion' do # corresponding metric is collected with alt_usage_data(fallback: nil) { operating_system } let(:key_path) { 'settings.operating_system' } - let(:name_suggestion) { '' } + let(:name_suggestion) { // } end end end diff --git a/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins_spec.rb b/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins_spec.rb new file mode 100644 index 00000000000..fb3bd564e34 --- /dev/null +++ b/spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Usage::Metrics::NamesSuggestions::RelationParsers::Joins do + describe '#accept' do + let(:collector) { Arel::Collectors::SubstituteBinds.new(ActiveRecord::Base.connection, Arel::Collectors::SQLString.new) } + + context 'with join added via string' do + it 'collects join parts' do + arel = Issue.joins('LEFT JOIN projects ON projects.id = issue.project_id') + + arel = arel.arel + result = described_class.new(ApplicationRecord.connection).accept(arel) + + expect(result).to match_array [{ source: "projects", constraints: "projects.id = issue.project_id" }] + end + end + + context 'with join added via arel node' do + it 'collects join parts' do + source_table = Arel::Table.new('records') + joined_table = Arel::Table.new('joins') + second_level_joined_table = Arel::Table.new('second_level_joins') + + arel = source_table + .from + .project(source_table['id'].count) + .join(joined_table, Arel::Nodes::OuterJoin) + .on(source_table[:id].eq(joined_table[:records_id])) + .join(second_level_joined_table, Arel::Nodes::OuterJoin) + .on(joined_table[:id].eq(second_level_joined_table[:joins_id])) + + result = described_class.new(ApplicationRecord.connection).accept(arel) + + expect(result).to match_array [{ source: "joins", constraints: "records.id = joins.records_id" }, { source: "second_level_joins", constraints: "joins.id = second_level_joins.joins_id" }] + end + end + end +end diff --git a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb index 1e1cd97e410..1b75c52d742 100644 --- a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb +++ b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb @@ -68,10 +68,6 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do context 'when using optional strategy' do let(:options) { { encrypted: :optional } } - before do - stub_feature_flags(dynamic_nonce_creation: false) - end - it 'returns decrypted token when an encrypted token is present' do allow(instance).to receive(:read_attribute) .with('some_field_encrypted') diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb index fe1c7c15de2..60e717533f7 100644 --- a/spec/requests/api/graphql/gitlab_schema_spec.rb +++ b/spec/requests/api/graphql/gitlab_schema_spec.rb @@ -191,6 +191,7 @@ RSpec.describe 'GitlabSchema configurations' do complexity: 181, depth: 13, duration_s: 7, + operation_name: 'IntrospectionQuery', used_fields: an_instance_of(Array), used_deprecated_fields: an_instance_of(Array) } diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb index 4eaf57a7d35..ca21e56dca3 100644 --- a/spec/requests/api/graphql_spec.rb +++ b/spec/requests/api/graphql_spec.rb @@ -13,6 +13,7 @@ RSpec.describe 'GraphQL' do query_string: query, variables: variables.to_s, duration_s: anything, + operation_name: nil, depth: 1, complexity: 1, used_fields: ['Query.echo'], diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index 75d9508f470..a7f9e16c489 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -222,9 +222,12 @@ module GraphqlHelpers lazy_vals.is_a?(Array) ? lazy_vals.map { |val| sync(val) } : sync(lazy_vals) end - def graphql_query_for(name, args = {}, selection = nil) + def graphql_query_for(name, args = {}, selection = nil, operation_name = nil) type = GitlabSchema.types['Query'].fields[GraphqlHelpers.fieldnamerize(name)]&.type - wrap_query(query_graphql_field(name, args, selection, type)) + query = wrap_query(query_graphql_field(name, args, selection, type)) + query = "query #{operation_name}#{query}" if operation_name + + query end def wrap_query(query) diff --git a/spec/validators/json_schema_validator_spec.rb b/spec/validators/json_schema_validator_spec.rb index 1e9420c5422..83eb0e2f3dd 100644 --- a/spec/validators/json_schema_validator_spec.rb +++ b/spec/validators/json_schema_validator_spec.rb @@ -29,36 +29,6 @@ RSpec.describe JsonSchemaValidator do expect(build_report_result.errors.full_messages).to eq(["Data must be a valid json schema"]) end end - - context 'when draft is > 4' do - let(:validator) { described_class.new(attributes: [:data], filename: "build_report_result_data", draft: 6) } - - it 'uses JSONSchemer to perform validations' do - expect(JSONSchemer).to receive(:schema).with(Pathname.new(Rails.root.join('app', 'validators', 'json_schemas', 'build_report_result_data.json').to_s)).and_call_original - - subject - end - end - - context 'when draft is <= 4' do - let(:validator) { described_class.new(attributes: [:data], filename: "build_report_result_data", draft: 4) } - - it 'uses JSON::Validator to perform validations' do - expect(JSON::Validator).to receive(:validate).with(Rails.root.join('app', 'validators', 'json_schemas', 'build_report_result_data.json').to_s, build_report_result.data) - - subject - end - end - - context 'when draft value is not provided' do - let(:validator) { described_class.new(attributes: [:data], filename: "build_report_result_data") } - - it 'uses JSON::Validator to perform validations' do - expect(JSON::Validator).to receive(:validate).with(Rails.root.join('app', 'validators', 'json_schemas', 'build_report_result_data.json').to_s, build_report_result.data) - - subject - end - end end context 'when filename is not set' do -- cgit v1.2.3