diff --git a/app/assets/javascripts/blob/components/blob_header_default_actions.vue b/app/assets/javascripts/blob/components/blob_header_default_actions.vue
index daade611651..6eddec31166 100644
--- a/app/assets/javascripts/blob/components/blob_header_default_actions.vue
+++ b/app/assets/javascripts/blob/components/blob_header_default_actions.vue
@@ -32,6 +32,7 @@ export default {
default: false,
},
},
+ inject: ['blobHash'],
computed: {
downloadUrl() {
return `${this.rawPath}?inline=false`;
@@ -39,6 +40,9 @@ export default {
copyDisabled() {
return this.activeViewer === RICH_BLOB_VIEWER;
},
+ getBlobHashTarget() {
+ return `[data-blob-hash="${this.blobHash}"]`;
+ },
},
BTN_COPY_CONTENTS_TITLE,
BTN_DOWNLOAD_TITLE,
@@ -53,7 +57,7 @@ export default {
:aria-label="$options.BTN_COPY_CONTENTS_TITLE"
:title="$options.BTN_COPY_CONTENTS_TITLE"
:disabled="copyDisabled"
- data-clipboard-target="#blob-code-content"
+ :data-clipboard-target="getBlobHashTarget"
data-testid="copyContentsButton"
icon="copy-to-clipboard"
category="primary"
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
index 19e6f8a2269..6935ead2706 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
@@ -18,7 +18,7 @@ export default {
};
},
computed: {
- ...mapGetters({ issue: 'activeIssue' }),
+ ...mapGetters({ issue: 'activeIssue', projectPathForActiveIssue: 'projectPathForActiveIssue' }),
hasDueDate() {
return this.issue.dueDate != null;
},
@@ -36,10 +36,6 @@ export default {
return dateInWords(this.parsedDueDate, true);
},
- projectPath() {
- const referencePath = this.issue.referencePath || '';
- return referencePath.slice(0, referencePath.indexOf('#'));
- },
},
methods: {
...mapActions(['setActiveIssueDueDate']),
@@ -53,7 +49,7 @@ export default {
try {
const dueDate = date ? formatDate(date, 'yyyy-mm-dd') : null;
- await this.setActiveIssueDueDate({ dueDate, projectPath: this.projectPath });
+ await this.setActiveIssueDueDate({ dueDate, projectPath: this.projectPathForActiveIssue });
} catch (e) {
createFlash({ message: this.$options.i18n.updateDueDateError });
} finally {
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
index 31094939733..9d537a4ef2c 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue
@@ -21,9 +21,9 @@ export default {
},
inject: ['labelsFetchPath', 'labelsManagePath', 'labelsFilterBasePath'],
computed: {
- ...mapGetters({ issue: 'activeIssue' }),
+ ...mapGetters(['activeIssue', 'projectPathForActiveIssue']),
selectedLabels() {
- const { labels = [] } = this.issue;
+ const { labels = [] } = this.activeIssue;
return labels.map(label => ({
...label,
@@ -31,17 +31,13 @@ export default {
}));
},
issueLabels() {
- const { labels = [] } = this.issue;
+ const { labels = [] } = this.activeIssue;
return labels.map(label => ({
...label,
scoped: isScopedLabel(label),
}));
},
- projectPath() {
- const { referencePath = '' } = this.issue;
- return referencePath.slice(0, referencePath.indexOf('#'));
- },
},
methods: {
...mapActions(['setActiveIssueLabels']),
@@ -55,7 +51,7 @@ export default {
.filter(label => !payload.find(selected => selected.id === label.id))
.map(label => label.id);
- const input = { addLabelIds, removeLabelIds, projectPath: this.projectPath };
+ const input = { addLabelIds, removeLabelIds, projectPath: this.projectPathForActiveIssue };
await this.setActiveIssueLabels(input);
} catch (e) {
createFlash({ message: __('An error occurred while updating labels.') });
@@ -68,7 +64,7 @@ export default {
try {
const removeLabelIds = [getIdFromGraphQLId(id)];
- const input = { removeLabelIds, projectPath: this.projectPath };
+ const input = { removeLabelIds, projectPath: this.projectPathForActiveIssue };
await this.setActiveIssueLabels(input);
} catch (e) {
createFlash({ message: __('An error occurred when removing the label.') });
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue
new file mode 100644
index 00000000000..ed069cea630
--- /dev/null
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_subscription.vue
@@ -0,0 +1,71 @@
+
+
+
+
+ {{ notificationText }}
+
+
+
diff --git a/app/assets/javascripts/boards/graphql/mutations/issue_set_subscription.mutation.graphql b/app/assets/javascripts/boards/graphql/mutations/issue_set_subscription.mutation.graphql
new file mode 100644
index 00000000000..1f383245ac2
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/mutations/issue_set_subscription.mutation.graphql
@@ -0,0 +1,8 @@
+mutation issueSetSubscription($input: IssueSetSubscriptionInput!) {
+ issueSetSubscription(input: $input) {
+ issue {
+ subscribed
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 2552a3a4113..dd950a45076 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -24,6 +24,7 @@ import destroyBoardListMutation from '../queries/board_list_destroy.mutation.gra
import issueCreateMutation from '../queries/issue_create.mutation.graphql';
import issueSetLabels from '../queries/issue_set_labels.mutation.graphql';
import issueSetDueDate from '../queries/issue_set_due_date.mutation.graphql';
+import issueSetSubscriptionMutation from '../graphql/mutations/issue_set_subscription.mutation.graphql';
const notImplemented = () => {
/* eslint-disable-next-line @gitlab/require-i18n-strings */
@@ -423,6 +424,29 @@ export default {
});
},
+ setActiveIssueSubscribed: async ({ commit, getters }, input) => {
+ const { data } = await gqlClient.mutate({
+ mutation: issueSetSubscriptionMutation,
+ variables: {
+ input: {
+ iid: String(getters.activeIssue.iid),
+ projectPath: input.projectPath,
+ subscribedState: input.subscribed,
+ },
+ },
+ });
+
+ if (data.issueSetSubscription?.errors?.length > 0) {
+ throw new Error(data.issueSetSubscription.errors);
+ }
+
+ commit(types.UPDATE_ISSUE_BY_ID, {
+ issueId: getters.activeIssue.id,
+ prop: 'subscribed',
+ value: data.issueSetSubscription.issue.subscribed,
+ });
+ },
+
fetchBacklog: () => {
notImplemented();
},
diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js
index f717b4101ab..cd28b4a0ff7 100644
--- a/app/assets/javascripts/boards/stores/getters.js
+++ b/app/assets/javascripts/boards/stores/getters.js
@@ -24,6 +24,11 @@ export default {
return state.issues[state.activeId] || {};
},
+ projectPathForActiveIssue: (_, getters) => {
+ const referencePath = getters.activeIssue.referencePath || '';
+ return referencePath.slice(0, referencePath.indexOf('#'));
+ },
+
getListByLabelId: state => labelId => {
return find(state.boardLists, l => l.label?.id === labelId);
},
diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js
index ef4d5338046..8c5f45e9d34 100644
--- a/app/assets/javascripts/persistent_user_callouts.js
+++ b/app/assets/javascripts/persistent_user_callouts.js
@@ -6,6 +6,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-admin-licensed-user-count-threshold',
'.js-buy-pipeline-minutes-notification-callout',
'.js-token-expiry-callout',
+ '.js-registration-enabled-callout',
];
const initCallouts = () => {
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_view.vue b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
index 8e817fdbe31..b965c15306d 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
@@ -51,6 +51,13 @@ export default {
required: true,
},
},
+ provide() {
+ return {
+ blobHash: Math.random()
+ .toString()
+ .split('.')[1],
+ };
+ },
data() {
return {
blobContent: '',
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
index bbe72a2b122..646e1703f1e 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
@@ -9,6 +9,7 @@ export default {
GlIcon,
},
mixins: [ViewerMixin],
+ inject: ['blobHash'],
data() {
return {
highlightedLine: null,
@@ -64,7 +65,7 @@ export default {
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index f566f145aed..512649b3008 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -394,6 +394,10 @@ module ApplicationSettingsHelper
def show_documentation_base_url_field?
Feature.enabled?(:help_page_documentation_redirect)
end
+
+ def signup_enabled?
+ !!Gitlab::CurrentSettings.signup_enabled
+ end
end
ApplicationSettingsHelper.prepend_if_ee('EE::ApplicationSettingsHelper')
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index 542a9ad2a70..61fcda6a504 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -67,6 +67,7 @@ module NotificationsHelper
when :custom
_('You will only receive notifications for the events you choose')
when :owner_disabled
+ # Any change must be reflected in board_sidebar_subscription.vue
_('Notifications have been disabled by the project or group owner')
end
end
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index 0cdf53d6174..e93c1b82cd7 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -10,6 +10,7 @@ module UserCalloutsHelper
WEBHOOKS_MOVED = 'webhooks_moved'
CUSTOMIZE_HOMEPAGE = 'customize_homepage'
FEATURE_FLAGS_NEW_VERSION = 'feature_flags_new_version'
+ REGISTRATION_ENABLED_CALLOUT = 'registration_enabled_callout'
def show_admin_integrations_moved?
!user_dismissed?(ADMIN_INTEGRATIONS_MOVED)
@@ -55,6 +56,10 @@ module UserCalloutsHelper
!user_dismissed?(FEATURE_FLAGS_NEW_VERSION)
end
+ def show_registration_enabled_user_callout?
+ current_user&.admin? && signup_enabled? && !user_dismissed?(REGISTRATION_ENABLED_CALLOUT)
+ end
+
private
def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil)
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 1fb8e74269d..ceefb6a8b8a 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -105,7 +105,7 @@ module Ci
raise ArgumentError, 'Offset is out of range' if offset < 0 || offset > size
raise ArgumentError, 'Chunk size overflow' if CHUNK_SIZE < (offset + new_data.bytesize)
- in_lock(*lock_params) { unsafe_append_data!(new_data, offset) }
+ in_lock(lock_key, **lock_params) { unsafe_append_data!(new_data, offset) }
schedule_to_persist! if full?
end
@@ -151,7 +151,7 @@ module Ci
# acquired
#
def persist_data!
- in_lock(*lock_params) do # exclusive Redis lock is acquired first
+ in_lock(lock_key, **lock_params) do # exclusive Redis lock is acquired first
raise FailedToPersistDataError, 'Modifed build trace chunk detected' if has_changes_to_save?
self.reset.then do |chunk| # we ensure having latest lock_version
@@ -289,11 +289,16 @@ module Ci
build.trace_chunks.maximum(:chunk_index).to_i
end
+ def lock_key
+ "trace_write:#{build_id}:chunks:#{chunk_index}"
+ end
+
def lock_params
- ["trace_write:#{build_id}:chunks:#{chunk_index}",
- { ttl: WRITE_LOCK_TTL,
- retries: WRITE_LOCK_RETRY,
- sleep_sec: WRITE_LOCK_SLEEP }]
+ {
+ ttl: WRITE_LOCK_TTL,
+ retries: WRITE_LOCK_RETRY,
+ sleep_sec: WRITE_LOCK_SLEEP
+ }
end
def metrics
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index e39ff8712fc..cfad58fc0db 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -25,7 +25,8 @@ class UserCallout < ApplicationRecord
personal_access_token_expiry: 21, # EE-only
suggest_pipeline: 22,
customize_homepage: 23,
- feature_flags_new_version: 24
+ feature_flags_new_version: 24,
+ registration_enabled_callout: 25
}
validates :user, presence: true
diff --git a/app/services/merge_requests/cleanup_refs_service.rb b/app/services/merge_requests/cleanup_refs_service.rb
index 92d5d96b7f7..23ac8e393f4 100644
--- a/app/services/merge_requests/cleanup_refs_service.rb
+++ b/app/services/merge_requests/cleanup_refs_service.rb
@@ -31,7 +31,7 @@ module MergeRequests
return error('Failed to create keep around refs.') unless kept_around?
return error('Failed to cache merge ref sha.') unless cache_merge_ref_sha
- delete_refs
+ delete_refs if repository.exists?
return error('Failed to update schedule.') unless update_schedule
diff --git a/app/services/merge_requests/reopen_service.rb b/app/services/merge_requests/reopen_service.rb
index 1d4a6249952..bcedbc61c65 100644
--- a/app/services/merge_requests/reopen_service.rb
+++ b/app/services/merge_requests/reopen_service.rb
@@ -16,6 +16,7 @@ module MergeRequests
merge_request.update_project_counter_caches
merge_request.cache_merge_request_closes_issues!(current_user)
merge_request.cleanup_schedule&.destroy
+ merge_request.update_column(:merge_ref_sha, nil)
end
merge_request
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 1d08f59838d..aae1d5b6a4e 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -14,7 +14,7 @@
- if @project.last_repository_check_failed?
.row
.col-md-12
- .gl-alert.gl-alert-danger.gl-mb-5
+ .gl-alert.gl-alert-danger.gl-mb-5{ data: { testid: 'last-repository-check-failed-alert' } }
= sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
.gl-alert-body
- last_check_message = _("Last repository check (%{last_check_timestamp}) failed. See the 'repocheck.log' file for error messages.")
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 700255eaffd..f6fc49393d8 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -10,6 +10,7 @@
= render_if_exists "layouts/header/token_expiry_notification"
= render "layouts/broadcast"
= render "layouts/header/read_only_banner"
+ = render "layouts/header/registration_enabled_callout"
= render "layouts/nav/classification_level_banner"
= yield :flash_message
= render "shared/ping_consent"
diff --git a/app/views/layouts/header/_registration_enabled_callout.html.haml b/app/views/layouts/header/_registration_enabled_callout.html.haml
new file mode 100644
index 00000000000..1b1804edcc7
--- /dev/null
+++ b/app/views/layouts/header/_registration_enabled_callout.html.haml
@@ -0,0 +1,15 @@
+- return unless show_registration_enabled_user_callout?
+
+%div{ class: [container_class, @content_class, 'gl-pt-5!'] }
+ .gl-alert.gl-alert-warning.js-registration-enabled-callout{ role: 'alert', data: { feature_id: UserCalloutsHelper::REGISTRATION_ENABLED_CALLOUT, dismiss_endpoint: user_callouts_path } }
+ = sprite_icon('warning', size: 16, css_class: 'gl-alert-icon')
+ %button.gl-alert-dismiss.js-close{ type: 'button', aria: { label: _('Close') }, data: { testid: 'close-registration-enabled-callout' } }
+ = sprite_icon('close', size: 16)
+ .gl-alert-title
+ = _('Open registration is enabled on your instance.')
+ .gl-alert-body
+ = html_escape(_('%{anchorOpen}Learn more%{anchorClose} about how you can customize / disable registration on your instance.')) % { anchorOpen: "".html_safe, anchorClose: ''.html_safe }
+ .gl-alert-actions
+ = link_to general_admin_application_settings_path(anchor: 'js-signup-settings'), class: 'btn gl-alert-action btn-info btn-md gl-button' do
+ %span.gl-button-text
+ = _('View setting')
diff --git a/changelogs/unreleased/237905-add-signup-enabled-alert.yml b/changelogs/unreleased/237905-add-signup-enabled-alert.yml
new file mode 100644
index 00000000000..0e76ad97930
--- /dev/null
+++ b/changelogs/unreleased/237905-add-signup-enabled-alert.yml
@@ -0,0 +1,5 @@
+---
+title: Add user callout to alert admins that registration is open by default
+merge_request: 47425
+author:
+type: added
diff --git a/changelogs/unreleased/245263-fix-cleanup-service-no-repository.yml b/changelogs/unreleased/245263-fix-cleanup-service-no-repository.yml
new file mode 100644
index 00000000000..7bb7f8366f9
--- /dev/null
+++ b/changelogs/unreleased/245263-fix-cleanup-service-no-repository.yml
@@ -0,0 +1,5 @@
+---
+title: Do not fail when cleaning up MR with no repository
+merge_request: 47744
+author:
+type: fixed
diff --git a/changelogs/unreleased/dmishunov-276913-snippet-copy-content.yml b/changelogs/unreleased/dmishunov-276913-snippet-copy-content.yml
new file mode 100644
index 00000000000..78b586942bb
--- /dev/null
+++ b/changelogs/unreleased/dmishunov-276913-snippet-copy-content.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed copy contents functionality for snippets
+merge_request: 47646
+author:
+type: fixed
diff --git a/changelogs/unreleased/pb-clear-merge-ref-sha-reopen.yml b/changelogs/unreleased/pb-clear-merge-ref-sha-reopen.yml
new file mode 100644
index 00000000000..78b1e723294
--- /dev/null
+++ b/changelogs/unreleased/pb-clear-merge-ref-sha-reopen.yml
@@ -0,0 +1,5 @@
+---
+title: Clear cached merge_ref_sha on reopen
+merge_request: 47747
+author:
+type: fixed
diff --git a/doc/development/database_review.md b/doc/development/database_review.md
index f5c03b9e8e6..d1ec32af464 100644
--- a/doc/development/database_review.md
+++ b/doc/development/database_review.md
@@ -106,7 +106,8 @@ test its execution using `CREATE INDEX CONCURRENTLY` in the `#database-lab` Slac
- Write the raw SQL in the MR description. Preferably formatted
nicely with [pgFormatter](https://sqlformat.darold.net) or
- [paste.depesz.com](https://paste.depesz.com).
+ [paste.depesz.com](https://paste.depesz.com) and using regular quotes
+ (e.g. `"projects"."id"`) and avoiding smart quotes (e.g. `“projects”.“id”`).
- Include the output of `EXPLAIN (ANALYZE, BUFFERS)` of the relevant
queries in the description. If the output is too long, wrap it in
`` blocks, paste it in a GitLab Snippet, or provide the
diff --git a/doc/development/integrations/jira_connect.md b/doc/development/integrations/jira_connect.md
index 983e97082d9..66a93f8c947 100644
--- a/doc/development/integrations/jira_connect.md
+++ b/doc/development/integrations/jira_connect.md
@@ -4,34 +4,34 @@ group: Ecosystem
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/#designated-technical-writers
---
-# Setting up a development environment
+# Set up a development environment
The following are required to install and test the app:
-1. A Jira Cloud instance
+- A Jira Cloud instance. Atlassian provides [free instances for development and testing](https://developer.atlassian.com/platform/marketplace/getting-started/#free-developer-instances-to-build-and-test-your-app).
+- A GitLab instance available over the internet. For the app to work, Jira Cloud should
+ be able to connect to the GitLab instance through the internet. To easily expose your
+ local development environment, you can use tools like:
+ - [serveo](https://medium.com/automationmaster/how-to-forward-my-local-port-to-public-using-serveo-4979f352a3bf)
+ - [ngrok](https://ngrok.com).
- Atlassian provides free instances for development and testing. [Click here to sign up](https://developer.atlassian.com/platform/marketplace/getting-started/#free-developer-instances-to-build-and-test-your-app).
+ These also take care of SSL for you because Jira requires all connections to the app
+ host to be over SSL.
-1. A GitLab instance available over the internet
+## Install the app in Jira
- For the app to work, Jira Cloud should be able to connect to the GitLab instance through the internet.
+To install the app in Jira:
- To easily expose your local development environment, you can use tools like
- [serveo](https://medium.com/automationmaster/how-to-forward-my-local-port-to-public-using-serveo-4979f352a3bf)
- or [ngrok](https://ngrok.com). These also take care of SSL for you because Jira
- requires all connections to the app host to be over SSL.
+1. Enable Jira development mode to install apps that are not from the Atlassian
+ Marketplace:
-## Installing the app in Jira
-
-1. Enable Jira development mode to install apps that are not from the Atlassian Marketplace
-
- 1. Navigate to **Jira settings** (cog icon) > **Apps** > **Manage apps**.
+ 1. In Jira, navigate to **Jira settings > Apps > Manage apps**.
1. Scroll to the bottom of the **Manage apps** page and click **Settings**.
1. Select **Enable development mode** and click **Apply**.
-1. Install the app
+1. Install the app:
- 1. Navigate to Jira, then choose **Jira settings** (cog icon) > **Apps** > **Manage apps**.
+ 1. In Jira, navigate to **Jira settings > Apps > Manage apps**.
1. Click **Upload app**.
1. In the **From this URL** field, provide a link to the app descriptor. The host and port must point to your GitLab instance.
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 4adca92fb46..e1c37c9057b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -356,6 +356,9 @@ msgstr ""
msgid "%{address} is an invalid IP address range"
msgstr ""
+msgid "%{anchorOpen}Learn more%{anchorClose} about how you can customize / disable registration on your instance."
+msgstr ""
+
msgid "%{author_link} wrote:"
msgstr ""
@@ -8410,9 +8413,6 @@ msgstr ""
msgid "DastProfiles|Copy HTTP header to clipboard"
msgstr ""
-msgid "DastProfiles|Could not create site validation token. Please refresh the page, or try again later."
-msgstr ""
-
msgid "DastProfiles|Could not create the scanner profile. Please try again."
msgstr ""
@@ -8437,9 +8437,6 @@ msgstr ""
msgid "DastProfiles|Could not fetch site profiles. Please refresh the page, or try again later."
msgstr ""
-msgid "DastProfiles|Could not retrieve site validation status. Please refresh the page, or try again later."
-msgstr ""
-
msgid "DastProfiles|Could not update the scanner profile. Please try again."
msgstr ""
@@ -8554,9 +8551,6 @@ msgstr ""
msgid "DastProfiles|Site is not validated yet, please follow the steps."
msgstr ""
-msgid "DastProfiles|Site must be validated to run an active scan."
-msgstr ""
-
msgid "DastProfiles|Spider timeout"
msgstr ""
@@ -8602,27 +8596,12 @@ msgstr ""
msgid "DastProfiles|Validate"
msgstr ""
-msgid "DastProfiles|Validate target site"
-msgstr ""
-
msgid "DastProfiles|Validating..."
msgstr ""
msgid "DastProfiles|Validation failed, please make sure that you follow the steps above with the chosen method."
msgstr ""
-msgid "DastProfiles|Validation failed. Please try again."
-msgstr ""
-
-msgid "DastProfiles|Validation is in progress..."
-msgstr ""
-
-msgid "DastProfiles|Validation must be turned off to change the target URL"
-msgstr ""
-
-msgid "DastProfiles|Validation succeeded. Both active and passive scans can be run against the target site."
-msgstr ""
-
msgid "Data is still calculating..."
msgstr ""
@@ -15098,6 +15077,9 @@ msgstr ""
msgid "IssueAnalytics|Weight"
msgstr ""
+msgid "IssueBoards|An error occurred while setting notifications status."
+msgstr ""
+
msgid "IssueBoards|Board"
msgstr ""
@@ -19068,6 +19050,9 @@ msgstr ""
msgid "Open raw"
msgstr ""
+msgid "Open registration is enabled on your instance."
+msgstr ""
+
msgid "Open sidebar"
msgstr ""
@@ -29895,6 +29880,9 @@ msgstr ""
msgid "View replaced file @ "
msgstr ""
+msgid "View setting"
+msgstr ""
+
msgid "View supported languages and frameworks"
msgstr ""
diff --git a/qa/qa/resource/file.rb b/qa/qa/resource/file.rb
index aaf0bc58ef7..0d2bf9890ea 100644
--- a/qa/qa/resource/file.rb
+++ b/qa/qa/resource/file.rb
@@ -5,10 +5,10 @@ module QA
class File < Base
attr_accessor :author_email,
:author_name,
- :branch,
:content,
:commit_message,
:name
+ attr_writer :branch
attribute :project do
Project.fabricate! do |resource|
@@ -29,6 +29,10 @@ module QA
@commit_message = 'QA Test - Commit message'
end
+ def branch
+ @branch ||= "master"
+ end
+
def fabricate!
project.visit!
@@ -42,12 +46,6 @@ module QA
end
end
- def resource_web_url(resource)
- super
- rescue ResourceURLMissingError
- # this particular resource does not expose a web_url property
- end
-
def api_get_path
"/projects/#{CGI.escape(project.path_with_namespace)}/repository/files/#{CGI.escape(@name)}"
end
@@ -58,13 +56,20 @@ module QA
def api_post_body
{
- branch: @branch || "master",
+ branch: branch,
author_email: @author_email || Runtime::User.default_email,
author_name: @author_name || Runtime::User.username,
content: content,
commit_message: commit_message
}
end
+
+ private
+
+ def transform_api_resource(api_resource)
+ api_resource[:web_url] = "#{Runtime::Scenario.gitlab_address}/#{project.full_path}/-/tree/#{branch}/#{api_resource[:file_path]}"
+ api_resource
+ end
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb
deleted file mode 100644
index 062dd815c52..00000000000
--- a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- RSpec.describe 'Create' do
- describe 'Files management' do
- it 'user creates, edits and deletes a file via the Web', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/451' do
- Flow::Login.sign_in
-
- # Create
- file_name = 'QA Test - File name'
- file_content = 'QA Test - File content'
- commit_message_for_create = 'QA Test - Create new file'
-
- Resource::File.fabricate_via_browser_ui! do |file|
- file.name = file_name
- file.content = file_content
- file.commit_message = commit_message_for_create
- end
-
- Page::File::Show.perform do |file|
- aggregate_failures 'file details' do
- expect(file).to have_file(file_name)
- expect(file).to have_file_content(file_content)
- expect(file).to have_commit_message(commit_message_for_create)
- end
- end
-
- # Edit
- updated_file_content = 'QA Test - Updated file content'
- commit_message_for_update = 'QA Test - Update file'
-
- Page::File::Show.perform(&:click_edit)
-
- Page::File::Form.perform do |file|
- file.remove_content
- file.add_content(updated_file_content)
- file.add_commit_message(commit_message_for_update)
- file.commit_changes
- end
-
- Page::File::Show.perform do |file|
- aggregate_failures 'file details' do
- expect(file).to have_notice('Your changes have been successfully committed.')
- expect(file).to have_file_content(updated_file_content)
- expect(file).to have_commit_message(commit_message_for_update)
- end
- end
-
- # Delete
- commit_message_for_delete = 'QA Test - Delete file'
-
- Page::File::Show.perform do |file|
- file.click_delete
- file.add_commit_message(commit_message_for_delete)
- file.click_delete_file
- end
-
- Page::Project::Show.perform do |project|
- aggregate_failures 'file details' do
- expect(project).to have_notice('The file has been successfully deleted.')
- expect(project).to have_commit_message(commit_message_for_delete)
- expect(project).not_to have_file(file_name)
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/file/create_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/file/create_file_via_web_spec.rb
new file mode 100644
index 00000000000..cd333b3cea2
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/file/create_file_via_web_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module QA
+ RSpec.describe 'Create' do
+ context 'File management' do
+ file_name = 'QA Test - File name'
+ file_content = 'QA Test - File content'
+ commit_message_for_create = 'QA Test - Create new file'
+
+ before do
+ Flow::Login.sign_in
+ end
+
+ it 'user creates a file via the Web', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1093' do
+ Resource::File.fabricate_via_browser_ui! do |file|
+ file.name = file_name
+ file.content = file_content
+ file.commit_message = commit_message_for_create
+ end
+
+ Page::File::Show.perform do |file|
+ aggregate_failures 'file details' do
+ expect(file).to have_file(file_name)
+ expect(file).to have_file_content(file_content)
+ expect(file).to have_commit_message(commit_message_for_create)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/file/delete_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/file/delete_file_via_web_spec.rb
new file mode 100644
index 00000000000..903001aa4f0
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/file/delete_file_via_web_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module QA
+ RSpec.describe 'Create' do
+ context 'File management' do
+ let(:file) { Resource::File.fabricate_via_api! }
+
+ commit_message_for_delete = 'QA Test - Delete file'
+
+ before do
+ Flow::Login.sign_in
+ file.visit!
+ end
+
+ it 'user deletes a file via the Web', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1095' do
+ Page::File::Show.perform do |file|
+ file.click_delete
+ file.add_commit_message(commit_message_for_delete)
+ file.click_delete_file
+ end
+
+ Page::Project::Show.perform do |project|
+ aggregate_failures 'file details' do
+ expect(project).to have_notice('The file has been successfully deleted.')
+ expect(project).to have_commit_message(commit_message_for_delete)
+ expect(project).not_to have_file(file.name)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/file/edit_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/file/edit_file_via_web_spec.rb
new file mode 100644
index 00000000000..0da774b557f
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/file/edit_file_via_web_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module QA
+ RSpec.describe 'Create' do
+ context 'File management' do
+ let(:file) { Resource::File.fabricate_via_api! }
+
+ updated_file_content = 'QA Test - Updated file content'
+ commit_message_for_update = 'QA Test - Update file'
+
+ before do
+ Flow::Login.sign_in
+ file.visit!
+ end
+
+ it 'user edits a file via the Web', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1094' do
+ Page::File::Show.perform(&:click_edit)
+
+ Page::File::Form.perform do |file|
+ file.remove_content
+ file.add_content(updated_file_content)
+ file.add_commit_message(commit_message_for_update)
+ file.commit_changes
+ end
+
+ Page::File::Show.perform do |file|
+ aggregate_failures 'file details' do
+ expect(file).to have_notice('Your changes have been successfully committed.')
+ expect(file).to have_file_content(updated_file_content)
+ expect(file).to have_commit_message(commit_message_for_update)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb
index 44642983a36..0fb5124f673 100644
--- a/spec/features/admin/admin_uses_repository_checks_spec.rb
+++ b/spec/features/admin/admin_uses_repository_checks_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe 'Admin uses repository checks', :request_store, :clean_gitlab_red
)
visit_admin_project_page(project)
- page.within('.gl-alert') do
+ page.within('[data-testid="last-repository-check-failed-alert"]') do
expect(page.text).to match(/Last repository check \(just now\) failed/)
end
end
diff --git a/spec/features/callouts/registration_enabled_spec.rb b/spec/features/callouts/registration_enabled_spec.rb
new file mode 100644
index 00000000000..4055965273f
--- /dev/null
+++ b/spec/features/callouts/registration_enabled_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Registration enabled callout' do
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:non_admin) { create(:user) }
+
+ context 'when "Sign-up enabled" setting is `true`' do
+ before do
+ stub_application_setting(signup_enabled: true)
+ end
+
+ context 'when an admin is logged in' do
+ before do
+ sign_in(admin)
+ visit root_dashboard_path
+ end
+
+ it 'displays callout' do
+ expect(page).to have_content 'Open registration is enabled on your instance.'
+ expect(page).to have_link 'View setting', href: general_admin_application_settings_path(anchor: 'js-signup-settings')
+ end
+
+ context 'when callout is dismissed', :js do
+ before do
+ find('[data-testid="close-registration-enabled-callout"]').click
+
+ visit root_dashboard_path
+ end
+
+ it 'does not display callout' do
+ expect(page).not_to have_content 'Open registration is enabled on your instance.'
+ end
+ end
+ end
+
+ context 'when a non-admin is logged in' do
+ before do
+ sign_in(non_admin)
+ visit root_dashboard_path
+ end
+
+ it 'does not display callout' do
+ expect(page).not_to have_content 'Open registration is enabled on your instance.'
+ end
+ end
+ end
+end
diff --git a/spec/frontend/blob/components/blob_header_default_actions_spec.js b/spec/frontend/blob/components/blob_header_default_actions_spec.js
index 590e36b16af..e2c73a5d5d9 100644
--- a/spec/frontend/blob/components/blob_header_default_actions_spec.js
+++ b/spec/frontend/blob/components/blob_header_default_actions_spec.js
@@ -14,8 +14,13 @@ describe('Blob Header Default Actions', () => {
let btnGroup;
let buttons;
+ const blobHash = 'foo-bar';
+
function createComponent(propsData = {}) {
wrapper = mount(BlobHeaderActions, {
+ provide: {
+ blobHash,
+ },
propsData: {
rawPath: Blob.rawPath,
...propsData,
diff --git a/spec/frontend/blob/components/blob_header_spec.js b/spec/frontend/blob/components/blob_header_spec.js
index 01d4bf834d2..3e84347bee4 100644
--- a/spec/frontend/blob/components/blob_header_spec.js
+++ b/spec/frontend/blob/components/blob_header_spec.js
@@ -11,7 +11,11 @@ describe('Blob Header Default Actions', () => {
function createComponent(blobProps = {}, options = {}, propsData = {}, shouldMount = false) {
const method = shouldMount ? mount : shallowMount;
+ const blobHash = 'foo-bar';
wrapper = method.call(this, BlobHeader, {
+ provide: {
+ blobHash,
+ },
propsData: {
blob: { ...Blob, ...blobProps },
...propsData,
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
new file mode 100644
index 00000000000..ee54c662167
--- /dev/null
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
@@ -0,0 +1,157 @@
+import Vuex from 'vuex';
+import { mount, createLocalVue } from '@vue/test-utils';
+import { GlToggle, GlLoadingIcon } from '@gitlab/ui';
+import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
+import * as types from '~/boards/stores/mutation_types';
+import { createStore } from '~/boards/stores';
+import { mockActiveIssue } from '../../mock_data';
+import createFlash from '~/flash';
+
+jest.mock('~/flash.js');
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('~/boards/components/sidebar/board_sidebar_subscription_spec.vue', () => {
+ let wrapper;
+ let store;
+
+ const findNotificationHeader = () => wrapper.find("[data-testid='notification-header-text']");
+ const findToggle = () => wrapper.find(GlToggle);
+ const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon);
+
+ const createComponent = (activeIssue = { ...mockActiveIssue }) => {
+ store = createStore();
+ store.state.issues = { [activeIssue.id]: activeIssue };
+ store.state.activeId = activeIssue.id;
+
+ wrapper = mount(BoardSidebarSubscription, {
+ localVue,
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ store = null;
+ jest.clearAllMocks();
+ });
+
+ describe('Board sidebar subscription component template', () => {
+ it('displays "notifications" heading', () => {
+ createComponent();
+
+ expect(findNotificationHeader().text()).toBe('Notifications');
+ });
+
+ it('renders toggle as "off" when currently not subscribed', () => {
+ createComponent();
+
+ expect(findToggle().exists()).toBe(true);
+ expect(findToggle().props('value')).toBe(false);
+ });
+
+ it('renders toggle as "on" when currently subscribed', () => {
+ createComponent({
+ ...mockActiveIssue,
+ subscribed: true,
+ });
+
+ expect(findToggle().exists()).toBe(true);
+ expect(findToggle().props('value')).toBe(true);
+ });
+
+ describe('when notification emails have been disabled', () => {
+ beforeEach(() => {
+ createComponent({
+ ...mockActiveIssue,
+ emailsDisabled: true,
+ });
+ });
+
+ it('displays a message that notification have been disabled', () => {
+ expect(findNotificationHeader().text()).toBe(
+ 'Notifications have been disabled by the project or group owner',
+ );
+ });
+
+ it('does not render the toggle button', () => {
+ expect(findToggle().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('Board sidebar subscription component `behavior`', () => {
+ const mockSetActiveIssueSubscribed = subscribedState => {
+ jest.spyOn(wrapper.vm, 'setActiveIssueSubscribed').mockImplementation(async () => {
+ store.commit(types.UPDATE_ISSUE_BY_ID, {
+ issueId: mockActiveIssue.id,
+ prop: 'subscribed',
+ value: subscribedState,
+ });
+ });
+ };
+
+ it('subscribing to notification', async () => {
+ createComponent();
+ mockSetActiveIssueSubscribed(true);
+
+ expect(findGlLoadingIcon().exists()).toBe(false);
+
+ findToggle().trigger('click');
+
+ await wrapper.vm.$nextTick();
+
+ expect(findGlLoadingIcon().exists()).toBe(true);
+ expect(wrapper.vm.setActiveIssueSubscribed).toHaveBeenCalledWith({
+ subscribed: true,
+ projectPath: 'gitlab-org/test-subgroup/gitlab-test',
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(findGlLoadingIcon().exists()).toBe(false);
+ expect(findToggle().props('value')).toBe(true);
+ });
+
+ it('unsubscribing from notification', async () => {
+ createComponent({
+ ...mockActiveIssue,
+ subscribed: true,
+ });
+ mockSetActiveIssueSubscribed(false);
+
+ expect(findGlLoadingIcon().exists()).toBe(false);
+
+ findToggle().trigger('click');
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.setActiveIssueSubscribed).toHaveBeenCalledWith({
+ subscribed: false,
+ projectPath: 'gitlab-org/test-subgroup/gitlab-test',
+ });
+ expect(findGlLoadingIcon().exists()).toBe(true);
+
+ await wrapper.vm.$nextTick();
+
+ expect(findGlLoadingIcon().exists()).toBe(false);
+ expect(findToggle().props('value')).toBe(false);
+ });
+
+ it('flashes an error message when setting the subscribed state fails', async () => {
+ createComponent();
+ jest.spyOn(wrapper.vm, 'setActiveIssueSubscribed').mockImplementation(async () => {
+ throw new Error();
+ });
+
+ findToggle().trigger('click');
+
+ await wrapper.vm.$nextTick();
+ expect(createFlash).toHaveBeenNthCalledWith(1, {
+ message: wrapper.vm.$options.i18n.updateSubscribedErrorMessage,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index fbca9e5b219..58f67231d55 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -176,6 +176,14 @@ export const mockIssue = {
},
};
+export const mockActiveIssue = {
+ ...mockIssue,
+ id: 436,
+ iid: '27',
+ subscribed: false,
+ emailsDisabled: false,
+};
+
export const mockIssueWithModel = new ListIssue(mockIssue);
export const mockIssue2 = {
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index f5eb0cb8474..4d529580a7a 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -9,6 +9,7 @@ import {
rawIssue,
mockIssues,
labels,
+ mockActiveIssue,
} from '../mock_data';
import actions, { gqlClient } from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
@@ -833,6 +834,57 @@ describe('setActiveIssueDueDate', () => {
});
});
+describe('setActiveIssueSubscribed', () => {
+ const state = { issues: { [mockActiveIssue.id]: mockActiveIssue } };
+ const getters = { activeIssue: mockActiveIssue };
+ const subscribedState = true;
+ const input = {
+ subscribedState,
+ projectPath: 'gitlab-org/gitlab-test',
+ };
+
+ it('should commit subscribed status', done => {
+ jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
+ data: {
+ issueSetSubscription: {
+ issue: {
+ subscribed: subscribedState,
+ },
+ errors: [],
+ },
+ },
+ });
+
+ const payload = {
+ issueId: getters.activeIssue.id,
+ prop: 'subscribed',
+ value: subscribedState,
+ };
+
+ testAction(
+ actions.setActiveIssueSubscribed,
+ input,
+ { ...state, ...getters },
+ [
+ {
+ type: types.UPDATE_ISSUE_BY_ID,
+ payload,
+ },
+ ],
+ [],
+ done,
+ );
+ });
+
+ it('throws error if fails', async () => {
+ jest
+ .spyOn(gqlClient, 'mutate')
+ .mockResolvedValue({ data: { issueSetSubscription: { errors: ['failed mutation'] } } });
+
+ await expect(actions.setActiveIssueSubscribed({ getters }, input)).rejects.toThrow(Error);
+ });
+});
+
describe('fetchBacklog', () => {
expectNotImplemented(actions.fetchBacklog);
});
diff --git a/spec/frontend/boards/stores/getters_spec.js b/spec/frontend/boards/stores/getters_spec.js
index 66c26d087bb..64025726dd1 100644
--- a/spec/frontend/boards/stores/getters_spec.js
+++ b/spec/frontend/boards/stores/getters_spec.js
@@ -124,6 +124,22 @@ describe('Boards - Getters', () => {
});
});
+ describe('projectPathByIssueId', () => {
+ it('returns project path for the active issue', () => {
+ const mockActiveIssue = {
+ referencePath: 'gitlab-org/gitlab-test#1',
+ };
+ expect(getters.projectPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual(
+ 'gitlab-org/gitlab-test',
+ );
+ });
+
+ it('returns empty string as project when active issue is an empty object', () => {
+ const mockActiveIssue = {};
+ expect(getters.projectPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual('');
+ });
+ });
+
describe('getIssuesByList', () => {
const boardsState = {
issuesByListId: mockIssuesByListId,
diff --git a/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap b/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap
index 4909d2d4226..023895099b1 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap
+++ b/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap
@@ -59,7 +59,7 @@ exports[`Blob Simple Viewer component rendering matches the snapshot 1`] = `
class="code highlight"
>
{
let wrapper;
const contentMock = `First\nSecond\nThird`;
+ const blobHash = 'foo-bar';
function createComponent(content = contentMock) {
wrapper = shallowMount(SimpleViewer, {
+ provide: {
+ blobHash,
+ },
propsData: {
content,
type: 'text',
diff --git a/spec/helpers/application_settings_helper_spec.rb b/spec/helpers/application_settings_helper_spec.rb
index 7f25721801f..479e2d7ef9d 100644
--- a/spec/helpers/application_settings_helper_spec.rb
+++ b/spec/helpers/application_settings_helper_spec.rb
@@ -166,4 +166,32 @@ RSpec.describe ApplicationSettingsHelper do
it { is_expected.to eq(false) }
end
end
+
+ describe '.signup_enabled?' do
+ subject { helper.signup_enabled? }
+
+ context 'when signup is enabled' do
+ before do
+ stub_application_setting(signup_enabled: true)
+ end
+
+ it { is_expected.to be true }
+ end
+
+ context 'when signup is disabled' do
+ before do
+ stub_application_setting(signup_enabled: false)
+ end
+
+ it { is_expected.to be false }
+ end
+
+ context 'when `signup_enabled` is nil' do
+ before do
+ stub_application_setting(signup_enabled: nil)
+ end
+
+ it { is_expected.to be false }
+ end
+ end
end
diff --git a/spec/helpers/user_callouts_helper_spec.rb b/spec/helpers/user_callouts_helper_spec.rb
index bcb0b5c51e7..4ab3be877b4 100644
--- a/spec/helpers/user_callouts_helper_spec.rb
+++ b/spec/helpers/user_callouts_helper_spec.rb
@@ -161,4 +161,50 @@ RSpec.describe UserCalloutsHelper do
it { is_expected.to be_falsy }
end
end
+
+ describe '.show_registration_enabled_user_callout?' do
+ let_it_be(:admin) { create(:user, :admin) }
+
+ subject { helper.show_registration_enabled_user_callout? }
+
+ context 'when `current_user` is not an admin' do
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ stub_application_setting(signup_enabled: true)
+ allow(helper).to receive(:user_dismissed?).with(described_class::REGISTRATION_ENABLED_CALLOUT) { false }
+ end
+
+ it { is_expected.to be false }
+ end
+
+ context 'when signup is disabled' do
+ before do
+ allow(helper).to receive(:current_user).and_return(admin)
+ stub_application_setting(signup_enabled: false)
+ allow(helper).to receive(:user_dismissed?).with(described_class::REGISTRATION_ENABLED_CALLOUT) { false }
+ end
+
+ it { is_expected.to be false }
+ end
+
+ context 'when user has dismissed callout' do
+ before do
+ allow(helper).to receive(:current_user).and_return(admin)
+ stub_application_setting(signup_enabled: true)
+ allow(helper).to receive(:user_dismissed?).with(described_class::REGISTRATION_ENABLED_CALLOUT) { true }
+ end
+
+ it { is_expected.to be false }
+ end
+
+ context 'when `current_user` is an admin, signup is enabled, and user has not dismissed callout' do
+ before do
+ allow(helper).to receive(:current_user).and_return(admin)
+ stub_application_setting(signup_enabled: true)
+ allow(helper).to receive(:user_dismissed?).with(described_class::REGISTRATION_ENABLED_CALLOUT) { false }
+ end
+
+ it { is_expected.to be true }
+ end
+ end
end
diff --git a/spec/services/merge_requests/cleanup_refs_service_spec.rb b/spec/services/merge_requests/cleanup_refs_service_spec.rb
index 70acb50bb29..38c0e204e54 100644
--- a/spec/services/merge_requests/cleanup_refs_service_spec.rb
+++ b/spec/services/merge_requests/cleanup_refs_service_spec.rb
@@ -115,6 +115,19 @@ RSpec.describe MergeRequests::CleanupRefsService do
it_behaves_like 'service that does not clean up merge request refs'
end
+
+ context 'when repository no longer exists' do
+ before do
+ Repositories::DestroyService.new(merge_request.project.repository).execute
+ end
+
+ it 'does not fail and still mark schedule as complete' do
+ aggregate_failures do
+ expect(result[:status]).to eq(:success)
+ expect(merge_request.cleanup_schedule.completed_at).to be_present
+ end
+ end
+ end
end
shared_examples_for 'service that does not clean up merge request refs' do
diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb
index bf8ceaf08d4..2bd83dc36a8 100644
--- a/spec/services/merge_requests/reopen_service_spec.rb
+++ b/spec/services/merge_requests/reopen_service_spec.rb
@@ -24,6 +24,7 @@ RSpec.describe MergeRequests::ReopenService do
before do
allow(service).to receive(:execute_hooks)
merge_request.create_cleanup_schedule(scheduled_at: Time.current)
+ merge_request.update_column(:merge_ref_sha, 'abc123')
perform_enqueued_jobs do
service.execute(merge_request)
@@ -48,6 +49,10 @@ RSpec.describe MergeRequests::ReopenService do
expect(merge_request.reload.cleanup_schedule).to be_nil
end
+ it 'clears the cached merge_ref_sha' do
+ expect(merge_request.reload.merge_ref_sha).to be_nil
+ end
+
context 'note creation' do
it 'creates resource state event about merge_request reopen' do
event = merge_request.resource_state_events.last