smtp_mock 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +132 -0
  3. data/.codeclimate.yml +13 -0
  4. data/.github/BRANCH_NAMING_CONVENTION.md +36 -0
  5. data/.github/ISSUE_TEMPLATE/bug_report.md +28 -0
  6. data/.github/ISSUE_TEMPLATE/feature_request.md +27 -0
  7. data/.github/ISSUE_TEMPLATE/issue_report.md +28 -0
  8. data/.github/ISSUE_TEMPLATE/question.md +22 -0
  9. data/.github/PULL_REQUEST_TEMPLATE.md +49 -0
  10. data/.gitignore +12 -0
  11. data/.overcommit.yml +32 -0
  12. data/.reek.yml +30 -0
  13. data/.rspec +3 -0
  14. data/.rubocop.yml +396 -0
  15. data/.ruby-gemset +1 -0
  16. data/.ruby-version +1 -0
  17. data/CHANGELOG.md +9 -0
  18. data/CODE_OF_CONDUCT.md +74 -0
  19. data/CONTRIBUTING.md +46 -0
  20. data/Gemfile +5 -0
  21. data/LICENSE.txt +21 -0
  22. data/README.md +242 -0
  23. data/Rakefile +8 -0
  24. data/bin/console +15 -0
  25. data/bin/setup +8 -0
  26. data/bin/smtp_mock +6 -0
  27. data/lib/smtp_mock/cli/resolver.rb +61 -0
  28. data/lib/smtp_mock/cli.rb +17 -0
  29. data/lib/smtp_mock/command_line_args_builder.rb +94 -0
  30. data/lib/smtp_mock/core.rb +18 -0
  31. data/lib/smtp_mock/dependency.rb +26 -0
  32. data/lib/smtp_mock/error/argument.rb +7 -0
  33. data/lib/smtp_mock/error/dependency.rb +9 -0
  34. data/lib/smtp_mock/error/server.rb +7 -0
  35. data/lib/smtp_mock/server/port.rb +27 -0
  36. data/lib/smtp_mock/server/process.rb +44 -0
  37. data/lib/smtp_mock/server.rb +59 -0
  38. data/lib/smtp_mock/test_framework/rspec/helper.rb +15 -0
  39. data/lib/smtp_mock/test_framework/rspec/interface.rb +29 -0
  40. data/lib/smtp_mock/test_framework/rspec.rb +10 -0
  41. data/lib/smtp_mock/version.rb +5 -0
  42. data/lib/smtp_mock.rb +19 -0
  43. data/smtp_mock.gemspec +49 -0
  44. data/tmp/.gitkeep +0 -0
  45. metadata +288 -0
data/README.md ADDED
@@ -0,0 +1,242 @@
1
+ # ![Ruby SmtpMock - mimic any 📤 SMTP server behaviour for your test environment and even more!](https://repository-images.githubusercontent.com/443795043/81ce5b00-0915-4dd0-93ad-88e6699e18cd)
2
+
3
+ [![Maintainability](https://api.codeclimate.com/v1/badges/5ea9da61ef468b8ad4c4/maintainability)](https://codeclimate.com/github/mocktools/ruby-smtp-mock/maintainability)
4
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/5ea9da61ef468b8ad4c4/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
+ [![GitHub](https://img.shields.io/github/license/mocktools/ruby-smtp-mock)](LICENSE.txt)
9
+ [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v1.4%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md)
10
+
11
+ 💎 Ruby SMTP mock - flexible Ruby wrapper over [`smtpmock`](https://github.com/mocktools/go-smtp-mock). Mimic any 📤 SMTP server behaviour for your test environment and even more.
12
+
13
+ ## Table of Contents
14
+
15
+ - [Features](#features)
16
+ - [Requirements](#requirements)
17
+ - [Installation](#installation)
18
+ - [Usage](#usage)
19
+ - [Dependency manager](#dependency-manager)
20
+ - [Available flags](#available-flags)
21
+ - [DSL](#dsl)
22
+ - [Available server options](#available-server-options)
23
+ - [Example of usage](#example-of-usage)
24
+ - [RSpec integration](#rspec-integration)
25
+ - [SmtpMock RSpec helper](#smtpmock-rspec-helper)
26
+ - [SmtpMock RSpec interface](#smtpmock-rspec-interface)
27
+ - [Contributing](#contributing)
28
+ - [License](#license)
29
+ - [Code of Conduct](#code-of-conduct)
30
+ - [Credits](#credits)
31
+ - [Versioning](#versioning)
32
+ - [Changelog](CHANGELOG.md)
33
+
34
+ ## Features
35
+
36
+ - Ability to handle configurable behaviour and life cycles of SMTP mock server(s)
37
+ - Dynamic/manual port assignment
38
+ - Test framework agnostic (it's PORO, so you can use it outside of `RSpec`, `Test::Unit` or `MiniTest`)
39
+ - Simple and intuitive DSL
40
+ - RSpec integration out of the box
41
+ - Includes easy system dependency manager
42
+
43
+ ## Requirements
44
+
45
+ Ruby MRI 2.5.0+
46
+
47
+ ## Installation
48
+
49
+ Add this line to your application's `Gemfile`:
50
+
51
+ ```ruby
52
+ group :development, :test do
53
+ gem 'smtp_mock', require: false
54
+ end
55
+ ```
56
+
57
+ And then execute:
58
+
59
+ $ bundle
60
+
61
+ Or install it yourself as:
62
+
63
+ $ gem install smtp_mock
64
+
65
+ Then install [`smtpmock`](https://github.com/mocktools/go-smtp-mock) as system dependency:
66
+
67
+ $ bundle exec smtp_mock -i ~
68
+
69
+ ## Usage
70
+
71
+ ### Dependency manager
72
+
73
+ This gem includes esasy system dependency manager. Run `bundle exec smtp_mock` with options for manage `smtpmock` system dependency.
74
+
75
+ #### Available flags
76
+
77
+ | Flag | Description | Example of usage |
78
+ | --- | --- | --- |
79
+ | `-s`, `--sudo` | Run command as sudo | `bundle exec smtp_mock -s -i ~` |
80
+ | `-i`, `--install=PATH` | Install smtpmock to the existing path | `bundle exec smtp_mock -i ~/existent_dir` |
81
+ | `-u`, `--uninstall` | Uninstall smtpmock | `bundle exec smtp_mock -u` |
82
+ | `-h`, `--help` | Prints help | `bundle exec smtp_mock -h` |
83
+
84
+ ### DSL
85
+
86
+ #### Available server options
87
+
88
+ | Example of usage kwarg | Description |
89
+ | --- | --- |
90
+ | `host: '0.0.0.0'` | Host address where smtpmock will run. It's equal to 127.0.0.1 by default |
91
+ | `port: 2525` | Server port number. If not specified it will be assigned dynamically |
92
+ | `log: true` | Enables log server activity. Disabled by default |
93
+ | `session_timeout: 60` | Session timeout in seconds. It's equal to 30 seconds by default |
94
+ | `shutdown_timeout: 5` | Graceful shutdown timeout in seconds. It's equal to 1 second by default |
95
+ | `fail_fast: true` | Enables fail fast scenario. Disabled by default |
96
+ | `blacklisted_helo_domains: %w[a.com b.com]` | Blacklisted `HELO` domains |
97
+ | `blacklisted_mailfrom_emails: %w[a@a.com b@b.com]` | Blacklisted `MAIL FROM` emails |
98
+ | `blacklisted_rcptto_emails: %w[c@c.com d@d.com]` | blacklisted `RCPT TO` emails |
99
+ | `not_registered_emails: %w[e@e.com f@f.com]` | Not registered (non-existent) `RCPT TO` emails |
100
+ | `msg_size_limit: 42` | Message body size limit in bytes. It's equal to 10485760 bytes by default |
101
+ | `msg_greeting: 'Greeting message'` | Custom server greeting message |
102
+ | `msg_invalid_cmd: 'Invalid command message'` | Custom invalid command message |
103
+ | `msg_invalid_cmd_helo_sequence: 'Invalid command HELO sequence message'` | Custom invalid command `HELO` sequence message |
104
+ | `msg_invalid_cmd_helo_arg: 'Invalid command HELO argument message'` | Custom invalid command `HELO` argument message |
105
+ | `msg_helo_blacklisted_domain: 'Blacklisted domain message'` | Custom `HELO` blacklisted domain message |
106
+ | `msg_helo_received: 'HELO received message'` | Custom `HELO` received message |
107
+ | `msg_invalid_cmd_mailfrom_sequence: 'Invalid command MAIL FROM sequence message'` | Custom invalid command `MAIL FROM` sequence message |
108
+ | `msg_invalid_cmd_mailfrom_arg: 'Invalid command MAIL FROM argument message'` | Custom invalid command `MAIL FROM` argument message |
109
+ | `msg_mailfrom_blacklisted_email: 'Blacklisted email message'` | Custom `MAIL FROM` blacklisted email message |
110
+ | `msg_mailfrom_received: 'MAIL FROM received message'` | Custom `MAIL FROM` received message |
111
+ | `msg_invalid_cmd_rcptto_sequence: 'Invalid command RCPT TO sequence message'` | Custom invalid command `RCPT TO` sequence message |
112
+ | `msg_invalid_cmd_rcptto_arg: 'Invalid command RCPT TO argument message'` | Custom invalid command `RCPT TO` argument message |
113
+ | `msg_rcptto_not_registered_email: 'Not registered email message'` | Custom `RCPT TO` not registered email message |
114
+ | `msg_rcptto_blacklisted_email: 'Blacklisted email message'` | Custom `RCPT TO` blacklisted email message |
115
+ | `msg_rcptto_received: 'RCPT TO received message'` | Custom `RCPT TO` received message |
116
+ | `msg_invalid_cmd_data_sequence: 'Invalid command DATA sequence message'` | Custom invalid command `DATA` sequence message |
117
+ | `msg_data_received: 'DATA received message'` | Custom `DATA` received message |
118
+ | `msg_msg_size_is_too_big: 'Message size is too big'` | Custom size is too big message |
119
+ | `msg_msg_received: 'Message has been received'` | Custom received message body message |
120
+ | `msg_quit_cmd: 'Quit command message'` | Custom quit command message |
121
+
122
+ #### Example of usage
123
+
124
+ ```ruby
125
+ # Public SmtpMock interface
126
+ # Without kwargs creates SMTP mock server with default behaviour.
127
+ # A free port for server will be randomly assigned in the range
128
+ # from 49152 to 65535. Returns current smtp mock server instance
129
+ smtp_mock_server = SmtpMock.start_server(not_registered_emails: %w[user@example.com]) # => SmtpMock::Server instance
130
+
131
+ # returns current smtp mock server port
132
+ smtp_mock_server.port # => 55640
133
+
134
+ # returns current smtp mock server port
135
+ smtp_mock_server.pid # => 38195
136
+
137
+ # interface for graceful shutdown current smtp mock server
138
+ smtp_mock_server.stop # => true
139
+
140
+ # interface for force shutdown current smtp mock server
141
+ smtp_mock_server.stop! # => true
142
+
143
+ # interface to check state of current smtp mock server
144
+ # returns true if server is running, otherwise returns false
145
+ smtp_mock_server.active? # => true
146
+
147
+ # returns list of running smtp mock servers
148
+ SmtpMock.running_servers # => [SmtpMock::Server instance]
149
+
150
+ # interface to stop all running smtp mock servers
151
+ SmtpMock.stop_running_servers! # => true
152
+ ```
153
+
154
+ ### RSpec integration
155
+
156
+ Require this either in your Gemfile or in RSpec's support scripts. So either:
157
+
158
+ ```ruby
159
+ # Gemfile
160
+ group :test do
161
+ gem 'rspec'
162
+ gem 'smtp_mock', require: 'smtp_mock/test_framework/rspec'
163
+ end
164
+ ```
165
+
166
+ or
167
+
168
+ ```ruby
169
+ # spec/support/config/smtp_mock.rb
170
+ require 'smtp_mock/test_framework/rspec'
171
+ ```
172
+
173
+ #### SmtpMock RSpec helper
174
+
175
+ Just add `SmtpMock::TestFramework::RSpec::Helper` if you wanna use shortcut `smtp_mock_server` for SmtpMock server instance inside of your `RSpec.describe` blocks:
176
+
177
+ ```ruby
178
+ # spec/support/config/smtp_mock.rb
179
+ RSpec.configure do |config|
180
+ config.include SmtpMock::TestFramework::RSpec::Helper
181
+ end
182
+ ```
183
+
184
+ ```ruby
185
+ # your awesome first_a_record_spec.rb
186
+
187
+ RSpec.describe SmtpClient do
188
+ subject(:smtp_response) do
189
+ described_class.call(
190
+ host: 'localhost',
191
+ port: smtp_mock_server.port,
192
+ mailfrom: mailfrom,
193
+ rcptto: rcptto,
194
+ message: message
195
+ )
196
+ end
197
+
198
+ let(:mailfrom) { 'sender@example.com' }
199
+ let(:rcptto) { 'receiver@example.com' }
200
+ let(:message) { 'Email message context' }
201
+ let(:expected_response_message) { '250 Custom successful response' }
202
+
203
+ before { smtp_mock_server(msg_msg_received: expected_response_message) }
204
+
205
+ it do
206
+ expect(smtp_response).to be_success
207
+ expect(smtp_response).to have_status(expected_response_status)
208
+ expect(smtp_response).to have_message_context(expected_response_message)
209
+ end
210
+ end
211
+ ```
212
+
213
+ #### SmtpMock RSpec interface
214
+
215
+ If you won't use `SmtpMock::TestFramework::RSpec::Helper` you can use `SmtpMock::TestFramework::RSpec::Interface` directly instead:
216
+
217
+ ```ruby
218
+ SmtpMock::TestFramework::RSpec::Interface.start_server # creates and runs SmtpMock server instance
219
+ SmtpMock::TestFramework::RSpec::Interface.stop_server! # stops and clears current SmtpMock server instance
220
+ SmtpMock::TestFramework::RSpec::Interface.clear_server! # clears current SmtpMock server instance
221
+ ```
222
+
223
+ ## Contributing
224
+
225
+ Bug reports and pull requests are welcome on GitHub at https://github.com/mocktools/ruby-smtp-mock. 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).
226
+
227
+ ## License
228
+
229
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
230
+
231
+ ## Code of Conduct
232
+
233
+ 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).
234
+
235
+ ## Credits
236
+
237
+ - [The Contributors](https://github.com/mocktools/ruby-smtp-mock/graphs/contributors) for code and awesome suggestions
238
+ - [The Stargazers](https://github.com/mocktools/ruby-smtp-mock/stargazers) for showing their support
239
+
240
+ ## Versioning
241
+
242
+ SmtpMock uses [Semantic Versioning 2.0.0](https://semver.org)
data/Rakefile ADDED
@@ -0,0 +1,8 @@
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
data/bin/console ADDED
@@ -0,0 +1,15 @@
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__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
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
data/bin/smtp_mock ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/smtp_mock'
5
+
6
+ SmtpMock::Cli.call(::ARGV)
@@ -0,0 +1,61 @@
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/MethodLength, Metrics/AbcSize
12
+ opt_parser = ::OptionParser.new do |parser|
13
+ parser.banner = SmtpMock::Cli::Resolver::USE_CASE
14
+
15
+ parser.on('-s', '--sudo', 'Run command as sudo') do
16
+ self.sudo = true
17
+ end
18
+
19
+ parser.on('-iPATH', '--install=PATH', 'Install smtpmock to the existing path') do |argument|
20
+ self.install_path = argument
21
+ return self.message = 'smtpmock is already installed' if ::File.exist?(binary_path)
22
+
23
+ ::Kernel.system("cd #{install_path} && curl -sL #{SmtpMock::Cli::Resolver::DOWNLOAD_SCRIPT} | bash")
24
+ ::Kernel.system("#{as_sudo}ln -s #{binary_path} #{SmtpMock::Dependency::SYMLINK}")
25
+
26
+ self.message = 'smtpmock was installed successfully'
27
+ end
28
+
29
+ parser.on('-u', '--uninstall', 'Uninstall smtpmock') do
30
+ current_smtpmock_path = SmtpMock::Dependency.smtpmock_path_by_symlink
31
+ return self.message = 'smtpmock not installed yet' if current_smtpmock_path.empty?
32
+
33
+ ::Kernel.system("#{as_sudo}unlink #{SmtpMock::Dependency::SYMLINK}")
34
+ ::Kernel.system("rm #{current_smtpmock_path}")
35
+
36
+ self.message = 'smtpmock was uninstalled successfully'
37
+ end
38
+
39
+ self.success = true
40
+
41
+ parser.on('-h', '--help', 'Prints help') do
42
+ ::Kernel.puts(parser.to_s)
43
+ ::Kernel.exit
44
+ end
45
+ end
46
+
47
+ opt_parser.parse(command_line_args) # TODO: add error handler
48
+ end
49
+
50
+ private
51
+
52
+ def binary_path
53
+ "#{install_path}/smtpmock"
54
+ end
55
+
56
+ def as_sudo
57
+ return 'sudo ' if sudo
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmtpMock
4
+ module Cli
5
+ Command = ::Struct.new(:install_path, :sudo, :success, :message) do
6
+ include 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
@@ -0,0 +1,94 @@
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].freeze,
18
+ SmtpMock::Types::Integer.constrained(gteq: 1) => %i[
19
+ port
20
+ session_timeout
21
+ shutdown_timeout
22
+ msg_size_limit
23
+ ].freeze,
24
+ SmtpMock::Types::String => %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_quit_cmd
45
+ ].freeze
46
+ }.freeze
47
+
48
+ class << self
49
+ def call(**options)
50
+ new(options).to_command_line_args_string
51
+ rescue Dry::Struct::Error => error
52
+ raise SmtpMock::Error::Argument, error.message
53
+ end
54
+
55
+ private
56
+
57
+ def define_attribute
58
+ ->((type, attributes)) { attributes.each { |field| attribute?(field, type) } }
59
+ end
60
+ end
61
+
62
+ schema(schema.strict)
63
+
64
+ attribute?(:host, SmtpMock::Types::String.constrained(format: SmtpMock::CommandLineArgsBuilder::IP_ADDRESS_PATTERN))
65
+ SmtpMock::CommandLineArgsBuilder::PERMITTED_ATTRS.each(&define_attribute)
66
+
67
+ def to_command_line_args_string
68
+ to_h.map do |key, value|
69
+ key = to_camel_case(key)
70
+ value = format_by_type(value)
71
+ value ? "-#{key}=#{value}" : "-#{key}"
72
+ end.sort.join(' ')
73
+ end
74
+
75
+ private
76
+
77
+ def to_camel_case(symbol)
78
+ symbol.to_s.gsub(/_(\D)/) { ::Regexp.last_match(1).upcase }
79
+ end
80
+
81
+ def to_quoted(string)
82
+ "\"#{string}\""
83
+ end
84
+
85
+ def format_by_type(object)
86
+ case object
87
+ when ::Array then to_quoted(object.join(','))
88
+ when ::String then to_quoted(object)
89
+ when ::TrueClass then nil
90
+ else object
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmtpMock
4
+ module Error
5
+ require_relative '../smtp_mock/error/argument'
6
+ require_relative '../smtp_mock/error/dependency'
7
+ require_relative '../smtp_mock/error/server'
8
+ end
9
+
10
+ require_relative '../smtp_mock/version'
11
+ require_relative '../smtp_mock/dependency'
12
+ require_relative '../smtp_mock/command_line_args_builder'
13
+ require_relative '../smtp_mock/cli/resolver'
14
+ require_relative '../smtp_mock/cli'
15
+ require_relative '../smtp_mock/server/port'
16
+ require_relative '../smtp_mock/server/process'
17
+ require_relative '../smtp_mock/server'
18
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmtpMock
4
+ module Dependency
5
+ BINARY_SHORTCUT = 'smtpmock'
6
+ SYMLINK = "/usr/local/bin/#{BINARY_SHORTCUT}"
7
+
8
+ class << self
9
+ def smtpmock_path_by_symlink
10
+ ::Kernel.public_send(:`, "readlink #{SmtpMock::Dependency::SYMLINK}")
11
+ end
12
+
13
+ def smtpmock?
14
+ !smtpmock_path_by_symlink.empty?
15
+ end
16
+
17
+ def verify_dependencies
18
+ raise SmtpMock::Error::Dependency, SmtpMock::Error::Dependency::SMTPMOCK_NOT_INSTALLED unless smtpmock?
19
+ end
20
+
21
+ def compose_command(command_line_args)
22
+ "#{SmtpMock::Dependency::BINARY_SHORTCUT} #{command_line_args}".strip
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmtpMock
4
+ module Error
5
+ Argument = ::Class.new(::ArgumentError)
6
+ end
7
+ end
@@ -0,0 +1,9 @@
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
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmtpMock
4
+ module Error
5
+ Server = ::Class.new(::RuntimeError)
6
+ end
7
+ end
@@ -0,0 +1,27 @@
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
@@ -0,0 +1,44 @@
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
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmtpMock
4
+ class Server
5
+ attr_reader :pid, :port
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
+ 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
@@ -0,0 +1,15 @@
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