summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop_todo.yml9
-rw-r--r--Gemfile6
-rw-r--r--Gemfile.lock12
-rw-r--r--app/assets/javascripts/pages/admin/services/edit/index.js6
-rw-r--r--app/assets/javascripts/pipelines/components/dag/dag.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue19
-rw-r--r--app/assets/javascripts/pipelines/components/unwrapping_utils.js2
-rw-r--r--app/assets/javascripts/releases/components/app_show.vue56
-rw-r--r--app/assets/javascripts/releases/mount_show.js28
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/actions.js4
-rw-r--r--app/controllers/graphql_controller.rb5
-rw-r--r--app/controllers/projects/forks_controller.rb10
-rw-r--r--app/controllers/projects/pipelines_controller.rb1
-rw-r--r--app/controllers/projects/releases_controller.rb1
-rw-r--r--app/models/concerns/token_authenticatable_strategies/encrypted.rb8
-rw-r--r--app/serializers/fork_namespace_entity.rb2
-rw-r--r--app/validators/json_schema_validator.rb11
-rw-r--r--app/validators/json_schemas/application_setting_kroki_formats.json1
-rw-r--r--app/validators/json_schemas/build_metadata_secrets.json1
-rw-r--r--app/validators/json_schemas/build_report_result_data.json1
-rw-r--r--app/validators/json_schemas/build_report_result_data_tests.json1
-rw-r--r--app/validators/json_schemas/codeclimate.json1
-rw-r--r--app/validators/json_schemas/daily_build_group_report_result_data.json1
-rw-r--r--app/validators/json_schemas/debian_fields.json1
-rw-r--r--app/validators/json_schemas/git_trailers.json1
-rw-r--r--app/validators/json_schemas/http_integration_payload_attribute_mapping.json1
-rw-r--r--app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json1
-rw-r--r--changelogs/unreleased/233431-set-traversal_ids-for-gitlab-org-group.yml5
-rw-r--r--changelogs/unreleased/299832-feature-flag-enable-individual-jira-issue-pages.yml5
-rw-r--r--changelogs/unreleased/322592-clean-up-a-token_with_ivs-table.yml6
-rw-r--r--changelogs/unreleased/change-json-validation.yml5
-rw-r--r--changelogs/unreleased/ci-add-extra-properties-to-external-validation-payload.yml5
-rw-r--r--changelogs/unreleased/nfriend-reorganize-release-detail-page-store.yml5
-rw-r--r--config/feature_flags/development/dynamic_nonce_creation.yml8
-rw-r--r--config/feature_flags/development/graphql_individual_release_page.yml8
-rw-r--r--config/feature_flags/development/pipeline_filter_jobs.yml (renamed from config/feature_flags/development/jira_issues_show_integration.yml)10
-rw-r--r--db/post_migrate/20210311045138_set_traversal_ids_for_gitlab_org_group_staging.rb88
-rw-r--r--db/schema_migrations/202103110451381
-rw-r--r--doc/administration/external_pipeline_validation.md29
-rw-r--r--doc/development/usage_ping/dictionary.md24
-rw-r--r--doc/user/project/integrations/jira-upload-app-success_v13_11.pngbin0 -> 11440 bytes
-rw-r--r--doc/user/project/integrations/jira-upload-app_v13_11.pngbin0 -> 20667 bytes
-rw-r--r--doc/user/project/integrations/jira.md29
-rw-r--r--doc/user/project/integrations/jira_integrations.md144
-rw-r--r--jest.config.base.js12
-rw-r--r--jest.config.integration.js10
-rw-r--r--lib/bulk_imports/groups/pipelines/labels_pipeline.rb11
-rw-r--r--lib/bulk_imports/groups/pipelines/members_pipeline.rb11
-rw-r--r--lib/bulk_imports/groups/pipelines/milestones_pipeline.rb11
-rw-r--r--lib/bulk_imports/pipeline/runner.rb27
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/external.rb34
-rw-r--r--lib/gitlab/ci/reports/codequality_reports.rb5
-rw-r--r--lib/gitlab/crypto_helper.rb6
-rw-r--r--lib/gitlab/graphql/query_analyzers/logger_analyzer.rb7
-rw-r--r--lib/gitlab/usage/metrics/names_suggestions/generator.rb130
-rw-r--r--lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins.rb74
-rw-r--r--lib/gitlab/usage_data_counters/known_events/epic_events.yml6
-rw-r--r--locale/gitlab.pot2
-rw-r--r--spec/controllers/graphql_controller_spec.rb32
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb41
-rw-r--r--spec/features/projects/releases/user_views_release_spec.rb43
-rw-r--r--spec/fixtures/api/schemas/external_validation.json15
-rw-r--r--spec/frontend/releases/components/app_show_spec.js189
-rw-r--r--spec/frontend/releases/stores/modules/detail/actions_spec.js2
-rw-r--r--spec/lib/bulk_imports/groups/pipelines/labels_pipeline_spec.rb71
-rw-r--r--spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb11
-rw-r--r--spec/lib/bulk_imports/groups/pipelines/milestones_pipeline_spec.rb80
-rw-r--r--spec/lib/bulk_imports/groups/pipelines/subgroup_entities_pipeline_spec.rb18
-rw-r--r--spec/lib/bulk_imports/pipeline/runner_spec.rb46
-rw-r--r--spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb1
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb24
-rw-r--r--spec/lib/gitlab/ci/reports/codequality_reports_spec.rb2
-rw-r--r--spec/lib/gitlab/crypto_helper_spec.rb48
-rw-r--r--spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb47
-rw-r--r--spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb39
-rw-r--r--spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins_spec.rb40
-rw-r--r--spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb4
-rw-r--r--spec/requests/api/graphql/gitlab_schema_spec.rb1
-rw-r--r--spec/requests/api/graphql_spec.rb1
-rw-r--r--spec/support/helpers/graphql_helpers.rb7
-rw-r--r--spec/validators/json_schema_validator_spec.rb30
81 files changed, 1118 insertions, 586 deletions
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 @@
<script>
import { capitalize, escape, isEmpty } from 'lodash';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MainGraphWrapper from '../graph_shared/main_graph_wrapper.vue';
import { accessValue } from './accessors';
import ActionComponent from './action_component.vue';
@@ -15,6 +16,7 @@ export default {
JobItem,
MainGraphWrapper,
},
+ mixins: [glFeatureFlagMixin()],
props: {
groups: {
type: Array,
@@ -57,6 +59,21 @@ export default {
'gl-pl-3',
],
computed: {
+ /*
+ currentGroups and filteredGroups are part of
+ a test to hunt down a bug
+ (see: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57142).
+
+ They should be removed when the bug is rectified.
+ */
+ currentGroups() {
+ return this.glFeatures.pipelineFilterJobs ? this.filteredGroups : this.groups;
+ },
+ filteredGroups() {
+ return this.groups.map((group) => {
+ return { ...group, jobs: group.jobs.filter(Boolean) };
+ });
+ },
formattedTitle() {
return capitalize(escape(this.title));
},
@@ -104,7 +121,7 @@ export default {
</template>
<template #jobs>
<div
- v-for="group in groups"
+ v-for="group in currentGroups"
:id="groupId(group)"
:key="getGroupId(group)"
data-testid="stage-column-group"
diff --git a/app/assets/javascripts/pipelines/components/unwrapping_utils.js b/app/assets/javascripts/pipelines/components/unwrapping_utils.js
index 15073079c0a..a261dc8b1f2 100644
--- a/app/assets/javascripts/pipelines/components/unwrapping_utils.js
+++ b/app/assets/javascripts/pipelines/components/unwrapping_utils.js
@@ -9,7 +9,7 @@ const unwrapGroups = (stages) => {
const unwrapNodesWithName = (jobArray, prop, field = 'name') => {
return jobArray.map((job) => {
- return { ...job, [prop]: job[prop].nodes.map((item) => item[field]) };
+ return { ...job, [prop]: job[prop].nodes.map((item) => item[field] || '') };
});
};
diff --git a/app/assets/javascripts/releases/components/app_show.vue b/app/assets/javascripts/releases/components/app_show.vue
index 9ef38503c10..c38e93d420b 100644
--- a/app/assets/javascripts/releases/components/app_show.vue
+++ b/app/assets/javascripts/releases/components/app_show.vue
@@ -1,5 +1,8 @@
<script>
-import { mapState, mapActions } from 'vuex';
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+import oneReleaseQuery from '../queries/one_release.query.graphql';
+import { convertGraphQLRelease } from '../util';
import ReleaseBlock from './release_block.vue';
import ReleaseSkeletonLoader from './release_skeleton_loader.vue';
@@ -9,21 +12,58 @@ export default {
ReleaseBlock,
ReleaseSkeletonLoader,
},
- computed: {
- ...mapState('detail', ['isFetchingRelease', 'fetchError', 'release']),
+ inject: {
+ fullPath: {
+ default: '',
+ },
+ tagName: {
+ default: '',
+ },
},
- created() {
- this.fetchRelease();
+ apollo: {
+ release: {
+ query: oneReleaseQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ tagName: this.tagName,
+ };
+ },
+ update(data) {
+ if (data.project?.release) {
+ return convertGraphQLRelease(data.project.release);
+ }
+
+ return null;
+ },
+ result(result) {
+ // Handle the case where the query succeeded but didn't return any data
+ if (!result.error && !this.release) {
+ this.showFlash(
+ new Error(`No release found in project "${this.fullPath}" with tag "${this.tagName}"`),
+ );
+ }
+ },
+ error(error) {
+ this.showFlash(error);
+ },
+ },
},
methods: {
- ...mapActions('detail', ['fetchRelease']),
+ showFlash(error) {
+ createFlash({
+ message: s__('Release|Something went wrong while getting the release details.'),
+ captureError: true,
+ error,
+ });
+ },
},
};
</script>
<template>
<div class="gl-mt-3">
- <release-skeleton-loader v-if="isFetchingRelease" />
+ <release-skeleton-loader v-if="$apollo.queries.release.loading" />
- <release-block v-else-if="!fetchError" :release="release" />
+ <release-block v-else-if="release" :release="release" />
</div>
</template>
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/pipeline_filter_jobs.yml
index dd89ace22be..6fb989a6815 100644
--- a/config/feature_flags/development/jira_issues_show_integration.yml
+++ b/config/feature_flags/development/pipeline_filter_jobs.yml
@@ -1,8 +1,8 @@
---
-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'
+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::ecosystem
+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
--- /dev/null
+++ b/doc/user/project/integrations/jira-upload-app-success_v13_11.png
Binary files 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
--- /dev/null
+++ b/doc/user/project/integrations/jira-upload-app_v13_11.png
Binary files 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': '<rootDir>/fixtures/emojis$1.json',
'^spec/test_constants$': '<rootDir>/spec/frontend/__helpers__/test_constants',
'^jest/(.*)$': '<rootDir>/spec/frontend/$1',
- '^test_helpers(/.*)$': '<rootDir>/spec/frontend_integration/test_helpers$1',
- '^ee_else_ce_test_helpers(/.*)$': '<rootDir>/spec/frontend_integration/test_helpers$1',
+ ...extModuleNameMapper,
};
const collectCoverageFrom = ['<rootDir>/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(/.*)$': '<rootDir>/ee/spec/frontend_integration/test_helpers$1',
'^ee_jest/(.*)$': '<rootDir>/ee/spec/frontend/$1',
[TEST_FIXTURES_PATTERN]: '<rootDir>/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(/.*)$': '<rootDir>/spec/frontend_integration/test_helpers$1',
+ '^ee_else_ce_test_helpers(/.*)$': '<rootDir>/spec/frontend_integration/test_helpers$1',
+ },
+ moduleNameMapperEE: {
+ '^ee_else_ce_test_helpers(/.*)$': '<rootDir>/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 << "<adjective describing: '#{constraints}'>"
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 = /(?<target>#{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_<adjective describing\: '\(clusters_applications_ingress\.modsecurity_enabled = TRUE AND clusters_applications_ingress\.modsecurity_mode = \d+ AND clusters.enabled = TRUE AND deployments.status = \d+\)'>_deployments_<with>_clusters_<having>_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_<adjective describing\: '\(issues\.author_id != \d+\)'>_issues_<with>_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_<adjective describing: '(jira_imports.status = 4)'>_jira_imports" }
+ let(:name_suggestion) { /sum_imported_issues_count_from_<adjective describing\: '\(jira_imports\.status = \d+\)'>_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_<adjective describing: '(snippets.type = 'PersonalSnippet')'>_snippets_and_count_<adjective describing: '(snippets.type = 'ProjectSnippet')'>_snippets" }
+ let(:name_suggestion) { /add_count_<adjective describing\: '\(snippets\.type = 'PersonalSnippet'\)'>_snippets_and_count_<adjective describing\: '\(snippets\.type = 'ProjectSnippet'\)'>_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) { '<please fill metric name>' }
+ let(:name_suggestion) { /<please fill metric name>/ }
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) { '<please fill metric name>' }
+ let(:name_suggestion) { /<please fill metric name>/ }
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