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.
- checksums.yaml +7 -0
- data/.circleci/config.yml +132 -0
- data/.codeclimate.yml +13 -0
- data/.github/BRANCH_NAMING_CONVENTION.md +36 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +28 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +27 -0
- data/.github/ISSUE_TEMPLATE/issue_report.md +28 -0
- data/.github/ISSUE_TEMPLATE/question.md +22 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +49 -0
- data/.gitignore +12 -0
- data/.overcommit.yml +32 -0
- data/.reek.yml +30 -0
- data/.rspec +3 -0
- data/.rubocop.yml +396 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +9 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +46 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +242 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/bin/smtp_mock +6 -0
- data/lib/smtp_mock/cli/resolver.rb +61 -0
- data/lib/smtp_mock/cli.rb +17 -0
- data/lib/smtp_mock/command_line_args_builder.rb +94 -0
- data/lib/smtp_mock/core.rb +18 -0
- data/lib/smtp_mock/dependency.rb +26 -0
- data/lib/smtp_mock/error/argument.rb +7 -0
- data/lib/smtp_mock/error/dependency.rb +9 -0
- data/lib/smtp_mock/error/server.rb +7 -0
- data/lib/smtp_mock/server/port.rb +27 -0
- data/lib/smtp_mock/server/process.rb +44 -0
- data/lib/smtp_mock/server.rb +59 -0
- data/lib/smtp_mock/test_framework/rspec/helper.rb +15 -0
- data/lib/smtp_mock/test_framework/rspec/interface.rb +29 -0
- data/lib/smtp_mock/test_framework/rspec.rb +10 -0
- data/lib/smtp_mock/version.rb +5 -0
- data/lib/smtp_mock.rb +19 -0
- data/smtp_mock.gemspec +49 -0
- data/tmp/.gitkeep +0 -0
- metadata +288 -0
data/README.md
ADDED
@@ -0,0 +1,242 @@
|
|
1
|
+
# 
|
2
|
+
|
3
|
+
[](https://codeclimate.com/github/mocktools/ruby-smtp-mock/maintainability)
|
4
|
+
[](https://codeclimate.com/github/mocktools/ruby-smtp-mock/test_coverage)
|
5
|
+
[](https://circleci.com/gh/mocktools/ruby-smtp-mock/tree/master)
|
6
|
+
[](https://badge.fury.io/rb/smtp_mock)
|
7
|
+
[](https://rubygems.org/gems/smtp_mock)
|
8
|
+
[](LICENSE.txt)
|
9
|
+
[](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
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
data/bin/smtp_mock
ADDED
@@ -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,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
|