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 @@ + + + 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