├── 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 | # ![Ruby SmtpMock - mimic any 📤 SMTP server behavior for your test environment with fake SMTP server](https://repository-images.githubusercontent.com/443795043/81ce5b00-0915-4dd0-93ad-88e6699e18cd) 2 | 3 | [![Maintainability](https://api.codeclimate.com/v1/badges/315c5fff7449a11868dd/maintainability)](https://codeclimate.com/github/mocktools/ruby-smtp-mock/maintainability) 4 | [![Test Coverage](https://api.codeclimate.com/v1/badges/315c5fff7449a11868dd/test_coverage)](https://codeclimate.com/github/mocktools/ruby-smtp-mock/test_coverage) 5 | [![CircleCI](https://circleci.com/gh/mocktools/ruby-smtp-mock/tree/master.svg?style=svg)](https://circleci.com/gh/mocktools/ruby-smtp-mock/tree/master) 6 | [![Gem Version](https://badge.fury.io/rb/smtp_mock.svg)](https://badge.fury.io/rb/smtp_mock) 7 | [![Downloads](https://img.shields.io/gem/dt/smtp_mock.svg?colorA=004d99&colorB=0073e6)](https://rubygems.org/gems/smtp_mock) 8 | [![In Awesome Ruby](https://raw.githubusercontent.com/sindresorhus/awesome/main/media/mentioned-badge.svg)](https://github.com/markets/awesome-ruby) 9 | [![GitHub](https://img.shields.io/github/license/mocktools/ruby-smtp-mock)](LICENSE.txt) 10 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v1.4%20adopted-ff69b4.svg)](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 | --------------------------------------------------------------------------------