diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-05 15:08:47 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-05 15:08:47 +0000 |
commit | dad16033c2b7cfd54ffe20ca5cc1d844e9e41be6 (patch) | |
tree | 8010601f9b7066e07166d997624b723ea4c3f816 | |
parent | 3c86701bc89302550abb9bbaa060132fdcd52480 (diff) | |
download | gitlab-ce-dad16033c2b7cfd54ffe20ca5cc1d844e9e41be6.tar.gz |
Add latest changes from gitlab-org/gitlab@master
173 files changed, 1758 insertions, 1676 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index 3e57949eeda..1a4d182c3d1 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -118,14 +118,10 @@ Dangerfile @gl-quality/eng-prod /spec/models/clusters/applications/cilium_spec.rb @gitlab-org/protect/container-security-backend /ee/app/controllers/projects/security/network_policies_controller.rb @gitlab-org/protect/container-security-backend /ee/spec/controllers/projects/security/network_policies_controller_spec.rb @gitlab-org/protect/container-security-backend -/ee/app/workers/network_policy_metrics_worker.rb @gitlab-org/protect/container-security-backend -/ee/spec/workers/network_policy_metrics_worker_spec.rb @gitlab-org/protect/container-security-backend /ee/app/services/network_policies/** @gitlab-org/protect/container-security-backend /ee/spec/services/network_policies/** @gitlab-org/protect/container-security-backend /ee/app/services/security/orchestration/** @gitlab-org/protect/container-security-backend /ee/spec/services/security/orchestration/** @gitlab-org/protect/container-security-backend -/ee/lib/gitlab/usage_data_counters/network_policy_counter.rb @gitlab-org/protect/container-security-backend -/ee/spec/lib/gitlab/usage_data_counters/network_policy_counter_spec.rb @gitlab-org/protect/container-security-backend ^[Code Owners] /ee/lib/gitlab/code_owners.rb @reprazent @kerrizor @garyh diff --git a/.rubocop_todo/layout/hash_alignment.yml b/.rubocop_todo/layout/hash_alignment.yml index aafacd5f265..b13a561958e 100644 --- a/.rubocop_todo/layout/hash_alignment.yml +++ b/.rubocop_todo/layout/hash_alignment.yml @@ -383,7 +383,6 @@ Layout/HashAlignment: - 'ee/lib/gitlab/elastic/helper.rb' - 'ee/lib/gitlab/elastic/indexer.rb' - 'ee/lib/gitlab/geo/replication/base_transfer.rb' - - 'ee/lib/gitlab/prometheus/queries/packet_flow_query.rb' - 'ee/spec/controllers/ee/projects/variables_controller_spec.rb' - 'ee/spec/controllers/groups/epic_boards_controller_spec.rb' - 'ee/spec/controllers/groups/issues_controller_spec.rb' diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index d46c7a6da62..f80c3f20437 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -4424,7 +4424,6 @@ Layout/LineLength: - 'spec/features/projects/files/user_browses_files_spec.rb' - 'spec/features/projects/files/user_edits_files_spec.rb' - 'spec/features/projects/infrastructure_registry_spec.rb' - - 'spec/features/projects/integrations/prometheus_external_alerts_spec.rb' - 'spec/features/projects/integrations/user_activates_issue_tracker_spec.rb' - 'spec/features/projects/integrations/user_activates_jira_spec.rb' - 'spec/features/projects/integrations/user_uses_inherited_settings_spec.rb' diff --git a/.rubocop_todo/rails/time_zone.yml b/.rubocop_todo/rails/time_zone.yml index 86d0632ac47..ff97dfeb444 100644 --- a/.rubocop_todo/rails/time_zone.yml +++ b/.rubocop_todo/rails/time_zone.yml @@ -13,7 +13,6 @@ Rails/TimeZone: - ee/lib/gitlab/geo/log_cursor/logger.rb - ee/lib/gitlab/geo/oauth/login_state.rb - ee/lib/gitlab/prometheus/queries/cluster_query.rb - - ee/lib/gitlab/prometheus/queries/packet_flow_query.rb - ee/spec/lib/ee/gitlab/checks/push_rules/commit_check_spec.rb - ee/spec/lib/ee/gitlab/ci/pipeline/quota/job_activity_spec.rb - ee/spec/lib/gitlab/analytics/cycle_analytics/data_collector_spec.rb @@ -44,7 +43,6 @@ Rails/TimeZone: - ee/spec/lib/gitlab/git_access_spec.rb - ee/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb - ee/spec/lib/gitlab/prometheus/queries/cluster_query_spec.rb - - ee/spec/lib/gitlab/prometheus/queries/packet_flow_query_spec.rb - lib/api/helpers.rb - lib/api/sidekiq_metrics.rb - lib/backup/manager.rb diff --git a/.rubocop_todo/rspec/verified_doubles.yml b/.rubocop_todo/rspec/verified_doubles.yml index e272fbc555c..70fb8414f55 100644 --- a/.rubocop_todo/rspec/verified_doubles.yml +++ b/.rubocop_todo/rspec/verified_doubles.yml @@ -90,8 +90,6 @@ RSpec/VerifiedDoubles: - ee/spec/lib/gitlab/middleware/ip_restrictor_spec.rb - ee/spec/lib/gitlab/patch/legacy_database_config_spec.rb - ee/spec/lib/gitlab/prometheus/queries/cluster_query_spec.rb - - ee/spec/lib/gitlab/prometheus/queries/packet_flow_metrics_query_spec.rb - - ee/spec/lib/gitlab/prometheus/queries/packet_flow_query_spec.rb - ee/spec/lib/gitlab/subscription_portal/clients/rest_spec.rb - ee/spec/lib/sidebars/groups/menus/analytics_menu_spec.rb - ee/spec/lib/system_check/app/elasticsearch_check_spec.rb diff --git a/.rubocop_todo/style/percent_literal_delimiters.yml b/.rubocop_todo/style/percent_literal_delimiters.yml index 4673f0d71d2..bf50c4c1922 100644 --- a/.rubocop_todo/style/percent_literal_delimiters.yml +++ b/.rubocop_todo/style/percent_literal_delimiters.yml @@ -324,7 +324,6 @@ Style/PercentLiteralDelimiters: - 'ee/lib/gitlab/ci/parsers/security/formatters/dast.rb' - 'ee/lib/gitlab/geo.rb' - 'ee/lib/gitlab/geo/replicator.rb' - - 'ee/lib/gitlab/prometheus/queries/packet_flow_query.rb' - 'ee/lib/gitlab/usage/metrics/instrumentations/license_metric.rb' - 'ee/lib/tasks/gitlab/elastic/test.rake' - 'ee/spec/config/metrics/every_metric_definition_spec.rb' diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/formatting.vue b/app/assets/javascripts/content_editor/components/bubble_menus/formatting.vue index 46c15de6b2c..e35fbf14de5 100644 --- a/app/assets/javascripts/content_editor/components/bubble_menus/formatting.vue +++ b/app/assets/javascripts/content_editor/components/bubble_menus/formatting.vue @@ -3,6 +3,9 @@ import { GlButtonGroup } from '@gitlab/ui'; import { BubbleMenu } from '@tiptap/vue-2'; import { BUBBLE_MENU_TRACKING_ACTION } from '../../constants'; import trackUIControl from '../../services/track_ui_control'; +import Image from '../../extensions/image'; +import Audio from '../../extensions/audio'; +import Video from '../../extensions/video'; import Code from '../../extensions/code'; import CodeBlockHighlight from '../../extensions/code_block_highlight'; import Diagram from '../../extensions/diagram'; @@ -24,7 +27,15 @@ export default { shouldShow: ({ editor, from, to }) => { if (from === to) return false; - const exclude = [Code.name, CodeBlockHighlight.name, Diagram.name, Frontmatter.name]; + const exclude = [ + Code.name, + CodeBlockHighlight.name, + Diagram.name, + Frontmatter.name, + Image.name, + Audio.name, + Video.name, + ]; return !exclude.some((type) => editor.isActive(type)); }, diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/link.vue b/app/assets/javascripts/content_editor/components/bubble_menus/link.vue index 2f446832516..abd225c0b1a 100644 --- a/app/assets/javascripts/content_editor/components/bubble_menus/link.vue +++ b/app/assets/javascripts/content_editor/components/bubble_menus/link.vue @@ -26,7 +26,7 @@ export default { directives: { GlTooltip, }, - inject: ['tiptapEditor'], + inject: ['tiptapEditor', 'contentEditor'], data() { return { linkHref: undefined, @@ -57,9 +57,11 @@ export default { this.isEditing = true; }, - endEditingLink() { + async endEditingLink() { this.isEditing = false; + this.linkHref = await this.contentEditor.resolveLink(this.linkCanonicalSrc); + if (!this.linkCanonicalSrc && !this.linkHref) { this.removeLink(); } @@ -70,7 +72,7 @@ export default { this.updateLinkToState(); }, - saveEditedLink() { + async saveEditedLink() { if (!this.linkCanonicalSrc) { this.removeLink(); } else { @@ -166,12 +168,12 @@ export default { @click="removeLink" /> </gl-button-group> - <gl-form v-else class="bubble-menu-form gl-p-4 gl-w-100" @submit="saveEditedLink"> - <gl-form-group data-testid="link-href-group" :label="__('URL')" label-for="link-href"> - <gl-form-input id="link-href" v-model="linkCanonicalSrc" /> + <gl-form v-else class="bubble-menu-form gl-p-4 gl-w-100" @submit.prevent="saveEditedLink"> + <gl-form-group :label="__('URL')" label-for="link-href"> + <gl-form-input id="link-href" v-model="linkCanonicalSrc" data-testid="link-href" /> </gl-form-group> - <gl-form-group data-testid="link-title-group" :label="__('Title')" label-for="link-title"> - <gl-form-input id="link-title" v-model="linkTitle" /> + <gl-form-group :label="__('Title')" label-for="link-title"> + <gl-form-input id="link-title" v-model="linkTitle" data-testid="link-title" /> </gl-form-group> <div class="gl-display-flex gl-justify-content-end"> <gl-button class="gl-mr-3" data-testid="cancel-link" @click="cancelEditingLink"> diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/media.vue b/app/assets/javascripts/content_editor/components/bubble_menus/media.vue new file mode 100644 index 00000000000..d1bc5c83948 --- /dev/null +++ b/app/assets/javascripts/content_editor/components/bubble_menus/media.vue @@ -0,0 +1,288 @@ +<script> +import { + GlLink, + GlForm, + GlFormGroup, + GlFormInput, + GlLoadingIcon, + GlButton, + GlButtonGroup, + GlTooltipDirective as GlTooltip, +} from '@gitlab/ui'; +import { BubbleMenu } from '@tiptap/vue-2'; +import { __ } from '~/locale'; +import Audio from '../../extensions/audio'; +import Image from '../../extensions/image'; +import Video from '../../extensions/video'; +import EditorStateObserver from '../editor_state_observer.vue'; +import { acceptedMimes } from '../../services/upload_helpers'; + +const MEDIA_TYPES = [Audio.name, Image.name, Video.name]; + +export default { + i18n: { + copySourceLabels: { + [Audio.name]: __('Copy audio URL'), + [Image.name]: __('Copy image URL'), + [Video.name]: __('Copy video URL'), + }, + editLabels: { + [Audio.name]: __('Edit audio description'), + [Image.name]: __('Edit image description'), + [Video.name]: __('Edit video description'), + }, + replaceLabels: { + [Audio.name]: __('Replace audio'), + [Image.name]: __('Replace image'), + [Video.name]: __('Replace video'), + }, + deleteLabels: { + [Audio.name]: __('Delete audio'), + [Image.name]: __('Delete image'), + [Video.name]: __('Delete video'), + }, + }, + components: { + BubbleMenu, + GlForm, + GlFormGroup, + GlFormInput, + GlLink, + GlLoadingIcon, + GlButton, + GlButtonGroup, + EditorStateObserver, + }, + directives: { + GlTooltip, + }, + inject: ['tiptapEditor', 'contentEditor'], + data() { + return { + mediaType: undefined, + mediaSrc: undefined, + mediaCanonicalSrc: undefined, + mediaAlt: undefined, + mediaTitle: undefined, + + isEditing: false, + isUpdating: false, + isUploading: false, + }; + }, + computed: { + copySourceLabel() { + return this.$options.i18n.copySourceLabels[this.mediaType]; + }, + editLabel() { + return this.$options.i18n.editLabels[this.mediaType]; + }, + replaceLabel() { + return this.$options.i18n.replaceLabels[this.mediaType]; + }, + deleteLabel() { + return this.$options.i18n.deleteLabels[this.mediaType]; + }, + showProgressIndicator() { + return this.isUploading || this.isUpdating; + }, + }, + methods: { + shouldShow() { + const shouldShow = MEDIA_TYPES.some((type) => this.tiptapEditor.isActive(type)); + + if (!shouldShow) this.isEditing = false; + + return shouldShow; + }, + + startEditingMedia() { + this.isEditing = true; + }, + + endEditingMedia() { + this.isEditing = false; + + this.updateMediaInfoToState(); + }, + + cancelEditingMedia() { + this.endEditingMedia(); + this.updateMediaInfoToState(); + }, + + async saveEditedMedia() { + this.isUpdating = true; + + this.mediaSrc = await this.contentEditor.resolveLink(this.mediaCanonicalSrc); + + const position = this.tiptapEditor.state.selection.from; + + this.tiptapEditor + .chain() + .focus() + .updateAttributes(this.mediaType, { + src: this.mediaSrc, + alt: this.mediaAlt, + canonicalSrc: this.mediaCanonicalSrc, + title: this.mediaTitle, + }) + .run(); + + this.tiptapEditor.commands.setNodeSelection(position); + + this.endEditingMedia(); + + this.isUpdating = false; + }, + + async updateMediaInfoToState() { + this.mediaType = MEDIA_TYPES.find((type) => this.tiptapEditor.isActive(type)); + + if (!this.mediaType) return; + + this.isUpdating = true; + + const { src, title, alt, canonicalSrc, uploading } = this.tiptapEditor.getAttributes( + this.mediaType, + ); + + this.mediaTitle = title; + this.mediaAlt = alt; + this.mediaCanonicalSrc = canonicalSrc || src; + this.isUploading = uploading; + this.mediaSrc = await this.contentEditor.resolveLink(this.mediaCanonicalSrc); + + this.isUpdating = false; + }, + + replaceMedia() { + this.$refs.fileSelector.click(); + }, + + onFileSelect(e) { + this.tiptapEditor + .chain() + .focus() + .deleteSelection() + .uploadAttachment({ + file: e.target.files[0], + }) + .run(); + + this.$refs.fileSelector.value = ''; + }, + + copyMediaSrc() { + navigator.clipboard.writeText(this.mediaCanonicalSrc); + }, + + deleteMedia() { + this.tiptapEditor.chain().focus().deleteSelection().run(); + }, + }, + + acceptedMimes, +}; +</script> +<template> + <bubble-menu + data-testid="media-bubble-menu" + class="gl-shadow gl-rounded-base gl-bg-white" + :editor="tiptapEditor" + plugin-key="bubbleMenuMedia" + :should-show="() => shouldShow()" + > + <editor-state-observer @transaction="updateMediaInfoToState"> + <gl-button-group v-if="!isEditing" class="gl-display-flex gl-align-items-center"> + <gl-loading-icon v-if="showProgressIndicator" class="gl-pl-4 gl-pr-3" /> + <input + ref="fileSelector" + type="file" + name="content_editor_image" + :accept="$options.acceptedMimes[mediaType]" + class="gl-display-none" + data-qa-selector="file_upload_field" + @change="onFileSelect" + /> + + <gl-link + v-if="!showProgressIndicator" + v-gl-tooltip + :href="mediaSrc" + :aria-label="mediaCanonicalSrc" + :title="mediaCanonicalSrc" + target="_blank" + class="gl-px-3 gl-overflow-hidden gl-white-space-nowrap gl-text-overflow-ellipsis" + > + {{ mediaCanonicalSrc }} + </gl-link> + <gl-button + v-gl-tooltip + variant="default" + category="tertiary" + size="medium" + data-testid="copy-media-src" + :aria-label="copySourceLabel" + :title="copySourceLabel" + icon="copy-to-clipboard" + @click="copyMediaSrc" + /> + <gl-button + v-if="!showProgressIndicator" + v-gl-tooltip + variant="default" + category="tertiary" + size="medium" + data-testid="edit-media" + :aria-label="editLabel" + :title="editLabel" + icon="pencil" + @click="startEditingMedia" + /> + <gl-button + v-gl-tooltip + variant="default" + category="tertiary" + size="medium" + data-testid="replace-media" + :aria-label="replaceLabel" + :title="replaceLabel" + icon="upload" + @click="replaceMedia" + /> + <gl-button + v-gl-tooltip + variant="default" + category="tertiary" + size="medium" + data-testid="delete-media" + :aria-label="deleteLabel" + :title="deleteLabel" + icon="remove" + @click="deleteMedia" + /> + </gl-button-group> + <gl-form v-else class="bubble-menu-form gl-p-4 gl-w-100" @submit.prevent="saveEditedMedia"> + <gl-form-group :label="__('URL')" label-for="media-src"> + <gl-form-input id="media-src" v-model="mediaCanonicalSrc" data-testid="media-src" /> + </gl-form-group> + <gl-form-group :label="__('Description (alt text)')" label-for="media-alt"> + <gl-form-input id="media-alt" v-model="mediaAlt" data-testid="media-alt" /> + </gl-form-group> + <gl-form-group :label="__('Title')" label-for="media-title"> + <gl-form-input id="media-title" v-model="mediaTitle" data-testid="media-title" /> + </gl-form-group> + <div class="gl-display-flex gl-justify-content-end"> + <gl-button + class="gl-mr-3" + data-testid="cancel-editing-media" + @click="cancelEditingMedia" + >{{ __('Cancel') }}</gl-button + > + <gl-button variant="confirm" type="submit">{{ __('Apply') }}</gl-button> + </div> + </gl-form> + </editor-state-observer> + </bubble-menu> +</template> diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue index a3247298b19..74ae37b6d06 100644 --- a/app/assets/javascripts/content_editor/components/content_editor.vue +++ b/app/assets/javascripts/content_editor/components/content_editor.vue @@ -7,6 +7,7 @@ import EditorStateObserver from './editor_state_observer.vue'; import FormattingBubbleMenu from './bubble_menus/formatting.vue'; import CodeBlockBubbleMenu from './bubble_menus/code_block.vue'; import LinkBubbleMenu from './bubble_menus/link.vue'; +import MediaBubbleMenu from './bubble_menus/media.vue'; import TopToolbar from './top_toolbar.vue'; import LoadingIndicator from './loading_indicator.vue'; @@ -20,6 +21,7 @@ export default { FormattingBubbleMenu, CodeBlockBubbleMenu, LinkBubbleMenu, + MediaBubbleMenu, EditorStateObserver, }, props: { @@ -95,6 +97,7 @@ export default { <formatting-bubble-menu /> <code-block-bubble-menu /> <link-bubble-menu /> + <media-bubble-menu /> <tiptap-editor-content class="md" :editor="contentEditor.tiptapEditor" /> <loading-indicator /> </div> diff --git a/app/assets/javascripts/content_editor/components/wrappers/media.vue b/app/assets/javascripts/content_editor/components/wrappers/media.vue deleted file mode 100644 index 37119bdd066..00000000000 --- a/app/assets/javascripts/content_editor/components/wrappers/media.vue +++ /dev/null @@ -1,51 +0,0 @@ -<script> -import { GlLoadingIcon } from '@gitlab/ui'; -import { NodeViewWrapper } from '@tiptap/vue-2'; - -const tagNameMap = { - image: 'img', - video: 'video', - audio: 'audio', -}; - -export default { - name: 'MediaWrapper', - components: { - NodeViewWrapper, - GlLoadingIcon, - }, - props: { - node: { - type: Object, - required: true, - }, - }, - computed: { - tagName() { - return tagNameMap[this.node.type.name] || 'img'; - }, - }, -}; -</script> -<template> - <node-view-wrapper class="gl-display-inline-block"> - <span class="gl-relative" :class="{ [`media-container ${tagName}-container`]: true }"> - <gl-loading-icon v-if="node.attrs.uploading" class="gl-absolute gl-left-50p gl-top-half" /> - <component - :is="tagName" - data-testid="media" - :class="{ - 'gl-max-w-full gl-h-auto': tagName !== 'audio', - 'gl-opacity-5': node.attrs.uploading, - }" - :title="node.attrs.title || node.attrs.alt" - :alt="node.attrs.alt" - :src="node.attrs.src" - controls="true" - /> - <a v-if="tagName !== 'img'" :href="node.attrs.canonicalSrc || node.attrs.src" @click.prevent> - {{ node.attrs.title || node.attrs.alt }} - </a> - </span> - </node-view-wrapper> -</template> diff --git a/app/assets/javascripts/content_editor/extensions/image.js b/app/assets/javascripts/content_editor/extensions/image.js index 311db8151cb..25f976f524f 100644 --- a/app/assets/javascripts/content_editor/extensions/image.js +++ b/app/assets/javascripts/content_editor/extensions/image.js @@ -1,6 +1,4 @@ import { Image } from '@tiptap/extension-image'; -import { VueNodeViewRenderer } from '@tiptap/vue-2'; -import MediaWrapper from '../components/wrappers/media.vue'; import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants'; const resolveImageEl = (element) => @@ -77,7 +75,4 @@ export default Image.extend({ }, ]; }, - addNodeView() { - return VueNodeViewRenderer(MediaWrapper); - }, }); diff --git a/app/assets/javascripts/content_editor/extensions/playable.js b/app/assets/javascripts/content_editor/extensions/playable.js index 2c5269377c5..ed343d8acf8 100644 --- a/app/assets/javascripts/content_editor/extensions/playable.js +++ b/app/assets/javascripts/content_editor/extensions/playable.js @@ -1,8 +1,6 @@ /* eslint-disable @gitlab/require-i18n-strings */ import { Node } from '@tiptap/core'; -import { VueNodeViewRenderer } from '@tiptap/vue-2'; -import MediaWrapper from '../components/wrappers/media.vue'; const queryPlayableElement = (element, mediaType) => element.querySelector(mediaType); @@ -68,8 +66,4 @@ export default Node.create({ ['a', { href: node.attrs.src }, node.attrs.title || node.attrs.alt || ''], ]; }, - - addNodeView() { - return VueNodeViewRenderer(MediaWrapper); - }, }); diff --git a/app/assets/javascripts/content_editor/services/asset_resolver.js b/app/assets/javascripts/content_editor/services/asset_resolver.js new file mode 100644 index 00000000000..942457b9664 --- /dev/null +++ b/app/assets/javascripts/content_editor/services/asset_resolver.js @@ -0,0 +1,13 @@ +import { memoize } from 'lodash'; + +export default ({ renderMarkdown }) => ({ + resolveUrl: memoize(async (canonicalSrc) => { + const html = await renderMarkdown(`[link](${canonicalSrc})`); + if (!html) return canonicalSrc; + + const parser = new DOMParser(); + const { body } = parser.parseFromString(html, 'text/html'); + + return body.querySelector('a').getAttribute('href'); + }), +}); diff --git a/app/assets/javascripts/content_editor/services/content_editor.js b/app/assets/javascripts/content_editor/services/content_editor.js index 21843c482a8..b993851a92f 100644 --- a/app/assets/javascripts/content_editor/services/content_editor.js +++ b/app/assets/javascripts/content_editor/services/content_editor.js @@ -3,12 +3,13 @@ import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } fro /* eslint-disable no-underscore-dangle */ export class ContentEditor { - constructor({ tiptapEditor, serializer, deserializer, eventHub, languageLoader }) { + constructor({ tiptapEditor, serializer, deserializer, assetResolver, eventHub, languageLoader }) { this._tiptapEditor = tiptapEditor; this._serializer = serializer; this._deserializer = deserializer; this._eventHub = eventHub; this._languageLoader = languageLoader; + this._assetResolver = assetResolver; } get tiptapEditor() { @@ -34,22 +35,27 @@ export class ContentEditor { this._eventHub.dispose(); } + deserialize(serializedContent) { + const { _tiptapEditor: editor, _deserializer: deserializer } = this; + + return deserializer.deserialize({ + schema: editor.schema, + content: serializedContent, + }); + } + + resolveAssetUrl(canonicalSrc) { + return this._assetResolver.resolveUrl(canonicalSrc); + } + async setSerializedContent(serializedContent) { - const { - _tiptapEditor: editor, - _deserializer: deserializer, - _eventHub: eventHub, - _languageLoader: languageLoader, - } = this; + const { _tiptapEditor: editor, _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 result = await deserializer.deserialize({ - schema: editor.schema, - content: serializedContent, - }); + const result = await this.deserialize(serializedContent); if (Object.keys(result).length !== 0) { const { document, languages } = result; 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 28041504e3c..adb1398b2c4 100644 --- a/app/assets/javascripts/content_editor/services/create_content_editor.js +++ b/app/assets/javascripts/content_editor/services/create_content_editor.js @@ -60,6 +60,7 @@ import { ContentEditor } from './content_editor'; import createMarkdownSerializer from './markdown_serializer'; import createGlApiMarkdownDeserializer from './gl_api_markdown_deserializer'; import createRemarkMarkdownDeserializer from './remark_markdown_deserializer'; +import createAssetResolver from './asset_resolver'; import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts'; import languageLoader from './code_block_language_loader'; @@ -152,6 +153,14 @@ export const createContentEditor = ({ : createGlApiMarkdownDeserializer({ render: renderMarkdown, }); + const assetResolver = createAssetResolver({ renderMarkdown }); - return new ContentEditor({ tiptapEditor, serializer, eventHub, deserializer, languageLoader }); + return new ContentEditor({ + tiptapEditor, + serializer, + eventHub, + deserializer, + languageLoader, + assetResolver, + }); }; diff --git a/app/assets/javascripts/content_editor/services/upload_helpers.js b/app/assets/javascripts/content_editor/services/upload_helpers.js index ed2c4b39131..09f0738b51b 100644 --- a/app/assets/javascripts/content_editor/services/upload_helpers.js +++ b/app/assets/javascripts/content_editor/services/upload_helpers.js @@ -70,6 +70,8 @@ const uploadContent = async ({ type, editor, file, uploadsPath, renderMarkdown, const position = state.selection.from - 1; const { tr } = state; + editor.commands.setNodeSelection(position); + try { const { src, canonicalSrc } = await uploadFile({ file, uploadsPath, renderMarkdown }); @@ -81,6 +83,8 @@ const uploadContent = async ({ type, editor, file, uploadsPath, renderMarkdown, canonicalSrc, }), ); + + editor.commands.setNodeSelection(position); } catch (e) { editor.commands.deleteRange({ from: position, to: position + 1 }); eventHub.$emit('alert', { diff --git a/app/assets/javascripts/content_editor/services/utils.js b/app/assets/javascripts/content_editor/services/utils.js index b3856b0dd74..e352fa8a9db 100644 --- a/app/assets/javascripts/content_editor/services/utils.js +++ b/app/assets/javascripts/content_editor/services/utils.js @@ -15,7 +15,7 @@ export const hasSelection = (tiptapEditor) => { * @returns {string} */ export const extractFilename = (src) => { - return src.replace(/^.*\/|\..+?$/g, ''); + return src.replace(/^.*\/|\.[^.]+?$/g, ''); }; export const readFileAsDataURL = (file) => { diff --git a/app/assets/javascripts/pages/projects/services/edit/index.js b/app/assets/javascripts/pages/projects/services/edit/index.js index 2048d3dfc37..64df0d07d74 100644 --- a/app/assets/javascripts/pages/projects/services/edit/index.js +++ b/app/assets/javascripts/pages/projects/services/edit/index.js @@ -1,5 +1,4 @@ import initIntegrationSettingsForm from '~/integrations/edit'; -import PrometheusAlerts from '~/prometheus_alerts'; import CustomMetrics from '~/prometheus_metrics/custom_metrics'; initIntegrationSettingsForm(); @@ -10,5 +9,3 @@ if (prometheusSettingsWrapper) { const customMetrics = new CustomMetrics(prometheusSettingsSelector); customMetrics.init(); } - -PrometheusAlerts(); diff --git a/app/assets/javascripts/prometheus_alerts/components/reset_key.vue b/app/assets/javascripts/prometheus_alerts/components/reset_key.vue deleted file mode 100644 index befbca48736..00000000000 --- a/app/assets/javascripts/prometheus_alerts/components/reset_key.vue +++ /dev/null @@ -1,149 +0,0 @@ -<script> -import { - GlButton, - GlFormGroup, - GlFormInput, - GlModal, - GlModalDirective, - GlSprintf, - GlLink, -} from '@gitlab/ui'; -import createFlash from '~/flash'; -import axios from '~/lib/utils/axios_utils'; -import { __ } from '~/locale'; -import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; - -export default { - copyToClipboard: __('Copy'), - components: { - GlButton, - GlFormGroup, - GlFormInput, - GlModal, - ClipboardButton, - GlSprintf, - GlLink, - }, - directives: { - 'gl-modal': GlModalDirective, - }, - props: { - initialAuthorizationKey: { - type: String, - required: false, - default: '', - }, - changeKeyUrl: { - type: String, - required: true, - }, - notifyUrl: { - type: String, - required: true, - }, - learnMoreUrl: { - type: String, - required: true, - }, - disabled: { - type: Boolean, - required: false, - default: false, - }, - }, - data() { - return { - authorizationKey: this.initialAuthorizationKey, - }; - }, - methods: { - resetKey() { - axios - .post(this.changeKeyUrl) - .then((res) => { - this.authorizationKey = res.data.token; - }) - .catch(() => { - createFlash({ - message: __('Failed to reset key. Please try again.'), - }); - }); - }, - }, -}; -</script> - -<template> - <div class="row py-4 border-top js-prometheus-alerts"> - <div class="col-lg-3"> - <h4 class="mt-0"> - {{ __('Alerts') }} - </h4> - <p> - {{ __('Receive alerts from manually configured Prometheus servers.') }} - </p> - </div> - <div class="col-lg-9"> - <gl-sprintf - :message=" - __( - 'To receive alerts from manually configured Prometheus services, add the following URL and Authorization key to your Prometheus webhook config file. Learn more about %{linkStart}configuring Prometheus%{linkEnd} to send alerts to GitLab.', - ) - " - > - <template #link="{ content }"> - <gl-link :href="learnMoreUrl" target="_blank">{{ content }}</gl-link> - </template> - </gl-sprintf> - <gl-form-group :label="__('URL')" label-for="notify-url" label-class="label-bold"> - <div class="input-group"> - <gl-form-input id="notify-url" :readonly="true" :value="notifyUrl" /> - <span class="input-group-append"> - <clipboard-button - :text="notifyUrl" - :title="$options.copyToClipboard" - :disabled="disabled" - /> - </span> - </div> - </gl-form-group> - <gl-form-group - :label="__('Authorization key')" - label-for="authorization-key" - label-class="label-bold" - > - <div class="input-group"> - <gl-form-input id="authorization-key" :readonly="true" :value="authorizationKey" /> - <span class="input-group-append"> - <clipboard-button - :text="authorizationKey" - :title="$options.copyToClipboard" - :disabled="disabled" - /> - </span> - </div> - </gl-form-group> - <template v-if="authorizationKey.length > 0"> - <gl-modal - modal-id="authKeyModal" - :title="__('Reset authorization key?')" - :ok-title="__('Reset authorization key')" - ok-variant="danger" - @ok="resetKey" - > - {{ - __( - 'Resetting the authorization key will invalidate the previous key. Existing alert configurations will need to be updated with the new key.', - ) - }} - </gl-modal> - <gl-button v-gl-modal.authKeyModal class="js-reset-auth-key" :disabled="disabled">{{ - __('Reset key') - }}</gl-button> - </template> - <gl-button v-else :disabled="disabled" class="js-reset-auth-key" @click="resetKey">{{ - __('Generate key') - }}</gl-button> - </div> - </div> -</template> diff --git a/app/assets/javascripts/prometheus_alerts/index.js b/app/assets/javascripts/prometheus_alerts/index.js deleted file mode 100644 index 7efe6ed186b..00000000000 --- a/app/assets/javascripts/prometheus_alerts/index.js +++ /dev/null @@ -1,28 +0,0 @@ -import Vue from 'vue'; -import ResetKey from './components/reset_key.vue'; - -export default () => { - const el = document.querySelector('#js-settings-prometheus-alerts'); - - if (!el) { - return; - } - - const { authorizationKey, changeKeyUrl, notifyUrl, learnMoreUrl, disabled } = el.dataset; - - // eslint-disable-next-line no-new - new Vue({ - el, - render(createElement) { - return createElement(ResetKey, { - props: { - initialAuthorizationKey: authorizationKey, - changeKeyUrl, - notifyUrl, - learnMoreUrl, - disabled, - }, - }); - }, - }); -}; diff --git a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue index accc9926a57..c2bb635e056 100644 --- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue +++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue @@ -38,7 +38,7 @@ import { } from '../runner_search_utils'; import { captureException } from '../sentry_utils'; -const runnersCountSmartQuery = { +const countSmartQuery = () => ({ query: runnersAdminCountQuery, fetchPolicy: fetchPolicies.NETWORK_ONLY, update(data) { @@ -47,6 +47,39 @@ const runnersCountSmartQuery = { error(error) { this.reportToSentry(error); }, +}); + +const tabCountSmartQuery = ({ type }) => { + return { + ...countSmartQuery(), + variables() { + return { + ...this.countVariables, + type, + }; + }, + }; +}; + +const statusCountSmartQuery = ({ status, name }) => { + return { + ...countSmartQuery(), + skip() { + // skip if filtering by status and not using _this_ status as filter + if (this.countVariables.status && this.countVariables.status !== status) { + // reset count for given status + this[name] = null; + return true; + } + return false; + }, + variables() { + return { + ...this.countVariables, + status, + }; + }, + }; }; export default { @@ -101,65 +134,30 @@ export default { this.reportToSentry(error); }, }, + + // Tabs counts allRunnersCount: { - ...runnersCountSmartQuery, - variables() { - return { - ...this.countVariables, - type: null, - }; - }, + ...tabCountSmartQuery({ type: null }), }, instanceRunnersCount: { - ...runnersCountSmartQuery, - variables() { - return { - ...this.countVariables, - type: INSTANCE_TYPE, - }; - }, + ...tabCountSmartQuery({ type: INSTANCE_TYPE }), }, groupRunnersCount: { - ...runnersCountSmartQuery, - variables() { - return { - ...this.countVariables, - type: GROUP_TYPE, - }; - }, + ...tabCountSmartQuery({ type: GROUP_TYPE }), }, projectRunnersCount: { - ...runnersCountSmartQuery, - variables() { - return { - ...this.countVariables, - type: PROJECT_TYPE, - }; - }, + ...tabCountSmartQuery({ type: PROJECT_TYPE }), }, + + // Runner stats onlineRunnersTotal: { - ...runnersCountSmartQuery, - variables() { - return { - status: STATUS_ONLINE, - }; - }, + ...statusCountSmartQuery({ status: STATUS_ONLINE, name: 'onlineRunnersTotal' }), }, offlineRunnersTotal: { - ...runnersCountSmartQuery, - variables() { - return { - status: STATUS_OFFLINE, - }; - }, + ...statusCountSmartQuery({ status: STATUS_OFFLINE, name: 'offlineRunnersTotal' }), }, staleRunnersTotal: { - ...runnersCountSmartQuery, - variables() { - return { - status: STATUS_STALE, - }; - }, + ...statusCountSmartQuery({ status: STATUS_STALE, name: 'staleRunnersTotal' }), }, }, computed: { @@ -263,12 +261,6 @@ export default { </script> <template> <div> - <runner-stats - :online-runners-count="onlineRunnersTotal" - :offline-runners-count="offlineRunnersTotal" - :stale-runners-count="staleRunnersTotal" - /> - <div class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0" > @@ -300,6 +292,12 @@ export default { :namespace="$options.filteredSearchNamespace" /> + <runner-stats + :online-runners-count="onlineRunnersTotal" + :offline-runners-count="offlineRunnersTotal" + :stale-runners-count="staleRunnersTotal" + /> + <div v-if="noRunnersFound" class="gl-text-center gl-p-5"> {{ __('No runners found') }} </div> diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue index b299d7c40fe..b5bd4b111fd 100644 --- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue +++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue @@ -34,7 +34,7 @@ import { } from '../runner_search_utils'; import { captureException } from '../sentry_utils'; -const runnersCountSmartQuery = { +const countSmartQuery = () => ({ query: groupRunnersCountQuery, fetchPolicy: fetchPolicies.NETWORK_ONLY, update(data) { @@ -43,6 +43,39 @@ const runnersCountSmartQuery = { error(error) { this.reportToSentry(error); }, +}); + +const tabCountSmartQuery = ({ type }) => { + return { + ...countSmartQuery(), + variables() { + return { + ...this.countVariables, + type, + }; + }, + }; +}; + +const statusCountSmartQuery = ({ status, name }) => { + return { + ...countSmartQuery(), + skip() { + // skip if filtering by status and not using _this_ status as filter + if (this.countVariables.status && this.countVariables.status !== status) { + // reset count for given status + this[name] = null; + return true; + } + return false; + }, + variables() { + return { + ...this.countVariables, + status, + }; + }, + }; }; export default { @@ -116,59 +149,27 @@ export default { this.reportToSentry(error); }, }, - onlineRunnersTotal: { - ...runnersCountSmartQuery, - variables() { - return { - groupFullPath: this.groupFullPath, - status: STATUS_ONLINE, - }; - }, - }, - offlineRunnersTotal: { - ...runnersCountSmartQuery, - variables() { - return { - groupFullPath: this.groupFullPath, - status: STATUS_OFFLINE, - }; - }, - }, - staleRunnersTotal: { - ...runnersCountSmartQuery, - variables() { - return { - groupFullPath: this.groupFullPath, - status: STATUS_STALE, - }; - }, - }, + + // Tabs counts allRunnersCount: { - ...runnersCountSmartQuery, - variables() { - return { - ...this.countVariables, - type: null, - }; - }, + ...tabCountSmartQuery({ type: null }), }, groupRunnersCount: { - ...runnersCountSmartQuery, - variables() { - return { - ...this.countVariables, - type: GROUP_TYPE, - }; - }, + ...tabCountSmartQuery({ type: GROUP_TYPE }), }, projectRunnersCount: { - ...runnersCountSmartQuery, - variables() { - return { - ...this.countVariables, - type: PROJECT_TYPE, - }; - }, + ...tabCountSmartQuery({ type: PROJECT_TYPE }), + }, + + // Runner status summary + onlineRunnersTotal: { + ...statusCountSmartQuery({ status: STATUS_ONLINE, name: 'onlineRunnersTotal' }), + }, + offlineRunnersTotal: { + ...statusCountSmartQuery({ status: STATUS_OFFLINE, name: 'offlineRunnersTotal' }), + }, + staleRunnersTotal: { + ...statusCountSmartQuery({ status: STATUS_STALE, name: 'staleRunnersTotal' }), }, }, computed: { @@ -263,12 +264,6 @@ export default { <template> <div> - <runner-stats - :online-runners-count="onlineRunnersTotal" - :offline-runners-count="offlineRunnersTotal" - :stale-runners-count="staleRunnersTotal" - /> - <div class="gl-display-flex gl-align-items-center"> <runner-type-tabs v-model="search" @@ -298,6 +293,12 @@ export default { :namespace="filteredSearchNamespace" /> + <runner-stats + :online-runners-count="onlineRunnersTotal" + :offline-runners-count="offlineRunnersTotal" + :stale-runners-count="staleRunnersTotal" + /> + <div v-if="noRunnersFound" class="gl-text-center gl-p-5"> {{ __('No runners found') }} </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index f350572a1f9..f12345426d4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -323,7 +323,7 @@ export default { restructuredWidgetShowMergeButtons() { if (this.glFeatures.restructuredMrWidget) { return ( - this.isMergeAllowed && + (this.isMergeAllowed || this.isAutoMergeAvailable) && this.state.userPermissions.canMerge && !this.mr.mergeOngoing && !this.mr.autoMergeEnabled @@ -443,6 +443,8 @@ export default { if (this.glFeatures.mergeRequestWidgetGraphql) { this.updateGraphqlState(); } + + this.isMakingRequest = false; }) .catch(() => { this.isMakingRequest = false; @@ -521,6 +523,7 @@ export default { <template> <div + data-testid="ready_to_merge_state" :class="{ 'gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-pl-7 gl-rounded-bottom-left-base gl-rounded-bottom-right-base': glFeatures.restructuredMrWidget, @@ -633,6 +636,7 @@ export default { glFeatures.restructuredMrWidget && (shouldShowSquashEdit || shouldShowMergeEdit) " v-model="editCommitMessage" + data-testid="widget_edit_commit_message" class="gl-display-flex gl-align-items-center" > {{ __('Edit commit message') }} diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 765bd146a03..1f309a19b14 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -373,8 +373,6 @@ export default { <suggestions v-if="hasSuggestion" :note-html="markdownPreview" - :from-line="lineNumber" - :from-content="lineContent" :line-type="lineType" :disabled="true" :suggestions="suggestions" diff --git a/app/assets/stylesheets/components/content_editor.scss b/app/assets/stylesheets/components/content_editor.scss index 7c64b0a6c86..870ed50c6eb 100644 --- a/app/assets/stylesheets/components/content_editor.scss +++ b/app/assets/stylesheets/components/content_editor.scss @@ -13,6 +13,11 @@ } } + img.ProseMirror-selectednode { + outline: 3px solid rgba($blue-400, 0.48); + outline-offset: -3px; + } + ul[data-type='taskList'] { list-style: none; padding: 0; @@ -125,4 +130,3 @@ .bubble-menu-form { width: 320px; } - diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb index 8dc9697c56d..ad2e384077a 100644 --- a/app/controllers/profiles/personal_access_tokens_controller.rb +++ b/app/controllers/profiles/personal_access_tokens_controller.rb @@ -63,5 +63,3 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController finder(state: 'active', sort: 'expires_at_asc').execute end end - -Profiles::PersonalAccessTokensController.prepend_mod_with('Profiles::PersonalAccessTokensController') diff --git a/app/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder.rb b/app/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder.rb new file mode 100644 index 00000000000..909a896d77c --- /dev/null +++ b/app/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +# Groups::ProjectsRequiringAuthorizationsRefresh::OnDirectMembershipFinder +# +# Given a group, this finder can be used to obtain a list of Project IDs of projects +# that requires their `project_authorizations` records to be refreshed in the event where +# a member has been added/removed/updated in the group. + +module Groups + module ProjectsRequiringAuthorizationsRefresh + class OnDirectMembershipFinder + def initialize(group) + @group = group + end + + def execute + project_ids = Set.new + + project_ids.merge(ids_of_projects_in_hierarchy_and_project_shares(@group)) + project_ids.merge(ids_of_projects_in_hierarchy_and_project_shares_of_shared_groups(@group)) + + project_ids.to_a + end + + private + + def ids_of_projects_in_hierarchy_and_project_shares(group) + project_ids = Set.new + + ids_of_projects_in_hierarchy = group.all_projects.pluck(:id) # rubocop: disable CodeReuse/ActiveRecord + ids_of_projects_in_project_shares = ids_of_projects_shared_with_self_and_descendant_groups(group) + + project_ids.merge(ids_of_projects_in_hierarchy) + project_ids.merge(ids_of_projects_in_project_shares) + + project_ids + end + + def ids_of_projects_shared_with_self_and_descendant_groups(group, batch_size: 50) + project_ids = Set.new + + group.self_and_descendants_ids.each_slice(batch_size) do |group_ids| + project_ids.merge(ProjectGroupLink.in_group(group_ids).pluck(:project_id)) # rubocop: disable CodeReuse/ActiveRecord + end + + project_ids + end + + def ids_of_projects_in_hierarchy_and_project_shares_of_shared_groups(group, batch_size: 10) + project_ids = Set.new + + group.shared_groups.each_batch(of: batch_size) do |shared_groups_batch| + shared_groups_batch.each do |shared_group| + project_ids.merge(ids_of_projects_in_hierarchy_and_project_shares(shared_group)) + end + end + + project_ids + end + end + end +end diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb index be266045951..7d356c1014c 100644 --- a/app/finders/personal_access_tokens_finder.rb +++ b/app/finders/personal_access_tokens_finder.rb @@ -79,8 +79,6 @@ class PersonalAccessTokensFinder tokens.active when 'inactive' tokens.inactive - when 'active_or_expired' - tokens.not_revoked.expired.or(tokens.active) else tokens end diff --git a/app/helpers/personal_access_tokens_helper.rb b/app/helpers/personal_access_tokens_helper.rb deleted file mode 100644 index 5cc8d21096f..00000000000 --- a/app/helpers/personal_access_tokens_helper.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -module PersonalAccessTokensHelper - def personal_access_token_expiration_enforced? - false - end -end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index b2943d61216..a49658ce7e0 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -13,6 +13,7 @@ class ApplicationSetting < ApplicationRecord ignore_column %i[max_package_files_for_package_destruction], remove_with: '14.9', remove_after: '2022-03-22' ignore_column :user_email_lookup_limit, remove_with: '15.0', remove_after: '2022-04-18' ignore_column :pseudonymizer_enabled, remove_with: '15.1', remove_after: '2022-06-22' + ignore_column :enforce_pat_expiration, remove_with: '15.2', remove_after: '2022-07-22' INSTANCE_REVIEW_MIN_USERS = 50 GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \ diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index 021ff789b13..68ba3d6eab4 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -49,10 +49,6 @@ class PersonalAccessToken < ApplicationRecord !revoked? && !expired? end - def expired_but_not_enforced? - false - end - def self.redis_getdel(user_id) Gitlab::Redis::SharedState.with do |redis| redis_key = redis_shared_state_key(user_id) diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml index a0fa69c54c5..d55efbaf701 100644 --- a/app/views/admin/application_settings/_account_and_limit.html.haml +++ b/app/views/admin/application_settings/_account_and_limit.html.haml @@ -29,7 +29,6 @@ = render_if_exists 'admin/application_settings/git_two_factor_session_expiry', form: f = render_if_exists 'admin/application_settings/personal_access_token_expiration_policy', form: f - = render_if_exists 'admin/application_settings/enforce_pat_expiration', form: f = render_if_exists 'admin/application_settings/ssh_key_expiration_policy', form: f = render_if_exists 'admin/application_settings/enforce_ssh_key_expiration', form: f diff --git a/app/views/admin/application_settings/_note_limits.html.haml b/app/views/admin/application_settings/_note_limits.html.haml index d4ae0d3944c..40760b3c45e 100644 --- a/app/views/admin/application_settings/_note_limits.html.haml +++ b/app/views/admin/application_settings/_note_limits.html.haml @@ -9,7 +9,7 @@ = f.label :notes_create_limit_allowlist, _('Users to exclude from the rate limit'), class: 'label-bold' = f.text_area :notes_create_limit_allowlist_raw, class: 'form-control gl-form-input', rows: 5, aria: { describedBy: 'note-create-limits-allowlist-field-description' } .form-text.text-muted{ id: 'note-create-limits-allowlist-field-description' } - = _('List of users allowed to exceed the rate limit.') + = _('List of users who are allowed to exceed the rate limit. Example: username1, username2') = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' } diff --git a/app/views/admin/application_settings/_users_api_limits.html.haml b/app/views/admin/application_settings/_users_api_limits.html.haml index 9b3502b3cfd..3918c76b12c 100644 --- a/app/views/admin/application_settings/_users_api_limits.html.haml +++ b/app/views/admin/application_settings/_users_api_limits.html.haml @@ -9,6 +9,6 @@ = f.label :users_get_by_id_limit_allowlist_raw, _('Users to exclude from the rate limit'), class: 'label-bold' = f.text_area :users_get_by_id_limit_allowlist_raw, class: 'form-control gl-form-input', rows: 5, aria: { describedBy: 'users-api-limit-users-allowlist-field-description' } .form-text.text-muted{ id: 'users-api-limit-users-allowlist-field-description' } - = _('List of users allowed to exceed the rate limit.') + = _('List of users who are allowed to exceed the rate limit. Example: username1, username2') = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' } diff --git a/app/views/projects/services/prometheus/_external_alerts.html.haml b/app/views/projects/services/prometheus/_external_alerts.html.haml deleted file mode 100644 index 168b4853a9a..00000000000 --- a/app/views/projects/services/prometheus/_external_alerts.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -- return unless can?(current_user, :read_prometheus_alerts, @project) -- return unless integration.manual_configuration? - -- notify_url = notify_project_prometheus_alerts_url(@project, format: :json) -- authorization_key = @project.alerting_setting.try(:token) -- learn_more_url = help_page_path('operations/metrics/alerts.md', anchor: 'external-prometheus-instances') - -#js-settings-prometheus-alerts{ data: { notify_url: notify_url, authorization_key: authorization_key, change_key_url: reset_alerting_token_project_settings_operations_path(@project), learn_more_url: learn_more_url, disabled: true } } diff --git a/app/views/projects/services/prometheus/_show.html.haml b/app/views/projects/services/prometheus/_show.html.haml index 3350ac8a6c5..c80dc46bdb5 100644 --- a/app/views/projects/services/prometheus/_show.html.haml +++ b/app/views/projects/services/prometheus/_show.html.haml @@ -5,5 +5,3 @@ .row.gl-mb-3.prometheus-metrics-monitoring.js-prometheus-metrics-monitoring = render 'projects/services/prometheus/metrics', project: @project, integration: integration - -= render 'projects/services/prometheus/external_alerts', project: @project, integration: integration diff --git a/app/views/projects/services/prometheus/_top.html.haml b/app/views/projects/services/prometheus/_top.html.haml deleted file mode 100644 index 52b29ea2e8f..00000000000 --- a/app/views/projects/services/prometheus/_top.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- return unless integration.manual_configuration? - -.row - .col-lg-12 - = render Pajamas::AlertComponent.new(dismissible: false) do - .gl-alert-body - = s_('AlertSettings|You can now set up alert endpoints for manually configured Prometheus instances in the Alerts section on the Operations settings page. Alert endpoint fields on this page have been deprecated.') - .gl-alert-actions - = link_to _('Visit settings page'), project_settings_operations_path(@project, anchor: 'js-alert-management-settings'), class: 'gl-button btn gl-alert-action btn-info' diff --git a/app/views/shared/access_tokens/_table.html.haml b/app/views/shared/access_tokens/_table.html.haml index 7f7dafbe5b0..5ca9cf8d9a4 100644 --- a/app/views/shared/access_tokens/_table.html.haml +++ b/app/views/shared/access_tokens/_table.html.haml @@ -1,23 +1,16 @@ - no_active_tokens_message = local_assigns.fetch(:no_active_tokens_message, _('This user has no active %{type}.') % { type: type_plural }) - impersonation = local_assigns.fetch(:impersonation, false) - resource = local_assigns.fetch(:resource, false) -- personal = !impersonation && !resource %hr %h5 = _('Active %{type} (%{token_length})') % { type: type_plural, token_length: active_tokens.length } -- if personal && !personal_access_token_expiration_enforced? - %p.profile-settings-content - = _("Personal access tokens are not revoked upon expiration.") - if impersonation %p.profile-settings-content = _("To see all the user's personal access tokens you must impersonate them first.") -- if personal - = render_if_exists 'profiles/personal_access_tokens/token_expiry_notification', active_tokens: active_tokens - - if active_tokens.present? .table-responsive %table.table.active-tokens @@ -46,12 +39,8 @@ %span.token-never-used-label= _('Never') %td - if token.expires? - - if token.expired? || token.expired_but_not_enforced? - %span{ class: 'text-danger has-tooltip', title: _('Token valid until revoked') } - = _('Expired') - - else - %span{ class: ('text-warning' if token.expires_soon?) } - = time_ago_with_tooltip(token.expires_at) + %span{ class: ('text-warning' if token.expires_soon?) } + = time_ago_with_tooltip(token.expires_at) - else %span.token-never-expires-label= _('Never') - if resource diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 65b33d8444b..81edeca4032 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -733,9 +733,6 @@ Gitlab.ee do Settings.cron_jobs['users_create_statistics_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['users_create_statistics_worker']['cron'] ||= '2 15 * * *' Settings.cron_jobs['users_create_statistics_worker']['job_class'] = 'Users::CreateStatisticsWorker' - Settings.cron_jobs['network_policy_metrics_worker'] ||= Settingslogic.new({}) - Settings.cron_jobs['network_policy_metrics_worker']['cron'] ||= '0 3 * * 0' - Settings.cron_jobs['network_policy_metrics_worker']['job_class'] = 'NetworkPolicyMetricsWorker' Settings.cron_jobs['iterations_update_status_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['iterations_update_status_worker']['cron'] ||= '5 0 * * *' Settings.cron_jobs['iterations_update_status_worker']['job_class'] = 'IterationsUpdateStatusWorker' diff --git a/config/metrics/counts_all/20210216175446_network_policy_forwards.yml b/config/metrics/counts_all/20210216175446_network_policy_forwards.yml index ef2b37bb001..64d7a9434be 100644 --- a/config/metrics/counts_all/20210216175446_network_policy_forwards.yml +++ b/config/metrics/counts_all/20210216175446_network_policy_forwards.yml @@ -8,7 +8,9 @@ product_stage: protect product_group: group::container security product_category: container_network_security value_type: number -status: active +status: removed +removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86351 +milestone_removed: 15.0 time_frame: all data_source: redis distribution: diff --git a/config/metrics/counts_all/20210216175448_network_policy_drops.yml b/config/metrics/counts_all/20210216175448_network_policy_drops.yml index 933363c0a14..d3a874253f9 100644 --- a/config/metrics/counts_all/20210216175448_network_policy_drops.yml +++ b/config/metrics/counts_all/20210216175448_network_policy_drops.yml @@ -8,7 +8,9 @@ product_stage: protect product_group: group::container security product_category: container_network_security value_type: number -status: active +status: removed +removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86351 +milestone_removed: "15.0" time_frame: all data_source: redis distribution: diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 9750c9dd14e..7be1f7f32c9 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -421,6 +421,8 @@ - 1 - - security_scans - 2 +- - security_sync_scan_policies + - 1 - - self_monitoring_project_create - 2 - - self_monitoring_project_delete diff --git a/data/deprecations/14-7-deprecate-static-site-editor.yml b/data/deprecations/14-7-deprecate-static-site-editor.yml index 18f2ec26645..0960bffe4cf 100644 --- a/data/deprecations/14-7-deprecate-static-site-editor.yml +++ b/data/deprecations/14-7-deprecate-static-site-editor.yml @@ -4,7 +4,9 @@ removal_milestone: "15.0" # The milestone when this feature is planned to be removed removal_date: "2022-05-22" # This should almost always be the 22nd of a month (YYYY-MM-22), the date of the milestone release when this feature is planned to be removed. body: | # Do not modify this line, instead modify the lines below. - The Static Site Editor will no longer be available starting in GitLab 15.0. Improvements to the Markdown editing experience across GitLab will deliver smiliar benefit but with a wider reach. Incoming requests to the Static Site Editor will be redirected to the Web IDE. Current users of the Static Site Editor can view the [documentation](https://docs.gitlab.com/ee/user/project/static_site_editor/) for more information, including how to remove the configuration files from existing projects. + The Static Site Editor will no longer be available starting in GitLab 15.0. Improvements to the Markdown editing experience across GitLab will deliver smiliar benefit but with a wider reach. Incoming requests to the Static Site Editor will be redirected to the [Web IDE](https://docs.gitlab.com/ee/user/project/web_ide/index.html). + + Current users of the Static Site Editor can view the [documentation](https://docs.gitlab.com/ee/user/project/static_site_editor/) for more information, including how to remove the configuration files from existing projects. # The following items are not published on the docs page, but may be used in the future. stage: Create # (optional - may be required in the future) String value of the stage that the feature was created in. e.g., Growth tiers: [Free, Premium, Ultimate] # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate] diff --git a/db/migrate/20220502125053_recreate_index_for_project_group_link_with_group_id_and_project_id.rb b/db/migrate/20220502125053_recreate_index_for_project_group_link_with_group_id_and_project_id.rb new file mode 100644 index 00000000000..1d9a18b7b23 --- /dev/null +++ b/db/migrate/20220502125053_recreate_index_for_project_group_link_with_group_id_and_project_id.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class RecreateIndexForProjectGroupLinkWithGroupIdAndProjectId < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + OLD_INDEX_NAME = 'index_project_group_links_on_group_id' + NEW_INDEX_NAME = 'index_project_group_links_on_group_id_and_project_id' + + def up + add_concurrent_index :project_group_links, [:group_id, :project_id], name: NEW_INDEX_NAME + remove_concurrent_index_by_name :project_group_links, OLD_INDEX_NAME + end + + def down + add_concurrent_index :project_group_links, [:group_id], name: OLD_INDEX_NAME + remove_concurrent_index_by_name :project_group_links, NEW_INDEX_NAME + end +end diff --git a/db/migrate/20220502150408_add_slack_integrations_bot_columns.rb b/db/migrate/20220502150408_add_slack_integrations_bot_columns.rb new file mode 100644 index 00000000000..cb5b201e71e --- /dev/null +++ b/db/migrate/20220502150408_add_slack_integrations_bot_columns.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddSlackIntegrationsBotColumns < Gitlab::Database::Migration[2.0] + def change + change_table :slack_integrations do |t| + t.column :bot_user_id, :text + t.column :encrypted_bot_access_token, :binary + t.column :encrypted_bot_access_token_iv, :binary + end + end +end diff --git a/db/migrate/20220502152633_add_slack_integrations_bot_user_id_text_limit.rb b/db/migrate/20220502152633_add_slack_integrations_bot_user_id_text_limit.rb new file mode 100644 index 00000000000..649d6ccf9d4 --- /dev/null +++ b/db/migrate/20220502152633_add_slack_integrations_bot_user_id_text_limit.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddSlackIntegrationsBotUserIdTextLimit < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + def up + add_text_limit :slack_integrations, :bot_user_id, 255 + end + + def down + remove_text_limit :slack_integrations, :bot_user_id + end +end diff --git a/db/migrate/20220503073401_recreate_index_for_group_group_link_with_both_group_ids.rb b/db/migrate/20220503073401_recreate_index_for_group_group_link_with_both_group_ids.rb new file mode 100644 index 00000000000..214e9c5e0a7 --- /dev/null +++ b/db/migrate/20220503073401_recreate_index_for_group_group_link_with_both_group_ids.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class RecreateIndexForGroupGroupLinkWithBothGroupIds < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + OLD_INDEX_NAME = 'index_group_group_links_on_shared_with_group_id' + NEW_INDEX_NAME = 'index_group_group_links_on_shared_with_group_and_shared_group' + + def up + add_concurrent_index :group_group_links, [:shared_with_group_id, :shared_group_id], name: NEW_INDEX_NAME + remove_concurrent_index_by_name :group_group_links, OLD_INDEX_NAME + end + + def down + add_concurrent_index :group_group_links, [:shared_with_group_id], name: OLD_INDEX_NAME + remove_concurrent_index_by_name :group_group_links, NEW_INDEX_NAME + end +end diff --git a/db/schema_migrations/20220502125053 b/db/schema_migrations/20220502125053 new file mode 100644 index 00000000000..9b026e23576 --- /dev/null +++ b/db/schema_migrations/20220502125053 @@ -0,0 +1 @@ +b87e7b69f4d88a5620180648568c499e6e86fe001a8cfd235eebf050d5cdc9a1
\ No newline at end of file diff --git a/db/schema_migrations/20220502150408 b/db/schema_migrations/20220502150408 new file mode 100644 index 00000000000..2bab54bbe7d --- /dev/null +++ b/db/schema_migrations/20220502150408 @@ -0,0 +1 @@ +a730ff7969895be95e92fff5bb9b468ed407bd65bccb9daf40f892e18b4d18b6
\ No newline at end of file diff --git a/db/schema_migrations/20220502152633 b/db/schema_migrations/20220502152633 new file mode 100644 index 00000000000..b5dd2256ac8 --- /dev/null +++ b/db/schema_migrations/20220502152633 @@ -0,0 +1 @@ +f8f34dc48e55723d868d1a247a92731ed1f1d5d185791c3202d0ed2cdedb41d3
\ No newline at end of file diff --git a/db/schema_migrations/20220503073401 b/db/schema_migrations/20220503073401 new file mode 100644 index 00000000000..bccca17138b --- /dev/null +++ b/db/schema_migrations/20220503073401 @@ -0,0 +1 @@ +3e05b07c5a3a0912884e0bdda08e0f4ef93ce95b6e3f5deb30b10eca74c6ea79
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index becf04c5a02..0d494106594 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -20602,7 +20602,11 @@ CREATE TABLE slack_integrations ( alias character varying NOT NULL, user_id character varying NOT NULL, created_at timestamp without time zone NOT NULL, - updated_at timestamp without time zone NOT NULL + updated_at timestamp without time zone NOT NULL, + bot_user_id text, + encrypted_bot_access_token bytea, + encrypted_bot_access_token_iv bytea, + CONSTRAINT check_bc553aea8a CHECK ((char_length(bot_user_id) <= 255)) ); CREATE SEQUENCE slack_integrations_id_seq @@ -27886,7 +27890,7 @@ CREATE UNIQUE INDEX index_group_deploy_tokens_on_group_and_deploy_token_ids ON g CREATE UNIQUE INDEX index_group_group_links_on_shared_group_and_shared_with_group ON group_group_links USING btree (shared_group_id, shared_with_group_id); -CREATE INDEX index_group_group_links_on_shared_with_group_id ON group_group_links USING btree (shared_with_group_id); +CREATE INDEX index_group_group_links_on_shared_with_group_and_shared_group ON group_group_links USING btree (shared_with_group_id, shared_group_id); CREATE INDEX index_group_import_states_on_group_id ON group_import_states USING btree (group_id); @@ -28768,7 +28772,7 @@ COMMENT ON INDEX index_project_features_on_project_id_include_container_registry CREATE INDEX index_project_features_on_project_id_ral_20 ON project_features USING btree (project_id) WHERE (repository_access_level = 20); -CREATE INDEX index_project_group_links_on_group_id ON project_group_links USING btree (group_id); +CREATE INDEX index_project_group_links_on_group_id_and_project_id ON project_group_links USING btree (group_id, project_id); CREATE INDEX index_project_group_links_on_project_id ON project_group_links USING btree (project_id); diff --git a/doc/administration/index.md b/doc/administration/index.md index b094bd59e58..1d8dcd34d68 100644 --- a/doc/administration/index.md +++ b/doc/administration/index.md @@ -205,10 +205,6 @@ Learn how to install, configure, update, and maintain your GitLab instance. - [GitLab performance monitoring with Grafana](monitoring/performance/grafana_configuration.md): Configure GitLab to visualize time series metrics through graphs and dashboards. - [Performance Bar](monitoring/performance/performance_bar.md): Get performance information for the current page. -## Analytics - -- [Pseudonymizer](pseudonymizer.md): Export data from a GitLab database to CSV files in a secure way. - ## Troubleshooting - [Debugging tips](troubleshooting/debug.md): Tips to debug problems when things go wrong. diff --git a/doc/administration/object_storage.md b/doc/administration/object_storage.md index 561108eef57..0560a8813df 100644 --- a/doc/administration/object_storage.md +++ b/doc/administration/object_storage.md @@ -536,7 +536,6 @@ supported by consolidated configuration form, refer to the following guides: | [Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage)| **{dotted-circle}** No | | [Packages](packages/index.md#using-object-storage) (optional feature) | **{check-circle}** Yes | | [Dependency Proxy](packages/dependency_proxy.md#using-object-storage) (optional feature) | **{check-circle}** Yes | -| [Pseudonymizer](pseudonymizer.md) (optional feature) | **{dotted-circle}** No | | [Autoscale runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional for improved performance) | **{dotted-circle}** No | | [Terraform state files](terraform_state.md#using-object-storage) | **{check-circle}** Yes | | [Pages content](pages/index.md#using-object-storage) | **{check-circle}** Yes | @@ -570,7 +569,7 @@ There are plans to [enable the use of a single bucket](https://gitlab.com/gitlab in the future. Helm-based installs require separate buckets to -[handle backup restorations](https://docs.gitlab.com/charts/advanced/external-object-storage/#lfs-artifacts-uploads-packages-external-diffs-pseudonymizer). +[handle backup restorations](https://docs.gitlab.com/charts/advanced/external-object-storage/#lfs-artifacts-uploads-packages-external-diffs-terraform-state-dependency-proxy). ### S3 API compatibility issues diff --git a/doc/administration/pseudonymizer.md b/doc/administration/pseudonymizer.md index 24d9792dcb0..ad4cfd11474 100644 --- a/doc/administration/pseudonymizer.md +++ b/doc/administration/pseudonymizer.md @@ -2,127 +2,14 @@ stage: Enablement group: Distribution 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 +remove_date: '2022-08-22' +redirect_to: 'index.md' --- -# Pseudonymizer (DEPRECATED) **(ULTIMATE)** +# Pseudonymizer (removed) **(ULTIMATE)** -> [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/219952) in GitLab 14.7. +> [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/219952) in +> GitLab 14.7 and removed in 15.0. WARNING: This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/219952) in GitLab 14.7. - -Your GitLab database contains sensitive information. To protect sensitive information -when you run analytics on your database, you can use the Pseudonymizer service, which: - -1. Uses `HMAC(SHA256)` to mutate fields containing sensitive information. -1. Preserves references (referential integrity) between fields. -1. Exports your GitLab data, scrubbed of sensitive material. - -WARNING: -If the source data is available, users can compare and correlate the scrubbed data -with the original. - -To generate a pseudonymized data set: - -1. [Configure Pseudonymizer](#configure-pseudonymizer) fields and output location. -1. [Enable Pseudonymizer data collection](#enable-pseudonymizer-data-collection). -1. Optional. [Generate a data set manually](#generate-data-set-manually). - -## Configure Pseudonymizer - -To use the Pseudonymizer, configure both the fields you want to anonymize, and the location to -store the scrubbed data: - -1. **Create a manifest file**: This file describes the fields to include or pseudonymize. - - **Default manifest** - GitLab provides a default manifest in your GitLab installation - ([example `manifest.yml` file](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/pseudonymizer.yml)). - To use the example manifest file, use the `config/pseudonymizer.yml` relative path - when you configure connection parameters. - - **Custom manifest** - To use a custom manifest file, use the absolute path to - the file when you configure the connection parameters. -1. **Configure connection parameters**: In the configuration method appropriate for - your version of GitLab, specify the [object storage](object_storage.md) - connection parameters (`pseudonymizer.upload.connection`). - -**For Omnibus installations:** - -1. Edit `/etc/gitlab/gitlab.rb` and add the following lines by replacing with - the values you want: - - ```ruby - gitlab_rails['pseudonymizer_manifest'] = 'config/pseudonymizer.yml' - gitlab_rails['pseudonymizer_upload_remote_directory'] = 'gitlab-elt' # bucket name - gitlab_rails['pseudonymizer_upload_connection'] = { - 'provider' => 'AWS', - 'region' => 'eu-central-1', - 'aws_access_key_id' => 'AWS_ACCESS_KEY_ID', - 'aws_secret_access_key' => 'AWS_SECRET_ACCESS_KEY' - } - ``` - - If you are using AWS IAM profiles, omit the AWS access key and secret access key/value pairs. - - ```ruby - gitlab_rails['pseudonymizer_upload_connection'] = { - 'provider' => 'AWS', - 'region' => 'eu-central-1', - 'use_iam_profile' => true - } - ``` - -1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) - for the changes to take effect. - ---- - -**For installations from source:** - -1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following - lines: - - ```yaml - pseudonymizer: - manifest: config/pseudonymizer.yml - upload: - remote_directory: 'gitlab-elt' # bucket name - connection: - provider: AWS - aws_access_key_id: AWS_ACCESS_KEY_ID - aws_secret_access_key: AWS_SECRET_ACCESS_KEY - region: eu-central-1 - ``` - -1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) - for the changes to take effect. - -## Enable Pseudonymizer data collection - -To enable data collection: - -1. On the top bar, select **Menu > Admin**. -1. On the left sidebar, select **Settings > Metrics and Profiling**, then expand - **Pseudonymizer data collection**. -1. Select **Enable Pseudonymizer data collection**. -1. Select **Save changes**. - -## Generate data set manually - -You can also run the Pseudonymizer manually: - -1. Set these environment variables: - - `PSEUDONYMIZER_OUTPUT_DIR` - Where to store the output CSV files. Defaults to `/tmp`. - These commands produce CSV files that can be quite large. Make sure the directory - can store a file at least 10% of the size of your database. - - `PSEUDONYMIZER_BATCH` - The batch size when querying the database. Defaults to `100000`. -1. Run the command appropriate for your application: - - **Omnibus GitLab**: - `sudo gitlab-rake gitlab:db:pseudonymizer` - - **Installations from source**: - `sudo -u git -H bundle exec rake gitlab:db:pseudonymizer RAILS_ENV=production` - -After you run the command, upload the output CSV files to your configured object -storage. After the upload completes, delete the output file from the local disk. - -## Related topics - -- [Using object storage with GitLab](object_storage.md). diff --git a/doc/administration/reference_architectures/10k_users.md b/doc/administration/reference_architectures/10k_users.md index e305c34fd5f..47eb149eb2d 100644 --- a/doc/administration/reference_architectures/10k_users.md +++ b/doc/administration/reference_architectures/10k_users.md @@ -2202,7 +2202,6 @@ on what features you intend to use: | [Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage)| No | | [Packages](../packages/index.md#using-object-storage) (optional feature) | Yes | | [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) | Yes | -| [Pseudonymizer](../pseudonymizer.md) (optional feature) | No | | [Autoscale runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional for improved performance) | No | | [Terraform state files](../terraform_state.md#using-object-storage) | Yes | diff --git a/doc/administration/reference_architectures/25k_users.md b/doc/administration/reference_architectures/25k_users.md index bd09edbebfc..225cf4b9c71 100644 --- a/doc/administration/reference_architectures/25k_users.md +++ b/doc/administration/reference_architectures/25k_users.md @@ -2206,7 +2206,6 @@ on what features you intend to use: | [Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage)| No | | [Packages](../packages/index.md#using-object-storage) (optional feature) | Yes | | [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) | Yes | -| [Pseudonymizer](../pseudonymizer.md) (optional feature) | No | | [Autoscale runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional for improved performance) | No | | [Terraform state files](../terraform_state.md#using-object-storage) | Yes | diff --git a/doc/administration/reference_architectures/2k_users.md b/doc/administration/reference_architectures/2k_users.md index bb6a9049f68..be089203f4f 100644 --- a/doc/administration/reference_architectures/2k_users.md +++ b/doc/administration/reference_architectures/2k_users.md @@ -924,7 +924,6 @@ on what features you intend to use: | [Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage)| No | | [Packages](../packages/index.md#using-object-storage) (optional feature) | Yes | | [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) | Yes | -| [Pseudonymizer](../pseudonymizer.md) (optional feature) | No | | [Autoscale runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional for improved performance) | No | | [Terraform state files](../terraform_state.md#using-object-storage) | Yes | diff --git a/doc/administration/reference_architectures/3k_users.md b/doc/administration/reference_architectures/3k_users.md index a55e1c53ae5..d1ee13b1940 100644 --- a/doc/administration/reference_architectures/3k_users.md +++ b/doc/administration/reference_architectures/3k_users.md @@ -2141,7 +2141,6 @@ on what features you intend to use: | [Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage)| No | | [Packages](../packages/index.md#using-object-storage) (optional feature) | Yes | | [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) | Yes | -| [Pseudonymizer](../pseudonymizer.md) (optional feature) | No | | [Autoscale runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional for improved performance) | No | | [Terraform state files](../terraform_state.md#using-object-storage) | Yes | diff --git a/doc/administration/reference_architectures/50k_users.md b/doc/administration/reference_architectures/50k_users.md index 66a8e9e67d4..7fb3f158848 100644 --- a/doc/administration/reference_architectures/50k_users.md +++ b/doc/administration/reference_architectures/50k_users.md @@ -2222,7 +2222,6 @@ on what features you intend to use: | [Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage)| No | | [Packages](../packages/index.md#using-object-storage) (optional feature) | Yes | | [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) | Yes | -| [Pseudonymizer](../pseudonymizer.md) (optional feature) | No | | [Autoscale runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional for improved performance) | No | | [Terraform state files](../terraform_state.md#using-object-storage) | Yes | diff --git a/doc/administration/reference_architectures/5k_users.md b/doc/administration/reference_architectures/5k_users.md index c3c4f1311f2..91a630a7eb0 100644 --- a/doc/administration/reference_architectures/5k_users.md +++ b/doc/administration/reference_architectures/5k_users.md @@ -2141,7 +2141,6 @@ on what features you intend to use: | [Mattermost](https://docs.mattermost.com/administration/config-settings.html#file-storage)| No | | [Packages](../packages/index.md#using-object-storage) (optional feature) | Yes | | [Dependency Proxy](../packages/dependency_proxy.md#using-object-storage) (optional feature) | Yes | -| [Pseudonymizer](../pseudonymizer.md) (optional feature) | No | | [Autoscale runner caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching) (optional for improved performance) | No | | [Terraform state files](../terraform_state.md#using-object-storage) | Yes | diff --git a/doc/administration/terraform_state.md b/doc/administration/terraform_state.md index 5e8f7cb18bb..c2fbcbdfc55 100644 --- a/doc/administration/terraform_state.md +++ b/doc/administration/terraform_state.md @@ -18,7 +18,7 @@ The storage location of these files defaults to: These locations can be configured using the options described below. -Use [external object storage](https://docs.gitlab.com/charts/advanced/external-object-storage/#lfs-artifacts-uploads-packages-external-diffs-pseudonymizer-terraform-state-dependency-proxy) configuration for [GitLab Helm chart](https://docs.gitlab.com/charts/) installations. +Use [external object storage](https://docs.gitlab.com/charts/advanced/external-object-storage/#lfs-artifacts-uploads-packages-external-diffs-terraform-state-dependency-proxy) configuration for [GitLab Helm chart](https://docs.gitlab.com/charts/) installations. ## Disabling Terraform state diff --git a/doc/api/settings.md b/doc/api/settings.md index f8df0220240..cf20cd279fc 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -392,7 +392,6 @@ listed in the descriptions of the relevant settings. | `project_export_enabled` | boolean | no | Enable project export. | | `prometheus_metrics_enabled` | boolean | no | Enable Prometheus metrics. | | `protected_ci_variables` | boolean | no | CI/CD variables are protected by default. | -| `pseudonymizer_enabled` **(PREMIUM)** | boolean | no | When enabled, GitLab runs a background job that produces pseudonymized CSVs of the GitLab database to upload to your configured object storage directory. | `push_event_activities_limit` | integer | no | Number of changes (branches or tags) in a single push to determine whether individual push events or bulk push events are created. [Bulk push events are created](../user/admin_area/settings/push_event_activities_limit.md) if it surpasses that value. | | `push_event_hooks_limit` | integer | no | Number of changes (branches or tags) in a single push to determine whether webhooks and services fire or not. Webhooks and services aren't submitted if it surpasses that value. | | `rate_limiting_response_text` | string | no | When rate limiting is enabled via the `throttle_*` settings, send this plain text response when a rate limit is exceeded. 'Retry later' is sent if this is blank. | diff --git a/doc/ci/variables/index.md b/doc/ci/variables/index.md index bebe9f9d20a..0bdfab97b9a 100644 --- a/doc/ci/variables/index.md +++ b/doc/ci/variables/index.md @@ -848,8 +848,8 @@ if [[ -d "/builds/gitlab-examples/ci-debug-trace/.git" ]]; then ++ CI_SERVER_PROTOCOL=https ++ export CI_SERVER_NAME=GitLab ++ CI_SERVER_NAME=GitLab -++ export GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics,description_diffs,elastic_search,group_bulk_edit,group_burndown_charts,group_webhooks,issuable_default_templates,issue_weights,jenkins_integration,ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees,multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users,push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board,usage_quotas,visual_review_app,wip_limits,adjourned_deletion_for_projects_and_groups,admin_audit_log,auditor_user,batch_comments,blocking_merge_requests,board_assignee_lists,board_milestone_lists,ci_cd_projects,cluster_deployments,code_analytics,code_owner_approval_required,commit_committer_check,cross_project_pipelines,custom_file_templates,custom_file_templates_for_namespace,custom_project_templates,custom_prometheus_metrics,cycle_analytics_for_groups,db_load_balancing,default_project_deletion_protection,dependency_proxy,deploy_board,design_management,email_additional_text,extended_audit_events,external_authorization_service_api_management,feature_flags,file_locks,geo,github_project_service_integration,group_allowed_email_domains,group_project_templates,group_saml,issues_analytics,jira_dev_panel_integration,ldap_group_sync_filter,merge_pipelines,merge_request_performance_metrics,merge_trains,metrics_reports,multiple_approval_rules,multiple_group_issue_boards,object_storage,operations_dashboard,packages,productivity_analytics,project_aliases,protected_environments,reject_unsigned_commits,required_ci_templates,scoped_labels,service_desk,smartcard_auth,group_timelogs,type_of_work_analytics,unprotection_restrictions,ci_project_subscriptions,container_scanning,dast,dependency_scanning,epics,group_ip_restriction,incident_management,insights,license_management,personal_access_token_expiration_policy,pod_logs,prometheus_alerts,pseudonymizer,report_approver_rules,sast,security_dashboard,tracing,web_ide_terminal -++ GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics,description_diffs,elastic_search,group_bulk_edit,group_burndown_charts,group_webhooks,issuable_default_templates,issue_weights,jenkins_integration,ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees,multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users,push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board,usage_quotas,visual_review_app,wip_limits,adjourned_deletion_for_projects_and_groups,admin_audit_log,auditor_user,batch_comments,blocking_merge_requests,board_assignee_lists,board_milestone_lists,ci_cd_projects,cluster_deployments,code_analytics,code_owner_approval_required,commit_committer_check,cross_project_pipelines,custom_file_templates,custom_file_templates_for_namespace,custom_project_templates,custom_prometheus_metrics,cycle_analytics_for_groups,db_load_balancing,default_project_deletion_protection,dependency_proxy,deploy_board,design_management,email_additional_text,extended_audit_events,external_authorization_service_api_management,feature_flags,file_locks,geo,github_project_service_integration,group_allowed_email_domains,group_project_templates,group_saml,issues_analytics,jira_dev_panel_integration,ldap_group_sync_filter,merge_pipelines,merge_request_performance_metrics,merge_trains,metrics_reports,multiple_approval_rules,multiple_group_issue_boards,object_storage,operations_dashboard,packages,productivity_analytics,project_aliases,protected_environments,reject_unsigned_commits,required_ci_templates,scoped_labels,service_desk,smartcard_auth,group_timelogs,type_of_work_analytics,unprotection_restrictions,ci_project_subscriptions,cluster_health,container_scanning,dast,dependency_scanning,epics,group_ip_restriction,incident_management,insights,license_management,personal_access_token_expiration_policy,pod_logs,prometheus_alerts,pseudonymizer,report_approver_rules,sast,security_dashboard,tracing,web_ide_terminal +++ export GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics,description_diffs,elastic_search,group_bulk_edit,group_burndown_charts,group_webhooks,issuable_default_templates,issue_weights,jenkins_integration,ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees,multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users,push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board,usage_quotas,visual_review_app,wip_limits,adjourned_deletion_for_projects_and_groups,admin_audit_log,auditor_user,batch_comments,blocking_merge_requests,board_assignee_lists,board_milestone_lists,ci_cd_projects,cluster_deployments,code_analytics,code_owner_approval_required,commit_committer_check,cross_project_pipelines,custom_file_templates,custom_file_templates_for_namespace,custom_project_templates,custom_prometheus_metrics,cycle_analytics_for_groups,db_load_balancing,default_project_deletion_protection,dependency_proxy,deploy_board,design_management,email_additional_text,extended_audit_events,external_authorization_service_api_management,feature_flags,file_locks,geo,github_project_service_integration,group_allowed_email_domains,group_project_templates,group_saml,issues_analytics,jira_dev_panel_integration,ldap_group_sync_filter,merge_pipelines,merge_request_performance_metrics,merge_trains,metrics_reports,multiple_approval_rules,multiple_group_issue_boards,object_storage,operations_dashboard,packages,productivity_analytics,project_aliases,protected_environments,reject_unsigned_commits,required_ci_templates,scoped_labels,service_desk,smartcard_auth,group_timelogs,type_of_work_analytics,unprotection_restrictions,ci_project_subscriptions,container_scanning,dast,dependency_scanning,epics,group_ip_restriction,incident_management,insights,license_management,personal_access_token_expiration_policy,pod_logs,prometheus_alerts,report_approver_rules,sast,security_dashboard,tracing,web_ide_terminal +++ GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics,description_diffs,elastic_search,group_bulk_edit,group_burndown_charts,group_webhooks,issuable_default_templates,issue_weights,jenkins_integration,ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees,multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users,push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board,usage_quotas,visual_review_app,wip_limits,adjourned_deletion_for_projects_and_groups,admin_audit_log,auditor_user,batch_comments,blocking_merge_requests,board_assignee_lists,board_milestone_lists,ci_cd_projects,cluster_deployments,code_analytics,code_owner_approval_required,commit_committer_check,cross_project_pipelines,custom_file_templates,custom_file_templates_for_namespace,custom_project_templates,custom_prometheus_metrics,cycle_analytics_for_groups,db_load_balancing,default_project_deletion_protection,dependency_proxy,deploy_board,design_management,email_additional_text,extended_audit_events,external_authorization_service_api_management,feature_flags,file_locks,geo,github_project_service_integration,group_allowed_email_domains,group_project_templates,group_saml,issues_analytics,jira_dev_panel_integration,ldap_group_sync_filter,merge_pipelines,merge_request_performance_metrics,merge_trains,metrics_reports,multiple_approval_rules,multiple_group_issue_boards,object_storage,operations_dashboard,packages,productivity_analytics,project_aliases,protected_environments,reject_unsigned_commits,required_ci_templates,scoped_labels,service_desk,smartcard_auth,group_timelogs,type_of_work_analytics,unprotection_restrictions,ci_project_subscriptions,cluster_health,container_scanning,dast,dependency_scanning,epics,group_ip_restriction,incident_management,insights,license_management,personal_access_token_expiration_policy,pod_logs,prometheus_alerts,report_approver_rules,sast,security_dashboard,tracing,web_ide_terminal ++ export CI_PROJECT_ID=17893 ++ CI_PROJECT_ID=17893 ++ export CI_PROJECT_NAME=ci-debug-trace diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md index a24b5ea8e3b..87954330878 100644 --- a/doc/update/deprecations.md +++ b/doc/update/deprecations.md @@ -1154,7 +1154,9 @@ to serve the Sidekiq metrics, similar to the way Sidekiq will behave in 15.0. ### Static Site Editor -The Static Site Editor will no longer be available starting in GitLab 15.0. Improvements to the Markdown editing experience across GitLab will deliver smiliar benefit but with a wider reach. Incoming requests to the Static Site Editor will be redirected to the Web IDE. Current users of the Static Site Editor can view the [documentation](https://docs.gitlab.com/ee/user/project/static_site_editor/) for more information, including how to remove the configuration files from existing projects. +The Static Site Editor will no longer be available starting in GitLab 15.0. Improvements to the Markdown editing experience across GitLab will deliver smiliar benefit but with a wider reach. Incoming requests to the Static Site Editor will be redirected to the [Web IDE](https://docs.gitlab.com/ee/user/project/web_ide/index.html). + +Current users of the Static Site Editor can view the [documentation](https://docs.gitlab.com/ee/user/project/static_site_editor/) for more information, including how to remove the configuration files from existing projects. **Planned removal milestone: 15.0 (2022-05-22)** diff --git a/doc/user/admin_area/credentials_inventory.md b/doc/user/admin_area/credentials_inventory.md index bcf15192ef0..4308b45df78 100644 --- a/doc/user/admin_area/credentials_inventory.md +++ b/doc/user/admin_area/credentials_inventory.md @@ -40,14 +40,11 @@ To access the Credentials inventory: If you see a **Revoke** button, you can revoke that user's PAT. Whether you see a **Revoke** button depends on the token state, and if an expiration date has been set. For more information, see the following table: -| Token state | [Token expiration enforced?](settings/account_and_limit_settings.md#allow-expired-access-tokens-to-be-used-deprecated) | Show Revoke button? | Comments | -|-------------|------------------------|--------------------|----------------------------------------------------------------------------| -| Active | Yes | Yes | Allows administrators to revoke the PAT, such as for a compromised account | -| Active | No | Yes | Allows administrators to revoke the PAT, such as for a compromised account | -| Expired | Yes | No | PAT expires automatically | -| Expired | No | Yes | The administrator may revoke the PAT to prevent indefinite use | -| Revoked | Yes | No | Not applicable; token is already revoked | -| Revoked | No | No | Not applicable; token is already revoked | +| Token state | Show Revoke button? | Comments | +|-------------|---------------------|----------------------------------------------------------------------------| +| Active | Yes | Allows administrators to revoke the PAT, such as for a compromised account | +| Expired | No | Not applicable; token is already expired | +| Revoked | No | Not applicable; token is already revoked | When a PAT is revoked from the credentials inventory, the instance notifies the user by email. diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md index 04952b6dcdf..df2ea82a556 100644 --- a/doc/user/admin_area/settings/account_and_limit_settings.md +++ b/doc/user/admin_area/settings/account_and_limit_settings.md @@ -285,23 +285,17 @@ Once a lifetime for access tokens is set, GitLab: allowed lifetime. Three hours is given to allow administrators to change the allowed lifetime, or remove it, before revocation takes place. -## Allow expired access tokens to be used (DEPRECATED) **(ULTIMATE SELF)** +<!-- start_remove The following content will be removed on remove_date: '2022-08-22' --> +## Allow expired access tokens to be used (removed) **(ULTIMATE SELF)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214723) in GitLab 13.1. > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/296881) in GitLab 13.9. > - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/351962) in GitLab 14.8. +> - [Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/351962) in GitLab 15.0. -WARNING: This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/351962) in GitLab 14.8. - -By default, expired personal access tokens (PATs) **are not usable**. - -To allow the use of expired PATs: - -1. On the top bar, select **Menu > Admin**. -1. On the left sidebar, select **Settings > General**. -1. Expand the **Account and limit** section. -1. Uncheck the **Enforce access token expiration** checkbox. +This feature was [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/351962) in GitLab 15.0. +<!-- end_remove --> ## Disable user profile name changes **(PREMIUM SELF)** diff --git a/doc/user/admin_area/settings/index.md b/doc/user/admin_area/settings/index.md index 052b6e26c07..9c6aa22e63c 100644 --- a/doc/user/admin_area/settings/index.md +++ b/doc/user/admin_area/settings/index.md @@ -112,8 +112,6 @@ The **Metrics and profiling** settings contain: - [Self monitoring](../../../administration/monitoring/gitlab_self_monitoring_project/index.md#create-the-self-monitoring-project) - Enable or disable instance self monitoring. - [Usage statistics](usage_statistics.md) - Enable or disable version check and Service Ping. -- [Pseudonymizer data collection](../../../administration/pseudonymizer.md) - - Enable or disable the Pseudonymizer data collection. ### Network diff --git a/doc/user/discussions/img/confidential_comments_v13_9.png b/doc/user/discussions/img/confidential_comments_v13_9.png Binary files differdeleted file mode 100644 index b5be5a622a9..00000000000 --- a/doc/user/discussions/img/confidential_comments_v13_9.png +++ /dev/null diff --git a/doc/user/discussions/img/confidential_comments_v15_0.png b/doc/user/discussions/img/confidential_comments_v15_0.png Binary files differnew file mode 100644 index 00000000000..36b7b466b4e --- /dev/null +++ b/doc/user/discussions/img/confidential_comments_v15_0.png diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index 8537856ef25..1cce69415d7 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -161,7 +161,7 @@ ask an administrator to [enable the feature flag](../../administration/feature_f On GitLab.com, this feature is not available. You should not use this feature for production environments. -You can make a comment **in an issue or an epic** confidential, so that it is visible only to you (the commenting user) and +You can make a comment **in an issue or an epic** confidential. It's then visible only to you (the commenting user) and the project members who have at least the Reporter role. Keep in mind: @@ -183,7 +183,7 @@ To mark a comment as confidential: 1. Below the comment, select the **Make this comment confidential** checkbox. 1. Select **Comment**. -![Confidential comments](img/confidential_comments_v13_9.png) +![Confidential comments](img/confidential_comments_v15_0.png) You can also make an [entire issue confidential](../project/issues/confidential_issues.md). diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 960a9309966..7cbb0c08720 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -456,18 +456,17 @@ To restore a group that is marked for deletion: ## Prevent group sharing outside the group hierarchy -This setting is only available on top-level groups. It affects all subgroups and projects. +You can configure a top-level group so its subgroups and projects +cannot invite other groups outside of the top-level group's hierarchy. +This option is only available for top-level groups. -When checked, any group in the top-level group hierarchy can only invite other groups from within the top-level -group's hierarchy. - -For example, with this setup: +For example, in the following group and project hierarchy: - **Animals > Dogs > Dog Project** - **Animals > Cats** - **Plants > Trees** -If you select this setting in the **Animals** group: +If you prevent group sharing outside the hierarchy for the **Animals** group: - **Dogs** can invite the group **Cats**. - **Dogs** cannot invite the group **Trees**. @@ -476,8 +475,9 @@ If you select this setting in the **Animals** group: To prevent sharing outside of the group's hierarchy: -1. Go to the group's **Settings > General** page. -1. Expand the **Permissions and group features** section. +1. On the top bar, select **Menu > Groups** and find your group. +1. On the left sidebar, select **Settings > General**. +1. Expand **Permissions and group features**. 1. Select **Prevent members from sending invitations to groups outside of `<group_name>` and its subgroups**. 1. Select **Save changes**. diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md index 46dca48d3b8..09e71ce9133 100644 --- a/doc/user/profile/personal_access_tokens.md +++ b/doc/user/profile/personal_access_tokens.md @@ -110,8 +110,6 @@ Personal access tokens expire on the date you define, at midnight UTC. - GitLab runs a check at 02:00 AM UTC every day to identify personal access tokens that expire on the current date. The owners of these tokens are notified by email. - In GitLab Ultimate, administrators can [limit the lifetime of access tokens](../admin_area/settings/account_and_limit_settings.md#limit-the-lifetime-of-access-tokens). -- In GitLab Ultimate, administrators can choose whether or not to - [enforce access token expiration](../admin_area/settings/account_and_limit_settings.md#allow-expired-access-tokens-to-be-used-deprecated). ## Create a personal access token programmatically **(FREE SELF)** diff --git a/doc/user/project/integrations/gitlab_slack_application.md b/doc/user/project/integrations/gitlab_slack_application.md index 2dae02dc093..dc56c2669f8 100644 --- a/doc/user/project/integrations/gitlab_slack_application.md +++ b/doc/user/project/integrations/gitlab_slack_application.md @@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w NOTE: The GitLab Slack application is only configurable for GitLab.com. It will **not** work for on-premises installations where you can configure the -[Slack slash commands](slack_slash_commands.md) service instead. We're planning +[Slack slash commands](slack_slash_commands.md) integration instead. We're planning to make this configurable for all GitLab installations, but there's no ETA - see [#28164](https://gitlab.com/gitlab-org/gitlab/-/issues/28164). @@ -31,17 +31,21 @@ Alternatively, you can configure the Slack application with a project's integration settings. Keep in mind that you must have the appropriate permissions for your Slack -team to be able to install a new application, read more in Slack's +workspace to be able to install a new application. Read more in Slack's documentation on [Adding an app to your workspace](https://slack.com/help/articles/202035138-Add-apps-to-your-Slack-workspace). -To enable the GitLab service for your Slack team: +To enable the GitLab integration for your Slack workspace: 1. Go to your project's **Settings > Integration > Slack application** (only visible on GitLab.com). -1. Select **Add to Slack**. +1. Select **Install Slack app**. +1. Select **Allow** on Slack's confirmation screen. That's all! You can now start using the Slack slash commands. +You can also select **Reinstall Slack app** to update the app in your Slack workspace +to the latest version. See the [Version history](#version-history) for details. + ## Create a project alias for Slack To create a project alias on GitLab.com for Slack integration: @@ -62,7 +66,7 @@ GitLab error: project or alias not found ## Usage -After confirming the installation, you, and everyone else in your Slack team, +After confirming the installation, you, and everyone else in your Slack workspace, can use all the [slash commands](../../../integration/slash_commands.md). When you perform your first slash command, you are asked to authorize your @@ -78,3 +82,11 @@ project, you would do: ```plaintext /gitlab gitlab-org/gitlab issue show 1001 ``` + +## Version history + +### 15.0+ + +In GitLab 15.0 the Slack app is updated to [Slack's new granular permissions app model](https://medium.com/slack-developer-blog/more-precision-less-restrictions-a3550006f9c3). + +There is no change in functionality. A reinstall is not required but recommended. diff --git a/doc/user/project/static_site_editor/img/edit_this_page_button_v12_10.png b/doc/user/project/static_site_editor/img/edit_this_page_button_v12_10.png Binary files differdeleted file mode 100644 index 380d96f1db9..00000000000 --- a/doc/user/project/static_site_editor/img/edit_this_page_button_v12_10.png +++ /dev/null diff --git a/doc/user/project/static_site_editor/img/front_matter_ui_v13_4.png b/doc/user/project/static_site_editor/img/front_matter_ui_v13_4.png Binary files differdeleted file mode 100644 index 89864858ed3..00000000000 --- a/doc/user/project/static_site_editor/img/front_matter_ui_v13_4.png +++ /dev/null diff --git a/doc/user/project/static_site_editor/img/wysiwyg_editor_v13_3.png b/doc/user/project/static_site_editor/img/wysiwyg_editor_v13_3.png Binary files differdeleted file mode 100644 index 52776c6a290..00000000000 --- a/doc/user/project/static_site_editor/img/wysiwyg_editor_v13_3.png +++ /dev/null diff --git a/doc/user/project/static_site_editor/index.md b/doc/user/project/static_site_editor/index.md index 220623d0372..343482757f5 100644 --- a/doc/user/project/static_site_editor/index.md +++ b/doc/user/project/static_site_editor/index.md @@ -2,35 +2,15 @@ stage: Create group: Editor 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" -type: reference, how-to -description: "The static site editor enables users to edit content on static websites without prior knowledge of the underlying templating language, site architecture or Git commands." +remove_date: '2022-08-03' +redirect_to: '../web_ide/index.md' --- -# Static Site Editor **(FREE)** +# Static Site Editor (removed) **(FREE)** -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28758) in GitLab 12.10. -> - WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214559) in GitLab 13.0. -> - Non-Markdown content blocks not editable on the WYSIWYG mode [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216836) in GitLab 13.3. -> - Formatting Markdown [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49052) in GitLab 13.7. -> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77246) in GitLab 14.7. - -WARNING: -This feature is in its end-of-life process. It is -[deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77246) -in GitLab 14.7, and is planned for -[removal](https://gitlab.com/groups/gitlab-org/-/epics/7351) in GitLab 14.10. -Users should instead use the [Web Editor](../repository/web_editor.md) or [Web IDE](../web_ide/index.md). [Removal instructions](#remove-the-static-site-editor) for existing projects are included on this page. - -Static Site Editor (SSE) enables users to edit content on static websites without -prior knowledge of the underlying templating language, site architecture, or -Git commands. A contributor to your project can quickly edit a Markdown page -and submit the changes for review. For example: - -- Non-technical collaborators can edit a page directly from the browser. - They don't need to know Git and the details of your project to contribute. -- Recently hired team members can quickly edit content. -- Temporary collaborators can jump from project to project and quickly edit pages instead - of having to clone or fork every single project they need to submit changes to. +This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77246) in GitLab 14.7 +and [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/352505) in 15.0. +Use the [Web Editor](../repository/web_editor.md) or [Web IDE](../web_ide/index.md) instead. ## Remove the Static Site Editor @@ -68,235 +48,3 @@ from an existing project, remove links that point back to the editor: `/helpers/custom_helpers.rb` entirely. 1. Clean up any extraneous configuration files. 1. Commit and push your changes. - -## Requirements - -- In order use the Static Site Editor feature, your project needs to be - pre-configured with the [Static Site Editor Middleman template](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman). -- You need to be logged into GitLab and be a member of the - project (with Developer or higher permission levels). - -## How it works - -The Static Site Editor is in an early stage of development and only supports -Middleman sites for now. You have to use a specific site template to start -using it. The project template is configured to deploy a [Middleman](https://middlemanapp.com/) -static website with [GitLab Pages](../pages/index.md). - -Once your website is up and running, an **Edit this page** button displays on -the bottom-left corner of its pages: - -![Edit this page button](img/edit_this_page_button_v12_10.png) - -When you click it, GitLab opens up an editor window from which the content -can be directly edited. When you're ready, you can submit your changes in a -click of a button: - -![Static Site Editor](img/wysiwyg_editor_v13_3.png) - -When an editor submits their changes, these are the following actions that GitLab -performs automatically in the background: - -1. Creates a new branch. -1. Commits their changes. - 1. Fixes formatting according to the [Handbook Markdown Style Guide](https://about.gitlab.com/handbook/markdown-guide/) - style guide and add them through another commit. -1. Opens a merge request against the default branch. - -The editor can then navigate to the merge request to assign it to a colleague for review. - -## Set up your project - -First, set up the project. Once done, you can use the Static Site Editor to -[edit your content](#edit-content). - -1. To get started, create a new project from the [Static Site Editor - Middleman](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman) - template. You can either [fork it](../repository/forking_workflow.md#creating-a-fork) - or [create a new project from a template](../working_with_projects.md#create-a-project-from-a-built-in-template). -1. Edit the [`data/config.yml`](#static-site-generator-configuration) configuration file - to replace `<username>` and `<project-name>` with the proper values for - your project's path. -1. Optional. Edit the [`.gitlab/static-site-editor.yml`](#static-site-editor-configuration-file) file - to customize the behavior of the Static Site Editor. -1. When you submit your changes, GitLab triggers a CI/CD pipeline to deploy your project with GitLab Pages. -1. When the pipeline finishes, from your project's left-side menu, go to **Settings > Pages** to find the URL of your new website. -1. Visit your website and look at the bottom-left corner of the screen to see the new **Edit this page** button. - -Anyone satisfying the [requirements](#requirements) can edit the -content of the pages without prior knowledge of Git or of your site's -codebase. - -## Edit content - -> - Support for modifying the default merge request title and description [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216861) in GitLab 13.5. -> - Support for selecting a merge request template [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/263252) in GitLab 13.6. - -After setting up your project, you can start editing content directly from the Static Site Editor. - -To edit a file: - -1. Visit the page you want to edit. -1. Select **Edit this page**. -1. The file is opened in the Static Site Editor in **WYSIWYG** mode. If you - wish to edit the raw Markdown instead, you can toggle the **Markdown** mode - in the bottom-right corner. -1. When you're done, click **Submit changes...**. -1. Optional. Adjust the default title and description of the merge request, to submit - with your changes. Alternatively, select a [merge request template](../../../user/project/description_templates.md#create-a-merge-request-template) - from the dropdown menu and edit it accordingly. -1. Select **Submit changes**. -1. A new merge request is automatically created and you can assign a colleague for review. - -### Text - -> Support for `*.md.erb` files [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223171) in GitLab 13.2. - -The Static Site Editors supports Markdown files (`.md`, `.md.erb`) for editing text. - -### Images - -> - Support for adding images through the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216640) in GitLab 13.1. -> - Support for uploading images via the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218529) in GitLab 13.6. - -#### Upload an image - -You can upload image files via the WYSIWYG editor directly to the repository to default upload directory -`source/images`. To do so: - -1. Select the image icon (**{doc-image}**). -1. Select the **Upload file** tab. -1. To select a file from your computer, select **Choose file**. -1. Optional. Add a description to the image for SEO and accessibility ([ALT text](https://moz.com/learn/seo/alt-text)). -1. Select **Insert image**. - -The selected file can be any supported image file (`.png`, `.jpg`, `.jpeg`, `.gif`). The editor renders -thumbnail previews so you can verify the correct image is included and there aren't any references to -missing images. - -#### Link to an image - -You can also link to an image if you'd like: - -1. Select the image icon (**{doc-image}**). -1. Select the **Link to an image** tab. -1. Add the link to the image into the **Image URL** field (use the full path; relative paths are not supported yet). -1. Optional. Add a description to the image for SEO and accessibility ([ALT text](https://moz.com/learn/seo/alt-text)). -1. Select **Insert image**. - -The link can reference images already hosted in your project, an asset hosted -externally on a content delivery network, or any other external URL. The editor renders thumbnail previews -so you can verify the correct image is included and there aren't any references to missing images. - -### Videos - -> - Support for embedding YouTube videos through the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216642) in GitLab 13.5. - -You can embed YouTube videos on the WYSIWYG mode by clicking the video icon (**{live-preview}**). -The following URL/ID formats are supported: - -- **YouTube watch URLs**: `https://www.youtube.com/watch?v=0t1DgySidms` -- **YouTube embed URLs**: `https://www.youtube.com/embed/0t1DgySidms` -- **YouTube video IDs**: `0t1DgySidms` - -### Front matter - -> - Markdown front matter hidden on the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216834) in GitLab 13.1. -> - Ability to edit page front matter [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/235921) in GitLab 13.5. - -Front matter is a flexible and convenient way to define page-specific variables in data files -intended to be parsed by a static site generator. Use it to set a page's -title, layout template, or author. You can also pass any kind of metadata to the -generator as the page renders out to HTML. Included at the very top of each data file, the -front matter is often formatted as YAML or JSON, and requires consistent and accurate syntax. - -To edit the front matter from the Static Site Editor you can use the GitLab regular file editor, -the Web IDE, or update the data directly from the WYSIWYG editor: - -1. Click the **Page settings** button on the bottom-right to reveal a web form with the data you - have on the page's front matter. The form is populated with the current data: - - ![Editing page front matter in the Static Site Editor](img/front_matter_ui_v13_4.png) - -1. Update the values as you wish and close the panel. -1. When you're done, click **Submit changes...**. -1. Describe your changes (add a commit message). -1. Click **Submit changes**. -1. Click **View merge request** to view it. - -Adding new attributes to the page's front matter from the form is not supported. -To add new attributes: - -- Edit the file locally -- Edit the file with the GitLab regular file editor. -- Edit the file with the Web IDE. - -After adding an attribute, the form loads the new fields. - -## Configuration files - -You can customize the behavior of a project which uses the Static Site Editor with -the following configuration files: - -- The [`.gitlab/static-site-editor.yml`](#static-site-editor-configuration-file), which customizes the - behavior of the Static Site Editor. -- [Static Site Generator configuration files](#static-site-generator-configuration), - such as `data/config.yml`, which configures the Static Site Generator itself. - It also controls the **Edit this page** button when the site is generated. - -### Static Site Editor configuration file - -> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4267) in GitLab 13.6. - -The `.gitlab/static-site-editor.yml` configuration file contains entries you can -use to customize behavior of the Static Site Editor (SSE). If the file does not exist, -default values which support a default Middleman project configuration are used. -The [Static Site Editor - Middleman](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman) project template generates a file pre-populated with these defaults. - -To customize the behavior of the SSE, edit `.gitlab/static-site-editor.yml`'s entries, -according to your project's needs. Make sure to respect YAML syntax. - -After the table, see an [example of the SSE configuration file](#gitlabstatic-site-editoryml-example). - -| Entry | GitLab version | Type | Default value | Description | -|---|---|---|---|---| -| `image_upload_path` | [13.6](https://gitlab.com/gitlab-org/gitlab/-/issues/216641) | String | `source/images` | Directory for images uploaded from the WYSIWYG editor. | - -#### `.gitlab/static-site-editor.yml` example - -```yaml -image_upload_path: 'source/images' # Relative path to the project's root. Don't include leading or trailing slashes. -``` - -### Static Site Generator configuration - -The Static Site Editor uses Middleman's configuration file, `data/config.yml` -to customize the behavior of the project itself. This file also controls the -**Edit this page** button, rendered through the file -[`layout.erb`](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman/-/blob/master/source/layouts/layout.erb). - -To [configure the project template to your own project](#set-up-your-project), -you must replace the `<username>` and `<project-name>` in the `data/config.yml` -file with the proper values for your project's path. - -[Other Static Site Generators](#using-other-static-site-generators) used with -the Static Site Editor may use different configuration files or approaches. - -## Using Other Static Site Generators - -Although Middleman is the only Static Site Generator officially supported -by the Static Site Editor, you can configure your project's build and deployment -to use a different Static Site Generator. In this case, use the Middleman layout -as an example, and follow a similar approach to properly render an **Edit this page** -button in your Static Site Generator's layout. - -## Upgrade from GitLab 12.10 to 13.0 - -In GitLab 13.0, we [introduced breaking changes](https://gitlab.com/gitlab-org/gitlab/-/issues/213282) -to the URL structure of the Static Site Editor. Follow the instructions in this -[snippet](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman/snippets/1976539) -to update your project with the 13.0 changes. - -## Limitations - -- The Static Site Editor still cannot be quickly added to existing Middleman sites. - Follow this [epic](https://gitlab.com/groups/gitlab-org/-/epics/2784) for updates. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 0cdece3990d..b1824e17c03 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -395,11 +395,6 @@ msgid_plural "%d tags per image name" msgstr[0] "" msgstr[1] "" -msgid "%d token has expired" -msgid_plural "%d tokens have expired" -msgstr[0] "" -msgstr[1] "" - msgid "%d unassigned issue" msgid_plural "%d unassigned issues" msgstr[0] "" @@ -3575,9 +3570,6 @@ msgstr "" msgid "AlertSettings|You can map default GitLab alert fields to your payload keys in the dropdowns below." msgstr "" -msgid "AlertSettings|You can now set up alert endpoints for manually configured Prometheus instances in the Alerts section on the Operations settings page. Alert endpoint fields on this page have been deprecated." -msgstr "" - msgid "AlertSettings|{ \"events\": [{ \"application\": \"Name of application\" }] }" msgstr "" @@ -5140,10 +5132,10 @@ msgstr "" msgid "At least one of group_id or project_id must be specified" msgstr "" -msgid "At least one of your Personal Access Tokens is expired, but expiration enforcement is disabled. %{generate_new}" +msgid "At least one of your Personal Access Tokens is expired. %{generate_new}" msgstr "" -msgid "At least one of your Personal Access Tokens will expire soon, but expiration enforcement is disabled. %{generate_new}" +msgid "At least one of your Personal Access Tokens will expire soon. %{generate_new}" msgstr "" msgid "At risk" @@ -5355,9 +5347,6 @@ msgstr "" msgid "Authorization code:" msgstr "" -msgid "Authorization key" -msgstr "" - msgid "Authorization required" msgstr "" @@ -9966,6 +9955,9 @@ msgstr "" msgid "Copy URL" msgstr "" +msgid "Copy audio URL" +msgstr "" + msgid "Copy branch name" msgstr "" @@ -9999,6 +9991,9 @@ msgstr "" msgid "Copy file path" msgstr "" +msgid "Copy image URL" +msgstr "" + msgid "Copy issue URL to clipboard" msgstr "" @@ -10047,6 +10042,9 @@ msgstr "" msgid "Copy value" msgstr "" +msgid "Copy video URL" +msgstr "" + msgid "Corpus Management" msgstr "" @@ -11817,6 +11815,9 @@ msgstr "" msgid "Delete artifacts" msgstr "" +msgid "Delete audio" +msgstr "" + msgid "Delete badge" msgstr "" @@ -11838,6 +11839,9 @@ msgstr "" msgid "Delete file" msgstr "" +msgid "Delete image" +msgstr "" + msgid "Delete image repository" msgstr "" @@ -11892,6 +11896,9 @@ msgstr "" msgid "Delete variable" msgstr "" +msgid "Delete video" +msgstr "" + msgid "DeleteProject|Failed to remove events. Please try again or contact administrator." msgstr "" @@ -12533,6 +12540,9 @@ msgstr "" msgid "Description" msgstr "" +msgid "Description (alt text)" +msgstr "" + msgid "Description (optional)" msgstr "" @@ -13440,6 +13450,9 @@ msgstr "" msgid "Edit application" msgstr "" +msgid "Edit audio description" +msgstr "" + msgid "Edit comment" msgstr "" @@ -13476,6 +13489,9 @@ msgstr "" msgid "Edit identity for %{user_name}" msgstr "" +msgid "Edit image description" +msgstr "" + msgid "Edit in pipeline editor" msgstr "" @@ -13518,6 +13534,9 @@ msgstr "" msgid "Edit user: %{user_name}" msgstr "" +msgid "Edit video description" +msgstr "" + msgid "Edit wiki page" msgstr "" @@ -13935,9 +13954,6 @@ msgstr "" msgid "Enforce SSH key expiration" msgstr "" -msgid "Enforce access token expiration" -msgstr "" - msgid "Enforce two-factor authentication" msgstr "" @@ -15427,9 +15443,6 @@ msgstr "" msgid "Failed to request attention because no user was found." msgstr "" -msgid "Failed to reset key. Please try again." -msgstr "" - msgid "Failed to retrieve page" msgstr "" @@ -16212,15 +16225,9 @@ msgstr "" msgid "Generate group access tokens scoped to this group for your applications that need access to the GitLab API." msgstr "" -msgid "Generate key" -msgstr "" - msgid "Generate new export" msgstr "" -msgid "Generate new token" -msgstr "" - msgid "Generate project access tokens scoped to this project for your applications that need access to the GitLab API." msgstr "" @@ -22734,7 +22741,7 @@ msgstr "" msgid "List of suitable GCP locations" msgstr "" -msgid "List of users allowed to exceed the rate limit." +msgid "List of users who are allowed to exceed the rate limit. Example: username1, username2" msgstr "" msgid "List options" @@ -27278,9 +27285,6 @@ msgstr "" msgid "Personal Access Token prefix" msgstr "" -msgid "Personal access tokens are not revoked upon expiration." -msgstr "" - msgid "Personal project creation is not allowed. Please contact your administrator with questions" msgstr "" @@ -30866,9 +30870,6 @@ msgstr "" msgid "Receive a %{strongOpen}$50 gift card%{strongClose} as a thank you for your time." msgstr "" -msgid "Receive alerts from manually configured Prometheus servers." -msgstr "" - msgid "Receive any notifications from GitLab." msgstr "" @@ -31497,9 +31498,18 @@ msgstr "" msgid "Replace all label(s)" msgstr "" +msgid "Replace audio" +msgstr "" + msgid "Replace file" msgstr "" +msgid "Replace image" +msgstr "" + +msgid "Replace video" +msgstr "" + msgid "Replaced all labels with %{label_references} %{label_text}." msgstr "" @@ -31979,12 +31989,6 @@ msgstr "" msgid "Reset" msgstr "" -msgid "Reset authorization key" -msgstr "" - -msgid "Reset authorization key?" -msgstr "" - msgid "Reset file" msgstr "" @@ -31994,9 +31998,6 @@ msgstr "" msgid "Reset health check access token" msgstr "" -msgid "Reset key" -msgstr "" - msgid "Reset link will be generated and sent to the user. %{break} User will be forced to set the password on first sign in." msgstr "" @@ -32012,9 +32013,6 @@ msgstr "" msgid "Reset to project defaults" msgstr "" -msgid "Resetting the authorization key will invalidate the previous key. Existing alert configurations will need to be updated with the new key." -msgstr "" - msgid "Resolve" msgstr "" @@ -34062,9 +34060,6 @@ msgstr "" msgid "See the affected projects in the GitLab admin panel" msgstr "" -msgid "See the list of available commands in Slack after setting up this service by entering" -msgstr "" - msgid "See vulnerability %{vulnerability_link} for any Remediation details." msgstr "" @@ -35059,12 +35054,21 @@ msgstr "" msgid "SlackIntegration|GitLab for Slack was successfully installed." msgstr "" +msgid "SlackIntegration|Install Slack app" +msgstr "" + msgid "SlackIntegration|Project alias" msgstr "" +msgid "SlackIntegration|Reinstall Slack app" +msgstr "" + msgid "SlackIntegration|Remove project" msgstr "" +msgid "SlackIntegration|See the list of available commands in Slack after setting up this integration by entering" +msgstr "" + msgid "SlackIntegration|Select a GitLab project to link with your Slack workspace." msgstr "" @@ -35074,12 +35078,15 @@ msgstr "" msgid "SlackIntegration|Team name" msgstr "" -msgid "SlackIntegration|To set up this integration press \"Add to Slack\"" +msgid "SlackIntegration|This integration allows users to perform common operations on this project by entering slash commands in Slack." msgstr "" msgid "SlackIntegration|You can now close this window and go to your Slack workspace." msgstr "" +msgid "SlackIntegration|You may need to reinstall the Slack application when we %{linkStart}make updates or change permissions%{linkEnd}." +msgstr "" + msgid "SlackService|1. %{slash_command_link_start}Add a slash command%{slash_command_link_end} in your Slack team using this information:" msgstr "" @@ -38731,9 +38738,6 @@ msgstr "" msgid "This runner will only run on pipelines triggered on protected branches" msgstr "" -msgid "This service allows users to perform common operations on this project by entering slash commands in Slack." -msgstr "" - msgid "This setting can be overridden in each project." msgstr "" @@ -39275,9 +39279,6 @@ msgstr "" msgid "To reactivate your account, sign in to GitLab at %{gitlab_url}." msgstr "" -msgid "To receive alerts from manually configured Prometheus services, add the following URL and Authorization key to your Prometheus webhook config file. Learn more about %{linkStart}configuring Prometheus%{linkEnd} to send alerts to GitLab." -msgstr "" - msgid "To renew, export your license usage file and email it to %{renewal_service_email}. A new license will be emailed to the email address registered in the %{customers_dot}. You can add this license to your instance." msgstr "" @@ -39443,9 +39444,6 @@ msgstr "" msgid "Token name" msgstr "" -msgid "Token valid until revoked" -msgstr "" - msgid "Tokens|Scopes set the permission levels granted to the token." msgstr "" @@ -40187,9 +40185,6 @@ msgstr "" msgid "Unsupported todo type passed. Supported todo types are: %{todo_types}" msgstr "" -msgid "Until revoked, expired personal access tokens pose a security risk." -msgstr "" - msgid "Unused" msgstr "" @@ -41546,9 +41541,6 @@ msgstr "" msgid "VisibilityLevel|Unknown" msgstr "" -msgid "Visit settings page" -msgstr "" - msgid "Visual Studio Code (HTTPS)" msgstr "" diff --git a/qa/qa/specs/features/api/3_create/gitaly/praefect_replication_queue_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/praefect_replication_queue_spec.rb index d066953d12e..b6296b5a263 100644 --- a/qa/qa/specs/features/api/3_create/gitaly/praefect_replication_queue_spec.rb +++ b/qa/qa/specs/features/api/3_create/gitaly/praefect_replication_queue_spec.rb @@ -3,7 +3,10 @@ require 'parallel' module QA - RSpec.describe 'Create' do + RSpec.describe 'Create', quarantine: { + issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/361382', + type: :investigating + } do context 'Gitaly Cluster replication queue', :orchestrated, :gitaly_cluster, :skip_live_env do let(:praefect_manager) { Service::PraefectManager.new } let(:project) do diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index 04f73050ea5..c7ab509bf59 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -79,7 +79,7 @@ RSpec.describe 'Database schema' do repository_languages: %w[programming_language_id], routes: %w[source_id], sent_notifications: %w[project_id noteable_id recipient_id commit_id in_reply_to_discussion_id], - slack_integrations: %w[team_id user_id], + slack_integrations: %w[team_id user_id bot_user_id], # these are external Slack IDs snippets: %w[author_id], spam_logs: %w[user_id], status_check_responses: %w[external_approval_rule_id], diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb index 507d427bf0b..b87ac743d02 100644 --- a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb +++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb @@ -76,14 +76,12 @@ RSpec.describe 'Resolving all open threads in a merge request from an issue', :j end it 'has a link to resolve all threads by creating an issue' do - page.within '.mr-widget-body' do - expect(page).to have_link 'Create issue to resolve all threads', href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid) - end + expect(page).to have_link 'Create issue to resolve all threads', href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid) end context 'creating an issue for threads' do before do - page.within '.mr-widget-body' do + page.within '.mr-state-widget' do page.click_link 'Create issue to resolve all threads', href: new_project_issue_path(project, merge_request_to_resolve_discussions_of: merge_request.iid) wait_for_all_requests diff --git a/spec/features/merge_request/user_accepts_merge_request_spec.rb b/spec/features/merge_request/user_accepts_merge_request_spec.rb index d4b185a82e9..a680ec78b2f 100644 --- a/spec/features/merge_request/user_accepts_merge_request_spec.rb +++ b/spec/features/merge_request/user_accepts_merge_request_spec.rb @@ -18,7 +18,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli click_button('Merge') - expect(page).to have_content("The changes were merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}") + expect(page).to have_content("Changes merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}") end context 'when merge method is set to fast-forward merge' do @@ -31,7 +31,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli click_button('Merge') - expect(page).to have_content("The changes were merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}") + expect(page).to have_content("Changes merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}") end it 'accepts a merge request with squash and merge' do @@ -41,7 +41,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli click_button('Merge') - expect(page).to have_content("The changes were merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}") + expect(page).to have_content("Changes merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}") end end end @@ -55,7 +55,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli check('Delete source branch') click_button('Merge') - expect(page).to have_content('The changes were merged into') + expect(page).to have_content('Changes merged into') expect(page).not_to have_selector('.js-remove-branch-button') # Wait for View Resource requests to complete so they don't blow up if they are @@ -72,7 +72,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli it 'accepts a merge request' do click_button('Merge') - expect(page).to have_content('The changes were merged into') + expect(page).to have_content('Changes merged into') expect(page).to have_selector('.js-remove-branch-button') # Wait for View Resource requests to complete so they don't blow up if they are @@ -90,7 +90,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli check('Delete source branch') click_button('Merge') - expect(page).to have_content('The changes were merged into') + expect(page).to have_content('Changes merged into') expect(page).not_to have_selector('.js-remove-branch-button') # Wait for View Resource requests to complete so they don't blow up if they are @@ -107,7 +107,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli end it 'accepts a merge request' do - find('.js-mr-widget-commits-count').click + find('[data-testid="widget_edit_commit_message"]').click fill_in('merge-message-edit', with: 'wow such merge') click_button('Merge') diff --git a/spec/features/merge_request/user_assigns_themselves_spec.rb b/spec/features/merge_request/user_assigns_themselves_spec.rb index fc925781a3b..2aaddc7791b 100644 --- a/spec/features/merge_request/user_assigns_themselves_spec.rb +++ b/spec/features/merge_request/user_assigns_themselves_spec.rb @@ -30,12 +30,6 @@ RSpec.describe 'Merge request > User assigns themselves' do end.to change { merge_request.reload.updated_at } end - it 'returns user to the merge request', :js do - click_link 'Assign yourself to these issues' - - expect(page).to have_content merge_request.description - end - context 'when related issues are already assigned' do before do [issue1, issue2].each { |issue| issue.update!(assignees: [user]) } diff --git a/spec/features/merge_request/user_awards_emoji_spec.rb b/spec/features/merge_request/user_awards_emoji_spec.rb index 35eadb34799..81a88cad458 100644 --- a/spec/features/merge_request/user_awards_emoji_spec.rb +++ b/spec/features/merge_request/user_awards_emoji_spec.rb @@ -38,6 +38,10 @@ RSpec.describe 'Merge request > User awards emoji', :js do it 'adds awards to note' do page.within('.note-actions') do first('.note-emoji-button').click + + # make sure emoji popup is visible + execute_script("window.scrollBy(0, 200)") + find('gl-emoji[data-name="8ball"]').click end diff --git a/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb b/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb index 67a232607cd..059e1eb89c5 100644 --- a/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb +++ b/spec/features/merge_request/user_customizes_merge_commit_message_spec.rb @@ -41,7 +41,7 @@ RSpec.describe 'Merge request < User customizes merge commit message', :js do it 'has commit message without description' do expect(page).not_to have_selector('#merge-message-edit') - first('.js-mr-widget-commits-count').click + find('[data-testid="widget_edit_commit_message"]').click expect(merge_textbox).to be_visible expect(merge_textbox.value).to eq(default_merge_commit_message) end @@ -51,7 +51,7 @@ RSpec.describe 'Merge request < User customizes merge commit message', :js do it 'uses merge commit template' do expect(page).not_to have_selector('#merge-message-edit') - first('.js-mr-widget-commits-count').click + find('[data-testid="widget_edit_commit_message"]').click expect(merge_textbox).to be_visible expect(merge_textbox.value).to eq(merge_request.title) end @@ -62,7 +62,7 @@ RSpec.describe 'Merge request < User customizes merge commit message', :js do it 'has default message with merge request title' do expect(page).not_to have_selector('#squash-message-edit') - first('.js-mr-widget-commits-count').click + find('[data-testid="widget_edit_commit_message"]').click expect(squash_textbox).to be_visible expect(merge_textbox).to be_visible expect(squash_textbox.value).to eq(merge_request.title) @@ -74,7 +74,7 @@ RSpec.describe 'Merge request < User customizes merge commit message', :js do it 'uses squash commit template' do expect(page).not_to have_selector('#squash-message-edit') - first('.js-mr-widget-commits-count').click + find('[data-testid="widget_edit_commit_message"]').click expect(squash_textbox).to be_visible expect(merge_textbox).to be_visible expect(squash_textbox.value).to eq(merge_request.description) diff --git a/spec/features/merge_request/user_merges_immediately_spec.rb b/spec/features/merge_request/user_merges_immediately_spec.rb index 3a05f35a671..91327059e0f 100644 --- a/spec/features/merge_request/user_merges_immediately_spec.rb +++ b/spec/features/merge_request/user_merges_immediately_spec.rb @@ -30,17 +30,17 @@ RSpec.describe 'Merge requests > User merges immediately', :js do it 'enables merge immediately' do wait_for_requests - page.within '.mr-widget-body' do + page.within '[data-testid="ready_to_merge_state"]' do find('.dropdown-toggle').click Sidekiq::Testing.fake! do click_button 'Merge immediately' - - expect(find('.media-body h4')).to have_content('Merging!') - - wait_for_requests end end + + expect(find('.media-body h4')).to have_content('Merging!') + + wait_for_requests end end end diff --git a/spec/features/merge_request/user_merges_merge_request_spec.rb b/spec/features/merge_request/user_merges_merge_request_spec.rb index a861ca2eea5..12518d634ec 100644 --- a/spec/features/merge_request/user_merges_merge_request_spec.rb +++ b/spec/features/merge_request/user_merges_merge_request_spec.rb @@ -27,6 +27,7 @@ RSpec.describe "User merges a merge request", :js do let(:project) { create(:project, :public, :repository, merge_requests_ff_only_enabled: true) } before do + stub_feature_flags(restructured_mr_widget: false) visit(merge_request_path(merge_request)) end diff --git a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb index 4d7ee11e366..d6b132b18da 100644 --- a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb +++ b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb @@ -56,7 +56,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js do wait_for_requests - expect(page).to have_css('button[disabled="disabled"]', text: 'Merge') + expect(page).not_to have_button('Merge') expect(page).to have_content('Merge blocked: pipeline must succeed. Push a commit that fixes the failure, or learn about other solutions.') end end diff --git a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb index 9057b96bff0..21f96299958 100644 --- a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb +++ b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb @@ -36,7 +36,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do click_button "Merge when pipeline succeeds" expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds" - expect(page).to have_content "Does not delete the source branch" + expect(page).to have_content "Source branch will not be deleted" expect(page).to have_selector ".js-cancel-auto-merge" visit project_merge_request_path(project, merge_request) # Needed to refresh the page expect(page).to have_content /enabled an automatic merge when the pipeline for \h{8} succeeds/i @@ -64,6 +64,9 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do context 'when enabled after it was previously canceled' do before do click_button "Merge when pipeline succeeds" + + wait_for_requests + click_button "Cancel auto-merge" wait_for_requests @@ -123,12 +126,6 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do expect(page).to have_content "canceled the automatic merge" end - it 'allows to delete source branch' do - click_button "Delete source branch" - - expect(page).to have_content "Deletes the source branch" - end - context 'when pipeline succeeds' do before do build.success @@ -136,7 +133,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do end it 'merges merge request', :sidekiq_might_not_need_inline do - expect(page).to have_content 'The changes were merged' + expect(page).to have_content 'Changes merged' expect(merge_request.reload).to be_merged end end diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index 150eee87cb0..2dafd66b406 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -321,8 +321,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do # Wait for the `ci_status` and `merge_check` requests wait_for_requests - expect(page).to have_selector('.accept-merge-request') - expect(find('.accept-merge-request')['disabled']).not_to be(true) + expect(page).not_to have_selector('.accept-merge-request') end end @@ -385,9 +384,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do # Wait for the `ci_status` and `merge_check` requests wait_for_requests - page.within('.mr-widget-body') do - expect(page).to have_content('Merge Merge blocked: fast-forward merge is not possible. To merge this request, first rebase locally.') - end + expect(page).to have_content('Merge blocked: fast-forward merge is not possible. To merge this request, first rebase locally.') end end @@ -445,7 +442,6 @@ RSpec.describe 'Merge request > User sees merge widget', :js do it 'user cannot remove source branch', :sidekiq_might_not_need_inline do expect(page).not_to have_field('remove-source-branch-input') - expect(page).to have_content('Deletes the source branch') end end diff --git a/spec/features/merge_request/user_squashes_merge_request_spec.rb b/spec/features/merge_request/user_squashes_merge_request_spec.rb index 2a48657ac4f..da0d4ca23d1 100644 --- a/spec/features/merge_request/user_squashes_merge_request_spec.rb +++ b/spec/features/merge_request/user_squashes_merge_request_spec.rb @@ -79,7 +79,7 @@ RSpec.describe 'User squashes a merge request', :js do context 'when squash message is the same as existing commit message' do before do - click_button("Modify commit messages") + find('[data-testid="widget_edit_commit_message"]').click fill_in('Squash commit message', with: project.commit(source_branch).safe_message) accept_mr end diff --git a/spec/features/projects/integrations/prometheus_external_alerts_spec.rb b/spec/features/projects/integrations/prometheus_external_alerts_spec.rb deleted file mode 100644 index 7e56ca13e23..00000000000 --- a/spec/features/projects/integrations/prometheus_external_alerts_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Prometheus external alerts', :js do - include_context 'project integration activation' - - let(:alerts_section_selector) { '.js-prometheus-alerts' } - let(:alerts_section) { page.find(alerts_section_selector) } - - context 'with manual configuration' do - before do - create(:prometheus_integration, project: project, api_url: 'http://prometheus.example.com', manual_configuration: '1', active: true) - end - - it 'shows the Alerts section' do - visit_project_integration('Prometheus') - - expect(alerts_section).to have_content('Alerts') - expect(alerts_section).to have_content('Receive alerts from manually configured Prometheus servers.') - expect(alerts_section).to have_content('URL') - expect(alerts_section).to have_content('Authorization key') - end - end - - context 'with no configuration' do - it 'does not show the Alerts section' do - visit_project_integration('Prometheus') - wait_for_requests - - expect(page).not_to have_css(alerts_section_selector) - end - end -end diff --git a/spec/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder_spec.rb b/spec/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder_spec.rb new file mode 100644 index 00000000000..8cdfa13ba3a --- /dev/null +++ b/spec/finders/groups/projects_requiring_authorizations_refresh/on_direct_membership_finder_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Groups::ProjectsRequiringAuthorizationsRefresh::OnDirectMembershipFinder do + # rubocop:disable Layout/LineLength + + # Group X Group A ------shared with-------------> Group B Group C + # | Group X_subgroup_1 | | | + # | | Project X_subgroup_1 ---shared with----->| Group A_subgroup_1 | Group B_subgroup_1 <--shared with--------- | Group C_subgroup_1 + # | | | Project A_subgroup_1 | | Project B_subgroup_1 | | Project C_subgroup_1 + # | Group A_subgroup_2 | Group B_subgroup_2 <----shared with ------- Project C + # | |Project A_subgroup_2 | | Project B_subgroup_2 + + # rubocop:enable Layout/LineLength + + let_it_be(:group_x) { create(:group) } + let_it_be(:group_a) { create(:group) } + let_it_be(:group_b) { create(:group) } + let_it_be(:group_c) { create(:group) } + let_it_be(:group_x_subgroup_1) { create(:group, parent: group_x) } + let_it_be(:group_a_subgroup_1) { create(:group, parent: group_a) } + let_it_be(:group_a_subgroup_2) { create(:group, parent: group_a) } + let_it_be(:group_b_subgroup_1) { create(:group, parent: group_b) } + let_it_be(:group_b_subgroup_2) { create(:group, parent: group_b) } + let_it_be(:group_c_subgroup_1) { create(:group, parent: group_c) } + let_it_be(:project_x_subgroup_1) { create(:project, group: group_x_subgroup_1, name: 'project_x_subgroup_1') } + let_it_be(:project_a_subgroup_1) { create(:project, group: group_a_subgroup_1, name: 'project_a_subgroup_1') } + let_it_be(:project_a_subgroup_2) { create(:project, group: group_a_subgroup_2, name: 'project_a_subgroup_2') } + let_it_be(:project_b_subgroup_1) { create(:project, group: group_b_subgroup_1, name: 'project_b_subgroup_1') } + let_it_be(:project_b_subgroup_2) { create(:project, group: group_b_subgroup_2, name: 'project_b_subgroup_2') } + let_it_be(:project_c_subgroup_1) { create(:project, group: group_c_subgroup_1, name: 'project_c_subgroup_1') } + let_it_be(:project_c) { create(:project, group: group_c, name: 'project_c') } + + describe '#execute' do + context 'projects affected when a new member is added to a specific group (here, `Group B`)' do + subject(:result) { described_class.new(group_b).execute } + + before do + create(:project_group_link, project: project_x_subgroup_1, group: group_a_subgroup_1) + create(:project_group_link, project: project_c, group: group_b_subgroup_2) + create(:group_group_link, shared_group: group_a, shared_with_group: group_b) + create(:group_group_link, shared_group: group_c_subgroup_1, shared_with_group: group_b_subgroup_1) + end + + it 'returns all projects IDs where authorizations need to be created for the user'\ + 'due to their new membership being created in `Group B`' do + new_user = create(:user) + group_b.add_maintainer(new_user) + + expect(result).to match_array(new_user.authorized_projects.ids) + end + + it 'includes only the expected projects' do + expected_projects = Project.id_in( + [ + project_b_subgroup_1, # direct member of Group B gets access to this project due to group hierarchy + project_b_subgroup_2, # direct member of Group B gets access to this project due to group hierarchy + project_c, # direct member of Group B gets access to this project via project-group share + project_a_subgroup_1, # direct member of Group B gets access to this project via group share + project_a_subgroup_2, # direct member of Group B gets access to this project via group share + + # direct member of Group B gets access to any projects shared with groups within its shared groups. + project_x_subgroup_1 + ] + ) + # project_c_subgroup_1 is not included in the list because only 'direct' members of + # `group_b_subgroup_1` gets access to that project via the group-group share. + expect(result).to match_array(expected_projects.ids) + end + end + end +end diff --git a/spec/finders/personal_access_tokens_finder_spec.rb b/spec/finders/personal_access_tokens_finder_spec.rb index 7607d08dc64..f22bff62082 100644 --- a/spec/finders/personal_access_tokens_finder_spec.rb +++ b/spec/finders/personal_access_tokens_finder_spec.rb @@ -286,24 +286,6 @@ RSpec.describe PersonalAccessTokensFinder do end end - describe 'with active or expired state' do - before do - params[:state] = 'active_or_expired' - end - - it 'includes active tokens' do - is_expected.to include(active_personal_access_token, active_impersonation_token) - end - - it 'includes expired tokens' do - is_expected.to include(expired_personal_access_token, expired_impersonation_token) - end - - it 'does not include revoked tokens' do - is_expected.not_to include(revoked_personal_access_token, revoked_impersonation_token) - end - end - describe 'with id' do subject { finder(params).find_by_id(active_personal_access_token.id) } diff --git a/spec/frontend/content_editor/components/bubble_menus/link_spec.js b/spec/frontend/content_editor/components/bubble_menus/link_spec.js index 4d6796a9da4..5910b9c110d 100644 --- a/spec/frontend/content_editor/components/bubble_menus/link_spec.js +++ b/spec/frontend/content_editor/components/bubble_menus/link_spec.js @@ -1,6 +1,5 @@ -import { GlLink, GlForm, GlFormInput } from '@gitlab/ui'; +import { GlLink, GlForm } from '@gitlab/ui'; import { BubbleMenu } from '@tiptap/vue-2'; -import { nextTick } from 'vue'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import LinkBubbleMenu from '~/content_editor/components/bubble_menus/link.vue'; import eventHubFactory from '~/helpers/event_hub_factory'; @@ -12,11 +11,13 @@ const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jes describe('content_editor/components/bubble_menus/link', () => { let wrapper; let tiptapEditor; + let contentEditor; let bubbleMenu; let eventHub; const buildEditor = () => { tiptapEditor = createTestEditor({ extensions: [Link] }); + contentEditor = { resolveLink: jest.fn() }; eventHub = eventHubFactory(); }; @@ -24,6 +25,7 @@ describe('content_editor/components/bubble_menus/link', () => { wrapper = mountExtended(LinkBubbleMenu, { provide: { tiptapEditor, + contentEditor, eventHub, }, }); @@ -133,8 +135,8 @@ describe('content_editor/components/bubble_menus/link', () => { beforeEach(async () => { await wrapper.findByTestId('edit-link').vm.$emit('click'); - linkHrefInput = wrapper.findByTestId('link-href-group').findComponent(GlFormInput); - linkTitleInput = wrapper.findByTestId('link-title-group').findComponent(GlFormInput); + linkHrefInput = wrapper.findByTestId('link-href'); + linkTitleInput = wrapper.findByTestId('link-title'); }); it('hides the link and copy/edit/remove link buttons', async () => { @@ -160,11 +162,9 @@ describe('content_editor/components/bubble_menus/link', () => { tiptapEditor.commands.setTextSelection(3); await emitEditorEvent({ event: 'transaction', tiptapEditor }); - await nextTick(); - // tiptapEditor.commands.setTextSelection(14); - // await emitEditorEvent({ event: 'transaction', tiptapEditor }); - // await nextTick(); + tiptapEditor.commands.setTextSelection(14); + await emitEditorEvent({ event: 'transaction', tiptapEditor }); expectLinkButtonsToExist(true); expect(wrapper.findComponent(GlForm).exists()).toBe(false); @@ -175,6 +175,8 @@ describe('content_editor/components/bubble_menus/link', () => { linkHrefInput.setValue('https://google.com'); linkTitleInput.setValue('Search Google'); + contentEditor.resolveLink.mockResolvedValue('https://google.com'); + await wrapper.findComponent(GlForm).vm.$emit('submit', createFakeEvent()); }); @@ -214,8 +216,8 @@ describe('content_editor/components/bubble_menus/link', () => { // click edit once again to show the form back await wrapper.findByTestId('edit-link').vm.$emit('click'); - linkHrefInput = wrapper.findByTestId('link-href-group').findComponent(GlFormInput); - linkTitleInput = wrapper.findByTestId('link-title-group').findComponent(GlFormInput); + linkHrefInput = wrapper.findByTestId('link-href'); + linkTitleInput = wrapper.findByTestId('link-title'); expect(linkHrefInput.element.value).toBe('uploads/my_file.pdf'); expect(linkTitleInput.element.value).toBe('Click here to download'); diff --git a/spec/frontend/content_editor/components/bubble_menus/media_spec.js b/spec/frontend/content_editor/components/bubble_menus/media_spec.js new file mode 100644 index 00000000000..a4bcfa9c39e --- /dev/null +++ b/spec/frontend/content_editor/components/bubble_menus/media_spec.js @@ -0,0 +1,234 @@ +import { GlLink, GlForm } from '@gitlab/ui'; +import { BubbleMenu } from '@tiptap/vue-2'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media.vue'; +import eventHubFactory from '~/helpers/event_hub_factory'; +import Image from '~/content_editor/extensions/image'; +import Audio from '~/content_editor/extensions/audio'; +import Video from '~/content_editor/extensions/video'; +import { createTestEditor, emitEditorEvent, mockChainedCommands } from '../../test_utils'; +import { + PROJECT_WIKI_ATTACHMENT_IMAGE_HTML, + PROJECT_WIKI_ATTACHMENT_AUDIO_HTML, + PROJECT_WIKI_ATTACHMENT_VIDEO_HTML, +} from '../../test_constants'; + +const TIPTAP_IMAGE_HTML = `<p> + <img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon" data-canonical-src="https://gitlab.com/favicon.png"> +</p>`; + +const TIPTAP_AUDIO_HTML = `<p> + <span class="media-container audio-container"><audio src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></audio><a href="https://gitlab.com/favicon.png">gitlab favicon</a></span> +</p>`; + +const TIPTAP_VIDEO_HTML = `<p> + <span class="media-container video-container"><video src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></video><a href="https://gitlab.com/favicon.png">gitlab favicon</a></span> +</p>`; + +const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() }); + +describe.each` + mediaType | mediaHTML | filePath | mediaOutputHTML + ${'image'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${'test-file.png'} | ${TIPTAP_IMAGE_HTML} + ${'audio'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${'test-file.mp3'} | ${TIPTAP_AUDIO_HTML} + ${'video'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${'test-file.mp4'} | ${TIPTAP_VIDEO_HTML} +`( + 'content_editor/components/bubble_menus/media ($mediaType)', + ({ mediaType, mediaHTML, filePath, mediaOutputHTML }) => { + let wrapper; + let tiptapEditor; + let contentEditor; + let bubbleMenu; + let eventHub; + + const buildEditor = () => { + tiptapEditor = createTestEditor({ extensions: [Image, Audio, Video] }); + contentEditor = { resolveLink: jest.fn() }; + eventHub = eventHubFactory(); + }; + + const buildWrapper = () => { + wrapper = mountExtended(MediaBubbleMenu, { + provide: { + tiptapEditor, + contentEditor, + eventHub, + }, + }); + }; + + const selectFile = async (file) => { + const input = wrapper.find({ ref: 'fileSelector' }); + + // override the property definition because `input.files` isn't directly modifyable + Object.defineProperty(input.element, 'files', { value: [file], writable: true }); + await input.trigger('change'); + }; + + const expectLinkButtonsToExist = (exist = true) => { + expect(wrapper.findComponent(GlLink).exists()).toBe(exist); + expect(wrapper.findByTestId('copy-media-src').exists()).toBe(exist); + expect(wrapper.findByTestId('edit-media').exists()).toBe(exist); + expect(wrapper.findByTestId('delete-media').exists()).toBe(exist); + }; + + beforeEach(async () => { + buildEditor(); + buildWrapper(); + + tiptapEditor + .chain() + .insertContent(mediaHTML) + .setNodeSelection(4) // select the media + .run(); + + contentEditor.resolveLink.mockResolvedValue(`/group1/project1/-/wikis/${filePath}`); + + await emitEditorEvent({ event: 'transaction', tiptapEditor }); + + bubbleMenu = wrapper.findComponent(BubbleMenu); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders bubble menu component', async () => { + expect(bubbleMenu.props('editor')).toBe(tiptapEditor); + expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']); + }); + + it('shows a clickable link to the image', async () => { + const link = wrapper.findComponent(GlLink); + expect(link.attributes()).toEqual( + expect.objectContaining({ + href: `/group1/project1/-/wikis/${filePath}`, + 'aria-label': filePath, + title: filePath, + target: '_blank', + }), + ); + expect(link.text()).toBe(filePath); + }); + + describe('copy button', () => { + it(`copies the canonical link to the ${mediaType} to clipboard`, async () => { + jest.spyOn(navigator.clipboard, 'writeText'); + + await wrapper.findByTestId('copy-media-src').vm.$emit('click'); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith(filePath); + }); + }); + + describe(`remove ${mediaType} button`, () => { + it(`removes the ${mediaType}`, async () => { + await wrapper.findByTestId('delete-media').vm.$emit('click'); + + expect(tiptapEditor.getHTML()).toBe('<p>\n \n</p>'); + }); + }); + + describe(`replace ${mediaType} button`, () => { + it('uploads and replaces the selected image when file input changes', async () => { + const commands = mockChainedCommands(tiptapEditor, [ + 'focus', + 'deleteSelection', + 'uploadAttachment', + 'run', + ]); + const file = new File(['foo'], 'foo.png', { type: 'image/png' }); + + await wrapper.findByTestId('replace-media').vm.$emit('click'); + await selectFile(file); + + expect(commands.focus).toHaveBeenCalled(); + expect(commands.deleteSelection).toHaveBeenCalled(); + expect(commands.uploadAttachment).toHaveBeenCalledWith({ file }); + expect(commands.run).toHaveBeenCalled(); + }); + }); + + describe('edit button', () => { + let mediaSrcInput; + let mediaTitleInput; + let mediaAltInput; + + beforeEach(async () => { + await wrapper.findByTestId('edit-media').vm.$emit('click'); + + mediaSrcInput = wrapper.findByTestId('media-src'); + mediaTitleInput = wrapper.findByTestId('media-title'); + mediaAltInput = wrapper.findByTestId('media-alt'); + }); + + it('hides the link and copy/edit/remove link buttons', async () => { + expectLinkButtonsToExist(false); + }); + + it(`shows a form to edit the ${mediaType} src/title/alt`, () => { + expect(wrapper.findComponent(GlForm).exists()).toBe(true); + + expect(mediaSrcInput.element.value).toBe(filePath); + expect(mediaTitleInput.element.value).toBe(''); + expect(mediaAltInput.element.value).toBe('test-file'); + }); + + describe('after making changes in the form and clicking apply', () => { + beforeEach(async () => { + mediaSrcInput.setValue('https://gitlab.com/favicon.png'); + mediaAltInput.setValue('gitlab favicon'); + mediaTitleInput.setValue('gitlab favicon'); + + contentEditor.resolveLink.mockResolvedValue('https://gitlab.com/favicon.png'); + + await wrapper.findComponent(GlForm).vm.$emit('submit', createFakeEvent()); + }); + + it(`updates prosemirror doc with new src to the ${mediaType}`, async () => { + expect(tiptapEditor.getHTML()).toBe(mediaOutputHTML); + }); + + it(`updates the link to the ${mediaType} in the bubble menu`, () => { + const link = wrapper.findComponent(GlLink); + expect(link.attributes()).toEqual( + expect.objectContaining({ + href: 'https://gitlab.com/favicon.png', + 'aria-label': 'https://gitlab.com/favicon.png', + title: 'https://gitlab.com/favicon.png', + target: '_blank', + }), + ); + expect(link.text()).toBe('https://gitlab.com/favicon.png'); + }); + }); + + describe('after making changes in the form and clicking cancel', () => { + beforeEach(async () => { + mediaSrcInput.setValue('https://gitlab.com/favicon.png'); + mediaAltInput.setValue('gitlab favicon'); + mediaTitleInput.setValue('gitlab favicon'); + + await wrapper.findByTestId('cancel-editing-media').vm.$emit('click'); + }); + + it('hides the form and shows the copy/edit/remove link buttons', () => { + expectLinkButtonsToExist(); + }); + + it(`resets the form with old values of the ${mediaType} from prosemirror`, async () => { + // click edit once again to show the form back + await wrapper.findByTestId('edit-media').vm.$emit('click'); + + mediaSrcInput = wrapper.findByTestId('media-src'); + mediaTitleInput = wrapper.findByTestId('media-title'); + mediaAltInput = wrapper.findByTestId('media-alt'); + + expect(mediaSrcInput.element.value).toBe(filePath); + expect(mediaAltInput.element.value).toBe('test-file'); + expect(mediaTitleInput.element.value).toBe(''); + }); + }); + }); + }, +); diff --git a/spec/frontend/content_editor/components/wrappers/media_spec.js b/spec/frontend/content_editor/components/wrappers/media_spec.js deleted file mode 100644 index 3e95e2f3914..00000000000 --- a/spec/frontend/content_editor/components/wrappers/media_spec.js +++ /dev/null @@ -1,69 +0,0 @@ -import { GlLoadingIcon } from '@gitlab/ui'; -import { NodeViewWrapper } from '@tiptap/vue-2'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import MediaWrapper from '~/content_editor/components/wrappers/media.vue'; - -describe('content/components/wrappers/media', () => { - let wrapper; - - const createWrapper = async (nodeAttrs = {}) => { - wrapper = shallowMountExtended(MediaWrapper, { - propsData: { - node: { - attrs: nodeAttrs, - type: { - name: 'image', - }, - }, - }, - }); - }; - const findMedia = () => wrapper.findByTestId('media'); - const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders a node-view-wrapper with display-inline-block class', () => { - createWrapper(); - - expect(wrapper.findComponent(NodeViewWrapper).classes()).toContain('gl-display-inline-block'); - }); - - it('renders an image that displays the node src', () => { - const src = 'foobar.png'; - - createWrapper({ src }); - - expect(findMedia().attributes().src).toBe(src); - }); - - describe('when uploading', () => { - beforeEach(() => { - createWrapper({ uploading: true }); - }); - - it('renders a gl-loading-icon component', () => { - expect(findLoadingIcon().exists()).toBe(true); - }); - - it('adds gl-opacity-5 class selector to the media tag', () => { - expect(findMedia().classes()).toContain('gl-opacity-5'); - }); - }); - - describe('when not uploading', () => { - beforeEach(() => { - createWrapper({ uploading: false }); - }); - - it('does not render a gl-loading-icon component', () => { - expect(findLoadingIcon().exists()).toBe(false); - }); - - it('does not add gl-opacity-5 class selector to the media tag', () => { - expect(findMedia().classes()).not.toContain('gl-opacity-5'); - }); - }); -}); diff --git a/spec/frontend/content_editor/extensions/attachment_spec.js b/spec/frontend/content_editor/extensions/attachment_spec.js index d3c42104e47..d528096be34 100644 --- a/spec/frontend/content_editor/extensions/attachment_spec.js +++ b/spec/frontend/content_editor/extensions/attachment_spec.js @@ -11,32 +11,12 @@ import { VARIANT_DANGER } from '~/flash'; import httpStatus from '~/lib/utils/http_status'; import eventHubFactory from '~/helpers/event_hub_factory'; import { createTestEditor, createDocBuilder } from '../test_utils'; - -const PROJECT_WIKI_ATTACHMENT_IMAGE_HTML = `<p data-sourcepos="1:1-1:27" dir="auto"> - <a class="no-attachment-icon" href="/group1/project1/-/wikis/test-file.png" target="_blank" rel="noopener noreferrer" data-canonical-src="test-file.png"> - <img alt="test-file" class="lazy" data-src="/group1/project1/-/wikis/test-file.png" data-canonical-src="test-file.png"> - </a> -</p>`; - -const PROJECT_WIKI_ATTACHMENT_VIDEO_HTML = `<p data-sourcepos="1:1-1:132" dir="auto"> - <span class="media-container video-container"> - <video src="/group1/project1/-/wikis/test-file.mp4" controls="true" data-setup="{}" data-title="test-file" width="400" preload="metadata" data-canonical-src="test-file.mp4"> - </video> - <a href="/himkp/test/-/wikis/test-file.mp4" target="_blank" rel="noopener noreferrer" title="Download 'test-file'" data-canonical-src="test-file.mp4">test-file</a> - </span> -</p>`; - -const PROJECT_WIKI_ATTACHMENT_AUDIO_HTML = `<p data-sourcepos="3:1-3:74" dir="auto"> - <span class="media-container audio-container"> - <audio src="/himkp/test/-/wikis/test-file.mp3" controls="true" data-setup="{}" data-title="test-file" data-canonical-src="test-file.mp3"> - </audio> - <a href="/himkp/test/-/wikis/test-file.mp3" target="_blank" rel="noopener noreferrer" title="Download 'test-file'" data-canonical-src="test-file.mp3">test-file</a> - </span> -</p>`; - -const PROJECT_WIKI_ATTACHMENT_LINK_HTML = `<p data-sourcepos="1:1-1:26" dir="auto"> - <a href="/group1/project1/-/wikis/test-file.zip" data-canonical-src="test-file.zip">test-file</a> -</p>`; +import { + PROJECT_WIKI_ATTACHMENT_IMAGE_HTML, + PROJECT_WIKI_ATTACHMENT_AUDIO_HTML, + PROJECT_WIKI_ATTACHMENT_VIDEO_HTML, + PROJECT_WIKI_ATTACHMENT_LINK_HTML, +} from '../test_constants'; describe('content_editor/extensions/attachment', () => { let tiptapEditor; diff --git a/spec/frontend/content_editor/services/asset_resolver_spec.js b/spec/frontend/content_editor/services/asset_resolver_spec.js new file mode 100644 index 00000000000..f4e7d9bf881 --- /dev/null +++ b/spec/frontend/content_editor/services/asset_resolver_spec.js @@ -0,0 +1,23 @@ +import createAssetResolver from '~/content_editor/services/asset_resolver'; + +describe('content_editor/services/asset_resolver', () => { + let renderMarkdown; + let assetResolver; + + beforeEach(() => { + renderMarkdown = jest.fn(); + assetResolver = createAssetResolver({ renderMarkdown }); + }); + + describe('resolveUrl', () => { + it('resolves a canonical url to an absolute url', async () => { + renderMarkdown.mockResolvedValue( + '<p><a href="/group1/project1/-/wikis/test-file.png" data-canonical-src="test-file.png">link</a></p>', + ); + + expect(await assetResolver.resolveUrl('test-file.png')).toBe( + '/group1/project1/-/wikis/test-file.png', + ); + }); + }); +}); diff --git a/spec/frontend/content_editor/services/content_editor_spec.js b/spec/frontend/content_editor/services/content_editor_spec.js index 18c5d89e391..fde4f8f6282 100644 --- a/spec/frontend/content_editor/services/content_editor_spec.js +++ b/spec/frontend/content_editor/services/content_editor_spec.js @@ -26,7 +26,7 @@ describe('content_editor/services/content_editor', () => { tiptapEditor, })); - serializer = { deserialize: jest.fn() }; + serializer = { serialize: jest.fn() }; deserializer = { deserialize: jest.fn() }; languageLoader = { loadLanguages: jest.fn() }; eventHub = eventHubFactory(); diff --git a/spec/frontend/content_editor/test_constants.js b/spec/frontend/content_editor/test_constants.js new file mode 100644 index 00000000000..45a0e4a8bd1 --- /dev/null +++ b/spec/frontend/content_editor/test_constants.js @@ -0,0 +1,25 @@ +export const PROJECT_WIKI_ATTACHMENT_IMAGE_HTML = `<p data-sourcepos="1:1-1:27" dir="auto"> + <a class="no-attachment-icon" href="/group1/project1/-/wikis/test-file.png" target="_blank" rel="noopener noreferrer" data-canonical-src="test-file.png"> + <img alt="test-file" class="lazy" data-src="/group1/project1/-/wikis/test-file.png" data-canonical-src="test-file.png"> + </a> +</p>`; + +export const PROJECT_WIKI_ATTACHMENT_VIDEO_HTML = `<p data-sourcepos="1:1-1:132" dir="auto"> + <span class="media-container video-container"> + <video src="/group1/project1/-/wikis/test-file.mp4" controls="true" data-setup="{}" data-title="test-file" width="400" preload="metadata" data-canonical-src="test-file.mp4"> + </video> + <a href="/group1/project1/-/wikis/test-file.mp4" target="_blank" rel="noopener noreferrer" title="Download 'test-file'" data-canonical-src="test-file.mp4">test-file</a> + </span> +</p>`; + +export const PROJECT_WIKI_ATTACHMENT_AUDIO_HTML = `<p data-sourcepos="3:1-3:74" dir="auto"> + <span class="media-container audio-container"> + <audio src="/group1/project1/-/wikis/test-file.mp3" controls="true" data-setup="{}" data-title="test-file" data-canonical-src="test-file.mp3"> + </audio> + <a href="/group1/project1/-/wikis/test-file.mp3" target="_blank" rel="noopener noreferrer" title="Download 'test-file'" data-canonical-src="test-file.mp3">test-file</a> + </span> +</p>`; + +export const PROJECT_WIKI_ATTACHMENT_LINK_HTML = `<p data-sourcepos="1:1-1:26" dir="auto"> + <a href="/group1/project1/-/wikis/test-file.zip" data-canonical-src="test-file.zip">test-file</a> +</p>`; diff --git a/spec/frontend/prometheus_alerts/components/reset_key_spec.js b/spec/frontend/prometheus_alerts/components/reset_key_spec.js deleted file mode 100644 index dc5fdb1dffc..00000000000 --- a/spec/frontend/prometheus_alerts/components/reset_key_spec.js +++ /dev/null @@ -1,99 +0,0 @@ -import { GlModal } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import MockAdapter from 'axios-mock-adapter'; -import { nextTick } from 'vue'; -import waitForPromises from 'helpers/wait_for_promises'; -import axios from '~/lib/utils/axios_utils'; -import ResetKey from '~/prometheus_alerts/components/reset_key.vue'; -import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; - -describe('ResetKey', () => { - let mock; - let vm; - - const propsData = { - initialAuthorizationKey: 'abcd1234', - changeKeyUrl: '/updateKeyUrl', - notifyUrl: '/root/autodevops-deploy/prometheus/alerts/notify.json', - learnMoreUrl: '/learnMore', - }; - - beforeEach(() => { - mock = new MockAdapter(axios); - setFixtures('<div class="flash-container"></div><div id="reset-key"></div>'); - }); - - afterEach(() => { - mock.restore(); - vm.destroy(); - }); - - describe('authorization key exists', () => { - beforeEach(() => { - propsData.initialAuthorizationKey = 'abcd1234'; - vm = shallowMount(ResetKey, { - propsData, - }); - }); - - it('shows fields and buttons', () => { - expect(vm.find('#notify-url').attributes('value')).toEqual(propsData.notifyUrl); - expect(vm.find('#authorization-key').attributes('value')).toEqual( - propsData.initialAuthorizationKey, - ); - - expect(vm.findAll(ClipboardButton).length).toBe(2); - expect(vm.find('.js-reset-auth-key').text()).toEqual('Reset key'); - }); - - it('reset updates key', async () => { - mock.onPost(propsData.changeKeyUrl).replyOnce(200, { token: 'newToken' }); - - vm.find(GlModal).vm.$emit('ok'); - - await nextTick(); - await waitForPromises(); - expect(vm.vm.authorizationKey).toEqual('newToken'); - expect(vm.find('#authorization-key').attributes('value')).toEqual('newToken'); - }); - - it('reset key failure shows error', async () => { - mock.onPost(propsData.changeKeyUrl).replyOnce(500); - - vm.find(GlModal).vm.$emit('ok'); - - await nextTick(); - await waitForPromises(); - expect(vm.find('#authorization-key').attributes('value')).toEqual( - propsData.initialAuthorizationKey, - ); - - expect(document.querySelector('.flash-container').innerText.trim()).toEqual( - 'Failed to reset key. Please try again.', - ); - }); - }); - - describe('authorization key has not been set', () => { - beforeEach(() => { - propsData.initialAuthorizationKey = ''; - vm = shallowMount(ResetKey, { - propsData, - }); - }); - - it('shows Generate Key button', () => { - expect(vm.find('.js-reset-auth-key').text()).toEqual('Generate key'); - expect(vm.find('#authorization-key').attributes('value')).toEqual(''); - }); - - it('Generate key button triggers key change', async () => { - mock.onPost(propsData.changeKeyUrl).replyOnce(200, { token: 'newToken' }); - - vm.find('.js-reset-auth-key').vm.$emit('click'); - - await waitForPromises(); - expect(vm.find('#authorization-key').attributes('value')).toEqual('newToken'); - }); - }); -}); diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js index 2ef856c90ab..405813be4e3 100644 --- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js +++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js @@ -35,6 +35,8 @@ import { PARAM_KEY_STATUS, PARAM_KEY_TAG, STATUS_ONLINE, + STATUS_OFFLINE, + STATUS_STALE, RUNNER_PAGE_SIZE, } from '~/runner/constants'; import adminRunnersQuery from '~/runner/graphql/list/admin_runners.query.graphql'; @@ -52,6 +54,7 @@ import { const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN'; const mockRunners = runnersData.data.runners.nodes; +const mockRunnersCount = runnersCountData.data.runners.count; jest.mock('~/flash'); jest.mock('~/runner/sentry_utils'); @@ -124,18 +127,6 @@ describe('AdminRunnersApp', () => { wrapper.destroy(); }); - it('shows total runner counts', async () => { - createComponent({ mountFn: mountExtended }); - - await waitForPromises(); - - const stats = findRunnerStats().text(); - - expect(stats).toMatch('Online runners 4'); - expect(stats).toMatch('Offline runners 4'); - expect(stats).toMatch('Stale runners 4'); - }); - it('shows the runner tabs with a runner count for each type', async () => { mockRunnersCountQuery.mockImplementation(({ type }) => { let count; @@ -197,6 +188,24 @@ describe('AdminRunnersApp', () => { expect(findRegistrationDropdown().props('type')).toBe(INSTANCE_TYPE); }); + it('shows total runner counts', async () => { + expect(mockRunnersCountQuery).toHaveBeenCalledWith({ + status: STATUS_ONLINE, + }); + expect(mockRunnersCountQuery).toHaveBeenCalledWith({ + status: STATUS_OFFLINE, + }); + expect(mockRunnersCountQuery).toHaveBeenCalledWith({ + status: STATUS_STALE, + }); + + expect(findRunnerStats().props()).toMatchObject({ + onlineRunnersCount: mockRunnersCount, + offlineRunnersCount: mockRunnersCount, + staleRunnersCount: mockRunnersCount, + }); + }); + it('shows the runners list', () => { expect(findRunnerList().props('runners')).toEqual(mockRunners); }); @@ -329,13 +338,30 @@ describe('AdminRunnersApp', () => { first: RUNNER_PAGE_SIZE, }); }); + + it('fetches count results for requested status', () => { + expect(mockRunnersCountQuery).toHaveBeenCalledWith({ + type: INSTANCE_TYPE, + status: STATUS_ONLINE, + tagList: ['tag1'], + }); + + expect(findRunnerStats().props()).toMatchObject({ + onlineRunnersCount: mockRunnersCount, + }); + }); }); describe('when a filter is selected by the user', () => { beforeEach(() => { + mockRunnersCountQuery.mockClear(); + findRunnerFilteredSearchBar().vm.$emit('input', { runnerType: null, - filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }], + filters: [ + { type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }, + { type: PARAM_KEY_TAG, value: { data: 'tag1', operator: '=' } }, + ], sort: CREATED_ASC, }); }); @@ -343,17 +369,45 @@ describe('AdminRunnersApp', () => { it('updates the browser url', () => { expect(updateHistory).toHaveBeenLastCalledWith({ title: expect.any(String), - url: 'http://test.host/admin/runners?status[]=ONLINE&sort=CREATED_ASC', + url: 'http://test.host/admin/runners?status[]=ONLINE&tag[]=tag1&sort=CREATED_ASC', }); }); it('requests the runners with filters', () => { expect(mockRunnersQuery).toHaveBeenLastCalledWith({ status: STATUS_ONLINE, + tagList: ['tag1'], sort: CREATED_ASC, first: RUNNER_PAGE_SIZE, }); }); + + it('fetches count results for requested status', () => { + expect(mockRunnersCountQuery).toHaveBeenCalledWith({ + tagList: ['tag1'], + status: STATUS_ONLINE, + }); + + expect(findRunnerStats().props()).toMatchObject({ + onlineRunnersCount: mockRunnersCount, + }); + }); + + it('skips fetching count results for status that were not in filter', () => { + expect(mockRunnersCountQuery).not.toHaveBeenCalledWith({ + tagList: ['tag1'], + status: STATUS_OFFLINE, + }); + expect(mockRunnersCountQuery).not.toHaveBeenCalledWith({ + tagList: ['tag1'], + status: STATUS_STALE, + }); + + expect(findRunnerStats().props()).toMatchObject({ + offlineRunnersCount: null, + staleRunnersCount: null, + }); + }); }); it('when runners have not loaded, shows a loading state', () => { diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js index 02348bf737a..52bd51a974b 100644 --- a/spec/frontend/runner/group_runners/group_runners_app_spec.js +++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js @@ -30,7 +30,10 @@ import { PROJECT_TYPE, PARAM_KEY_PAUSED, PARAM_KEY_STATUS, + PARAM_KEY_TAG, STATUS_ONLINE, + STATUS_OFFLINE, + STATUS_STALE, RUNNER_PAGE_SIZE, I18N_EDIT, } from '~/runner/constants'; @@ -53,7 +56,7 @@ Vue.use(GlToast); const mockGroupFullPath = 'group1'; const mockRegistrationToken = 'AABBCC'; const mockGroupRunnersEdges = groupRunnersData.data.group.runners.edges; -const mockGroupRunnersLimitedCount = mockGroupRunnersEdges.length; +const mockGroupRunnersCount = mockGroupRunnersEdges.length; jest.mock('~/flash'); jest.mock('~/runner/sentry_utils'); @@ -94,7 +97,7 @@ describe('GroupRunnersApp', () => { propsData: { registrationToken: mockRegistrationToken, groupFullPath: mockGroupFullPath, - groupRunnersLimitedCount: mockGroupRunnersLimitedCount, + groupRunnersLimitedCount: mockGroupRunnersCount, ...props, }, provide: { @@ -115,15 +118,24 @@ describe('GroupRunnersApp', () => { }); it('shows total runner counts', async () => { - createComponent({ mountFn: mountExtended }); - - await waitForPromises(); - - const stats = findRunnerStats().text(); + expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({ + groupFullPath: mockGroupFullPath, + status: STATUS_ONLINE, + }); + expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({ + groupFullPath: mockGroupFullPath, + status: STATUS_OFFLINE, + }); + expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({ + groupFullPath: mockGroupFullPath, + status: STATUS_STALE, + }); - expect(stats).toMatch('Online runners 2'); - expect(stats).toMatch('Offline runners 2'); - expect(stats).toMatch('Stale runners 2'); + expect(findRunnerStats().props()).toMatchObject({ + onlineRunnersCount: mockGroupRunnersCount, + offlineRunnersCount: mockGroupRunnersCount, + staleRunnersCount: mockGroupRunnersCount, + }); }); it('shows the runner tabs with a runner count for each type', async () => { @@ -281,13 +293,28 @@ describe('GroupRunnersApp', () => { first: RUNNER_PAGE_SIZE, }); }); + + it('fetches count results for requested status', () => { + expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({ + groupFullPath: mockGroupFullPath, + type: INSTANCE_TYPE, + status: STATUS_ONLINE, + }); + + expect(findRunnerStats().props()).toMatchObject({ + onlineRunnersCount: mockGroupRunnersCount, + }); + }); }); describe('when a filter is selected by the user', () => { beforeEach(async () => { findRunnerFilteredSearchBar().vm.$emit('input', { runnerType: null, - filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }], + filters: [ + { type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }, + { type: PARAM_KEY_TAG, value: { data: 'tag1', operator: '=' } }, + ], sort: CREATED_ASC, }); @@ -297,7 +324,7 @@ describe('GroupRunnersApp', () => { it('updates the browser url', () => { expect(updateHistory).toHaveBeenLastCalledWith({ title: expect.any(String), - url: 'http://test.host/groups/group1/-/runners?status[]=ONLINE&sort=CREATED_ASC', + url: 'http://test.host/groups/group1/-/runners?status[]=ONLINE&tag[]=tag1&sort=CREATED_ASC', }); }); @@ -305,10 +332,41 @@ describe('GroupRunnersApp', () => { expect(mockGroupRunnersQuery).toHaveBeenLastCalledWith({ groupFullPath: mockGroupFullPath, status: STATUS_ONLINE, + tagList: ['tag1'], sort: CREATED_ASC, first: RUNNER_PAGE_SIZE, }); }); + + it('fetches count results for requested status', () => { + expect(mockGroupRunnersCountQuery).toHaveBeenCalledWith({ + groupFullPath: mockGroupFullPath, + tagList: ['tag1'], + status: STATUS_ONLINE, + }); + + expect(findRunnerStats().props()).toMatchObject({ + onlineRunnersCount: mockGroupRunnersCount, + }); + }); + + it('skips fetching count results for status that were not in filter', () => { + expect(mockGroupRunnersCountQuery).not.toHaveBeenCalledWith({ + groupFullPath: mockGroupFullPath, + tagList: ['tag1'], + status: STATUS_OFFLINE, + }); + expect(mockGroupRunnersCountQuery).not.toHaveBeenCalledWith({ + groupFullPath: mockGroupFullPath, + tagList: ['tag1'], + status: STATUS_STALE, + }); + + expect(findRunnerStats().props()).toMatchObject({ + offlineRunnersCount: null, + staleRunnersCount: null, + }); + }); }); it('when runners have not loaded, shows a loading state', () => { diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index 22c5735803b..da3a323e8ea 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -322,7 +322,6 @@ describe('ReadyToMerge', () => { await waitForPromises(); - expect(wrapper.vm.isMakingRequest).toBeTruthy(); expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); expect(eventHub.$emit).toHaveBeenCalledWith('StateMachineValueChanged', { transition: 'start-auto-merge', @@ -349,7 +348,6 @@ describe('ReadyToMerge', () => { await waitForPromises(); - expect(wrapper.vm.isMakingRequest).toBeTruthy(); expect(eventHub.$emit).toHaveBeenCalledWith('FailedToMerge', undefined); const params = wrapper.vm.service.merge.mock.calls[0][0]; @@ -372,7 +370,6 @@ describe('ReadyToMerge', () => { await waitForPromises(); - expect(wrapper.vm.isMakingRequest).toBeTruthy(); expect(wrapper.vm.mr.transitionStateMachine).toHaveBeenCalledWith({ transition: 'start-merge', }); diff --git a/spec/graphql/mutations/container_repositories/destroy_spec.rb b/spec/graphql/mutations/container_repositories/destroy_spec.rb index 3903196a511..7c674dddb15 100644 --- a/spec/graphql/mutations/container_repositories/destroy_spec.rb +++ b/spec/graphql/mutations/container_repositories/destroy_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Mutations::ContainerRepositories::Destroy do let_it_be(:user) { create(:user) } let(:project) { container_repository.project } - let(:id) { container_repository.to_global_id.to_s } + let(:id) { container_repository.to_global_id } specify { expect(described_class).to require_graphql_authorizations(:destroy_container_image) } diff --git a/spec/graphql/mutations/container_repositories/destroy_tags_spec.rb b/spec/graphql/mutations/container_repositories/destroy_tags_spec.rb index f22d9ffe753..3e5f28ee244 100644 --- a/spec/graphql/mutations/container_repositories/destroy_tags_spec.rb +++ b/spec/graphql/mutations/container_repositories/destroy_tags_spec.rb @@ -3,10 +3,12 @@ require 'spec_helper' RSpec.describe Mutations::ContainerRepositories::DestroyTags do + include GraphqlHelpers + include_context 'container repository delete tags service shared context' using RSpec::Parameterized::TableSyntax - let(:id) { repository.to_global_id.to_s } + let(:id) { repository.to_global_id } specify { expect(described_class).to require_graphql_authorizations(:destroy_container_image) } @@ -67,8 +69,8 @@ RSpec.describe Mutations::ContainerRepositories::DestroyTags do end end - context 'with invalid id' do - let(:id) { 'gid://gitlab/ContainerRepository/5555' } + context 'with non-existing id' do + let(:id) { global_id_of(id: non_existing_record_id, model_name: 'ContainerRepository') } it_behaves_like 'denying access to container respository' end diff --git a/spec/graphql/mutations/customer_relations/contacts/create_spec.rb b/spec/graphql/mutations/customer_relations/contacts/create_spec.rb index d17d11305b1..dafc7b4c367 100644 --- a/spec/graphql/mutations/customer_relations/contacts/create_spec.rb +++ b/spec/graphql/mutations/customer_relations/contacts/create_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Mutations::CustomerRelations::Contacts::Create do + include GraphqlHelpers + let_it_be(:user) { create(:user) } let(:group) { create(:group, :crm_enabled) } @@ -78,9 +80,9 @@ RSpec.describe Mutations::CustomerRelations::Contacts::Create do end end - context 'when organization_id is invalid' do + context 'when organization does not exist' do before do - valid_params[:organization_id] = "gid://gitlab/CustomerRelations::Organization/#{non_existing_record_id}" + valid_params[:organization_id] = global_id_of(model_name: 'CustomerRelations::Organization', id: non_existing_record_id) end it 'returns the relevant error' do diff --git a/spec/graphql/mutations/discussions/toggle_resolve_spec.rb b/spec/graphql/mutations/discussions/toggle_resolve_spec.rb index 2041b86d6e7..e5dc6f85c2a 100644 --- a/spec/graphql/mutations/discussions/toggle_resolve_spec.rb +++ b/spec/graphql/mutations/discussions/toggle_resolve_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Mutations::Discussions::ToggleResolve do + include GraphqlHelpers + subject(:mutation) do described_class.new(object: nil, context: { current_user: user }, field: nil) end @@ -15,7 +17,7 @@ RSpec.describe Mutations::Discussions::ToggleResolve do mutation.resolve(id: id_arg, resolve: resolve_arg) end - let(:id_arg) { discussion.to_global_id.to_s } + let(:id_arg) { global_id_of(discussion) } let(:resolve_arg) { true } let(:mutated_discussion) { subject[:discussion] } let(:errors) { subject[:errors] } @@ -36,7 +38,7 @@ RSpec.describe Mutations::Discussions::ToggleResolve do let_it_be(:user) { create(:user, developer_projects: [project]) } context 'when discussion cannot be found' do - let(:id_arg) { "#{discussion.to_global_id}foo" } + let(:id_arg) { global_id_of(id: non_existing_record_id, model_name: discussion.class.name) } it 'raises an error' do expect { subject }.to raise_error( @@ -52,7 +54,7 @@ RSpec.describe Mutations::Discussions::ToggleResolve do it 'raises an error' do expect { subject }.to raise_error( GraphQL::CoercionError, - "\"#{discussion.to_global_id}\" does not represent an instance of Discussion" + "\"#{global_id_of(discussion)}\" does not represent an instance of Discussion" ) end end diff --git a/spec/graphql/mutations/environments/canary_ingress/update_spec.rb b/spec/graphql/mutations/environments/canary_ingress/update_spec.rb index fdf9cbaf25b..e719ca050a8 100644 --- a/spec/graphql/mutations/environments/canary_ingress/update_spec.rb +++ b/spec/graphql/mutations/environments/canary_ingress/update_spec.rb @@ -20,7 +20,7 @@ RSpec.describe Mutations::Environments::CanaryIngress::Update do describe '#resolve' do subject { mutation.resolve(id: environment_id, weight: weight) } - let(:environment_id) { environment.to_global_id.to_s } + let(:environment_id) { environment.to_global_id } let(:weight) { 50 } let(:update_service) { double('update_service') } diff --git a/spec/graphql/mutations/release_asset_links/delete_spec.rb b/spec/graphql/mutations/release_asset_links/delete_spec.rb index cda292f2ffa..67576bdda57 100644 --- a/spec/graphql/mutations/release_asset_links/delete_spec.rb +++ b/spec/graphql/mutations/release_asset_links/delete_spec.rb @@ -52,7 +52,9 @@ RSpec.describe Mutations::ReleaseAssetLinks::Delete do end context "when the link doesn't exist" do - let(:mutation_arguments) { super().merge(id: "gid://gitlab/Releases::Link/#{non_existing_record_id}") } + let(:mutation_arguments) do + super().merge(id: global_id_of(id: non_existing_record_id, model_name: release_link.class.name)) + end it 'raises an error' do expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) diff --git a/spec/graphql/mutations/release_asset_links/update_spec.rb b/spec/graphql/mutations/release_asset_links/update_spec.rb index 64648687336..cb7474123ad 100644 --- a/spec/graphql/mutations/release_asset_links/update_spec.rb +++ b/spec/graphql/mutations/release_asset_links/update_spec.rb @@ -186,7 +186,9 @@ RSpec.describe Mutations::ReleaseAssetLinks::Update do end context "when the link doesn't exist" do - let(:mutation_arguments) { super().merge(id: "gid://gitlab/Releases::Link/#{non_existing_record_id}") } + let(:mutation_arguments) do + super().merge(id: global_id_of(id: non_existing_record_id, model_name: "Releases::Link")) + end it 'raises an error' do expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) diff --git a/spec/graphql/mutations/timelogs/delete_spec.rb b/spec/graphql/mutations/timelogs/delete_spec.rb index 5012d10f32e..f4a258e0f78 100644 --- a/spec/graphql/mutations/timelogs/delete_spec.rb +++ b/spec/graphql/mutations/timelogs/delete_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Mutations::Timelogs::Delete do + include GraphqlHelpers + let_it_be(:author) { create(:user) } let_it_be(:maintainer) { create(:user) } let_it_be(:administrator) { create(:user, :admin) } @@ -11,7 +13,7 @@ RSpec.describe Mutations::Timelogs::Delete do let_it_be_with_reload(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800) } let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) } - let(:timelog_id) { timelog.to_global_id.to_s } + let(:timelog_id) { global_id_of(timelog) } let(:mutation_arguments) { { id: timelog_id } } describe '#resolve' do @@ -21,7 +23,7 @@ RSpec.describe Mutations::Timelogs::Delete do context 'when the timelog id is not valid' do let(:current_user) { author } - let(:timelog_id) { 'gid://gitlab/Timelog/%d' % non_existing_record_id } + let(:timelog_id) { global_id_of(model_name: 'Timelog', id: non_existing_record_id) } it 'raises Gitlab::Graphql::Errors::ResourceNotAvailable' do expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) diff --git a/spec/graphql/resolvers/concerns/resolves_ids_spec.rb b/spec/graphql/resolvers/concerns/resolves_ids_spec.rb index 1dd27c0eff0..732b7cd2bbc 100644 --- a/spec/graphql/resolvers/concerns/resolves_ids_spec.rb +++ b/spec/graphql/resolvers/concerns/resolves_ids_spec.rb @@ -3,33 +3,35 @@ require 'spec_helper' RSpec.describe ResolvesIds do + include GraphqlHelpers + # gid://gitlab/Project/6 # gid://gitlab/Issue/6 # gid://gitlab/Project/6 gid://gitlab/Issue/6 context 'with a single project' do - let(:ids) { 'gid://gitlab/Project/6' } + let(:ids) { global_id_of(model_name: 'Project', id: 6) } let(:type) { ::Types::GlobalIDType[::Project] } it 'returns the correct array' do - expect(resolve_ids).to match_array(['6']) + expect(resolve_ids).to contain_exactly('6') end end context 'with a single issue' do - let(:ids) { 'gid://gitlab/Issue/9' } + let(:ids) { global_id_of(model_name: 'Issue', id: 9) } let(:type) { ::Types::GlobalIDType[::Issue] } it 'returns the correct array' do - expect(resolve_ids).to match_array(['9']) + expect(resolve_ids).to contain_exactly('9') end end context 'with multiple users' do - let(:ids) { ['gid://gitlab/User/7', 'gid://gitlab/User/13', 'gid://gitlab/User/21'] } + let(:ids) { [7, 13, 21].map { global_id_of(model_name: 'User', id: _1) } } let(:type) { ::Types::GlobalIDType[::User] } it 'returns the correct array' do - expect(resolve_ids).to match_array(%w[7 13 21]) + expect(resolve_ids).to eq %w[7 13 21] end end diff --git a/spec/graphql/resolvers/design_management/design_at_version_resolver_spec.rb b/spec/graphql/resolvers/design_management/design_at_version_resolver_spec.rb index a16e8821cb5..3fe1ec4b5a4 100644 --- a/spec/graphql/resolvers/design_management/design_at_version_resolver_spec.rb +++ b/spec/graphql/resolvers/design_management/design_at_version_resolver_spec.rb @@ -14,7 +14,7 @@ RSpec.describe Resolvers::DesignManagement::DesignAtVersionResolver do let(:current_user) { user } let(:object) { issue.design_collection } - let(:global_id) { GitlabSchema.id_from_object(design_at_version).to_s } + let(:global_id) { GitlabSchema.id_from_object(design_at_version) } let(:design_at_version) { ::DesignManagement::DesignAtVersion.new(design: design_a, version: version_a) } diff --git a/spec/graphql/resolvers/design_management/design_resolver_spec.rb b/spec/graphql/resolvers/design_management/design_resolver_spec.rb index 4c8b3116875..5b530b68a5b 100644 --- a/spec/graphql/resolvers/design_management/design_resolver_spec.rb +++ b/spec/graphql/resolvers/design_management/design_resolver_spec.rb @@ -24,7 +24,7 @@ RSpec.describe Resolvers::DesignManagement::DesignResolver do create(:design, issue: create(:issue, project: project), versions: [create(:design_version)]) end - let(:args) { { id: GitlabSchema.id_from_object(first_design).to_s } } + let(:args) { { id: GitlabSchema.id_from_object(first_design) } } let(:gql_context) { { current_user: current_user } } before do @@ -50,7 +50,7 @@ RSpec.describe Resolvers::DesignManagement::DesignResolver do end context 'when both arguments have been passed' do - let(:args) { { filename: first_design.filename, id: GitlabSchema.id_from_object(first_design).to_s } } + let(:args) { { filename: first_design.filename, id: GitlabSchema.id_from_object(first_design) } } it 'generates an error' do expect_graphql_error_to_be_created(::Gitlab::Graphql::Errors::ArgumentError, /may/) do diff --git a/spec/graphql/resolvers/design_management/designs_resolver_spec.rb b/spec/graphql/resolvers/design_management/designs_resolver_spec.rb index b091e58b06f..64eae14d888 100644 --- a/spec/graphql/resolvers/design_management/designs_resolver_spec.rb +++ b/spec/graphql/resolvers/design_management/designs_resolver_spec.rb @@ -109,6 +109,8 @@ RSpec.describe Resolvers::DesignManagement::DesignsResolver do end def resolve_designs - resolve(described_class, obj: issue.design_collection, args: args, ctx: gql_context) + Gitlab::Graphql::Lazy.force( + resolve(described_class, obj: issue.design_collection, args: args, ctx: gql_context) + ) end end diff --git a/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb b/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb index 32c53ba2302..4b34a750883 100644 --- a/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb +++ b/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb @@ -74,6 +74,6 @@ RSpec.describe Resolvers::ErrorTracking::SentryDetailedErrorResolver do end def issue_global_id(issue_id) - Gitlab::ErrorTracking::DetailedError.new(id: issue_id).to_global_id.to_s + Gitlab::ErrorTracking::DetailedError.new(id: issue_id).to_global_id end end diff --git a/spec/graphql/resolvers/timelog_resolver_spec.rb b/spec/graphql/resolvers/timelog_resolver_spec.rb index 84fa2932829..da2747fdf72 100644 --- a/spec/graphql/resolvers/timelog_resolver_spec.rb +++ b/spec/graphql/resolvers/timelog_resolver_spec.rb @@ -265,7 +265,7 @@ RSpec.describe Resolvers::TimelogResolver do context 'when > `default_max_page_size` records' do let(:object) { nil } let!(:timelog_list) { create_list(:timelog, 101, issue: issue) } - let(:args) { { project_id: "gid://gitlab/Project/#{project.id}" } } + let(:args) { { project_id: global_id_of(project) } } let(:extra_args) { {} } it 'pagination returns `default_max_page_size` and sets `has_next_page` true' do diff --git a/spec/graphql/resolvers/work_item_resolver_spec.rb b/spec/graphql/resolvers/work_item_resolver_spec.rb index bfa0cf1d8a2..c44ed395102 100644 --- a/spec/graphql/resolvers/work_item_resolver_spec.rb +++ b/spec/graphql/resolvers/work_item_resolver_spec.rb @@ -12,7 +12,7 @@ RSpec.describe Resolvers::WorkItemResolver do let(:current_user) { developer } - subject(:resolved_work_item) { resolve_work_item('id' => work_item.to_gid.to_s) } + subject(:resolved_work_item) { resolve_work_item('id' => work_item.to_gid) } context 'when the user can read the work item' do it { is_expected.to eq(work_item) } diff --git a/spec/graphql/types/terraform/state_version_type_spec.rb b/spec/graphql/types/terraform/state_version_type_spec.rb index b015a2045da..6a17d932d03 100644 --- a/spec/graphql/types/terraform/state_version_type_spec.rb +++ b/spec/graphql/types/terraform/state_version_type_spec.rb @@ -52,8 +52,8 @@ RSpec.describe GitlabSchema.types['TerraformStateVersion'] do shared_examples 'returning latest version' do it 'returns latest version of terraform state' do - expect(execute.dig('data', 'project', 'terraformState', 'latestVersion', 'id')).to eq( - global_id_of(terraform_state.latest_version) + expect(execute.dig('data', 'project', 'terraformState', 'latestVersion')).to match a_graphql_entity_for( + terraform_state.latest_version ) end end diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb index 125ac7fb102..69866d497a1 100644 --- a/spec/models/personal_access_token_spec.rb +++ b/spec/models/personal_access_token_spec.rb @@ -94,14 +94,6 @@ RSpec.describe PersonalAccessToken do end end - describe '#expired_but_not_enforced?' do - let(:token) { build(:personal_access_token) } - - it 'returns false', :aggregate_failures do - expect(token).not_to be_expired_but_not_enforced - end - end - describe 'Redis storage' do let(:user_id) { 123 } let(:token) { 'KS3wegQYXBLYhQsciwsj' } diff --git a/spec/requests/api/graphql/boards/board_lists_query_spec.rb b/spec/requests/api/graphql/boards/board_lists_query_spec.rb index e8fb9daa43b..eb206465bce 100644 --- a/spec/requests/api/graphql/boards/board_lists_query_spec.rb +++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb @@ -69,6 +69,10 @@ RSpec.describe 'get board lists' do let(:data_path) { [board_parent_type, :boards, :nodes, 0, :lists] } + def pagination_results_data(lists) + lists + end + def pagination_query(params) graphql_query_for( board_parent_type, @@ -94,7 +98,7 @@ RSpec.describe 'get board lists' do it_behaves_like 'sorted paginated query' do let(:sort_param) { } let(:first_param) { 2 } - let(:all_records) { lists.map { |list| global_id_of(list) } } + let(:all_records) { lists.map { |list| a_graphql_entity_for(list) } } end end end diff --git a/spec/requests/api/graphql/ci/job_spec.rb b/spec/requests/api/graphql/ci/job_spec.rb index ddb2664d353..2fb90dcd92b 100644 --- a/spec/requests/api/graphql/ci/job_spec.rb +++ b/spec/requests/api/graphql/ci/job_spec.rb @@ -47,10 +47,8 @@ RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do ) post_graphql(query, current_user: user) - expect(graphql_data_at(*path)).to match a_hash_including( - 'id' => global_id_of(job_2), - 'name' => job_2.name, - 'allowFailure' => job_2.allow_failure, + expect(graphql_data_at(*path)).to match a_graphql_entity_for( + job_2, :name, :allow_failure, 'duration' => 25, 'kind' => 'BUILD', 'queuedDuration' => 2.0, @@ -66,10 +64,7 @@ RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do it 'retrieves scalar fields' do post_graphql(query, current_user: user) - expect(graphql_data_at(*path)).to match a_hash_including( - 'id' => global_id_of(job_2), - 'name' => job_2.name - ) + expect(graphql_data_at(*path)).to match a_graphql_entity_for(job_2, :name) end end end @@ -102,8 +97,8 @@ RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do 'name' => test_stage.name, 'jobs' => a_hash_including( 'nodes' => contain_exactly( - a_hash_including('id' => global_id_of(job_2)), - a_hash_including('id' => global_id_of(job_3)) + a_graphql_entity_for(job_2), + a_graphql_entity_for(job_3) ) ) ) diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb index 922a9ab277e..847fa72522e 100644 --- a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb +++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb @@ -127,7 +127,7 @@ RSpec.describe 'container repository details' do let(:query) do <<~GQL - query($id: ID!, $n: Int) { + query($id: ContainerRepositoryID!, $n: Int) { containerRepository(id: $id) { tags(first: $n) { edges { @@ -157,7 +157,7 @@ RSpec.describe 'container repository details' do let(:query) do <<~GQL - query($id: ID!, $n: ContainerRepositoryTagSort) { + query($id: ContainerRepositoryID!, $n: ContainerRepositoryTagSort) { containerRepository(id: $id) { tags(sort: $n) { edges { @@ -194,7 +194,7 @@ RSpec.describe 'container repository details' do let(:query) do <<~GQL - query($id: ID!, $n: String) { + query($id: ContainerRepositoryID!, $n: String) { containerRepository(id: $id) { tags(name: $n) { edges { @@ -232,7 +232,7 @@ RSpec.describe 'container repository details' do let(:query) do <<~GQL - query($id: ID!) { + query($id: ContainerRepositoryID!) { containerRepository(id: $id) { size } diff --git a/spec/requests/api/graphql/current_user_todos_spec.rb b/spec/requests/api/graphql/current_user_todos_spec.rb index 7f37abba74a..da1c893ec2b 100644 --- a/spec/requests/api/graphql/current_user_todos_spec.rb +++ b/spec/requests/api/graphql/current_user_todos_spec.rb @@ -37,8 +37,8 @@ RSpec.describe 'A Todoable that implements the CurrentUserTodos interface' do post_graphql(query, current_user: current_user) expect(todoable_response).to contain_exactly( - a_hash_including('id' => global_id_of(done_todo)), - a_hash_including('id' => global_id_of(pending_todo)) + a_graphql_entity_for(done_todo), + a_graphql_entity_for(pending_todo) ) end @@ -63,7 +63,7 @@ RSpec.describe 'A Todoable that implements the CurrentUserTodos interface' do post_graphql(query, current_user: current_user) expect(todoable_response).to contain_exactly( - a_hash_including('id' => global_id_of(pending_todo)) + a_graphql_entity_for(pending_todo) ) end end @@ -75,7 +75,7 @@ RSpec.describe 'A Todoable that implements the CurrentUserTodos interface' do post_graphql(query, current_user: current_user) expect(todoable_response).to contain_exactly( - a_hash_including('id' => global_id_of(done_todo)) + a_graphql_entity_for(done_todo) ) end end diff --git a/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb index 3527c8183f6..c7149c100b2 100644 --- a/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb +++ b/spec/requests/api/graphql/group/dependency_proxy_manifests_spec.rb @@ -122,12 +122,12 @@ RSpec.describe 'getting dependency proxy manifests in a group' do let(:current_user) { owner } context 'with default sorting' do - let_it_be(:descending_manifests) { manifests.reverse.map { |manifest| global_id_of(manifest)} } + let_it_be(:descending_manifests) { manifests.reverse.map { |manifest| global_id_of(manifest) } } it_behaves_like 'sorted paginated query' do let(:sort_param) { '' } let(:first_param) { 2 } - let(:all_records) { descending_manifests } + let(:all_records) { descending_manifests.map(&:to_s) } end end diff --git a/spec/requests/api/graphql/group/group_members_spec.rb b/spec/requests/api/graphql/group/group_members_spec.rb index 8830320c6f7..fec866486ae 100644 --- a/spec/requests/api/graphql/group/group_members_spec.rb +++ b/spec/requests/api/graphql/group/group_members_spec.rb @@ -24,8 +24,8 @@ RSpec.describe 'getting group members information' do expect(graphql_errors).to be_nil expect(graphql_data_at(:group, :group_members, :edges, :node)).to contain_exactly( - { 'user' => { 'id' => global_id_of(user_1) } }, - { 'user' => { 'id' => global_id_of(user_2) } }, + { 'user' => a_graphql_entity_for(user_1) }, + { 'user' => a_graphql_entity_for(user_2) }, 'user' => nil ) end @@ -224,8 +224,8 @@ RSpec.describe 'getting group members information' do def expect_array_response(*items) expect(response).to have_gitlab_http_status(:success) - member_gids = graphql_data_at(:group, :group_members, :edges, :node, :user, :id) + members = graphql_data_at(:group, :group_members, :edges, :node, :user) - expect(member_gids).to match_array(items.map { |u| global_id_of(u) }) + expect(members).to match_array(items.map { |u| a_graphql_entity_for(u) }) end end diff --git a/spec/requests/api/graphql/group/merge_requests_spec.rb b/spec/requests/api/graphql/group/merge_requests_spec.rb index c0faff11c8d..434b0d16569 100644 --- a/spec/requests/api/graphql/group/merge_requests_spec.rb +++ b/spec/requests/api/graphql/group/merge_requests_spec.rb @@ -39,7 +39,7 @@ RSpec.describe 'Query.group.mergeRequests' do end def expected_mrs(mrs) - mrs.map { |mr| a_hash_including('id' => global_id_of(mr)) } + mrs.map { |mr| a_graphql_entity_for(mr) } end describe 'not passing any arguments' do diff --git a/spec/requests/api/graphql/group/milestones_spec.rb b/spec/requests/api/graphql/group/milestones_spec.rb index 2b80b5239c8..7c51409f907 100644 --- a/spec/requests/api/graphql/group/milestones_spec.rb +++ b/spec/requests/api/graphql/group/milestones_spec.rb @@ -170,10 +170,8 @@ RSpec.describe 'Milestones through GroupQuery' do end it 'returns correct values for scalar fields' do - expect(post_query).to eq({ - 'id' => global_id_of(milestone), - 'title' => milestone.title, - 'description' => milestone.description, + expect(post_query).to match a_graphql_entity_for( + milestone, :title, :description, 'state' => 'active', 'webPath' => milestone_path(milestone), 'dueDate' => milestone.due_date.iso8601, @@ -183,7 +181,7 @@ RSpec.describe 'Milestones through GroupQuery' do 'projectMilestone' => false, 'groupMilestone' => true, 'subgroupMilestone' => false - }) + ) end context 'milestone statistics' do diff --git a/spec/requests/api/graphql/issue/issue_spec.rb b/spec/requests/api/graphql/issue/issue_spec.rb index 42ca3348384..05fd6bf3022 100644 --- a/spec/requests/api/graphql/issue/issue_spec.rb +++ b/spec/requests/api/graphql/issue/issue_spec.rb @@ -8,8 +8,8 @@ RSpec.describe 'Query.issue(id)' do let_it_be(:project) { create(:project) } let_it_be(:issue) { create(:issue, project: project) } let_it_be(:current_user) { create(:user) } - let_it_be(:issue_params) { { 'id' => issue.to_global_id.to_s } } + let(:issue_params) { { 'id' => global_id_of(issue) } } let(:issue_data) { graphql_data['issue'] } let(:issue_fields) { all_graphql_fields_for('Issue'.classify) } @@ -100,7 +100,8 @@ RSpec.describe 'Query.issue(id)' do let_it_be(:issue_fields) { ['moved', 'movedTo { title }'] } let_it_be(:new_issue) { create(:issue) } let_it_be(:issue) { create(:issue, project: project, moved_to: new_issue) } - let_it_be(:issue_params) { { 'id' => issue.to_global_id.to_s } } + + let(:issue_params) { { 'id' => global_id_of(issue) } } before_all do new_issue.project.add_developer(current_user) diff --git a/spec/requests/api/graphql/merge_request/merge_request_spec.rb b/spec/requests/api/graphql/merge_request/merge_request_spec.rb index 75dd01a0763..d89f381753e 100644 --- a/spec/requests/api/graphql/merge_request/merge_request_spec.rb +++ b/spec/requests/api/graphql/merge_request/merge_request_spec.rb @@ -8,8 +8,8 @@ RSpec.describe 'Query.merge_request(id)' do let_it_be(:project) { create(:project, :empty_repo) } let_it_be(:merge_request) { create(:merge_request, source_project: project) } let_it_be(:current_user) { create(:user) } - let_it_be(:merge_request_params) { { 'id' => merge_request.to_global_id.to_s } } + let(:merge_request_params) { { 'id' => global_id_of(merge_request) } } let(:merge_request_data) { graphql_data['mergeRequest'] } let(:merge_request_fields) { all_graphql_fields_for('MergeRequest'.classify) } diff --git a/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb b/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb index 02b79dac489..715507c3cc5 100644 --- a/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/set_crm_contacts_spec.rb @@ -15,7 +15,7 @@ RSpec.describe 'Setting issues crm contacts' do let(:operation_mode) { Types::MutationOperationModeEnum.default_mode } let(:initial_contacts) { contacts[0..1] } let(:mutation_contacts) { contacts[1..2] } - let(:contact_ids) { contact_global_ids(mutation_contacts) } + let(:contact_ids) { mutation_contacts.map { global_id_of(_1) } } let(:does_not_exist_or_no_permission) { "The resource that you are attempting to access does not exist or you don't have permission to perform this action" } let(:mutation) do @@ -45,8 +45,8 @@ RSpec.describe 'Setting issues crm contacts' do graphql_mutation_response(:issue_set_crm_contacts) end - def contact_global_ids(contacts) - contacts.map { |contact| global_id_of(contact) } + def expected_contacts(contacts) + contacts.map { |contact| a_graphql_entity_for(contact) } end before do @@ -58,8 +58,8 @@ RSpec.describe 'Setting issues crm contacts' do it 'updates the issue with correct contacts' do post_graphql_mutation(mutation, current_user: user) - expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id)) - .to match_array(contact_global_ids(mutation_contacts)) + expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes)) + .to match_array(expected_contacts(mutation_contacts)) end end @@ -70,8 +70,8 @@ RSpec.describe 'Setting issues crm contacts' do it 'updates the issue with correct contacts' do post_graphql_mutation(mutation, current_user: user) - expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id)) - .to match_array(contact_global_ids(initial_contacts + mutation_contacts)) + expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes)) + .to match_array(expected_contacts(initial_contacts + mutation_contacts)) end end @@ -82,8 +82,8 @@ RSpec.describe 'Setting issues crm contacts' do it 'updates the issue with correct contacts' do post_graphql_mutation(mutation, current_user: user) - expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes, :id)) - .to match_array(contact_global_ids(initial_contacts - mutation_contacts)) + expect(graphql_data_at(:issue_set_crm_contacts, :issue, :customer_relations_contacts, :nodes)) + .to match_array(expected_contacts(initial_contacts - mutation_contacts)) end end end @@ -117,7 +117,7 @@ RSpec.describe 'Setting issues crm contacts' do it_behaves_like 'successful mutation' context 'when the contact does not exist' do - let(:contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] } + let(:contact_ids) { [global_id_of(model_name: 'CustomerRelations::Contact', id: non_existing_record_id)] } it 'returns expected error' do post_graphql_mutation(mutation, current_user: user) @@ -159,7 +159,7 @@ RSpec.describe 'Setting issues crm contacts' do context 'when trying to remove non-existent contact' do let(:operation_mode) { Types::MutationOperationModeEnum.enum[:remove] } - let(:contact_ids) { ["gid://gitlab/CustomerRelations::Contact/#{non_existing_record_id}"] } + let(:contact_ids) { [global_id_of(model_name: 'CustomerRelations::Contact', id: non_existing_record_id)] } it 'raises expected error' do post_graphql_mutation(mutation, current_user: user) diff --git a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb index 63b94dccca0..dee8f80bc5d 100644 --- a/spec/requests/api/graphql/mutations/notes/create/note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/create/note_spec.rb @@ -64,7 +64,7 @@ RSpec.describe 'Adding a Note' do it 'creates a Note in a discussion' do post_graphql_mutation(mutation, current_user: current_user) - expect(mutation_response['note']['discussion']['id']).to eq(discussion.to_global_id.to_s) + expect(mutation_response['note']['discussion']).to match a_graphql_entity_for(discussion) end context 'when the discussion_id is not for a Discussion' do diff --git a/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb b/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb index 89e3a71280f..0f7ccac3179 100644 --- a/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb +++ b/spec/requests/api/graphql/mutations/notes/reposition_image_diff_note_spec.rb @@ -39,7 +39,7 @@ RSpec.describe 'Repositioning an ImageDiffNote' do post_graphql_mutation(mutation, current_user: current_user) end.to change { note.reset.position.x }.to(10) - expect(mutation_response['note']).to eq('id' => global_id_of(note)) + expect(mutation_response['note']).to match a_graphql_entity_for(note) expect(mutation_response['errors']).to be_empty end @@ -59,7 +59,7 @@ RSpec.describe 'Repositioning an ImageDiffNote' do post_graphql_mutation(mutation, current_user: current_user) end.not_to change { note.reset.position.x } - expect(mutation_response['note']).to eq('id' => global_id_of(note)) + expect(mutation_response['note']).to match a_graphql_entity_for(note) expect(mutation_response['errors']).to be_empty end end diff --git a/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb b/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb index c5c34e16717..dc20fde8e3c 100644 --- a/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb +++ b/spec/requests/api/graphql/mutations/todos/mark_all_done_spec.rb @@ -46,8 +46,8 @@ RSpec.describe 'Marking all todos done' do expect(todo3.reload.state).to eq('done') expect(other_user_todo.reload.state).to eq('pending') - updated_todo_ids = mutation_response['todos'].map { |todo| todo['id'] } - expect(updated_todo_ids).to contain_exactly(global_id_of(todo1), global_id_of(todo3)) + updated_todos = mutation_response['todos'] + expect(updated_todos).to contain_exactly(a_graphql_entity_for(todo1), a_graphql_entity_for(todo3)) end context 'when target_id is given', :aggregate_failures do @@ -66,8 +66,8 @@ RSpec.describe 'Marking all todos done' do expect(todo1.reload.state).to eq('pending') expect(todo3.reload.state).to eq('pending') - updated_todo_ids = mutation_response['todos'].map { |todo| todo['id'] } - expect(updated_todo_ids).to contain_exactly(global_id_of(target_todo1), global_id_of(target_todo2)) + updated_todos = mutation_response['todos'] + expect(updated_todos).to contain_exactly(a_graphql_entity_for(target_todo1), a_graphql_entity_for(target_todo2)) end context 'when target does not exist' do diff --git a/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb b/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb index 70e3cc7f5cd..4316bd060c1 100644 --- a/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb +++ b/spec/requests/api/graphql/mutations/todos/restore_many_spec.rb @@ -11,8 +11,8 @@ RSpec.describe 'Restoring many Todos' do let_it_be(:author) { create(:user) } let_it_be(:other_user) { create(:user) } - let_it_be(:todo1) { create(:todo, user: current_user, author: author, state: :done, target: issue) } - let_it_be(:todo2) { create(:todo, user: current_user, author: author, state: :done, target: issue) } + let_it_be_with_reload(:todo1) { create(:todo, user: current_user, author: author, state: :done, target: issue) } + let_it_be_with_reload(:todo2) { create(:todo, user: current_user, author: author, state: :done, target: issue) } let_it_be(:other_user_todo) { create(:todo, user: other_user, author: author, state: :done) } @@ -50,8 +50,8 @@ RSpec.describe 'Restoring many Todos' do expect(mutation_response).to include( 'errors' => be_empty, 'todos' => contain_exactly( - { 'id' => global_id_of(todo1), 'state' => 'pending' }, - { 'id' => global_id_of(todo2), 'state' => 'pending' } + a_graphql_entity_for(todo1, 'state' => 'pending'), + a_graphql_entity_for(todo2, 'state' => 'pending') ) ) end diff --git a/spec/requests/api/graphql/packages/conan_spec.rb b/spec/requests/api/graphql/packages/conan_spec.rb index 84c5af33e5d..1f3732980d9 100644 --- a/spec/requests/api/graphql/packages/conan_spec.rb +++ b/spec/requests/api/graphql/packages/conan_spec.rb @@ -37,22 +37,19 @@ RSpec.describe 'conan package details' do it_behaves_like 'a package with files' it 'has the correct metadata' do - expect(metadata_response).to include( - 'id' => global_id_of(package.conan_metadatum), - 'recipe' => package.conan_metadatum.recipe, - 'packageChannel' => package.conan_metadatum.package_channel, - 'packageUsername' => package.conan_metadatum.package_username, - 'recipePath' => package.conan_metadatum.recipe_path + expect(metadata_response).to match( + a_graphql_entity_for(package.conan_metadatum, + :recipe, :package_channel, :package_username, :recipe_path) ) end it 'has the correct file metadata' do - expect(first_file_response_metadata).to include( - 'id' => global_id_of(first_file.conan_file_metadatum), - 'packageRevision' => first_file.conan_file_metadatum.package_revision, - 'conanPackageReference' => first_file.conan_file_metadatum.conan_package_reference, - 'recipeRevision' => first_file.conan_file_metadatum.recipe_revision, - 'conanFileType' => first_file.conan_file_metadatum.conan_file_type.upcase + expect(first_file_response_metadata).to match( + a_graphql_entity_for( + first_file.conan_file_metadatum, + :package_revision, :conan_package_reference, :recipe_revision, + conan_file_type: first_file.conan_file_metadatum.conan_file_type.upcase + ) ) end end diff --git a/spec/requests/api/graphql/packages/maven_spec.rb b/spec/requests/api/graphql/packages/maven_spec.rb index d28d32b0df5..9d59a922660 100644 --- a/spec/requests/api/graphql/packages/maven_spec.rb +++ b/spec/requests/api/graphql/packages/maven_spec.rb @@ -11,12 +11,8 @@ RSpec.describe 'maven package details' do shared_examples 'correct maven metadata' do it 'has the correct metadata' do - expect(metadata_response).to include( - 'id' => global_id_of(package.maven_metadatum), - 'path' => package.maven_metadatum.path, - 'appGroup' => package.maven_metadatum.app_group, - 'appVersion' => package.maven_metadatum.app_version, - 'appName' => package.maven_metadatum.app_name + expect(metadata_response).to match a_graphql_entity_for( + package.maven_metadatum, :path, :app_group, :app_version, :app_name ) end end diff --git a/spec/requests/api/graphql/packages/nuget_spec.rb b/spec/requests/api/graphql/packages/nuget_spec.rb index ba8d2ca42d2..87cffc67ce5 100644 --- a/spec/requests/api/graphql/packages/nuget_spec.rb +++ b/spec/requests/api/graphql/packages/nuget_spec.rb @@ -22,24 +22,19 @@ RSpec.describe 'nuget package details' do it_behaves_like 'a package with files' it 'has the correct metadata' do - expect(metadata_response).to include( - 'id' => global_id_of(package.nuget_metadatum), - 'licenseUrl' => package.nuget_metadatum.license_url, - 'projectUrl' => package.nuget_metadatum.project_url, - 'iconUrl' => package.nuget_metadatum.icon_url + expect(metadata_response).to match a_graphql_entity_for( + package.nuget_metadatum, :license_url, :project_url, :icon_url ) end it 'has dependency links' do - expect(dependency_link_response).to include( - 'id' => global_id_of(dependency_link), + expect(dependency_link_response).to match a_graphql_entity_for( + dependency_link, 'dependencyType' => dependency_link.dependency_type.upcase ) - expect(dependency_response).to include( - 'id' => global_id_of(dependency_link.dependency), - 'name' => dependency_link.dependency.name, - 'versionPattern' => dependency_link.dependency.version_pattern + expect(dependency_response).to match a_graphql_entity_for( + dependency_link.dependency, :name, :version_pattern ) end diff --git a/spec/requests/api/graphql/packages/pypi_spec.rb b/spec/requests/api/graphql/packages/pypi_spec.rb index 64fe7d29a7a..0cc5bd2e3b2 100644 --- a/spec/requests/api/graphql/packages/pypi_spec.rb +++ b/spec/requests/api/graphql/packages/pypi_spec.rb @@ -19,9 +19,8 @@ RSpec.describe 'pypi package details' do it_behaves_like 'a package with files' it 'has the correct metadata' do - expect(metadata_response).to include( - 'id' => global_id_of(package.pypi_metadatum), - 'requiredPython' => package.pypi_metadatum.required_python + expect(metadata_response).to match a_graphql_entity_for( + package.pypi_metadatum, :required_python ) end end diff --git a/spec/requests/api/graphql/project/alert_management/integrations_spec.rb b/spec/requests/api/graphql/project/alert_management/integrations_spec.rb index 1793d4961eb..773922c1864 100644 --- a/spec/requests/api/graphql/project/alert_management/integrations_spec.rb +++ b/spec/requests/api/graphql/project/alert_management/integrations_spec.rb @@ -53,33 +53,24 @@ RSpec.describe 'getting Alert Management Integrations' do end context 'when no extra params given' do - let(:http_integration_response) { integrations.first } - let(:prometheus_integration_response) { integrations.second } - it_behaves_like 'a working graphql query' - it { expect(integrations.size).to eq(2) } - it 'returns the correct properties of the integrations' do - expect(http_integration_response).to include( - 'id' => global_id_of(active_http_integration), - 'type' => 'HTTP', - 'name' => active_http_integration.name, - 'active' => active_http_integration.active, - 'token' => active_http_integration.token, - 'url' => active_http_integration.url, - 'apiUrl' => nil - ) - - expect(prometheus_integration_response).to include( - 'id' => global_id_of(prometheus_integration), - 'type' => 'PROMETHEUS', - 'name' => 'Prometheus', - 'active' => prometheus_integration.manual_configuration?, - 'token' => project_alerting_setting.token, - 'url' => "http://localhost/#{project.full_path}/prometheus/alerts/notify.json", - 'apiUrl' => prometheus_integration.api_url - ) + expect(integrations).to match [ + a_graphql_entity_for( + active_http_integration, + :name, :active, :token, :url, type: 'HTTP', api_url: nil + ), + a_graphql_entity_for( + prometheus_integration, + 'type' => 'PROMETHEUS', + 'name' => 'Prometheus', + 'active' => prometheus_integration.manual_configuration?, + 'token' => project_alerting_setting.token, + 'url' => "http://localhost/#{project.full_path}/prometheus/alerts/notify.json", + 'apiUrl' => prometheus_integration.api_url + ) + ] end end @@ -88,17 +79,9 @@ RSpec.describe 'getting Alert Management Integrations' do it_behaves_like 'a working graphql query' - it { expect(integrations).to be_one } - it 'returns the correct properties of the HTTP integration' do - expect(integrations.first).to include( - 'id' => global_id_of(active_http_integration), - 'type' => 'HTTP', - 'name' => active_http_integration.name, - 'active' => active_http_integration.active, - 'token' => active_http_integration.token, - 'url' => active_http_integration.url, - 'apiUrl' => nil + expect(integrations).to contain_exactly a_graphql_entity_for( + active_http_integration, :name, :active, :token, :url, type: 'HTTP', api_url: nil ) end end @@ -108,11 +91,9 @@ RSpec.describe 'getting Alert Management Integrations' do it_behaves_like 'a working graphql query' - it { expect(integrations).to be_one } - it 'returns the correct properties of the Prometheus Integration' do - expect(integrations.first).to include( - 'id' => global_id_of(prometheus_integration), + expect(integrations).to contain_exactly a_graphql_entity_for( + prometheus_integration, 'type' => 'PROMETHEUS', 'name' => 'Prometheus', 'active' => prometheus_integration.manual_configuration?, diff --git a/spec/requests/api/graphql/project/cluster_agents_spec.rb b/spec/requests/api/graphql/project/cluster_agents_spec.rb index c9900fea277..a34df0ee6f4 100644 --- a/spec/requests/api/graphql/project/cluster_agents_spec.rb +++ b/spec/requests/api/graphql/project/cluster_agents_spec.rb @@ -29,7 +29,7 @@ RSpec.describe 'Project.cluster_agents' do post_graphql(query, current_user: current_user) expect(graphql_data_at(:project, :cluster_agents, :nodes)).to match_array( - agents.map { |agent| a_hash_including('id' => global_id_of(agent)) } + agents.map { |agent| a_graphql_entity_for(agent) } ) end @@ -62,9 +62,9 @@ RSpec.describe 'Project.cluster_agents' do tokens = graphql_data_at(:project, :cluster_agents, :nodes, :tokens, :nodes) expect(tokens).to match([ - a_hash_including('id' => global_id_of(token_3)), - a_hash_including('id' => global_id_of(token_2)), - a_hash_including('id' => global_id_of(token_1)) + a_graphql_entity_for(token_3), + a_graphql_entity_for(token_2), + a_graphql_entity_for(token_1) ]) end diff --git a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb index f544d78ecbb..8cda61f0628 100644 --- a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb +++ b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb @@ -71,11 +71,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha) it 'finds all the designs as of the given version' do post_query - expect(data).to match( - a_hash_including( - 'id' => global_id_of(design_at_version), - 'filename' => design.filename - )) + expect(data).to match a_graphql_entity_for(design_at_version, filename: design.filename) end context 'when the current_user is not authorized' do @@ -119,7 +115,8 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha) let(:results) do issue.designs.visible_at_version(version).map do |d| dav = build(:design_at_version, design: d, version: version) - { 'id' => global_id_of(dav), 'filename' => d.filename } + + a_graphql_entity_for(dav, filename: d.filename) end end @@ -132,8 +129,8 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha) describe 'filtering' do let(:designs) { issue.designs.sample(3) } let(:filenames) { designs.map(&:filename) } - let(:ids) do - designs.map { |d| global_id_of(build(:design_at_version, design: d, version: version)) } + let(:expected_designs) do + designs.map { |d| a_graphql_entity_for(build(:design_at_version, design: d, version: version)) } end before do @@ -144,7 +141,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha) let(:dav_params) { { filenames: filenames } } it 'finds the designs by filename' do - expect(data.map { |e| e.dig('node', 'id') }).to match_array(ids) + expect(data.map { |e| e['node'] }).to match_array expected_designs end end @@ -160,9 +157,9 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha) describe 'pagination' do let(:end_cursor) { graphql_data_at(*path_prefix, :designs_at_version, :page_info, :end_cursor) } - let(:ids) do + let(:entities) do ::DesignManagement::Design.visible_at_version(version).order(:id).map do |d| - global_id_of(build(:design_at_version, design: d, version: version)) + a_graphql_entity_for(build(:design_at_version, design: d, version: version)) end end @@ -178,19 +175,19 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha) let(:fields) { ['pageInfo { endCursor }', 'edges { node { id } }'] } def response_values(data = graphql_data) - data.dig(*path).map { |e| e.dig('node', 'id') } + data.dig(*path).map { |e| e['node'] } end it 'sorts designs for reliable pagination' do post_graphql(query, current_user: current_user) - expect(response_values).to match_array(ids.take(2)) + expect(response_values).to match_array(entities.take(2)) post_graphql(cursored_query, current_user: current_user) new_data = Gitlab::Json.parse(response.body).fetch('data') - expect(response_values(new_data)).to match_array(ids.drop(2)) + expect(response_values(new_data)).to match_array(entities.drop(2)) end end end @@ -202,9 +199,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha) end let(:results) do - version.designs.map do |design| - { 'id' => global_id_of(design), 'filename' => design.filename } - end + version.designs.map { |design| a_graphql_entity_for(design, :filename) } end it 'finds all the designs as of the given version' do diff --git a/spec/requests/api/graphql/project/issue/designs/designs_spec.rb b/spec/requests/api/graphql/project/issue/designs/designs_spec.rb index 459a30508eb..02bc9457c07 100644 --- a/spec/requests/api/graphql/project/issue/designs/designs_spec.rb +++ b/spec/requests/api/graphql/project/issue/designs/designs_spec.rb @@ -58,8 +58,8 @@ RSpec.describe 'Getting designs related to an issue' do post_graphql(query, current_user: current_user) - expect(design_response).to eq( - 'id' => design.to_global_id.to_s, + expect(design_response).to match a_graphql_entity_for( + design, 'event' => 'CREATION', 'fullPath' => design.full_path, 'filename' => design.filename, @@ -93,7 +93,7 @@ RSpec.describe 'Getting designs related to an issue' do let(:end_cursor) { design_collection.dig('designs', 'pageInfo', 'endCursor') } - let(:ids) { issue.designs.order(:id).map { |d| global_id_of(d) } } + let(:expected_designs) { issue.designs.order(:id).map { |d| a_graphql_entity_for(d) } } let(:query) { make_query(designs_fragment(first: 2)) } @@ -107,19 +107,19 @@ RSpec.describe 'Getting designs related to an issue' do query_graphql_field(:designs, params, design_query_fields) end - def response_ids(data = graphql_data) + def response_designs(data = graphql_data) path = %w[project issue designCollection designs edges] - data.dig(*path).map { |e| e.dig('node', 'id') } + data.dig(*path).map { |e| e['node'] } end it 'sorts designs for reliable pagination' do - expect(response_ids).to match_array(ids.take(2)) + expect(response_designs).to match_array(expected_designs.take(2)) post_graphql(cursored_query, current_user: current_user) new_data = Gitlab::Json.parse(response.body).fetch('data') - expect(response_ids(new_data)).to match_array(ids.drop(2)) + expect(response_designs(new_data)).to match_array(expected_designs.drop(2)) end end diff --git a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb index de2ace95757..3b1eb0b4b02 100644 --- a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb +++ b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb @@ -51,7 +51,7 @@ RSpec.describe 'Getting designs related to an issue' do design_data = designs_data['nodes'].first note_data = design_data['notes']['nodes'].first - expect(note_data['id']).to eq(note.to_global_id.to_s) + expect(note_data).to match(a_graphql_entity_for(note)) end def query(note_fields = all_graphql_fields_for(Note, max_depth: 1)) diff --git a/spec/requests/api/graphql/project/issue_spec.rb b/spec/requests/api/graphql/project/issue_spec.rb index ddf63a8f2c9..2415e9ef60f 100644 --- a/spec/requests/api/graphql/project/issue_spec.rb +++ b/spec/requests/api/graphql/project/issue_spec.rb @@ -144,10 +144,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid)' do data = graphql_data.dig(*path) - expect(data).to match( - a_hash_including('id' => global_id_of(version), - 'sha' => version.sha) - ) + expect(data).to match a_graphql_entity_for(version, :sha) end end @@ -184,6 +181,6 @@ RSpec.describe 'Query.project(fullPath).issue(iid)' do end def id_hash(object) - a_hash_including('id' => global_id_of(object)) + a_graphql_entity_for(object) end end diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb index cefe88aafc8..395b052369e 100644 --- a/spec/requests/api/graphql/project/merge_request_spec.rb +++ b/spec/requests/api/graphql/project/merge_request_spec.rb @@ -66,7 +66,7 @@ RSpec.describe 'getting merge request information nested in a project' do it 'includes reviewers' do expected = merge_request.reviewers.map do |r| - a_hash_including('id' => global_id_of(r), 'username' => r.username) + a_graphql_entity_for(r, :username) end post_graphql(query, current_user: current_user) diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb index 303748bc70e..c7f121c48ab 100644 --- a/spec/requests/api/graphql/project/merge_requests_spec.rb +++ b/spec/requests/api/graphql/project/merge_requests_spec.rb @@ -9,7 +9,7 @@ RSpec.describe 'getting merge request listings nested in a project' do let_it_be(:current_user) { create(:user) } let_it_be(:label) { create(:label, project: project) } - let_it_be(:merge_request_a) do + let_it_be_with_reload(:merge_request_a) do create(:labeled_merge_request, :unique_branches, source_project: project, labels: [label]) end @@ -412,6 +412,10 @@ RSpec.describe 'getting merge request listings nested in a project' do describe 'sorting and pagination' do let(:data_path) { [:project, :mergeRequests] } + def pagination_results_data(nodes) + nodes + end + def pagination_query(params) graphql_query_for(:project, { full_path: project.full_path }, <<~QUERY) mergeRequests(#{params}) { @@ -429,7 +433,7 @@ RSpec.describe 'getting merge request listings nested in a project' do merge_request_c, merge_request_e, merge_request_a - ].map { |mr| global_id_of(mr) } + ].map { |mr| a_graphql_entity_for(mr) } end before do @@ -455,7 +459,7 @@ RSpec.describe 'getting merge request listings nested in a project' do query = pagination_query(params) post_graphql(query, current_user: current_user) - expect(results.map { |item| item["id"] }).to eq(all_records.last(2)) + expect(results).to match(all_records.last(2)) end end end @@ -469,7 +473,7 @@ RSpec.describe 'getting merge request listings nested in a project' do merge_request_c, merge_request_e, merge_request_a - ].map { |mr| global_id_of(mr) } + ].map { |mr| a_graphql_entity_for(mr) } end before do @@ -495,7 +499,7 @@ RSpec.describe 'getting merge request listings nested in a project' do query = pagination_query(params) post_graphql(query, current_user: current_user) - expect(results.map { |item| item["id"] }).to eq(all_records.last(2)) + expect(results).to match(all_records.last(2)) end end end diff --git a/spec/requests/api/graphql/project/milestones_spec.rb b/spec/requests/api/graphql/project/milestones_spec.rb index 2fede4c7285..3e8948d83b1 100644 --- a/spec/requests/api/graphql/project/milestones_spec.rb +++ b/spec/requests/api/graphql/project/milestones_spec.rb @@ -33,7 +33,7 @@ RSpec.describe 'getting milestone listings nested in a project' do def result_list(expected) expected.map do |milestone| - a_hash_including('id' => global_id_of(milestone)) + a_graphql_entity_for(milestone) end end diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb index 73e02e2a4b1..ccf97918021 100644 --- a/spec/requests/api/graphql/project/pipeline_spec.rb +++ b/spec/requests/api/graphql/project/pipeline_spec.rb @@ -89,17 +89,16 @@ RSpec.describe 'getting pipeline information nested in a project' do post_graphql(query, current_user: current_user) expect(graphql_data_at(*path, :jobs, :nodes)).to contain_exactly( - a_hash_including( - 'name' => build_job.name, - 'status' => build_job.status.upcase, - 'duration' => build_job.duration + a_graphql_entity_for( + build_job, :name, :duration, + 'status' => build_job.status.upcase ), - a_hash_including( - 'id' => global_id_of(failed_build), + a_graphql_entity_for( + failed_build, 'status' => failed_build.status.upcase ), - a_hash_including( - 'id' => global_id_of(bridge), + a_graphql_entity_for( + bridge, 'status' => bridge.status.upcase ) ) @@ -135,7 +134,7 @@ RSpec.describe 'getting pipeline information nested in a project' do post_graphql(query, current_user: current_user, variables: variables) expect(graphql_data_at(*path, :jobs, :nodes)) - .to contain_exactly(a_hash_including('id' => global_id_of(failed_build))) + .to contain_exactly(a_graphql_entity_for(failed_build)) end end @@ -166,7 +165,7 @@ RSpec.describe 'getting pipeline information nested in a project' do end let(:the_job) do - a_hash_including('name' => build_job.name, 'id' => global_id_of(build_job)) + a_graphql_entity_for(build_job, :name) end it 'can request a build by name' do diff --git a/spec/requests/api/graphql/project/project_members_spec.rb b/spec/requests/api/graphql/project/project_members_spec.rb index 315d44884ff..c3281b44954 100644 --- a/spec/requests/api/graphql/project/project_members_spec.rb +++ b/spec/requests/api/graphql/project/project_members_spec.rb @@ -60,7 +60,10 @@ RSpec.describe 'getting project members information' do fetch_members(project: parent_project, args: { relations: [:DIRECT] }) expect(graphql_errors).to be_nil - expect(graphql_data_at(:project, :project_members, :edges, :node)).to contain_exactly({ 'user' => { 'id' => global_id_of(user) } }, 'user' => nil) + expect(graphql_data_at(:project, :project_members, :edges, :node)).to contain_exactly( + a_graphql_entity_for(user: a_graphql_entity_for(user)), + { 'user' => nil } + ) end end @@ -238,7 +241,7 @@ RSpec.describe 'getting project members information' do def expect_array_response(*items) expect(response).to have_gitlab_http_status(:success) - member_gids = graphql_data_at(:project, :project_members, :edges, :node, :user, :id) - expect(member_gids).to match_array(items.map { |u| global_id_of(u) }) + members = graphql_data_at(:project, :project_members, :edges, :node, :user) + expect(members).to match_array(items.map { |u| a_graphql_entity_for(u) }) end end diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb index 77abac4ef04..c4899dbb71e 100644 --- a/spec/requests/api/graphql/project/release_spec.rb +++ b/spec/requests/api/graphql/project/release_spec.rb @@ -77,10 +77,10 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do post_query expected = release.milestones.order_by_dates_and_title.map do |milestone| - { 'id' => global_id_of(milestone), 'title' => milestone.title } + a_graphql_entity_for(milestone, :title) end - expect(data).to eq(expected) + expect(data).to match(expected) end end @@ -94,10 +94,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do it 'finds the author of the release' do post_query - expect(data).to eq( - 'id' => global_id_of(release.author), - 'username' => release.author.username - ) + expect(data).to match a_graphql_entity_for(release.author, :username) end end @@ -142,13 +139,11 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do post_query expected = release.links.map do |link| - { - 'id' => global_id_of(link), - 'name' => link.name, - 'url' => link.url, + a_graphql_entity_for( + link, :name, :url, 'external' => link.external?, 'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << "/downloads#{link.filepath}" : link.url - } + ) end expect(data).to match_array(expected) @@ -218,10 +213,8 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do evidence = release.evidences.first.present - expect(data["nodes"].first).to eq( - 'id' => global_id_of(evidence), - 'sha' => evidence.sha, - 'filepath' => evidence.filepath, + expect(data["nodes"].first).to match a_graphql_entity_for( + evidence, :sha, :filepath, 'collectedAt' => evidence.collected_at.utc.iso8601 ) end @@ -274,10 +267,10 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do post_query expected = release.milestones.order_by_dates_and_title.map do |milestone| - { 'id' => global_id_of(milestone), 'title' => milestone.title } + a_graphql_entity_for(milestone, :title) end - expect(data).to eq(expected) + expect(data).to match(expected) end end @@ -291,10 +284,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do it 'finds the author of the release' do post_query - expect(data).to eq( - 'id' => global_id_of(release.author), - 'username' => release.author.username - ) + expect(data).to match a_graphql_entity_for(release.author, :username) end end @@ -339,13 +329,11 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do post_query expected = release.links.map do |link| - { - 'id' => global_id_of(link), - 'name' => link.name, - 'url' => link.url, + a_graphql_entity_for( + link, :name, :url, 'external' => true, 'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << "/downloads#{link.filepath}" : link.url - } + ) end expect(data).to match_array(expected) diff --git a/spec/requests/api/graphql/project/terraform/state_spec.rb b/spec/requests/api/graphql/project/terraform/state_spec.rb index 9f1d9ab204a..8f2d2cffef2 100644 --- a/spec/requests/api/graphql/project/terraform/state_spec.rb +++ b/spec/requests/api/graphql/project/terraform/state_spec.rb @@ -57,22 +57,22 @@ RSpec.describe 'query a single terraform state' do it_behaves_like 'a working graphql query' it 'returns terraform state data' do - expect(data).to match(a_hash_including({ - 'id' => global_id_of(terraform_state), - 'name' => terraform_state.name, + expect(data).to match a_graphql_entity_for( + terraform_state, + :name, 'lockedAt' => terraform_state.locked_at.iso8601, 'createdAt' => terraform_state.created_at.iso8601, 'updatedAt' => terraform_state.updated_at.iso8601, - 'lockedByUser' => { 'id' => global_id_of(terraform_state.locked_by_user) }, - 'latestVersion' => { - 'id' => eq(global_id_of(latest_version)), + 'lockedByUser' => a_graphql_entity_for(terraform_state.locked_by_user), + 'latestVersion' => a_graphql_entity_for( + latest_version, 'serial' => eq(latest_version.version), 'createdAt' => eq(latest_version.created_at.iso8601), 'updatedAt' => eq(latest_version.updated_at.iso8601), - 'createdByUser' => { 'id' => eq(global_id_of(latest_version.created_by_user)) }, + 'createdByUser' => a_graphql_entity_for(latest_version.created_by_user), 'job' => { 'name' => eq(latest_version.build.name) } - } - })) + ) + ) end context 'unauthorized users' do diff --git a/spec/requests/api/graphql/project/terraform/states_spec.rb b/spec/requests/api/graphql/project/terraform/states_spec.rb index 2879530acc5..a7ec6f69776 100644 --- a/spec/requests/api/graphql/project/terraform/states_spec.rb +++ b/spec/requests/api/graphql/project/terraform/states_spec.rb @@ -62,23 +62,22 @@ RSpec.describe 'query terraform states' do ) ) - expect(data['nodes']).to contain_exactly({ - 'id' => global_id_of(terraform_state), - 'name' => terraform_state.name, + expect(data['nodes']).to contain_exactly a_graphql_entity_for( + terraform_state, :name, 'lockedAt' => terraform_state.locked_at.iso8601, 'createdAt' => terraform_state.created_at.iso8601, 'updatedAt' => terraform_state.updated_at.iso8601, - 'lockedByUser' => { 'id' => global_id_of(terraform_state.locked_by_user) }, - 'latestVersion' => { - 'id' => eq(global_id_of(latest_version)), + 'lockedByUser' => a_graphql_entity_for(terraform_state.locked_by_user), + 'latestVersion' => a_graphql_entity_for( + latest_version, 'serial' => eq(latest_version.version), 'downloadPath' => eq(download_path), 'createdAt' => eq(latest_version.created_at.iso8601), 'updatedAt' => eq(latest_version.updated_at.iso8601), - 'createdByUser' => { 'id' => eq(global_id_of(latest_version.created_by_user)) }, + 'createdByUser' => a_graphql_entity_for(latest_version.created_by_user), 'job' => { 'name' => eq(latest_version.build.name) } - } - }) + ) + ) end it 'returns count of terraform states' do diff --git a/spec/requests/api/graphql/query_spec.rb b/spec/requests/api/graphql/query_spec.rb index d650acc8354..4aa9c4b8254 100644 --- a/spec/requests/api/graphql/query_spec.rb +++ b/spec/requests/api/graphql/query_spec.rb @@ -76,10 +76,8 @@ RSpec.describe 'Query' do it_behaves_like 'a working graphql query' it_behaves_like 'a query that needs authorization' - context 'the current user is able to read designs' do - it 'fetches the expected data' do - expect(query_result).to eq('id' => global_id_of(version), 'sha' => version.sha) - end + it 'fetches the expected data' do + expect(query_result).to match a_graphql_entity_for(version, :sha) end end @@ -106,13 +104,13 @@ RSpec.describe 'Query' do context 'the current user is able to read designs' do it 'fetches the expected data, including the correct associations' do - expect(query_result).to eq( - 'id' => global_id_of(design_at_version), + expect(query_result).to match a_graphql_entity_for( + design_at_version, 'filename' => design_at_version.design.filename, - 'version' => { 'id' => global_id_of(version), 'sha' => version.sha }, - 'design' => { 'id' => global_id_of(design) }, + 'version' => a_graphql_entity_for(version, :sha), + 'design' => a_graphql_entity_for(design), 'issue' => { 'title' => issue.title, 'iid' => issue.iid.to_s }, - 'project' => { 'id' => global_id_of(project), 'fullPath' => project.full_path } + 'project' => a_graphql_entity_for(project, :full_path) ) end end diff --git a/spec/requests/api/graphql/user/starred_projects_query_spec.rb b/spec/requests/api/graphql/user/starred_projects_query_spec.rb index a8c087d1fbf..37a85b98e5f 100644 --- a/spec/requests/api/graphql/user/starred_projects_query_spec.rb +++ b/spec/requests/api/graphql/user/starred_projects_query_spec.rb @@ -42,7 +42,7 @@ RSpec.describe 'Getting starredProjects of the user' do it 'found only public project' do expect(starred_projects).to contain_exactly( - a_hash_including('id' => global_id_of(project_a)) + a_graphql_entity_for(project_a) ) end @@ -51,9 +51,9 @@ RSpec.describe 'Getting starredProjects of the user' do it 'found all projects' do expect(starred_projects).to contain_exactly( - a_hash_including('id' => global_id_of(project_a)), - a_hash_including('id' => global_id_of(project_b)), - a_hash_including('id' => global_id_of(project_c)) + a_graphql_entity_for(project_a), + a_graphql_entity_for(project_b), + a_graphql_entity_for(project_c) ) end end @@ -69,8 +69,8 @@ RSpec.describe 'Getting starredProjects of the user' do it 'finds public and member projects' do expect(starred_projects).to contain_exactly( - a_hash_including('id' => global_id_of(project_a)), - a_hash_including('id' => global_id_of(project_b)) + a_graphql_entity_for(project_a), + a_graphql_entity_for(project_b) ) end end @@ -93,9 +93,9 @@ RSpec.describe 'Getting starredProjects of the user' do it 'finds all projects starred by the user, which the current user has access to' do expect(starred_projects).to contain_exactly( - a_hash_including('id' => global_id_of(project_a)), - a_hash_including('id' => global_id_of(project_b)), - a_hash_including('id' => global_id_of(project_c)) + a_graphql_entity_for(project_a), + a_graphql_entity_for(project_b), + a_graphql_entity_for(project_c) ) end end diff --git a/spec/requests/api/graphql/user_query_spec.rb b/spec/requests/api/graphql/user_query_spec.rb index 1cba3674d25..8f286180617 100644 --- a/spec/requests/api/graphql/user_query_spec.rb +++ b/spec/requests/api/graphql/user_query_spec.rb @@ -91,11 +91,11 @@ RSpec.describe 'getting user information' do presenter = UserPresenter.new(user) expect(graphql_data['user']).to match( - a_hash_including( - 'id' => global_id_of(user), + a_graphql_entity_for( + user, + :username, 'state' => presenter.state, 'name' => presenter.name, - 'username' => presenter.username, 'webUrl' => presenter.web_url, 'avatarUrl' => presenter.avatar_url, 'email' => presenter.public_email, @@ -121,9 +121,9 @@ RSpec.describe 'getting user information' do it 'can be found' do expect(assigned_mrs).to contain_exactly( - a_hash_including('id' => global_id_of(assigned_mr)), - a_hash_including('id' => global_id_of(assigned_mr_b)), - a_hash_including('id' => global_id_of(assigned_mr_c)) + a_graphql_entity_for(assigned_mr), + a_graphql_entity_for(assigned_mr_b), + a_graphql_entity_for(assigned_mr_c) ) end @@ -145,7 +145,7 @@ RSpec.describe 'getting user information' do it 'selects the correct MRs' do expect(assigned_mrs).to contain_exactly( - a_hash_including('id' => global_id_of(assigned_mr_b)) + a_graphql_entity_for(assigned_mr_b) ) end end @@ -157,8 +157,8 @@ RSpec.describe 'getting user information' do it 'selects the correct MRs' do expect(assigned_mrs).to contain_exactly( - a_hash_including('id' => global_id_of(assigned_mr_b)), - a_hash_including('id' => global_id_of(assigned_mr_c)) + a_graphql_entity_for(assigned_mr_b), + a_graphql_entity_for(assigned_mr_c) ) end end @@ -169,7 +169,7 @@ RSpec.describe 'getting user information' do it 'finds the authored mrs' do expect(assigned_mrs).to contain_exactly( - a_hash_including('id' => global_id_of(assigned_mr_b)) + a_graphql_entity_for(assigned_mr_b) ) end end @@ -185,8 +185,8 @@ RSpec.describe 'getting user information' do post_graphql(query, current_user: current_user) expect(assigned_mrs).to contain_exactly( - a_hash_including('id' => global_id_of(assigned_mr_b)), - a_hash_including('id' => global_id_of(assigned_mr_c)) + a_graphql_entity_for(assigned_mr_b), + a_graphql_entity_for(assigned_mr_c) ) end end @@ -212,9 +212,9 @@ RSpec.describe 'getting user information' do it 'can be found' do expect(reviewed_mrs).to contain_exactly( - a_hash_including('id' => global_id_of(reviewed_mr)), - a_hash_including('id' => global_id_of(reviewed_mr_b)), - a_hash_including('id' => global_id_of(reviewed_mr_c)) + a_graphql_entity_for(reviewed_mr), + a_graphql_entity_for(reviewed_mr_b), + a_graphql_entity_for(reviewed_mr_c) ) end @@ -236,7 +236,7 @@ RSpec.describe 'getting user information' do it 'selects the correct MRs' do expect(reviewed_mrs).to contain_exactly( - a_hash_including('id' => global_id_of(reviewed_mr_b)) + a_graphql_entity_for(reviewed_mr_b) ) end end @@ -248,8 +248,8 @@ RSpec.describe 'getting user information' do it 'selects the correct MRs' do expect(reviewed_mrs).to contain_exactly( - a_hash_including('id' => global_id_of(reviewed_mr_b)), - a_hash_including('id' => global_id_of(reviewed_mr_c)) + a_graphql_entity_for(reviewed_mr_b), + a_graphql_entity_for(reviewed_mr_c) ) end end @@ -260,7 +260,7 @@ RSpec.describe 'getting user information' do it 'finds the authored mrs' do expect(reviewed_mrs).to contain_exactly( - a_hash_including('id' => global_id_of(reviewed_mr_b)) + a_graphql_entity_for(reviewed_mr_b) ) end end @@ -275,7 +275,7 @@ RSpec.describe 'getting user information' do post_graphql(query, current_user: current_user) expect(reviewed_mrs).to contain_exactly( - a_hash_including('id' => global_id_of(reviewed_mr_c)) + a_graphql_entity_for(reviewed_mr_c) ) end end @@ -301,9 +301,9 @@ RSpec.describe 'getting user information' do it 'can be found' do expect(authored_mrs).to contain_exactly( - a_hash_including('id' => global_id_of(authored_mr)), - a_hash_including('id' => global_id_of(authored_mr_b)), - a_hash_including('id' => global_id_of(authored_mr_c)) + a_graphql_entity_for(authored_mr), + a_graphql_entity_for(authored_mr_b), + a_graphql_entity_for(authored_mr_c) ) end @@ -329,8 +329,8 @@ RSpec.describe 'getting user information' do post_graphql(query, current_user: current_user) expect(authored_mrs).to contain_exactly( - a_hash_including('id' => global_id_of(authored_mr)), - a_hash_including('id' => global_id_of(authored_mr_c)) + a_graphql_entity_for(authored_mr), + a_graphql_entity_for(authored_mr_c) ) end end @@ -346,8 +346,8 @@ RSpec.describe 'getting user information' do post_graphql(query, current_user: current_user) expect(authored_mrs).to contain_exactly( - a_hash_including('id' => global_id_of(authored_mr_b)), - a_hash_including('id' => global_id_of(authored_mr_c)) + a_graphql_entity_for(authored_mr_b), + a_graphql_entity_for(authored_mr_c) ) end end @@ -359,7 +359,7 @@ RSpec.describe 'getting user information' do it 'selects the correct MRs' do expect(authored_mrs).to contain_exactly( - a_hash_including('id' => global_id_of(authored_mr_b)) + a_graphql_entity_for(authored_mr_b) ) end end @@ -371,8 +371,8 @@ RSpec.describe 'getting user information' do it 'selects the correct MRs' do expect(authored_mrs).to contain_exactly( - a_hash_including('id' => global_id_of(authored_mr_b)), - a_hash_including('id' => global_id_of(authored_mr_c)) + a_graphql_entity_for(authored_mr_b), + a_graphql_entity_for(authored_mr_c) ) end end @@ -417,7 +417,7 @@ RSpec.describe 'getting user information' do it 'can be found' do expect(group_memberships).to include( - a_hash_including('id' => global_id_of(membership_a)) + a_graphql_entity_for(membership_a) ) end end @@ -440,7 +440,7 @@ RSpec.describe 'getting user information' do it 'can be found' do expect(project_memberships).to include( - a_hash_including('id' => global_id_of(membership_a)) + a_graphql_entity_for(membership_a) ) end end @@ -460,7 +460,7 @@ RSpec.describe 'getting user information' do it 'can be found' do expect(authored_mrs).to include( - a_hash_including('id' => global_id_of(authored_mr)) + a_graphql_entity_for(authored_mr) ) end end @@ -480,9 +480,9 @@ RSpec.describe 'getting user information' do it 'can be found' do expect(assigned_mrs).to contain_exactly( - a_hash_including('id' => global_id_of(assigned_mr)), - a_hash_including('id' => global_id_of(assigned_mr_b)), - a_hash_including('id' => global_id_of(assigned_mr_c)) + a_graphql_entity_for(assigned_mr), + a_graphql_entity_for(assigned_mr_b), + a_graphql_entity_for(assigned_mr_c) ) end end diff --git a/spec/requests/api/graphql/users_spec.rb b/spec/requests/api/graphql/users_spec.rb index fe824834a2c..a6bbfc75451 100644 --- a/spec/requests/api/graphql/users_spec.rb +++ b/spec/requests/api/graphql/users_spec.rb @@ -72,12 +72,12 @@ RSpec.describe 'Users' do post_query expect(graphql_data.dig('users', 'nodes')).to include( - { "id" => user0.to_global_id.to_s }, - { "id" => user1.to_global_id.to_s }, - { "id" => user2.to_global_id.to_s }, - { "id" => user3.to_global_id.to_s }, - { "id" => admin.to_global_id.to_s }, - { "id" => another_admin.to_global_id.to_s } + a_graphql_entity_for(user0), + a_graphql_entity_for(user1), + a_graphql_entity_for(user2), + a_graphql_entity_for(user3), + a_graphql_entity_for(admin), + a_graphql_entity_for(another_admin) ) end end @@ -91,15 +91,15 @@ RSpec.describe 'Users' do post_graphql(query, current_user: current_user) expect(graphql_data.dig('users', 'nodes')).to include( - { "id" => another_admin.to_global_id.to_s }, - { "id" => admin.to_global_id.to_s } + a_graphql_entity_for(another_admin), + a_graphql_entity_for(admin) ) expect(graphql_data.dig('users', 'nodes')).not_to include( - { "id" => user0.to_global_id.to_s }, - { "id" => user1.to_global_id.to_s }, - { "id" => user2.to_global_id.to_s }, - { "id" => user3.to_global_id.to_s } + a_graphql_entity_for(user0), + a_graphql_entity_for(user1), + a_graphql_entity_for(user2), + a_graphql_entity_for(user3) ) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 88f10cc2a01..cf2c0780298 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -292,9 +292,6 @@ RSpec.configure do |config| # tests, until we introduce it in user settings stub_feature_flags(forti_token_cloud: false) - # Disable for now whilst we add more states - stub_feature_flags(restructured_mr_widget: false) - # These feature flag are by default disabled and used in disaster recovery mode stub_feature_flags(ci_queueing_disaster_recovery_disable_fair_scheduling: false) stub_feature_flags(ci_queueing_disaster_recovery_disable_quota: false) diff --git a/spec/support/graphql/arguments.rb b/spec/support/graphql/arguments.rb index 20e940030f8..a5bb01c31a3 100644 --- a/spec/support/graphql/arguments.rb +++ b/spec/support/graphql/arguments.rb @@ -40,7 +40,7 @@ module Graphql when Array then "[#{value.map { |v| as_graphql_literal(v) }.join(',')}]" when Hash then "{#{new(value)}}" when Integer, Float, Symbol then value.to_s - when String then "\"#{value.gsub(/"/, '\\"')}\"" + when String, GlobalID then "\"#{value.to_s.gsub(/"/, '\\"')}\"" when Time, Date then "\"#{value.iso8601}\"" when nil then 'null' when true then 'true' @@ -49,7 +49,7 @@ module Graphql value.to_graphql_value end rescue NoMethodError - raise ArgumentError, "Cannot represent #{value} as GraphQL literal" + raise ArgumentError, "Cannot represent #{value} (instance of #{value.class}) as GraphQL literal" end def merge(other) diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index 652cc51fcc0..eb0e5a25733 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -244,6 +244,7 @@ module GraphqlHelpers def graphql_mutation(name, input, fields = nil, &block) raise ArgumentError, 'Please pass either `fields` parameter or a block to `#graphql_mutation`, but not both.' if fields.present? && block_given? + name = name.graphql_name if name.respond_to?(:graphql_name) mutation_name = GraphqlHelpers.fieldnamerize(name) input_variable_name = "$#{input_variable_name_for_mutation(name)}" mutation_field = GitlabSchema.mutation.fields[mutation_name] @@ -264,7 +265,7 @@ module GraphqlHelpers end def variables_for_mutation(name, input) - graphql_input = prepare_input_for_mutation(input) + graphql_input = prepare_variables(input) { input_variable_name_for_mutation(name) => graphql_input } end @@ -273,18 +274,28 @@ module GraphqlHelpers return unless variables return variables if variables.is_a?(String) - ::Gitlab::Utils::MergeHash.merge(Array.wrap(variables).map(&:to_h)).to_json + # Combine variables into a single hash. + hash = ::Gitlab::Utils::MergeHash.merge(Array.wrap(variables).map(&:to_h)) + + prepare_variables(hash).to_json end - # Recursively convert a Hash with Ruby-style keys to GraphQL fieldname-style keys + # Recursively convert any ruby object we can pass as a variable value + # to an object we can serialize with JSON, using fieldname-style keys # - # prepare_input_for_mutation({ 'my_key' => 1 }) - # => { 'myKey' => 1} - def prepare_input_for_mutation(input) - input.to_h do |name, value| - value = prepare_input_for_mutation(value) if value.is_a?(Hash) + # prepare_variables({ 'my_key' => 1 }) + # => { 'myKey' => 1 } + # prepare_variables({ enums: [:FOO, :BAR], user_id: global_id_of(user) }) + # => { 'enums' => ['FOO', 'BAR'], 'userId' => "gid://User/123" } + # prepare_variables({ nested: { hash_values: { are_supported: true } } }) + # => { 'nested' => { 'hashValues' => { 'areSupported' => true } } } + def prepare_variables(input) + return input.map { prepare_variables(_1) } if input.is_a?(Array) + return input.to_s if input.is_a?(GlobalID) || input.is_a?(Symbol) + return input unless input.is_a?(Hash) - [GraphqlHelpers.fieldnamerize(name), value] + input.to_h do |name, value| + [GraphqlHelpers.fieldnamerize(name), prepare_variables(value)] end end @@ -650,9 +661,9 @@ module GraphqlHelpers end end - def global_id_of(model, id: nil, model_name: nil) + def global_id_of(model = nil, id: nil, model_name: nil) if id || model_name - ::Gitlab::GlobalId.build(model, id: id, model_name: model_name).to_s + ::Gitlab::GlobalId.as_global_id(id || model.id, model_name: model_name || model.class.name).to_s else model.to_global_id.to_s end @@ -714,6 +725,67 @@ module GraphqlHelpers end end + # Wrapper around a_hash_including that supports unpacking with ** + class UnpackableMatcher < SimpleDelegator + include RSpec::Matchers + + attr_reader :to_hash + + def initialize(hash) + @to_hash = hash + super(a_hash_including(hash)) + end + + def to_json(_opts = {}) + to_hash.to_json + end + + def as_json(opts = {}) + to_hash.as_json(opts) + end + end + + # Construct a matcher for GraphQL entity response objects, of the form + # `{ "id" => "some-gid" }`. + # + # Usage: + # + # ```ruby + # expect(graphql_data_at(:path, :to, :entity)).to match a_graphql_entity_for(user) + # ``` + # + # This can be called as: + # + # ```ruby + # a_graphql_entity_for(project, :full_path) # also checks that `entity['fullPath'] == project.full_path + # a_graphql_entity_for(project, full_path: 'some/path') # same as above, with explicit values + # a_graphql_entity_for(user, :username, foo: 'bar') # combinations of the above + # a_graphql_entity_for(foo: 'bar') # if properties are defined, the model is not necessary + # ``` + # + # Note that the model instance must not be nil, unless some properties are + # explicitly passed in. The following are rejected with `ArgumentError`: + # + # ``` + # a_graphql_entity_for(nil, :username) + # a_graphql_entity_for(:username) + # a_graphql_entity_for + # ``` + # + def a_graphql_entity_for(model = nil, *fields, **attrs) + raise ArgumentError, 'model is nil' if model.nil? && fields.any? + + attrs.transform_keys! { GraphqlHelpers.fieldnamerize(_1) } + attrs['id'] = global_id_of(model) if model + fields.each do |name| + attrs[GraphqlHelpers.fieldnamerize(name)] = model.public_send(name) + end + + raise ArgumentError, 'no attributes' if attrs.empty? + + UnpackableMatcher.new(attrs) + end + # A lookahead that selects everything def positive_lookahead double(selects?: true).tap do |selection| diff --git a/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb b/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb index 13e7ecf2669..b29a231f3a6 100644 --- a/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb +++ b/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb @@ -14,7 +14,7 @@ RSpec.shared_context 'package details setup' do let(:user) { project.first_owner } let(:package_details) { graphql_data_at(:package) } let(:metadata_response) { graphql_data_at(:package, :metadata) } - let(:first_file) { package.package_files.find { |f| global_id_of(f) == first_file_response['id'] } } + let(:first_file) { package.package_files.find { |f| a_graphql_entity_for(f).matches?(first_file_response) } } let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) } let(:first_file_response) { graphql_data_at(:package, :package_files, :nodes, 0)} let(:first_file_response_metadata) { graphql_data_at(:package, :package_files, :nodes, 0, :file_metadata)} diff --git a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb index 37a805902a9..6d6e7b761f6 100644 --- a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb +++ b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb @@ -101,7 +101,7 @@ RSpec.shared_examples 'sorted paginated query' do |conditions = {}| context 'when sorting' do it 'sorts correctly' do - expect(results).to eq all_records + expect(results).to match all_records end context 'when paginating' do @@ -110,17 +110,17 @@ RSpec.shared_examples 'sorted paginated query' do |conditions = {}| let(:rest) { all_records.drop(first_param) } it 'paginates correctly' do - expect(results).to eq first_page + expect(results).to match first_page fwds = pagination_query(sort_argument.merge(after: end_cursor)) post_graphql(fwds, current_user: current_user) - expect(results).to eq rest + expect(results).to match rest bwds = pagination_query(sort_argument.merge(before: start_cursor)) post_graphql(bwds, current_user: current_user) - expect(results).to eq first_page + expect(results).to match first_page end end @@ -130,7 +130,7 @@ RSpec.shared_examples 'sorted paginated query' do |conditions = {}| it 'fetches last elements without error' do post_graphql(pagination_query(params), current_user: current_user) - expect(results.first).to eq(all_records.last) + expect(results.first).to match all_records.last end end end diff --git a/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb index 7e1f4500779..31f2519a132 100644 --- a/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb @@ -12,8 +12,8 @@ RSpec.shared_examples 'a noteable graphql type we can query' do def expected noteable.discussions.map do |discussion| - include( - 'id' => global_id_of(discussion), + a_graphql_entity_for( + discussion, 'replyId' => global_id_of(discussion, id: discussion.reply_id), 'createdAt' => discussion.created_at.iso8601, 'notes' => include( @@ -50,8 +50,8 @@ RSpec.shared_examples 'a noteable graphql type we can query' do post_graphql(query(fields), current_user: current_user) - data = graphql_data_at(*path_to_noteable, :discussions, :nodes, :noteable, :id) - expect(data[0]).to eq(global_id_of(noteable)) + entities = graphql_data_at(*path_to_noteable, :discussions, :nodes, :noteable) + expect(entities).to all(match(a_graphql_entity_for(noteable))) end end @@ -62,10 +62,10 @@ RSpec.shared_examples 'a noteable graphql type we can query' do def expected noteable.notes.map do |note| - include( - 'id' => global_id_of(note), - 'project' => include('id' => global_id_of(project)), - 'author' => include('id' => global_id_of(note.author)), + a_graphql_entity_for( + note, + 'project' => a_graphql_entity_for(project), + 'author' => a_graphql_entity_for(note.author), 'createdAt' => note.created_at.iso8601, 'body' => eq(note.note) ) diff --git a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb index 5d6e95f2fbc..9f7ec6e90e9 100644 --- a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb @@ -104,7 +104,7 @@ RSpec.shared_examples 'group and project packages query' do } end - let(:expected_packages) { sorted_packages.map { |package| global_id_of(package) } } + let(:expected_packages) { sorted_packages.map { |package| global_id_of(package).to_s } } let(:data_path) { [resource_type, :packages] } diff --git a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb index ab93f54111b..b4019d7c232 100644 --- a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb @@ -28,14 +28,10 @@ RSpec.shared_examples 'a package with files' do end it 'has the basic package files data' do - expect(first_file_response).to include( - 'id' => global_id_of(first_file), - 'fileName' => first_file.file_name, - 'size' => first_file.size.to_s, - 'downloadPath' => first_file.download_path, - 'fileSha1' => first_file.file_sha1, - 'fileMd5' => first_file.file_md5, - 'fileSha256' => first_file.file_sha256 + expect(first_file_response).to match a_graphql_entity_for( + first_file, + :file_name, :download_path, :file_sha1, :file_md5, :file_sha256, + 'size' => first_file.size.to_s ) end diff --git a/spec/support/shared_examples/requests/api/graphql/projects/alert_management/integrations_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/projects/alert_management/integrations_shared_examples.rb index c134f7d1839..3c5f25baaa1 100644 --- a/spec/support/shared_examples/requests/api/graphql/projects/alert_management/integrations_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/projects/alert_management/integrations_shared_examples.rb @@ -30,14 +30,12 @@ RSpec.shared_examples 'GraphQL query with several integrations requested' do |gr it 'returns the correct properties of the integrations', :aggregate_failures do post_graphql(multi_selection_query, current_user: current_user) - expect(graphql_data.dig('project', 'ai', 'nodes')).to include( - 'id' => global_id_of(active_http_integration), - 'name' => active_http_integration.name + expect(graphql_data.dig('project', 'ai', 'nodes')).to match a_graphql_entity_for( + active_http_integration, :name ) - expect(graphql_data.dig('project', 'ii', 'nodes')).to include( - 'id' => global_id_of(inactive_http_integration), - 'name' => inactive_http_integration.name + expect(graphql_data.dig('project', 'ii', 'nodes')).to match a_graphql_entity_for( + inactive_http_integration, :name ) end diff --git a/spec/support_specs/helpers/graphql_helpers_spec.rb b/spec/support_specs/helpers/graphql_helpers_spec.rb index fae29ec32f5..0f9918351e2 100644 --- a/spec/support_specs/helpers/graphql_helpers_spec.rb +++ b/spec/support_specs/helpers/graphql_helpers_spec.rb @@ -10,6 +10,81 @@ RSpec.describe GraphqlHelpers do query.tr("\n", ' ').gsub(/\s+/, ' ').strip end + describe 'a_graphql_entity_for' do + context 'when no arguments are passed' do + it 'raises an error' do + expect { a_graphql_entity_for }.to raise_error(ArgumentError) + end + end + + context 'when the model is nil, with no properties' do + it 'raises an error' do + expect { a_graphql_entity_for(nil) }.to raise_error(ArgumentError) + end + end + + context 'when the model is nil, any fields are passed' do + it 'raises an error' do + expect { a_graphql_entity_for(nil, :username) }.to raise_error(ArgumentError) + end + end + + context 'with no model' do + it 'behaves like hash-inclusion with camel-casing' do + response = { 'foo' => 1, 'bar' => 2, 'camelCased' => 3 } + + expect(response).to match a_graphql_entity_for(foo: 1, camel_cased: 3) + expect(response).not_to match a_graphql_entity_for(missing: 5) + end + end + + context 'with just a model' do + it 'only considers the ID' do + user = build_stubbed(:user) + response = { 'username' => 'foo', 'id' => global_id_of(user) } + + expect(response).to match a_graphql_entity_for(user) + end + end + + context 'with a model and some method names' do + it 'also considers the method names' do + user = build_stubbed(:user) + response = { 'username' => user.username, 'id' => global_id_of(user) } + + expect(response).to match a_graphql_entity_for(user, :username) + expect(response).not_to match a_graphql_entity_for(user, :name) + end + end + + context 'with a model and some other properties' do + it 'behaves like the superset' do + user = build_stubbed(:user) + response = { 'username' => 'foo', 'id' => global_id_of(user) } + + expect(response).to match a_graphql_entity_for(user, username: 'foo') + expect(response).not_to match a_graphql_entity_for(user, name: 'foo') + end + end + + context 'with a model, method names, and some other properties' do + it 'behaves like the superset' do + user = build_stubbed(:user) + response = { + 'username' => user.username, + 'name' => user.name, + 'foo' => 'bar', + 'baz' => 'fop', + 'id' => global_id_of(user) + } + + expect(response).to match a_graphql_entity_for(user, :username, :name, foo: 'bar') + expect(response).to match a_graphql_entity_for(user, :name, foo: 'bar') + expect(response).not_to match a_graphql_entity_for(user, :name, bar: 'foo') + end + end + end + describe 'graphql_dig_at' do it 'transforms symbol keys to graphql field names' do data = { 'camelCased' => 'names' } diff --git a/spec/views/shared/access_tokens/_table.html.haml_spec.rb b/spec/views/shared/access_tokens/_table.html.haml_spec.rb index fca2fc3183c..74de9e12d04 100644 --- a/spec/views/shared/access_tokens/_table.html.haml_spec.rb +++ b/spec/views/shared/access_tokens/_table.html.haml_spec.rb @@ -6,7 +6,6 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do let(:type) { 'token' } let(:type_plural) { 'tokens' } let(:empty_message) { nil } - let(:token_expiry_enforced?) { false } let(:impersonation) { false } let_it_be(:user) { create(:user) } @@ -14,12 +13,6 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do let_it_be(:resource) { false } before do - stub_licensed_features(enforce_personal_access_token_expiration: true) - allow(Gitlab::CurrentSettings).to receive(:enforce_pat_expiration?).and_return(false) - - allow(view).to receive(:personal_access_token_expiration_enforced?).and_return(token_expiry_enforced?) - allow(view).to receive(:show_profile_token_expiry_notification?).and_return(true) - if resource resource.add_maintainer(user) end @@ -51,22 +44,6 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do expect(rendered).not_to have_content 'To see all the user\'s personal access tokens you must impersonate them first.' expect(rendered).not_to have_selector 'th', text: 'Role' end - - context 'if token expiration is enforced' do - let(:token_expiry_enforced?) { true } - - it 'does not show the subtext' do - expect(rendered).not_to have_content 'Personal access tokens are not revoked upon expiration.' - end - end - - context 'if token expiration is not enforced' do - let(:token_expiry_enforced?) { false } - - it 'does show the subtext' do - expect(rendered).to have_content 'Personal access tokens are not revoked upon expiration.' - end - end end context 'if impersonation' do @@ -124,16 +101,16 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do context 'with tokens' do let_it_be(:tokens) do [ - create(:personal_access_token, user: user, name: 'Access token', last_used_at: 1.day.ago, expires_at: nil), - create(:personal_access_token, user: user, expires_at: 5.days.ago), - create(:personal_access_token, user: user, expires_at: Time.now), - create(:personal_access_token, user: user, expires_at: 5.days.from_now, scopes: [:read_api, :read_user]) + create(:personal_access_token, user: user, name: 'Access token', last_used_at: 4.days.from_now, expires_at: nil, scopes: [:read_api, :read_user]), + create(:personal_access_token, user: user, expires_at: 1.day.from_now, scopes: [:read_api, :read_user]) ] end + let_it_be(:expired_token) { build(:personal_access_token, name: "Expired token", expires_at: 2.days.ago).tap { |t| t.save!(validate: false) } } + it 'has the correct content', :aggregate_failures do # Heading content - expect(rendered).to have_content 'Active tokens (4)' + expect(rendered).to have_content 'Active tokens (2)' # Table headers expect(rendered).to have_selector 'th', text: 'Token name' @@ -144,17 +121,15 @@ RSpec.describe 'shared/access_tokens/_table.html.haml' do # Table contents expect(rendered).to have_content 'Access token' + expect(rendered).not_to have_content 'Expired token' expect(rendered).to have_content 'read_api, read_user' expect(rendered).to have_content 'no scopes selected' expect(rendered).to have_content Time.now.to_date.to_s(:medium) - expect(rendered).to have_content l(1.day.ago, format: "%b %d, %Y") - - # Expiry - expect(rendered).to have_content 'Expired', count: 2 + expect(rendered).to have_content l(4.days.from_now, format: "%b %d, %Y") # Revoke buttons expect(rendered).to have_link 'Revoke', href: 'path/', class: 'btn-danger-secondary', count: 1 - expect(rendered).to have_link 'Revoke', href: 'path/', count: 4 + expect(rendered).to have_link 'Revoke', href: 'path/', count: 2 end context 'without the last used time' do diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb index 52df224e9d4..adee70fbf87 100644 --- a/spec/workers/every_sidekiq_worker_spec.rb +++ b/spec/workers/every_sidekiq_worker_spec.rb @@ -350,7 +350,6 @@ RSpec.describe 'Every Sidekiq worker' do 'Namespaces::RefreshRootStatisticsWorker' => 3, 'Namespaces::RootStatisticsWorker' => 3, 'Namespaces::ScheduleAggregationWorker' => 3, - 'NetworkPolicyMetricsWorker' => 3, 'NewEpicWorker' => 3, 'NewIssueWorker' => 3, 'NewMergeRequestWorker' => 3, |