tcp-client 0.7.0 → 0.8.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 +4 -4
- data/README.md +18 -16
- data/lib/tcp-client/address.rb +6 -2
- data/lib/tcp-client/configuration.rb +16 -6
- data/lib/tcp-client/errors.rb +4 -2
- data/lib/tcp-client/ssl_socket.rb +19 -2
- data/lib/tcp-client/version.rb +1 -1
- data/lib/tcp-client.rb +43 -22
- data/rakefile.rb +2 -5
- data/sample/google_ssl.rb +7 -6
- data/spec/helper.rb +12 -0
- data/spec/tcp-client/address_spec.rb +158 -0
- data/spec/tcp-client/configuration_spec.rb +308 -0
- data/spec/tcp-client/default_configuration_spec.rb +22 -0
- data/spec/tcp-client/version_spec.rb +13 -0
- data/spec/tcp_client_spec.rb +596 -0
- data/tcp-client.gemspec +2 -2
- metadata +16 -18
- data/test/helper.rb +0 -41
- data/test/tcp-client/address_test.rb +0 -65
- data/test/tcp-client/configuration_test.rb +0 -141
- data/test/tcp-client/deadline_test.rb +0 -26
- data/test/tcp-client/default_configuration_test.rb +0 -59
- data/test/tcp-client/version_test.rb +0 -9
- data/test/tcp_client_test.rb +0 -184
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 14f787ad4e8e06910bfebf67755ae7142183df4c4fb090edce84aae7d497a2b6
|
4
|
+
data.tar.gz: 6349bea2eac6bac053c724e0c6a162b1fe99502e94cf1c6734854d97f194f37a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b53fafbf091a67a329832663c058b4e8243b36a779f8bc52cc7f37de556ad0aa016245e0a059489b972e6afaccfea7d26a63a735686d5f50d88036a2f4ecaca1
|
7
|
+
data.tar.gz: 4a6ff368e221073c5addfab51e31df1b094ee3f69e09ebe3f38d6d552ebc761b14c552a1a2cd90dcfd148b1ba34ebb1b2e3abfcea9622ded8413870f21a60285
|
data/README.md
CHANGED
@@ -11,20 +11,22 @@ This Gem implements a TCP client with (optional) SSL support. It is an easy to u
|
|
11
11
|
```ruby
|
12
12
|
require 'tcp-client'
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
14
|
+
# create a configuration:
|
15
|
+
# - don't use internal buffering
|
16
|
+
# - use TLS 1.2 or TLS 1.3
|
17
|
+
cfg = TCPClient::Configuration.create(
|
18
|
+
buffered: false,
|
19
|
+
ssl_params: {min_version: :TLS1_2, max_version: :TLS1_3}
|
20
|
+
)
|
21
|
+
|
22
|
+
# request to Google.com:
|
23
|
+
# - limit all network interactions to 1.5 seconds
|
24
|
+
# - use the Configuration cfg
|
25
|
+
# - send a simple HTTP get request
|
26
|
+
# - read 12 byte: "HTTP/1.1 " + 3 byte HTTP status code
|
27
|
+
TCPClient.with_deadline(1.5, 'www.google.com:443', cfg) do |client|
|
28
|
+
client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n") # >= 40
|
29
|
+
client.read(12) # => "HTTP/1.1 200"
|
28
30
|
end
|
29
31
|
```
|
30
32
|
|
@@ -41,13 +43,13 @@ gem 'tcp-client'
|
|
41
43
|
and install it by running Bundler:
|
42
44
|
|
43
45
|
```bash
|
44
|
-
|
46
|
+
bundle
|
45
47
|
```
|
46
48
|
|
47
49
|
To install the gem globally use:
|
48
50
|
|
49
51
|
```bash
|
50
|
-
|
52
|
+
gem install tcp-client
|
51
53
|
```
|
52
54
|
|
53
55
|
After that you need only a single line of code in your project to have all tools on board:
|
data/lib/tcp-client/address.rb
CHANGED
@@ -25,12 +25,16 @@ class TCPClient
|
|
25
25
|
"#{@hostname}:#{@addrinfo.ip_port}"
|
26
26
|
end
|
27
27
|
|
28
|
-
def
|
28
|
+
def to_hash
|
29
29
|
{ host: @hostname, port: @addrinfo.ip_port }
|
30
30
|
end
|
31
31
|
|
32
|
+
def to_h(*args)
|
33
|
+
args.empty? ? to_hash : to_hash.slice(*args)
|
34
|
+
end
|
35
|
+
|
32
36
|
def ==(other)
|
33
|
-
|
37
|
+
to_hash == other.to_hash
|
34
38
|
end
|
35
39
|
alias eql? ==
|
36
40
|
|
@@ -13,6 +13,7 @@ class TCPClient
|
|
13
13
|
attr_reader :buffered,
|
14
14
|
:keep_alive,
|
15
15
|
:reverse_lookup,
|
16
|
+
:normalize_network_errors,
|
16
17
|
:connect_timeout,
|
17
18
|
:read_timeout,
|
18
19
|
:write_timeout,
|
@@ -27,6 +28,7 @@ class TCPClient
|
|
27
28
|
@connect_timeout_error = ConnectTimeoutError
|
28
29
|
@read_timeout_error = ReadTimeoutError
|
29
30
|
@write_timeout_error = WriteTimeoutError
|
31
|
+
@normalize_network_errors = false
|
30
32
|
options.each_pair { |attribute, value| set(attribute, value) }
|
31
33
|
end
|
32
34
|
|
@@ -47,8 +49,8 @@ class TCPClient
|
|
47
49
|
|
48
50
|
def ssl=(value)
|
49
51
|
@ssl_params =
|
50
|
-
if
|
51
|
-
Hash[value]
|
52
|
+
if value.respond_to?(:to_hash)
|
53
|
+
Hash[value.to_hash]
|
52
54
|
else
|
53
55
|
value ? {} : nil
|
54
56
|
end
|
@@ -66,6 +68,10 @@ class TCPClient
|
|
66
68
|
@reverse_lookup = value ? true : false
|
67
69
|
end
|
68
70
|
|
71
|
+
def normalize_network_errors=(value)
|
72
|
+
@normalize_network_errors = value ? true : false
|
73
|
+
end
|
74
|
+
|
69
75
|
def timeout=(seconds)
|
70
76
|
@connect_timeout = @write_timeout = @read_timeout = seconds(seconds)
|
71
77
|
end
|
@@ -103,23 +109,27 @@ class TCPClient
|
|
103
109
|
@write_timeout_error = exception
|
104
110
|
end
|
105
111
|
|
106
|
-
def
|
112
|
+
def to_hash
|
107
113
|
{
|
108
114
|
buffered: @buffered,
|
109
115
|
keep_alive: @keep_alive,
|
110
116
|
reverse_lookup: @reverse_lookup,
|
111
117
|
connect_timeout: @connect_timeout,
|
112
|
-
read_timeout: @read_timeout,
|
113
|
-
write_timeout: @write_timeout,
|
114
118
|
connect_timeout_error: @connect_timeout_error,
|
119
|
+
read_timeout: @read_timeout,
|
115
120
|
read_timeout_error: @read_timeout_error,
|
121
|
+
write_timeout: @write_timeout,
|
116
122
|
write_timeout_error: @write_timeout_error,
|
117
123
|
ssl_params: @ssl_params
|
118
124
|
}
|
119
125
|
end
|
120
126
|
|
127
|
+
def to_h(*args)
|
128
|
+
args.empty? ? to_hash : to_hash.slice(*args)
|
129
|
+
end
|
130
|
+
|
121
131
|
def ==(other)
|
122
|
-
|
132
|
+
to_hash == other.to_hash
|
123
133
|
end
|
124
134
|
alias eql? ==
|
125
135
|
|
data/lib/tcp-client/errors.rb
CHANGED
@@ -31,13 +31,15 @@ class TCPClient
|
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
-
|
34
|
+
NetworkError = Class.new(StandardError)
|
35
|
+
|
36
|
+
class NotConnectedError < NetworkError
|
35
37
|
def initialize
|
36
38
|
super('client not connected')
|
37
39
|
end
|
38
40
|
end
|
39
41
|
|
40
|
-
class TimeoutError <
|
42
|
+
class TimeoutError < NetworkError
|
41
43
|
def initialize(message = nil)
|
42
44
|
super(message || "unable to #{action} in time")
|
43
45
|
end
|
@@ -18,6 +18,7 @@ class TCPClient
|
|
18
18
|
super(socket, create_context(ssl_params))
|
19
19
|
self.sync_close = true
|
20
20
|
self.hostname = address.hostname
|
21
|
+
check_new_session if @new_session
|
21
22
|
deadline.valid? ? connect_with_deadline(deadline, exception) : connect
|
22
23
|
post_connection_check(address.hostname) if should_verify?(ssl_params)
|
23
24
|
end
|
@@ -25,7 +26,19 @@ class TCPClient
|
|
25
26
|
private
|
26
27
|
|
27
28
|
def create_context(ssl_params)
|
28
|
-
|
29
|
+
@new_session = nil
|
30
|
+
::OpenSSL::SSL::SSLContext.new.tap do |ctx|
|
31
|
+
ctx.set_params(ssl_params)
|
32
|
+
ctx.session_cache_mode = CONTEXT_CACHE_MODE
|
33
|
+
ctx.session_new_cb = proc { |_, sess| @new_session = sess }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def check_new_session
|
38
|
+
time = @new_session.time.to_f + @new_session.timeout
|
39
|
+
if Process.clock_gettime(Process::CLOCK_REALTIME) < time
|
40
|
+
self.session = @new_session
|
41
|
+
end
|
29
42
|
end
|
30
43
|
|
31
44
|
def connect_with_deadline(deadline, exception)
|
@@ -33,9 +46,13 @@ class TCPClient
|
|
33
46
|
end
|
34
47
|
|
35
48
|
def should_verify?(ssl_params)
|
36
|
-
ssl_params[:verify_mode] != OpenSSL::SSL::VERIFY_NONE &&
|
49
|
+
ssl_params[:verify_mode] != ::OpenSSL::SSL::VERIFY_NONE &&
|
37
50
|
context.verify_hostname
|
38
51
|
end
|
52
|
+
|
53
|
+
CONTEXT_CACHE_MODE =
|
54
|
+
::OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT |
|
55
|
+
::OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
|
39
56
|
end
|
40
57
|
|
41
58
|
private_constant(:SSLSocket)
|
data/lib/tcp-client/version.rb
CHANGED
data/lib/tcp-client.rb
CHANGED
@@ -10,7 +10,7 @@ require_relative 'tcp-client/default_configuration'
|
|
10
10
|
require_relative 'tcp-client/version'
|
11
11
|
|
12
12
|
class TCPClient
|
13
|
-
def self.open(address, configuration =
|
13
|
+
def self.open(address, configuration = nil)
|
14
14
|
client = new
|
15
15
|
client.connect(Address.new(address), configuration)
|
16
16
|
block_given? ? yield(client) : client
|
@@ -18,11 +18,7 @@ class TCPClient
|
|
18
18
|
client.close if block_given?
|
19
19
|
end
|
20
20
|
|
21
|
-
def self.with_deadline(
|
22
|
-
timeout,
|
23
|
-
address,
|
24
|
-
configuration = Configuration.default
|
25
|
-
)
|
21
|
+
def self.with_deadline(timeout, address, configuration = nil)
|
26
22
|
client = nil
|
27
23
|
raise(NoBlockGivenError) unless block_given?
|
28
24
|
address = Address.new(address)
|
@@ -36,19 +32,15 @@ class TCPClient
|
|
36
32
|
|
37
33
|
attr_reader :address, :configuration
|
38
34
|
|
39
|
-
def initialize
|
40
|
-
@socket = @address = @deadline = @configuration = nil
|
41
|
-
end
|
42
|
-
|
43
35
|
def to_s
|
44
36
|
@address&.to_s || ''
|
45
37
|
end
|
46
38
|
|
47
|
-
def connect(address, configuration, timeout: nil, exception: nil)
|
39
|
+
def connect(address, configuration = nil, timeout: nil, exception: nil)
|
48
40
|
close if @socket
|
49
41
|
raise(NoOpenSSLError) if configuration.ssl? && !defined?(SSLSocket)
|
50
42
|
@address = Address.new(address)
|
51
|
-
@configuration = configuration.dup
|
43
|
+
@configuration = (configuration || Configuration.default).dup
|
52
44
|
@socket = create_socket(timeout, exception)
|
53
45
|
self
|
54
46
|
end
|
@@ -56,7 +48,7 @@ class TCPClient
|
|
56
48
|
def close
|
57
49
|
@socket&.close
|
58
50
|
self
|
59
|
-
rescue
|
51
|
+
rescue *NETWORK_ERRORS
|
60
52
|
self
|
61
53
|
ensure
|
62
54
|
@socket = @deadline = nil
|
@@ -79,23 +71,27 @@ class TCPClient
|
|
79
71
|
def read(nbytes = nil, timeout: nil, exception: nil)
|
80
72
|
raise(NotConnectedError) if closed?
|
81
73
|
deadline = create_deadline(timeout, configuration.read_timeout)
|
82
|
-
return @socket.read(nbytes) unless deadline.valid?
|
74
|
+
return stem_errors { @socket.read(nbytes) } unless deadline.valid?
|
83
75
|
exception ||= configuration.read_timeout_error
|
84
|
-
|
76
|
+
stem_errors(exception) do
|
77
|
+
@socket.read_with_deadline(nbytes, deadline, exception)
|
78
|
+
end
|
85
79
|
end
|
86
80
|
|
87
81
|
def write(*msg, timeout: nil, exception: nil)
|
88
82
|
raise(NotConnectedError) if closed?
|
89
83
|
deadline = create_deadline(timeout, configuration.write_timeout)
|
90
|
-
return @socket.write(*msg) unless deadline.valid?
|
84
|
+
return stem_errors { @socket.write(*msg) } unless deadline.valid?
|
91
85
|
exception ||= configuration.write_timeout_error
|
92
|
-
|
93
|
-
|
86
|
+
stem_errors(exception) do
|
87
|
+
msg.sum do |chunk|
|
88
|
+
@socket.write_with_deadline(chunk.b, deadline, exception)
|
89
|
+
end
|
94
90
|
end
|
95
91
|
end
|
96
92
|
|
97
93
|
def flush
|
98
|
-
@socket&.flush
|
94
|
+
stem_errors { @socket&.flush }
|
99
95
|
self
|
100
96
|
end
|
101
97
|
|
@@ -108,8 +104,33 @@ class TCPClient
|
|
108
104
|
def create_socket(timeout, exception)
|
109
105
|
deadline = create_deadline(timeout, configuration.connect_timeout)
|
110
106
|
exception ||= configuration.connect_timeout_error
|
111
|
-
|
112
|
-
|
113
|
-
|
107
|
+
stem_errors(exception) do
|
108
|
+
@socket = TCPSocket.new(address, configuration, deadline, exception)
|
109
|
+
return @socket unless configuration.ssl?
|
110
|
+
SSLSocket.new(@socket, address, configuration, deadline, exception)
|
111
|
+
end
|
114
112
|
end
|
113
|
+
|
114
|
+
def stem_errors(except = nil)
|
115
|
+
yield
|
116
|
+
rescue *NETWORK_ERRORS => e
|
117
|
+
raise unless configuration.normalize_network_errors
|
118
|
+
(except && e.is_a?(except)) ? raise : raise(NetworkError, e)
|
119
|
+
end
|
120
|
+
|
121
|
+
NETWORK_ERRORS =
|
122
|
+
[
|
123
|
+
Errno::EADDRNOTAVAIL,
|
124
|
+
Errno::ECONNABORTED,
|
125
|
+
Errno::ECONNREFUSED,
|
126
|
+
Errno::ECONNRESET,
|
127
|
+
Errno::EHOSTUNREACH,
|
128
|
+
Errno::EINVAL,
|
129
|
+
Errno::ENETUNREACH,
|
130
|
+
Errno::EPIPE,
|
131
|
+
IOError,
|
132
|
+
SocketError
|
133
|
+
].tap do |errors|
|
134
|
+
errors << ::OpenSSL::SSL::SSLError if defined?(::OpenSSL::SSL::SSLError)
|
135
|
+
end.freeze
|
115
136
|
end
|
data/rakefile.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'rake/clean'
|
4
|
-
require 'rake/testtask'
|
5
4
|
require 'bundler/gem_tasks'
|
5
|
+
require 'rspec/core/rake_task'
|
6
6
|
|
7
7
|
$stdout.sync = $stderr.sync = true
|
8
8
|
|
@@ -10,7 +10,4 @@ CLOBBER << 'prj'
|
|
10
10
|
|
11
11
|
task(:default) { exec('rake --tasks') }
|
12
12
|
|
13
|
-
|
14
|
-
task.pattern = 'test/**/*_test.rb'
|
15
|
-
task.warning = task.verbose = true
|
16
|
-
end
|
13
|
+
RSpec::Core::RakeTask.new { |task| task.ruby_opts = %w[-w] }
|
data/sample/google_ssl.rb
CHANGED
@@ -2,23 +2,24 @@
|
|
2
2
|
|
3
3
|
require_relative '../lib/tcp-client'
|
4
4
|
|
5
|
-
# create a configuration
|
6
|
-
# - use TLS 1.2
|
5
|
+
# create a configuration:
|
7
6
|
# - don't use internal buffering
|
7
|
+
# - use TLS 1.2 or TLS 1.3
|
8
8
|
cfg =
|
9
9
|
TCPClient::Configuration.create(
|
10
10
|
buffered: false,
|
11
11
|
ssl_params: {
|
12
|
-
|
12
|
+
min_version: :TLS1_2,
|
13
|
+
max_version: :TLS1_3
|
13
14
|
}
|
14
15
|
)
|
15
16
|
|
16
|
-
# request to Google:
|
17
|
-
# - limit all interactions to
|
17
|
+
# request to Google.com:
|
18
|
+
# - limit all network interactions to 1.5 seconds
|
18
19
|
# - use the Configuration cfg
|
19
20
|
# - send a simple HTTP get request
|
20
21
|
# - read 12 byte: "HTTP/1.1 " + 3 byte HTTP status code
|
21
|
-
TCPClient.with_deadline(
|
22
|
+
TCPClient.with_deadline(1.5, 'www.google.com:443', cfg) do |client|
|
22
23
|
p client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n")
|
23
24
|
p client.read(12)
|
24
25
|
end
|
data/spec/helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rspec/core'
|
4
|
+
require_relative '../lib/tcp-client'
|
5
|
+
|
6
|
+
$stdout.sync = $stderr.sync = true
|
7
|
+
|
8
|
+
RSpec.configure do |config|
|
9
|
+
config.disable_monkey_patching!
|
10
|
+
config.warnings = true
|
11
|
+
config.order = :random
|
12
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../helper'
|
4
|
+
|
5
|
+
RSpec.describe TCPClient::Address do
|
6
|
+
describe '.new' do
|
7
|
+
context 'when called with an Integer parameter' do
|
8
|
+
subject(:address) { TCPClient::Address.new(42) }
|
9
|
+
|
10
|
+
it 'points to the given port on localhost' do
|
11
|
+
expect(address.hostname).to eq 'localhost'
|
12
|
+
expect(address.to_s).to eq 'localhost:42'
|
13
|
+
expect(address.addrinfo.ip_port).to be 42
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'uses IPv6' do
|
17
|
+
expect(address.addrinfo.ip?).to be true
|
18
|
+
expect(address.addrinfo.ipv6?).to be true
|
19
|
+
expect(address.addrinfo.ipv4?).to be false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'when called with an Addrinfo' do
|
24
|
+
subject(:address) { TCPClient::Address.new(addrinfo) }
|
25
|
+
let(:addrinfo) { Addrinfo.tcp('::1', 42) }
|
26
|
+
|
27
|
+
it 'uses the given Addrinfo' do
|
28
|
+
expect(address.addrinfo).to eq addrinfo
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'points to the given host and port' do
|
32
|
+
expect(address.hostname).to eq addrinfo.getnameinfo[0]
|
33
|
+
expect(address.addrinfo.ip_port).to be 42
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'uses IPv6' do
|
37
|
+
expect(address.addrinfo.ip?).to be true
|
38
|
+
expect(address.addrinfo.ipv6?).to be true
|
39
|
+
expect(address.addrinfo.ipv4?).to be false
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'when called with a String' do
|
44
|
+
context 'when a host name and port is provided' do
|
45
|
+
subject(:address) { TCPClient::Address.new('localhost:42') }
|
46
|
+
|
47
|
+
it 'points to the given host and port' do
|
48
|
+
expect(address.hostname).to eq 'localhost'
|
49
|
+
expect(address.to_s).to eq 'localhost:42'
|
50
|
+
expect(address.addrinfo.ip_port).to be 42
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'uses IPv6' do
|
54
|
+
expect(address.addrinfo.ip?).to be true
|
55
|
+
expect(address.addrinfo.ipv6?).to be true
|
56
|
+
expect(address.addrinfo.ipv4?).to be false
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'when only a port is provided' do
|
61
|
+
subject(:address) { TCPClient::Address.new(':21') }
|
62
|
+
|
63
|
+
it 'points to the given port on localhost' do
|
64
|
+
expect(address.hostname).to eq ''
|
65
|
+
expect(address.to_s).to eq ':21'
|
66
|
+
expect(address.addrinfo.ip_port).to be 21
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'uses IPv4' do
|
70
|
+
expect(address.addrinfo.ip?).to be true
|
71
|
+
expect(address.addrinfo.ipv6?).to be false
|
72
|
+
expect(address.addrinfo.ipv4?).to be true
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'when an IPv6 address is provided' do
|
77
|
+
subject(:address) { TCPClient::Address.new('[::1]:42') }
|
78
|
+
|
79
|
+
it 'points to the given port on localhost' do
|
80
|
+
expect(address.hostname).to eq '::1'
|
81
|
+
expect(address.to_s).to eq '[::1]:42'
|
82
|
+
expect(address.addrinfo.ip_port).to be 42
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'uses IPv6' do
|
86
|
+
expect(address.addrinfo.ip?).to be true
|
87
|
+
expect(address.addrinfo.ipv6?).to be true
|
88
|
+
expect(address.addrinfo.ipv4?).to be false
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe '#to_hash' do
|
95
|
+
subject(:address) { TCPClient::Address.new('localhost:42') }
|
96
|
+
|
97
|
+
it 'returns itself as an Hash' do
|
98
|
+
expect(address.to_hash).to eq(host: 'localhost', port: 42)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe '#to_h' do
|
103
|
+
subject(:address) { TCPClient::Address.new('localhost:42') }
|
104
|
+
|
105
|
+
it 'returns itself as an Hash' do
|
106
|
+
expect(address.to_h).to eq(host: 'localhost', port: 42)
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'allows to specify the keys the result should contain' do
|
110
|
+
expect(address.to_h(:port)).to eq(port: 42)
|
111
|
+
expect(address.to_h(:host)).to eq(host: 'localhost')
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe 'comparison' do
|
116
|
+
context 'comparing two equal instances' do
|
117
|
+
let(:address_a) { TCPClient::Address.new('localhost:42') }
|
118
|
+
let(:address_b) { TCPClient::Address.new('localhost:42') }
|
119
|
+
|
120
|
+
it 'compares to equal' do
|
121
|
+
expect(address_a).to eq address_b
|
122
|
+
end
|
123
|
+
|
124
|
+
context 'using the == opperator' do
|
125
|
+
it 'compares to equal' do
|
126
|
+
expect(address_a == address_b).to be true
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
context 'using the === opperator' do
|
131
|
+
it 'compares to equal' do
|
132
|
+
expect(address_a === address_b).to be true
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
context 'comparing two non-equal instances' do
|
138
|
+
let(:address_a) { TCPClient::Address.new('localhost:42') }
|
139
|
+
let(:address_b) { TCPClient::Address.new('localhost:21') }
|
140
|
+
|
141
|
+
it 'compares not to equal' do
|
142
|
+
expect(address_a).not_to eq address_b
|
143
|
+
end
|
144
|
+
|
145
|
+
context 'using the == opperator' do
|
146
|
+
it 'compares not to equal' do
|
147
|
+
expect(address_a == address_b).to be false
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
context 'using the === opperator' do
|
152
|
+
it 'compares not to equal' do
|
153
|
+
expect(address_a === address_b).to be false
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|