├── tmp
└── .gitkeep
├── .ruby-gemset
├── .ruby-version
├── spec
├── support
│ ├── fixtures
│ │ ├── err_log_empty
│ │ └── err_log_with_context
│ ├── config
│ │ ├── ffaker.rb
│ │ ├── bundler.rb
│ │ ├── pry.rb
│ │ └── simplecov.rb
│ ├── matchers
│ │ ├── have_status.rb
│ │ ├── match_semver_regex_pattern.rb
│ │ └── have_message_context.rb
│ └── helpers
│ │ ├── dependency.rb
│ │ ├── server.rb
│ │ ├── context_generator.rb
│ │ └── client.rb
├── smtp_mock
│ ├── version_spec.rb
│ ├── error
│ │ ├── server_spec.rb
│ │ ├── argument_spec.rb
│ │ └── dependency_spec.rb
│ ├── rspec_helper
│ │ ├── dependency_spec.rb
│ │ ├── server_spec.rb
│ │ ├── context_generator_spec.rb
│ │ └── client
│ │ │ └── smtp_client_spec.rb
│ ├── test_framework
│ │ └── rspec
│ │ │ ├── helper_spec.rb
│ │ │ └── interface_spec.rb
│ ├── server
│ │ ├── port_spec.rb
│ │ └── process_spec.rb
│ ├── dependency_spec.rb
│ ├── command_line_args_builder_spec.rb
│ ├── server_spec.rb
│ └── cli_spec.rb
├── spec_helper.rb
└── smtp_mock_spec.rb
├── .github
├── FUNDING.yml
├── DEVELOPMENT_ENVIRONMENT_GUIDE.md
├── ISSUE_TEMPLATE
│ ├── question.md
│ ├── bug_report.md
│ ├── issue_report.md
│ └── feature_request.md
├── BRANCH_NAMING_CONVENTION.md
└── PULL_REQUEST_TEMPLATE.md
├── .rspec
├── .circleci
├── linter_configs
│ ├── .bundler-audit.yml
│ ├── .fasterer.yml
│ ├── .yamllint.yml
│ ├── .markdownlint.yml
│ ├── .cspell.yml
│ ├── .commitspell.yml
│ ├── .lefthook.yml
│ └── .rubocop.yml
├── scripts
│ ├── set_publisher_credentials.sh
│ ├── commitspell.sh
│ ├── changeloglint.sh
│ └── release.sh
├── gemspecs
│ ├── compatible
│ └── latest
└── config.yml
├── lib
├── smtp_mock
│ ├── version.rb
│ ├── error
│ │ ├── server.rb
│ │ ├── argument.rb
│ │ └── dependency.rb
│ ├── test_framework
│ │ ├── rspec.rb
│ │ └── rspec
│ │ │ ├── helper.rb
│ │ │ └── interface.rb
│ ├── cli.rb
│ ├── core.rb
│ ├── server
│ │ ├── port.rb
│ │ └── process.rb
│ ├── dependency.rb
│ ├── server.rb
│ ├── cli
│ │ └── resolver.rb
│ └── command_line_args_builder.rb
└── smtp_mock.rb
├── bin
├── smtp_mock
├── setup
└── console
├── Gemfile
├── Rakefile
├── .gitignore
├── .codeclimate.yml
├── .reek.yml
├── LICENSE.txt
├── smtp_mock.gemspec
├── CONTRIBUTING.md
├── CODE_OF_CONDUCT.md
├── CHANGELOG.md
└── README.md
/tmp/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.ruby-gemset:
--------------------------------------------------------------------------------
1 | smtp_mock
2 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | ruby-2.5.0
2 |
--------------------------------------------------------------------------------
/spec/support/fixtures/err_log_empty:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | github: [bestwebua]
4 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --require spec_helper
2 | --format documentation
3 | --color
4 |
--------------------------------------------------------------------------------
/spec/support/fixtures/err_log_with_context:
--------------------------------------------------------------------------------
1 | Some error context here
2 |
--------------------------------------------------------------------------------
/.circleci/linter_configs/.bundler-audit.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | ignore:
4 | - EXA-MPLE-XXXX
5 |
--------------------------------------------------------------------------------
/spec/support/config/ffaker.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'ffaker'
4 |
--------------------------------------------------------------------------------
/.circleci/linter_configs/.fasterer.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | exclude_paths:
4 | - '.circleci/**/*.rb'
5 |
--------------------------------------------------------------------------------
/spec/support/config/bundler.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'bundler/setup'
4 |
--------------------------------------------------------------------------------
/lib/smtp_mock/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmtpMock
4 | VERSION = '1.4.4'
5 | end
6 |
--------------------------------------------------------------------------------
/spec/support/config/pry.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'pry' if ::RUBY_VERSION[/\A3\.3.+\z/]
4 |
--------------------------------------------------------------------------------
/.circleci/linter_configs/.yamllint.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | extends: default
4 |
5 | rules:
6 | line-length:
7 | max: 200
8 |
--------------------------------------------------------------------------------
/bin/smtp_mock:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require_relative '../lib/smtp_mock'
5 |
6 | SmtpMock::Cli.call(::ARGV)
7 |
--------------------------------------------------------------------------------
/spec/smtp_mock/version_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe SmtpMock::VERSION do
4 | it { is_expected.not_to be_nil }
5 | end
6 |
--------------------------------------------------------------------------------
/.circleci/linter_configs/.markdownlint.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | default: true
4 |
5 | MD013:
6 | line_length: 500
7 |
8 | MD024:
9 | siblings_only: true
10 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
5 | gemspec
6 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 |
6 | bundle install
7 |
8 | # Do any other automated setup that you need to do here
9 |
--------------------------------------------------------------------------------
/lib/smtp_mock/error/server.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmtpMock
4 | module Error
5 | Server = ::Class.new(::RuntimeError)
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/smtp_mock/error/argument.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmtpMock
4 | module Error
5 | Argument = ::Class.new(::ArgumentError)
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'bundler/gem_tasks'
4 | require 'rspec/core/rake_task'
5 |
6 | RSpec::Core::RakeTask.new(:spec)
7 |
8 | task default: :spec
9 |
--------------------------------------------------------------------------------
/spec/smtp_mock/error/server_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe SmtpMock::Error::Server do
4 | it { expect(described_class).to be < ::RuntimeError }
5 | end
6 |
--------------------------------------------------------------------------------
/spec/smtp_mock/error/argument_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe SmtpMock::Error::Argument do
4 | it { expect(described_class).to be < ::ArgumentError }
5 | end
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /_yardoc/
4 | /coverage/
5 | /doc/
6 | /pkg/
7 | /spec/reports/
8 | /tmp/err_log
9 | /bin/smtpmock
10 | .rspec_status
11 | .DS_Store
12 | Gemfile.lock
13 |
--------------------------------------------------------------------------------
/spec/support/matchers/have_status.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec::Matchers.define(:have_status) do |status|
4 | match { |net_smtp_instance| net_smtp_instance.status.to_i.eql?(status) }
5 | end
6 |
--------------------------------------------------------------------------------
/spec/support/matchers/match_semver_regex_pattern.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec::Matchers.define(:match_semver_regex_pattern) do
4 | match { |semver_string| /\d+\.\d+.\d+/.match?(semver_string) }
5 | end
6 |
--------------------------------------------------------------------------------
/spec/support/config/simplecov.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | if ::RUBY_VERSION[/\A3\.3.+\z/]
4 | require 'simplecov'
5 |
6 | SimpleCov.minimum_coverage(100)
7 | SimpleCov.start { add_filter(%r{\A/spec/}) }
8 | end
9 |
--------------------------------------------------------------------------------
/spec/support/matchers/have_message_context.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec::Matchers.define(:have_message_context) do |message|
4 | match { |net_smtp_instance| net_smtp_instance.message.strip.eql?(message) }
5 | end
6 |
--------------------------------------------------------------------------------
/.circleci/scripts/set_publisher_credentials.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 | set +x
4 | mkdir -p ~/.gem
5 |
6 | cat << EOF > ~/.gem/credentials
7 | ---
8 | :rubygems_api_key: ${RUBYGEMS_API_KEY}
9 | EOF
10 |
11 | chmod 0600 ~/.gem/credentials
12 | set -x
13 |
--------------------------------------------------------------------------------
/spec/support/helpers/dependency.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmtpMock
4 | module RspecHelper
5 | module Dependency
6 | def compose_command(command_line_args)
7 | SmtpMock::Dependency.compose_command(command_line_args)
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | checks:
4 | argument-count:
5 | enabled: false
6 | method-complexity:
7 | enabled: false
8 |
9 | plugins:
10 | rubocop:
11 | enabled: true
12 | channel: rubocop-1-67
13 | config:
14 | file: .circleci/linter_configs/.rubocop.yml
15 |
16 | reek:
17 | enabled: true
18 |
--------------------------------------------------------------------------------
/lib/smtp_mock/test_framework/rspec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'rspec/core'
4 | require_relative '../../smtp_mock'
5 | require_relative 'rspec/interface'
6 | require_relative 'rspec/helper'
7 |
8 | RSpec.configure do |config|
9 | config.after { SmtpMock::TestFramework::RSpec::Interface.stop_server! }
10 | end
11 |
--------------------------------------------------------------------------------
/lib/smtp_mock/test_framework/rspec/helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'interface'
4 |
5 | module SmtpMock
6 | module TestFramework
7 | module RSpec
8 | module Helper
9 | def smtp_mock_server(**options)
10 | SmtpMock::TestFramework::RSpec::Interface.start_server(**options)
11 | end
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/smtp_mock/error/dependency_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe SmtpMock::Error::Dependency do
4 | describe 'defined constants' do
5 | it { expect(described_class).to be_const_defined(:SMTPMOCK_NOT_INSTALLED) }
6 | it { expect(described_class).to be_const_defined(:SMTPMOCK_MIN_VERSION) }
7 | end
8 |
9 | it { expect(described_class).to be < ::RuntimeError }
10 | end
11 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require 'bundler/setup'
5 | require 'smtp_mock'
6 |
7 | # You can add fixtures and/or initialization code here to make experimenting
8 | # with your gem easier. You can also use a different console, if you like.
9 |
10 | # (If you use this, don't forget to add pry to your Gemfile!)
11 | # require "pry"
12 | # Pry.start
13 |
14 | require 'irb'
15 | IRB.start(__FILE__)
16 |
--------------------------------------------------------------------------------
/lib/smtp_mock/error/dependency.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmtpMock
4 | module Error
5 | class Dependency < ::RuntimeError
6 | SMTPMOCK_NOT_INSTALLED = 'smtpmock is required system dependency. Run `bundle exec smtp_mock -h` for details'
7 | SMTPMOCK_MIN_VERSION = "smtpmock #{SmtpMock::SMTPMOCK_MIN_VERSION} or higher is required. Run `bundle exec smtp_mock -g` for version upgrade"
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/smtp_mock/rspec_helper/dependency_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe SmtpMock::RspecHelper::Dependency, type: :helper do
4 | describe '#compose_command' do
5 | let(:command_line_args) { '-a -b 42' }
6 |
7 | it 'returns composed command' do
8 | expect(SmtpMock::Dependency).to receive(:compose_command).with(command_line_args).and_call_original
9 | compose_command(command_line_args)
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/smtp_mock.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'smtp_mock/core'
4 |
5 | module SmtpMock
6 | class << self
7 | def start_server(server = SmtpMock::Server, **options)
8 | server.new(**options)
9 | end
10 |
11 | def running_servers
12 | ::ObjectSpace.each_object(SmtpMock::Server).select(&:active?)
13 | end
14 |
15 | def stop_running_servers!
16 | running_servers.all?(&:stop!)
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/smtp_mock/cli.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmtpMock
4 | module Cli
5 | Command = ::Struct.new(:install_path, :sudo, :success, :message) do
6 | include SmtpMock::Cli::Resolver
7 | end
8 |
9 | def self.call(command_line_args, command = SmtpMock::Cli::Command)
10 | command.new.tap do |cmd|
11 | cmd.resolve(command_line_args)
12 | ::Kernel.puts(cmd.message)
13 | ::Kernel.exit(cmd.success ? 0 : 1)
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/.circleci/scripts/commitspell.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 |
4 | configuration=$(if [ "$2" = "" ]; then echo "$2"; else echo " $1 $2"; fi)
5 | latest_commit=$(git rev-parse HEAD)
6 |
7 | spellcheck_info() {
8 | echo "Checking the spelling of the latest commit ($latest_commit) message..."
9 | }
10 |
11 | compose_cspell_command() {
12 | echo "cspell-cli lint stdin$configuration"
13 | }
14 |
15 | cspell="$(compose_cspell_command)"
16 |
17 | spellcheck_latest_commit() {
18 | git log -1 --pretty=%B | $cspell
19 | }
20 |
21 | spellcheck_info
22 | spellcheck_latest_commit
23 |
--------------------------------------------------------------------------------
/spec/support/helpers/server.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmtpMock
4 | module RspecHelper
5 | module Server
6 | def create_fake_servers(active: 1, inactive: 1)
7 | server = ::Struct.new(:active?, :stop!)
8 | active_servers = ::Array.new(active) { server.new(true, true) }
9 | inactive_servers = ::Array.new(inactive) { server.new }
10 | (active_servers + inactive_servers).shuffle
11 | end
12 |
13 | def reset_err_log
14 | SmtpMock::Server::Process.instance_variable_set(:@err_log, nil)
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/.circleci/linter_configs/.cspell.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | enableGlobDot: true
4 |
5 | patterns:
6 | - name: GithubUser
7 | pattern: /\[@.+\]/gmx
8 | - name: MarkdownCode
9 | pattern: /`{1,3}.+`{1,3}/gmx
10 | - name: MarkdownCodeBlock
11 | pattern: /^\s*```[\s\S]*?^\s*```/gmx
12 |
13 | languageSettings:
14 | - languageId: markdown
15 | ignoreRegExpList:
16 | - Email
17 | - GithubUser
18 | - MarkdownCode
19 | - MarkdownCodeBlock
20 |
21 | words:
22 | - Commiting
23 | - PORO
24 | - Trotsenko
25 | - Vladislav
26 | - bestwebua
27 | - codebases
28 | - gemspecs
29 | - kwarg
30 |
--------------------------------------------------------------------------------
/.github/DEVELOPMENT_ENVIRONMENT_GUIDE.md:
--------------------------------------------------------------------------------
1 | # Development environment guide
2 |
3 | ## Preparing
4 |
5 | Clone `smtp_mock` repository:
6 |
7 | ```bash
8 | git clone https://github.com/mocktools/ruby-smtp-mock.git
9 | cd ruby-smtp-mock
10 | ```
11 |
12 | Configure latest Ruby environment:
13 |
14 | ```bash
15 | echo 'ruby-3.1.2' > .ruby-version
16 | cp .circleci/gemspec_latest smtp_mock.gemspec
17 | ```
18 |
19 | ## Commiting
20 |
21 | Commit your changes excluding `.ruby-version`, `smtp_mock.gemspec`
22 |
23 | ```bash
24 | git add . ':!.ruby-version' ':!smtp_mock.gemspec'
25 | git commit -m 'Your new awesome smtp_mock feature'
26 | ```
27 |
--------------------------------------------------------------------------------
/.circleci/linter_configs/.commitspell.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | enableGlobDot: true
4 |
5 | patterns:
6 | - name: GithubUser
7 | pattern: /\[@.+\]/gmx
8 |
9 | languageSettings:
10 | - languageId: markdown
11 | ignoreRegExpList:
12 | - Email
13 | - GithubUser
14 |
15 | words:
16 | - bagage
17 | - bagages
18 | - bestwebua
19 | - changeloglint
20 | - codebases
21 | - codeclimate
22 | - commitspell
23 | - ffaker
24 | - gemspecs
25 | - hostnames
26 | - lefthook
27 | - markdownlint
28 | - mocktools
29 | - mdlrc
30 | - punycode
31 | - rubocop
32 | - representer
33 | - rset
34 | - shortcuting
35 | - simplecov
36 | - simpleidn
37 | - stdlib
38 | - smtpmock
39 | - yamlint
40 |
--------------------------------------------------------------------------------
/lib/smtp_mock/core.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmtpMock
4 | SMTPMOCK_MIN_VERSION = '1.5.0'
5 |
6 | module Error
7 | require_relative '../smtp_mock/error/argument'
8 | require_relative '../smtp_mock/error/dependency'
9 | require_relative '../smtp_mock/error/server'
10 | end
11 |
12 | require_relative '../smtp_mock/version'
13 | require_relative '../smtp_mock/dependency'
14 | require_relative '../smtp_mock/command_line_args_builder'
15 | require_relative '../smtp_mock/cli/resolver'
16 | require_relative '../smtp_mock/cli'
17 | require_relative '../smtp_mock/server/port'
18 | require_relative '../smtp_mock/server/process'
19 | require_relative '../smtp_mock/server'
20 | end
21 |
--------------------------------------------------------------------------------
/lib/smtp_mock/server/port.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmtpMock
4 | class Server
5 | class Port
6 | require 'socket'
7 |
8 | LOCALHOST = '127.0.0.1'
9 | RANDOM_FREE_PORT = 0
10 |
11 | class << self
12 | def random_free_port
13 | server = ::TCPServer.new(SmtpMock::Server::Port::LOCALHOST, SmtpMock::Server::Port::RANDOM_FREE_PORT)
14 | port = server.addr[1]
15 | server.close
16 | port
17 | end
18 |
19 | def port_open?(port)
20 | !::TCPSocket.new(SmtpMock::Server::Port::LOCALHOST, port).close
21 | rescue ::Errno::ECONNREFUSED, ::Errno::EHOSTUNREACH
22 | false
23 | end
24 | end
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/smtp_mock/test_framework/rspec/interface.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmtpMock
4 | module TestFramework
5 | module RSpec
6 | module Interface
7 | class << self
8 | attr_reader :smtp_mock_server
9 |
10 | def start_server(**options)
11 | @smtp_mock_server ||= SmtpMock.start_server(**options) # rubocop:disable Naming/MemoizedInstanceVariableName
12 | end
13 |
14 | def clear_server!
15 | @smtp_mock_server = nil
16 | end
17 |
18 | def stop_server!
19 | return unless smtp_mock_server
20 |
21 | smtp_mock_server.stop!
22 | clear_server!
23 | true
24 | end
25 | end
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/.circleci/scripts/changeloglint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 |
4 | changelog=$(if [ "$1" = "" ]; then echo "CHANGELOG.md"; else echo "$1"; fi)
5 |
6 | get_current_gem_version() {
7 | ruby -r rubygems -e "puts Gem::Specification::load('$(ls -- *.gemspec)').version"
8 | }
9 |
10 | latest_changelog_tag() {
11 | grep -Po "(?<=\#\# \[)[0-9]+\.[0-9]+\.[0-9]+?(?=\])" "$changelog" | head -n 1
12 | }
13 |
14 | current_gem_version="$(get_current_gem_version)"
15 |
16 | if [ "$current_gem_version" = "$(latest_changelog_tag)" ]
17 | then
18 | echo "SUCCESS: Current gem version ($current_gem_version) has been found on the top of project changelog."
19 | else
20 | echo "FAILURE: Following to \"Keep a Changelog\" convention current gem version ($current_gem_version) must be mentioned on the top of project changelog."
21 | exit 1
22 | fi
23 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Question
3 | about: Ask your question to SmtpMock team
4 | title: "[QUESTION] Your question title here"
5 | labels: question
6 | assignees: bestwebua
7 |
8 | ---
9 |
10 |
11 |
12 | ### New question checklist
13 |
14 | - [ ] I have read the [Contribution Guidelines](https://github.com/mocktools/ruby-smtp-mock/blob/master/CONTRIBUTING.md)
15 | - [ ] I have read the [documentation](https://github.com/mocktools/ruby-smtp-mock/blob/master/README.md)
16 | - [ ] I have searched for [existing GitHub issues](https://github.com/mocktools/ruby-smtp-mock/issues)
17 |
18 |
19 |
20 | ### Question
21 |
22 |
23 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | rspec_custom = ::File.join(::File.dirname(__FILE__), 'support/**/*.rb')
4 | ::Dir[::File.expand_path(rspec_custom)].sort.each { |file| require file unless file[/\A.+_spec\.rb\z/] }
5 |
6 | require_relative '../lib/smtp_mock'
7 |
8 | RSpec.configure do |config|
9 | config.expect_with(:rspec) do |expectations|
10 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true
11 | expectations.syntax = :expect
12 | end
13 |
14 | config.mock_with(:rspec) do |mocks|
15 | mocks.verify_partial_doubles = true
16 | end
17 |
18 | config.example_status_persistence_file_path = '.rspec_status'
19 | config.disable_monkey_patching!
20 | config.order = :random
21 |
22 | config.include SmtpMock::RspecHelper::ContextGenerator
23 | config.include SmtpMock::RspecHelper::Dependency
24 | config.include SmtpMock::RspecHelper::Server
25 | config.include SmtpMock::RspecHelper::Client
26 |
27 | ::Kernel.srand(config.seed)
28 | end
29 |
--------------------------------------------------------------------------------
/spec/support/helpers/context_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmtpMock
4 | module RspecHelper
5 | module ContextGenerator
6 | def random_ip_v4_address
7 | ffaker.ip_v4_address
8 | end
9 |
10 | def random_hostname
11 | ffaker.domain_name
12 | end
13 |
14 | def random_email
15 | ffaker.email
16 | end
17 |
18 | def random_pid
19 | random.rand(1_000..2_000)
20 | end
21 |
22 | def random_port_number
23 | random.rand(49_152..65_535)
24 | end
25 |
26 | def random_signal
27 | random.rand(1..39)
28 | end
29 |
30 | def random_message
31 | FFaker::Lorem.sentence
32 | end
33 |
34 | def random_sem_version
35 | ::Array.new(3) { rand(0..42) }.join('.')
36 | end
37 |
38 | private
39 |
40 | def ffaker
41 | FFaker::Internet
42 | end
43 |
44 | def random
45 | ::Random
46 | end
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/.reek.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | detectors:
4 | IrresponsibleModule:
5 | enabled: false
6 |
7 | UtilityFunction:
8 | exclude:
9 | - SmtpMock::CommandLineArgsBuilder#to_camel_case
10 | - SmtpMock::Cli::Resolver#install_to
11 | - SmtpMock::ServerHelper
12 | - SmtpMock::TestFramework::RSpec::Helper#smtp_mock_server
13 | - SmtpMock::RspecHelper::ContextGenerator#random_message
14 | - SmtpMock::RspecHelper::Dependency#compose_command
15 | - SmtpMock::RspecHelper::Server#create_fake_servers
16 | - SmtpMock::RspecHelper::Server#reset_err_log
17 |
18 | NestedIterators:
19 | exclude:
20 | - SmtpMock::CommandLineArgsBuilder#define_attribute
21 |
22 | TooManyStatements:
23 | exclude:
24 | - SmtpMock::Server#run
25 | - SmtpMock::Cli::Resolver#install
26 | - SmtpMock::Cli::Resolver#resolve
27 | - SmtpMock::RspecHelper::Server#create_fake_servers
28 |
29 | TooManyInstanceVariables:
30 | exclude:
31 | - SmtpMock::Server
32 |
33 | LongParameterList:
34 | exclude:
35 | - SmtpMock::RspecHelper::Client#smtp_request
36 |
--------------------------------------------------------------------------------
/.github/BRANCH_NAMING_CONVENTION.md:
--------------------------------------------------------------------------------
1 | # Branch naming convention
2 |
3 | ## Branch naming
4 |
5 | > Please note for new pull requests create new branches from current `develop` branch only.
6 |
7 | Branch name should include type of your contribution and context. Please follow next pattern for naming your branches:
8 |
9 | ```bash
10 | feature/add-some-feature
11 | technical/some-technical-improvements
12 | bugfix/fix-some-bug-name
13 | ```
14 |
15 | ## Before PR actions
16 |
17 | ### Squash commits
18 |
19 | Please squash all branch commits into the one before opening your PR from your fork. It's simple to do with the git:
20 |
21 | ```bash
22 | git rebase -i [hash your first commit of your branch]~1
23 | git rebase -i 6467fe36232401fa740af067cfd8ac9ec932fed2~1 # example
24 | ```
25 |
26 | ### Add commit description
27 |
28 | Please complete your commit description following next pattern:
29 |
30 | ```code
31 | Technical/Add info files # should be the same name as your branch name
32 |
33 | * Added license, changelog, contributing, code of conduct docs
34 | * Added GitHub templates
35 | * Updated project license link
36 | ```
37 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2022-2024 Vladislav Trotsenko
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[BUG] Your bug report title here"
5 | labels: bug
6 | assignees: bestwebua
7 |
8 | ---
9 |
10 |
11 |
12 | ### New bug checklist
13 |
14 | - [ ] I have updated `dns_mock` to the latest version
15 | - [ ] I have read the [Contribution Guidelines](https://github.com/mocktools/ruby-smtp-mock/blob/master/CONTRIBUTING.md)
16 | - [ ] I have read the [documentation](https://github.com/mocktools/ruby-smtp-mock/blob/master/README.md)
17 | - [ ] I have searched for [existing GitHub issues](https://github.com/mocktools/ruby-smtp-mock/issues)
18 |
19 |
20 |
21 | ### Bug description
22 |
23 |
24 | ##### Complete output when running dns_mock, including the stack trace and command used
25 |
26 |
27 | [INSERT OUTPUT HERE]
28 |
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/issue_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Issue report
3 | about: Create a report to help us improve
4 | title: "[ISSUE] Your issue report title here"
5 | labels: ''
6 | assignees: bestwebua
7 |
8 | ---
9 |
10 |
11 |
12 | ### New issue checklist
13 |
14 | - [ ] I have updated `dns_mock` to the latest version
15 | - [ ] I have read the [Contribution Guidelines](https://github.com/mocktools/ruby-smtp-mock/blob/master/CONTRIBUTING.md)
16 | - [ ] I have read the [documentation](https://github.com/mocktools/ruby-smtp-mock/blob/master/README.md)
17 | - [ ] I have searched for [existing GitHub issues](https://github.com/mocktools/ruby-smtp-mock/issues)
18 |
19 |
20 |
21 | ### Issue description
22 |
23 |
24 | ##### Complete output when running dns_mock, including the stack trace and command used
25 |
26 |
27 | [INSERT OUTPUT HERE]
28 |
29 |
--------------------------------------------------------------------------------
/lib/smtp_mock/server/process.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmtpMock
4 | class Server
5 | class Process
6 | SIGNULL = 0
7 | SIGKILL = 9
8 | SIGTERM = 15
9 | TMP_LOG_PATH = '../../../tmp/err_log'
10 | WARMUP_DELAY = 0.1
11 |
12 | class << self
13 | def create(command)
14 | pid = ::Process.spawn(command, err: err_log)
15 | ::Kernel.sleep(SmtpMock::Server::Process::WARMUP_DELAY)
16 | error_context = ::IO.readlines(err_log)[0]
17 | raise SmtpMock::Error::Server, error_context.strip if error_context
18 | pid
19 | end
20 |
21 | def alive?(pid)
22 | ::Process.kill(SmtpMock::Server::Process::SIGNULL, pid)
23 | true
24 | rescue ::Errno::ESRCH
25 | false
26 | end
27 |
28 | def kill(signal_number, pid)
29 | ::Process.detach(pid)
30 | ::Process.kill(signal_number, pid)
31 | true
32 | rescue ::Errno::ESRCH
33 | false
34 | end
35 |
36 | private
37 |
38 | def err_log
39 | @err_log ||= ::File.expand_path(SmtpMock::Server::Process::TMP_LOG_PATH, ::File.dirname(__FILE__))
40 | end
41 | end
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/.circleci/linter_configs/.lefthook.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | no_tty: true
4 | skip_output:
5 | - meta
6 |
7 | commit-linters:
8 | commands:
9 | commitspell:
10 | run: .circleci/scripts/commitspell.sh -c '.circleci/linter_configs/.commitspell.yml'
11 |
12 | code-style-linters:
13 | commands:
14 | reek:
15 | run: bundle exec reek
16 | rubocop:
17 | run: bundle exec rubocop -c '.circleci/linter_configs/.rubocop.yml'
18 | shellcheck:
19 | glob: '*.{sh}'
20 | run: shellcheck --norc {all_files}
21 | yamllint:
22 | run: yamllint -c '.circleci/linter_configs/.yamllint.yml' .
23 |
24 | code-performance-linters:
25 | commands:
26 | fasterer:
27 | run: bundle exec fasterer
28 |
29 | code-vulnerability-linters:
30 | commands:
31 | bundle-audit:
32 | run: bundle exec bundle-audit check -c '.circleci/linter_configs/.bundler-audit.yml' --update
33 |
34 | code-documentation-linters:
35 | commands:
36 | cspell:
37 | run: cspell-cli lint -c '.circleci/linter_configs/.cspell.yml' '**/*.{txt,md}'
38 | markdownlint:
39 | run: markdownlint -c '.circleci/linter_configs/.markdownlint.yml' '**/*.md'
40 |
41 | release-linters:
42 | commands:
43 | changeloglint:
44 | run: .circleci/scripts/changeloglint.sh
45 |
--------------------------------------------------------------------------------
/.circleci/gemspecs/compatible:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'lib/smtp_mock/version'
4 |
5 | Gem::Specification.new do |spec|
6 | spec.name = 'smtp_mock'
7 | spec.version = SmtpMock::VERSION
8 | spec.authors = ['Vladislav Trotsenko']
9 | spec.email = %w[admin@bestweb.com.ua]
10 | spec.summary = %(💎 Ruby SMTP mock. Mimic any SMTP server behavior for your test environment)
11 | spec.description = %(💎 Ruby SMTP mock. Mimic any SMTP server behavior for your test environment.)
12 | spec.homepage = 'https://github.com/mocktools/ruby-smtp-mock'
13 | spec.license = 'MIT'
14 |
15 | current_ruby_version = ::Gem::Version.new(::RUBY_VERSION)
16 | dry_struct_version = current_ruby_version >= ::Gem::Version.new('2.7.0') ? '~> 1.6' : '~> 1.4'
17 |
18 | spec.required_ruby_version = '>= 2.5.0'
19 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20 | spec.executables = %w[smtp_mock]
21 | spec.require_paths = %w[lib]
22 |
23 | spec.add_runtime_dependency 'dry-struct', dry_struct_version
24 |
25 | spec.add_development_dependency 'ffaker'
26 | spec.add_development_dependency 'net-smtp' if current_ruby_version >= ::Gem::Version.new('3.1.0')
27 | spec.add_development_dependency 'rake'
28 | spec.add_development_dependency 'rspec'
29 | end
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for SmtpMock
4 | title: "[FEATURE] Your feature request title here"
5 | labels: enhancement
6 | assignees: bestwebua
7 |
8 | ---
9 |
10 |
11 |
12 | ### New feature request checklist
13 |
14 | - [ ] I have updated `dns_mock` to the latest version
15 | - [ ] I have read the [Contribution Guidelines](https://github.com/mocktools/ruby-smtp-mock/blob/master/CONTRIBUTING.md)
16 | - [ ] I have read the [documentation](https://github.com/mocktools/ruby-smtp-mock/blob/master/README.md)
17 | - [ ] I have searched for [existing GitHub issues](https://github.com/mocktools/ruby-smtp-mock/issues)
18 |
19 |
20 |
21 | ### Feature description
22 |
23 |
28 |
--------------------------------------------------------------------------------
/spec/smtp_mock/test_framework/rspec/helper_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative '../../../../lib/smtp_mock/test_framework/rspec/helper'
4 |
5 | class TestClass
6 | include SmtpMock::TestFramework::RSpec::Helper
7 | end
8 |
9 | RSpec.describe SmtpMock::TestFramework::RSpec::Helper do
10 | let(:test_class_instance) { TestClass.new }
11 |
12 | describe '.smtp_mock_server' do
13 | let(:smtp_mock_server_instance) { instance_double('SmtpMockServerInstance') }
14 |
15 | context 'with kwargs' do
16 | subject(:helper) { test_class_instance.smtp_mock_server(**options) }
17 |
18 | let(:host) { random_ip_v4_address }
19 | let(:port) { random_port_number }
20 | let(:options) { { host: host, port: port } }
21 |
22 | it do
23 | expect(SmtpMock::TestFramework::RSpec::Interface)
24 | .to receive(:start_server)
25 | .with(**options)
26 | .and_return(smtp_mock_server_instance)
27 | expect(helper).to eq(smtp_mock_server_instance)
28 | end
29 | end
30 |
31 | context 'without kwargs' do
32 | subject(:helper) { test_class_instance.smtp_mock_server }
33 |
34 | it do
35 | expect(SmtpMock::TestFramework::RSpec::Interface)
36 | .to receive(:start_server)
37 | .and_return(smtp_mock_server_instance)
38 | expect(helper).to eq(smtp_mock_server_instance)
39 | end
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/spec/smtp_mock/rspec_helper/server_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe SmtpMock::RspecHelper::Server, type: :helper do
4 | describe '#create_fake_servers' do
5 | subject(:fake_servers) { create_fake_servers(**options) }
6 |
7 | context 'when active 1, inactive 0' do
8 | let(:options) { { inactive: 0 } }
9 |
10 | it 'returns array with one active fake server' do
11 | expect(fake_servers.size).to eq(1)
12 | server = fake_servers.first
13 | expect(server.active?).to be(true)
14 | expect(server.stop!).to be(true)
15 | end
16 | end
17 |
18 | context 'when active 0, inactive 0' do
19 | let(:options) { { active: 0, inactive: 0 } }
20 |
21 | it 'returns empty array' do
22 | expect(fake_servers).to be_empty
23 | end
24 | end
25 |
26 | context 'when active 0, inactive 1' do
27 | let(:options) { { active: 0 } }
28 |
29 | it 'returns array with one active fake server' do
30 | expect(fake_servers.size).to eq(1)
31 | server = fake_servers.first
32 | expect(server.active?).to be_nil
33 | expect(server.stop!).to be_nil
34 | end
35 | end
36 | end
37 |
38 | describe '#reset_err_log' do
39 | it 'resets err_log instance variable' do
40 | expect(SmtpMock::Server::Process).to receive(:instance_variable_set).with(:@err_log, nil)
41 | reset_err_log
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/smtp_mock/dependency.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmtpMock
4 | module Dependency
5 | BINARY_SHORTCUT = 'smtpmock'
6 | SYMLINK = "/usr/local/bin/#{BINARY_SHORTCUT}"
7 | VERSION_REGEX_PATTERN = /#{BINARY_SHORTCUT}: (.+)/.freeze
8 |
9 | class << self
10 | def smtpmock_path_by_symlink
11 | ::Kernel.public_send(:`, "readlink #{SmtpMock::Dependency::SYMLINK}")
12 | end
13 |
14 | def smtpmock?
15 | !smtpmock_path_by_symlink.empty?
16 | end
17 |
18 | def verify_dependencies
19 | raise SmtpMock::Error::Dependency, SmtpMock::Error::Dependency::SMTPMOCK_NOT_INSTALLED unless smtpmock?
20 | current_version = version
21 | raise SmtpMock::Error::Dependency, SmtpMock::Error::Dependency::SMTPMOCK_MIN_VERSION unless minimal_version?(current_version)
22 | current_version
23 | end
24 |
25 | def compose_command(command_line_args)
26 | "#{SmtpMock::Dependency::BINARY_SHORTCUT} #{command_line_args}".strip
27 | end
28 |
29 | def version
30 | ::Kernel.public_send(
31 | :`,
32 | "#{SmtpMock::Dependency::BINARY_SHORTCUT} -v"
33 | )[SmtpMock::Dependency::VERSION_REGEX_PATTERN, 1]
34 | end
35 |
36 | private
37 |
38 | def minimal_version?(current_version)
39 | !!current_version && ::Gem::Version.new(current_version) >= ::Gem::Version.new(SmtpMock::SMTPMOCK_MIN_VERSION)
40 | end
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/spec/smtp_mock/server/port_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe SmtpMock::Server::Port do
4 | describe 'defined constants' do
5 | it { expect(described_class).to be_const_defined(:LOCALHOST) }
6 | it { expect(described_class).to be_const_defined(:RANDOM_FREE_PORT) }
7 | end
8 |
9 | describe '.random_free_port' do
10 | subject(:random_free_port) { described_class.random_free_port }
11 |
12 | it 'return random free port number' do
13 | expect(random_free_port).to be_an_instance_of(::Integer)
14 | expect(described_class.port_open?(random_free_port)).to be(false)
15 | end
16 | end
17 |
18 | describe '.port_open?' do
19 | subject(:port_open?) { described_class.port_open?(port) }
20 |
21 | let(:port) { random_port_number }
22 | let(:tcp_socket_instance) { instance_double('TcpSocketInstance', close: nil) }
23 |
24 | context 'when port is opened' do
25 | it do
26 | expect(::TCPSocket)
27 | .to receive(:new)
28 | .with(SmtpMock::Server::Port::LOCALHOST, port)
29 | .and_return(tcp_socket_instance)
30 | expect(port_open?).to be(true)
31 | end
32 | end
33 |
34 | context 'when port is closed' do
35 | [::Errno::ECONNREFUSED, ::Errno::EHOSTUNREACH].each do |error|
36 | it do
37 | expect(::TCPSocket)
38 | .to receive(:new)
39 | .with(SmtpMock::Server::Port::LOCALHOST, port)
40 | .and_raise(error)
41 | expect(port_open?).to be(false)
42 | end
43 | end
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/lib/smtp_mock/server.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmtpMock
4 | class Server
5 | attr_reader :pid, :port, :version
6 |
7 | def initialize( # rubocop:disable Metrics/ParameterLists
8 | deps_handler = SmtpMock::Dependency,
9 | port_checker = SmtpMock::Server::Port,
10 | args_builder = SmtpMock::CommandLineArgsBuilder,
11 | process = SmtpMock::Server::Process,
12 | **args
13 | )
14 | @version = deps_handler.verify_dependencies
15 | args[:port] = port_checker.random_free_port unless args.include?(:port)
16 | @command_line_args, @port = args_builder.call(**args), args[:port]
17 | @deps_handler, @port_checker, @process = deps_handler, port_checker, process
18 | run
19 | end
20 |
21 | def active?
22 | process_alive? && port_open?
23 | end
24 |
25 | def stop
26 | process_kill(SmtpMock::Server::Process::SIGTERM)
27 | end
28 |
29 | def stop!
30 | process_kill(SmtpMock::Server::Process::SIGKILL)
31 | end
32 |
33 | private
34 |
35 | attr_reader :deps_handler, :command_line_args, :port_checker, :process
36 | attr_writer :pid, :port
37 |
38 | def process_kill(signal_number)
39 | process.kill(signal_number, pid)
40 | end
41 |
42 | def compose_command
43 | deps_handler.compose_command(command_line_args)
44 | end
45 |
46 | def process_alive?
47 | process.alive?(pid)
48 | end
49 |
50 | def port_open?
51 | port_checker.port_open?(port)
52 | end
53 |
54 | def run
55 | self.pid = process.create(compose_command)
56 | ::Kernel.at_exit { stop! }
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/.circleci/gemspecs/latest:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'lib/smtp_mock/version'
4 |
5 | Gem::Specification.new do |spec|
6 | spec.name = 'smtp_mock'
7 | spec.version = SmtpMock::VERSION
8 | spec.authors = ['Vladislav Trotsenko']
9 | spec.email = %w[admin@bestweb.com.ua]
10 | spec.summary = %(💎 Ruby SMTP mock. Mimic any SMTP server behavior for your test environment)
11 | spec.description = %(💎 Ruby SMTP mock. Mimic any SMTP server behavior for your test environment.)
12 | spec.homepage = 'https://github.com/mocktools/ruby-smtp-mock'
13 | spec.license = 'MIT'
14 |
15 | spec.required_ruby_version = '>= 2.5.0'
16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17 | spec.executables = %w[smtp_mock]
18 | spec.require_paths = %w[lib]
19 |
20 | spec.add_runtime_dependency 'dry-struct', '~> 1.6'
21 |
22 | spec.add_development_dependency 'bundler-audit', '~> 0.9.2'
23 | spec.add_development_dependency 'fasterer', '~> 0.11.0'
24 | spec.add_development_dependency 'ffaker', '~> 2.23'
25 | spec.add_development_dependency 'net-smtp', '~> 0.5.0'
26 | spec.add_development_dependency 'pry-byebug', '~> 3.10', '>= 3.10.1'
27 | spec.add_development_dependency 'rake', '~> 13.2', '>= 13.2.1'
28 | spec.add_development_dependency 'reek', '~> 6.3'
29 | spec.add_development_dependency 'rspec', '~> 3.13'
30 | spec.add_development_dependency 'rubocop', '~> 1.67'
31 | spec.add_development_dependency 'rubocop-performance', '~> 1.22', '>= 1.22.1'
32 | spec.add_development_dependency 'rubocop-rspec', '~> 3.2'
33 | spec.add_development_dependency 'simplecov', '~> 0.22.0'
34 | end
35 |
--------------------------------------------------------------------------------
/spec/support/helpers/client.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmtpMock
4 | module RspecHelper
5 | module Client
6 | class SmtpClient
7 | require 'net/smtp'
8 |
9 | UNDEFINED_VERSION = '0.0.0'
10 |
11 | Error = ::Class.new(::StandardError)
12 |
13 | def initialize(host, port, net_class = ::Net::SMTP)
14 | @net_class = net_class
15 | @net_smtp_version = resolve_net_smtp_version
16 | @net_smtp = old_net_smtp? ? net_class.new(host, port) : net_class.new(host, port, tls_verify: false)
17 | end
18 |
19 | def start(helo_domain, &block)
20 | return net_smtp.start(helo_domain, &block) if net_smtp_version < '0.2.0'
21 | return net_smtp.start(helo_domain, tls_verify: false, &block) if old_net_smtp?
22 | net_smtp.start(helo: helo_domain, &block)
23 | end
24 |
25 | private
26 |
27 | attr_reader :net_class, :net_smtp_version, :net_smtp
28 |
29 | def resolve_net_smtp_version
30 | return net_class::VERSION if net_class.const_defined?(:VERSION)
31 | SmtpMock::RspecHelper::Client::SmtpClient::UNDEFINED_VERSION
32 | end
33 |
34 | def old_net_smtp?
35 | net_smtp_version < '0.3.0'
36 | end
37 | end
38 |
39 | def smtp_request(host:, port:, mailfrom:, rcptto:, message:, helo_domain: nil) # rubocop:disable Metrics/ParameterLists
40 | SmtpMock::RspecHelper::Client::SmtpClient.new(host, port).start(helo_domain) do |session|
41 | session.send_message(message, mailfrom, rcptto)
42 | rescue ::Net::SMTPFatalError => error
43 | raise SmtpMock::RspecHelper::Client::SmtpClient::Error, error.message.strip
44 | end
45 | end
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/spec/smtp_mock/rspec_helper/context_generator_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe SmtpMock::RspecHelper::ContextGenerator, type: :helper do
4 | describe '#random_ip_v4_address' do
5 | it 'returns random ip v4 address' do
6 | expect(FFaker::Internet).to receive(:ip_v4_address).and_call_original
7 | expect(random_ip_v4_address).to match(SmtpMock::CommandLineArgsBuilder::IP_ADDRESS_PATTERN)
8 | end
9 | end
10 |
11 | describe '#random_hostname' do
12 | it 'returns random hostname' do
13 | expect(FFaker::Internet).to receive(:domain_name).and_call_original
14 | expect(random_hostname).to be_an_instance_of(::String)
15 | end
16 | end
17 |
18 | describe '#random_email' do
19 | it 'returns random email' do
20 | expect(FFaker::Internet).to receive(:email).and_call_original
21 | expect(random_email).to match(/.+@.+/)
22 | end
23 | end
24 |
25 | describe '#random_pid' do
26 | it 'returns random pid' do
27 | expect(::Random).to receive(:rand).with(1_000..2_000).and_call_original
28 | expect(random_pid).to be_an_instance_of(::Integer)
29 | end
30 | end
31 |
32 | describe '#random_port_number' do
33 | it 'returns random port' do
34 | expect(::Random).to receive(:rand).with(49_152..65_535).and_call_original
35 | expect(random_port_number).to be_an_instance_of(::Integer)
36 | end
37 | end
38 |
39 | describe '#random_signal' do
40 | it 'returns random signal number' do
41 | expect(::Random).to receive(:rand).with(1..39).and_call_original
42 | expect(random_signal).to be_an_instance_of(::Integer)
43 | end
44 | end
45 |
46 | describe '#random_message' do
47 | it 'returns random message' do
48 | expect(::FFaker::Lorem).to receive(:sentence).and_call_original
49 | expect(random_message).to be_an_instance_of(::String)
50 | end
51 | end
52 |
53 | describe '#random_sem_version' do
54 | it 'returns random semantic version' do
55 | expect(random_sem_version).to match_semver_regex_pattern
56 | end
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/smtp_mock.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'lib/smtp_mock/version'
4 |
5 | Gem::Specification.new do |spec|
6 | spec.name = 'smtp_mock'
7 | spec.version = SmtpMock::VERSION
8 | spec.authors = ['Vladislav Trotsenko']
9 | spec.email = %w[admin@bestweb.com.ua]
10 |
11 | spec.summary = %(💎 Ruby SMTP mock. Mimic any SMTP server behavior for your test environment)
12 | spec.description = %(💎 Ruby SMTP mock. Mimic any SMTP server behavior for your test environment.)
13 |
14 | spec.homepage = 'https://github.com/mocktools/ruby-smtp-mock'
15 | spec.license = 'MIT'
16 |
17 | spec.metadata = {
18 | 'homepage_uri' => 'https://github.com/mocktools/ruby-smtp-mock',
19 | 'changelog_uri' => 'https://github.com/mocktools/ruby-smtp-mock/blob/master/CHANGELOG.md',
20 | 'source_code_uri' => 'https://github.com/mocktools/ruby-smtp-mock',
21 | 'documentation_uri' => 'https://github.com/mocktools/ruby-smtp-mock/blob/master/README.md',
22 | 'bug_tracker_uri' => 'https://github.com/mocktools/ruby-smtp-mock/issues'
23 | }
24 |
25 | current_ruby_version = ::Gem::Version.new(::RUBY_VERSION)
26 | dry_struct_version = current_ruby_version >= ::Gem::Version.new('2.7.0') ? '~> 1.6' : '~> 1.4'
27 | ffaker_version = current_ruby_version >= ::Gem::Version.new('3.0.0') ? '~> 2.23' : '~> 2.21'
28 |
29 | spec.required_ruby_version = '>= 2.5.0'
30 | spec.files = `git ls-files -z`.split("\x0").select { |f| f.match(%r{^(bin|lib|tmp)/|.ruby-version|smtp_mock.gemspec|LICENSE}) }
31 | spec.executables = %w[smtp_mock]
32 | spec.require_paths = %w[lib]
33 | spec.post_install_message = 'smtpmock is required system dependency. For more details run: `bundle exec smtp_mock -h`'
34 |
35 | spec.add_runtime_dependency 'dry-struct', dry_struct_version
36 |
37 | spec.add_development_dependency 'ffaker', ffaker_version
38 | spec.add_development_dependency 'net-smtp', '~> 0.5.0' if current_ruby_version >= ::Gem::Version.new('3.1.0')
39 | spec.add_development_dependency 'rake', '~> 13.2', '>= 13.2.1'
40 | spec.add_development_dependency 'rspec', '~> 3.13'
41 | end
42 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | # PR Details
2 |
3 |
4 |
5 |
6 |
7 |
8 | ## Description
9 |
10 |
11 |
12 | ## Related Issue
13 |
14 |
15 |
16 |
17 |
18 |
19 | ## Motivation and Context
20 |
21 |
22 |
23 | ## How Has This Been Tested
24 |
25 |
26 |
27 |
28 |
29 | ## Types of changes
30 |
31 |
32 |
33 | - [ ] Docs change / refactoring / dependency upgrade
34 | - [ ] Bug fix (non-breaking change which fixes an issue)
35 | - [ ] New feature (non-breaking change which adds functionality)
36 | - [ ] Breaking change (fix or feature that would cause existing functionality to change)
37 |
38 | ## Checklist
39 |
40 |
41 |
42 |
43 | - [ ] My code follows the code style of this project
44 | - [ ] My change requires a change to the documentation
45 | - [ ] I have updated the documentation accordingly
46 | - [ ] I have read the [**CONTRIBUTING** document](https://github.com/mocktools/ruby-smtp-mock/blob/master/CONTRIBUTING.md)
47 | - [ ] I have added tests to cover my changes
48 | - [ ] I have run `bundle exec rspec` from the root directory to see all new and existing tests pass
49 | - [ ] I have run `rubocop` and `reek` to ensure the code style is valid
50 |
--------------------------------------------------------------------------------
/.circleci/scripts/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 |
4 | GH_CLI_RELEASES_URL="https://github.com/cli/cli/releases"
5 | FILE_NAME="gh"
6 | BUILD_ARCHITECTURE="linux_amd64.deb"
7 | DELIMETER="_"
8 | PACKAGE_FILE="$FILE_NAME$DELIMETER$BUILD_ARCHITECTURE"
9 |
10 | gh_cli_latest_release() {
11 | curl -sL -o /dev/null -w '%{url_effective}' "$GH_CLI_RELEASES_URL/latest" | rev | cut -f 1 -d '/'| rev
12 | }
13 |
14 | download_gh_cli() {
15 | test -z "$VERSION" && VERSION="$(gh_cli_latest_release)"
16 | test -z "$VERSION" && {
17 | echo "Unable to get GitHub CLI release." >&2
18 | exit 1
19 | }
20 | curl -s -L -o "$PACKAGE_FILE" "$GH_CLI_RELEASES_URL/download/$VERSION/$FILE_NAME$DELIMETER$(printf '%s' "$VERSION" | cut -c 2-100)$DELIMETER$BUILD_ARCHITECTURE"
21 | }
22 |
23 | install_gh_cli() {
24 | sudo dpkg -i "$PACKAGE_FILE"
25 | rm "$PACKAGE_FILE"
26 | }
27 |
28 | get_release_candidate_version() {
29 | ruby -r rubygems -e "puts Gem::Specification::load('$(ls -- *.gemspec)').version"
30 | }
31 |
32 | release_candidate_tag="v$(get_release_candidate_version)"
33 |
34 | is_an_existing_github_release() {
35 | git fetch origin "refs/tags/$release_candidate_tag" >/dev/null 2>&1
36 | }
37 |
38 | release_to_rubygems() {
39 | echo "Setting RubyGems publisher credentials..."
40 | ./.circleci/scripts/set_publisher_credentials.sh
41 | echo "Preparation for release..."
42 | git config --global user.email "${PUBLISHER_EMAIL}"
43 | git config --global user.name "${PUBLISHER_NAME}"
44 | git stash
45 | gem install yard gem-ctags
46 | bundle install
47 | echo "Publishing new gem release to RubyGems..."
48 | rake release
49 | }
50 |
51 | release_to_github() {
52 | echo "Downloading and installing latest gh cli..."
53 | download_gh_cli
54 | install_gh_cli
55 | echo "Publishing new release notes to GitHub..."
56 | gh release create "$release_candidate_tag" --generate-notes
57 | }
58 |
59 | update_develop_branch() {
60 | echo "Updating develop branch with new release tag..."
61 | git checkout develop
62 | git merge "$release_candidate_tag" --ff --no-edit
63 | git push origin develop
64 | }
65 |
66 | if is_an_existing_github_release
67 | then echo "Tag $release_candidate_tag already exists on GitHub. Skipping releasing flow..."
68 | else release_to_rubygems; release_to_github; update_develop_branch
69 | fi
70 |
--------------------------------------------------------------------------------
/spec/smtp_mock/server/process_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe SmtpMock::Server::Process do
4 | let(:pid) { random_pid }
5 |
6 | describe 'defined constants' do
7 | it { expect(described_class).to be_const_defined(:SIGNULL) }
8 | it { expect(described_class).to be_const_defined(:SIGKILL) }
9 | it { expect(described_class).to be_const_defined(:SIGTERM) }
10 | it { expect(described_class).to be_const_defined(:TMP_LOG_PATH) }
11 | it { expect(described_class).to be_const_defined(:WARMUP_DELAY) }
12 | end
13 |
14 | describe '.create' do
15 | subject(:create_process) { described_class.create(command) }
16 |
17 | let(:command) { 'some_command -some_args' }
18 | let(:err_output) { described_class.send(:err_log) }
19 |
20 | before do
21 | reset_err_log
22 | stub_const('SmtpMock::Server::Process::TMP_LOG_PATH', err_output_path)
23 | allow(::Process).to receive(:spawn).with(command, err: err_output).and_return(pid)
24 | allow(::Kernel).to receive(:sleep).with(SmtpMock::Server::Process::WARMUP_DELAY)
25 | end
26 |
27 | after { reset_err_log }
28 |
29 | context 'when the error did not happen' do
30 | let(:err_output_path) { '../../../spec/support/fixtures/err_log_empty' }
31 |
32 | it 'creates background process, returns pid' do
33 | expect(create_process).to eq(pid)
34 | end
35 | end
36 |
37 | context 'when the error happened' do
38 | let(:err_output_path) { '../../../spec/support/fixtures/err_log_with_context' }
39 |
40 | it { expect { create_process }.to raise_error(SmtpMock::Error::Server, 'Some error context here') }
41 | end
42 | end
43 |
44 | describe '.alive?' do
45 | subject(:alive?) { described_class.alive?(pid) }
46 |
47 | context 'when existent pid' do
48 | it do
49 | expect(::Process).to receive(:kill).with(SmtpMock::Server::Process::SIGNULL, pid)
50 | expect(alive?).to be(true)
51 | end
52 | end
53 |
54 | context 'when non-existent pid' do
55 | it do
56 | expect(::Process).to receive(:kill).with(SmtpMock::Server::Process::SIGNULL, pid).and_raise(::Errno::ESRCH)
57 | expect(alive?).to be(false)
58 | end
59 | end
60 | end
61 |
62 | describe '.kill' do
63 | subject(:kill_process) { described_class.kill(signal_number, pid) }
64 |
65 | let(:signal_number) { random_signal }
66 |
67 | before { allow(::Process).to receive(:detach).with(pid) }
68 |
69 | context 'when existent pid' do
70 | it do
71 | expect(::Process).to receive(:kill).with(signal_number, pid)
72 | expect(kill_process).to be(true)
73 | end
74 | end
75 |
76 | context 'when non-existent pid' do
77 | it do
78 | expect(::Process).to receive(:kill).with(signal_number, pid).and_raise(::Errno::ESRCH)
79 | expect(kill_process).to be(false)
80 | end
81 | end
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/spec/smtp_mock/test_framework/rspec/interface_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative '../../../../lib/smtp_mock/test_framework/rspec/interface'
4 |
5 | RSpec.describe SmtpMock::TestFramework::RSpec::Interface do
6 | before { described_class.clear_server! }
7 |
8 | describe '.start_server' do
9 | let(:smtp_mock_server_instance) { instance_double('SmtpMockServerInstance') }
10 |
11 | context 'with kwargs' do
12 | subject(:start_server) { described_class.start_server(**options) }
13 |
14 | let(:host) { random_ip_v4_address }
15 | let(:port) { random_port_number }
16 | let(:options) { { host: host, port: port } }
17 |
18 | it do
19 | expect(SmtpMock).to receive(:start_server).with(**options).and_return(smtp_mock_server_instance)
20 | expect(start_server).to eq(smtp_mock_server_instance)
21 | end
22 | end
23 |
24 | context 'without kwargs' do
25 | subject(:start_server) { described_class.start_server }
26 |
27 | it do
28 | expect(SmtpMock).to receive(:start_server).and_return(smtp_mock_server_instance)
29 | expect(start_server).to eq(smtp_mock_server_instance)
30 | end
31 | end
32 | end
33 |
34 | describe '.smtp_mock_server' do
35 | subject(:smtp_mock_server) { described_class.smtp_mock_server }
36 |
37 | let(:smtp_mock_server_instance) { instance_double('SmtpMockServerInstance', stop!: true) }
38 |
39 | context 'when smtp mock server exists' do
40 | before do
41 | allow(SmtpMock).to receive(:start_server).and_return(smtp_mock_server_instance)
42 | described_class.start_server
43 | end
44 |
45 | it { is_expected.to eq(smtp_mock_server_instance) }
46 | end
47 |
48 | context 'when smtp mock server not exists' do
49 | it { is_expected.to be_nil }
50 | end
51 | end
52 |
53 | describe '.stop_server!' do
54 | subject(:stop_server) { described_class.stop_server! }
55 |
56 | let(:smtp_mock_server_instance) { instance_double('SmtpMockServerInstance', stop!: true) }
57 |
58 | context 'when smtp mock server exists' do
59 | before do
60 | allow(SmtpMock).to receive(:start_server).and_return(smtp_mock_server_instance)
61 | described_class.start_server
62 | end
63 |
64 | it do
65 | expect(smtp_mock_server_instance).to receive(:stop!)
66 | expect(described_class).to receive(:clear_server!)
67 | expect(stop_server).to be(true)
68 | end
69 | end
70 |
71 | context 'when smtp mock server not exists' do
72 | it do
73 | expect(smtp_mock_server_instance).not_to receive(:stop!)
74 | expect(described_class).not_to receive(:clear_server!)
75 | expect(stop_server).to be_nil
76 | end
77 | end
78 | end
79 |
80 | describe 'clear_server!' do
81 | subject(:clear_server) { described_class.clear_server! }
82 |
83 | it { is_expected.to be_nil }
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/lib/smtp_mock/cli/resolver.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmtpMock
4 | module Cli
5 | module Resolver
6 | require 'optparse'
7 |
8 | USE_CASE = 'Usage: smtp_mock [options], example: `bundle exec smtp_mock -s -i ~/existent_dir`'
9 | DOWNLOAD_SCRIPT = 'https://raw.githubusercontent.com/mocktools/go-smtp-mock/master/script/download.sh'
10 |
11 | def resolve(command_line_args) # rubocop:disable Metrics/AbcSize
12 | opt_parser = ::OptionParser.new do |parser|
13 | parser.banner = SmtpMock::Cli::Resolver::USE_CASE
14 | parser.on('-s', '--sudo', 'Run command as sudo') { self.sudo = true }
15 | parser.on('-iPATH', '--install=PATH', 'Install smtpmock to the existing path', &install)
16 | parser.on('-u', '--uninstall', 'Uninstall smtpmock', &uninstall)
17 | parser.on('-g', '--upgrade', 'Upgrade to latest version of smtpmock', &upgrade)
18 | parser.on('-v', '--version', 'Prints current smtpmock version', &version)
19 | parser.on('-h', '--help', 'Prints help') { self.message = parser.to_s }
20 |
21 | self.success = true
22 | end
23 |
24 | opt_parser.parse(command_line_args) # TODO: add error handler
25 | end
26 |
27 | private
28 |
29 | def install
30 | lambda do |argument|
31 | self.install_path = argument
32 | return self.message = 'smtpmock is already installed' if ::File.exist?(binary_path)
33 |
34 | install_to(install_path)
35 | ::Kernel.system("#{as_sudo}ln -s #{binary_path} #{SmtpMock::Dependency::SYMLINK}")
36 | self.message = 'smtpmock was installed successfully'
37 | end
38 | end
39 |
40 | def uninstall
41 | lambda do |_|
42 | return if not_installed?
43 |
44 | ::Kernel.system("#{as_sudo}unlink #{SmtpMock::Dependency::SYMLINK}")
45 | ::Kernel.system("rm #{current_smtpmock_path}")
46 | self.message = 'smtpmock was uninstalled successfully'
47 | end
48 | end
49 |
50 | def upgrade
51 | lambda do |_|
52 | return if not_installed?
53 |
54 | install_to(current_smtpmock_path[%r{(.+)/.+}, 1])
55 | self.message = 'smtpmock was upgraded successfully'
56 | end
57 | end
58 |
59 | def version
60 | lambda do |_|
61 | return if not_installed?
62 |
63 | self.message = SmtpMock::Dependency.version
64 | end
65 | end
66 |
67 | def binary_path
68 | "#{install_path}/smtpmock"
69 | end
70 |
71 | def install_to(install_path)
72 | ::Kernel.system("cd #{install_path} && curl -sL #{SmtpMock::Cli::Resolver::DOWNLOAD_SCRIPT} | bash")
73 | end
74 |
75 | def as_sudo
76 | 'sudo ' if sudo
77 | end
78 |
79 | def current_smtpmock_path
80 | @current_smtpmock_path ||= SmtpMock::Dependency.smtpmock_path_by_symlink
81 | end
82 |
83 | def not_installed?
84 | return false unless current_smtpmock_path.empty?
85 | self.message = 'smtpmock not installed yet'
86 | true
87 | end
88 | end
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/.circleci/linter_configs/.rubocop.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | require:
4 | - rubocop-rspec
5 | - rubocop-performance
6 |
7 | AllCops:
8 | DisplayCopNames: true
9 | DisplayStyleGuide: true
10 | TargetRubyVersion: 2.5
11 | SuggestExtensions: false
12 | NewCops: enable
13 |
14 | # Metrics ---------------------------------------------------------------------
15 |
16 | Metrics/ClassLength:
17 | Max: 150
18 |
19 | Metrics/MethodLength:
20 | Max: 15
21 |
22 | Metrics/BlockLength:
23 | Enabled: false
24 |
25 | Metrics/CyclomaticComplexity:
26 | Enabled: false
27 |
28 | Metrics/PerceivedComplexity:
29 | Enabled: false
30 |
31 | # Naming ----------------------------------------------------------------------
32 |
33 | Naming/VariableNumber:
34 | Enabled: false
35 |
36 | Naming/RescuedExceptionsVariableName:
37 | Enabled: false
38 |
39 | Naming/InclusiveLanguage:
40 | Enabled: false
41 |
42 | # Style -----------------------------------------------------------------------
43 |
44 | Style/Documentation:
45 | Enabled: false
46 |
47 | Style/DoubleNegation:
48 | Enabled: false
49 |
50 | Style/EmptyCaseCondition:
51 | Enabled: false
52 |
53 | Style/ParallelAssignment:
54 | Enabled: false
55 |
56 | Style/RescueStandardError:
57 | Enabled: false
58 |
59 | Style/RedundantConstantBase:
60 | Enabled: false
61 |
62 | # Layout ----------------------------------------------------------------------
63 |
64 | Layout/LineLength:
65 | Max: 150
66 |
67 | Layout/ClassStructure:
68 | Enabled: true
69 | Categories:
70 | module_inclusion:
71 | - include
72 | - prepend
73 | - extend
74 | ExpectedOrder:
75 | - module_inclusion
76 | - constants
77 | - public_class_methods
78 | - initializer
79 | - public_methods
80 | - protected_methods
81 | - private_methods
82 |
83 | Layout/EmptyLineAfterGuardClause:
84 | Enabled: false
85 |
86 | # Gemspec ---------------------------------------------------------------------
87 |
88 | Gemspec/RequireMFA:
89 | Enabled: false
90 |
91 | Gemspec/DevelopmentDependencies:
92 | Enabled: false
93 |
94 | Gemspec/AddRuntimeDependency:
95 | Enabled: false
96 |
97 | # Performance -----------------------------------------------------------------
98 |
99 | Performance/MethodObjectAsBlock:
100 | Enabled: false
101 |
102 | # RSpec -----------------------------------------------------------------------
103 |
104 | RSpec/ExampleLength:
105 | Enabled: false
106 |
107 | RSpec/NestedGroups:
108 | Enabled: false
109 |
110 | RSpec/MultipleExpectations:
111 | Enabled: false
112 |
113 | RSpec/MessageChain:
114 | Enabled: false
115 |
116 | RSpec/ContextWording:
117 | Enabled: false
118 |
119 | RSpec/AnyInstance:
120 | Enabled: false
121 |
122 | RSpec/MessageSpies:
123 | Enabled: false
124 |
125 | RSpec/MultipleDescribes:
126 | Enabled: false
127 |
128 | RSpec/MultipleMemoizedHelpers:
129 | Enabled: false
130 |
131 | RSpec/StubbedMock:
132 | Enabled: false
133 |
134 | RSpec/VerifiedDoubleReference:
135 | Enabled: false
136 |
137 | RSpec/StringAsInstanceDoubleConstant:
138 | Enabled: false
139 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to SmtpMock
2 |
3 | Please take a moment to review this document in order to make the contribution process easy and effective for everyone involved.
4 |
5 | Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue or assessing patches and features.
6 |
7 | ## Using the issue tracker
8 |
9 | The issue tracker is the preferred channel for [issue/bug reports](#issuebug-reports), [feature requests](#feature-requests), [questions](#questions) and submitting [pull requests](#pull-requests).
10 |
11 | ## Issue/bug reports
12 |
13 | A bug is a _demonstrable problem_ that is caused by the code in the repository. Good bug reports are extremely helpful - thank you!
14 |
15 | Guidelines for issue/bug reports:
16 |
17 | 1. **Use the GitHub issue search** — check if the issue has already been reported
18 | 2. **Check if the issue has been fixed** — try to reproduce it using the latest `master` or `develop` branch in the repository
19 | 3. SmtpMock [issue template](.github/ISSUE_TEMPLATE/issue_report.md)/[bug template](.github/ISSUE_TEMPLATE/bug_report.md)
20 |
21 | A good bug report shouldn't leave others needing to chase you up for more information. Please try to be as detailed as possible in your report. What is your environment? What steps will reproduce the issue? What would you expect to be the outcome? All these details will help people to fix any potential bugs.
22 |
23 | ## Feature requests
24 |
25 | Feature requests are welcome. But take a moment to find out whether your idea fits with the scope and aims of the project. It's up to _you_ to make a strong case to convince the project's developers of the merits of this feature. Please provide as much detail and context as possible.
26 |
27 | ## Questions
28 |
29 | We're always open to a new conversations. So if you have any questions just ask us.
30 |
31 | ## Pull requests
32 |
33 | Good pull requests - patches, improvements, new features - are a fantastic help. They should remain focused in scope and avoid containing unrelated commits.
34 |
35 | **Please ask first** before embarking on any significant pull request (e.g. implementing features, refactoring code, porting to a different language), otherwise you risk spending a lot of time working on something that the project's developers might not want to merge into the project.
36 |
37 | Please adhere to the coding conventions used throughout a project (indentation, accurate comments, etc.) and any other requirements (such as test coverage). Not all features proposed will be added but we are open to having a conversation about a feature you are championing.
38 |
39 | Guidelines for pull requests:
40 |
41 | 1. SmtpMock [pull request template](.github/PULL_REQUEST_TEMPLATE.md)
42 | 2. Fork the repo, checkout to `develop` branch
43 | 3. Run the tests. This is to make sure your starting point works
44 | 4. Read our [branch naming convention](.github/BRANCH_NAMING_CONVENTION.md)
45 | 5. Create a new branch
46 | 6. Read our [setup development environment guide](.github/DEVELOPMENT_ENVIRONMENT_GUIDE.md)
47 | 7. Make your changes. Please note that your PR should include tests for the new codebase!
48 | 8. Push to your fork and submit a pull request to `develop` branch
49 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at . All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [http://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: http://contributor-covenant.org
74 | [version]: http://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/lib/smtp_mock/command_line_args_builder.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SmtpMock
4 | require 'dry/struct'
5 |
6 | Types = ::Class.new { include Dry.Types }
7 |
8 | class CommandLineArgsBuilder < Dry::Struct
9 | IP_ADDRESS_PATTERN = /\A((1\d|[1-9]|2[0-4])?\d|25[0-5])(\.\g<1>){3}\z/.freeze
10 | PERMITTED_ATTRS = {
11 | SmtpMock::Types::Array.constrained(min_size: 1) => %i[
12 | blacklisted_helo_domains
13 | blacklisted_mailfrom_emails
14 | blacklisted_rcptto_emails
15 | not_registered_emails
16 | ].freeze,
17 | SmtpMock::Types::Bool.constrained(eql: true) => %i[log fail_fast multiple_rcptto multiple_message_receiving].freeze,
18 | SmtpMock::Types::Integer.constrained(gteq: 1) => %i[
19 | port
20 | session_timeout
21 | shutdown_timeout
22 | response_delay_helo
23 | response_delay_mailfrom
24 | response_delay_rcptto
25 | response_delay_data
26 | response_delay_message
27 | response_delay_rset
28 | response_delay_quit
29 | msg_size_limit
30 | ].freeze,
31 | SmtpMock::Types::String => %i[
32 | msg_greeting
33 | msg_invalid_cmd
34 | msg_invalid_cmd_helo_sequence
35 | msg_invalid_cmd_helo_arg
36 | msg_helo_blacklisted_domain
37 | msg_helo_received
38 | msg_invalid_cmd_mailfrom_sequence
39 | msg_invalid_cmd_mailfrom_arg
40 | msg_mailfrom_blacklisted_email
41 | msg_mailfrom_received
42 | msg_invalid_cmd_rcptto_sequence
43 | msg_invalid_cmd_rcptto_arg
44 | msg_rcptto_not_registered_email
45 | msg_rcptto_blacklisted_email
46 | msg_rcptto_received
47 | msg_invalid_cmd_data_sequence
48 | msg_data_received
49 | msg_msg_size_is_too_big
50 | msg_msg_received
51 | msg_invalid_cmd_rset_sequence
52 | msg_invalid_cmd_rset_arg
53 | msg_rset_received
54 | msg_quit_cmd
55 | ].freeze
56 | }.freeze
57 |
58 | class << self
59 | def call(**options)
60 | new(options).to_command_line_args_string
61 | rescue Dry::Struct::Error => error
62 | raise SmtpMock::Error::Argument, error.message
63 | end
64 |
65 | private
66 |
67 | def define_attribute
68 | ->((type, attributes)) { attributes.each { |field| attribute?(field, type) } }
69 | end
70 | end
71 |
72 | schema(schema.strict)
73 |
74 | attribute?(:host, SmtpMock::Types::String.constrained(format: SmtpMock::CommandLineArgsBuilder::IP_ADDRESS_PATTERN))
75 | SmtpMock::CommandLineArgsBuilder::PERMITTED_ATTRS.each(&define_attribute)
76 |
77 | def to_command_line_args_string
78 | to_h.map do |key, value|
79 | key = to_camel_case(key)
80 | value = format_by_type(value)
81 | value ? "-#{key}=#{value}" : "-#{key}"
82 | end.sort.join(' ')
83 | end
84 |
85 | private
86 |
87 | def to_camel_case(symbol)
88 | symbol.to_s.gsub(/_(\D)/) { ::Regexp.last_match(1).upcase }
89 | end
90 |
91 | def to_quoted(string)
92 | "\"#{string}\""
93 | end
94 |
95 | def format_by_type(object)
96 | case object
97 | when ::Array then to_quoted(object.join(','))
98 | when ::String then to_quoted(object)
99 | when ::TrueClass then nil
100 | else object
101 | end
102 | end
103 | end
104 | end
105 |
--------------------------------------------------------------------------------
/spec/smtp_mock/rspec_helper/client/smtp_client_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe SmtpMock::RspecHelper::Client::SmtpClient, type: :helper do
4 | subject(:smtp_client_instance) { described_class.new(host, port, net_class) }
5 |
6 | let(:host) { random_hostname }
7 | let(:port) { random_port_number }
8 | let(:net_smtp_instance) { instance_double('NetSmtpInstance') }
9 | let(:net_class) { class_double('NetClass') }
10 |
11 | describe 'defined constants' do
12 | it { expect(described_class).to be_const_defined(:UNDEFINED_VERSION) }
13 | it { expect(described_class::Error).to be < ::StandardError }
14 | end
15 |
16 | describe '.new' do
17 | context 'when out of the box Net::SMTP version' do
18 | it 'creates session instance with net smtp instance inside' do
19 | expect(net_class).to receive(:new).with(host, port).and_return(net_smtp_instance)
20 | smtp_client_instance
21 | end
22 | end
23 |
24 | context 'when Net::SMTP version < 0.3.0' do
25 | before { net_class.send(:const_set, :VERSION, '0.2.128506') }
26 |
27 | it 'creates session instance with net smtp instance inside' do
28 | expect(net_class).to receive(:new).with(host, port).and_return(net_smtp_instance)
29 | smtp_client_instance
30 | end
31 | end
32 |
33 | context 'when Net::SMTP version >= 0.3.0' do
34 | before { net_class.send(:const_set, :VERSION, '0.3.0') }
35 |
36 | it 'creates session instance with net smtp instance inside' do
37 | expect(net_class).to receive(:new).with(host, port, tls_verify: false)
38 | smtp_client_instance
39 | end
40 | end
41 | end
42 |
43 | describe '#start' do
44 | subject(:session_start) { smtp_client_instance.start(helo_domain, &session_actions) }
45 |
46 | let(:helo_domain) { random_hostname }
47 | let(:session_actions) { proc {} }
48 |
49 | context 'when out of the box Net::SMTP version' do
50 | before { allow(net_class).to receive(:new).with(host, port).and_return(net_smtp_instance) }
51 |
52 | it 'passes helo domain as position argument' do
53 | expect(net_smtp_instance).to receive(:start).with(helo_domain, &session_actions)
54 | session_start
55 | end
56 | end
57 |
58 | context 'when Net::SMTP version in range 0.1.0...0.2.0' do
59 | before do
60 | net_class.send(:const_set, :VERSION, '0.1.314')
61 | allow(net_class).to receive(:new).with(host, port).and_return(net_smtp_instance)
62 | end
63 |
64 | it 'passes helo domain as position argument' do
65 | expect(net_smtp_instance).to receive(:start).with(helo_domain, &session_actions)
66 | session_start
67 | end
68 | end
69 |
70 | context 'when Net::SMTP version in range 0.2.0...0.3.0' do
71 | before do
72 | net_class.send(:const_set, :VERSION, '0.2.128506')
73 | allow(net_class).to receive(:new).with(host, port).and_return(net_smtp_instance)
74 | end
75 |
76 | it 'passes helo domain as position argument' do
77 | expect(net_smtp_instance).to receive(:start).with(helo_domain, tls_verify: false, &session_actions)
78 | session_start
79 | end
80 | end
81 |
82 | context 'when Net::SMTP version >= 0.3.0' do
83 | before do
84 | net_class.send(:const_set, :VERSION, '0.3.0')
85 | allow(net_class).to receive(:new).with(host, port, tls_verify: false).and_return(net_smtp_instance)
86 | end
87 |
88 | it 'passes helo domain as keyword argument' do
89 | expect(net_smtp_instance).to receive(:start).with(helo: helo_domain, &session_actions)
90 | session_start
91 | end
92 | end
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/spec/smtp_mock_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe SmtpMock do
4 | describe 'defined constants' do
5 | it { expect(described_class).to be_const_defined(:SMTPMOCK_MIN_VERSION) }
6 | it { expect(described_class).to be_const_defined(:Types) }
7 | end
8 |
9 | describe '.start_server' do
10 | subject(:smtp_mock_server) { described_class.start_server(server_class, **options) }
11 |
12 | let(:server_class) { class_double('SmtpMockServer') }
13 |
14 | context 'without keyword args' do
15 | let(:options) { {} }
16 |
17 | it 'creates and runs SMTP mock server instance with default settings' do
18 | expect(server_class).to receive(:new)
19 | smtp_mock_server
20 | end
21 | end
22 |
23 | context 'with keyword args' do
24 | let(:options) { { host: random_ip_v4_address, port: random_port_number } }
25 |
26 | it 'creates and runs SMTP mock server instance with custom settings' do
27 | expect(server_class).to receive(:new).with(**options)
28 | smtp_mock_server
29 | end
30 | end
31 | end
32 |
33 | describe '.running_servers' do
34 | subject(:running_servers) { described_class.running_servers }
35 |
36 | let(:total_active_servers) { 2 }
37 | let(:smtp_mock_server_instances) { create_fake_servers(active: total_active_servers, inactive: 5) }
38 |
39 | it do
40 | expect(::ObjectSpace).to receive(:each_object).with(described_class::Server).and_return(smtp_mock_server_instances)
41 | expect(running_servers.size).to eq(total_active_servers)
42 | end
43 | end
44 |
45 | describe '.stop_running_servers!' do
46 | subject(:stop_running_servers) { described_class.stop_running_servers! }
47 |
48 | before { allow(described_class).to receive(:running_servers).and_return(smtp_mock_server_instances) }
49 |
50 | context 'when servers not found' do
51 | let(:smtp_mock_server_instances) { [] }
52 |
53 | it { is_expected.to be(true) }
54 | end
55 |
56 | context 'when servers found' do
57 | let(:smtp_mock_server_instances) { create_fake_servers(active: 2, inactive: 0) }
58 |
59 | it { is_expected.to be(true) }
60 | end
61 | end
62 |
63 | describe 'SMTP mock server integration tests' do
64 | let(:host) { '127.0.0.1' }
65 | let(:helo_domain) { random_hostname }
66 | let(:mailfrom) { random_email }
67 | let(:rcptto) { random_email }
68 |
69 | context 'when successful scenario' do
70 | let(:expected_response_status) { 250 }
71 | let(:expected_response_message) { "#{expected_response_status} #{random_message}" }
72 | let(:smtp_mock_server_options) do
73 | {
74 | host: host,
75 | msg_msg_received: expected_response_message
76 | }
77 | end
78 |
79 | it 'SMTP client receives predefined values by SMTP mock server' do
80 | smtp_mock_server = described_class.start_server(**smtp_mock_server_options)
81 | smtp_response = smtp_request(
82 | host: host,
83 | port: smtp_mock_server.port,
84 | helo_domain: helo_domain,
85 | mailfrom: mailfrom,
86 | rcptto: rcptto,
87 | message: random_message
88 | )
89 |
90 | expect(smtp_response).to be_success
91 | expect(smtp_response).to have_status(expected_response_status)
92 | expect(smtp_response).to have_message_context(expected_response_message)
93 |
94 | smtp_mock_server.stop!
95 | end
96 | end
97 |
98 | context 'when failure scenario' do
99 | let(:expected_response_status) { 550 }
100 | let(:expected_response_message) { "#{expected_response_status} #{random_message}" }
101 | let(:smtp_mock_server_options) do
102 | {
103 | host: host,
104 | not_registered_emails: [rcptto],
105 | msg_rcptto_not_registered_email: expected_response_message
106 | }
107 | end
108 |
109 | it 'SMTP client raises exeption with expected error context' do
110 | smtp_mock_server = described_class.start_server(**smtp_mock_server_options)
111 |
112 | expect do
113 | smtp_request(
114 | host: host,
115 | port: smtp_mock_server.port,
116 | helo_domain: helo_domain,
117 | mailfrom: mailfrom,
118 | rcptto: rcptto,
119 | message: random_message
120 | )
121 | end.to raise_error(SmtpMock::RspecHelper::Client::SmtpClient::Error, expected_response_message)
122 |
123 | smtp_mock_server.stop!
124 | end
125 | end
126 | end
127 | end
128 |
--------------------------------------------------------------------------------
/spec/smtp_mock/dependency_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe SmtpMock::Dependency do
4 | describe 'defined constants' do
5 | it { expect(described_class).to be_const_defined(:BINARY_SHORTCUT) }
6 | it { expect(described_class).to be_const_defined(:SYMLINK) }
7 | it { expect(described_class).to be_const_defined(:VERSION_REGEX_PATTERN) }
8 | end
9 |
10 | describe '.smtpmock_path_by_symlink' do
11 | subject(:smtpmock_path_by_symlink) { described_class.smtpmock_path_by_symlink }
12 |
13 | it 'returns smtpmock path by symlink' do
14 | expect(::Kernel).to receive(:`).with("readlink #{SmtpMock::Dependency::SYMLINK}")
15 | smtpmock_path_by_symlink
16 | end
17 | end
18 |
19 | describe '.smtpmock?' do
20 | subject(:smtpmock?) { described_class.smtpmock? }
21 |
22 | before { allow(described_class).to receive(:smtpmock_path_by_symlink).and_return(smtpmock_path) }
23 |
24 | context 'when smtpmock path found by symlink' do
25 | let(:smtpmock_path) { 'smtpmock_path' }
26 |
27 | it { is_expected.to be(true) }
28 | end
29 |
30 | context 'when smtpmock path not found by symlink' do
31 | let(:smtpmock_path) { '' }
32 |
33 | it { is_expected.to be(false) }
34 | end
35 | end
36 |
37 | describe '.verify_dependencies' do
38 | subject(:verify_dependencies) { described_class.verify_dependencies }
39 |
40 | context 'when smtpmock version satisfies minimum version' do
41 | it 'not raises SmtpMock::Error::Dependency error' do
42 | expect(described_class).to receive(:smtpmock?).and_return(true)
43 | expect(described_class).to receive(:version).and_return(SmtpMock::SMTPMOCK_MIN_VERSION)
44 | expect(verify_dependencies).to match_semver_regex_pattern
45 | end
46 |
47 | it 'not raises SmtpMock::Error::Dependency error in case of comparing greater semantic version' do
48 | expect(described_class).to receive(:smtpmock?).and_return(true)
49 | expect(described_class).to receive(:version).and_return('1.10.0')
50 | expect(verify_dependencies).to match_semver_regex_pattern
51 | end
52 | end
53 |
54 | context 'when not supported smtpmock version installed' do
55 | shared_examples 'raises SmtpMock::Error::Dependency error' do
56 | it do
57 | expect(described_class).to receive(:smtpmock?).and_return(true)
58 | expect(described_class).to receive(:version).and_return(version)
59 | expect { verify_dependencies }
60 | .to raise_error(
61 | SmtpMock::Error::Dependency,
62 | SmtpMock::Error::Dependency::SMTPMOCK_MIN_VERSION
63 | )
64 | end
65 | end
66 |
67 | context 'when failed to determine current smtpmock version' do
68 | let(:version) { nil }
69 |
70 | include_examples 'raises SmtpMock::Error::Dependency error'
71 | end
72 |
73 | context 'when current smtpmock version does not satisfy the minimum version' do
74 | let(:version) { '1.4.9' }
75 |
76 | include_examples 'raises SmtpMock::Error::Dependency error'
77 | end
78 | end
79 |
80 | context 'when smtpmock not installed' do
81 | it do
82 | expect(described_class).to receive(:smtpmock?).and_return(false)
83 | expect { verify_dependencies }
84 | .to raise_error(
85 | SmtpMock::Error::Dependency,
86 | SmtpMock::Error::Dependency::SMTPMOCK_NOT_INSTALLED
87 | )
88 | end
89 | end
90 | end
91 |
92 | describe '.compose_command' do
93 | subject(:compose_command) { described_class.compose_command(command_line_args) }
94 |
95 | context 'when command line args are not empty' do
96 | let(:command_line_args) { '-x -y -z 42' }
97 |
98 | it { is_expected.to eq("#{SmtpMock::Dependency::BINARY_SHORTCUT} #{command_line_args}") }
99 | end
100 |
101 | context 'when command line args is empty' do
102 | let(:command_line_args) { '' }
103 |
104 | it { is_expected.to eq(SmtpMock::Dependency::BINARY_SHORTCUT) }
105 | end
106 | end
107 |
108 | describe '.version' do
109 | subject(:version) { described_class.version }
110 |
111 | before { allow(::Kernel).to receive(:`).with("#{SmtpMock::Dependency::BINARY_SHORTCUT} -v").and_return(ver) }
112 |
113 | context 'when failed to determine smtpmock version' do
114 | let(:ver) { '' }
115 |
116 | it { is_expected.to be_nil }
117 | end
118 |
119 | context 'when it was possible to determine smtpmock version' do
120 | let(:ver) { "smtpmock: 3.14.0\ncommit: 2128506\nbuilt at: 2022-01-31T23:32:59Z" }
121 |
122 | it { is_expected.to match_semver_regex_pattern }
123 | end
124 | end
125 |
126 | describe '.minimal_version?' do
127 | subject(:minimal_version?) { described_class.send(:minimal_version?, current_version) }
128 |
129 | context 'when failed to determine current smtpmock version' do
130 | let(:current_version) { nil }
131 |
132 | it { is_expected.to be(false) }
133 | end
134 |
135 | context 'when current smtpmock version does not satisfy the minimum version' do
136 | let(:current_version) { '1.4.9' }
137 |
138 | it { is_expected.to be(false) }
139 | end
140 |
141 | context 'when current smtpmock version satisfies the minimum version' do
142 | let(:current_version) { SmtpMock::SMTPMOCK_MIN_VERSION }
143 |
144 | it { is_expected.to be(true) }
145 | end
146 | end
147 | end
148 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4 |
5 | ## [1.4.4] - 2024-10-29
6 |
7 | ### Updated
8 |
9 | - Updated gem development dependencies
10 | - Updated gem version
11 |
12 | ## [1.4.3] - 2024-07-12
13 |
14 | ### Updated
15 |
16 | - Updated gem development dependencies
17 | - Updated gem version
18 |
19 | ## [1.4.2] - 2024-05-19
20 |
21 | ### Updated
22 |
23 | - Updated gem development dependencies
24 | - Updated gem version
25 |
26 | ## [1.4.1] - 2024-04-20
27 |
28 | ### Added
29 |
30 | - Added `commitspell` linter
31 |
32 | ### Updated
33 |
34 | - Updated gem development dependencies
35 | - Updated gem documentation
36 | - Updated gem version
37 |
38 | ## [1.4.0] - 2024-02-08
39 |
40 | ### Added
41 |
42 | - Added and tested Ruby 3.3.x support
43 |
44 | ### Updated
45 |
46 | - Updated gem development dependencies
47 | - Updated gem version
48 |
49 | ## [1.3.6] - 2024-01-02
50 |
51 | ### Updated
52 |
53 | - Updated gem development dependencies
54 | - Updated gem version
55 | - Updated license
56 |
57 | ## [1.3.5] - 2023-10-17
58 |
59 | ### Updated
60 |
61 | - Updated gem development dependencies
62 | - Updated RSpec structure
63 | - Updated linters configs
64 | - Updated gem version
65 |
66 | ## [1.3.4] - 2023-01-11
67 |
68 | ### Updated
69 |
70 | - Updated release script
71 |
72 | ## [1.3.3] - 2023-01-10
73 |
74 | ### Added
75 |
76 | - Added and tested Ruby 3.2.x support
77 | - Added `changeloglint`
78 |
79 | ### Updated
80 |
81 | - Updated `CircleCI` config
82 | - Updated `lefthook`/`codeclimate`/`simplecov`/`pry` configs
83 | - Updated gem deploy flow (tagging new release on master merge commit)
84 | - Updated gem version, license
85 |
86 | ## [1.3.2] - 2022-12-12
87 |
88 | ### Added
89 |
90 | - Added [`cspell`](https://cspell.org) linter
91 | - Added [`markdownlint`](https://github.com/DavidAnson/markdownlint) linter
92 | - Added [`shellcheck`](https://www.shellcheck.net) linter
93 | - Added [`yamllint`](https://yamllint.readthedocs.io) linter
94 |
95 | ### Fixed
96 |
97 | - Fixed typos in project's codebase
98 | - Fixed new project's linter issues
99 |
100 | ### Updated
101 |
102 | - Updated `CircleCI` config
103 | - Updated [`lefthook`](https://github.com/evilmartians/lefthook) linters aggregator config
104 | - Updated releasing script, gemspecs
105 | - Updated gem version
106 |
107 | ## [1.3.1] - 2022-12-01
108 |
109 | ### Added
110 |
111 | - Added new bunch of project linters
112 | - Added auto deploy to RubyGems
113 | - Added auto creating release notes on GitHub
114 |
115 | ### Updated
116 |
117 | - Updated gemspecs
118 | - Updated `codeclimate`/`circleci` configs
119 | - Updated gem development dependencies
120 | - Updated gem version
121 |
122 | ### Removed
123 |
124 | - Removed `overcommit` dependency
125 |
126 | ## [1.3.0] - 2022-11-19
127 |
128 | ### Added
129 |
130 | - Added ability to configure multiple `RCPT TO` receiving scenario
131 |
132 | ### Updated
133 |
134 | - Updated `SmtpMock::Types::Bool`, tests
135 | - Updated `codeclimate`/`circleci` configs
136 | - Updated gemspecs
137 | - Updated gem runtime/development dependencies
138 | - Updated gem documentation, version
139 |
140 | ## [1.2.2] - 2022-10-05
141 |
142 | ### Fixed
143 |
144 | - Fixed wrong semantic version comparison in `SmtpMock::Dependency#minimal_version?`
145 |
146 | ### Updated
147 |
148 | - Updated gemspecs
149 | - Updated tests
150 | - Updated `codeclimate`/`circleci` configs
151 | - Updated gem development dependencies
152 | - Updated gem version
153 |
154 | ## [1.2.1] - 2022-07-27
155 |
156 | ### Fixed
157 |
158 | - Fixed documentation
159 |
160 | ### Updated
161 |
162 | - Updated gem documentation, version
163 |
164 | ## [1.2.0] - 2022-07-27
165 |
166 | ### Added
167 |
168 | - Added ability to use `RSET` SMTP command
169 | - Added ability to configure multiple message receiving flow during one session
170 | - Added ability to configure SMTP command delay responses
171 |
172 | ### Updated
173 |
174 | - Updated gemspecs
175 | - Updated tests
176 | - Updated `rubocop`/`codeclimate`/`circleci` configs
177 | - Updated gem development dependencies
178 | - Updated gem documentation, version
179 |
180 | ## [1.1.0] - 2022-05-17
181 |
182 | ### Added
183 |
184 | - Ability to check `smtpmock` version from cli
185 |
186 | ### Updated
187 |
188 | - Updated gemspecs
189 | - Updated `codeclimate`/`circleci` configs
190 | - Updated gem development dependencies
191 | - Updated gem version
192 |
193 | ## [1.0.1] - 2022-03-10
194 |
195 | ### Added
196 |
197 | - Development environment guide
198 |
199 | ### Updated
200 |
201 | - Updated gemspecs
202 | - Updated `codeclimate`/`circleci` configs
203 | - Updated gem development dependencies
204 | - Updated gem version
205 |
206 | ## [1.0.0] - 2022-01-31
207 |
208 | ### Added
209 |
210 | - Added `smtpmock` version checker
211 | - Added command for upgrade `smtpmock` to latest version
212 | - Added `SmtpMock::Error::Dependency::SMTPMOCK_MIN_VERSION`
213 | - Added `SmtpMock::Server#version`, tests
214 |
215 | ### Updated
216 |
217 | - Updated `SmtpMock::Dependency.verify_dependencies`, tests
218 | - Updated `SmtpMock::Cli::Command`, tests
219 | - Updated gem version, documentation
220 |
221 | ## [0.1.2] - 2022-01-24
222 |
223 | ### Updated
224 |
225 | - Updated `SmtpMock::Cli::Command`, tests
226 | - Updated gem version, documentation
227 |
228 | ## [0.1.1] - 2022-01-18
229 |
230 | ### Updated
231 |
232 | - Updated gem documentation
233 | - Updated `codeclimate` config
234 |
235 | ## [0.1.0] - 2022-01-18
236 |
237 | ### Added
238 |
239 | - First release of `SmtpMock`. Thanks [@le0pard](https://github.com/le0pard) for support 🚀
240 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | version: 2.1
4 |
5 | defaults: &defaults
6 | working_directory: ~/ruby-smtp-mock
7 | docker:
8 | - image: cimg/ruby:<< parameters.ruby-version >>
9 |
10 | orbs:
11 | ruby: circleci/ruby@2.3.0
12 |
13 | references:
14 | bundle_install: &bundle_install
15 | run:
16 | name: Installing gems
17 | command: |
18 | bundle config set --local path '~/vendor/bundle'
19 | bundle install
20 |
21 | install_system_dependencies: &install_system_dependencies
22 | run:
23 | name: Installing system dependencies
24 | command: bundle exec smtp_mock -s -i ~
25 |
26 | install_linters: &install_linters
27 | run:
28 | name: Installing bunch of linters
29 | command: |
30 | curl -1sLf 'https://dl.cloudsmith.io/public/evilmartians/lefthook/setup.deb.sh' | sudo -E bash
31 | sudo apt-get update -y
32 | sudo apt-get install -y lefthook shellcheck yamllint
33 | npm install --prefix='~/.local' --global --save-dev git+https://github.com/streetsidesoftware/cspell-cli markdownlint-cli
34 | cp .circleci/linter_configs/.fasterer.yml .fasterer.yml
35 | cp .circleci/linter_configs/.lefthook.yml lefthook.yml
36 |
37 | install_codeclimate_reporter: &install_codeclimate_reporter
38 | run:
39 | name: Installing CodeClimate test reporter
40 | command: |
41 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
42 | chmod +x ./cc-test-reporter
43 |
44 | use_latest_bundler: &use_latest_bundler
45 | run:
46 | name: Using latest bundler
47 | command: gem install bundler
48 |
49 | use_latest_gemspec: &use_latest_gemspec
50 | run:
51 | name: Using latest gemspec
52 | command: cp .circleci/gemspecs/latest smtp_mock.gemspec
53 |
54 | use_compatible_gemspec: &use_compatible_gemspec
55 | run:
56 | name: Using compatible gemspec
57 | command: cp .circleci/gemspecs/compatible smtp_mock.gemspec
58 |
59 | jobs:
60 | linters-ruby:
61 | parameters:
62 | ruby-version:
63 | type: string
64 |
65 | <<: *defaults
66 |
67 | steps:
68 | - checkout
69 |
70 | - <<: *use_latest_bundler
71 | - <<: *use_latest_gemspec
72 | - <<: *bundle_install
73 | - <<: *install_linters
74 |
75 | - run:
76 | name: Running commit linters
77 | command: lefthook run commit-linters
78 |
79 | - run:
80 | name: Running code style linters
81 | command: lefthook run code-style-linters
82 |
83 | - run:
84 | name: Running code performance linters
85 | command: lefthook run code-performance-linters
86 |
87 | - run:
88 | name: Running code vulnerability linters
89 | command: lefthook run code-vulnerability-linters
90 |
91 | - run:
92 | name: Running code documentation linters
93 | command: lefthook run code-documentation-linters
94 |
95 | - run:
96 | name: Running release linters
97 | command: lefthook run release-linters
98 |
99 | tests-ruby:
100 | parameters:
101 | ruby-version:
102 | type: string
103 |
104 | <<: *defaults
105 |
106 | steps:
107 | - checkout
108 |
109 | - <<: *use_latest_bundler
110 | - <<: *use_latest_gemspec
111 | - <<: *bundle_install
112 | - <<: *install_system_dependencies
113 | - <<: *install_codeclimate_reporter
114 |
115 | - run:
116 | name: Running RSpec
117 | command: |
118 | ./cc-test-reporter before-build
119 | bundle exec rspec
120 |
121 | - run:
122 | name: Creating CodeClimate test coverage report
123 | command: |
124 | ./cc-test-reporter format-coverage -t simplecov -o "coverage/codeclimate.$CIRCLE_NODE_INDEX.json"
125 |
126 | - store_artifacts:
127 | name: Saving Simplecov coverage artifacts
128 | path: ~/ruby-smtp-mock/coverage
129 | destination: coverage
130 |
131 | - deploy:
132 | name: Uploading CodeClimate test coverage report
133 | command: |
134 | ./cc-test-reporter sum-coverage --output - --parts $CIRCLE_NODE_TOTAL coverage/codeclimate.*.json | ./cc-test-reporter upload-coverage --debug --input -
135 |
136 | compatibility-ruby:
137 | parameters:
138 | ruby-version:
139 | type: string
140 |
141 | <<: *defaults
142 |
143 | steps:
144 | - checkout
145 |
146 | - <<: *use_compatible_gemspec
147 |
148 | - ruby/install-deps:
149 | bundler-version: "2.3.26"
150 | with-cache: false
151 | path: '~/vendor/custom_bundle'
152 |
153 | - <<: *install_system_dependencies
154 |
155 | - run:
156 | name: Running compatibility tests
157 | command: bundle exec rspec
158 |
159 | rubygems-deps-ruby:
160 | parameters:
161 | ruby-version:
162 | type: string
163 |
164 | <<: *defaults
165 |
166 | steps:
167 | - checkout
168 |
169 | - run:
170 | name: Building rubygems dependencies from default gemspec on minimal Ruby version
171 | command: bundle install
172 |
173 | releasing-gem-from-ruby:
174 | parameters:
175 | ruby-version:
176 | type: string
177 |
178 | <<: *defaults
179 |
180 | steps:
181 | - checkout
182 |
183 | - add_ssh_keys:
184 | fingerprints:
185 | - "55:d3:88:af:10:c9:b1:9d:53:f4:d8:fc:79:4c:69:f7"
186 |
187 | - run:
188 | name: Publishing new release
189 | command: ./.circleci/scripts/release.sh
190 |
191 | workflows:
192 | build_test_deploy:
193 | jobs:
194 | - linters-ruby:
195 | matrix:
196 | parameters:
197 | ruby-version: ["3.3-node"]
198 | - tests-ruby:
199 | matrix:
200 | parameters:
201 | ruby-version: ["3.3"]
202 | - compatibility-ruby:
203 | matrix:
204 | parameters:
205 | ruby-version: ["2.5", "2.6", "2.7", "3.0", "3.1", "3.2"]
206 | - rubygems-deps-ruby:
207 | matrix:
208 | parameters:
209 | ruby-version: ["2.5"]
210 | - releasing-gem-from-ruby:
211 | requires:
212 | - linters-ruby
213 | - tests-ruby
214 | - compatibility-ruby
215 | - rubygems-deps-ruby
216 | matrix:
217 | parameters:
218 | ruby-version: ["2.5"]
219 | filters:
220 | branches:
221 | only: master
222 |
--------------------------------------------------------------------------------
/spec/smtp_mock/command_line_args_builder_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe SmtpMock::CommandLineArgsBuilder do
4 | describe 'defined constants' do
5 | it { expect(described_class).to be_const_defined(:IP_ADDRESS_PATTERN) }
6 | it { expect(described_class).to be_const_defined(:PERMITTED_ATTRS) }
7 | end
8 |
9 | describe '.call' do
10 | subject(:command_line_args_builder) { described_class.call(**options) }
11 |
12 | describe 'Success' do
13 | context 'when permitted command line arguments with valid values passed' do
14 | let(:host) { random_ip_v4_address }
15 | let(:port) { rand(2525..3535) }
16 | let(:session_timeout) { rand(10..20) }
17 | let(:shutdown_timeout) { rand(30..40) }
18 | let(:msg_size_limit) { rand(10_000..20_000) }
19 | let(:blacklisted_helo_domains) { ::Array.new(2) { random_hostname } }
20 | let(:blacklisted_mailfrom_emails) { ::Array.new(2) { random_email } }
21 | let(:blacklisted_rcptto_emails) { ::Array.new(2) { random_email } }
22 | let(:not_registered_emails) { ::Array.new(2) { random_email } }
23 | let(:string_args) do
24 | %i[
25 | msg_greeting
26 | msg_invalid_cmd
27 | msg_invalid_cmd_helo_sequence
28 | msg_invalid_cmd_helo_arg
29 | msg_helo_blacklisted_domain
30 | msg_helo_received
31 | msg_invalid_cmd_mailfrom_sequence
32 | msg_invalid_cmd_mailfrom_arg
33 | msg_mailfrom_blacklisted_email
34 | msg_mailfrom_received
35 | msg_invalid_cmd_rcptto_sequence
36 | msg_invalid_cmd_rcptto_arg
37 | msg_rcptto_not_registered_email
38 | msg_rcptto_blacklisted_email
39 | msg_rcptto_received
40 | msg_invalid_cmd_data_sequence
41 | msg_data_received
42 | msg_msg_size_is_too_big
43 | msg_msg_received
44 | msg_invalid_cmd_rset_sequence
45 | msg_invalid_cmd_rset_arg
46 | msg_rset_received
47 | msg_quit_cmd
48 | ].zip('a'..'z').to_h
49 | end
50 | let(:options) do
51 | {
52 | host: host,
53 | port: port,
54 | log: true,
55 | session_timeout: session_timeout,
56 | shutdown_timeout: shutdown_timeout,
57 | fail_fast: true,
58 | multiple_rcptto: true,
59 | multiple_message_receiving: true,
60 | msg_size_limit: msg_size_limit,
61 | blacklisted_helo_domains: blacklisted_helo_domains,
62 | blacklisted_mailfrom_emails: blacklisted_mailfrom_emails,
63 | blacklisted_rcptto_emails: blacklisted_rcptto_emails,
64 | not_registered_emails: not_registered_emails,
65 | **string_args
66 | }
67 | end
68 | let(:builded_command_line_args_string) do
69 | %(-blacklistedHeloDomains="#{blacklisted_helo_domains.join(',')}"
70 | -blacklistedMailfromEmails="#{blacklisted_mailfrom_emails.join(',')}"
71 | -blacklistedRcpttoEmails="#{blacklisted_rcptto_emails.join(',')}"
72 | -failFast
73 | -host="#{host}"
74 | -log
75 | -msgDataReceived="q"
76 | -msgGreeting="a"
77 | -msgHeloBlacklistedDomain="e"
78 | -msgHeloReceived="f"
79 | -msgInvalidCmd="b"
80 | -msgInvalidCmdDataSequence="p"
81 | -msgInvalidCmdHeloArg="d"
82 | -msgInvalidCmdHeloSequence="c"
83 | -msgInvalidCmdMailfromArg="h"
84 | -msgInvalidCmdMailfromSequence="g"
85 | -msgInvalidCmdRcpttoArg="l"
86 | -msgInvalidCmdRcpttoSequence="k"
87 | -msgInvalidCmdRsetArg="u"
88 | -msgInvalidCmdRsetSequence="t"
89 | -msgMailfromBlacklistedEmail="i"
90 | -msgMailfromReceived="j"
91 | -msgMsgReceived="s"
92 | -msgMsgSizeIsTooBig="r"
93 | -msgQuitCmd="w"
94 | -msgRcpttoBlacklistedEmail="n"
95 | -msgRcpttoNotRegisteredEmail="m"
96 | -msgRcpttoReceived="o"
97 | -msgRsetReceived="v"
98 | -msgSizeLimit=#{msg_size_limit}
99 | -multipleMessageReceiving
100 | -multipleRcptto
101 | -notRegisteredEmails="#{not_registered_emails.join(',')}"
102 | -port=#{port}
103 | -sessionTimeout=#{session_timeout}
104 | -shutdownTimeout=#{shutdown_timeout})
105 | end
106 |
107 | it 'returns string with builded command line arguments' do
108 | expect(command_line_args_builder).to eq(builded_command_line_args_string.tr("\n", ' '))
109 | end
110 | end
111 | end
112 |
113 | describe 'Failure' do
114 | shared_examples 'invalid command line argument' do
115 | it do
116 | expect { command_line_args_builder }.to raise_error(SmtpMock::Error::Argument)
117 | end
118 | end
119 |
120 | context 'when not permitted command line arguments passed' do
121 | let(:options) { { not_permitted_arg_1: 42, not_permitted_arg_2: 43 } }
122 |
123 | it_behaves_like 'invalid command line argument'
124 | end
125 |
126 | [42, 'not_ip_address', '255.255.255.256'].each do |value|
127 | context 'when invalid host command line argument value passed' do
128 | let(:options) { { host: value } }
129 |
130 | it_behaves_like 'invalid command line argument'
131 | end
132 | end
133 |
134 | described_class::PERMITTED_ATTRS[SmtpMock::Types::Array.constrained(min_size: 1)]
135 | .product([42, []]).each do |key, value|
136 | context 'when invalid value for array command line argument type passed' do
137 | let(:options) { { key => value } }
138 |
139 | it_behaves_like 'invalid command line argument'
140 | end
141 | end
142 |
143 | described_class::PERMITTED_ATTRS[SmtpMock::Types::Bool.constrained(eql: true)]
144 | .product([42, false]).each do |key, value|
145 | context 'when invalid value for boolean command line argument type passed' do
146 | let(:options) { { key => value } }
147 |
148 | it_behaves_like 'invalid command line argument'
149 | end
150 | end
151 |
152 | described_class::PERMITTED_ATTRS[SmtpMock::Types::Integer.constrained(gteq: 1)]
153 | .product(['42', -42]).each do |key, value|
154 | context 'when invalid value for integer command line argument type passed' do
155 | let(:options) { { key => value } }
156 |
157 | it_behaves_like 'invalid command line argument'
158 | end
159 | end
160 |
161 | described_class::PERMITTED_ATTRS[SmtpMock::Types::String]
162 | .product([42]).each do |key, value|
163 | context 'when invalid value for integer command line argument type passed' do
164 | let(:options) { { key => value } }
165 |
166 | it_behaves_like 'invalid command line argument'
167 | end
168 | end
169 | end
170 | end
171 | end
172 |
--------------------------------------------------------------------------------
/spec/smtp_mock/server_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe SmtpMock::Server do
4 | let(:port) { random_port_number }
5 | let(:converted_command_line_args) { '-a -b 42' }
6 | let(:pid) { random_pid }
7 |
8 | describe '.new' do
9 | describe 'Success' do
10 | subject(:server_instance) { described_class.new(deps_checker, port_checker, args_builder, process, **args) }
11 |
12 | let(:deps_checker) { SmtpMock::Dependency }
13 | let(:port_checker) { class_double('ServerPort') }
14 | let(:process) { class_double('ServerProcess') }
15 | let(:args_builder) { class_double('CommandLineArgsBuilder') }
16 | let(:host) { random_ip_v4_address }
17 | let(:composed_command_with_args) { compose_command(converted_command_line_args) }
18 | let(:version) { ::Array.new(3) { ::Random.rand(1..10) }.join('.') }
19 |
20 | before do
21 | allow(deps_checker).to receive(:verify_dependencies).and_return(version)
22 | allow(deps_checker)
23 | .to receive(:compose_command)
24 | .with(converted_command_line_args)
25 | .and_return(composed_command_with_args)
26 | allow(::Kernel).to receive(:at_exit)
27 | end
28 |
29 | context 'when port passed' do
30 | let(:args) { { host: host, port: port } }
31 |
32 | it 'creates and runs server instance, gets port from args' do
33 | expect(port_checker).not_to receive(:random_free_port)
34 | expect(args_builder).to receive(:call).with(args).and_return(converted_command_line_args)
35 | expect(process).to receive(:create).with(composed_command_with_args).and_return(pid)
36 | expect(server_instance.pid).to eq(pid)
37 | expect(server_instance.port).to eq(port)
38 | expect(server_instance.version).to eq(version)
39 | end
40 | end
41 |
42 | context 'when port not passed' do
43 | let(:args) { { host: host } }
44 |
45 | it 'creates and runs server instance, gets port from SmtpMock::Server::Port' do
46 | expect(port_checker).to receive(:random_free_port).and_return(port)
47 | expect(args_builder).to receive(:call).with(args.merge(port: port)).and_return(converted_command_line_args)
48 | expect(process).to receive(:create).with(composed_command_with_args).and_return(pid)
49 | expect(server_instance.pid).to eq(pid)
50 | expect(server_instance.port).to eq(port)
51 | expect(server_instance.version).to eq(version)
52 | end
53 | end
54 | end
55 |
56 | describe 'Failure' do
57 | context 'when current system dependencies do not satisfy gem requirements' do
58 | subject(:server_instance) { described_class.new(deps_checker, args_builder) }
59 |
60 | let(:args_builder) { SmtpMock::CommandLineArgsBuilder }
61 | let(:deps_checker) { SmtpMock::Dependency }
62 |
63 | it do
64 | expect(deps_checker).to receive(:verify_dependencies).and_call_original
65 | expect(deps_checker).to receive(:smtpmock?).and_return(false)
66 | expect { server_instance }
67 | .to raise_error(
68 | SmtpMock::Error::Dependency,
69 | SmtpMock::Error::Dependency::SMTPMOCK_NOT_INSTALLED
70 | )
71 | end
72 | end
73 |
74 | context 'when invalid keyword argument passed' do
75 | subject(:server_instance) { described_class.new(invalid_argument: 42) }
76 |
77 | it do
78 | expect(SmtpMock::Dependency).to receive(:verify_dependencies)
79 | expect { server_instance }.to raise_error(SmtpMock::Error::Argument)
80 | end
81 | end
82 |
83 | context 'when smtpmock not starts' do
84 | subject(:server_instance) { described_class.new }
85 |
86 | let(:err_output) { SmtpMock::Server::Process.send(:err_log) }
87 | let(:err_output_path) { '../../../spec/support/fixtures/err_log_with_context' }
88 |
89 | before do
90 | reset_err_log
91 | stub_const('SmtpMock::Server::Process::TMP_LOG_PATH', err_output_path)
92 | end
93 |
94 | after { reset_err_log }
95 |
96 | it do
97 | expect(SmtpMock::Dependency).to receive(:verify_dependencies)
98 | expect(SmtpMock::Server::Port).to receive(:random_free_port)
99 | expect(SmtpMock::CommandLineArgsBuilder).to receive(:call).and_return(converted_command_line_args)
100 | expect(::Process).to receive(:spawn).with(compose_command(converted_command_line_args), err: err_output)
101 | expect { server_instance }.to raise_error(SmtpMock::Error::Server, 'Some error context here')
102 | end
103 | end
104 | end
105 | end
106 |
107 | describe '#active?' do
108 | subject(:server_instance) { described_class.new }
109 |
110 | before do
111 | allow(SmtpMock::Dependency).to receive(:verify_dependencies)
112 | allow(SmtpMock::Server::Port).to receive(:random_free_port).and_return(port)
113 | allow(SmtpMock::CommandLineArgsBuilder).to receive(:call).and_return(converted_command_line_args)
114 | allow(SmtpMock::Server::Process).to receive(:create).with(compose_command(converted_command_line_args)).and_return(pid)
115 | allow(::Kernel).to receive(:at_exit)
116 | end
117 |
118 | context 'when server is active' do
119 | it do
120 | expect(SmtpMock::Server::Process).to receive(:alive?).with(pid).and_return(true)
121 | expect(SmtpMock::Server::Port).to receive(:port_open?).with(port).and_return(true)
122 | expect(server_instance.active?).to be(true)
123 | end
124 | end
125 |
126 | context 'when server is inactive' do
127 | context 'when process is dead' do
128 | it do
129 | expect(SmtpMock::Server::Process).to receive(:alive?).with(pid).and_return(false)
130 | expect(SmtpMock::Server::Port).not_to receive(:port_open?)
131 | expect(server_instance.active?).to be(false)
132 | end
133 | end
134 |
135 | context 'when port is closed' do
136 | it do
137 | expect(SmtpMock::Server::Process).to receive(:alive?).with(pid).and_return(true)
138 | expect(SmtpMock::Server::Port).to receive(:port_open?).with(port).and_return(false)
139 | expect(server_instance.active?).to be(false)
140 | end
141 | end
142 | end
143 | end
144 |
145 | describe '#stop' do
146 | subject(:server_instance) { described_class.new }
147 |
148 | before do
149 | allow(SmtpMock::Dependency).to receive(:verify_dependencies)
150 | allow(SmtpMock::Server::Port).to receive(:random_free_port).and_return(port)
151 | allow(SmtpMock::CommandLineArgsBuilder).to receive(:call).and_return(converted_command_line_args)
152 | allow(SmtpMock::Server::Process).to receive(:create).with(compose_command(converted_command_line_args)).and_return(pid)
153 | allow(::Kernel).to receive(:at_exit)
154 | end
155 |
156 | context 'when existent pid' do
157 | it 'stops current server by pid' do
158 | expect(SmtpMock::Server::Process)
159 | .to receive(:kill)
160 | .with(SmtpMock::Server::Process::SIGTERM, pid)
161 | .and_return(true)
162 | expect(server_instance.stop).to be(true)
163 | end
164 | end
165 |
166 | context 'when non-existent pid' do
167 | it 'stops current server by pid' do
168 | expect(SmtpMock::Server::Process)
169 | .to receive(:kill)
170 | .with(SmtpMock::Server::Process::SIGTERM, pid)
171 | .and_return(false)
172 | expect(server_instance.stop).to be(false)
173 | end
174 | end
175 | end
176 |
177 | describe '#stop!' do
178 | subject(:server_instance) { described_class.new }
179 |
180 | let(:port) { random_port_number }
181 |
182 | before do
183 | allow(SmtpMock::Dependency).to receive(:verify_dependencies)
184 | allow(SmtpMock::Server::Port).to receive(:random_free_port).and_return(port)
185 | allow(SmtpMock::CommandLineArgsBuilder).to receive(:call).and_return(converted_command_line_args)
186 | allow(SmtpMock::Server::Process).to receive(:create).with(compose_command(converted_command_line_args)).and_return(pid)
187 | allow(::Kernel).to receive(:at_exit)
188 | end
189 |
190 | context 'when existent pid' do
191 | it 'stops current server by pid' do
192 | expect(SmtpMock::Server::Process)
193 | .to receive(:kill)
194 | .with(SmtpMock::Server::Process::SIGKILL, pid)
195 | .and_return(true)
196 | expect(server_instance.stop!).to be(true)
197 | end
198 | end
199 |
200 | context 'when non-existent pid' do
201 | it 'stops current server by pid' do
202 | expect(SmtpMock::Server::Process)
203 | .to receive(:kill)
204 | .with(SmtpMock::Server::Process::SIGKILL, pid)
205 | .and_return(false)
206 | expect(server_instance.stop!).to be(false)
207 | end
208 | end
209 | end
210 | end
211 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 
2 |
3 | [](https://codeclimate.com/github/mocktools/ruby-smtp-mock/maintainability)
4 | [](https://codeclimate.com/github/mocktools/ruby-smtp-mock/test_coverage)
5 | [](https://circleci.com/gh/mocktools/ruby-smtp-mock/tree/master)
6 | [](https://badge.fury.io/rb/smtp_mock)
7 | [](https://rubygems.org/gems/smtp_mock)
8 | [](https://github.com/markets/awesome-ruby)
9 | [](LICENSE.txt)
10 | [](CODE_OF_CONDUCT.md)
11 |
12 | 💎 Ruby SMTP mock - flexible Ruby wrapper over [`smtpmock`](https://github.com/mocktools/go-smtp-mock). Mimic any 📤 SMTP server behavior for your test environment and even more.
13 |
14 | ## Table of Contents
15 |
16 | - [Features](#features)
17 | - [Requirements](#requirements)
18 | - [Installation](#installation)
19 | - [Usage](#usage)
20 | - [Dependency manager](#dependency-manager)
21 | - [Available flags](#available-flags)
22 | - [DSL](#dsl)
23 | - [Available server options](#available-server-options)
24 | - [Example of usage](#example-of-usage)
25 | - [RSpec integration](#rspec-integration)
26 | - [SmtpMock RSpec helper](#smtpmock-rspec-helper)
27 | - [SmtpMock RSpec interface](#smtpmock-rspec-interface)
28 | - [Contributing](#contributing)
29 | - [License](#license)
30 | - [Code of Conduct](#code-of-conduct)
31 | - [Credits](#credits)
32 | - [Versioning](#versioning)
33 | - [Changelog](CHANGELOG.md)
34 |
35 | ## Features
36 |
37 | - Ability to handle configurable behavior and life cycles of SMTP mock server(s)
38 | - Dynamic/manual port assignment
39 | - Test framework agnostic (it's PORO, so you can use it outside of `RSpec`, `Test::Unit` or `MiniTest`)
40 | - Simple and intuitive DSL
41 | - RSpec integration out of the box
42 | - Includes easy system dependency manager
43 |
44 | ## Requirements
45 |
46 | Ruby MRI 2.5.0+
47 |
48 | ## Installation
49 |
50 | Add this line to your application's `Gemfile`:
51 |
52 | ```ruby
53 | group :development, :test do
54 | gem 'smtp_mock', require: false
55 | end
56 | ```
57 |
58 | And then execute:
59 |
60 | ```bash
61 | bundle
62 | ```
63 |
64 | Or install it yourself as:
65 |
66 | ```bash
67 | gem install smtp_mock
68 | ```
69 |
70 | Then install [`smtpmock`](https://github.com/mocktools/go-smtp-mock) as system dependency:
71 |
72 | ```bash
73 | bundle exec smtp_mock -i ~
74 | ```
75 |
76 | ## Usage
77 |
78 | ### Dependency manager
79 |
80 | This gem includes easy system dependency manager. Run `bundle exec smtp_mock` with options for manage `smtpmock` system dependency.
81 |
82 | #### Available flags
83 |
84 | | Flag | Description | Example of usage |
85 | | --- | --- | --- |
86 | | `-s`, `--sudo` | Run command as sudo | `bundle exec smtp_mock -s -i ~` |
87 | | `-i`, `--install=PATH` | Install `smtpmock` to the existing path | `bundle exec smtp_mock -i ~/existent_dir` |
88 | | `-u`, `--uninstall` | Uninstall `smtpmock` | `bundle exec smtp_mock -u` |
89 | | `-g`, `--upgrade` | Upgrade to latest version of `smtpmock` | `bundle exec smtp_mock -g` |
90 | | `-v`, `--version` | Prints current version of `smtpmock` | `bundle exec smtp_mock -v` |
91 | | `-h`, `--help` | Prints help | `bundle exec smtp_mock -h` |
92 |
93 | ### DSL
94 |
95 | #### Available server options
96 |
97 | | Example of usage kwarg | Description |
98 | | --- | --- |
99 | | `host: '0.0.0.0'` | Host address where `smtpmock` will run. It's equal to 127.0.0.1 by default |
100 | | `port: 2525` | Server port number. If not specified it will be assigned dynamically |
101 | | `log: true` | Enables log server activity. Disabled by default |
102 | | `session_timeout: 60` | Session timeout in seconds. It's equal to 30 seconds by default |
103 | | `shutdown_timeout: 5` | Graceful shutdown timeout in seconds. It's equal to 1 second by default |
104 | | `fail_fast: true` | Enables fail fast scenario. Disabled by default |
105 | | `multiple_rcptto: true` | Enables multiple `RCPT TO` receiving scenario. Disabled by default |
106 | | `multiple_message_receiving: true` | Enables multiple message receiving scenario. Disabled by default |
107 | | `blacklisted_helo_domains: %w[a.com b.com]` | Blacklisted `HELO` domains |
108 | | `blacklisted_mailfrom_emails: %w[a@a.com b@b.com]` | Blacklisted `MAIL FROM` emails |
109 | | `blacklisted_rcptto_emails: %w[c@c.com d@d.com]` | blacklisted `RCPT TO` emails |
110 | | `not_registered_emails: %w[e@e.com f@f.com]` | Not registered (non-existent) `RCPT TO` emails |
111 | | `response_delay_helo: 2` | `HELO` response delay in seconds. It's equal to 0 seconds by default |
112 | | `response_delay_mailfrom: 2` | `MAIL FROM` response delay in seconds. It's equal to 0 seconds by default |
113 | | `response_delay_rcptto: 2` | `RCPT TO` response delay in seconds. It's equal to 0 seconds by default |
114 | | `response_delay_data: 2` | `DATA` response delay in seconds. It's equal to 0 seconds by default |
115 | | `response_delay_message: 2` | Message response delay in seconds. It's equal to 0 seconds by default |
116 | | `response_delay_rset: 2` | `RSET` response delay in seconds. It's equal to 0 seconds by default |
117 | | `response_delay_quit: 2` | `QUIT` response delay in seconds. It's equal to 0 seconds by default |
118 | | `msg_size_limit: 42` | Message body size limit in bytes. It's equal to 10485760 bytes by default |
119 | | `msg_greeting: 'Greeting message'` | Custom server greeting message |
120 | | `msg_invalid_cmd: 'Invalid command message'` | Custom invalid command message |
121 | | `msg_invalid_cmd_helo_sequence: 'Invalid command HELO sequence message'` | Custom invalid command `HELO` sequence message |
122 | | `msg_invalid_cmd_helo_arg: 'Invalid command HELO argument message'` | Custom invalid command `HELO` argument message |
123 | | `msg_helo_blacklisted_domain: 'Blacklisted domain message'` | Custom `HELO` blacklisted domain message |
124 | | `msg_helo_received: 'HELO received message'` | Custom `HELO` received message |
125 | | `msg_invalid_cmd_mailfrom_sequence: 'Invalid command MAIL FROM sequence message'` | Custom invalid command `MAIL FROM` sequence message |
126 | | `msg_invalid_cmd_mailfrom_arg: 'Invalid command MAIL FROM argument message'` | Custom invalid command `MAIL FROM` argument message |
127 | | `msg_mailfrom_blacklisted_email: 'Blacklisted email message'` | Custom `MAIL FROM` blacklisted email message |
128 | | `msg_mailfrom_received: 'MAIL FROM received message'` | Custom `MAIL FROM` received message |
129 | | `msg_invalid_cmd_rcptto_sequence: 'Invalid command RCPT TO sequence message'` | Custom invalid command `RCPT TO` sequence message |
130 | | `msg_invalid_cmd_rcptto_arg: 'Invalid command RCPT TO argument message'` | Custom invalid command `RCPT TO` argument message |
131 | | `msg_rcptto_not_registered_email: 'Not registered email message'` | Custom `RCPT TO` not registered email message |
132 | | `msg_rcptto_blacklisted_email: 'Blacklisted email message'` | Custom `RCPT TO` blacklisted email message |
133 | | `msg_rcptto_received: 'RCPT TO received message'` | Custom `RCPT TO` received message |
134 | | `msg_invalid_cmd_data_sequence: 'Invalid command DATA sequence message'` | Custom invalid command `DATA` sequence message |
135 | | `msg_data_received: 'DATA received message'` | Custom `DATA` received message |
136 | | `msg_msg_size_is_too_big: 'Message size is too big'` | Custom size is too big message |
137 | | `msg_invalid_cmd_rset_sequence: 'Invalid command RSET sequence message'` | Custom invalid command `RSET` sequence message |
138 | | `msg_invalid_cmd_rset_arg: 'Invalid command RSET argument message'` | Custom invalid command `RSET` argument message |
139 | | `msg_rset_received: 'RSET received message'` | Custom `RSET` received message |
140 | | `msg_quit_cmd: 'Quit command message'` | Custom quit command message |
141 |
142 | #### Example of usage
143 |
144 | ```ruby
145 | # Public SmtpMock interface
146 | # Without kwargs creates SMTP mock server with default behavior.
147 | # A free port for server will be randomly assigned in the range
148 | # from 49152 to 65535. Returns current smtp mock server instance
149 | smtp_mock_server = SmtpMock.start_server(not_registered_emails: %w[user@example.com]) # => SmtpMock::Server instance
150 |
151 | # returns current smtp mock server port
152 | smtp_mock_server.port # => 55640
153 |
154 | # returns current smtp mock server process identification number (PID)
155 | smtp_mock_server.pid # => 38195
156 |
157 | # returns current smtp mock server version
158 | smtp_mock_server.version # => '1.5.2'
159 |
160 | # interface for graceful shutdown current smtp mock server
161 | smtp_mock_server.stop # => true
162 |
163 | # interface for force shutdown current smtp mock server
164 | smtp_mock_server.stop! # => true
165 |
166 | # interface to check state of current smtp mock server
167 | # returns true if server is running, otherwise returns false
168 | smtp_mock_server.active? # => true
169 |
170 | # returns list of running smtp mock servers
171 | SmtpMock.running_servers # => [SmtpMock::Server instance]
172 |
173 | # interface to stop all running smtp mock servers
174 | SmtpMock.stop_running_servers! # => true
175 | ```
176 |
177 | ### RSpec integration
178 |
179 | Require this either in your Gemfile or in RSpec's support scripts. So either:
180 |
181 | ```ruby
182 | # Gemfile
183 |
184 | group :test do
185 | gem 'rspec'
186 | gem 'smtp_mock', require: 'smtp_mock/test_framework/rspec'
187 | end
188 | ```
189 |
190 | or
191 |
192 | ```ruby
193 | # spec/support/config/smtp_mock.rb
194 |
195 | require 'smtp_mock/test_framework/rspec'
196 | ```
197 |
198 | #### SmtpMock RSpec helper
199 |
200 | Just add `SmtpMock::TestFramework::RSpec::Helper` if you wanna use shortcut `smtp_mock_server` for SmtpMock server instance inside of your `RSpec.describe` blocks:
201 |
202 | ```ruby
203 | # spec/support/config/smtp_mock.rb
204 |
205 | RSpec.configure do |config|
206 | config.include SmtpMock::TestFramework::RSpec::Helper
207 | end
208 | ```
209 |
210 | ```ruby
211 | # your awesome smtp_client_spec.rb
212 |
213 | RSpec.describe SmtpClient do
214 | subject(:smtp_response) do
215 | described_class.call(
216 | host: 'localhost',
217 | port: smtp_mock_server.port,
218 | mailfrom: mailfrom,
219 | rcptto: rcptto,
220 | message: message
221 | )
222 | end
223 |
224 | let(:mailfrom) { 'sender@example.com' }
225 | let(:rcptto) { 'receiver@example.com' }
226 | let(:message) { 'Email message context' }
227 | let(:expected_response_message) { '250 Custom successful response' }
228 |
229 | before { smtp_mock_server(msg_msg_received: expected_response_message) }
230 |
231 | it do
232 | expect(smtp_response).to be_success
233 | expect(smtp_response).to have_status(expected_response_status)
234 | expect(smtp_response).to have_message_context(expected_response_message)
235 | end
236 | end
237 | ```
238 |
239 | #### SmtpMock RSpec interface
240 |
241 | If you won't use `SmtpMock::TestFramework::RSpec::Helper` you can use `SmtpMock::TestFramework::RSpec::Interface` directly instead:
242 |
243 | ```ruby
244 | SmtpMock::TestFramework::RSpec::Interface.start_server # creates and runs SmtpMock server instance
245 | SmtpMock::TestFramework::RSpec::Interface.stop_server! # stops and clears current SmtpMock server instance
246 | SmtpMock::TestFramework::RSpec::Interface.clear_server! # clears current SmtpMock server instance
247 | ```
248 |
249 | ## Contributing
250 |
251 | Bug reports and pull requests are welcome on GitHub at . This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. Please check the [open tickets](https://github.com/mocktools/ruby-smtp-mock/issues). Be sure to follow Contributor Code of Conduct below and our [Contributing Guidelines](CONTRIBUTING.md).
252 |
253 | ## License
254 |
255 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
256 |
257 | ## Code of Conduct
258 |
259 | Everyone interacting in the SmtpMock project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
260 |
261 | ## Credits
262 |
263 | - [The Contributors](https://github.com/mocktools/ruby-smtp-mock/graphs/contributors) for code and awesome suggestions
264 | - [The Stargazers](https://github.com/mocktools/ruby-smtp-mock/stargazers) for showing their support
265 |
266 | ## Versioning
267 |
268 | SmtpMock uses [Semantic Versioning 2.0.0](https://semver.org)
269 |
--------------------------------------------------------------------------------
/spec/smtp_mock/cli_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe SmtpMock::Cli do
4 | describe '.call' do
5 | subject(:command) { described_class.call(command_line_args, command_class) }
6 |
7 | let(:command_line_args) { [] }
8 | let(:command_class) { class_double('SmtpMock::Cli::Command') }
9 | let(:message) { 'some message' }
10 | let(:command_instance) do
11 | described_class::Command.new.tap do |object|
12 | object.message = message
13 | object.success = success
14 | end
15 | end
16 |
17 | before do
18 | allow(command_class).to receive(:new).and_return(command_instance)
19 | allow(command_instance).to receive(:resolve).with(command_line_args)
20 | end
21 |
22 | context 'when succesful command' do
23 | let(:success) { true }
24 |
25 | it 'prints message to stdout and exits with status 0' do
26 | expect(::Kernel).to receive(:puts).with(message)
27 | expect(::Kernel).to receive(:exit).with(0)
28 | command
29 | end
30 | end
31 |
32 | context 'when failed command' do
33 | let(:success) { false }
34 |
35 | it 'prints message to stdout and exits with status 1' do
36 | expect(::Kernel).to receive(:puts).with(message)
37 | expect(::Kernel).to receive(:exit).with(1)
38 | command
39 | end
40 | end
41 | end
42 | end
43 |
44 | RSpec.describe SmtpMock::Cli::Command do
45 | subject(:command_instance) { described_class.new }
46 |
47 | describe 'defined constants' do
48 | it { expect(described_class).to be_const_defined(:USE_CASE) }
49 | it { expect(described_class).to be_const_defined(:DOWNLOAD_SCRIPT) }
50 | end
51 |
52 | it 'have specified attr accesptors' do
53 | expect(command_instance.members).to match_array(%i[install_path sudo success message])
54 | end
55 |
56 | describe '#resolve' do
57 | subject(:resolve) { command_instance.resolve(command_line_args) }
58 |
59 | context 'when sudo key passed' do
60 | %w[-s --sudo].each do |key|
61 | let(:command_line_args) { [key] }
62 |
63 | it do
64 | expect { resolve }
65 | .to change(command_instance, :sudo)
66 | .from(nil).to(true)
67 | .and change(command_instance, :success)
68 | .from(nil).to(true)
69 | end
70 | end
71 | end
72 |
73 | context 'when install key passed' do
74 | [%w[-i some_path], %w[--install=some_path]].each do |keys|
75 | let(:command_line_args) { keys }
76 |
77 | context 'when smtpmock is already installed' do
78 | it 'not installes binary file and not addes symlink' do
79 | expect(::File).to receive(:exist?).with('some_path/smtpmock').and_return(true)
80 | expect { resolve }
81 | .to change(command_instance, :install_path)
82 | .from(nil).to('some_path')
83 | .and change(command_instance, :message)
84 | .from(nil).to('smtpmock is already installed')
85 | .and change(command_instance, :success)
86 | .from(nil).to(true)
87 | end
88 | end
89 |
90 | context 'when smtpmock is not installed yet' do
91 | it 'installes binary file and addes symlink' do
92 | expect(::File).to receive(:exist?).with('some_path/smtpmock').and_return(false)
93 | expect(::Kernel).to receive(:system).with("cd some_path && curl -sL #{SmtpMock::Cli::Resolver::DOWNLOAD_SCRIPT} | bash")
94 | expect(::Kernel).to receive(:system).with("ln -s some_path/smtpmock #{SmtpMock::Dependency::SYMLINK}")
95 | expect { resolve }
96 | .to change(command_instance, :install_path)
97 | .from(nil).to('some_path')
98 | .and change(command_instance, :message)
99 | .from(nil).to('smtpmock was installed successfully')
100 | .and change(command_instance, :success)
101 | .from(nil).to(true)
102 | end
103 | end
104 | end
105 | end
106 |
107 | context 'when sudo with install key passed' do
108 | [%w[-s -i some_path], %w[--sudo --install=some_path]].each do |keys|
109 | let(:command_line_args) { keys }
110 |
111 | context 'when smtpmock is already installed' do
112 | it 'not installes binary file and not addes symlink' do
113 | expect(::File).to receive(:exist?).with('some_path/smtpmock').and_return(true)
114 | expect { resolve }
115 | .to change(command_instance, :sudo)
116 | .from(nil).to(true)
117 | .and change(command_instance, :install_path)
118 | .from(nil).to('some_path')
119 | .and change(command_instance, :message)
120 | .from(nil).to('smtpmock is already installed')
121 | .and change(command_instance, :success)
122 | .from(nil).to(true)
123 | end
124 | end
125 |
126 | context 'when smtpmock is not installed yet' do
127 | it 'installes binary file and addes symlink' do
128 | expect(::File).to receive(:exist?).with('some_path/smtpmock').and_return(false)
129 | expect(::Kernel).to receive(:system).with("cd some_path && curl -sL #{SmtpMock::Cli::Resolver::DOWNLOAD_SCRIPT} | bash")
130 | expect(::Kernel).to receive(:system).with("sudo ln -s some_path/smtpmock #{SmtpMock::Dependency::SYMLINK}")
131 | expect { resolve }
132 | .to change(command_instance, :sudo)
133 | .from(nil).to(true)
134 | .and change(command_instance, :install_path)
135 | .from(nil).to('some_path')
136 | .and change(command_instance, :message)
137 | .from(nil).to('smtpmock was installed successfully')
138 | .and change(command_instance, :success)
139 | .from(nil).to(true)
140 | end
141 | end
142 | end
143 | end
144 |
145 | context 'when uninstall key passed' do
146 | %w[-u --uninstall].each do |key|
147 | let(:command_line_args) { [key] }
148 |
149 | before { stub_const('SmtpMock::Dependency::SYMLINK', symlink) }
150 |
151 | context 'when non-existent symlink' do
152 | let(:symlink) { '/usr/local/bin/non-existent-smtpmock' }
153 |
154 | it 'not removes symlink and binary file' do
155 | expect(SmtpMock::Dependency).to receive(:smtpmock_path_by_symlink).and_return('')
156 | expect { resolve }
157 | .to change(command_instance, :message)
158 | .from(nil).to('smtpmock not installed yet')
159 | .and change(command_instance, :success)
160 | .from(nil).to(true)
161 | end
162 | end
163 |
164 | context 'when existent symlink' do
165 | let(:symlink) { '/usr/local/bin/existent-smtpmock' }
166 | let(:binary_path) { '/some_binary_path' }
167 |
168 | it 'removes symlink and binary file' do
169 | expect(SmtpMock::Dependency).to receive(:smtpmock_path_by_symlink).and_return(binary_path)
170 | expect(::Kernel).to receive(:system).with("unlink #{SmtpMock::Dependency::SYMLINK}")
171 | expect(::Kernel).to receive(:system).with("rm #{binary_path}")
172 | expect { resolve }
173 | .to change(command_instance, :message)
174 | .from(nil).to('smtpmock was uninstalled successfully')
175 | .and change(command_instance, :success)
176 | .from(nil).to(true)
177 | end
178 | end
179 | end
180 | end
181 |
182 | context 'when sudo with uninstall key passed' do
183 | [%w[-s -u], %w[--sudo --uninstall]].each do |keys|
184 | let(:command_line_args) { keys }
185 |
186 | before { stub_const('SmtpMock::Dependency::SYMLINK', symlink) }
187 |
188 | context 'when non-existent symlink' do
189 | let(:symlink) { '/usr/local/bin/non-existent-smtpmock' }
190 |
191 | it 'not removes symlink and binary file' do
192 | expect(SmtpMock::Dependency).to receive(:smtpmock_path_by_symlink).and_return('')
193 | expect { resolve }
194 | .to change(command_instance, :sudo)
195 | .from(nil).to(true)
196 | .and change(command_instance, :message)
197 | .from(nil).to('smtpmock not installed yet')
198 | .and change(command_instance, :success)
199 | .from(nil).to(true)
200 | end
201 | end
202 |
203 | context 'when existent symlink' do
204 | let(:symlink) { '/usr/local/bin/existent-smtpmock' }
205 | let(:binary_path) { '/some_binary_path' }
206 |
207 | it 'removes symlink and binary file' do
208 | expect(SmtpMock::Dependency).to receive(:smtpmock_path_by_symlink).and_return(binary_path)
209 | expect(::Kernel).to receive(:system).with("sudo unlink #{SmtpMock::Dependency::SYMLINK}")
210 | expect(::Kernel).to receive(:system).with("rm #{binary_path}")
211 | expect { resolve }
212 | .to change(command_instance, :sudo)
213 | .from(nil).to(true)
214 | .and change(command_instance, :message)
215 | .from(nil).to('smtpmock was uninstalled successfully')
216 | .and change(command_instance, :success)
217 | .from(nil).to(true)
218 | end
219 | end
220 | end
221 | end
222 |
223 | context 'when upgrade key passed' do
224 | %w[-g --upgrade].each do |key|
225 | let(:command_line_args) { [key] }
226 |
227 | before { stub_const('SmtpMock::Dependency::SYMLINK', symlink) }
228 |
229 | context 'when non-existent symlink' do
230 | let(:symlink) { '/usr/local/bin/non-existent-smtpmock' }
231 |
232 | it 'not replaces binary file' do
233 | expect(SmtpMock::Dependency).to receive(:smtpmock_path_by_symlink).and_return('')
234 | expect { resolve }
235 | .to change(command_instance, :message)
236 | .from(nil).to('smtpmock not installed yet')
237 | .and change(command_instance, :success)
238 | .from(nil).to(true)
239 | end
240 | end
241 |
242 | context 'when existent symlink' do
243 | let(:symlink) { '/usr/local/bin/existent-smtpmock' }
244 | let(:binary_dir) { '/some_binary_path' }
245 | let(:binary_path) { "#{binary_dir}/binary" }
246 |
247 | it 'replaces binary file' do
248 | expect(SmtpMock::Dependency).to receive(:smtpmock_path_by_symlink).and_return(binary_path)
249 | expect(::Kernel).to receive(:system).with("cd #{binary_dir} && curl -sL #{SmtpMock::Cli::Resolver::DOWNLOAD_SCRIPT} | bash")
250 | expect { resolve }
251 | .to change(command_instance, :message)
252 | .from(nil).to('smtpmock was upgraded successfully')
253 | .and change(command_instance, :success)
254 | .from(nil).to(true)
255 | end
256 | end
257 | end
258 | end
259 |
260 | context 'when sudo with upgrade key passed' do
261 | [%w[-s -g], %w[--sudo --upgrade]].each do |keys|
262 | let(:command_line_args) { keys }
263 |
264 | before { stub_const('SmtpMock::Dependency::SYMLINK', symlink) }
265 |
266 | context 'when non-existent symlink' do
267 | let(:symlink) { '/usr/local/bin/non-existent-smtpmock' }
268 |
269 | it 'not replaces binary file' do
270 | expect(SmtpMock::Dependency).to receive(:smtpmock_path_by_symlink).and_return('')
271 | expect { resolve }
272 | .to change(command_instance, :sudo)
273 | .from(nil).to(true)
274 | .and change(command_instance, :message)
275 | .from(nil).to('smtpmock not installed yet')
276 | .and change(command_instance, :success)
277 | .from(nil).to(true)
278 | end
279 | end
280 |
281 | context 'when existent symlink' do
282 | let(:symlink) { '/usr/local/bin/existent-smtpmock' }
283 | let(:binary_dir) { '/some_binary_path' }
284 | let(:binary_path) { "#{binary_dir}/binary" }
285 |
286 | it 'replaces binary file' do
287 | expect(SmtpMock::Dependency).to receive(:smtpmock_path_by_symlink).and_return(binary_path)
288 | expect(::Kernel).to receive(:system).with("cd #{binary_dir} && curl -sL #{SmtpMock::Cli::Resolver::DOWNLOAD_SCRIPT} | bash")
289 | expect { resolve }
290 | .to change(command_instance, :sudo)
291 | .from(nil).to(true)
292 | .and change(command_instance, :message)
293 | .from(nil).to('smtpmock was upgraded successfully')
294 | .and change(command_instance, :success)
295 | .from(nil).to(true)
296 | end
297 | end
298 | end
299 | end
300 |
301 | context 'when version key passed' do
302 | [%w[-v], %w[--version]].each do |keys|
303 | let(:command_line_args) { keys }
304 | let(:version) { random_sem_version }
305 |
306 | context 'when smtpmock is already installed' do
307 | it 'returns current version of smtpmock' do
308 | expect(::SmtpMock::Dependency).to receive(:version).and_return(version)
309 | expect { resolve }
310 | .to change(command_instance, :message)
311 | .from(nil).to(version)
312 | .and change(command_instance, :success)
313 | .from(nil).to(true)
314 | end
315 | end
316 |
317 | context 'when smtpmock is not installed yet' do
318 | it 'not returns current version of smtpmock' do
319 | expect(::SmtpMock::Dependency).to receive(:smtpmock_path_by_symlink).and_return([])
320 | expect { resolve }
321 | .to change(command_instance, :message)
322 | .from(nil).to('smtpmock not installed yet')
323 | .and change(command_instance, :success)
324 | .from(nil).to(true)
325 | end
326 | end
327 | end
328 | end
329 |
330 | context 'when help key passed' do
331 | %w[-h --help].each do |key|
332 | let(:command_line_args) { [key] }
333 | let(:expected_string) do
334 | %(#{SmtpMock::Cli::Resolver::USE_CASE}
335 | -s, --sudo Run command as sudo
336 | -i, --install=PATH Install smtpmock to the existing path
337 | -u, --uninstall Uninstall smtpmock
338 | -g, --upgrade Upgrade to latest version of smtpmock
339 | -v, --version Prints current smtpmock version
340 | -h, --help Prints help
341 | )
342 | end
343 |
344 | it do
345 | expect { resolve }
346 | .to change(command_instance, :message)
347 | .from(nil).to(expected_string)
348 | .and change(command_instance, :success)
349 | .from(nil).to(true)
350 | end
351 | end
352 | end
353 | end
354 |
355 | describe '#binary_path' do
356 | subject(:binary_path) { command_instance.send(:binary_path) }
357 |
358 | let(:install_path) { 'some_install_path' }
359 |
360 | before { command_instance.install_path = install_path }
361 |
362 | it { is_expected.to eq("#{install_path}/smtpmock") }
363 | end
364 |
365 | describe '#install_to' do
366 | subject(:install_to) { command_instance.send(:install_to, install_path) }
367 |
368 | let(:install_path) { 'some_install_path' }
369 | let(:command) { "cd #{install_path} && curl -sL #{SmtpMock::Cli::Resolver::DOWNLOAD_SCRIPT} | bash" }
370 |
371 | it do
372 | expect(::Kernel).to receive(:system).with(command)
373 | install_to
374 | end
375 | end
376 |
377 | describe '#as_sudo' do
378 | subject(:as_sudo) { command_instance.send(:as_sudo) }
379 |
380 | context 'when sudo true' do
381 | before { command_instance.sudo = true }
382 |
383 | it { is_expected.to eq('sudo ') }
384 | end
385 |
386 | context 'when sudo false' do
387 | it { is_expected.to be_nil }
388 | end
389 | end
390 |
391 | describe '#current_smtpmock_path' do
392 | subject(:current_smtpmock_path) { command_instance.send(:current_smtpmock_path) }
393 |
394 | it 'memoizes current smtpmock path' do
395 | expect(SmtpMock::Dependency).to receive(:smtpmock_path_by_symlink)
396 | 2.times { current_smtpmock_path }
397 | end
398 | end
399 |
400 | describe '#not_installed?' do
401 | subject(:not_installed?) { command_instance.send(:not_installed?) }
402 |
403 | before { allow(SmtpMock::Dependency).to receive(:smtpmock_path_by_symlink).and_return(path_by_symlink) }
404 |
405 | context 'when smtpmock have been installed' do
406 | let(:path_by_symlink) { 'some_path' }
407 |
408 | it { is_expected.to be(false) }
409 | end
410 |
411 | context 'when smtpmock have not been installed' do
412 | let(:path_by_symlink) { '' }
413 |
414 | it do
415 | expect { not_installed? }
416 | .to change(command_instance, :message)
417 | .from(nil).to('smtpmock not installed yet')
418 | expect(not_installed?).to be(true)
419 | end
420 | end
421 | end
422 | end
423 |
--------------------------------------------------------------------------------