summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-03-16 21:09:14 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-03-16 21:09:14 +0000
commit06b21ad63ca4f7d0cada7cc44b9cddbf17df2193 (patch)
tree9f79eb73e6335bc2bef652aaeb19691bc6ae44df
parent204df35415f2b0ed86c83b31b1d276f52e07e577 (diff)
downloadgitlab-ce-06b21ad63ca4f7d0cada7cc44b9cddbf17df2193.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/review-apps/qa.gitlab-ci.yml7
-rw-r--r--app/assets/javascripts/diffs/components/hidden_files_warning.vue52
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue12
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js13
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js14
-rw-r--r--app/assets/javascripts/merge_request_tabs.js9
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue14
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js6
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue33
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql2
-rw-r--r--app/helpers/markup_helper.rb5
-rw-r--r--app/models/user.rb37
-rw-r--r--app/views/groups/registry/repositories/index.html.haml1
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml10
-rw-r--r--app/views/projects/registry/repositories/index.html.haml1
-rw-r--r--app/workers/all_queues.yml9
-rw-r--r--app/workers/quality/test_data_cleanup_worker.rb33
-rw-r--r--doc/administration/logs.md5
-rw-r--r--doc/api/group_wikis.md2
-rw-r--r--doc/api/wikis.md2
-rw-r--r--doc/development/img/merge_request_reports_v14_7.pngbin0 -> 66876 bytes
-rw-r--r--doc/development/img/merge_widget_v14_7.pngbin0 -> 56335 bytes
-rw-r--r--doc/development/merge_request_concepts/index.md62
-rw-r--r--doc/user/packages/container_registry/index.md7
-rw-r--r--doc/user/project/merge_requests/commit_templates.md6
-rw-r--r--lib/api/entities/wiki_page.rb2
-rw-r--r--lib/api/helpers/wikis_helpers.rb4
-rw-r--r--lib/api/wikis.rb3
-rw-r--r--lib/gitlab/database/background_migration/batched_job.rb22
-rw-r--r--locale/gitlab.pot6
-rw-r--r--qa/qa/resource/project.rb12
-rw-r--r--spec/factories/groups.rb9
-rw-r--r--spec/features/groups/container_registry_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_diff_spec.rb4
-rw-r--r--spec/features/projects/container_registry_spec.rb2
-rw-r--r--spec/frontend/diffs/components/hidden_files_warning_spec.js18
-rw-r--r--spec/frontend/fixtures/merge_requests.rb19
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js58
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js25
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js53
-rw-r--r--spec/frontend/merge_request_tabs_spec.js1
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js21
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js9
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js32
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js114
-rw-r--r--spec/helpers/markup_helper_spec.rb5
-rw-r--r--spec/lib/api/entities/wiki_page_spec.rb14
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_job_spec.rb92
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb1
-rw-r--r--spec/models/user_spec.rb64
-rw-r--r--spec/requests/api/wikis_spec.rb24
-rw-r--r--spec/support/shared_examples/features/container_registry_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/lib/wikis_api_examples.rb38
-rw-r--r--spec/workers/quality/test_data_cleanup_worker_spec.rb44
55 files changed, 885 insertions, 185 deletions
diff --git a/.gitlab/ci/review-apps/qa.gitlab-ci.yml b/.gitlab/ci/review-apps/qa.gitlab-ci.yml
index eaf6429ad58..d0234367e26 100644
--- a/.gitlab/ci/review-apps/qa.gitlab-ci.yml
+++ b/.gitlab/ci/review-apps/qa.gitlab-ci.yml
@@ -42,9 +42,6 @@
expire_in: 7 days
when: always
-.parallel-qa-base:
- parallel: 5
-
.allure-report-base:
image:
name: ${GITLAB_DEPENDENCY_PROXY}andrcuns/allure-report-publisher:0.4.2
@@ -82,7 +79,7 @@ review-qa-reliable:
extends:
- .review-qa-base
- .review:rules:review-qa-reliable
- - .parallel-qa-base
+ parallel: 8
retry: 1
variables:
QA_RUN_TYPE: review-qa-reliable
@@ -92,7 +89,7 @@ review-qa-all:
extends:
- .review-qa-base
- .review:rules:review-qa-all
- - .parallel-qa-base
+ parallel: 5
variables:
QA_RUN_TYPE: review-qa-all
QA_SCENARIO: Test::Instance::All
diff --git a/app/assets/javascripts/diffs/components/hidden_files_warning.vue b/app/assets/javascripts/diffs/components/hidden_files_warning.vue
index baf7471582a..b9962682848 100644
--- a/app/assets/javascripts/diffs/components/hidden_files_warning.vue
+++ b/app/assets/javascripts/diffs/components/hidden_files_warning.vue
@@ -1,6 +1,19 @@
<script>
-/* eslint-disable @gitlab/vue-require-i18n-strings */
+import { GlAlert, GlSprintf } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export const i18n = {
+ title: __('Too many changes to show.'),
+ plainDiff: __('Plain diff'),
+ emailPatch: __('Email patch'),
+};
+
export default {
+ i18n,
+ components: {
+ GlAlert,
+ GlSprintf,
+ },
props: {
total: {
type: String,
@@ -23,17 +36,28 @@ export default {
</script>
<template>
- <div class="alert alert-warning">
- <h4>
- {{ __('Too many changes to show.') }}
- <div class="float-right">
- <a :href="plainDiffPath" class="btn btn-sm"> {{ __('Plain diff') }} </a>
- <a :href="emailPatchPath" class="btn btn-sm"> {{ __('Email patch') }} </a>
- </div>
- </h4>
- <p>
- To preserve performance only <strong> {{ visible }} of {{ total }} </strong> files are
- displayed.
- </p>
- </div>
+ <gl-alert
+ variant="warning"
+ :title="$options.i18n.title"
+ :primary-button-text="$options.i18n.plainDiff"
+ :primary-button-link="plainDiffPath"
+ :secondary-button-text="$options.i18n.emailPatch"
+ :secondary-button-link="emailPatchPath"
+ :dismissible="false"
+ >
+ <gl-sprintf
+ :message="
+ sprintf(
+ __(
+ 'To preserve performance only %{strongStart}%{visible} of %{total}%{strongEnd} files are displayed.',
+ ),
+ { visible, total },
+ )
+ "
+ >
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 05493db1dff..f14d86114b8 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -147,6 +147,9 @@ export default {
fileType() {
return this.previewMode?.id || '';
},
+ showTabs() {
+ return !this.shouldHideEditor && this.isEditModeActive && this.previewMode;
+ },
},
watch: {
'file.name': {
@@ -194,6 +197,9 @@ export default {
this.refreshEditorDimensions();
}
},
+ showTabs() {
+ this.$nextTick(() => this.refreshEditorDimensions());
+ },
rightPaneIsOpen() {
this.refreshEditorDimensions();
},
@@ -410,7 +416,7 @@ export default {
}
},
refreshEditorDimensions() {
- if (this.showEditor) {
+ if (this.showEditor && this.editor) {
this.editor.updateDimensions();
}
},
@@ -495,7 +501,7 @@ export default {
<template>
<div id="ide" class="blob-viewer-container blob-editor-container">
- <div v-if="!shouldHideEditor && isEditModeActive" class="ide-mode-tabs clearfix">
+ <div v-if="showTabs" class="ide-mode-tabs clearfix">
<ul class="nav-links float-left border-bottom-0">
<li :class="editTabCSS">
<a
@@ -506,7 +512,7 @@ export default {
>{{ __('Edit') }}</a
>
</li>
- <li v-if="previewMode" :class="previewTabCSS">
+ <li :class="previewTabCSS">
<a
href="javascript:void(0);"
role="button"
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index cf6ce2c4889..96d019f62f2 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -130,19 +130,6 @@ export const isInViewport = (el, offset = {}) => {
);
};
-export const parseUrl = (url) => {
- const parser = document.createElement('a');
- parser.href = url;
- return parser;
-};
-
-export const parseUrlPathname = (url) => {
- const parsedUrl = parseUrl(url);
- // parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11
- // We have to make sure we always have an absolute path.
- return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : `/${parsedUrl.pathname}`;
-};
-
export const isMetaKey = (e) => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
// Identify following special clicks
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 12462a2575e..335cd6a16e5 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -18,6 +18,20 @@ function resetRegExp(regex) {
return regex;
}
+/**
+ * Returns the absolute pathname for a relative or absolute URL string.
+ *
+ * A few examples of inputs and outputs:
+ * 1) 'http://a.com/b/c/d' => '/b/c/d'
+ * 2) '/b/c/d' => '/b/c/d'
+ * 3) 'b/c/d' => '/b/c/d' or '[path]/b/c/d' depending of the current path of the
+ * document.location
+ */
+export const parseUrlPathname = (url) => {
+ const { pathname } = new URL(url, document.location.href);
+ return pathname;
+};
+
// Returns a decoded url parameter value
// - Treats '+' as '%20'
function decodeUrlParameter(val) {
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 5d28c95d8a2..61f7a079d77 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -2,13 +2,8 @@
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
import Vue from 'vue';
-import {
- getCookie,
- parseUrlPathname,
- isMetaClick,
- parseBoolean,
- scrollToElement,
-} from '~/lib/utils/common_utils';
+import { getCookie, isMetaClick, parseBoolean, scrollToElement } from '~/lib/utils/common_utils';
+import { parseUrlPathname } from '~/lib/utils/url_utility';
import createEventHub from '~/helpers/event_hub_factory';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import Diff from './diff';
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue
index 56d2ff86fb7..1b7d5af6134 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue
@@ -1,7 +1,11 @@
<script>
import { GlSprintf, GlAlert, GlLink } from '@gitlab/ui';
-import { ALERT_MESSAGES, ADMIN_GARBAGE_COLLECTION_TIP } from '../../constants/index';
+import {
+ ALERT_MESSAGES,
+ ADMIN_GARBAGE_COLLECTION_TIP,
+ ALERT_DANGER_IMPORTING,
+} from '../../constants/index';
export default {
components: {
@@ -23,6 +27,7 @@ export default {
},
},
garbageCollectionHelpPagePath: { type: String, required: false, default: '' },
+ containerRegistryImportingHelpPagePath: { type: String, required: false, default: '' },
isAdmin: {
type: Boolean,
default: false,
@@ -48,6 +53,11 @@ export default {
}
return config;
},
+ alertHref() {
+ return this.deleteAlertType === ALERT_DANGER_IMPORTING
+ ? this.containerRegistryImportingHelpPagePath
+ : this.garbageCollectionHelpPagePath;
+ },
},
};
</script>
@@ -61,7 +71,7 @@ export default {
>
<gl-sprintf :message="deleteAlertConfig.message">
<template #docLink="{ content }">
- <gl-link :href="garbageCollectionHelpPagePath" target="_blank">
+ <gl-link :href="alertHref" target="_blank">
{{ content }}
</gl-link>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
index 8b8769a884d..3c7f7ca9aa8 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
@@ -93,6 +93,10 @@ export const DETAILS_DELETE_IMAGE_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while scheduling the image for deletion.',
);
+export const DETAILS_IMPORTING_ERROR_MESSAGE = s__(
+ 'ContainerRegistry|Tags temporarily cannot be marked for deletion. Please try again in a few minutes. %{docLinkStart}More details%{docLinkEnd}.',
+);
+
export const DELETE_IMAGE_CONFIRMATION_TITLE = s__('ContainerRegistry|Delete image repository?');
export const DELETE_IMAGE_CONFIRMATION_TEXT = s__(
'ContainerRegistry|Deleting the image repository will delete all images and tags inside. This action cannot be undone. Please type the following to confirm: %{code}',
@@ -133,6 +137,7 @@ export const ALERT_DANGER_TAG = 'danger_tag';
export const ALERT_SUCCESS_TAGS = 'success_tags';
export const ALERT_DANGER_TAGS = 'danger_tags';
export const ALERT_DANGER_IMAGE = 'danger_image';
+export const ALERT_DANGER_IMPORTING = 'danger_importing';
export const DELETE_SCHEDULED = 'DELETE_SCHEDULED';
export const DELETE_FAILED = 'DELETE_FAILED';
@@ -143,6 +148,7 @@ export const ALERT_MESSAGES = {
[ALERT_SUCCESS_TAGS]: DELETE_TAGS_SUCCESS_MESSAGE,
[ALERT_DANGER_TAGS]: DELETE_TAGS_ERROR_MESSAGE,
[ALERT_DANGER_IMAGE]: DETAILS_DELETE_IMAGE_ERROR_MESSAGE,
+ [ALERT_DANGER_IMPORTING]: DETAILS_IMPORTING_ERROR_MESSAGE,
};
export const UNFINISHED_STATUS = 'UNFINISHED';
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
index 931849c9918..71a85d8885e 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
@@ -20,6 +20,7 @@ import {
ALERT_SUCCESS_TAGS,
ALERT_DANGER_TAGS,
ALERT_DANGER_IMAGE,
+ ALERT_DANGER_IMPORTING,
FETCH_IMAGES_LIST_ERROR_MESSAGE,
UNFINISHED_STATUS,
MISSING_OR_DELETED_IMAGE_BREADCRUMB,
@@ -32,6 +33,8 @@ import deleteContainerRepositoryTagsMutation from '../graphql/mutations/delete_c
import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.query.graphql';
import getContainerRepositoryTagsQuery from '../graphql/queries/get_container_repository_tags.query.graphql';
+const REPOSITORY_IMPORTING_ERROR_MESSAGE = 'repository importing';
+
export default {
name: 'RegistryDetailsPage',
components: {
@@ -147,12 +150,17 @@ export default {
});
if (data?.destroyContainerRepositoryTags?.errors[0]) {
- throw new Error();
+ throw new Error(data.destroyContainerRepositoryTags.errors[0]);
}
this.deleteAlertType =
itemsToBeDeleted.length === 0 ? ALERT_SUCCESS_TAG : ALERT_SUCCESS_TAGS;
} catch (e) {
- this.deleteAlertType = itemsToBeDeleted.length === 0 ? ALERT_DANGER_TAG : ALERT_DANGER_TAGS;
+ if (e.message === REPOSITORY_IMPORTING_ERROR_MESSAGE) {
+ this.deleteAlertType = ALERT_DANGER_IMPORTING;
+ } else {
+ this.deleteAlertType =
+ itemsToBeDeleted.length === 0 ? ALERT_DANGER_TAG : ALERT_DANGER_TAGS;
+ }
}
this.mutationLoading = false;
@@ -188,6 +196,7 @@ export default {
<delete-alert
v-model="deleteAlertType"
:garbage-collection-help-page-path="config.garbageCollectionHelpPagePath"
+ :container-registry-importing-help-page-path="config.containerRegistryImportingHelpPagePath"
:is-admin="config.isAdmin"
class="gl-my-2"
/>
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 239529ed8f6..4f8faeb877f 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
@@ -82,6 +82,13 @@ export default {
};
this.loading = false;
+ if (!this.commitMessageIsTouched) {
+ this.commitMessage = this.state.defaultMergeCommitMessage;
+ }
+ if (!this.squashCommitMessageIsTouched) {
+ this.squashCommitMessage = this.state.defaultSquashCommitMessage;
+ }
+
if (this.state.mergeTrainsCount !== null && this.state.mergeTrainsCount !== undefined) {
this.initPolling();
}
@@ -133,9 +140,11 @@ export default {
isMakingRequest: false,
isMergingImmediately: false,
commitMessage: this.mr.commitMessage,
+ commitMessageIsTouched: false,
squashBeforeMerge: this.mr.squashIsSelected,
isSquashReadOnly: this.mr.squashIsReadonly,
squashCommitMessage: this.mr.squashCommitMessage,
+ squashCommitMessageIsTouched: false,
isPipelineFailedModalVisibleMergeTrain: false,
isPipelineFailedModalVisibleNormalMerge: false,
editCommitMessage: false,
@@ -465,6 +474,14 @@ export default {
});
});
},
+ setCommitMessage(val) {
+ this.commitMessage = val;
+ this.commitMessageIsTouched = true;
+ },
+ setSquashCommitMessage(val) {
+ this.squashCommitMessage = val;
+ this.squashCommitMessageIsTouched = true;
+ },
},
i18n: {
mergeCommitTemplateHintText: s__(
@@ -630,21 +647,23 @@ export default {
>
<commit-edit
v-if="shouldShowSquashEdit"
- v-model="squashCommitMessage"
+ :value="squashCommitMessage"
:label="__('Squash commit message')"
input-id="squash-message-edit"
class="gl-m-0! gl-p-0!"
+ @input="setSquashCommitMessage"
>
<template #header>
- <commit-message-dropdown v-model="squashCommitMessage" :commits="commits" />
+ <commit-message-dropdown :commits="commits" @input="setSquashCommitMessage" />
</template>
</commit-edit>
<commit-edit
v-if="shouldShowMergeEdit"
- v-model="commitMessage"
+ :value="commitMessage"
:label="__('Merge commit message')"
input-id="merge-message-edit"
class="gl-m-0! gl-p-0!"
+ @input="setCommitMessage"
/>
<li class="gl-m-0! gl-p-0!">
<p class="form-text text-muted">
@@ -748,20 +767,22 @@ export default {
<ul class="border-top content-list commits-list flex-list">
<commit-edit
v-if="shouldShowSquashEdit"
- v-model="squashCommitMessage"
+ :value="squashCommitMessage"
:label="__('Squash commit message')"
input-id="squash-message-edit"
squash
+ @input="setSquashCommitMessage"
>
<template #header>
- <commit-message-dropdown v-model="squashCommitMessage" :commits="commits" />
+ <commit-message-dropdown :commits="commits" @input="setSquashCommitMessage" />
</template>
</commit-edit>
<commit-edit
v-if="shouldShowMergeEdit"
- v-model="commitMessage"
+ :value="commitMessage"
:label="__('Merge commit message')"
input-id="merge-message-edit"
+ @input="setCommitMessage"
/>
<li>
<p class="form-text text-muted">
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
index d85794f7245..99e6f4e9beb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
@@ -1,9 +1,11 @@
fragment ReadyToMerge on Project {
+ __typename
id
onlyAllowMergeIfPipelineSucceeds
mergeRequestsFfOnlyEnabled
squashReadOnly
mergeRequest(iid: $iid) {
+ __typename
id
autoMergeEnabled
shouldRemoveSourceBranch
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index 22243b606ed..7a4cc61af79 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -181,7 +181,8 @@ module MarkupHelper
wiki: wiki,
repository: wiki.repository,
page_slug: wiki_page.slug,
- issuable_reference_expansion_enabled: true
+ issuable_reference_expansion_enabled: true,
+ requested_path: wiki_page.path
).merge(render_wiki_content_context_container(wiki))
end
@@ -263,7 +264,7 @@ module MarkupHelper
end
def asciidoc_unsafe(text, context = {})
- context.merge!(
+ context.reverse_merge!(
commit: @commit,
ref: @ref,
requested_path: @path
diff --git a/app/models/user.rb b/app/models/user.rb
index 4c375fe5230..2905db7ce6c 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -277,7 +277,19 @@ class User < ApplicationRecord
after_update :username_changed_hook, if: :saved_change_to_username?
after_destroy :post_destroy_hook
after_destroy :remove_key_cache
- after_create :add_primary_email_to_emails!, if: :confirmed?
+ after_save if: -> { saved_change_to_email? && confirmed? } do
+ email_to_confirm = self.emails.find_by(email: self.email)
+
+ if email_to_confirm.present?
+ if skip_confirmation_period_expiry_check
+ email_to_confirm.force_confirm
+ else
+ email_to_confirm.confirm
+ end
+ else
+ add_primary_email_to_emails!
+ end
+ end
after_commit(on: :update) do
if previous_changes.key?('email')
# Add the old primary email to Emails if not added already - this should be removed
@@ -2013,29 +2025,6 @@ class User < ApplicationRecord
ci_job_token_scope.present?
end
- # override from Devise::Models::Confirmable
- #
- # Add the primary email to user.emails (or confirm it if it was already
- # present) when the primary email is confirmed.
- def confirm(args = {})
- saved = super(args)
- return false unless saved
-
- email_to_confirm = self.emails.find_by(email: self.email)
-
- if email_to_confirm.present?
- if skip_confirmation_period_expiry_check
- email_to_confirm.force_confirm(args)
- else
- email_to_confirm.confirm(args)
- end
- else
- add_primary_email_to_emails!
- end
-
- saved
- end
-
def user_project
strong_memoize(:user_project) do
personal_projects.find_by(path: username, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
diff --git a/app/views/groups/registry/repositories/index.html.haml b/app/views/groups/registry/repositories/index.html.haml
index f6d05959d2e..6060d697f52 100644
--- a/app/views/groups/registry/repositories/index.html.haml
+++ b/app/views/groups/registry/repositories/index.html.haml
@@ -12,6 +12,7 @@
"registry_host_url_with_port" => escape_once(registry_config.host_port),
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
+ "container_registry_importing_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'tags-temporarily-cannot-be-marked-for-deletion'),
"is_admin": current_user&.admin.to_s,
is_group_page: "true",
"group_path": @group.full_path,
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 67f24383b17..5f8b21b2646 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -40,12 +40,10 @@
= _('Time based: Yes')
= form_tag profile_two_factor_auth_path, method: :post do |f|
- if @error
- .gl-alert.gl-alert-danger.gl-mb-5
- .gl-alert-container
- .gl-alert-content
- %p.gl-alert-body.gl-md-0
- = @error[:message]
- = link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'troubleshooting'), target: '_blank', rel: 'noopener noreferrer'
+ = render 'shared/global_alert', title: @error[:message], variant: :danger, dismissible: false do
+ .gl-alert-body
+ = link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'troubleshooting'), target: '_blank', rel: 'noopener noreferrer'
+
.form-group
= label_tag :pin_code, _('Pin code'), class: "label-bold"
= text_field_tag :pin_code, nil, class: "form-control gl-form-input", required: true, data: { qa_selector: 'pin_code_field' }
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index 3efe1fd2e82..aab5e9fca98 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -15,6 +15,7 @@
"expiration_policy_help_page_path" => help_page_path('user/packages/container_registry/reduce_container_registry_storage', anchor: 'cleanup-policy'),
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
+ "container_registry_importing_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'tags-temporarily-cannot-be-marked-for-deletion'),
"project_path": @project.full_path,
"gid_prefix": container_repository_gid_prefix,
"is_admin": current_user&.admin.to_s,
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 08e37308936..43dd2c3e759 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -570,6 +570,15 @@
:weight: 1
:idempotent:
:tags: []
+- :name: cronjob:quality_test_data_cleanup
+ :worker_name: Quality::TestDataCleanupWorker
+ :feature_category: :quality_management
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: cronjob:releases_manage_evidence
:worker_name: Releases::ManageEvidenceWorker
:feature_category: :release_evidence
diff --git a/app/workers/quality/test_data_cleanup_worker.rb b/app/workers/quality/test_data_cleanup_worker.rb
new file mode 100644
index 00000000000..68b36cacbbf
--- /dev/null
+++ b/app/workers/quality/test_data_cleanup_worker.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Quality
+ class TestDataCleanupWorker
+ include ApplicationWorker
+
+ data_consistency :always
+ feature_category :quality_management
+ urgency :low
+
+ include CronjobQueue
+ idempotent!
+
+ KEEP_RECENT_DATA_DAY = 3
+ GROUP_PATH_PATTERN = 'test-group-fulfillment'
+ GROUP_OWNER_EMAIL_PATTERN = %w(test-user- gitlab-qa-user qa-user-).freeze
+
+ # Remove test groups generated in E2E tests on gstg
+ # rubocop: disable CodeReuse/ActiveRecord
+ def perform
+ return unless Gitlab.staging?
+
+ Group.where('path like ?', "#{GROUP_PATH_PATTERN}%").where('created_at < ?', KEEP_RECENT_DATA_DAY.days.ago).each do |group|
+ next unless GROUP_OWNER_EMAIL_PATTERN.any? { |pattern| group.owners.first.email.include?(pattern) }
+
+ with_context(namespace: group, user: group.owners.first) do
+ Groups::DestroyService.new(group, group.owners.first).execute
+ end
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+end
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index 53ab8061ffa..22e13270179 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -1049,7 +1049,10 @@ For example:
## Mattermost Logs
-For Omnibus GitLab installations, Mattermost logs are in `/var/log/gitlab/mattermost/mattermost.log`.
+For Omnibus GitLab installations, Mattermost logs are in these locations:
+
+- `/var/log/gitlab/mattermost/mattermost.log`
+- `/var/log/gitlab/mattermost/current`
## Workhorse Logs
diff --git a/doc/api/group_wikis.md b/doc/api/group_wikis.md
index a4de537004d..a78f989a755 100644
--- a/doc/api/group_wikis.md
+++ b/doc/api/group_wikis.md
@@ -10,6 +10,7 @@ type: reference, api
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/212199) in GitLab 13.5.
> - The `encoding` field was [added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81150) in GitLab 14.9.
> - The `render_html` attribute was [added](https://gitlab.com/gitlab-org/gitlab/-/issues/336792) in GitLab 14.9.
+> - The `version` attribute was [added](https://gitlab.com/gitlab-org/gitlab/-/issues/336792) in GitLab 14.9.
The [group wikis](../user/project/wiki/group.md) API is available only in APIv4.
An API for [project wikis](wikis.md) is also available.
@@ -71,6 +72,7 @@ GET /groups/:id/wikis/:slug
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) |
| `slug` | string | yes | URL-encoded slug (a unique string) of the wiki page, such as `dir%2Fpage_name` |
| `render_html` | boolean | no | Return the rendered HTML of the wiki page |
+| `version` | string | no | Wiki page version sha |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/1/wikis/home"
diff --git a/doc/api/wikis.md b/doc/api/wikis.md
index 281ee414442..b7c9d0d6008 100644
--- a/doc/api/wikis.md
+++ b/doc/api/wikis.md
@@ -8,6 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - The `encoding` field was [added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81150) in GitLab 14.9.
> - The `render_html` attribute was [added](https://gitlab.com/gitlab-org/gitlab/-/issues/336792) in GitLab 14.9.
+> - The `version` attribute was [added](https://gitlab.com/gitlab-org/gitlab/-/issues/336792) in GitLab 14.9.
The project [wikis](../user/project/wiki/index.md) API is available only in APIv4.
An API for [group wikis](group_wikis.md) is also available.
@@ -69,6 +70,7 @@ GET /projects/:id/wikis/:slug
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) |
| `slug` | string | yes | URLencoded slug (a unique string) of the wiki page, such as `dir%2Fpage_name` |
| `render_html` | boolean | no | Return the rendered HTML of the wiki page |
+| `version` | string | no | Wiki page version sha |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/wikis/home"
diff --git a/doc/development/img/merge_request_reports_v14_7.png b/doc/development/img/merge_request_reports_v14_7.png
new file mode 100644
index 00000000000..282d6f96aa6
--- /dev/null
+++ b/doc/development/img/merge_request_reports_v14_7.png
Binary files differ
diff --git a/doc/development/img/merge_widget_v14_7.png b/doc/development/img/merge_widget_v14_7.png
new file mode 100644
index 00000000000..d5e8ed8df52
--- /dev/null
+++ b/doc/development/img/merge_widget_v14_7.png
Binary files differ
diff --git a/doc/development/merge_request_concepts/index.md b/doc/development/merge_request_concepts/index.md
new file mode 100644
index 00000000000..f1dab69543a
--- /dev/null
+++ b/doc/development/merge_request_concepts/index.md
@@ -0,0 +1,62 @@
+---
+type: reference, dev
+stage: create
+group: code_review
+info: "See the Technical Writers assigned to Development Guidelines: https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments-to-development-guidelines"
+---
+
+# Merge Request Concepts
+
+**NOTE**:
+The documentation below is the single source of truth for the merge request terminology and functionality.
+
+## Overview
+
+The merge request is made up of several different key components and ideas that encompass the overall merge request experience. These concepts sometimes have competing and confusing terminology or overlap with other concepts. The concepts this will cover are:
+
+1. Merge widget
+1. Report widgets
+1. Merge checks
+1. Approval rules
+
+### Merge widget
+
+The merge widget is the component of the merge request where the `merge` button exists:
+
+![merge widget](../img/merge_widget_v14_7.png)
+
+This area of the merge request is where all of the options and commit messages are defined prior to merging. It also contains information about what is in the merge request, what issues may be closed, and other important information to the merging process.
+
+### Report widgets
+
+Reports are widgets within the merge request that report information about changes within the merge request. These widgets provide information to better help the author understand the changes and further improvements to the proposed changes.
+
+[Design Documentation](https://design.gitlab.com/regions/merge-request-reports)
+
+![merge request reports](../img/merge_request_reports_v14_7.png)
+
+### Merge checks
+
+Merge checks are statuses that can either pass or fail and conditionally control the availability of the merge button being available within a merge request. The key distinguishing factor in a merge check is that users **do not** interact with the merge checks inside of the merge request, but are able to influence whether or not the check passes or fails. Results from the check are processed as true/false to determine whether or not a merge request can be merged. Examples include:
+
+1. merge conflicts
+1. pipeline success
+1. threads resolution
+1. [external status checks](../../user/project/merge_requests/status_checks.md)
+1. required approvals
+
+When all of the required merge checks are satisfied a merge request becomes mergeable.
+
+### Approvals
+
+Approval rules specify users that are required to or can optionally approve a merge request based on some kind of organizational policy. When approvals are required, they effectively become a required merge check. The key differentiator between merge checks and approval rules is that users **do** interact with approval rules, by deciding to approve the merge request.
+
+Additionally, approval settings provide configuration options to define how those approval rules are applied in a merge request. They can set limitations, add requirements, or modify approvals.
+
+Examples of approval rules and settings include:
+
+1. [merge request approval rules](../../user/project/merge_requests/approvals/rules.md)
+1. [code owner approvals](../../user/project/code_owners.md)
+1. [security approvals](../../user/application_security/index.md#security-approvals-in-merge-requests)
+1. [prevent editing approval rules](../../user/project/merge_requests/approvals/settings.md#prevent-editing-approval-rules-in-merge-requests)]
+1. [remove all approvals when commits are added](../../user/project/merge_requests/approvals/settings.md#remove-all-approvals-when-commits-are-added-to-the-source-branch)
diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md
index 4655982a03b..5b6c71d4dd2 100644
--- a/doc/user/packages/container_registry/index.md
+++ b/doc/user/packages/container_registry/index.md
@@ -695,3 +695,10 @@ There may be some errors not properly cached. Follow these steps to investigate
Once adjusted, trigger another tag deletion. You should be able to successfully delete tags.
Follow [this issue](https://gitlab.com/gitlab-org/container-registry/-/issues/551) for details.
+
+### Tags temporarily cannot be marked for deletion
+
+GitLab is [migrating to the next generation of the Container Registry](https://gitlab.com/groups/gitlab-org/-/epics/5523).
+During the migration, you may encounter difficulty deleting tags.
+If you encounter an error, it's likely that your image repository is in the process of being migrated.
+Please wait a few minutes and try again.
diff --git a/doc/user/project/merge_requests/commit_templates.md b/doc/user/project/merge_requests/commit_templates.md
index ca371af56a3..b9b65d759dc 100644
--- a/doc/user/project/merge_requests/commit_templates.md
+++ b/doc/user/project/merge_requests/commit_templates.md
@@ -84,7 +84,7 @@ Commit message templates support these variables:
| `%{first_commit}` | Full message of the first commit in merge request diff. | `Update README.md` |
| `%{first_multiline_commit}` | Full message of the first commit that's not a merge commit and has more than one line in message body. Merge request title if all commits aren't multiline. | `Update README.md`<br><br>`Improved project description in readme file.` |
| `%{url}` | Full URL to the merge request. | `https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1` |
-| `%{approved_by}` | Line-separated list of the merge request approvers. This value is not updated until the first page refresh after an approval. | `Approved-by: Sidney Jones <sjones@example.com>` <br> `Approved-by: Zhang Wei <zwei@example.com>` |
+| `%{approved_by}` | Line-separated list of the merge request approvers. | `Approved-by: Sidney Jones <sjones@example.com>` <br> `Approved-by: Zhang Wei <zwei@example.com>` |
| `%{merged_by}` | User who merged the merge request. | `Alex Garcia <agarcia@example.com>` |
| `%{co_authored_by}` | Names and emails of commit authors in a `Co-authored-by` Git commit trailer format. Limited to authors of 100 most recent commits in merge request. | `Co-authored-by: Zane Doe <zdoe@example.com>` <br> `Co-authored-by: Blake Smith <bsmith@example.com>` |
| `%{all_commits}` | Messages from all commits in the merge request. Limited to 100 most recent commits. Skips commit bodies exceeding 100KiB and merge commit messages. | `* Feature introduced` <br><br> `This commit implements feature` <br> `Changelog:added` <br><br> `* Bug fixed` <br><br> `* Documentation improved` <br><br>`This commit introduced better docs.`|
@@ -92,6 +92,10 @@ Commit message templates support these variables:
Any line containing only an empty variable is removed. If the line to be removed is both
preceded and followed by an empty line, the preceding empty line is also removed.
+After you edit a commit message on an open merge request, GitLab will
+not automatically update the commit message again.
+To restore the commit message to the project template, reload the page.
+
## Related topics
- [Squash and merge](squash_and_merge.md).
diff --git a/lib/api/entities/wiki_page.rb b/lib/api/entities/wiki_page.rb
index c6dba10e441..43af6a336d2 100644
--- a/lib/api/entities/wiki_page.rb
+++ b/lib/api/entities/wiki_page.rb
@@ -6,7 +6,7 @@ module API
include ::MarkupHelper
expose :content do |wiki_page, options|
- options[:render_html] ? render_wiki_content(wiki_page) : wiki_page.content
+ options[:render_html] ? render_wiki_content(wiki_page, ref: wiki_page.version.id) : wiki_page.content
end
expose :encoding do |wiki_page|
diff --git a/lib/api/helpers/wikis_helpers.rb b/lib/api/helpers/wikis_helpers.rb
index 4a14dc1f40a..a9cd0e2919d 100644
--- a/lib/api/helpers/wikis_helpers.rb
+++ b/lib/api/helpers/wikis_helpers.rb
@@ -13,8 +13,8 @@ module API
raise "Unknown wiki container #{kind}"
end
- def wiki_page
- Wiki.for_container(container, current_user).find_page(params[:slug]) || not_found!('Wiki Page')
+ def wiki_page(version = nil)
+ Wiki.for_container(container, current_user).find_page(params[:slug], version.presence) || not_found!('Wiki Page')
end
def commit_params(attrs)
diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb
index fca690df916..e90d88940a5 100644
--- a/lib/api/wikis.rb
+++ b/lib/api/wikis.rb
@@ -45,12 +45,13 @@ module API
end
params do
requires :slug, type: String, desc: 'The slug of a wiki page'
+ optional :version, type: String, desc: 'The version hash of a wiki page'
optional :render_html, type: Boolean, default: false, desc: 'Render content to HTML'
end
get ':id/wikis/:slug' do
authorize! :read_wiki, container
- present wiki_page, with: Entities::WikiPage, render_html: params[:render_html]
+ present wiki_page(params[:version]), with: Entities::WikiPage, render_html: params[:render_html]
end
desc 'Create a wiki page' do
diff --git a/lib/gitlab/database/background_migration/batched_job.rb b/lib/gitlab/database/background_migration/batched_job.rb
index fba0eb2fb13..f3160679d64 100644
--- a/lib/gitlab/database/background_migration/batched_job.rb
+++ b/lib/gitlab/database/background_migration/batched_job.rb
@@ -3,6 +3,8 @@
module Gitlab
module Database
module BackgroundMigration
+ SplitAndRetryError = Class.new(StandardError)
+
class BatchedJob < SharedModel
include EachBatch
include FromUnion
@@ -11,6 +13,8 @@ module Gitlab
MAX_ATTEMPTS = 3
STUCK_JOBS_TIMEOUT = 1.hour.freeze
+ TIMEOUT_EXCEPTIONS = [ActiveRecord::StatementTimeout, ActiveRecord::ConnectionTimeoutError,
+ ActiveRecord::AdapterTimeout, ActiveRecord::LockWaitTimeout].freeze
belongs_to :batched_migration, foreign_key: :batched_background_migration_id
has_many :batched_job_transition_logs, foreign_key: :batched_background_migration_job_id
@@ -51,6 +55,16 @@ module Gitlab
job.metrics = {}
end
+ after_transition any => :failed do |job, transition|
+ error_hash = transition.args.find { |arg| arg[:error].present? }
+
+ exception = error_hash&.fetch(:error)
+
+ job.split_and_retry! if job.can_split?(exception)
+ rescue SplitAndRetryError => error
+ Gitlab::AppLogger.error(message: error.message, batched_job_id: job.id)
+ end
+
after_transition do |job, transition|
error_hash = transition.args.find { |arg| arg[:error].present? }
@@ -79,13 +93,17 @@ module Gitlab
duration.to_f / batched_migration.interval
end
+ def can_split?(exception)
+ attempts >= MAX_ATTEMPTS && TIMEOUT_EXCEPTIONS.include?(exception&.class) && batch_size > sub_batch_size
+ end
+
def split_and_retry!
with_lock do
- raise 'Only failed jobs can be split' unless failed?
+ raise SplitAndRetryError, 'Only failed jobs can be split' unless failed?
new_batch_size = batch_size / 2
- raise 'Job cannot be split further' if new_batch_size < 1
+ raise SplitAndRetryError, 'Job cannot be split further' if new_batch_size < 1
batching_strategy = batched_migration.batch_class.new(connection: self.class.connection)
next_batch_bounds = batching_strategy.next_batch(
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ea9a5c64a05..50f015c9038 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -9723,6 +9723,9 @@ msgstr ""
msgid "ContainerRegistry|Tags successfully marked for deletion."
msgstr ""
+msgid "ContainerRegistry|Tags temporarily cannot be marked for deletion. Please try again in a few minutes. %{docLinkStart}More details%{docLinkEnd}."
+msgstr ""
+
msgid "ContainerRegistry|Tags that match these rules are %{strongStart}kept%{strongEnd}, even if they match a removal rule below. The %{secondStrongStart}latest%{secondStrongEnd} tag is always kept."
msgstr ""
@@ -38768,6 +38771,9 @@ msgstr ""
msgid "To personalize your GitLab experience, we'd like to know a bit more about you"
msgstr ""
+msgid "To preserve performance only %{strongStart}%{visible} of %{total}%{strongEnd} files are displayed."
+msgstr ""
+
msgid "To preserve performance only %{strong_open}%{display_size} of %{real_size}%{strong_close} files are displayed."
msgstr ""
diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb
index 117d47b96c8..740a8920cf2 100644
--- a/qa/qa/resource/project.rb
+++ b/qa/qa/resource/project.rb
@@ -385,6 +385,18 @@ module QA
end
end
+ def remove_via_api!
+ super
+
+ Support::Retrier.retry_until(max_duration: 60, sleep_interval: 1, message: "Waiting for #{self.class.name} to be removed") do
+ !exists?
+ rescue InternalServerError
+ # Retry on transient errors that are likely to be due to race conditions between concurrent delete operations
+ # when parts of a resource are stored in multiple tables
+ false
+ end
+ end
+
protected
# Return subset of fields for comparing projects
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index 152ae061605..aa264ad3377 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -118,5 +118,14 @@ FactoryBot.define do
create(:crm_settings, group: group, enabled: true)
end
end
+
+ trait :test_group do
+ path { "test-group-fulfillment#{SecureRandom.hex(4)}" }
+ created_at { 4.days.ago }
+
+ after(:create) do |group|
+ group.add_owner(create(:user, email: "test-user-#{SecureRandom.hex(4)}@test.com"))
+ end
+ end
end
end
diff --git a/spec/features/groups/container_registry_spec.rb b/spec/features/groups/container_registry_spec.rb
index f5af9ba8b7b..7bef2dc9416 100644
--- a/spec/features/groups/container_registry_spec.rb
+++ b/spec/features/groups/container_registry_spec.rb
@@ -97,6 +97,8 @@ RSpec.describe 'Container Registry', :js do
expect(find('.modal .modal-title')).to have_content _('Remove tag')
find('.modal .modal-footer .btn-danger').click
end
+
+ it_behaves_like 'rejecting tags destruction for an importing repository on', tags: ['latest']
end
end
diff --git a/spec/features/merge_request/user_sees_diff_spec.rb b/spec/features/merge_request/user_sees_diff_spec.rb
index 03f67669061..50f4cce5c23 100644
--- a/spec/features/merge_request/user_sees_diff_spec.rb
+++ b/spec/features/merge_request/user_sees_diff_spec.rb
@@ -45,8 +45,8 @@ RSpec.describe 'Merge request > User sees diff', :js do
visit diffs_project_merge_request_path(project, merge_request)
- page.within('.alert') do
- expect(page).to have_text("Too many changes to show. Plain diff Email patch To preserve performance only 3 of 3+ files are displayed.")
+ page.within('.gl-alert') do
+ expect(page).to have_text("Too many changes to show. To preserve performance only 3 of 3+ files are displayed. Plain diff Email patch")
end
end
end
diff --git a/spec/features/projects/container_registry_spec.rb b/spec/features/projects/container_registry_spec.rb
index 4ebcb69592b..17eb421191f 100644
--- a/spec/features/projects/container_registry_spec.rb
+++ b/spec/features/projects/container_registry_spec.rb
@@ -103,6 +103,8 @@ RSpec.describe 'Container Registry', :js do
find('.modal .modal-footer .btn-danger').click
end
+ it_behaves_like 'rejecting tags destruction for an importing repository on', tags: ['1']
+
it('pagination navigate to the second page') do
visit_next_page
diff --git a/spec/frontend/diffs/components/hidden_files_warning_spec.js b/spec/frontend/diffs/components/hidden_files_warning_spec.js
index 3f1f23a40f5..bbd4f5faeec 100644
--- a/spec/frontend/diffs/components/hidden_files_warning_spec.js
+++ b/spec/frontend/diffs/components/hidden_files_warning_spec.js
@@ -1,4 +1,6 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
const propsData = {
@@ -12,7 +14,7 @@ describe('HiddenFilesWarning', () => {
let wrapper;
const createComponent = () => {
- wrapper = shallowMount(HiddenFilesWarning, {
+ wrapper = mount(HiddenFilesWarning, {
propsData,
});
};
@@ -26,22 +28,20 @@ describe('HiddenFilesWarning', () => {
});
it('has a correct plain diff URL', () => {
- const plainDiffLink = wrapper.findAll('a').wrappers.filter((x) => x.text() === 'Plain diff')[0];
+ const plainDiffLink = wrapper.findAllComponents(GlButton).at(0);
expect(plainDiffLink.attributes('href')).toBe(propsData.plainDiffPath);
});
it('has a correct email patch URL', () => {
- const emailPatchLink = wrapper
- .findAll('a')
- .wrappers.filter((x) => x.text() === 'Email patch')[0];
+ const emailPatchLink = wrapper.findAllComponents(GlButton).at(1);
expect(emailPatchLink.attributes('href')).toBe(propsData.emailPatchPath);
});
it('has a correct visible/total files text', () => {
- const filesText = wrapper.find('strong');
-
- expect(filesText.text()).toBe('5 of 10');
+ expect(wrapper.text()).toContain(
+ __('To preserve performance only 5 of 10 files are displayed.'),
+ );
});
});
diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb
index 1eb48c0ce2c..1eae854eca3 100644
--- a/spec/frontend/fixtures/merge_requests.rb
+++ b/spec/frontend/fixtures/merge_requests.rb
@@ -130,6 +130,25 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type:
expect(response).to be_successful
end
+ describe GraphQL::Query, type: :request do
+ include ApiHelpers
+ include GraphqlHelpers
+
+ context 'merge request in state readyToMerge query' do
+ base_input_path = 'vue_merge_request_widget/queries/states/'
+ base_output_path = 'graphql/merge_requests/states/'
+ query_name = 'ready_to_merge.query.graphql'
+
+ it "#{base_output_path}#{query_name}.json" do
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}", ee: true)
+
+ post_graphql(query, current_user: user, variables: { projectPath: project.full_path, iid: merge_request.iid.to_s })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+ end
+
private
def render_discussions_json(merge_request)
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index 96c9baeb328..9a30fd5f5c3 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -169,12 +169,11 @@ describe('RepoEditor', () => {
expect(findEditor().isVisible()).toBe(true);
});
- it('renders only an edit tab', async () => {
+ it('renders no tabs', async () => {
await createComponent();
const tabs = findTabs();
- expect(tabs).toHaveLength(1);
- expect(tabs.at(0).text()).toBe('Edit');
+ expect(tabs).toHaveLength(0);
});
});
@@ -196,25 +195,48 @@ describe('RepoEditor', () => {
mock.restore();
});
- it('renders an Edit and a Preview Tab', async () => {
- await createComponent({ activeFile });
- const tabs = findTabs();
+ describe('when files is markdown', () => {
+ let layoutSpy;
- expect(tabs).toHaveLength(2);
- expect(tabs.at(0).text()).toBe('Edit');
- expect(tabs.at(1).text()).toBe('Preview Markdown');
- });
+ beforeEach(async () => {
+ await createComponent({ activeFile });
+ layoutSpy = jest.spyOn(wrapper.vm.editor, 'layout');
+ });
- it('renders markdown for tempFile', async () => {
- // by default files created in the spec are temp: no need for explicitly sending the param
- await createComponent({ activeFile });
+ it('renders an Edit and a Preview Tab', () => {
+ const tabs = findTabs();
- findPreviewTab().trigger('click');
- await waitForPromises();
- expect(wrapper.find(ContentViewer).html()).toContain(defaultFileProps.content);
+ expect(tabs).toHaveLength(2);
+ expect(tabs.at(0).text()).toBe('Edit');
+ expect(tabs.at(1).text()).toBe('Preview Markdown');
+ });
+
+ it('renders markdown for tempFile', async () => {
+ findPreviewTab().trigger('click');
+ await waitForPromises();
+ expect(wrapper.find(ContentViewer).html()).toContain(defaultFileProps.content);
+ });
+
+ it('should not trigger layout', async () => {
+ expect(layoutSpy).not.toHaveBeenCalled();
+ });
+
+ describe('when file changes to non-markdown file', () => {
+ beforeEach(async () => {
+ wrapper.setProps({ file: dummyFile.empty });
+ });
+
+ it('should hide tabs', () => {
+ expect(findTabs()).toHaveLength(0);
+ });
+
+ it('should trigger refresh dimensions', async () => {
+ expect(layoutSpy).toHaveBeenCalledTimes(1);
+ });
+ });
});
- it('shows no tabs when not in Edit mode', async () => {
+ it('when not in edit mode, shows no tabs', async () => {
await createComponent({
state: {
currentActivityView: leftSidebarViews.review.name,
@@ -405,7 +427,7 @@ describe('RepoEditor', () => {
it.each`
mode | isVisible
- ${'edit'} | ${true}
+ ${'edit'} | ${false}
${'review'} | ${false}
${'commit'} | ${false}
`('tabs in $mode are $isVisible', async ({ mode, isVisible } = {}) => {
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index 3fea08d5512..0be0bf89210 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -51,31 +51,6 @@ describe('common_utils', () => {
});
});
- describe('parseUrl', () => {
- it('returns an anchor tag with url', () => {
- expect(commonUtils.parseUrl('/some/absolute/url').pathname).toContain('some/absolute/url');
- });
-
- it('url is escaped', () => {
- // IE11 will return a relative pathname while other browsers will return a full pathname.
- // parseUrl uses an anchor element for parsing an url. With relative urls, the anchor
- // element will create an absolute url relative to the current execution context.
- // The JavaScript test suite is executed at '/' which will lead to an absolute url
- // starting with '/'.
- expect(commonUtils.parseUrl('" test="asf"').pathname).toContain('/%22%20test=%22asf%22');
- });
- });
-
- describe('parseUrlPathname', () => {
- it('returns an absolute url when given an absolute url', () => {
- expect(commonUtils.parseUrlPathname('/some/absolute/url')).toEqual('/some/absolute/url');
- });
-
- it('returns an absolute url when given a relative url', () => {
- expect(commonUtils.parseUrlPathname('some/relative/url')).toEqual('/some/relative/url');
- });
- });
-
describe('handleLocationHash', () => {
beforeEach(() => {
jest.spyOn(window.document, 'getElementById');
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index c6edba19c56..7608cff4c9e 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -22,6 +22,27 @@ beforeEach(() => {
});
describe('URL utility', () => {
+ describe('parseUrlPathname', () => {
+ it('returns an absolute url when given an absolute url', () => {
+ expect(urlUtils.parseUrlPathname('/some/absolute/url')).toBe('/some/absolute/url');
+ });
+
+ it('returns an absolute url when given a relative url', () => {
+ expect(urlUtils.parseUrlPathname('some/relative/url')).toBe('/some/relative/url');
+ });
+
+ it('returns an absolute url that includes the document.location path when given a relative url', () => {
+ // Change the location to see the `/mypath/` included in the result
+ setWindowLocation(`${TEST_HOST}/mypath/`);
+
+ expect(urlUtils.parseUrlPathname('some/relative/url')).toBe('/mypath/some/relative/url');
+ });
+
+ it('encodes certain character in the url', () => {
+ expect(urlUtils.parseUrlPathname('test="a b"')).toBe('/test=%22a%20b%22');
+ });
+ });
+
describe('webIDEUrl', () => {
afterEach(() => {
gon.relative_url_root = '';
@@ -636,7 +657,7 @@ describe('URL utility', () => {
`('returns "$expectation" with "$protocol" protocol', ({ protocol, expectation }) => {
setWindowLocation(`${protocol}//example.com`);
- expect(urlUtils.getWebSocketProtocol()).toEqual(expectation);
+ expect(urlUtils.getWebSocketProtocol()).toBe(expectation);
});
});
@@ -646,7 +667,7 @@ describe('URL utility', () => {
const path = '/lorem/ipsum?a=bc';
- expect(urlUtils.getWebSocketUrl(path)).toEqual('ws://example.com/lorem/ipsum?a=bc');
+ expect(urlUtils.getWebSocketUrl(path)).toBe('ws://example.com/lorem/ipsum?a=bc');
});
});
@@ -696,7 +717,7 @@ describe('URL utility', () => {
it('should return valid parameter', () => {
setWindowLocation('?scope=all&p=2');
- expect(getParameterByName('p')).toEqual('2');
+ expect(getParameterByName('p')).toBe('2');
expect(getParameterByName('scope')).toBe('all');
});
@@ -737,7 +758,7 @@ describe('URL utility', () => {
it('converts search query object back into a search query', () => {
const searchQueryObject = { one: '1', two: '2' };
- expect(urlUtils.objectToQuery(searchQueryObject)).toEqual('one=1&two=2');
+ expect(urlUtils.objectToQuery(searchQueryObject)).toBe('one=1&two=2');
});
it('returns empty string when `params` is undefined, null or empty string', () => {
@@ -833,15 +854,15 @@ describe('URL utility', () => {
it('adds new params as query string', () => {
const url = 'https://gitlab.com/test';
- expect(
- urlUtils.setUrlParams({ group_id: 'gitlab-org', project_id: 'my-project' }, url),
- ).toEqual('https://gitlab.com/test?group_id=gitlab-org&project_id=my-project');
+ expect(urlUtils.setUrlParams({ group_id: 'gitlab-org', project_id: 'my-project' }, url)).toBe(
+ 'https://gitlab.com/test?group_id=gitlab-org&project_id=my-project',
+ );
});
it('updates an existing parameter', () => {
const url = 'https://gitlab.com/test?group_id=gitlab-org&project_id=my-project';
- expect(urlUtils.setUrlParams({ project_id: 'gitlab-test' }, url)).toEqual(
+ expect(urlUtils.setUrlParams({ project_id: 'gitlab-test' }, url)).toBe(
'https://gitlab.com/test?group_id=gitlab-org&project_id=gitlab-test',
);
});
@@ -849,7 +870,7 @@ describe('URL utility', () => {
it("removes the project_id param when it's value is null", () => {
const url = 'https://gitlab.com/test?group_id=gitlab-org&project_id=my-project';
- expect(urlUtils.setUrlParams({ project_id: null }, url)).toEqual(
+ expect(urlUtils.setUrlParams({ project_id: null }, url)).toBe(
'https://gitlab.com/test?group_id=gitlab-org',
);
});
@@ -857,7 +878,7 @@ describe('URL utility', () => {
it('adds parameters from arrays', () => {
const url = 'https://gitlab.com/test';
- expect(urlUtils.setUrlParams({ labels: ['foo', 'bar'] }, url)).toEqual(
+ expect(urlUtils.setUrlParams({ labels: ['foo', 'bar'] }, url)).toBe(
'https://gitlab.com/test?labels=foo&labels=bar',
);
});
@@ -865,13 +886,13 @@ describe('URL utility', () => {
it('removes parameters from empty arrays', () => {
const url = 'https://gitlab.com/test?labels=foo&labels=bar';
- expect(urlUtils.setUrlParams({ labels: [] }, url)).toEqual('https://gitlab.com/test');
+ expect(urlUtils.setUrlParams({ labels: [] }, url)).toBe('https://gitlab.com/test');
});
it('removes parameters from empty arrays while keeping other parameters', () => {
const url = 'https://gitlab.com/test?labels=foo&labels=bar&unrelated=unrelated';
- expect(urlUtils.setUrlParams({ labels: [] }, url)).toEqual(
+ expect(urlUtils.setUrlParams({ labels: [] }, url)).toBe(
'https://gitlab.com/test?unrelated=unrelated',
);
});
@@ -879,7 +900,7 @@ describe('URL utility', () => {
it('adds parameters from arrays when railsArraySyntax=true', () => {
const url = 'https://gitlab.com/test';
- expect(urlUtils.setUrlParams({ labels: ['foo', 'bar'] }, url, false, true)).toEqual(
+ expect(urlUtils.setUrlParams({ labels: ['foo', 'bar'] }, url, false, true)).toBe(
'https://gitlab.com/test?labels%5B%5D=foo&labels%5B%5D=bar',
);
});
@@ -887,7 +908,7 @@ describe('URL utility', () => {
it('removes parameters from empty arrays when railsArraySyntax=true', () => {
const url = 'https://gitlab.com/test?labels%5B%5D=foo&labels%5B%5D=bar';
- expect(urlUtils.setUrlParams({ labels: [] }, url, false, true)).toEqual(
+ expect(urlUtils.setUrlParams({ labels: [] }, url, false, true)).toBe(
'https://gitlab.com/test',
);
});
@@ -895,7 +916,7 @@ describe('URL utility', () => {
it('decodes URI when decodeURI=true', () => {
const url = 'https://gitlab.com/test';
- expect(urlUtils.setUrlParams({ labels: ['foo', 'bar'] }, url, false, true, true)).toEqual(
+ expect(urlUtils.setUrlParams({ labels: ['foo', 'bar'] }, url, false, true, true)).toBe(
'https://gitlab.com/test?labels[]=foo&labels[]=bar',
);
});
@@ -903,7 +924,7 @@ describe('URL utility', () => {
it('removes all existing URL params and sets a new param when cleanParams=true', () => {
const url = 'https://gitlab.com/test?group_id=gitlab-org&project_id=my-project';
- expect(urlUtils.setUrlParams({ foo: 'bar' }, url, true)).toEqual(
+ expect(urlUtils.setUrlParams({ foo: 'bar' }, url, true)).toBe(
'https://gitlab.com/test?foo=bar',
);
});
diff --git a/spec/frontend/merge_request_tabs_spec.js b/spec/frontend/merge_request_tabs_spec.js
index ced9b71125b..5c24a070342 100644
--- a/spec/frontend/merge_request_tabs_spec.js
+++ b/spec/frontend/merge_request_tabs_spec.js
@@ -4,6 +4,7 @@ import initMrPage from 'helpers/init_vue_mr_page_helper';
import axios from '~/lib/utils/axios_utils';
import MergeRequestTabs from '~/merge_request_tabs';
import '~/lib/utils/common_utils';
+import '~/lib/utils/url_utility';
jest.mock('~/lib/utils/webpack', () => ({
resetServiceWorkersPublicPath: jest.fn(),
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js
index e25162f4da5..9680e273add 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js
@@ -6,6 +6,7 @@ import {
DELETE_TAG_ERROR_MESSAGE,
DELETE_TAGS_SUCCESS_MESSAGE,
DELETE_TAGS_ERROR_MESSAGE,
+ DETAILS_IMPORTING_ERROR_MESSAGE,
ADMIN_GARBAGE_COLLECTION_TIP,
} from '~/packages_and_registries/container_registry/explorer/constants';
@@ -76,6 +77,7 @@ describe('Delete alert', () => {
});
});
});
+
describe('error states', () => {
describe.each`
deleteAlertType | message
@@ -105,6 +107,25 @@ describe('Delete alert', () => {
});
});
+ describe('importing repository error state', () => {
+ beforeEach(() => {
+ mountComponent({
+ deleteAlertType: 'danger_importing',
+ containerRegistryImportingHelpPagePath: 'https://foobar',
+ });
+ });
+
+ it('alert exist and text is appropriate', () => {
+ expect(findAlert().text()).toMatchInterpolatedText(DETAILS_IMPORTING_ERROR_MESSAGE);
+ });
+
+ it('alert body contains link', () => {
+ const alertLink = findLink();
+ expect(alertLink.exists()).toBe(true);
+ expect(alertLink.attributes('href')).toBe('https://foobar');
+ });
+ });
+
describe('dismissing alert', () => {
it('GlAlert dismiss event triggers a change event', () => {
mountComponent({ deleteAlertType: 'success_tags' });
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
index f0c586fe7f4..fda1db4b7e1 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
@@ -239,6 +239,15 @@ export const graphQLDeleteImageRepositoryTagsMock = {
},
};
+export const graphQLDeleteImageRepositoryTagImportingErrorMock = {
+ data: {
+ destroyContainerRepositoryTags: {
+ errors: ['repository importing'],
+ __typename: 'DestroyContainerRepositoryTagsPayload',
+ },
+ },
+};
+
export const dockerCommands = {
dockerBuildCommand: 'foofoo',
dockerPushCommand: 'barbar',
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
index c602b37c3b5..59ca47bee50 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
@@ -18,6 +18,7 @@ import {
UNFINISHED_STATUS,
DELETE_SCHEDULED,
ALERT_DANGER_IMAGE,
+ ALERT_DANGER_IMPORTING,
MISSING_OR_DELETED_IMAGE_BREADCRUMB,
ROOT_IMAGE_TEXT,
MISSING_OR_DELETED_IMAGE_TITLE,
@@ -33,6 +34,7 @@ import Tracking from '~/tracking';
import {
graphQLImageDetailsMock,
graphQLDeleteImageRepositoryTagsMock,
+ graphQLDeleteImageRepositoryTagImportingErrorMock,
containerRepositoryMock,
graphQLEmptyImageDetailsMock,
tagsMock,
@@ -329,6 +331,7 @@ describe('Details Page', () => {
const config = {
isAdmin: true,
garbageCollectionHelpPagePath: 'baz',
+ containerRegistryImportingHelpPagePath: 'https://foobar',
};
const deleteAlertType = 'success_tag';
@@ -353,6 +356,35 @@ describe('Details Page', () => {
expect(findDeleteAlert().props()).toEqual({ ...config, deleteAlertType });
});
+
+ describe('importing repository error', () => {
+ let mutationResolver;
+ let tagsResolver;
+
+ beforeEach(async () => {
+ mutationResolver = jest
+ .fn()
+ .mockResolvedValue(graphQLDeleteImageRepositoryTagImportingErrorMock);
+ tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock));
+
+ mountComponent({ mutationResolver, tagsResolver });
+ await waitForApolloRequestRender();
+ });
+
+ it('displays the proper alert', async () => {
+ findTagsList().vm.$emit('delete', [cleanTags[0]]);
+ await nextTick();
+
+ findDeleteModal().vm.$emit('confirmDelete');
+ await waitForPromises();
+
+ expect(tagsResolver).toHaveBeenCalled();
+
+ const deleteAlert = findDeleteAlert();
+ expect(deleteAlert.exists()).toBe(true);
+ expect(deleteAlert.props('deleteAlertType')).toBe(ALERT_DANGER_IMPORTING);
+ });
+ });
});
describe('Partial Cleanup Alert', () => {
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 78585ed75bc..0e364eb6800 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
@@ -1,7 +1,12 @@
-import { shallowMount } from '@vue/test-utils';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { GlSprintf } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import produce from 'immer';
+import readyToMergeResponse from 'test_fixtures/graphql/merge_requests/states/ready_to_merge.query.graphql.json';
import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql';
import simplePoll from '~/lib/utils/simple_poll';
import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit.vue';
import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue';
@@ -19,9 +24,11 @@ jest.mock('~/commons/nav/user_merge_requests', () => ({
refreshUserMergeRequestCounts: jest.fn(),
}));
-const commitMessage = 'This is the commit message';
-const squashCommitMessage = 'This is the squash commit message';
-const commitMessageWithDescription = 'This is the commit message description';
+const commitMessage = readyToMergeResponse.data.project.mergeRequest.defaultMergeCommitMessage;
+const squashCommitMessage =
+ readyToMergeResponse.data.project.mergeRequest.defaultSquashCommitMessage;
+const commitMessageWithDescription =
+ readyToMergeResponse.data.project.mergeRequest.defaultMergeCommitMessageWithDescription;
const createTestMr = (customConfig) => {
const mr = {
isPipelineActive: false,
@@ -42,6 +49,8 @@ const createTestMr = (customConfig) => {
commitMessage,
squashCommitMessage,
commitMessageWithDescription,
+ defaultMergeCommitMessage: commitMessage,
+ defaultSquashCommitMessage: squashCommitMessage,
shouldRemoveSourceBranch: true,
canRemoveSourceBranch: false,
targetBranch: 'main',
@@ -61,15 +70,25 @@ const createTestService = () => ({
merge: jest.fn(),
poll: jest.fn().mockResolvedValue(),
});
+const localVue = createLocalVue();
+localVue.use(VueApollo);
let wrapper;
+let readyToMergeResponseSpy;
const findMergeButton = () => wrapper.find('[data-testid="merge-button"]');
const findPipelineFailedConfirmModal = () =>
wrapper.findComponent(MergeFailedPipelineConfirmationDialog);
+const createReadyToMergeResponse = (customMr) => {
+ return produce(readyToMergeResponse, (draft) => {
+ Object.assign(draft.data.project.mergeRequest, customMr);
+ });
+};
+
const createComponent = (customConfig = {}, mergeRequestWidgetGraphql = false) => {
wrapper = shallowMount(ReadyToMerge, {
+ localVue,
propsData: {
mr: createTestMr(customConfig),
service: createTestService(),
@@ -82,10 +101,29 @@ const createComponent = (customConfig = {}, mergeRequestWidgetGraphql = false) =
stubs: {
CommitEdit,
},
+ apolloProvider: createMockApollo([[readyToMergeQuery, readyToMergeResponseSpy]]),
});
};
+const findCheckboxElement = () => wrapper.find(SquashBeforeMerge);
+const findCommitsHeaderElement = () => wrapper.find(CommitsHeader);
+const findCommitEditElements = () => wrapper.findAll(CommitEdit);
+const findCommitDropdownElement = () => wrapper.find(CommitMessageDropdown);
+const findFirstCommitEditLabel = () => findCommitEditElements().at(0).props('label');
+const findTipLink = () => wrapper.find(GlSprintf);
+const findCommitEditWithInputId = (inputId) =>
+ findCommitEditElements().wrappers.find((x) => x.props('inputId') === inputId);
+const findMergeCommitMessage = () => findCommitEditWithInputId('merge-message-edit').props('value');
+const findSquashCommitMessage = () =>
+ findCommitEditWithInputId('squash-message-edit').props('value');
+
+const triggerApprovalUpdated = () => eventHub.$emit('ApprovalUpdated');
+
describe('ReadyToMerge', () => {
+ beforeEach(() => {
+ readyToMergeResponseSpy = jest.fn().mockResolvedValueOnce(readyToMergeResponse);
+ });
+
afterEach(() => {
wrapper.destroy();
});
@@ -447,13 +485,6 @@ describe('ReadyToMerge', () => {
});
describe('render children components', () => {
- const findCheckboxElement = () => wrapper.find(SquashBeforeMerge);
- const findCommitsHeaderElement = () => wrapper.find(CommitsHeader);
- const findCommitEditElements = () => wrapper.findAll(CommitEdit);
- const findCommitDropdownElement = () => wrapper.find(CommitMessageDropdown);
- const findFirstCommitEditLabel = () => findCommitEditElements().at(0).props('label');
- const findTipLink = () => wrapper.find(GlSprintf);
-
describe('squash checkbox', () => {
it('should be rendered when squash before merge is enabled and there is more than 1 commit', () => {
createComponent({
@@ -772,4 +803,65 @@ describe('ReadyToMerge', () => {
expect(findPipelineFailedConfirmModal().props()).toEqual({ visible: true });
});
});
+
+ describe('updating graphql data triggers commit message update when default changed', () => {
+ const UPDATED_MERGE_COMMIT_MESSAGE = 'New merge message from BE';
+ const UPDATED_SQUASH_COMMIT_MESSAGE = 'New squash message from BE';
+ const USER_COMMIT_MESSAGE = 'Merge message provided manually by user';
+
+ const createDefaultGqlComponent = () =>
+ createComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: true } }, true);
+
+ beforeEach(() => {
+ readyToMergeResponseSpy = jest
+ .fn()
+ .mockResolvedValueOnce(createReadyToMergeResponse({ squash: true, squashOnMerge: true }))
+ .mockResolvedValue(
+ createReadyToMergeResponse({
+ squash: true,
+ squashOnMerge: true,
+ defaultMergeCommitMessage: UPDATED_MERGE_COMMIT_MESSAGE,
+ defaultSquashCommitMessage: UPDATED_SQUASH_COMMIT_MESSAGE,
+ }),
+ );
+ });
+
+ describe.each`
+ desc | finderFn | initialValue | updatedValue | inputId
+ ${'merge commit message'} | ${findMergeCommitMessage} | ${commitMessage} | ${UPDATED_MERGE_COMMIT_MESSAGE} | ${'#merge-message-edit'}
+ ${'squash commit message'} | ${findSquashCommitMessage} | ${squashCommitMessage} | ${UPDATED_SQUASH_COMMIT_MESSAGE} | ${'#squash-message-edit'}
+ `('with $desc', ({ finderFn, initialValue, updatedValue, inputId }) => {
+ it('should have initial value', async () => {
+ createDefaultGqlComponent();
+
+ await waitForPromises();
+
+ expect(finderFn()).toBe(initialValue);
+ });
+
+ it('should have updated value after graphql refetch', async () => {
+ createDefaultGqlComponent();
+ await waitForPromises();
+
+ triggerApprovalUpdated();
+ await waitForPromises();
+
+ expect(finderFn()).toBe(updatedValue);
+ });
+
+ it('should not update if user has touched', async () => {
+ createDefaultGqlComponent();
+ await waitForPromises();
+
+ const input = wrapper.find(inputId);
+ input.element.value = USER_COMMIT_MESSAGE;
+ input.trigger('input');
+
+ triggerApprovalUpdated();
+ await waitForPromises();
+
+ expect(finderFn()).toBe(USER_COMMIT_MESSAGE);
+ });
+ });
+ });
});
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index e5c48c0c9c4..a7e657f2636 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -318,13 +318,14 @@ RSpec.describe MarkupHelper do
let(:wiki) { build(:wiki, container: project) }
let(:content) { 'wiki content' }
let(:slug) { 'nested/page' }
- let(:wiki_page) { double('WikiPage', path: "file.#{extension}", content: content, slug: slug, wiki: wiki) }
+ let(:path) { "file.#{extension}" }
+ let(:wiki_page) { double('WikiPage', path: path, content: content, slug: slug, wiki: wiki) }
let(:context) do
{
pipeline: :wiki, project: project, wiki: wiki,
page_slug: slug, issuable_reference_expansion_enabled: true,
- repository: wiki.repository
+ repository: wiki.repository, requested_path: path
}
end
diff --git a/spec/lib/api/entities/wiki_page_spec.rb b/spec/lib/api/entities/wiki_page_spec.rb
index de36ebc3a76..238c8233a14 100644
--- a/spec/lib/api/entities/wiki_page_spec.rb
+++ b/spec/lib/api/entities/wiki_page_spec.rb
@@ -29,6 +29,20 @@ RSpec.describe API::Entities::WikiPage do
it 'returns the wiki page content rendered' do
expect(subject[:content]).to eq "<p data-sourcepos=\"1:1-1:#{wiki_page.content.size}\" dir=\"auto\">#{wiki_page.content}</p>"
end
+
+ it 'includes the wiki page version in the render context' do
+ expect(entity).to receive(:render_wiki_content).with(anything, hash_including(ref: wiki_page.version.id)).and_call_original
+
+ subject[:content]
+ end
+
+ context 'when page is an Ascii document' do
+ let(:wiki_page) { create(:wiki_page, content: "*Test* _content_", format: :asciidoc) }
+
+ it 'renders the page without errors' do
+ expect(subject[:content]).to eq("<div>&#x000A;<p><strong>Test</strong> <em>content</em></p>&#x000A;</div>")
+ end
+ end
end
context 'when it is false' do
diff --git a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
index c5127082e2c..8c663ff9f8a 100644
--- a/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_job_spec.rb
@@ -7,6 +7,8 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
it { is_expected.to be_a Gitlab::Database::SharedModel }
+ it { expect(described_class::TIMEOUT_EXCEPTIONS).to match_array [ActiveRecord::StatementTimeout, ActiveRecord::ConnectionTimeoutError, ActiveRecord::AdapterTimeout, ActiveRecord::LockWaitTimeout] }
+
describe 'associations' do
it { is_expected.to belong_to(:batched_migration).with_foreign_key(:batched_background_migration_id) }
it { is_expected.to have_many(:batched_job_transition_logs).with_foreign_key(:batched_background_migration_job_id) }
@@ -15,6 +17,8 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
describe 'state machine' do
let_it_be(:job) { create(:batched_background_migration_job, :failed) }
+ it { expect(described_class.state_machine.states.map(&:name)).to eql(%i(pending running failed succeeded)) }
+
context 'when a job is running' do
it 'logs the transition' do
expect(Gitlab::AppLogger).to receive(:info).with( { batched_job_id: job.id, message: 'BatchedJob transition', new_state: :running, previous_state: :failed } )
@@ -47,6 +51,51 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
end
end
+ context 'when a job fails the number of max times' do
+ let(:max_times) { described_class::MAX_ATTEMPTS }
+ let!(:job) { create(:batched_background_migration_job, :running, batch_size: 10, min_value: 6, max_value: 15, attempts: max_times) }
+
+ context 'when job can be split' do
+ let(:exception) { ActiveRecord::StatementTimeout.new('Timeout!') }
+
+ before do
+ allow_next_instance_of(Gitlab::BackgroundMigration::BatchingStrategies::PrimaryKeyBatchingStrategy) do |batch_class|
+ allow(batch_class).to receive(:next_batch).and_return([6, 10])
+ end
+ end
+
+ it 'splits the job into two retriable jobs' do
+ expect { job.failure!(error: exception) }.to change { job.batched_migration.batched_jobs.retriable.count }.from(0).to(2)
+ end
+ end
+
+ context 'when the job cannot be split' do
+ let(:exception) { ActiveRecord::StatementTimeout.new('Timeout!') }
+ let(:max_times) { described_class::MAX_ATTEMPTS }
+ let!(:job) { create(:batched_background_migration_job, :running, batch_size: 50, sub_batch_size: 20, min_value: 6, max_value: 15, attempts: max_times) }
+ let(:error_message) { 'Job cannot be split further' }
+ let(:split_and_retry_exception) { Gitlab::Database::BackgroundMigration::SplitAndRetryError.new(error_message) }
+
+ before do
+ allow(job).to receive(:split_and_retry!).and_raise(split_and_retry_exception)
+ end
+
+ it 'does not split the job' do
+ expect { job.failure!(error: exception) }.not_to change { job.batched_migration.batched_jobs.retriable.count }
+ end
+
+ it 'keeps the same job attributes' do
+ expect { job.failure!(error: exception) }.not_to change { job }
+ end
+
+ it 'logs the error' do
+ expect(Gitlab::AppLogger).to receive(:error).with( { message: error_message, batched_job_id: job.id } )
+
+ job.failure!(error: exception)
+ end
+ end
+ end
+
context 'when a job fails' do
let(:job) { create(:batched_background_migration_job, :running) }
@@ -147,6 +196,49 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedJob, type: :model d
end
end
+ describe '#can_split?' do
+ subject { job.can_split?(exception) }
+
+ context 'when the number of attempts is greater than the limit and the batch_size is greater than the sub_batch_size' do
+ let(:job) { create(:batched_background_migration_job, :failed, batch_size: 4, sub_batch_size: 2, attempts: described_class::MAX_ATTEMPTS + 1) }
+
+ context 'when is a timeout exception' do
+ let(:exception) { ActiveRecord::StatementTimeout.new }
+
+ it { expect(subject).to be_truthy }
+ end
+
+ context 'when is not a timeout exception' do
+ let(:exception) { RuntimeError.new }
+
+ it { expect(subject).to be_falsey }
+ end
+ end
+
+ context 'when the number of attempts is lower than the limit and the batch_size is greater than the sub_batch_size' do
+ let(:job) { create(:batched_background_migration_job, :failed, batch_size: 4, sub_batch_size: 2, attempts: described_class::MAX_ATTEMPTS - 1) }
+
+ context 'when is a timeout exception' do
+ let(:exception) { ActiveRecord::StatementTimeout.new }
+
+ it { expect(subject).to be_falsey }
+ end
+
+ context 'when is not a timeout exception' do
+ let(:exception) { RuntimeError.new }
+
+ it { expect(subject).to be_falsey }
+ end
+ end
+
+ context 'when the batch_size is lower than the sub_batch_size' do
+ let(:job) { create(:batched_background_migration_job, :failed, batch_size: 2, sub_batch_size: 4) }
+ let(:exception) { ActiveRecord::StatementTimeout.new }
+
+ it { expect(subject).to be_falsey }
+ end
+ end
+
describe '#time_efficiency' do
subject { job.time_efficiency }
diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
index 4f5536d8771..d6c984c7adb 100644
--- a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb
@@ -193,6 +193,7 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, '
it_behaves_like 'an error is raised', RuntimeError.new('Something broke!')
it_behaves_like 'an error is raised', SignalException.new('SIGTERM')
+ it_behaves_like 'an error is raised', ActiveRecord::StatementTimeout.new('Timeout!')
end
context 'when the batched background migration does not inherit from BaseJob' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index c41f62e68d2..b16a76211eb 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1172,8 +1172,8 @@ RSpec.describe User do
@user.update!(email: 'new_primary@example.com')
@user.reload
- expect(@user.emails.count).to eq 2
- expect(@user.emails.pluck(:email)).to match_array([@secondary.email, 'primary@example.com'])
+ expect(@user.emails.count).to eq 3
+ expect(@user.emails.pluck(:email)).to match_array([@secondary.email, 'primary@example.com', 'new_primary@example.com'])
end
context 'when the first email was unconfirmed and the second email gets confirmed' do
@@ -1594,6 +1594,66 @@ RSpec.describe User do
end
end
+ describe 'saving primary email to the emails table' do
+ context 'when calling skip_reconfirmation! while updating the primary email' do
+ let(:user) { create(:user, email: 'primary@example.com') }
+
+ it 'adds the new email to emails' do
+ user.skip_reconfirmation!
+ user.update!(email: 'new_primary@example.com')
+
+ expect(user.email).to eq('new_primary@example.com')
+ expect(user.unconfirmed_email).to be_nil
+ expect(user).to be_confirmed
+ expect(user.emails.pluck(:email)).to include('new_primary@example.com')
+ expect(user.emails.find_by(email: 'new_primary@example.com')).to be_confirmed
+ end
+ end
+
+ context 'when the email is changed but not confirmed' do
+ let(:user) { create(:user, email: 'primary@example.com') }
+
+ it 'does not add the new email to emails yet' do
+ user.update!(email: 'new_primary@example.com')
+
+ expect(user.unconfirmed_email).to eq('new_primary@example.com')
+ expect(user.email).to eq('primary@example.com')
+ expect(user).to be_confirmed
+ expect(user.emails.pluck(:email)).not_to include('new_primary@example.com')
+ end
+ end
+
+ context 'when the user is created as not confirmed' do
+ let(:user) { create(:user, :unconfirmed, email: 'primary@example.com') }
+
+ it 'does not add the email to emails yet' do
+ expect(user).not_to be_confirmed
+ expect(user.emails.pluck(:email)).not_to include('primary@example.com')
+ end
+ end
+
+ context 'when the user is created as confirmed' do
+ let(:user) { create(:user, email: 'primary@example.com', confirmed_at: DateTime.now.utc) }
+
+ it 'adds the email to emails' do
+ expect(user).to be_confirmed
+ expect(user.emails.pluck(:email)).to include('primary@example.com')
+ end
+ end
+
+ context 'when skip_confirmation! is called' do
+ let(:user) { build(:user, :unconfirmed, email: 'primary@example.com') }
+
+ it 'adds the email to emails' do
+ user.skip_confirmation!
+ user.save!
+
+ expect(user).to be_confirmed
+ expect(user.emails.pluck(:email)).to include('primary@example.com')
+ end
+ end
+ end
+
describe '#force_confirm' do
let(:expired_confirmation_sent_at) { Date.today - described_class.confirm_within - 7.days }
let(:extant_confirmation_sent_at) { Date.today }
diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb
index 45b7a2090cd..06ae61ca5eb 100644
--- a/spec/requests/api/wikis_spec.rb
+++ b/spec/requests/api/wikis_spec.rb
@@ -174,8 +174,6 @@ RSpec.describe API::Wikis do
context 'when user is developer' do
before do
project.add_developer(user)
-
- request
end
include_examples 'wikis API returns wiki page'
@@ -183,6 +181,10 @@ RSpec.describe API::Wikis do
context 'when page is not existing' do
let(:url) { "/projects/#{project.id}/wikis/unknown" }
+ before do
+ request
+ end
+
include_examples 'wiki API 404 Wiki Page Not Found'
end
end
@@ -190,8 +192,6 @@ RSpec.describe API::Wikis do
context 'when user is maintainer' do
before do
project.add_maintainer(user)
-
- request
end
include_examples 'wikis API returns wiki page'
@@ -199,6 +199,10 @@ RSpec.describe API::Wikis do
context 'when page is not existing' do
let(:url) { "/projects/#{project.id}/wikis/unknown" }
+ before do
+ request
+ end
+
include_examples 'wiki API 404 Wiki Page Not Found'
end
end
@@ -220,8 +224,6 @@ RSpec.describe API::Wikis do
context 'when user is developer' do
before do
project.add_developer(user)
-
- request
end
include_examples 'wikis API returns wiki page'
@@ -229,6 +231,10 @@ RSpec.describe API::Wikis do
context 'when page is not existing' do
let(:url) { "/projects/#{project.id}/wikis/unknown" }
+ before do
+ request
+ end
+
include_examples 'wiki API 404 Wiki Page Not Found'
end
end
@@ -236,8 +242,6 @@ RSpec.describe API::Wikis do
context 'when user is maintainer' do
before do
project.add_maintainer(user)
-
- request
end
include_examples 'wikis API returns wiki page'
@@ -245,6 +249,10 @@ RSpec.describe API::Wikis do
context 'when page is not existing' do
let(:url) { "/projects/#{project.id}/wikis/unknown" }
+ before do
+ request
+ end
+
include_examples 'wiki API 404 Wiki Page Not Found'
end
end
diff --git a/spec/support/shared_examples/features/container_registry_shared_examples.rb b/spec/support/shared_examples/features/container_registry_shared_examples.rb
index 06b2b8c621c..6aa7e6e6270 100644
--- a/spec/support/shared_examples/features/container_registry_shared_examples.rb
+++ b/spec/support/shared_examples/features/container_registry_shared_examples.rb
@@ -7,3 +7,20 @@ RSpec.shared_examples 'handling feature network errors with the container regist
expect(page).to have_content 'We are having trouble connecting to the Container Registry'
end
end
+
+RSpec.shared_examples 'rejecting tags destruction for an importing repository on' do |tags: []|
+ it 'rejects the tag destruction operation' do
+ service = instance_double('Projects::ContainerRepository::DeleteTagsService')
+ expect(service).to receive(:execute).with(container_repository) { { status: :error, message: 'repository importing' } }
+ expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(container_repository.project, user, tags: tags) { service }
+
+ first('[data-testid="additional-actions"]').click
+ first('[data-testid="single-delete-button"]').click
+ expect(find('.modal .modal-title')).to have_content _('Remove tag')
+ find('.modal .modal-footer .btn-danger').click
+
+ alert_body = find('.gl-alert-body')
+ expect(alert_body).to have_content('Tags temporarily cannot be marked for deletion. Please try again in a few minutes.')
+ expect(alert_body).to have_link('More details', href: help_page_path('user/packages/container_registry/index', anchor: 'tags-temporarily-cannot-be-marked-for-deletion'))
+ end
+end
diff --git a/spec/support/shared_examples/lib/wikis_api_examples.rb b/spec/support/shared_examples/lib/wikis_api_examples.rb
index 697bfaca558..f068a7676ad 100644
--- a/spec/support/shared_examples/lib/wikis_api_examples.rb
+++ b/spec/support/shared_examples/lib/wikis_api_examples.rb
@@ -44,7 +44,13 @@ RSpec.shared_examples_for 'wikis API returns list of wiki pages' do
end
RSpec.shared_examples_for 'wikis API returns wiki page' do
+ subject(:request) { get api(url, user), params: params }
+
shared_examples 'returns wiki page' do
+ before do
+ request
+ end
+
specify do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(5)
@@ -71,6 +77,38 @@ RSpec.shared_examples_for 'wikis API returns wiki page' do
it_behaves_like 'returns wiki page'
end
+
+ context 'when wiki page has versions' do
+ let(:new_content) { 'New content' }
+
+ before do
+ wiki.update_page(page.page, content: new_content, message: 'updated page')
+
+ expect(page.count_versions).to eq(2)
+
+ request
+ end
+
+ context 'when version param is not present' do
+ it 'retrieves the last version' do
+ expect(json_response['content']).to eq(new_content)
+ end
+ end
+
+ context 'when version param is set' do
+ let(:params) { { version: page.version.id } }
+
+ it 'retrieves the specific page version' do
+ expect(json_response['content']).to eq(page.content)
+ end
+
+ context 'when version param is not valid or inexistent' do
+ let(:params) { { version: 'foobar' } }
+
+ it_behaves_like 'wiki API 404 Wiki Page Not Found'
+ end
+ end
+ end
end
RSpec.shared_examples_for 'wikis API creates wiki page' do
diff --git a/spec/workers/quality/test_data_cleanup_worker_spec.rb b/spec/workers/quality/test_data_cleanup_worker_spec.rb
new file mode 100644
index 00000000000..a17e6e0cb1a
--- /dev/null
+++ b/spec/workers/quality/test_data_cleanup_worker_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Quality::TestDataCleanupWorker do
+ subject { described_class.new }
+
+ shared_examples 'successful deletion' do
+ before do
+ allow(Gitlab).to receive(:staging?).and_return(true)
+ end
+
+ it 'removes test groups' do
+ expect { subject.perform }.to change(Group, :count).by(-test_group_count)
+ end
+ end
+
+ describe "#perform" do
+ context 'with multiple test groups to remove' do
+ let(:test_group_count) { 5 }
+ let!(:groups_to_remove) { create_list(:group, test_group_count, :test_group) }
+ let!(:group_to_keep) { create(:group, path: 'test-group-fulfillment-keep', created_at: 1.day.ago) }
+ let!(:non_test_group) { create(:group) }
+ let(:non_test_owner_group) { create(:group, path: 'test-group-fulfillment1234', created_at: 4.days.ago) }
+
+ before do
+ non_test_owner_group.add_owner(create(:user))
+ end
+
+ it_behaves_like 'successful deletion'
+ end
+
+ context 'with paid groups' do
+ let(:test_group_count) { 1 }
+ let!(:paid_group) { create(:group, :test_group) }
+
+ before do
+ allow(paid_group).to receive(:paid?).and_return(true)
+ end
+
+ it_behaves_like 'successful deletion'
+ end
+ end
+end