Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-04-10 00:09:11 +00:00
parent 017841e3c0
commit 89cb3fa774
35 changed files with 715 additions and 29 deletions

View file

@ -102,6 +102,7 @@ include:
- local: .gitlab/ci/qa.gitlab-ci.yml
- local: .gitlab/ci/reports.gitlab-ci.yml
- local: .gitlab/ci/rails.gitlab-ci.yml
- local: .gitlab/ci/vendored-gems.gitlab-ci.yml
- local: .gitlab/ci/review.gitlab-ci.yml
- local: .gitlab/ci/rules.gitlab-ci.yml
- local: .gitlab/ci/setup.gitlab-ci.yml

View file

@ -920,6 +920,16 @@
- <<: *if-merge-request
changes: [".gitlab/ci/rails.gitlab-ci.yml"]
#######################
# Vendored gems rules #
#######################
.vendor:rules:mail-smtp_pool:
rules:
- <<: *if-merge-request
changes: ["vendor/gems/mail-smtp_pool/**/*"]
- <<: *if-merge-request-title-run-all-rspec
##################
# Releases rules #
##################

View file

@ -0,0 +1,7 @@
vendor mail-smtp_pool:
extends:
- .vendor:rules:mail-smtp_pool
needs: []
trigger:
include: vendor/gems/mail-smtp_pool/.gitlab-ci.yml
strategy: depend

View file

@ -511,6 +511,7 @@ gem 'erubi', '~> 1.9.0'
# Monkey-patched in `config/initializers/mail_encoding_patch.rb`
# See https://gitlab.com/gitlab-org/gitlab/issues/197386
gem 'mail', '= 2.7.1'
gem 'mail-smtp_pool', '~> 0.1.0', path: 'vendor/gems/mail-smtp_pool', require: false
# File encryption
gem 'lockbox', '~> 0.6.2'

View file

@ -1,3 +1,10 @@
PATH
remote: vendor/gems/mail-smtp_pool
specs:
mail-smtp_pool (0.1.0)
connection_pool (~> 2.0)
mail (~> 2.7)
PATH
remote: vendor/shims/mimemagic
specs:
@ -1483,6 +1490,7 @@ DEPENDENCIES
loofah (~> 2.2)
lru_redux
mail (= 2.7.1)
mail-smtp_pool (~> 0.1.0)!
marginalia (~> 1.10.0)
memory_profiler (~> 0.9)
method_source (~> 1.0)

View file

@ -7,6 +7,7 @@ import {
GlFormCombobox,
GlFormGroup,
GlFormSelect,
GlFormInput,
GlFormTextarea,
GlIcon,
GlLink,
@ -41,6 +42,7 @@ export default {
GlFormCombobox,
GlFormGroup,
GlFormSelect,
GlFormInput,
GlFormTextarea,
GlIcon,
GlLink,
@ -128,6 +130,12 @@ export default {
return true;
},
scopedVariablesEnabled() {
return !this.isGroup || this.glFeatures.scopedGroupVariables;
},
scopedVariablesAvailable() {
return !this.isGroup || this.glFeatures.groupScopedCiVariables;
},
variableValidationFeedback() {
return `${this.tokenValidationFeedback} ${this.maskedFeedback}`;
},
@ -226,24 +234,27 @@ export default {
:label="__('Type')"
label-for="ci-variable-type"
class="w-50 gl-mr-5"
:class="{ 'w-100': isGroup }"
:class="{ 'w-100': !scopedVariablesEnabled }"
>
<gl-form-select id="ci-variable-type" v-model="variable_type" :options="typeOptions" />
</gl-form-group>
<gl-form-group
v-if="!isGroup"
v-if="scopedVariablesEnabled"
:label="__('Environment scope')"
label-for="ci-variable-env"
class="w-50"
data-testid="environment-scope"
>
<ci-environments-dropdown
v-if="scopedVariablesAvailable"
class="w-100"
:value="environment_scope"
@selectEnvironment="setEnvironmentScope"
@createClicked="addWildCardScope"
/>
<gl-form-input v-else v-model="environment_scope" class="w-100" readonly />
</gl-form-group>
</div>

View file

@ -2,6 +2,7 @@
import { GlTable, GlButton, GlModalDirective, GlIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { s__, __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ADD_CI_VARIABLE_MODAL_ID } from '../constants';
import CiVariablePopover from './ci_variable_popover.vue';
@ -59,6 +60,7 @@ export default {
directives: {
GlModalDirective,
},
mixins: [glFeatureFlagsMixin()],
computed: {
...mapState(['variables', 'valuesHidden', 'isGroup', 'isLoading', 'isDeleting']),
valuesButtonText() {
@ -68,7 +70,7 @@ export default {
return this.variables && this.variables.length > 0;
},
fields() {
if (this.isGroup) {
if (this.isGroup && !this.glFeatures.scopedGroupVariables) {
return this.$options.fields.filter((field) => field.key !== 'environment_scope');
}
return this.$options.fields;

View file

@ -1,12 +1,15 @@
import Vue from 'vue';
import ContributorsGraphs from './components/contributors.vue';
import store from './stores';
import { createStore } from './stores';
export default () => {
const el = document.querySelector('.js-contributors-graph');
if (!el) return null;
const { projectGraphPath, projectBranch, defaultBranch } = el.dataset;
const store = createStore(defaultBranch);
return new Vue({
el,
store,
@ -14,8 +17,8 @@ export default () => {
render(createElement) {
return createElement(ContributorsGraphs, {
props: {
endpoint: el.dataset.projectGraphPath,
branch: el.dataset.projectBranch,
endpoint: projectGraphPath,
branch: projectBranch,
},
});
},

View file

@ -7,12 +7,12 @@ import state from './state';
Vue.use(Vuex);
export const createStore = () =>
export const createStore = (defaultBranch) =>
new Vuex.Store({
actions,
mutations,
getters,
state: state(),
state: state(defaultBranch),
});
export default createStore();
export default createStore;

View file

@ -1,5 +1,5 @@
export default () => ({
export default (branch) => ({
loading: false,
chartData: null,
branch: 'master',
branch,
});

View file

@ -58,9 +58,13 @@ export default {
<span v-if="request.hasWarnings">(!)</span>
</option>
</select>
<span v-if="requestsWithWarnings.length">
<span v-if="requestsWithWarnings.length" class="gl-cursor-default">
<span id="performance-bar-request-selector-warning" v-html="glEmojiTag('warning')"></span>
<gl-popover target="performance-bar-request-selector-warning" :content="warningMessage" />
<gl-popover
placement="bottom"
target="performance-bar-request-selector-warning"
:content="warningMessage"
/>
</span>
</div>
</template>

View file

@ -35,8 +35,8 @@ export default {
};
</script>
<template>
<span v-if="hasWarnings">
<span v-if="hasWarnings" class="gl-cursor-default">
<span :id="htmlId" v-html="glEmojiTag('warning')"></span>
<gl-popover :target="htmlId" :content="warningMessage" />
<gl-popover placement="bottom" :target="htmlId" :content="warningMessage" />
</span>
</template>

View file

@ -10,6 +10,8 @@ module Groups
before_action :authorize_admin_group!
before_action :authorize_update_max_artifacts_size!, only: [:update]
before_action :define_variables, only: [:show]
before_action :push_feature_flags, only: [:show]
before_action :push_licensed_features, only: [:show]
feature_category :continuous_integration
@ -91,6 +93,16 @@ module Groups
def update_group_params
params.require(:group).permit(:max_artifacts_size)
end
def push_feature_flags
push_frontend_feature_flag(:scoped_group_variables, group)
end
# Overridden in EE
def push_licensed_features
end
end
end
end
Groups::Settings::CiCdController.prepend_if_ee('EE::Groups::Settings::CiCdController')

View file

@ -56,3 +56,5 @@ module Groups
end
end
end
Groups::VariablesController.prepend_if_ee('EE::Groups::VariablesController')

View file

@ -86,7 +86,7 @@ class Group < Namespace
validate :visibility_level_allowed_by_sub_groups
validate :visibility_level_allowed_by_parent
validate :two_factor_authentication_allowed
validates :variables, nested_attributes_duplicates: true
validates :variables, nested_attributes_duplicates: { scope: :environment_scope }
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }

View file

@ -2,5 +2,6 @@
module Ci
class GroupVariableEntity < Ci::BasicVariableEntity
expose :environment_scope
end
end

View file

@ -5,4 +5,4 @@
= render 'shared/ref_switcher', destination: 'graphs'
= link_to s_('Commits|History'), project_commits_path(@project, current_ref), class: 'btn gl-button btn-default'
.js-contributors-graph{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json), 'data-project-branch': current_ref }
.js-contributors-graph{ class: container_class, data: { project_graph_path: project_graph_path(@project, current_ref, format: :json), project_branch: current_ref, default_branch: @project.default_branch } }

View file

@ -0,0 +1,5 @@
---
title: Add support for SMTP connection pooling when sending emails
merge_request: 57805
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Update popover placement and cursor on warning icon in PB
merge_request: 58552
author: Yogi (@yo)
type: changed

View file

@ -22,3 +22,28 @@ if Rails.env.production?
openssl_verify_mode: 'peer' # See ActionMailer documentation for other possible options
}
end
# To use an SMTP connection pool, uncomment the following section:
#
# require 'mail/smtp_pool'
#
# ActionMailer::Base.add_delivery_method :smtp_pool, Mail::SMTPPool
#
# if Rails.env.production?
# Rails.application.config.action_mailer.delivery_method = :smtp_pool
#
# ActionMailer::Base.delivery_method = :smtp_pool
# ActionMailer::Base.smtp_pool_settings = {
# pool: Mail::SMTPPool.create_pool(
# pool_size: Gitlab::Runtime.max_threads,
# address: "email.server.com",
# port: 465,
# user_name: "smtp",
# password: "123456",
# domain: "gitlab.company.com",
# authentication: :login,
# enable_starttls_auto: true,
# openssl_verify_mode: 'peer' # See ActionMailer documentation for other possible options
# )
# }
# end

View file

@ -20,7 +20,7 @@ Return all of the raw SQL queries used to compute usage ping.
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/usage_data/queries
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/usage_data/queries"
```
Sample response
@ -66,3 +66,51 @@ Sample response
"ci_runners": "SELECT COUNT(\"ci_runners\".\"id\") FROM \"ci_runners\"",
...
```
## UsageDataNonSqlMetrics API
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57050) in GitLab 13.11.
> - [Deployed behind a feature flag](../user/feature_flags.md), disabled by default.
Return all non-SQL metrics data used in the usage ping.
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/usage_data/non_sql_metrics"
```
Sample response:
```json
{
"recorded_at": "2021-03-26T07:04:03.724Z",
"uuid": null,
"hostname": "localhost",
"version": "13.11.0-pre",
"installation_type": "gitlab-development-kit",
"active_user_count": -3,
"edition": "EE",
"license_md5": "bb8cd0d8a6d9569ff3f70b8927a1f949",
"license_id": null,
"historical_max_users": 0,
"licensee": {
"Name": "John Doe1"
},
"license_user_count": null,
"license_starts_at": "1970-01-01",
"license_expires_at": "2022-02-26",
"license_plan": "starter",
"license_add_ons": {
"GitLab_FileLocks": 1,
"GitLab_Auditor_User": 1
},
"license_trial": null,
"license_subscription_id": "0000",
"license": {},
"settings": {
"ldap_encrypted_secrets_enabled": false,
"operating_system": "mac_os_x-11.2.2"
},
...
```

View file

@ -1,6 +1,7 @@
import { GlButton } from '@gitlab/ui';
import { GlButton, GlFormInput } from '@gitlab/ui';
import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
import CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue';
import { AWS_ACCESS_KEY_ID } from '~/ci_variable_list/constants';
import createStore from '~/ci_variable_list/store';
@ -15,7 +16,7 @@ describe('Ci variable modal', () => {
let store;
const createComponent = (method, options = {}) => {
store = createStore();
store = createStore({ isGroup: options.isGroup });
wrapper = method(CiVariableModal, {
attachTo: document.body,
stubs: {
@ -27,6 +28,7 @@ describe('Ci variable modal', () => {
});
};
const findCiEnvironmentsDropdown = () => wrapper.find(CiEnvironmentsDropdown);
const findModal = () => wrapper.find(ModalStub);
const findAddorUpdateButton = () =>
findModal()
@ -149,6 +151,61 @@ describe('Ci variable modal', () => {
});
});
describe('Environment scope', () => {
describe('group level variables', () => {
it('renders the environment dropdown', () => {
createComponent(shallowMount, {
isGroup: true,
provide: {
glFeatures: {
scopedGroupVariables: true,
groupScopedCiVariables: true,
},
},
});
expect(findCiEnvironmentsDropdown().exists()).toBe(true);
expect(findCiEnvironmentsDropdown().isVisible()).toBe(true);
});
describe('feature flag is disabled', () => {
it('hides the dropdown', () => {
createComponent(shallowMount, {
isGroup: true,
provide: {
glFeatures: {
scopedGroupVariables: false,
groupScopedCiVariables: true,
},
},
});
expect(findCiEnvironmentsDropdown().exists()).toBe(false);
});
});
describe('licensed feature is not available', () => {
it('disables the dropdown', () => {
createComponent(mount, {
isGroup: true,
provide: {
glFeatures: {
scopedGroupVariables: true,
groupScopedCiVariables: false,
},
},
});
const environmentScopeInput = wrapper
.find('[data-testid="environment-scope"]')
.find(GlFormInput);
expect(findCiEnvironmentsDropdown().exists()).toBe(false);
expect(environmentScopeInput.attributes('readonly')).toBe('readonly');
});
});
});
});
describe('Validations', () => {
const maskError = 'This variable can not be masked.';

View file

@ -1,4 +1,3 @@
import { GlTable } from '@gitlab/ui';
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import CiVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
@ -12,21 +11,24 @@ describe('Ci variable table', () => {
let wrapper;
let store;
const createComponent = () => {
store = createStore();
store.state.isGroup = true;
const createComponent = (isGroup = false, scopedGroupVariables = false) => {
store = createStore({ isGroup });
jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = mount(CiVariableTable, {
attachTo: document.body,
localVue,
store,
provide: {
glFeatures: {
scopedGroupVariables,
},
},
});
};
const findRevealButton = () => wrapper.find({ ref: 'secret-value-reveal-button' });
const findEditButton = () => wrapper.find({ ref: 'edit-ci-variable' });
const findEmptyVariablesPlaceholder = () => wrapper.find({ ref: 'empty-variables' });
const findTable = () => wrapper.find(GlTable);
beforeEach(() => {
createComponent();
@ -40,12 +42,14 @@ describe('Ci variable table', () => {
expect(store.dispatch).toHaveBeenCalledWith('fetchVariables');
});
it('fields prop does not contain environment_scope if group', () => {
expect(findTable().props('fields')).not.toEqual(
it('fields do not contain environment_scope if group level and feature is disabled', () => {
createComponent(true, false);
expect(wrapper.vm.fields).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
key: 'environment_scope',
label: 'Environment Scope',
label: 'Environments',
}),
]),
);

View file

@ -10,7 +10,7 @@ RSpec.describe Ci::GroupVariableEntity do
subject { entity.as_json }
it 'contains required fields' do
expect(subject).to include(:id, :key, :value, :protected, :variable_type)
expect(subject).to include(:id, :key, :value, :protected, :variable_type, :environment_scope)
end
end
end

3
vendor/gems/mail-smtp_pool/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
Gemfile.lock
*.gem
.bundle

View file

@ -0,0 +1,29 @@
workflow:
rules:
- if: $CI_MERGE_REQUEST_ID
.rspec:
cache:
key: mail-smtp_pool-ruby
paths:
- vendor/gems/mail-smtp_pool/vendor/ruby
before_script:
- cd vendor/gems/mail-smtp_pool
- ruby -v # Print out ruby version for debugging
- gem install bundler --no-document # Bundler is not installed with the image
- bundle config set --local path 'vendor' # Install dependencies into ./vendor/ruby
- bundle install -j $(nproc)
script:
- bundle exec rspec
rspec-2.6:
image: "ruby:2.6"
extends: .rspec
rspec-2.7:
image: "ruby:2.7"
extends: .rspec
rspec-3.0:
image: "ruby:3.0"
extends: .rspec

5
vendor/gems/mail-smtp_pool/Gemfile vendored Normal file
View file

@ -0,0 +1,5 @@
# frozen_string_literal: true
source 'https://rubygems.org'
gemspec

21
vendor/gems/mail-smtp_pool/LICENSE vendored Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016-2021 GitLab B.V.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

57
vendor/gems/mail-smtp_pool/README.md vendored Normal file
View file

@ -0,0 +1,57 @@
# Mail::SMTPPool
This gem is an extension to `Mail` that allows delivery of emails using an SMTP connection pool
## Installation
Add this line to your application's Gemfile:
```ruby
gem 'mail-smtp_pool'
```
And then execute:
```shell
bundle
```
Or install it yourself as:
```shell
gem install mail-smtp_pool
```
## Usage with ActionMailer
```ruby
# config/environments/development.rb
Rails.application.configure do
...
ActionMailer::Base.add_delivery_method :smtp_pool, Mail::SMTPPool
config.action_mailer.perform_deliveries = true
config.action_mailer.smtp_pool_settings = {
pool: Mail::SMTPPool.create_pool(
pool_size: 5,
pool_timeout: 5,
address: 'smtp.gmail.com',
port: 587,
domain: 'example.com',
user_name: '<username>',
password: '<password>',
authentication: 'plain',
enable_starttls_auto: true
)
}
end
```
Configuration options:
* `pool_size` - The maximum number of SMTP connections in the pool. Connections are created lazily as needed.
* `pool_timeout` - The number of seconds to wait for a connection in the pool to be available. A `Timeout::Error` exception is raised when this is exceeded.
This also accepts all options supported by `Mail::SMTP`. See https://www.rubydoc.info/gems/mail/2.6.1/Mail/SMTP for more information.

View file

@ -0,0 +1,34 @@
# frozen_string_literal: true
require 'connection_pool'
require 'mail/smtp_pool/connection'
module Mail
class SMTPPool
POOL_DEFAULTS = {
pool_size: 5,
pool_timeout: 5
}.freeze
class << self
def create_pool(settings = {})
pool_settings = POOL_DEFAULTS.merge(settings)
smtp_settings = settings.reject { |k, v| POOL_DEFAULTS.keys.include?(k) }
ConnectionPool.new(size: pool_settings[:pool_size], timeout: pool_settings[:pool_timeout]) do
Mail::SMTPPool::Connection.new(smtp_settings)
end
end
end
def initialize(settings)
raise ArgumentError, 'pool is required. You can create one using Mail::SMTPPool.create_pool.' if settings[:pool].nil?
@pool = settings[:pool]
end
def deliver!(mail)
@pool.with { |conn| conn.deliver!(mail) }
end
end
end

View file

@ -0,0 +1,60 @@
# frozen_string_literal: true
# A connection object that can be used to deliver mail.
#
# This is meant to be used in a pool so the main difference between this
# and Mail::SMTP is that this expects deliver! to be called multiple times.
#
# SMTP connection reset and error handling is handled by this class and
# the SMTP connection is not closed after a delivery.
require 'mail'
module Mail
class SMTPPool
class Connection < Mail::SMTP
def initialize(values)
super
@smtp_session = nil
end
def deliver!(mail)
response = Mail::SMTPConnection.new(connection: smtp_session, return_response: true).deliver!(mail)
settings[:return_response] ? response : self
end
def finish
finish_smtp_session if @smtp_session && @smtp_session.started?
end
private
def smtp_session
return start_smtp_session if @smtp_session.nil? || !@smtp_session.started?
return @smtp_session if reset_smtp_session
finish_smtp_session
start_smtp_session
end
def start_smtp_session
@smtp_session = build_smtp_session.start(settings[:domain], settings[:user_name], settings[:password], settings[:authentication])
end
def reset_smtp_session
!@smtp_session.instance_variable_get(:@error_occurred) && @smtp_session.rset.success?
rescue Net::SMTPError, IOError
false
end
def finish_smtp_session
@smtp_session.finish
rescue Net::SMTPError, IOError
ensure
@smtp_session = nil
end
end
end
end

View file

@ -0,0 +1,26 @@
# frozen_string_literal: true
lib = File.expand_path('lib', __dir__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
Gem::Specification.new do |spec|
spec.name = 'mail-smtp_pool'
spec.version = '0.1.0'
spec.authors = ['Heinrich Lee Yu']
spec.email = ['heinrich@gitlab.com']
spec.summary = 'Mail extension for sending using an SMTP connection pool'
spec.homepage = 'https://gitlab.com/gitlab-org/gitlab/-/tree/master/vendor/gems/mail-smtp_pool'
spec.metadata = { 'source_code_uri' => 'https://gitlab.com/gitlab-org/gitlab/-/tree/master/vendor/gems/mail-smtp_pool' }
spec.license = 'MIT'
spec.files = Dir['lib/**/*.rb']
spec.require_paths = ['lib']
# Please maintain alphabetical order for dependencies
spec.add_runtime_dependency 'connection_pool', '~> 2.0'
spec.add_runtime_dependency 'mail', '~> 2.7'
# Please maintain alphabetical order for dev dependencies
spec.add_development_dependency 'rspec', '~> 3.10.0'
end

View file

@ -0,0 +1,93 @@
# frozen_string_literal: true
require 'spec_helper'
describe Mail::SMTPPool::Connection do
let(:connection) { described_class.new({}) }
let(:mail) do
Mail.new do
from 'mikel@test.lindsaar.net'
to 'you@test.lindsaar.net'
subject 'This is a test email'
body 'Test body'
end
end
after do
MockSMTP.clear_deliveries
end
describe '#deliver!' do
it 'delivers mail using the same SMTP connection' do
mock_smtp = MockSMTP.new
expect(Net::SMTP).to receive(:new).once.and_return(mock_smtp)
expect(mock_smtp).to receive(:sendmail).twice.and_call_original
expect(mock_smtp).to receive(:rset).once.and_call_original
connection.deliver!(mail)
connection.deliver!(mail)
expect(MockSMTP.deliveries.size).to eq(2)
end
context 'when RSET fails' do
let(:mock_smtp) { MockSMTP.new }
let(:mock_smtp_2) { MockSMTP.new }
before do
expect(Net::SMTP).to receive(:new).twice.and_return(mock_smtp, mock_smtp_2)
end
context 'with an IOError' do
before do
expect(mock_smtp).to receive(:rset).once.and_raise(IOError)
end
it 'creates a new SMTP connection' do
expect(mock_smtp).to receive(:sendmail).once.and_call_original
expect(mock_smtp).to receive(:finish).once.and_call_original
expect(mock_smtp_2).to receive(:sendmail).once.and_call_original
connection.deliver!(mail)
connection.deliver!(mail)
expect(MockSMTP.deliveries.size).to eq(2)
end
end
context 'with an SMTP error' do
before do
expect(mock_smtp).to receive(:rset).once.and_raise(Net::SMTPServerBusy)
end
it 'creates a new SMTP connection' do
expect(mock_smtp).to receive(:sendmail).once.and_call_original
expect(mock_smtp).to receive(:finish).once.and_call_original
expect(mock_smtp_2).to receive(:sendmail).once.and_call_original
connection.deliver!(mail)
connection.deliver!(mail)
expect(MockSMTP.deliveries.size).to eq(2)
end
context 'and closing the old connection fails' do
before do
expect(mock_smtp).to receive(:finish).once.and_raise(IOError)
end
it 'creates a new SMTP connection' do
expect(mock_smtp).to receive(:sendmail).once.and_call_original
expect(mock_smtp_2).to receive(:sendmail).once.and_call_original
connection.deliver!(mail)
connection.deliver!(mail)
expect(MockSMTP.deliveries.size).to eq(2)
end
end
end
end
end
end

View file

@ -0,0 +1,68 @@
# frozen_string_literal: true
require 'spec_helper'
describe Mail::SMTPPool do
describe '.create_pool' do
it 'sets the default pool settings' do
expect(ConnectionPool).to receive(:new).with(size: 5, timeout: 5).once
described_class.create_pool
end
it 'allows overriding pool size and timeout' do
expect(ConnectionPool).to receive(:new).with(size: 3, timeout: 2).once
described_class.create_pool(pool_size: 3, pool_timeout: 2)
end
it 'creates an SMTP connection with the correct settings' do
settings = { address: 'smtp.example.com', port: '465' }
smtp_pool = described_class.create_pool(settings)
expect(Mail::SMTPPool::Connection).to receive(:new).with(settings).once.and_call_original
smtp_pool.checkout
end
end
describe '#initialize' do
it 'raises an error if a pool is not specified' do
expect { described_class.new({}) }.to raise_error(
ArgumentError, 'pool is required. You can create one using Mail::SMTPPool.create_pool.'
)
end
end
describe '#deliver!' do
let(:mail) do
Mail.new do
from 'mikel@test.lindsaar.net'
to 'you@test.lindsaar.net'
subject 'This is a test email'
body 'Test body'
end
end
after do
MockSMTP.clear_deliveries
end
it 'delivers mail using a connection from the pool' do
connection_pool = double(ConnectionPool)
connection = double(Mail::SMTPPool::Connection)
expect(connection_pool).to receive(:with).and_yield(connection)
expect(connection).to receive(:deliver!).with(mail)
described_class.new(pool: connection_pool).deliver!(mail)
end
it 'delivers mail' do
described_class.new(pool: described_class.create_pool).deliver!(mail)
expect(MockSMTP.deliveries.size).to eq(1)
end
end
end

View file

@ -0,0 +1,84 @@
# frozen_string_literal: true
require 'mail/smtp_pool'
# Original mockup from ActionMailer
# Based on https://github.com/mikel/mail/blob/22a7afc23f253319965bf9228a0a430eec94e06d/spec/spec_helper.rb#L74-L138
class MockSMTP
def self.deliveries
@@deliveries
end
def self.security
@@security
end
def initialize
@@deliveries = []
@@security = nil
@started = false
end
def sendmail(mail, from, to)
@@deliveries << [mail, from, to]
'OK'
end
def rset
Net::SMTP::Response.parse('250 OK')
end
def start(*args)
@started = true
if block_given?
result = yield(self)
@started = false
return result
else
return self
end
end
def started?
@started
end
def finish
@started = false
return true
end
def self.clear_deliveries
@@deliveries = []
end
def self.clear_security
@@security = nil
end
def enable_tls(context)
raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @@security && @@security != :enable_tls
@@security = :enable_tls
context
end
def enable_starttls(context = nil)
raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @@security == :enable_tls
@@security = :enable_starttls
context
end
def enable_starttls_auto(context)
raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @@security == :enable_tls
@@security = :enable_starttls_auto
context
end
end
class Net::SMTP
def self.new(*args)
MockSMTP.new
end
end