Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-05-06 18:10:06 +00:00
parent 0a5ea888dc
commit 52dbfea964
53 changed files with 511 additions and 179 deletions

View file

@ -13,15 +13,10 @@
# WIP See https://gitlab.com/gitlab-org/gitlab/-/issues/322903 # WIP See https://gitlab.com/gitlab-org/gitlab/-/issues/322903
Graphql/Descriptions: Graphql/Descriptions:
Exclude: Exclude:
- 'app/graphql/types/container_expiration_policy_cadence_enum.rb'
- 'app/graphql/types/container_expiration_policy_keep_enum.rb'
- 'app/graphql/types/container_expiration_policy_older_than_enum.rb'
- 'app/graphql/types/packages/package_type_enum.rb'
- 'app/graphql/types/snippets/blob_action_enum.rb' - 'app/graphql/types/snippets/blob_action_enum.rb'
- 'app/graphql/types/snippets/type_enum.rb' - 'app/graphql/types/snippets/type_enum.rb'
- 'app/graphql/types/snippets/visibility_scopes_enum.rb' - 'app/graphql/types/snippets/visibility_scopes_enum.rb'
- 'ee/app/graphql/ee/types/list_limit_metric_enum.rb' - 'ee/app/graphql/ee/types/list_limit_metric_enum.rb'
- 'ee/app/graphql/types/alert_management/payload_alert_field_name_enum.rb'
- 'ee/app/graphql/types/epic_state_enum.rb' - 'ee/app/graphql/types/epic_state_enum.rb'
- 'ee/app/graphql/types/health_status_enum.rb' - 'ee/app/graphql/types/health_status_enum.rb'
- 'ee/app/graphql/types/iteration_state_enum.rb' - 'ee/app/graphql/types/iteration_state_enum.rb'

View file

@ -232,7 +232,7 @@ export function insertMarkdownText({
.join('\n'); .join('\n');
} }
} else if (tag.indexOf(textPlaceholder) > -1) { } else if (tag.indexOf(textPlaceholder) > -1) {
textToInsert = tag.replace(textPlaceholder, selected.replace(/\\n/g, '\n')); textToInsert = tag.replace(textPlaceholder, () => selected.replace(/\\n/g, '\n'));
} else { } else {
textToInsert = String(startChar) + tag + selected + (wrap ? tag : ''); textToInsert = String(startChar) + tag + selected + (wrap ? tag : '');
} }

View file

@ -18,9 +18,13 @@ export default {
required: true, required: true,
type: Object, type: Object,
}, },
sections: {
required: true,
type: Object,
},
}, },
maxValue: Object.keys(ACTION_LABELS).length, maxValue: Object.keys(ACTION_LABELS).length,
sections: Object.keys(ACTION_SECTIONS), actionSections: Object.keys(ACTION_SECTIONS),
computed: { computed: {
progressValue() { progressValue() {
return Object.values(this.actions).filter((a) => a.completed).length; return Object.values(this.actions).filter((a) => a.completed).length;
@ -38,6 +42,9 @@ export default {
); );
return actions; return actions;
}, },
svgFor(section) {
return this.sections[section].svg;
},
}, },
}; };
</script> </script>
@ -59,8 +66,12 @@ export default {
<gl-progress-bar :value="progressValue" :max="$options.maxValue" /> <gl-progress-bar :value="progressValue" :max="$options.maxValue" />
</div> </div>
<div class="row row-cols-1 row-cols-md-3 gl-mt-5"> <div class="row row-cols-1 row-cols-md-3 gl-mt-5">
<div v-for="section in $options.sections" :key="section" class="col gl-mb-6"> <div v-for="section in $options.actionSections" :key="section" class="col gl-mb-6">
<learn-gitlab-section-card :section="section" :actions="actionsFor(section)" /> <learn-gitlab-section-card
:section="section"
:svg="svgFor(section)"
:actions="actionsFor(section)"
/>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,6 +1,5 @@
<script> <script>
import { GlCard } from '@gitlab/ui'; import { GlCard } from '@gitlab/ui';
import { imagePath } from '~/lib/utils/common_utils';
import { ACTION_LABELS, ACTION_SECTIONS } from '../constants'; import { ACTION_LABELS, ACTION_SECTIONS } from '../constants';
import LearnGitlabSectionLink from './learn_gitlab_section_link.vue'; import LearnGitlabSectionLink from './learn_gitlab_section_link.vue';
@ -16,6 +15,10 @@ export default {
required: true, required: true,
type: String, type: String,
}, },
svg: {
required: true,
type: String,
},
actions: { actions: {
required: true, required: true,
type: Object, type: Object,
@ -28,17 +31,12 @@ export default {
); );
}, },
}, },
methods: {
svg(section) {
return imagePath(`learn_gitlab/section_${section}.svg`);
},
},
}; };
</script> </script>
<template> <template>
<gl-card class="gl-pt-0 learn-gitlab-section-card"> <gl-card class="gl-pt-0 learn-gitlab-section-card">
<div class="learn-gitlab-section-card-header"> <div class="learn-gitlab-section-card-header">
<img :src="svg(section)" /> <img :src="svg" />
<h2 class="gl-font-lg gl-mb-3">{{ $options.i18n[section].title }}</h2> <h2 class="gl-font-lg gl-mb-3">{{ $options.i18n[section].title }}</h2>
<p class="gl-text-gray-700 gl-mb-6">{{ $options.i18n[section].description }}</p> <p class="gl-text-gray-700 gl-mb-6">{{ $options.i18n[section].description }}</p>
</div> </div>

View file

@ -12,6 +12,7 @@ function initLearnGitlab() {
} }
const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions)); const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions));
const sections = convertObjectPropsToCamelCase(JSON.parse(el.dataset.sections));
const { learnGitlabA } = gon.experiments; const { learnGitlabA } = gon.experiments;
@ -20,7 +21,9 @@ function initLearnGitlab() {
return new Vue({ return new Vue({
el, el,
render(createElement) { render(createElement) {
return createElement(learnGitlabA ? LearnGitlabA : LearnGitlabB, { props: { actions } }); return createElement(learnGitlabA ? LearnGitlabA : LearnGitlabB, {
props: { actions, sections },
});
}, },
}); });
} }

View file

@ -36,14 +36,6 @@ export default {
}; };
}, },
computed: { computed: {
displayPipelineActions() {
return (
this.pipeline.flags.retryable ||
this.pipeline.flags.cancelable ||
this.pipeline.details.manual_actions.length ||
this.pipeline.details.has_downloadable_artifacts
);
},
actions() { actions() {
if (!this.pipeline || !this.pipeline.details) { if (!this.pipeline || !this.pipeline.details) {
return []; return [];
@ -54,9 +46,6 @@ export default {
isCancelling() { isCancelling() {
return this.cancelingPipeline === this.pipeline.id; return this.cancelingPipeline === this.pipeline.id;
}, },
showArtifacts() {
return this.pipeline.details.has_downloadable_artifacts;
},
}, },
watch: { watch: {
pipeline() { pipeline() {
@ -79,7 +68,7 @@ export default {
</script> </script>
<template> <template>
<div v-if="displayPipelineActions" class="gl-text-right"> <div class="gl-text-right">
<div class="btn-group"> <div class="btn-group">
<pipelines-manual-actions v-if="actions.length > 0" :actions="actions" /> <pipelines-manual-actions v-if="actions.length > 0" :actions="actions" />
@ -113,7 +102,7 @@ export default {
@click="handleCancelClick" @click="handleCancelClick"
/> />
<pipeline-multi-actions v-if="showArtifacts" :pipeline-id="pipeline.id" /> <pipeline-multi-actions :pipeline-id="pipeline.id" />
</div> </div>
</div> </div>
</template> </template>

View file

@ -11,7 +11,7 @@ module Types
}.freeze }.freeze
::ContainerExpirationPolicy.cadence_options.each do |option, description| ::ContainerExpirationPolicy.cadence_options.each do |option, description|
value OPTIONS_MAPPING[option], description, value: option.to_s value OPTIONS_MAPPING[option], description: description, value: option.to_s
end end
end end
end end

View file

@ -12,7 +12,7 @@ module Types
}.freeze }.freeze
::ContainerExpirationPolicy.keep_n_options.each do |option, description| ::ContainerExpirationPolicy.keep_n_options.each do |option, description|
value OPTIONS_MAPPING[option], description, value: option value OPTIONS_MAPPING[option], description: description, value: option
end end
end end
end end

View file

@ -10,7 +10,7 @@ module Types
}.freeze }.freeze
::ContainerExpirationPolicy.older_than_options.each do |option, description| ::ContainerExpirationPolicy.older_than_options.each do |option, description|
value OPTIONS_MAPPING[option], description, value: option.to_s value OPTIONS_MAPPING[option], description: description, value: option.to_s
end end
end end
end end

View file

@ -10,7 +10,7 @@ module Types
::Packages::Package.package_types.keys.each do |package_type| ::Packages::Package.package_types.keys.each do |package_type|
type_name = PACKAGE_TYPE_NAMES.fetch(package_type.to_sym, package_type.capitalize) type_name = PACKAGE_TYPE_NAMES.fetch(package_type.to_sym, package_type.capitalize)
value package_type.to_s.upcase, "Packages from the #{type_name} package manager", value: package_type.to_s value package_type.to_s.upcase, description: "Packages from the #{type_name} package manager", value: package_type.to_s
end end
end end
end end

View file

@ -36,6 +36,20 @@ module LearnGitlabHelper
Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_b, subject: current_user) Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_b, subject: current_user)
end end
def onboarding_sections_data
{
workspace: {
svg: image_path("learn_gitlab/section_workspace.svg")
},
plan: {
svg: image_path("learn_gitlab/section_plan.svg")
},
deploy: {
svg: image_path("learn_gitlab/section_deploy.svg")
}
}
end
private private
def action_urls def action_urls

View file

@ -1076,14 +1076,6 @@ module Ci
complete? && builds.latest.with_exposed_artifacts.exists? complete? && builds.latest.with_exposed_artifacts.exists?
end end
def has_downloadable_artifacts?
if downloadable_artifacts.loaded?
downloadable_artifacts.any?
else
downloadable_artifacts.exists?
end
end
def branch_updated? def branch_updated?
strong_memoize(:branch_updated) do strong_memoize(:branch_updated) do
push_details.branch_updated? push_details.branch_updated?

View file

@ -8,7 +8,6 @@ class PipelineDetailsEntity < Ci::PipelineEntity
end end
expose :details do expose :details do
expose :has_downloadable_artifacts?, as: :has_downloadable_artifacts
expose :artifacts, unless: proc { options[:disable_artifacts] } do |pipeline, options| expose :artifacts, unless: proc { options[:disable_artifacts] } do |pipeline, options|
rel = pipeline.downloadable_artifacts rel = pipeline.downloadable_artifacts

View file

@ -3,8 +3,18 @@
class IssueRebalancingService class IssueRebalancingService
MAX_ISSUE_COUNT = 10_000 MAX_ISSUE_COUNT = 10_000
BATCH_SIZE = 100 BATCH_SIZE = 100
SMALLEST_BATCH_SIZE = 5
RETRIES_LIMIT = 3
TooManyIssues = Class.new(StandardError) TooManyIssues = Class.new(StandardError)
TIMING_CONFIGURATION = [
[0.1.seconds, 0.05.seconds], # short timings, lock_timeout: 100ms, sleep after LockWaitTimeout: 50ms
[0.5.seconds, 0.05.seconds],
[1.second, 0.5.seconds],
[1.second, 0.5.seconds],
[5.seconds, 1.second]
].freeze
def initialize(issue) def initialize(issue)
@issue = issue @issue = issue
@base = Issue.relative_positioning_query_base(issue) @base = Issue.relative_positioning_query_base(issue)
@ -23,14 +33,23 @@ class IssueRebalancingService
assign_positions(start, indexed_ids) assign_positions(start, indexed_ids)
.sort_by(&:first) .sort_by(&:first)
.each_slice(BATCH_SIZE) do |pairs_with_position| .each_slice(BATCH_SIZE) do |pairs_with_position|
update_positions(pairs_with_position, 'rebalance issue positions in batches ordered by id') if Feature.enabled?(:issue_rebalancing_with_retry)
update_positions_with_retry(pairs_with_position, 'rebalance issue positions in batches ordered by id')
else
update_positions(pairs_with_position, 'rebalance issue positions in batches ordered by id')
end
end end
end end
else else
Issue.transaction do Issue.transaction do
indexed_ids.each_slice(BATCH_SIZE) do |pairs| indexed_ids.each_slice(BATCH_SIZE) do |pairs|
pairs_with_position = assign_positions(start, pairs) pairs_with_position = assign_positions(start, pairs)
update_positions(pairs_with_position, 'rebalance issue positions')
if Feature.enabled?(:issue_rebalancing_with_retry)
update_positions_with_retry(pairs_with_position, 'rebalance issue positions')
else
update_positions(pairs_with_position, 'rebalance issue positions')
end
end end
end end
end end
@ -52,12 +71,37 @@ class IssueRebalancingService
end end
end end
def update_positions_with_retry(pairs_with_position, query_name)
retries = 0
batch_size = pairs_with_position.size
until pairs_with_position.empty?
begin
update_positions(pairs_with_position.first(batch_size), query_name)
pairs_with_position = pairs_with_position.drop(batch_size)
retries = 0
rescue ActiveRecord::StatementTimeout, ActiveRecord::QueryCanceled => ex
raise ex if batch_size < SMALLEST_BATCH_SIZE
if (retries += 1) == RETRIES_LIMIT
# shrink the batch size in half when RETRIES limit is reached and update still fails perhaps because batch size is still too big
batch_size = (batch_size / 2).to_i
retries = 0
end
retry
end
end
end
def update_positions(pairs_with_position, query_name) def update_positions(pairs_with_position, query_name)
values = pairs_with_position.map do |id, index| values = pairs_with_position.map do |id, index|
"(#{id}, #{index})" "(#{id}, #{index})"
end.join(', ') end.join(', ')
run_update_query(values, query_name) Gitlab::Database::WithLockRetries.new(timing_configuration: TIMING_CONFIGURATION, klass: self.class).run do
run_update_query(values, query_name)
end
end end
def run_update_query(values, query_name) def run_update_query(values, query_name)

View file

@ -1,28 +1,3 @@
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
= link_to project_snippets_path(@project), class: 'shortcuts-snippets', data: { qa_selector: 'snippets_link' } do
.nav-icon-container
= sprite_icon('snippet')
%span.nav-item-name
= _('Snippets')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :snippets, html_options: { class: "fly-out-top-item" } ) do
= link_to project_snippets_path(@project) do
%strong.fly-out-top-item-name
= _('Snippets')
= nav_link(controller: :project_members) do
= link_to project_project_members_path(@project), title: _('Members'), class: 'qa-members-link', id: 'js-onboarding-members-link' do
.nav-icon-container
= sprite_icon('users')
%span.nav-item-name
= _('Members')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: %w[members#show], html_options: { class: "fly-out-top-item" } ) do
= link_to project_project_members_path(@project) do
%strong.fly-out-top-item-name
= _('Members')
- if project_nav_tab? :settings - if project_nav_tab? :settings
= nav_link(path: sidebar_settings_paths) do = nav_link(path: sidebar_settings_paths) do
= link_to edit_project_path(@project) do = link_to edit_project_path(@project) do

View file

@ -2,4 +2,4 @@
- page_title _("Learn GitLab") - page_title _("Learn GitLab")
- add_page_specific_style 'page_bundles/learn_gitlab' - add_page_specific_style 'page_bundles/learn_gitlab'
#js-learn-gitlab-app{ data: { actions: onboarding_actions_data(@project).to_json } } #js-learn-gitlab-app{ data: { actions: onboarding_actions_data(@project).to_json, sections: onboarding_sections_data.to_json } }

View file

@ -0,0 +1,6 @@
---
title: Fix SMTP errors when delivering service desk thank you emails with SMTP pool
enabled
merge_request: 60843
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Stop exposing has_downloadable_artifacts in pipelines.json
merge_request: 60950
author:
type: performance

View file

@ -0,0 +1,5 @@
---
title: Fixed dollar signs in suggestions getting replaced incorrectly
merge_request: 61041
author:
type: fixed

View file

@ -1,5 +1,5 @@
--- ---
title: Increase load time of project select dropdowns title: Decrease load time of project select dropdowns
merge_request: 61117 merge_request: 61117
author: author:
type: performance type: performance

View file

@ -0,0 +1,8 @@
---
name: issue_rebalancing_with_retry
introduced_by_url:
rollout_issue_url:
milestone: '13.11'
type: development
group: group::project management
default_enabled: false

View file

@ -27,7 +27,7 @@ have a high degree of confidence in being able to perform them accurately.
## Not all data is automatically replicated ## Not all data is automatically replicated
If you are using any GitLab features that Geo [doesn't support](../index.md#limitations), If you are using any GitLab features that Geo [doesn't support](../replication/datatypes.md#limitations-on-replicationverification),
you must make separate provisions to ensure that the **secondary** node has an you must make separate provisions to ensure that the **secondary** node has an
up-to-date copy of any data associated with that feature. This may extend the up-to-date copy of any data associated with that feature. This may extend the
required scheduled maintenance period significantly. required scheduled maintenance period significantly.
@ -40,8 +40,7 @@ final transfer inside the maintenance window) will then transfer only the
Repository-centric strategies for using `rsync` effectively can be found in the Repository-centric strategies for using `rsync` effectively can be found in the
[moving repositories](../../operations/moving_repositories.md) documentation; these strategies can [moving repositories](../../operations/moving_repositories.md) documentation; these strategies can
be adapted for use with any other file-based data, such as GitLab Pages (to be adapted for use with any other file-based data, such as [GitLab Pages](../../pages/index.md#change-storage-path).
be found in `/var/opt/gitlab/gitlab-rails/shared/pages` if using Omnibus).
## Preflight checks ## Preflight checks

View file

@ -181,6 +181,25 @@ When something is marked to be updated in the tracking database instance, asynch
This new architecture allows GitLab to be resilient to connectivity issues between the nodes. It doesn't matter how long the **secondary** node is disconnected from the **primary** node as it will be able to replay all the events in the correct order and become synchronized with the **primary** node again. This new architecture allows GitLab to be resilient to connectivity issues between the nodes. It doesn't matter how long the **secondary** node is disconnected from the **primary** node as it will be able to replay all the events in the correct order and become synchronized with the **primary** node again.
## Limitations
WARNING:
This list of limitations only reflects the latest version of GitLab. If you are using an older version, extra limitations may be in place.
- Pushing directly to a **secondary** node redirects (for HTTP) or proxies (for SSH) the request to the **primary** node instead of [handling it directly](https://gitlab.com/gitlab-org/gitlab/-/issues/1381), except when using Git over HTTP with credentials embedded within the URI. For example, `https://user:password@secondary.tld`.
- The **primary** node has to be online for OAuth login to happen. Existing sessions and Git are not affected. Support for the **secondary** node to use an OAuth provider independent from the primary is [being planned](https://gitlab.com/gitlab-org/gitlab/-/issues/208465).
- The installation takes multiple manual steps that together can take about an hour depending on circumstances. We are working on improving this experience. See [Omnibus GitLab issue #2978](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/2978) for details.
- Real-time updates of issues/merge requests (for example, via long polling) doesn't work on the **secondary** node.
- [Selective synchronization](replication/configuration.md#selective-synchronization) applies only to files and repositories. Other datasets are replicated to the **secondary** node in full, making it inappropriate for use as an access control mechanism.
- Object pools for forked project deduplication work only on the **primary** node, and are duplicated on the **secondary** node.
- GitLab Runners cannot register with a **secondary** node. Support for this is [planned for the future](https://gitlab.com/gitlab-org/gitlab/-/issues/3294).
- Geo **secondary** nodes can not be configured to [use high-availability configurations of PostgreSQL](https://gitlab.com/groups/gitlab-org/-/epics/2536).
- [Selective synchronization](replication/configuration.md#selective-synchronization) only limits what repositories are replicated. The entire PostgreSQL data is still replicated. Selective synchronization is not built to accomodate compliance / export control use cases.
### Limitations on replication/verification
There is a complete list of all GitLab [data types](replication/datatypes.md) and [existing support for replication and verification](replication/datatypes.md#limitations-on-replicationverification).
## Setup instructions ## Setup instructions
For setup instructions, see [Setting up Geo](setup/index.md). For setup instructions, see [Setting up Geo](setup/index.md).
@ -275,25 +294,6 @@ For more information on removing a Geo node, see [Removing **secondary** Geo nod
To find out how to disable Geo, see [Disabling Geo](replication/disable_geo.md). To find out how to disable Geo, see [Disabling Geo](replication/disable_geo.md).
## Limitations
WARNING:
This list of limitations only reflects the latest version of GitLab. If you are using an older version, extra limitations may be in place.
- Pushing directly to a **secondary** node redirects (for HTTP) or proxies (for SSH) the request to the **primary** node instead of [handling it directly](https://gitlab.com/gitlab-org/gitlab/-/issues/1381), except when using Git over HTTP with credentials embedded within the URI. For example, `https://user:password@secondary.tld`.
- The **primary** node has to be online for OAuth login to happen. Existing sessions and Git are not affected. Support for the **secondary** node to use an OAuth provider independent from the primary is [being planned](https://gitlab.com/gitlab-org/gitlab/-/issues/208465).
- The installation takes multiple manual steps that together can take about an hour depending on circumstances. We are working on improving this experience. See [Omnibus GitLab issue #2978](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/2978) for details.
- Real-time updates of issues/merge requests (for example, via long polling) doesn't work on the **secondary** node.
- [Selective synchronization](replication/configuration.md#selective-synchronization) applies only to files and repositories. Other datasets are replicated to the **secondary** node in full, making it inappropriate for use as an access control mechanism.
- Object pools for forked project deduplication work only on the **primary** node, and are duplicated on the **secondary** node.
- GitLab Runners cannot register with a **secondary** node. Support for this is [planned for the future](https://gitlab.com/gitlab-org/gitlab/-/issues/3294).
- Geo **secondary** nodes can not be configured to [use high-availability configurations of PostgreSQL](https://gitlab.com/groups/gitlab-org/-/epics/2536).
- [Selective synchronization](replication/configuration.md#selective-synchronization) only limits what repositories are replicated. The entire PostgreSQL data is still replicated. Selective synchronization is not built to accomodate compliance / export control use cases.
### Limitations on replication/verification
There is a complete list of all GitLab [data types](replication/datatypes.md) and [existing support for replication and verification](replication/datatypes.md#limitations-on-replicationverification).
## Frequently Asked Questions ## Frequently Asked Questions
For answers to common questions, see the [Geo FAQ](replication/faq.md). For answers to common questions, see the [Geo FAQ](replication/faq.md).

View file

@ -853,6 +853,12 @@ To resolve this issue:
the **primary** node using IPv4 in the `/etc/hosts` file. Alternatively, you should the **primary** node using IPv4 in the `/etc/hosts` file. Alternatively, you should
[enable IPv6 on the **primary** node](https://docs.gitlab.com/omnibus/settings/nginx.html#setting-the-nginx-listen-address-or-addresses). [enable IPv6 on the **primary** node](https://docs.gitlab.com/omnibus/settings/nginx.html#setting-the-nginx-listen-address-or-addresses).
### GitLab Pages return 404 errors after promoting
This is due to [Pages data not being managed by Geo](datatypes.md#limitations-on-replicationverification).
Find advice to resolve those errors in the
[Pages administration documentation](../../../administration/pages/index.md#404-error-after-promoting-a-geo-secondary-to-a-primary-node).
## Fixing client errors ## Fixing client errors
### Authorization errors from LFS HTTP(s) client requests ### Authorization errors from LFS HTTP(s) client requests

View file

@ -1167,6 +1167,17 @@ date > /var/opt/gitlab/gitlab-rails/shared/pages/.update
If you've customized the Pages storage path, adjust the command above to use your custom path. If you've customized the Pages storage path, adjust the command above to use your custom path.
### 404 error after promoting a Geo secondary to a primary node
These are due to the Pages files not being among the
[supported data types](../geo/replication/datatypes.md#limitations-on-replicationverification).
It is possible to copy the subfolders and files in the [Pages path](#change-storage-path)
to the new primary node to resolve this.
For example, you can adapt the `rsync` strategy from the
[moving repositories documenation](../operations/moving_repositories.md).
Alternatively, run the CI pipelines of those projects that contain a `pages` job again.
### Failed to connect to the internal GitLab API ### Failed to connect to the internal GitLab API
If you have enabled [API-based configuration](#gitlab-api-based-configuration) and see the following error: If you have enabled [API-based configuration](#gitlab-api-based-configuration) and see the following error:

View file

@ -24,7 +24,9 @@ Fields that are deprecated are marked with **{warning-solid}**.
Items (fields, enums, etc) that have been removed according to our [deprecation process](../index.md#deprecation-and-removal-process) can be found Items (fields, enums, etc) that have been removed according to our [deprecation process](../index.md#deprecation-and-removal-process) can be found
in [Removed Items](../removed_items.md). in [Removed Items](../removed_items.md).
<!-- vale gitlab.Spelling = NO --> <!-- vale off -->
<!-- Docs linting disabled after this line. -->
<!-- See https://docs.gitlab.com/ee/development/documentation/testing.html#disable-vale-tests -->
## `Query` type ## `Query` type

View file

@ -10,8 +10,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
Please do not edit this file directly, check generate_metrics_dictionary task on lib/tasks/gitlab/usage_data.rake. Please do not edit this file directly, check generate_metrics_dictionary task on lib/tasks/gitlab/usage_data.rake.
---> --->
<!-- vale gitlab.Spelling = NO -->
# Metrics Dictionary # Metrics Dictionary
This file is autogenerated, please do not edit directly. This file is autogenerated, please do not edit directly.
@ -30,6 +28,10 @@ The Metrics Dictionary is based on the following metrics definition YAML files:
Each table includes a `milestone`, which corresponds to the GitLab version when the metric Each table includes a `milestone`, which corresponds to the GitLab version when the metric
was released. was released.
<!-- vale off -->
<!-- Docs linting disabled after this line. -->
<!-- See https://docs.gitlab.com/ee/development/documentation/testing.html#disable-vale-tests -->
## Metrics Definitions ## Metrics Definitions
### `active_user_count` ### `active_user_count`

View file

@ -302,6 +302,9 @@ A 404 can also be related to incorrect permissions. If [Pages Access Control](pa
navigates to the Pages URL and receives a 404 response, it is possible that the user does not have permission to view the site. navigates to the Pages URL and receives a 404 response, it is possible that the user does not have permission to view the site.
To fix this, verify that the user is a member of the project. To fix this, verify that the user is a member of the project.
For Geo instances, 404 errors on Pages occur after promoting a secondary to a primary.
Find more details in the [Pages administration documentation](../../../administration/pages/index.md#404-error-after-promoting-a-geo-secondary-to-a-primary-node)
### Cannot play media content on Safari ### Cannot play media content on Safari
Safari requires the web server to support the [Range request header](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/CreatingVideoforSafarioniPhone/CreatingVideoforSafarioniPhone.html#//apple_ref/doc/uid/TP40006514-SW6) Safari requires the web server to support the [Range request header](https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/CreatingVideoforSafarioniPhone/CreatingVideoforSafarioniPhone.html#//apple_ref/doc/uid/TP40006514-SW6)

View file

@ -38,7 +38,7 @@ module Gitlab
if from_address if from_address
add_email_participant add_email_participant
send_thank_you_email! send_thank_you_email
end end
end end
@ -92,8 +92,8 @@ module Gitlab
end end
end end
def send_thank_you_email! def send_thank_you_email
Notify.service_desk_thank_you_email(@issue.id).deliver_later! Notify.service_desk_thank_you_email(@issue.id).deliver_later
end end
def message_including_template def message_including_template

View file

@ -17,7 +17,9 @@
Items (fields, enums, etc) that have been removed according to our [deprecation process](../index.md#deprecation-and-removal-process) can be found Items (fields, enums, etc) that have been removed according to our [deprecation process](../index.md#deprecation-and-removal-process) can be found
in [Removed Items](../removed_items.md). in [Removed Items](../removed_items.md).
<!-- vale gitlab.Spelling = NO --> <!-- vale off -->
<!-- Docs linting disabled after this line. -->
<!-- See https://docs.gitlab.com/ee/development/documentation/testing.html#disable-vale-tests -->
\ \
:plain :plain

View file

@ -18,8 +18,6 @@ module Gitlab
Please do not edit this file directly, check generate_metrics_dictionary task on lib/tasks/gitlab/usage_data.rake. Please do not edit this file directly, check generate_metrics_dictionary task on lib/tasks/gitlab/usage_data.rake.
---> --->
<!-- vale gitlab.Spelling = NO -->
MARKDOWN MARKDOWN
end end

View file

@ -19,6 +19,10 @@
Each table includes a `milestone`, which corresponds to the GitLab version when the metric Each table includes a `milestone`, which corresponds to the GitLab version when the metric
was released. was released.
<!-- vale off -->
<!-- Docs linting disabled after this line. -->
<!-- See https://docs.gitlab.com/ee/development/documentation/testing.html#disable-vale-tests -->
## Metrics Definitions ## Metrics Definitions
\ \

View file

@ -0,0 +1,41 @@
# frozen_string_literal: true
module Sidebars
module Projects
module Menus
class MembersMenu < ::Sidebars::Menu
override :link
def link
project_project_members_path(context.project)
end
override :extra_container_html_options
def extra_container_html_options
{
id: 'js-onboarding-members-link'
}
end
override :title
def title
_('Members')
end
override :sprite_icon
def sprite_icon
'users'
end
override :render?
def render?
can?(context.current_user, :read_project_member, context.project)
end
override :active_routes
def active_routes
{ controller: :project_members }
end
end
end
end
end

View file

@ -0,0 +1,41 @@
# frozen_string_literal: true
module Sidebars
module Projects
module Menus
class SnippetsMenu < ::Sidebars::Menu
override :link
def link
project_snippets_path(context.project)
end
override :extra_container_html_options
def extra_container_html_options
{
class: 'shortcuts-snippets'
}
end
override :title
def title
_('Snippets')
end
override :sprite_icon
def sprite_icon
'snippet'
end
override :render?
def render?
can?(context.current_user, :read_snippet, context.project)
end
override :active_routes
def active_routes
{ controller: :snippets }
end
end
end
end
end

View file

@ -22,6 +22,8 @@ module Sidebars
add_menu(Sidebars::Projects::Menus::AnalyticsMenu.new(context)) add_menu(Sidebars::Projects::Menus::AnalyticsMenu.new(context))
add_menu(confluence_or_wiki_menu) add_menu(confluence_or_wiki_menu)
add_menu(Sidebars::Projects::Menus::ExternalWikiMenu.new(context)) add_menu(Sidebars::Projects::Menus::ExternalWikiMenu.new(context))
add_menu(Sidebars::Projects::Menus::SnippetsMenu.new(context))
add_menu(Sidebars::Projects::Menus::MembersMenu.new(context))
end end
override :render_raw_menus_partial override :render_raw_menus_partial

View file

@ -13,11 +13,6 @@ module QA
include SubMenus::Settings include SubMenus::Settings
include SubMenus::Packages include SubMenus::Packages
view 'app/views/layouts/nav/sidebar/_project_menus.html.haml' do
element :snippets_link
element :members_link
end
def click_merge_requests def click_merge_requests
within_sidebar do within_sidebar do
click_element(:sidebar_menu_link, menu_item: 'Merge requests') click_element(:sidebar_menu_link, menu_item: 'Merge requests')
@ -38,13 +33,13 @@ module QA
def click_snippets def click_snippets
within_sidebar do within_sidebar do
click_element(:snippets_link) click_element(:sidebar_menu_link, menu_item: 'Snippets')
end end
end end
def click_members def click_members
within_sidebar do within_sidebar do
click_element(:members_link) click_element(:sidebar_menu_link, menu_item: 'Members')
end end
end end
end end

View file

@ -88,12 +88,18 @@ RSpec.describe 'GitLab Markdown Benchmark', :aggregate_failures do
def build_filter_text(pipeline, initial_text) def build_filter_text(pipeline, initial_text)
filter_source = {} filter_source = {}
input_text = initial_text input_text = initial_text
result = nil
pipeline.filters.each do |filter_klass| pipeline.filters.each do |filter_klass|
filter_source[filter_klass] = input_text # store inputs for current filter_klass
filter_source[filter_klass] = { input_text: input_text, input_result: result }
output = filter_klass.call(input_text, context) filter = filter_klass.new(input_text, context, result)
output = filter.call
# save these for the next filter_klass
input_text = output input_text = output
result = filter.result
end end
filter_source filter_source
@ -111,7 +117,12 @@ RSpec.describe 'GitLab Markdown Benchmark', :aggregate_failures do
pipeline.filters.each do |filter_klass| pipeline.filters.each do |filter_klass|
label = filter_klass.name.demodulize.delete_suffix('Filter').truncate(20) label = filter_klass.name.demodulize.delete_suffix('Filter').truncate(20)
x.report(label) { filter_klass.call(filter_source[filter_klass], context) } x.report(label) do
filter = filter_klass.new(filter_source[filter_klass][:input_text],
context,
filter_source[filter_klass][:input_result])
filter.call
end
end end
x.compare! x.compare!

View file

@ -51,6 +51,25 @@ describe('init markdown', () => {
expect(textArea.value).toEqual(`${initialValue}- `); expect(textArea.value).toEqual(`${initialValue}- `);
}); });
it('inserts dollar signs correctly', () => {
const initialValue = '';
textArea.value = initialValue;
textArea.selectionStart = 0;
textArea.selectionEnd = 0;
insertMarkdownText({
textArea,
text: textArea.value,
tag: '```suggestion:-0+0\n{text}\n```',
blockTag: true,
selected: '# Does not parse the `$` currently.',
wrap: false,
});
expect(textArea.value).toContain('# Does not parse the `$` currently.');
});
it('inserts the tag on a new line if the current one is not empty', () => { it('inserts the tag on a new line if the current one is not empty', () => {
const initialValue = 'some text'; const initialValue = 'some text';

View file

@ -68,7 +68,7 @@ exports[`Learn GitLab Design A renders correctly 1`] = `
class="learn-gitlab-section-card-header" class="learn-gitlab-section-card-header"
> >
<img <img
src="/assets/learn_gitlab/section_workspace.svg" src="workspace.svg"
/> />
<h2 <h2
@ -246,7 +246,7 @@ exports[`Learn GitLab Design A renders correctly 1`] = `
class="learn-gitlab-section-card-header" class="learn-gitlab-section-card-header"
> >
<img <img
src="/assets/learn_gitlab/section_plan.svg" src="plan.svg"
/> />
<h2 <h2
@ -324,7 +324,7 @@ exports[`Learn GitLab Design A renders correctly 1`] = `
class="learn-gitlab-section-card-header" class="learn-gitlab-section-card-header"
> >
<img <img
src="/assets/learn_gitlab/section_deploy.svg" src="deploy.svg"
/> />
<h2 <h2

View file

@ -11,7 +11,7 @@ exports[`Learn GitLab Section Card renders correctly 1`] = `
class="learn-gitlab-section-card-header" class="learn-gitlab-section-card-header"
> >
<img <img
src="/assets/learn_gitlab/section_workspace.svg" src="workspace.svg"
/> />
<h2 <h2

View file

@ -1,13 +1,13 @@
import { GlProgressBar } from '@gitlab/ui'; import { GlProgressBar } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import LearnGitlabA from '~/pages/projects/learn_gitlab/components/learn_gitlab_a.vue'; import LearnGitlabA from '~/pages/projects/learn_gitlab/components/learn_gitlab_a.vue';
import { testActions } from './mock_data'; import { testActions, testSections } from './mock_data';
describe('Learn GitLab Design A', () => { describe('Learn GitLab Design A', () => {
let wrapper; let wrapper;
const createWrapper = () => { const createWrapper = () => {
wrapper = mount(LearnGitlabA, { propsData: { actions: testActions } }); wrapper = mount(LearnGitlabA, { propsData: { actions: testActions, sections: testSections } });
}; };
beforeEach(() => { beforeEach(() => {

View file

@ -3,6 +3,7 @@ import LearnGitlabSectionCard from '~/pages/projects/learn_gitlab/components/lea
import { testActions } from './mock_data'; import { testActions } from './mock_data';
const defaultSection = 'workspace'; const defaultSection = 'workspace';
const testImage = 'workspace.svg';
describe('Learn GitLab Section Card', () => { describe('Learn GitLab Section Card', () => {
let wrapper; let wrapper;
@ -14,7 +15,7 @@ describe('Learn GitLab Section Card', () => {
const createWrapper = () => { const createWrapper = () => {
wrapper = shallowMount(LearnGitlabSectionCard, { wrapper = shallowMount(LearnGitlabSectionCard, {
propsData: { section: defaultSection, actions: testActions }, propsData: { section: defaultSection, actions: testActions, svg: testImage },
}); });
}; };

View file

@ -45,3 +45,15 @@ export const testActions = {
svg: 'http://example.com/images/illustration.svg', svg: 'http://example.com/images/illustration.svg',
}, },
}; };
export const testSections = {
workspace: {
svg: 'workspace.svg',
},
deploy: {
svg: 'deploy.svg',
},
plan: {
svg: 'plan.svg',
},
};

View file

@ -96,6 +96,17 @@ RSpec.describe LearnGitlabHelper do
end end
end end
describe '.onboarding_sections_data' do
subject(:sections) { helper.onboarding_sections_data }
it 'has the right keys' do
expect(sections.keys).to contain_exactly(:deploy, :plan, :workspace)
end
it 'has the svg' do
expect(sections.values.map { |section| section.keys }).to eq([[:svg]] * 3)
end
end
describe '.learn_gitlab_experiment_tracking_category' do describe '.learn_gitlab_experiment_tracking_category' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax

View file

@ -90,11 +90,6 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
context 'when quick actions are present' do context 'when quick actions are present' do
let(:label) { create(:label, project: project, title: 'label1') } let(:label) { create(:label, project: project, title: 'label1') }
let(:milestone) { create(:milestone, project: project) } let(:milestone) { create(:milestone, project: project) }
let!(:user) { create(:user, username: 'user1') }
before do
project.add_developer(user)
end
it 'applies quick action commands present on templates' do it 'applies quick action commands present on templates' do
file_content = %(Text from template \n/label ~#{label.title} \n/milestone %"#{milestone.name}"") file_content = %(Text from template \n/label ~#{label.title} \n/milestone %"#{milestone.name}"")

View file

@ -0,0 +1,27 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::MembersMenu do
let(:project) { build(:project) }
let(:user) { project.owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
subject { described_class.new(context) }
describe '#render?' do
context 'when user cannot access members' do
let(:user) { nil }
it 'returns false' do
expect(subject.render?).to eq false
end
end
context 'when user can access members' do
it 'returns true' do
expect(subject.render?).to eq true
end
end
end
end

View file

@ -0,0 +1,27 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::SnippetsMenu do
let(:project) { build(:project) }
let(:user) { project.owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
subject { described_class.new(context) }
describe '#render?' do
context 'when user cannot access snippets' do
let(:user) { nil }
it 'returns false' do
expect(subject.render?).to eq false
end
end
context 'when user can access snippets' do
it 'returns true' do
expect(subject.render?).to eq true
end
end
end
end

View file

@ -4491,18 +4491,4 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
.not_to exceed_query_limit(control_count) .not_to exceed_query_limit(control_count)
end end
end end
describe '#has_downloadable_artifacts?' do
it 'returns false when when pipeline does not have downloadable artifacts' do
pipeline = create(:ci_pipeline, :success)
expect(pipeline.has_downloadable_artifacts?). to eq(false)
end
it 'returns false when when pipeline does not have downloadable artifacts' do
pipeline = create(:ci_pipeline, :with_codequality_reports)
expect(pipeline.has_downloadable_artifacts?). to eq(true)
end
end
end end

View file

@ -32,7 +32,7 @@ RSpec.describe PipelineDetailsEntity do
expect(subject[:details]) expect(subject[:details])
.to include :duration, :finished_at .to include :duration, :finished_at
expect(subject[:details]) expect(subject[:details])
.to include :stages, :artifacts, :has_downloadable_artifacts, :manual_actions, :scheduled_actions .to include :stages, :artifacts, :manual_actions, :scheduled_actions
expect(subject[:details][:status]).to include :icon, :favicon, :text, :label expect(subject[:details][:status]).to include :icon, :favicon, :text, :label
end end
@ -186,35 +186,5 @@ RSpec.describe PipelineDetailsEntity do
end end
it_behaves_like 'public artifacts' it_behaves_like 'public artifacts'
context 'when pipeline has downloadable artifacts' do
subject(:entity) { described_class.represent(pipeline, request: request, disable_artifacts: disable_artifacts).as_json }
let_it_be(:pipeline) { create(:ci_pipeline, :with_codequality_reports) }
context 'when disable_artifacts is true' do
subject(:entity) { described_class.represent(pipeline, request: request, disable_artifacts: true).as_json }
it 'excludes artifacts data' do
expect(entity[:details]).not_to include(:artifacts)
end
it 'returns true for has_downloadable_artifacts' do
expect(entity[:details][:has_downloadable_artifacts]).to eq(true)
end
end
context 'when disable_artifacts is false' do
subject(:entity) { described_class.represent(pipeline, request: request, disable_artifacts: false).as_json }
it 'includes artifacts data' do
expect(entity[:details]).to include(:artifacts)
end
it 'returns true for has_downloadable_artifacts' do
expect(entity[:details][:has_downloadable_artifacts]).to eq(true)
end
end
end
end end
end end

View file

@ -3,31 +3,35 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe IssueRebalancingService do RSpec.describe IssueRebalancingService do
let_it_be(:project) { create(:project) } let_it_be(:project, reload: true) { create(:project) }
let_it_be(:user) { project.creator } let_it_be(:user) { project.creator }
let_it_be(:start) { RelativePositioning::START_POSITION } let_it_be(:start) { RelativePositioning::START_POSITION }
let_it_be(:max_pos) { RelativePositioning::MAX_POSITION } let_it_be(:max_pos) { RelativePositioning::MAX_POSITION }
let_it_be(:min_pos) { RelativePositioning::MIN_POSITION } let_it_be(:min_pos) { RelativePositioning::MIN_POSITION }
let_it_be(:clump_size) { 300 } let_it_be(:clump_size) { 300 }
let_it_be(:unclumped) do let_it_be(:unclumped, reload: true) do
(0..clump_size).to_a.map do |i| (1..clump_size).to_a.map do |i|
create(:issue, project: project, author: user, relative_position: start + (1024 * i)) create(:issue, project: project, author: user, relative_position: start + (1024 * i))
end end
end end
let_it_be(:end_clump) do let_it_be(:end_clump, reload: true) do
(0..clump_size).to_a.map do |i| (1..clump_size).to_a.map do |i|
create(:issue, project: project, author: user, relative_position: max_pos - i) create(:issue, project: project, author: user, relative_position: max_pos - i)
end end
end end
let_it_be(:start_clump) do let_it_be(:start_clump, reload: true) do
(0..clump_size).to_a.map do |i| (1..clump_size).to_a.map do |i|
create(:issue, project: project, author: user, relative_position: min_pos + i) create(:issue, project: project, author: user, relative_position: min_pos + i)
end end
end end
before do
stub_feature_flags(issue_rebalancing_with_retry: false)
end
def issues_in_position_order def issues_in_position_order
project.reload.issues.reorder(relative_position: :asc).to_a project.reload.issues.reorder(relative_position: :asc).to_a
end end
@ -101,19 +105,70 @@ RSpec.describe IssueRebalancingService do
end end
end end
shared_examples 'rebalancing is retried on statement timeout exceptions' do
subject { described_class.new(project.issues.first) }
it 'retries update statement' do
call_count = 0
allow(subject).to receive(:run_update_query) do
call_count += 1
if call_count < 13
raise(ActiveRecord::QueryCanceled)
else
call_count = 0 if call_count == 13 + 16 # 16 = 17 sub-batches - 1 call that succeeded as part of 5th batch
true
end
end
# call math:
# batches start at 100 and are split in half after every 3 retries if ActiveRecord::StatementTimeout exception is raised.
# We raise ActiveRecord::StatementTimeout exception for 13 calls:
# 1. 100 => 3 calls
# 2. 100/2=50 => 3 calls + 3 above = 6 calls, raise ActiveRecord::StatementTimeout
# 3. 50/2=25 => 3 calls + 6 above = 9 calls, raise ActiveRecord::StatementTimeout
# 4. 25/2=12 => 3 calls + 9 above = 12 calls, raise ActiveRecord::StatementTimeout
# 5. 12/2=6 => 1 call + 12 above = 13 calls, run successfully
#
# so out of 100 elements we created batches of 6 items => 100/6 = 17 sub-batches of 6 or less elements
#
# project.issues.count: 900 issues, so 9 batches of 100 => 9 * (13+16) = 261
expect(subject).to receive(:update_positions).exactly(261).times.and_call_original
subject.execute
end
end
context 'when issue_rebalancing_optimization feature flag is on' do context 'when issue_rebalancing_optimization feature flag is on' do
before do before do
stub_feature_flags(issue_rebalancing_optimization: true) stub_feature_flags(issue_rebalancing_optimization: true)
end end
it_behaves_like 'IssueRebalancingService shared examples' it_behaves_like 'IssueRebalancingService shared examples'
context 'when issue_rebalancing_with_retry feature flag is on' do
before do
stub_feature_flags(issue_rebalancing_with_retry: true)
end
it_behaves_like 'IssueRebalancingService shared examples'
it_behaves_like 'rebalancing is retried on statement timeout exceptions'
end
end end
context 'when issue_rebalancing_optimization feature flag is on' do context 'when issue_rebalancing_optimization feature flag is off' do
before do before do
stub_feature_flags(issue_rebalancing_optimization: false) stub_feature_flags(issue_rebalancing_optimization: false)
end end
it_behaves_like 'IssueRebalancingService shared examples' it_behaves_like 'IssueRebalancingService shared examples'
context 'when issue_rebalancing_with_retry feature flag is on' do
before do
stub_feature_flags(issue_rebalancing_with_retry: true)
end
it_behaves_like 'IssueRebalancingService shared examples'
it_behaves_like 'rebalancing is retried on statement timeout exceptions'
end
end end
end end

View file

@ -863,6 +863,46 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end end
end end
describe 'Snippets' do
before do
render
end
context 'when user can access snippets' do
it 'shows Snippets link' do
expect(rendered).to have_link('Snippets', href: project_snippets_path(project))
end
end
context 'when user cannot access snippets' do
let(:user) { nil }
it 'does not show Snippets link' do
expect(rendered).not_to have_link('Snippets')
end
end
end
describe 'Members' do
before do
render
end
context 'when user can access members' do
it 'show Members link' do
expect(rendered).to have_link('Members', href: project_project_members_path(project))
end
end
context 'when user cannot access members' do
let(:user) { nil }
it 'show Members link' do
expect(rendered).not_to have_link('Members')
end
end
end
describe 'operations settings tab' do describe 'operations settings tab' do
describe 'archive projects' do describe 'archive projects' do
before do before do

View file

@ -30,5 +30,11 @@ module Mail
def deliver!(mail) def deliver!(mail)
@pool.with { |conn| conn.deliver!(mail) } @pool.with { |conn| conn.deliver!(mail) }
end end
# This makes it compatible with Mail's `#deliver!` method
# https://github.com/mikel/mail/blob/22a7afc23f253319965bf9228a0a430eec94e06d/lib/mail/message.rb#L271
def settings
{}
end
end end
end end

View file

@ -64,5 +64,27 @@ describe Mail::SMTPPool do
expect(MockSMTP.deliveries.size).to eq(1) expect(MockSMTP.deliveries.size).to eq(1)
end end
context 'when called from Mail:Message' do
before do
mail.delivery_method(described_class, { pool: described_class.create_pool })
end
describe '#deliver' do
it 'delivers mail' do
mail.deliver
expect(MockSMTP.deliveries.size).to eq(1)
end
end
describe '#deliver!' do
it 'delivers mail' do
mail.deliver!
expect(MockSMTP.deliveries.size).to eq(1)
end
end
end
end end
end end