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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/content_editor/extensions/code_block_highlight.js42
-rw-r--r--app/assets/javascripts/content_editor/services/code_block_language_loader.js35
-rw-r--r--app/assets/javascripts/content_editor/services/content_editor.js29
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js9
-rw-r--r--app/assets/javascripts/security_configuration/components/constants.js7
-rw-r--r--app/assets/javascripts/security_configuration/components/training_provider_list.vue6
-rw-r--r--app/controllers/groups/releases_controller.rb16
-rw-r--r--app/controllers/projects/google_cloud/base_controller.rb2
-rw-r--r--app/finders/releases/group_releases_finder.rb74
-rw-r--r--app/models/release.rb1
-rw-r--r--app/models/users/credit_card_validation.rb2
-rw-r--r--app/views/admin/application_settings/_search_limits.html.haml4
-rw-r--r--app/views/admin/application_settings/network.html.haml2
-rw-r--r--config/feature_flags/development/group_releases_finder_inoperator.yml8
-rw-r--r--db/migrate/20220310101118_update_holder_name_limit.rb28
-rw-r--r--db/post_migrate/20220223112304_schedule_nullify_orphan_runner_id_on_ci_builds.rb6
-rw-r--r--db/post_migrate/20220308115219_schedule_reset_duplicate_ci_runners_token_encrypted_values_on_projects.rb27
-rw-r--r--db/post_migrate/20220308115502_schedule_reset_duplicate_ci_runners_token_values_on_projects.rb27
-rw-r--r--db/schema_migrations/202203081152191
-rw-r--r--db/schema_migrations/202203081155021
-rw-r--r--db/schema_migrations/202203101011181
-rw-r--r--db/structure.sql6
-rw-r--r--doc/administration/clusters/kas.md4
-rw-r--r--doc/administration/instance_limits.md11
-rw-r--r--doc/ci/environments/deployment_approvals.md27
-rw-r--r--doc/development/documentation/graphql_styleguide.md2
-rw-r--r--doc/user/admin_area/settings/index.md1
-rw-r--r--doc/user/project/integrations/harbor.md50
-rw-r--r--doc/user/project/integrations/overview.md1
-rw-r--r--lib/api/repositories.rb2
-rw-r--r--lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects.rb37
-rw-r--r--lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects.rb37
-rw-r--r--locale/gitlab.pot7
-rw-r--r--spec/controllers/groups/releases_controller_spec.rb34
-rw-r--r--spec/features/admin/admin_settings_spec.rb14
-rw-r--r--spec/finders/releases/group_releases_finder_spec.rb204
-rw-r--r--spec/frontend/content_editor/extensions/code_block_highlight_spec.js74
-rw-r--r--spec/frontend/content_editor/services/code_block_language_loader_spec.js70
-rw-r--r--spec/frontend/content_editor/services/content_editor_spec.js24
-rw-r--r--spec/frontend/fixtures/merge_requests.rb2
-rw-r--r--spec/frontend/security_configuration/components/training_provider_list_spec.js80
-rw-r--r--spec/frontend/security_configuration/mock_data.js2
-rw-r--r--spec/graphql/resolvers/board_list_issues_resolver_spec.rb6
-rw-r--r--spec/graphql/resolvers/design_management/version_in_collection_resolver_spec.rb6
-rw-r--r--spec/graphql/resolvers/design_management/version_resolver_spec.rb6
-rw-r--r--spec/graphql/resolvers/design_management/versions_resolver_spec.rb6
-rw-r--r--spec/graphql/resolvers/group_issues_resolver_spec.rb6
-rw-r--r--spec/graphql/resolvers/issue_status_counts_resolver_spec.rb6
-rw-r--r--spec/graphql/resolvers/issues_resolver_spec.rb31
-rw-r--r--spec/graphql/resolvers/kas/agent_configurations_resolver_spec.rb6
-rw-r--r--spec/graphql/resolvers/labels_resolver_spec.rb4
-rw-r--r--spec/graphql/resolvers/package_pipelines_resolver_spec.rb24
-rw-r--r--spec/graphql/resolvers/paginated_tree_resolver_spec.rb6
-rw-r--r--spec/graphql/resolvers/project_milestones_resolver_spec.rb24
-rw-r--r--spec/graphql/resolvers/project_pipeline_resolver_spec.rb10
-rw-r--r--spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb13
-rw-r--r--spec/graphql/resolvers/timelog_resolver_spec.rb28
-rw-r--r--spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects_spec.rb37
-rw-r--r--spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects_spec.rb37
-rw-r--r--spec/models/users/credit_card_validation_spec.rb2
-rw-r--r--spec/requests/api/repositories_spec.rb7
-rw-r--r--spec/support/shared_examples/graphql/members_shared_examples.rb6
62 files changed, 1102 insertions, 186 deletions
diff --git a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
index 204ac07d401..74f620b57b6 100644
--- a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
+++ b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
@@ -1,11 +1,33 @@
import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
-import { lowlight } from 'lowlight/lib/all';
+import { textblockTypeInputRule } from '@tiptap/core';
+import { isFunction } from 'lodash';
const extractLanguage = (element) => element.getAttribute('lang');
+const backtickInputRegex = /^```([a-z]+)?[\s\n]$/;
+const tildeInputRegex = /^~~~([a-z]+)?[\s\n]$/;
+
+const loadLanguageFromInputRule = (languageLoader) => (match) => {
+ const language = match[1];
+
+ if (isFunction(languageLoader?.loadLanguages)) {
+ languageLoader.loadLanguages([language]);
+ }
+
+ return {
+ language,
+ };
+};
export default CodeBlockLowlight.extend({
isolating: true,
+ addOptions() {
+ return {
+ ...this.parent?.(),
+ languageLoader: {},
+ };
+ },
+
addAttributes() {
return {
language: {
@@ -18,6 +40,22 @@ export default CodeBlockLowlight.extend({
},
};
},
+ addInputRules() {
+ const { languageLoader } = this.options;
+
+ return [
+ textblockTypeInputRule({
+ find: backtickInputRegex,
+ type: this.type,
+ getAttributes: loadLanguageFromInputRule(languageLoader),
+ }),
+ textblockTypeInputRule({
+ find: tildeInputRegex,
+ type: this.type,
+ getAttributes: loadLanguageFromInputRule(languageLoader),
+ }),
+ ];
+ },
renderHTML({ HTMLAttributes }) {
return [
'pre',
@@ -28,6 +66,4 @@ export default CodeBlockLowlight.extend({
['code', {}, 0],
];
},
-}).configure({
- lowlight,
});
diff --git a/app/assets/javascripts/content_editor/services/code_block_language_loader.js b/app/assets/javascripts/content_editor/services/code_block_language_loader.js
new file mode 100644
index 00000000000..3c12cf614a5
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/code_block_language_loader.js
@@ -0,0 +1,35 @@
+export default class CodeBlockLanguageLoader {
+ constructor(lowlight) {
+ this.lowlight = lowlight;
+ }
+
+ isLanguageLoaded(language) {
+ return this.lowlight.registered(language);
+ }
+
+ loadLanguagesFromDOM(domTree) {
+ const languages = [];
+
+ domTree.querySelectorAll('pre').forEach((preElement) => {
+ languages.push(preElement.getAttribute('lang'));
+ });
+
+ return this.loadLanguages(languages);
+ }
+
+ loadLanguages(languageList = []) {
+ const loaders = languageList
+ .filter((languageName) => !this.isLanguageLoaded(languageName))
+ .map((languageName) => {
+ return import(
+ /* webpackChunkName: 'highlight.language.js' */ `highlight.js/lib/languages/${languageName}`
+ )
+ .then(({ default: language }) => {
+ this.lowlight.registerLanguage(languageName, language);
+ })
+ .catch(() => false);
+ });
+
+ return Promise.all(loaders);
+ }
+}
diff --git a/app/assets/javascripts/content_editor/services/content_editor.js b/app/assets/javascripts/content_editor/services/content_editor.js
index c5638da2daf..05c831109a5 100644
--- a/app/assets/javascripts/content_editor/services/content_editor.js
+++ b/app/assets/javascripts/content_editor/services/content_editor.js
@@ -3,11 +3,12 @@ import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } fro
/* eslint-disable no-underscore-dangle */
export class ContentEditor {
- constructor({ tiptapEditor, serializer, deserializer, eventHub }) {
+ constructor({ tiptapEditor, serializer, deserializer, eventHub, languageLoader }) {
this._tiptapEditor = tiptapEditor;
this._serializer = serializer;
this._deserializer = deserializer;
this._eventHub = eventHub;
+ this._languageLoader = languageLoader;
}
get tiptapEditor() {
@@ -34,23 +35,35 @@ export class ContentEditor {
}
async setSerializedContent(serializedContent) {
- const { _tiptapEditor: editor, _deserializer: deserializer, _eventHub: eventHub } = this;
+ const {
+ _tiptapEditor: editor,
+ _deserializer: deserializer,
+ _eventHub: eventHub,
+ _languageLoader: languageLoader,
+ } = this;
const { doc, tr } = editor.state;
const selection = TextSelection.create(doc, 0, doc.content.size);
try {
eventHub.$emit(LOADING_CONTENT_EVENT);
- const { document } = await deserializer.deserialize({
+ const result = await deserializer.deserialize({
schema: editor.schema,
content: serializedContent,
});
- if (document) {
- tr.setSelection(selection)
- .replaceSelectionWith(document, false)
- .setMeta('preventUpdate', true);
- editor.view.dispatch(tr);
+ if (Object.keys(result).length === 0) {
+ return;
}
+
+ const { document, dom } = result;
+
+ await languageLoader.loadLanguagesFromDOM(dom);
+
+ tr.setSelection(selection)
+ .replaceSelectionWith(document, false)
+ .setMeta('preventUpdate', true);
+ editor.view.dispatch(tr);
+
eventHub.$emit(LOADING_SUCCESS_EVENT);
} catch (e) {
eventHub.$emit(LOADING_ERROR_EVENT, e);
diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js
index d9d39a387d0..5b637eee176 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -1,5 +1,6 @@
import { Editor } from '@tiptap/vue-2';
import { isFunction } from 'lodash';
+import { lowlight } from 'lowlight/lib/core';
import eventHubFactory from '~/helpers/event_hub_factory';
import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '../constants';
import Attachment from '../extensions/attachment';
@@ -58,6 +59,7 @@ import { ContentEditor } from './content_editor';
import createMarkdownSerializer from './markdown_serializer';
import createMarkdownDeserializer from './markdown_deserializer';
import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts';
+import CodeBlockLanguageLoader from './code_block_language_loader';
const createTiptapEditor = ({ extensions = [], ...options } = {}) =>
new Editor({
@@ -83,6 +85,7 @@ export const createContentEditor = ({
const eventHub = eventHubFactory();
+ const languageLoader = new CodeBlockLanguageLoader(lowlight);
const builtInContentEditorExtensions = [
Attachment.configure({ uploadsPath, renderMarkdown, eventHub }),
Audio,
@@ -91,7 +94,7 @@ export const createContentEditor = ({
BulletList,
Code,
ColorChip,
- CodeBlockHighlight,
+ CodeBlockHighlight.configure({ lowlight, languageLoader }),
DescriptionItem,
DescriptionList,
Details,
@@ -105,7 +108,7 @@ export const createContentEditor = ({
FootnoteDefinition,
FootnoteReference,
FootnotesSection,
- Frontmatter,
+ Frontmatter.configure({ lowlight }),
Gapcursor,
HardBreak,
Heading,
@@ -144,5 +147,5 @@ export const createContentEditor = ({
const serializer = createMarkdownSerializer({ serializerConfig });
const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
- return new ContentEditor({ tiptapEditor, serializer, eventHub, deserializer });
+ return new ContentEditor({ tiptapEditor, serializer, eventHub, deserializer, languageLoader });
};
diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js
index 6a4009b222d..39a2939f52a 100644
--- a/app/assets/javascripts/security_configuration/components/constants.js
+++ b/app/assets/javascripts/security_configuration/components/constants.js
@@ -292,3 +292,10 @@ export const TEMP_PROVIDER_LOGOS = {
svg: scwLogo,
},
};
+
+// Use the `url` field from the GraphQL query once this issue is resolved
+// https://gitlab.com/gitlab-org/gitlab/-/issues/356129
+export const TEMP_PROVIDER_URLS = {
+ Kontra: 'https://application.security/',
+ [__('Secure Code Warrior')]: 'https://www.securecodewarrior.com/',
+};
diff --git a/app/assets/javascripts/security_configuration/components/training_provider_list.vue b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
index 6f130bec15e..bb540303cfd 100644
--- a/app/assets/javascripts/security_configuration/components/training_provider_list.vue
+++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
@@ -25,7 +25,7 @@ import {
updateSecurityTrainingCache,
updateSecurityTrainingOptimisticResponse,
} from '~/security_configuration/graphql/cache_utils';
-import { TEMP_PROVIDER_LOGOS } from './constants';
+import { TEMP_PROVIDER_LOGOS, TEMP_PROVIDER_URLS } from './constants';
const i18n = {
providerQueryErrorMessage: __(
@@ -206,6 +206,7 @@ export default {
},
i18n,
TEMP_PROVIDER_LOGOS,
+ TEMP_PROVIDER_URLS,
};
</script>
@@ -247,7 +248,8 @@ export default {
<p>
{{ provider.description }}
<gl-link
- :href="provider.url"
+ v-if="$options.TEMP_PROVIDER_URLS[provider.name]"
+ :href="$options.TEMP_PROVIDER_URLS[provider.name]"
target="_blank"
@click="trackProviderLearnMoreClick(provider.id)"
>
diff --git a/app/controllers/groups/releases_controller.rb b/app/controllers/groups/releases_controller.rb
index 6a42f30b847..db5385ecc71 100644
--- a/app/controllers/groups/releases_controller.rb
+++ b/app/controllers/groups/releases_controller.rb
@@ -15,11 +15,17 @@ module Groups
private
def releases
- ReleasesFinder
- .new(@group, current_user, { include_subgroups: true })
- .execute(preload: false)
- .page(params[:page])
- .per(30)
+ if Feature.enabled?(:group_releases_finder_inoperator)
+ Releases::GroupReleasesFinder
+ .new(@group, current_user, { include_subgroups: true, page: params[:page], per: 30 })
+ .execute(preload: false)
+ else
+ ReleasesFinder
+ .new(@group, current_user, { include_subgroups: true })
+ .execute(preload: false)
+ .page(params[:page])
+ .per(30)
+ end
end
end
end
diff --git a/app/controllers/projects/google_cloud/base_controller.rb b/app/controllers/projects/google_cloud/base_controller.rb
index b8a2cec9ac6..f293ec752ab 100644
--- a/app/controllers/projects/google_cloud/base_controller.rb
+++ b/app/controllers/projects/google_cloud/base_controller.rb
@@ -25,7 +25,7 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController
end
def feature_flag_enabled!
- unless Feature.enabled?(:incubation_5mp_google_cloud, project)
+ unless Feature.enabled?(:incubation_5mp_google_cloud)
track_event('feature_flag_enabled!', 'access_denied', 'feature_flag_not_enabled')
access_denied!
end
diff --git a/app/finders/releases/group_releases_finder.rb b/app/finders/releases/group_releases_finder.rb
new file mode 100644
index 00000000000..d87ba8c0b03
--- /dev/null
+++ b/app/finders/releases/group_releases_finder.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+module Releases
+ ##
+ # The GroupReleasesFinder does not support all the options of ReleasesFinder
+ # due to use of InOperatorOptimization for finding subprojects/subgroups
+ #
+ # order_by - only ordering by released_at is supported
+ # filter by tag - currently not supported
+ class GroupReleasesFinder
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :parent, :current_user, :params
+
+ def initialize(parent, current_user = nil, params = {})
+ @parent = parent
+ @current_user = current_user
+ @params = params
+
+ params[:order_by] ||= 'released_at'
+ params[:sort] ||= 'desc'
+ params[:page] ||= 0
+ params[:per] ||= 30
+ end
+
+ def execute(preload: true)
+ return Release.none unless Ability.allowed?(current_user, :read_release, parent)
+
+ releases = get_releases(preload: preload)
+
+ paginate_releases(releases)
+ end
+
+ private
+
+ def include_subgroups?
+ params.fetch(:include_subgroups, false)
+ end
+
+ def accessible_projects_scope
+ if include_subgroups?
+ Project.for_group_and_its_subgroups(parent)
+ else
+ parent.projects
+ end
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def get_releases(preload: true)
+ Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new(
+ scope: releases_scope(preload: preload),
+ array_scope: accessible_projects_scope.select(:id),
+ array_mapping_scope: -> (project_id_expression) { Release.where(Release.arel_table[:project_id].eq(project_id_expression)) },
+ finder_query: -> (order_by, id_expression) { Release.where(Release.arel_table[:id].eq(id_expression)) }
+ )
+ .execute
+ end
+
+ def releases_scope(preload: true)
+ scope = Release.all
+ scope = order_releases(scope)
+ scope = scope.preloaded if preload
+ scope
+ end
+
+ def order_releases(scope)
+ scope.sort_by_attribute("released_at_#{params[:sort]}").order(id: params[:sort])
+ end
+
+ def paginate_releases(releases)
+ releases.page(params[:page].to_i).per(params[:per])
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+end
diff --git a/app/models/release.rb b/app/models/release.rb
index cee52e132a4..c6c0920c4d0 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -6,6 +6,7 @@ class Release < ApplicationRecord
include Importable
include Gitlab::Utils::StrongMemoize
include EachBatch
+ include FromUnion
cache_markdown_field :description
diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb
index 556ee03605d..998a5deb0fd 100644
--- a/app/models/users/credit_card_validation.rb
+++ b/app/models/users/credit_card_validation.rb
@@ -8,7 +8,7 @@ module Users
belongs_to :user
- validates :holder_name, length: { maximum: 26 }
+ validates :holder_name, length: { maximum: 50 }
validates :network, length: { maximum: 32 }
validates :last_digits, allow_nil: true, numericality: {
greater_than_or_equal_to: 0, less_than_or_equal_to: 9999
diff --git a/app/views/admin/application_settings/_search_limits.html.haml b/app/views/admin/application_settings/_search_limits.html.haml
index 24403fe8fd3..945c9397f0d 100644
--- a/app/views/admin/application_settings/_search_limits.html.haml
+++ b/app/views/admin/application_settings/_search_limits.html.haml
@@ -3,11 +3,11 @@
%fieldset
.form-group
- = f.label :search_rate_limit, _('Maximum authenticated requests by a user per minute'), class: 'label-bold'
+ = f.label :search_rate_limit, _('Maximum number of requests per minute for an authenticated user'), class: 'label-bold'
.form-text.gl-text-gray-600
= _("Set this number to 0 to disable the limit.")
+ = f.number_field :search_rate_limit, class: 'form-control gl-form-input'
- = f.label :search_rate_limit, _('Maximum number of requests per minute for an authenticated user'), class: 'label-bold'
.form-group
= f.label :search_rate_limit_unauthenticated, _('Maximum number of requests per minute for an unauthenticated IP address'), class: 'label-bold'
= f.number_field :search_rate_limit_unauthenticated, class: 'form-control gl-form-input'
diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml
index b0e3f8182f6..ea35b7ab9c4 100644
--- a/app/views/admin/application_settings/network.html.haml
+++ b/app/views/admin/application_settings/network.html.haml
@@ -48,7 +48,7 @@
.settings-content
= render partial: 'network_rate_limits', locals: { anchor: 'js-files-limits-settings', setting_fragment: 'files_api' }
-%section.settings.as-note-limits.no-animate#js-search-limits-settings{ class: ('expanded' if expanded_by_default?) }
+%section.settings.as-search-limits.no-animate#js-search-limits-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= _('Search rate limits')
diff --git a/config/feature_flags/development/group_releases_finder_inoperator.yml b/config/feature_flags/development/group_releases_finder_inoperator.yml
new file mode 100644
index 00000000000..c76c328b5bf
--- /dev/null
+++ b/config/feature_flags/development/group_releases_finder_inoperator.yml
@@ -0,0 +1,8 @@
+---
+name: group_releases_finder_inoperator
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80093
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/355463
+milestone: '14.9'
+type: development
+group: group::release
+default_enabled: false
diff --git a/db/migrate/20220310101118_update_holder_name_limit.rb b/db/migrate/20220310101118_update_holder_name_limit.rb
new file mode 100644
index 00000000000..55eb8f75d70
--- /dev/null
+++ b/db/migrate/20220310101118_update_holder_name_limit.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+# See https://docs.gitlab.com/ee/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class UpdateHolderNameLimit < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :user_credit_card_validations, :holder_name, 50, constraint_name: new_constraint_name
+ remove_text_limit :user_credit_card_validations, :holder_name, constraint_name: old_constraint_name
+ end
+
+ def down
+ add_text_limit :user_credit_card_validations, :holder_name, 26, validate: false, constraint_name: old_constraint_name
+ remove_text_limit :user_credit_card_validations, :holder_name, constraint_name: new_constraint_name
+ end
+
+ private
+
+ def old_constraint_name
+ check_constraint_name(:user_credit_card_validations, :holder_name, 'max_length')
+ end
+
+ def new_constraint_name
+ check_constraint_name(:user_credit_card_validations, :holder_name, 'max_length_50')
+ end
+end
diff --git a/db/post_migrate/20220223112304_schedule_nullify_orphan_runner_id_on_ci_builds.rb b/db/post_migrate/20220223112304_schedule_nullify_orphan_runner_id_on_ci_builds.rb
index c62f459d270..e4005885c3b 100644
--- a/db/post_migrate/20220223112304_schedule_nullify_orphan_runner_id_on_ci_builds.rb
+++ b/db/post_migrate/20220223112304_schedule_nullify_orphan_runner_id_on_ci_builds.rb
@@ -3,9 +3,9 @@
class ScheduleNullifyOrphanRunnerIdOnCiBuilds < Gitlab::Database::Migration[1.0]
MIGRATION = 'NullifyOrphanRunnerIdOnCiBuilds'
INTERVAL = 2.minutes
- BATCH_SIZE = 100_000
- MAX_BATCH_SIZE = 100_000 # 100k * 25k = 2.5B ci_builds
- SUB_BATCH_SIZE = 1_000
+ BATCH_SIZE = 50_000
+ MAX_BATCH_SIZE = 150_000
+ SUB_BATCH_SIZE = 500
def up
queue_batched_background_migration(
diff --git a/db/post_migrate/20220308115219_schedule_reset_duplicate_ci_runners_token_encrypted_values_on_projects.rb b/db/post_migrate/20220308115219_schedule_reset_duplicate_ci_runners_token_encrypted_values_on_projects.rb
new file mode 100644
index 00000000000..27e7af9a550
--- /dev/null
+++ b/db/post_migrate/20220308115219_schedule_reset_duplicate_ci_runners_token_encrypted_values_on_projects.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class ScheduleResetDuplicateCiRunnersTokenEncryptedValuesOnProjects < Gitlab::Database::Migration[1.0]
+ MIGRATION = 'ResetDuplicateCiRunnersTokenEncryptedValuesOnProjects'
+ TOKEN_COLUMN_NAME = :runners_token_encrypted
+ TEMP_INDEX_NAME = "tmp_index_projects_on_id_and_#{TOKEN_COLUMN_NAME}"
+ BATCH_SIZE = 10_000
+ DELAY_INTERVAL = 2.minutes
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :projects, [:id, TOKEN_COLUMN_NAME], where: "#{TOKEN_COLUMN_NAME} IS NOT NULL", unique: false, name: TEMP_INDEX_NAME
+
+ queue_background_migration_jobs_by_range_at_intervals(
+ Gitlab::BackgroundMigration::ResetDuplicateCiRunnersTokenEncryptedValuesOnProjects::Project.base_query,
+ MIGRATION,
+ DELAY_INTERVAL,
+ batch_size: BATCH_SIZE,
+ track_jobs: true
+ )
+ end
+
+ def down
+ remove_concurrent_index_by_name(:projects, name: TEMP_INDEX_NAME)
+ end
+end
diff --git a/db/post_migrate/20220308115502_schedule_reset_duplicate_ci_runners_token_values_on_projects.rb b/db/post_migrate/20220308115502_schedule_reset_duplicate_ci_runners_token_values_on_projects.rb
new file mode 100644
index 00000000000..f076b0a740e
--- /dev/null
+++ b/db/post_migrate/20220308115502_schedule_reset_duplicate_ci_runners_token_values_on_projects.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class ScheduleResetDuplicateCiRunnersTokenValuesOnProjects < Gitlab::Database::Migration[1.0]
+ MIGRATION = 'ResetDuplicateCiRunnersTokenValuesOnProjects'
+ TOKEN_COLUMN_NAME = :runners_token
+ TEMP_INDEX_NAME = "tmp_index_projects_on_id_and_#{TOKEN_COLUMN_NAME}"
+ BATCH_SIZE = 10_000
+ DELAY_INTERVAL = 2.minutes
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :projects, [:id, TOKEN_COLUMN_NAME], where: "#{TOKEN_COLUMN_NAME} IS NOT NULL", unique: false, name: TEMP_INDEX_NAME
+
+ queue_background_migration_jobs_by_range_at_intervals(
+ Gitlab::BackgroundMigration::ResetDuplicateCiRunnersTokenValuesOnProjects::Project.base_query,
+ MIGRATION,
+ DELAY_INTERVAL,
+ batch_size: BATCH_SIZE,
+ track_jobs: true
+ )
+ end
+
+ def down
+ remove_concurrent_index_by_name(:projects, name: TEMP_INDEX_NAME)
+ end
+end
diff --git a/db/schema_migrations/20220308115219 b/db/schema_migrations/20220308115219
new file mode 100644
index 00000000000..6e55d2fdabe
--- /dev/null
+++ b/db/schema_migrations/20220308115219
@@ -0,0 +1 @@
+e18ed9e6b2a98c77190ff2ce33f4d2b1984710b438e851d6a526ec8bb1f33c80 \ No newline at end of file
diff --git a/db/schema_migrations/20220308115502 b/db/schema_migrations/20220308115502
new file mode 100644
index 00000000000..c379b67485c
--- /dev/null
+++ b/db/schema_migrations/20220308115502
@@ -0,0 +1 @@
+0aacf46a4a5b430a718336108f52c1c0bed4283846f36c2ab1de80100dcae0b4 \ No newline at end of file
diff --git a/db/schema_migrations/20220310101118 b/db/schema_migrations/20220310101118
new file mode 100644
index 00000000000..c87f727c8b9
--- /dev/null
+++ b/db/schema_migrations/20220310101118
@@ -0,0 +1 @@
+e4d6111f19f05b42b51e8d066e221205460514cef88ecf15ca99aa59788c4153 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 3895dcd9822..28fc3638728 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -21129,7 +21129,7 @@ CREATE TABLE user_credit_card_validations (
network text,
CONSTRAINT check_1765e2b30f CHECK ((char_length(network) <= 32)),
CONSTRAINT check_3eea080c91 CHECK (((last_digits >= 0) AND (last_digits <= 9999))),
- CONSTRAINT check_eafe45d88b CHECK ((char_length(holder_name) <= 26))
+ CONSTRAINT check_cc0c8dc0fe CHECK ((char_length(holder_name) <= 50))
);
CREATE TABLE user_custom_attributes (
@@ -29551,6 +29551,10 @@ CREATE UNIQUE INDEX tmp_index_on_tmp_project_id_on_namespaces ON namespaces USIN
CREATE INDEX tmp_index_on_vulnerabilities_non_dismissed ON vulnerabilities USING btree (id) WHERE (state <> 2);
+CREATE INDEX tmp_index_projects_on_id_and_runners_token ON projects USING btree (id, runners_token) WHERE (runners_token IS NOT NULL);
+
+CREATE INDEX tmp_index_projects_on_id_and_runners_token_encrypted ON projects USING btree (id, runners_token_encrypted) WHERE (runners_token_encrypted IS NOT NULL);
+
CREATE UNIQUE INDEX uniq_pkgs_deb_grp_architectures_on_distribution_id_and_name ON packages_debian_group_architectures USING btree (distribution_id, name);
CREATE UNIQUE INDEX uniq_pkgs_deb_grp_components_on_distribution_id_and_name ON packages_debian_group_components USING btree (distribution_id, name);
diff --git a/doc/administration/clusters/kas.md b/doc/administration/clusters/kas.md
index abc3ffa539e..e5c371b9d40 100644
--- a/doc/administration/clusters/kas.md
+++ b/doc/administration/clusters/kas.md
@@ -9,8 +9,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3834) in GitLab 13.10, the GitLab agent server (KAS) became available on GitLab.com at `wss://kas.gitlab.com`.
> - [Moved](https://gitlab.com/groups/gitlab-org/-/epics/6290) from GitLab Premium to GitLab Free in 14.5.
-The GitLab agent server for Kubernetes (KAS) is a service that
-manages the [GitLab agent for Kubernetes](../../user/clusters/agent/index.md).
+The agent server is a component you install together with GitLab. It is required to
+manage the [GitLab agent for Kubernetes](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent).
The KAS acronym refers to the former name, `Kubernetes agent server`.
diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md
index c7aa44f3a97..11a34f5b5f8 100644
--- a/doc/administration/instance_limits.md
+++ b/doc/administration/instance_limits.md
@@ -153,6 +153,17 @@ Set the limit to `0` to disable it.
- **Default rate limit**: Disabled (unlimited).
+### Search rate limit
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80631) in GitLab 14.9
+
+This setting limits global search requests.
+
+| Limit | Default (requests per minute) |
+|-------------------------|-------------------------------|
+| Authenticated user | 30 |
+| Unauthenticated user | 10 |
+
## Gitaly concurrency limit
Clone traffic can put a large strain on your Gitaly service. To prevent such workloads from overwhelming your Gitaly server, you can set concurrency limits in Gitaly's configuration file.
diff --git a/doc/ci/environments/deployment_approvals.md b/doc/ci/environments/deployment_approvals.md
index e42cde031ad..a44458f0490 100644
--- a/doc/ci/environments/deployment_approvals.md
+++ b/doc/ci/environments/deployment_approvals.md
@@ -81,23 +81,34 @@ Maintainer role.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/342180/) in GitLab 14.9
-A blocked deployment is enqueued as soon as it receives the required number of approvals. A single
-rejection causes the deployment to fail. The creator of a deployment cannot approve it, even if they
-have permission to deploy.
+You can use the UI or API to take these actions on a deployment:
-You can approve or reject a deployment to a protected environment either in the UI or using the API:
+- Approve it
+- Allow it to proceed
+- Reject it
-### Using the UI
+### Approve or reject a deployment using the UI
+
+Prerequisites:
+
+- Permission to deploy to the protected environment.
+
+To approve or reject a deployment to a protected environment using the UI:
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Deployments > Environments**.
1. In the deployment's row, select **Approval options** (**{thumb-up}**).
1. Select **Approve** or **Reject**.
-### Using the API
+### Approve or reject a deployment using the API
+
+Prerequisites:
+
+- Permission to deploy to the protected environment.
-Users who are allowed to deploy to the protected environment can approve or reject a blocked
-deployment using the [Deployments API](../../api/deployments.md#approve-or-reject-a-blocked-deployment).
+To approve or reject a deployment to a protected environment using the API, pass the
+required attributes. For more details, see
+[Approve or reject a blocked deployment](../../api/deployments.md#approve-or-reject-a-blocked-deployment).
Example:
diff --git a/doc/development/documentation/graphql_styleguide.md b/doc/development/documentation/graphql_styleguide.md
index 5acc8bda6a6..ad19a40a3f5 100644
--- a/doc/development/documentation/graphql_styleguide.md
+++ b/doc/development/documentation/graphql_styleguide.md
@@ -6,7 +6,7 @@ info: "See the Technical Writers assigned to Development Guidelines: https://abo
description: "Writing styles, markup, formatting, and other standards for GraphQL API's GitLab Documentation."
---
-# GraphQL API
+# Creating a GraphQL example page
GraphQL APIs are different from [RESTful APIs](restful_api_styleguide.md). Reference
information is generated in our [GraphQL reference](../../api/graphql/reference/index.md).
diff --git a/doc/user/admin_area/settings/index.md b/doc/user/admin_area/settings/index.md
index da8bc4062f1..052b6e26c07 100644
--- a/doc/user/admin_area/settings/index.md
+++ b/doc/user/admin_area/settings/index.md
@@ -130,6 +130,7 @@ The **Network** settings contain:
Git LFS requests that supersede the user and IP rate limits.
- [Files API Rate Limits](files_api_rate_limits.md) - Configure specific limits for
Files API requests that supersede the user and IP rate limits.
+ - [Search rate limits](../../../administration/instance_limits.md#search-rate-limit) - Configure global search request rate limits for authenticated and unauthenticated users.
- [Deprecated API Rate Limits](deprecated_api_rate_limits.md) - Configure specific limits
for deprecated API requests that supersede the user and IP rate limits.
- [Outbound requests](../../../security/webhooks.md) - Allow requests to the local network from hooks and services.
diff --git a/doc/user/project/integrations/harbor.md b/doc/user/project/integrations/harbor.md
new file mode 100644
index 00000000000..d66e2222538
--- /dev/null
+++ b/doc/user/project/integrations/harbor.md
@@ -0,0 +1,50 @@
+---
+stage: Ecosystem
+group: Integrations
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+---
+
+# Harbor container registry integration **(FREE)**
+
+Use Harbor as the container registry for your GitLab project.
+
+[Harbor](https://goharbor.io/) is an open source registry that can help you manage artifacts across cloud native compute platforms, like Kubernetes and Docker.
+
+This integration can help you if you need GitLab CI/CD and a container image repository.
+
+## Prerequisites
+
+In the Harbor instance, ensure that:
+
+- The project to be integrated has been created.
+- The signed-in user has permission to pull, push, and edit images in the Harbor project.
+
+## Configure GitLab
+
+GitLab supports integrating Harbor projects at the group or project level. Complete these steps in GitLab:
+
+1. On the top bar, select **Menu > Projects** and find your project.
+1. On the left sidebar, select **Settings > Integrations**.
+1. Select **Harbor**.
+1. Turn on the **Active** toggle under **Enable Integration**.
+1. Provide the Harbor configuration information:
+ - **Harbor URL**: The base URL of Harbor instance which is being linked to this GitLab project. For example, `https://harbor.example.net`.
+ - **Harbor project name**: The project name in the Harbor instance. For example, `testproject`.
+ - **Username**: Your username in the Harbor instance, which should meet the requirements in [prerequisites](#prerequisites).
+ - **Password**: Password of your username.
+
+1. Select **Save changes**.
+
+After the Harbor integration is activated:
+
+- The global variables `$HARBOR_USER`, `$HARBOR_PASSWORD`, `$HARBOR_URL`, and `$HARBOR_PROJECT` are created for CI/CD use.
+- The project-level integration settings override the group-level integration settings.
+
+## Secure your requests to the Harbor APIs
+
+For each API request through the Harbor integration, the credentials for your connection to the Harbor API use
+the `username:password` combination. The following are suggestions for safe use:
+
+- Use TLS on the Harbor APIs you connect to.
+- Follow the principle of least privilege (for access on Harbor) with your credentials.
+- Have a rotation policy on your credentials.
diff --git a/doc/user/project/integrations/overview.md b/doc/user/project/integrations/overview.md
index 8ecc16050be..2cb62b8924e 100644
--- a/doc/user/project/integrations/overview.md
+++ b/doc/user/project/integrations/overview.md
@@ -43,6 +43,7 @@ Click on the service links to see further configuration instructions and details
| [Flowdock](../../../api/integrations.md#flowdock) | Send notifications from GitLab to Flowdock flows. | **{dotted-circle}** No |
| [GitHub](github.md) | Obtain statuses for commits and pull requests. | **{dotted-circle}** No |
| [Google Chat](hangouts_chat.md) | Send notifications from your GitLab project to a room in Google Chat.| **{dotted-circle}** No |
+| [Harbor](harbor.md) | Use Harbor as the container registry. | **{dotted-circle}** No |
| [irker (IRC gateway)](irker.md) | Send IRC messages. | **{dotted-circle}** No |
| [Jenkins](../../../integration/jenkins.md) | Run CI/CD pipelines with Jenkins. | **{check-circle}** Yes |
| JetBrains TeamCity CI | Run CI/CD pipelines with TeamCity. | **{check-circle}** Yes |
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index c3632c812f3..2e21f591667 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -248,6 +248,8 @@ module API
changelog = service.execute(commit_to_changelog: false)
present changelog, with: Entities::Changelog
+ rescue Gitlab::Changelog::Error => ex
+ render_api_error!("Failed to generate the changelog: #{ex.message}", 422)
end
desc 'Generates a changelog section for a release and commits it in a changelog file' do
diff --git a/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects.rb b/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects.rb
new file mode 100644
index 00000000000..80ca76ef37f
--- /dev/null
+++ b/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # A job to nullify duplicate runners_token_encrypted values in projects table in batches
+ class ResetDuplicateCiRunnersTokenEncryptedValuesOnProjects
+ class Project < ActiveRecord::Base # rubocop:disable Style/Documentation
+ include ::EachBatch
+
+ self.table_name = 'projects'
+
+ scope :base_query, -> do
+ where.not(runners_token_encrypted: nil)
+ end
+ end
+
+ def perform(start_id, end_id)
+ # Reset duplicate runner tokens that would prevent creating an unique index.
+ duplicate_tokens = Project.base_query
+ .where(id: start_id..end_id)
+ .group(:runners_token_encrypted)
+ .having('COUNT(*) > 1')
+ .pluck(:runners_token_encrypted)
+
+ Project.where(runners_token_encrypted: duplicate_tokens).update_all(runners_token_encrypted: nil) if duplicate_tokens.any?
+
+ mark_job_as_succeeded(start_id, end_id)
+ end
+
+ private
+
+ def mark_job_as_succeeded(*arguments)
+ Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded('ResetDuplicateCiRunnersTokenEncryptedValuesOnProjects', arguments)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects.rb b/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects.rb
new file mode 100644
index 00000000000..d87ce6c88d3
--- /dev/null
+++ b/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # A job to nullify duplicate ci_runners_token values in projects table in batches
+ class ResetDuplicateCiRunnersTokenValuesOnProjects
+ class Project < ActiveRecord::Base # rubocop:disable Style/Documentation
+ include ::EachBatch
+
+ self.table_name = 'projects'
+
+ scope :base_query, -> do
+ where.not(runners_token: nil)
+ end
+ end
+
+ def perform(start_id, end_id)
+ # Reset duplicate runner tokens that would prevent creating an unique index.
+ duplicate_tokens = Project.base_query
+ .where(id: start_id..end_id)
+ .group(:runners_token)
+ .having('COUNT(*) > 1')
+ .pluck(:runners_token)
+
+ Project.where(runners_token: duplicate_tokens).update_all(runners_token: nil) if duplicate_tokens.any?
+
+ mark_job_as_succeeded(start_id, end_id)
+ end
+
+ private
+
+ def mark_job_as_succeeded(*arguments)
+ Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded('ResetDuplicateCiRunnerValuesTokensOnProjects', arguments)
+ end
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 4ff3918efb6..95267f78373 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -22841,9 +22841,6 @@ msgstr ""
msgid "Maximum authenticated API requests per rate limit period per user"
msgstr ""
-msgid "Maximum authenticated requests by a user per minute"
-msgstr ""
-
msgid "Maximum authenticated web requests per rate limit period per user"
msgstr ""
@@ -23214,7 +23211,7 @@ msgstr ""
msgid "Merge automatically (%{strategy})"
msgstr ""
-msgid "Merge blocked: all merge request dependencies must be merged or closed."
+msgid "Merge blocked: all merge request dependencies must be merged."
msgstr ""
msgid "Merge blocked: merge request must be marked as ready. It's still marked as draft."
@@ -32351,7 +32348,7 @@ msgstr ""
msgid "Saving project."
msgstr ""
-msgid "ScanResultPolicy|%{ifLabelStart}if%{ifLabelEnd} %{scanners} scan in an open merge request targeting the %{branches} branch(es) finds %{vulnerabilitiesAllowed} or more %{severities} vulnerabilities that are %{vulnerabilityStates}"
+msgid "ScanResultPolicy|%{ifLabelStart}if%{ifLabelEnd} %{scanners} find(s) more than %{vulnerabilitiesAllowed} %{severities} %{vulnerabilityStates} vulnerabilities in an open merge request targeting %{branches}"
msgstr ""
msgid "ScanResultPolicy|%{thenLabelStart}Then%{thenLabelEnd} Require approval from %{approvalsRequired} of the following approvers:"
diff --git a/spec/controllers/groups/releases_controller_spec.rb b/spec/controllers/groups/releases_controller_spec.rb
index 582a77b1c50..8b08f913e10 100644
--- a/spec/controllers/groups/releases_controller_spec.rb
+++ b/spec/controllers/groups/releases_controller_spec.rb
@@ -20,11 +20,11 @@ RSpec.describe Groups::ReleasesController do
context 'as json' do
let(:format) { :json }
- subject { get :index, params: { group_id: group }, format: format }
+ subject(:index) { get :index, params: { group_id: group }, format: format }
context 'json_response' do
before do
- subject
+ index
end
it 'returns an application/json content_type' do
@@ -38,7 +38,7 @@ RSpec.describe Groups::ReleasesController do
context 'the user is not authorized' do
before do
- subject
+ index
end
it 'does not return any releases' do
@@ -54,12 +54,38 @@ RSpec.describe Groups::ReleasesController do
it "returns all group's public and private project's releases as JSON, ordered by released_at" do
sign_in(guest)
- subject
+ index
expect(json_response.map {|r| r['tag'] } ).to match_array(%w(p2 p1 v2 v1))
end
end
+ context 'group_releases_finder_inoperator feature flag' do
+ before do
+ sign_in(guest)
+ end
+
+ it 'calls old code when disabled' do
+ stub_feature_flags(group_releases_finder_inoperator: false)
+
+ allow(ReleasesFinder).to receive(:new).and_call_original
+
+ index
+
+ expect(ReleasesFinder).to have_received(:new)
+ end
+
+ it 'calls new code when enabled' do
+ stub_feature_flags(group_releases_finder_inoperator: true)
+
+ allow(Releases::GroupReleasesFinder).to receive(:new).and_call_original
+
+ index
+
+ expect(Releases::GroupReleasesFinder).to have_received(:new)
+ end
+ end
+
context 'N+1 queries' do
it 'avoids N+1 database queries' do
control_count = ActiveRecord::QueryRecorder.new { subject }.count
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 33e22c377a3..df93bd773a6 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -707,6 +707,20 @@ RSpec.describe 'Admin updates settings' do
include_examples 'regular throttle rate limit settings'
end
+
+ it 'changes search rate limits' do
+ visit network_admin_application_settings_path
+
+ page.within('.as-search-limits') do
+ fill_in 'Maximum number of requests per minute for an authenticated user', with: 98
+ fill_in 'Maximum number of requests per minute for an unauthenticated IP address', with: 76
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+ expect(current_settings.search_rate_limit).to eq(98)
+ expect(current_settings.search_rate_limit_unauthenticated).to eq(76)
+ end
end
context 'Preferences page' do
diff --git a/spec/finders/releases/group_releases_finder_spec.rb b/spec/finders/releases/group_releases_finder_spec.rb
new file mode 100644
index 00000000000..b8899a8ee40
--- /dev/null
+++ b/spec/finders/releases/group_releases_finder_spec.rb
@@ -0,0 +1,204 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Releases::GroupReleasesFinder do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, :repository, group: group) }
+ let(:params) { {} }
+ let(:args) { {} }
+ let(:repository) { project.repository }
+ let(:v1_0_0) { create(:release, project: project, tag: 'v1.0.0') }
+ let(:v1_1_0) { create(:release, project: project, tag: 'v1.1.0') }
+ let(:v1_1_1) { create(:release, project: project, tag: 'v1.1.1') }
+
+ before do
+ v1_0_0.update_attribute(:released_at, 2.days.ago)
+ v1_1_0.update_attribute(:released_at, 1.day.ago)
+ v1_1_1.update_attribute(:released_at, 0.5.days.ago)
+ end
+
+ shared_examples_for 'when the user is not part of the project' do
+ it 'returns no releases' do
+ is_expected.to be_empty
+ end
+ end
+
+ shared_examples_for 'when the user is not part of the group' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :read_release, group).and_return(false)
+ end
+
+ it 'returns no releases' do
+ is_expected.to be_empty
+ end
+ end
+
+ shared_examples_for 'preload' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :read_release, group).and_return(true)
+ end
+
+ it 'preloads associations' do
+ expect(Release).to receive(:preloaded).once.and_call_original
+
+ releases
+ end
+
+ context 'when preload is false' do
+ let(:args) { { preload: false } }
+
+ it 'does not preload associations' do
+ expect(Release).not_to receive(:preloaded)
+
+ releases
+ end
+ end
+ end
+
+ describe 'when parent is a group' do
+ context 'without subgroups' do
+ let(:project2) { create(:project, :repository, namespace: group) }
+ let!(:v6) { create(:release, project: project2, tag: 'v6') }
+
+ subject(:releases) { described_class.new(group, user, params).execute(**args) }
+
+ it_behaves_like 'preload'
+ it_behaves_like 'when the user is not part of the group'
+
+ context 'when the user is a project guest on one sibling project' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'does not return any releases' do
+ expect(releases.size).to eq(0)
+ expect(releases).to eq([])
+ end
+ end
+
+ context 'when the user is a guest on the group' do
+ before do
+ group.add_guest(user)
+ v1_0_0.update_attribute(:released_at, 3.days.ago)
+ v6.update_attribute(:released_at, 2.days.ago)
+ v1_1_0.update_attribute(:released_at, 1.day.ago)
+ v1_1_1.update_attribute(:released_at, v1_1_0.released_at)
+ end
+
+ it 'sorts by release date and id' do
+ expect(releases.size).to eq(4)
+ expect(releases).to eq([v1_1_1, v1_1_0, v6, v1_0_0])
+ end
+ end
+ end
+
+ describe 'with subgroups' do
+ let(:params) { { include_subgroups: true } }
+
+ subject(:releases) { described_class.new(group, user, params).execute(**args) }
+
+ context 'with a single-level subgroup' do
+ let(:subgroup) { create(:group, parent: group) }
+ let(:project2) { create(:project, :repository, namespace: subgroup) }
+ let!(:v6) { create(:release, project: project2, tag: 'v6') }
+
+ it_behaves_like 'when the user is not part of the group'
+
+ context 'when the user a project guest in the subgroup project' do
+ before do
+ project2.add_guest(user)
+ end
+
+ it 'does not return any releases' do
+ expect(releases).to match_array([])
+ end
+ end
+
+ context 'when the user is a guest on the group' do
+ before do
+ group.add_guest(user)
+ v6.update_attribute(:released_at, 2.days.ago)
+ end
+
+ it 'returns all releases' do
+ expect(releases).to match_array([v1_1_1, v1_1_0, v1_0_0, v6])
+ end
+ end
+ end
+
+ context 'with a multi-level subgroup' do
+ let(:subgroup) { create(:group, parent: group) }
+ let(:subsubgroup) { create(:group, parent: subgroup) }
+ let(:project2) { create(:project, :repository, namespace: subgroup) }
+ let(:project3) { create(:project, :repository, namespace: subsubgroup) }
+ let!(:v6) { create(:release, project: project2, tag: 'v6') }
+ let!(:p3) { create(:release, project: project3, tag: 'p3') }
+
+ before do
+ v6.update_attribute(:released_at, 2.days.ago)
+ p3.update_attribute(:released_at, 3.days.ago)
+ end
+
+ it_behaves_like 'when the user is not part of the group'
+
+ context 'when the user a project guest in the subgroup and subsubgroup project' do
+ before do
+ project2.add_guest(user)
+ project3.add_guest(user)
+ end
+
+ it 'does not return any releases' do
+ expect(releases).to match_array([])
+ end
+ end
+
+ context 'when the user a project guest in the subsubgroup project' do
+ before do
+ project3.add_guest(user)
+ end
+
+ it 'does not return any releases' do
+ expect(releases).to match_array([])
+ end
+ end
+
+ context 'when the user a guest on the group' do
+ before do
+ group.add_guest(user)
+ end
+
+ it 'returns all releases' do
+ expect(releases).to match_array([v1_1_1, v1_1_0, v6, v1_0_0, p3])
+ end
+ end
+
+ context 'performance testing' do
+ shared_examples 'avoids N+1 queries' do |query_params = {}|
+ context 'with subgroups' do
+ let(:params) { query_params }
+
+ it 'include_subgroups avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ releases
+ end.count
+
+ subgroups = create_list(:group, 10, parent: group)
+ projects = create_list(:project, 10, namespace: subgroups[0])
+ create_list(:release, 10, project: projects[0], author: user)
+
+ expect do
+ releases
+ end.not_to exceed_all_query_limit(control_count)
+ end
+ end
+ end
+
+ it_behaves_like 'avoids N+1 queries'
+ it_behaves_like 'avoids N+1 queries', { simple: true }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
index 05fa0f79ef0..02e5b1dc271 100644
--- a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
+++ b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
@@ -1,5 +1,5 @@
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
-import { createTestEditor } from '../test_utils';
+import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
const CODE_BLOCK_HTML = `<pre class="code highlight js-syntax-highlight language-javascript" lang="javascript" v-pre="true">
<code>
@@ -12,34 +12,78 @@ const CODE_BLOCK_HTML = `<pre class="code highlight js-syntax-highlight language
describe('content_editor/extensions/code_block_highlight', () => {
let parsedCodeBlockHtmlFixture;
let tiptapEditor;
+ let doc;
+ let codeBlock;
+ let languageLoader;
const parseHTML = (html) => new DOMParser().parseFromString(html, 'text/html');
const preElement = () => parsedCodeBlockHtmlFixture.querySelector('pre');
beforeEach(() => {
- tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight] });
- parsedCodeBlockHtmlFixture = parseHTML(CODE_BLOCK_HTML);
+ languageLoader = { loadLanguages: jest.fn() };
+ tiptapEditor = createTestEditor({
+ extensions: [CodeBlockHighlight.configure({ languageLoader })],
+ });
- tiptapEditor.commands.setContent(CODE_BLOCK_HTML);
+ ({
+ builders: { doc, codeBlock },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ codeBlock: { nodeType: CodeBlockHighlight.name },
+ },
+ }));
});
- it('extracts language and params attributes from Markdown API output', () => {
- const language = preElement().getAttribute('lang');
+ describe('when parsing HTML', () => {
+ beforeEach(() => {
+ parsedCodeBlockHtmlFixture = parseHTML(CODE_BLOCK_HTML);
- expect(tiptapEditor.getJSON().content[0].attrs).toMatchObject({
- language,
+ tiptapEditor.commands.setContent(CODE_BLOCK_HTML);
+ });
+ it('extracts language and params attributes from Markdown API output', () => {
+ const language = preElement().getAttribute('lang');
+
+ expect(tiptapEditor.getJSON().content[0].attrs).toMatchObject({
+ language,
+ });
+ });
+
+ it('adds code, highlight, and js-syntax-highlight to code block element', () => {
+ const editorHtmlOutput = parseHTML(tiptapEditor.getHTML()).querySelector('pre');
+
+ expect(editorHtmlOutput.classList.toString()).toContain('code highlight js-syntax-highlight');
});
- });
- it('adds code, highlight, and js-syntax-highlight to code block element', () => {
- const editorHtmlOutput = parseHTML(tiptapEditor.getHTML()).querySelector('pre');
+ it('adds content-editor-code-block class to the pre element', () => {
+ const editorHtmlOutput = parseHTML(tiptapEditor.getHTML()).querySelector('pre');
- expect(editorHtmlOutput.classList.toString()).toContain('code highlight js-syntax-highlight');
+ expect(editorHtmlOutput.classList.toString()).toContain('content-editor-code-block');
+ });
});
- it('adds content-editor-code-block class to the pre element', () => {
- const editorHtmlOutput = parseHTML(tiptapEditor.getHTML()).querySelector('pre');
+ describe.each`
+ inputRule
+ ${'```'}
+ ${'~~~'}
+ `('when typing $inputRule input rule', ({ inputRule }) => {
+ const language = 'javascript';
+
+ beforeEach(() => {
+ triggerNodeInputRule({
+ tiptapEditor,
+ inputRuleText: `${inputRule}${language} `,
+ });
+ });
+
+ it('creates a new code block and loads related language', () => {
+ const expectedDoc = doc(codeBlock({ language }));
- expect(editorHtmlOutput.classList.toString()).toContain('content-editor-code-block');
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+
+ it('loads language when language loader is available', () => {
+ expect(languageLoader.loadLanguages).toHaveBeenCalledWith([language]);
+ });
});
});
diff --git a/spec/frontend/content_editor/services/code_block_language_loader_spec.js b/spec/frontend/content_editor/services/code_block_language_loader_spec.js
new file mode 100644
index 00000000000..bb97c9afa41
--- /dev/null
+++ b/spec/frontend/content_editor/services/code_block_language_loader_spec.js
@@ -0,0 +1,70 @@
+import CodeBlockLanguageBlocker from '~/content_editor/services/code_block_language_loader';
+
+describe('content_editor/services/code_block_language_loader', () => {
+ let languageLoader;
+ let lowlight;
+
+ beforeEach(() => {
+ lowlight = {
+ languages: [],
+ registerLanguage: jest
+ .fn()
+ .mockImplementation((language) => lowlight.languages.push(language)),
+ registered: jest.fn().mockImplementation((language) => lowlight.languages.includes(language)),
+ };
+ languageLoader = new CodeBlockLanguageBlocker(lowlight);
+ });
+
+ describe('loadLanguages', () => {
+ it('loads highlight.js language packages identified by a list of languages', async () => {
+ const languages = ['javascript', 'ruby'];
+
+ await languageLoader.loadLanguages(languages);
+
+ languages.forEach((language) => {
+ expect(lowlight.registerLanguage).toHaveBeenCalledWith(language, expect.any(Function));
+ });
+ });
+
+ describe('when language is already registered', () => {
+ it('does not load the language again', async () => {
+ const languages = ['javascript'];
+
+ await languageLoader.loadLanguages(languages);
+ await languageLoader.loadLanguages(languages);
+
+ expect(lowlight.registerLanguage).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+
+ describe('loadLanguagesFromDOM', () => {
+ it('loads highlight.js language packages identified by pre tags in a DOM fragment', async () => {
+ const parser = new DOMParser();
+ const { body } = parser.parseFromString(
+ `
+ <pre lang="javascript"></pre>
+ <pre lang="ruby"></pre>
+ `,
+ 'text/html',
+ );
+
+ await languageLoader.loadLanguagesFromDOM(body);
+
+ expect(lowlight.registerLanguage).toHaveBeenCalledWith('javascript', expect.any(Function));
+ expect(lowlight.registerLanguage).toHaveBeenCalledWith('ruby', expect.any(Function));
+ });
+ });
+
+ describe('isLanguageLoaded', () => {
+ it('returns true when a language is registered', async () => {
+ const language = 'javascript';
+
+ expect(languageLoader.isLanguageLoaded(language)).toBe(false);
+
+ await languageLoader.loadLanguages([language]);
+
+ expect(languageLoader.isLanguageLoaded(language)).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/services/content_editor_spec.js b/spec/frontend/content_editor/services/content_editor_spec.js
index 3bc72b13302..5b7a27b501d 100644
--- a/spec/frontend/content_editor/services/content_editor_spec.js
+++ b/spec/frontend/content_editor/services/content_editor_spec.js
@@ -11,6 +11,7 @@ describe('content_editor/services/content_editor', () => {
let contentEditor;
let serializer;
let deserializer;
+ let languageLoader;
let eventHub;
let doc;
let p;
@@ -27,8 +28,15 @@ describe('content_editor/services/content_editor', () => {
serializer = { deserialize: jest.fn() };
deserializer = { deserialize: jest.fn() };
+ languageLoader = { loadLanguagesFromDOM: jest.fn() };
eventHub = eventHubFactory();
- contentEditor = new ContentEditor({ tiptapEditor, serializer, deserializer, eventHub });
+ contentEditor = new ContentEditor({
+ tiptapEditor,
+ serializer,
+ deserializer,
+ eventHub,
+ languageLoader,
+ });
});
describe('.dispose', () => {
@@ -43,10 +51,12 @@ describe('content_editor/services/content_editor', () => {
describe('when setSerializedContent succeeds', () => {
let document;
+ const dom = {};
+ const testMarkdown = '**bold text**';
beforeEach(() => {
document = doc(p('document'));
- deserializer.deserialize.mockResolvedValueOnce({ document });
+ deserializer.deserialize.mockResolvedValueOnce({ document, dom });
});
it('emits loadingContent and loadingSuccess event in the eventHub', () => {
@@ -59,14 +69,20 @@ describe('content_editor/services/content_editor', () => {
expect(loadingContentEmitted).toBe(true);
});
- contentEditor.setSerializedContent('**bold text**');
+ contentEditor.setSerializedContent(testMarkdown);
});
it('sets the deserialized document in the tiptap editor object', async () => {
- await contentEditor.setSerializedContent('**bold text**');
+ await contentEditor.setSerializedContent(testMarkdown);
expect(contentEditor.tiptapEditor.state.doc.toJSON()).toEqual(document.toJSON());
});
+
+ it('passes deserialized DOM document to language loader', async () => {
+ await contentEditor.setSerializedContent(testMarkdown);
+
+ expect(languageLoader.loadLanguagesFromDOM).toHaveBeenCalledWith(dom);
+ });
});
describe('when setSerializedContent fails', () => {
diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb
index 1eae854eca3..cb4eb43b88d 100644
--- a/spec/frontend/fixtures/merge_requests.rb
+++ b/spec/frontend/fixtures/merge_requests.rb
@@ -140,7 +140,7 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type:
query_name = 'ready_to_merge.query.graphql'
it "#{base_output_path}#{query_name}.json" do
- query = get_graphql_query_as_string("#{base_input_path}#{query_name}", ee: true)
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}", ee: Gitlab.ee?)
post_graphql(query, current_user: user, variables: { projectPath: project.full_path, iid: merge_request.iid.to_s })
diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js
index db56f77b60e..b8c1bef0ddd 100644
--- a/spec/frontend/security_configuration/components/training_provider_list_spec.js
+++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js
@@ -12,6 +12,7 @@ import {
TRACK_PROVIDER_LEARN_MORE_CLICK_ACTION,
TRACK_PROVIDER_LEARN_MORE_CLICK_LABEL,
} from '~/security_configuration/constants';
+import { TEMP_PROVIDER_URLS } from '~/security_configuration/components/constants';
import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
import { updateSecurityTrainingOptimisticResponse } from '~/security_configuration/graphql/cache_utils';
import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql';
@@ -145,55 +146,60 @@ describe('TrainingProviderList component', () => {
expect(findCards()).toHaveLength(TEST_TRAINING_PROVIDERS_DEFAULT.data.length);
});
- TEST_TRAINING_PROVIDERS_DEFAULT.data.forEach(
- ({ name, description, url, isEnabled }, index) => {
- it(`shows the name for card ${index}`, () => {
- expect(findCards().at(index).text()).toContain(name);
- });
+ TEST_TRAINING_PROVIDERS_DEFAULT.data.forEach(({ name, description, isEnabled }, index) => {
+ it(`shows the name for card ${index}`, () => {
+ expect(findCards().at(index).text()).toContain(name);
+ });
- it(`shows the description for card ${index}`, () => {
- expect(findCards().at(index).text()).toContain(description);
- });
+ it(`shows the description for card ${index}`, () => {
+ expect(findCards().at(index).text()).toContain(description);
+ });
+
+ it(`shows the learn more link for enabled card ${index}`, () => {
+ const learnMoreLink = findCards().at(index).find(GlLink);
+ const tempLogo = TEMP_PROVIDER_URLS[name];
- it(`shows the learn more link for card ${index}`, () => {
- expect(findLinks().at(index).attributes()).toEqual({
+ if (tempLogo) {
+ expect(learnMoreLink.attributes()).toEqual({
target: '_blank',
- href: url,
+ href: TEMP_PROVIDER_URLS[name],
});
- });
+ } else {
+ expect(learnMoreLink.exists()).toBe(false);
+ }
+ });
- it(`shows the toggle with the correct value for card ${index}`, () => {
- expect(findToggles().at(index).props('value')).toEqual(isEnabled);
- });
+ it(`shows the toggle with the correct value for card ${index}`, () => {
+ expect(findToggles().at(index).props('value')).toEqual(isEnabled);
+ });
- it(`shows a radio button to select the provider as primary within card ${index}`, () => {
- const primaryProviderRadioForCurrentCard = findPrimaryProviderRadios().at(index);
+ it(`shows a radio button to select the provider as primary within card ${index}`, () => {
+ const primaryProviderRadioForCurrentCard = findPrimaryProviderRadios().at(index);
- // if the given provider is not enabled it should not be possible select it as primary
- expect(primaryProviderRadioForCurrentCard.find('input').attributes('disabled')).toBe(
- isEnabled ? undefined : 'disabled',
- );
+ // if the given provider is not enabled it should not be possible select it as primary
+ expect(primaryProviderRadioForCurrentCard.find('input').attributes('disabled')).toBe(
+ isEnabled ? undefined : 'disabled',
+ );
- expect(primaryProviderRadioForCurrentCard.text()).toBe(
- TrainingProviderList.i18n.primaryTraining,
- );
- });
+ expect(primaryProviderRadioForCurrentCard.text()).toBe(
+ TrainingProviderList.i18n.primaryTraining,
+ );
+ });
- it('shows a info-tooltip that describes the purpose of a primary provider', () => {
- const infoIcon = findPrimaryProviderRadios().at(index).find(GlIcon);
- const tooltip = getBinding(infoIcon.element, 'gl-tooltip');
+ it('shows a info-tooltip that describes the purpose of a primary provider', () => {
+ const infoIcon = findPrimaryProviderRadios().at(index).find(GlIcon);
+ const tooltip = getBinding(infoIcon.element, 'gl-tooltip');
- expect(infoIcon.props()).toMatchObject({
- name: 'information-o',
- });
- expect(tooltip.value).toBe(TrainingProviderList.i18n.primaryTrainingDescription);
+ expect(infoIcon.props()).toMatchObject({
+ name: 'information-o',
});
+ expect(tooltip.value).toBe(TrainingProviderList.i18n.primaryTrainingDescription);
+ });
- it('does not show loader when query is populated', () => {
- expect(findLoader().exists()).toBe(false);
- });
- },
- );
+ it('does not show loader when query is populated', () => {
+ expect(findLoader().exists()).toBe(false);
+ });
+ });
});
describe('provider logo', () => {
diff --git a/spec/frontend/security_configuration/mock_data.js b/spec/frontend/security_configuration/mock_data.js
index 55f5c20e45d..18a480bf082 100644
--- a/spec/frontend/security_configuration/mock_data.js
+++ b/spec/frontend/security_configuration/mock_data.js
@@ -1,6 +1,6 @@
export const testProjectPath = 'foo/bar';
export const testProviderIds = [101, 102, 103];
-export const testProviderName = ['Vendor Name 1', 'Vendor Name 2', 'Vendor Name 3'];
+export const testProviderName = ['Kontra', 'Secure Code Warrior', 'Other Vendor'];
export const testTrainingUrls = [
'https://www.vendornameone.com/url',
'https://www.vendornametwo.com/url',
diff --git a/spec/graphql/resolvers/board_list_issues_resolver_spec.rb b/spec/graphql/resolvers/board_list_issues_resolver_spec.rb
index a6b536e1158..392385d2a30 100644
--- a/spec/graphql/resolvers/board_list_issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/board_list_issues_resolver_spec.rb
@@ -25,10 +25,10 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
let(:wildcard_started) { 'STARTED' }
let(:filters) { { milestone_title: ["started"], milestone_wildcard_id: wildcard_started } }
- it 'raises a mutually exclusive filter error when milestone wildcard and title are provided' do
- expect do
+ it 'generates a mutually exclusive filter error when milestone wildcard and title are provided' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError) do
resolve_board_list_issues(args: { filters: filters })
- end.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+ end
end
it 'returns the issues in the correct order' do
diff --git a/spec/graphql/resolvers/design_management/version_in_collection_resolver_spec.rb b/spec/graphql/resolvers/design_management/version_in_collection_resolver_spec.rb
index b0fc78af2af..8b9874c3580 100644
--- a/spec/graphql/resolvers/design_management/version_in_collection_resolver_spec.rb
+++ b/spec/graphql/resolvers/design_management/version_in_collection_resolver_spec.rb
@@ -26,8 +26,10 @@ RSpec.describe Resolvers::DesignManagement::VersionInCollectionResolver do
subject(:result) { resolve_version(issue.design_collection) }
context 'Neither id nor sha is passed as parameters' do
- it 'raises an appropriate error' do
- expect { result }.to raise_error(appropriate_error)
+ it 'generates an appropriate error' do
+ expect_graphql_error_to_be_created(appropriate_error) do
+ result
+ end
end
end
diff --git a/spec/graphql/resolvers/design_management/version_resolver_spec.rb b/spec/graphql/resolvers/design_management/version_resolver_spec.rb
index af1e6a73d09..ab1d7d4d9c5 100644
--- a/spec/graphql/resolvers/design_management/version_resolver_spec.rb
+++ b/spec/graphql/resolvers/design_management/version_resolver_spec.rb
@@ -22,8 +22,10 @@ RSpec.describe Resolvers::DesignManagement::VersionResolver do
context 'the current user is not authorized' do
let(:current_user) { create(:user) }
- it 'raises an error on resolution' do
- expect { resolve_version }.to raise_error(::Gitlab::Graphql::Errors::ResourceNotAvailable)
+ it 'generates an error on resolution' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do
+ resolve_version
+ end
end
end
diff --git a/spec/graphql/resolvers/design_management/versions_resolver_spec.rb b/spec/graphql/resolvers/design_management/versions_resolver_spec.rb
index 2c9c3a47650..d98138f6385 100644
--- a/spec/graphql/resolvers/design_management/versions_resolver_spec.rb
+++ b/spec/graphql/resolvers/design_management/versions_resolver_spec.rb
@@ -98,8 +98,10 @@ RSpec.describe Resolvers::DesignManagement::VersionsResolver do
}
end
- it 'raises a suitable error' do
- expect { result }.to raise_error(GraphQL::ExecutionError)
+ it 'generates a suitable error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do
+ result
+ end
end
end
end
diff --git a/spec/graphql/resolvers/group_issues_resolver_spec.rb b/spec/graphql/resolvers/group_issues_resolver_spec.rb
index e17429560ac..f5f6086cc09 100644
--- a/spec/graphql/resolvers/group_issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/group_issues_resolver_spec.rb
@@ -86,10 +86,10 @@ RSpec.describe Resolvers::GroupIssuesResolver do
end
context 'release_tag filter' do
- it 'returns an error when trying to filter by negated release_tag' do
- expect do
+ it 'generates an error when trying to filter by negated release_tag' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'releaseTag filter is not allowed when parent is a group.') do
resolve_issues(not: { release_tag: ['v1.0'] })
- end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'releaseTag filter is not allowed when parent is a group.')
+ end
end
end
end
diff --git a/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb b/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb
index 3fbd9bd2368..77f4ce4cac5 100644
--- a/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb
+++ b/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb
@@ -70,10 +70,10 @@ RSpec.describe Resolvers::IssueStatusCountsResolver do
end
context 'when both assignee_username and assignee_usernames are provided' do
- it 'raises a mutually exclusive filter error' do
- expect do
+ it 'generates a mutually exclusive filter error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.') do
resolve_issue_status_counts(assignee_usernames: [current_user.username], assignee_username: current_user.username)
- end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.')
+ end
end
end
diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb
index 326c105a358..5e9a3d0a68b 100644
--- a/spec/graphql/resolvers/issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/issues_resolver_spec.rb
@@ -78,10 +78,10 @@ RSpec.describe Resolvers::IssuesResolver do
expect(resolve_issues(milestone_wildcard_id: wildcard_none)).to contain_exactly(issue2)
end
- it 'raises a mutually exclusive filter error when wildcard and title are provided' do
- expect do
+ it 'generates a mutually exclusive filter error when wildcard and title are provided' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'only one of [milestoneTitle, milestoneWildcardId] arguments is allowed at the same time.') do
resolve_issues(milestone_title: ["started milestone"], milestone_wildcard_id: wildcard_started)
- end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'only one of [milestoneTitle, milestoneWildcardId] arguments is allowed at the same time.')
+ end
end
context 'negated filtering' do
@@ -97,10 +97,10 @@ RSpec.describe Resolvers::IssuesResolver do
expect(resolve_issues(not: { milestone_wildcard_id: wildcard_upcoming })).to contain_exactly(issue6)
end
- it 'raises a mutually exclusive filter error when wildcard and title are provided as negated filters' do
- expect do
+ it 'generates a mutually exclusive filter error when wildcard and title are provided as negated filters' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'only one of [milestoneTitle, milestoneWildcardId] arguments is allowed at the same time.') do
resolve_issues(not: { milestone_title: ["started milestone"], milestone_wildcard_id: wildcard_started })
- end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'only one of [milestoneTitle, milestoneWildcardId] arguments is allowed at the same time.')
+ end
end
end
end
@@ -122,10 +122,10 @@ RSpec.describe Resolvers::IssuesResolver do
end
context 'when release_tag_wildcard_id is also provided' do
- it 'raises a mutually eclusive argument error' do
- expect do
+ it 'generates a mutually eclusive argument error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'only one of [releaseTag, releaseTagWildcardId] arguments is allowed at the same time.') do
resolve_issues(release_tag: [release1.tag], release_tag_wildcard_id: 'ANY')
- end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'only one of [releaseTag, releaseTagWildcardId] arguments is allowed at the same time.')
+ end
end
end
end
@@ -191,10 +191,10 @@ RSpec.describe Resolvers::IssuesResolver do
end
context 'when both assignee_username and assignee_usernames are provided' do
- it 'raises a mutually exclusive filter error' do
- expect do
+ it 'generates a mutually exclusive filter error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.') do
resolve_issues(assignee_usernames: [assignee.username], assignee_username: assignee.username)
- end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.')
+ end
end
end
end
@@ -331,11 +331,12 @@ RSpec.describe Resolvers::IssuesResolver do
stub_feature_flags(disable_anonymous_search: true)
end
- it 'returns an error' do
+ it 'generates an error' do
error_message = "User must be authenticated to include the `search` argument."
- expect { resolve(described_class, obj: public_project, args: { search: 'test' }, ctx: { current_user: nil }) }
- .to raise_error(Gitlab::Graphql::Errors::ArgumentError, error_message)
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, error_message) do
+ resolve(described_class, obj: public_project, args: { search: 'test' }, ctx: { current_user: nil })
+ end
end
end
diff --git a/spec/graphql/resolvers/kas/agent_configurations_resolver_spec.rb b/spec/graphql/resolvers/kas/agent_configurations_resolver_spec.rb
index bdb1ced46ae..e4cf62b0361 100644
--- a/spec/graphql/resolvers/kas/agent_configurations_resolver_spec.rb
+++ b/spec/graphql/resolvers/kas/agent_configurations_resolver_spec.rb
@@ -34,8 +34,10 @@ RSpec.describe Resolvers::Kas::AgentConfigurationsResolver do
allow(kas_client).to receive(:list_agent_config_files).and_raise(GRPC::DeadlineExceeded)
end
- it 'raises a graphql error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable, 'GRPC::DeadlineExceeded')
+ it 'generates a graphql error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable, 'GRPC::DeadlineExceeded') do
+ subject
+ end
end
end
diff --git a/spec/graphql/resolvers/labels_resolver_spec.rb b/spec/graphql/resolvers/labels_resolver_spec.rb
index be6229553d7..efd2596b9eb 100644
--- a/spec/graphql/resolvers/labels_resolver_spec.rb
+++ b/spec/graphql/resolvers/labels_resolver_spec.rb
@@ -28,7 +28,9 @@ RSpec.describe Resolvers::LabelsResolver do
describe '#resolve' do
context 'with unauthorized user' do
it 'returns no labels' do
- expect { resolve_labels(project) }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do
+ resolve_labels(project)
+ end
end
end
diff --git a/spec/graphql/resolvers/package_pipelines_resolver_spec.rb b/spec/graphql/resolvers/package_pipelines_resolver_spec.rb
index 892dc641201..c757c876616 100644
--- a/spec/graphql/resolvers/package_pipelines_resolver_spec.rb
+++ b/spec/graphql/resolvers/package_pipelines_resolver_spec.rb
@@ -25,32 +25,40 @@ RSpec.describe Resolvers::PackagePipelinesResolver do
context 'with invalid after' do
let(:args) { { first: 1, after: 'not_json_string' } }
- it 'raises argument error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+ it 'generates an argument error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError) do
+ subject
+ end
end
end
context 'with invalid after key' do
let(:args) { { first: 1, after: encode_cursor(foo: 3) } }
- it 'raises argument error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+ it 'generates an argument error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError) do
+ subject
+ end
end
end
context 'with invalid before' do
let(:args) { { last: 1, before: 'not_json_string' } }
- it 'raises argument error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+ it 'generates an argument error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError) do
+ subject
+ end
end
end
context 'with invalid before key' do
let(:args) { { last: 1, before: encode_cursor(foo: 3) } }
- it 'raises argument error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+ it 'generates an argument error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError) do
+ subject
+ end
end
end
diff --git a/spec/graphql/resolvers/paginated_tree_resolver_spec.rb b/spec/graphql/resolvers/paginated_tree_resolver_spec.rb
index 82b05937aa3..4b05e9076d7 100644
--- a/spec/graphql/resolvers/paginated_tree_resolver_spec.rb
+++ b/spec/graphql/resolvers/paginated_tree_resolver_spec.rb
@@ -65,7 +65,11 @@ RSpec.describe Resolvers::PaginatedTreeResolver do
context 'when cursor is invalid' do
let(:args) { super().merge(after: 'invalid') }
- it { expect { subject }.to raise_error(Gitlab::Graphql::Errors::ArgumentError) }
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError) do
+ subject
+ end
+ end
end
it 'returns all tree entries during cursor pagination' do
diff --git a/spec/graphql/resolvers/project_milestones_resolver_spec.rb b/spec/graphql/resolvers/project_milestones_resolver_spec.rb
index e168291c804..2cf490c2b6a 100644
--- a/spec/graphql/resolvers/project_milestones_resolver_spec.rb
+++ b/spec/graphql/resolvers/project_milestones_resolver_spec.rb
@@ -103,27 +103,27 @@ RSpec.describe Resolvers::ProjectMilestonesResolver do
end
context 'when start date is after end_date' do
- it 'raises error' do
- expect do
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'startDate is after endDate') do
resolve_project_milestones(start_date: Time.now, end_date: Time.now - 2.days)
- end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, "startDate is after endDate")
+ end
end
end
end
context 'when only start_date is present' do
- it 'raises error' do
- expect do
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, /Both startDate and endDate/) do
resolve_project_milestones(start_date: Time.now)
- end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /Both startDate and endDate/)
+ end
end
end
context 'when only end_date is present' do
- it 'raises error' do
- expect do
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, /Both startDate and endDate/) do
resolve_project_milestones(end_date: Time.now)
- end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /Both startDate and endDate/)
+ end
end
end
@@ -174,12 +174,12 @@ RSpec.describe Resolvers::ProjectMilestonesResolver do
end
context 'when user cannot read milestones' do
- it 'raises error' do
+ it 'generates an error' do
unauthorized_user = create(:user)
- expect do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do
resolve_project_milestones({}, { current_user: unauthorized_user })
- end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
end
end
end
diff --git a/spec/graphql/resolvers/project_pipeline_resolver_spec.rb b/spec/graphql/resolvers/project_pipeline_resolver_spec.rb
index 6a8aa39f3b2..398f8f52269 100644
--- a/spec/graphql/resolvers/project_pipeline_resolver_spec.rb
+++ b/spec/graphql/resolvers/project_pipeline_resolver_spec.rb
@@ -85,13 +85,15 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
end
it 'errors when no iid or sha is passed' do
- expect { resolve_pipeline(project, {}) }
- .to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError) do
+ resolve_pipeline(project, {})
+ end
end
it 'errors when both iid and sha are passed' do
- expect { resolve_pipeline(project, { iid: '1234', sha: 'sha' }) }
- .to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError) do
+ resolve_pipeline(project, { iid: '1234', sha: 'sha' })
+ end
end
context 'when the pipeline is a dangling pipeline' do
diff --git a/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb b/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb
index c6d8c518fb7..b95bab41e3e 100644
--- a/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb
+++ b/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb
@@ -14,10 +14,10 @@ RSpec.describe Resolvers::Projects::JiraProjectsResolver do
let_it_be(:project) { create(:project) }
shared_examples 'no project service access' do
- it 'raises error' do
- expect do
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do
resolve_jira_projects
- end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
end
end
@@ -89,11 +89,14 @@ RSpec.describe Resolvers::Projects::JiraProjectsResolver do
.to_raise(JIRA::HTTPError.new(double(message: '{"errorMessages":["Some failure"]}')))
end
- it 'raises failure error' do
+ it 'generates a failure error' do
config_docs_link_url = Rails.application.routes.url_helpers.help_page_path('integration/jira/configure')
docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: config_docs_link_url }
error_message = 'An error occurred while requesting data from Jira: Some failure. Check your %{docs_link_start}Jira integration configuration</a> and try again.' % { docs_link_start: docs_link_start }
- expect { resolve_jira_projects }.to raise_error(error_message)
+
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::BaseError, error_message) do
+ resolve_jira_projects
+ end
end
end
end
diff --git a/spec/graphql/resolvers/timelog_resolver_spec.rb b/spec/graphql/resolvers/timelog_resolver_spec.rb
index 82ed572c3ee..84fa2932829 100644
--- a/spec/graphql/resolvers/timelog_resolver_spec.rb
+++ b/spec/graphql/resolvers/timelog_resolver_spec.rb
@@ -85,27 +85,30 @@ RSpec.describe Resolvers::TimelogResolver do
context 'when start_time and start_date are present' do
let(:args) { { start_time: 6.days.ago, start_date: 6.days.ago } }
- it 'returns correct error' do
- expect { timelogs }
- .to raise_error(error_class, /Provide either a start date or time, but not both/)
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(error_class, /Provide either a start date or time, but not both/) do
+ timelogs
+ end
end
end
context 'when end_time and end_date are present' do
let(:args) { { end_time: 2.days.ago, end_date: 2.days.ago } }
- it 'returns correct error' do
- expect { timelogs }
- .to raise_error(error_class, /Provide either an end date or time, but not both/)
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(error_class, /Provide either an end date or time, but not both/) do
+ timelogs
+ end
end
end
context 'when start argument is after end argument' do
let(:args) { { start_time: 2.days.ago, end_time: 6.days.ago } }
- it 'returns correct error' do
- expect { timelogs }
- .to raise_error(error_class, /Start argument must be before End argument/)
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(error_class, /Start argument must be before End argument/) do
+ timelogs
+ end
end
end
end
@@ -276,9 +279,10 @@ RSpec.describe Resolvers::TimelogResolver do
let(:args) { {} }
let(:extra_args) { {} }
- it 'returns correct error' do
- expect { timelogs }
- .to raise_error(error_class, /Provide at least one argument/)
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(error_class, /Provide at least one argument/) do
+ timelogs
+ end
end
end
diff --git a/spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects_spec.rb b/spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects_spec.rb
new file mode 100644
index 00000000000..6aea549b136
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::ResetDuplicateCiRunnersTokenEncryptedValuesOnProjects do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+
+ let(:perform) { described_class.new.perform(1, 4) }
+
+ before do
+ namespaces.create!(id: 123, name: 'sample', path: 'sample')
+
+ projects.create!(id: 1, namespace_id: 123, runners_token_encrypted: 'duplicate')
+ projects.create!(id: 2, namespace_id: 123, runners_token_encrypted: 'a-runners-token')
+ projects.create!(id: 3, namespace_id: 123, runners_token_encrypted: 'duplicate')
+ projects.create!(id: 4, namespace_id: 123, runners_token_encrypted: nil)
+ projects.create!(id: 5, namespace_id: 123, runners_token_encrypted: 'duplicate-2')
+ projects.create!(id: 6, namespace_id: 123, runners_token_encrypted: 'duplicate-2')
+ end
+
+ describe '#up' do
+ before do
+ stub_const("#{described_class}::SUB_BATCH_SIZE", 2)
+ end
+
+ it 'nullifies duplicate tokens', :aggregate_failures do
+ perform
+
+ expect(projects.count).to eq(6)
+ expect(projects.all.pluck(:id, :runners_token_encrypted).to_h).to eq(
+ { 1 => nil, 2 => 'a-runners-token', 3 => nil, 4 => nil, 5 => 'duplicate-2', 6 => 'duplicate-2' }
+ )
+ expect(projects.pluck(:runners_token_encrypted).uniq).to match_array [nil, 'a-runners-token', 'duplicate-2']
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects_spec.rb b/spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects_spec.rb
new file mode 100644
index 00000000000..cbe762c2680
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::ResetDuplicateCiRunnersTokenValuesOnProjects do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+
+ let(:perform) { described_class.new.perform(1, 4) }
+
+ before do
+ namespaces.create!(id: 123, name: 'sample', path: 'sample')
+
+ projects.create!(id: 1, namespace_id: 123, runners_token: 'duplicate')
+ projects.create!(id: 2, namespace_id: 123, runners_token: 'a-runners-token')
+ projects.create!(id: 3, namespace_id: 123, runners_token: 'duplicate')
+ projects.create!(id: 4, namespace_id: 123, runners_token: nil)
+ projects.create!(id: 5, namespace_id: 123, runners_token: 'duplicate-2')
+ projects.create!(id: 6, namespace_id: 123, runners_token: 'duplicate-2')
+ end
+
+ describe '#up' do
+ before do
+ stub_const("#{described_class}::SUB_BATCH_SIZE", 2)
+ end
+
+ it 'nullifies duplicate tokens', :aggregate_failures do
+ perform
+
+ expect(projects.count).to eq(6)
+ expect(projects.all.pluck(:id, :runners_token).to_h).to eq(
+ { 1 => nil, 2 => 'a-runners-token', 3 => nil, 4 => nil, 5 => 'duplicate-2', 6 => 'duplicate-2' }
+ )
+ expect(projects.pluck(:runners_token).uniq).to match_array [nil, 'a-runners-token', 'duplicate-2']
+ end
+ end
+end
diff --git a/spec/models/users/credit_card_validation_spec.rb b/spec/models/users/credit_card_validation_spec.rb
index 43edf7ed093..34cfd500c26 100644
--- a/spec/models/users/credit_card_validation_spec.rb
+++ b/spec/models/users/credit_card_validation_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Users::CreditCardValidation do
it { is_expected.to belong_to(:user) }
- it { is_expected.to validate_length_of(:holder_name).is_at_most(26) }
+ it { is_expected.to validate_length_of(:holder_name).is_at_most(50) }
it { is_expected.to validate_length_of(:network).is_at_most(32) }
it { is_expected.to validate_numericality_of(:last_digits).is_less_than_or_equal_to(9999) }
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index f42fc7aabc2..1d199a72d1d 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -783,6 +783,13 @@ RSpec.describe API::Repositories do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['notes']).to be_present
end
+
+ context 'when previous tag version does not exist' do
+ it_behaves_like '422 response' do
+ let(:request) { get api("/projects/#{project.id}/repository/changelog", user), params: { version: 'v0.0.0' } }
+ let(:message) { 'Failed to generate the changelog: The commit start range is unspecified, and no previous tag could be found to use instead' }
+ end
+ end
end
describe 'POST /projects/:id/repository/changelog' do
diff --git a/spec/support/shared_examples/graphql/members_shared_examples.rb b/spec/support/shared_examples/graphql/members_shared_examples.rb
index b0bdd27a95f..8e9e22f4359 100644
--- a/spec/support/shared_examples/graphql/members_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/members_shared_examples.rb
@@ -76,8 +76,10 @@ RSpec.shared_examples 'querying members with a group' do
resolve(described_class, obj: resource, args: base_args.merge(args), ctx: { current_user: other_user })
end
- it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do
+ subject
+ end
end
end
end