costan-rtunnel 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +13 -0
- data/LICENSE +21 -0
- data/Manifest +49 -0
- data/README.markdown +84 -0
- data/Rakefile +45 -0
- data/bin/rtunnel_client +4 -0
- data/bin/rtunnel_server +4 -0
- data/lib/rtunnel.rb +20 -0
- data/lib/rtunnel/client.rb +308 -0
- data/lib/rtunnel/command_processor.rb +62 -0
- data/lib/rtunnel/command_protocol.rb +50 -0
- data/lib/rtunnel/commands.rb +233 -0
- data/lib/rtunnel/connection_id.rb +24 -0
- data/lib/rtunnel/core.rb +58 -0
- data/lib/rtunnel/crypto.rb +106 -0
- data/lib/rtunnel/frame_protocol.rb +34 -0
- data/lib/rtunnel/io_extensions.rb +54 -0
- data/lib/rtunnel/leak.rb +35 -0
- data/lib/rtunnel/rtunnel_client_cmd.rb +41 -0
- data/lib/rtunnel/rtunnel_server_cmd.rb +32 -0
- data/lib/rtunnel/server.rb +351 -0
- data/lib/rtunnel/socket_factory.rb +119 -0
- data/spec/client_spec.rb +47 -0
- data/spec/cmds_spec.rb +127 -0
- data/spec/integration_spec.rb +105 -0
- data/spec/server_spec.rb +21 -0
- data/spec/spec_helper.rb +3 -0
- data/test/command_stubs.rb +77 -0
- data/test/protocol_mocks.rb +43 -0
- data/test/scenario_connection.rb +109 -0
- data/test/test_client.rb +48 -0
- data/test/test_command_protocol.rb +82 -0
- data/test/test_commands.rb +49 -0
- data/test/test_connection_id.rb +30 -0
- data/test/test_crypto.rb +127 -0
- data/test/test_frame_protocol.rb +109 -0
- data/test/test_io_extensions.rb +70 -0
- data/test/test_server.rb +70 -0
- data/test/test_socket_factory.rb +42 -0
- data/test/test_tunnel.rb +186 -0
- data/test_data/authorized_keys2 +4 -0
- data/test_data/known_hosts +4 -0
- data/test_data/random_rsa_key +27 -0
- data/test_data/ssh_host_dsa_key +12 -0
- data/test_data/ssh_host_rsa_key +27 -0
- data/tests/_ab_test.rb +16 -0
- data/tests/_stress_test.rb +96 -0
- data/tests/lo_http_server.rb +55 -0
- metadata +121 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
# Mocks the sending end of an EventMachine connection.
|
2
|
+
# The sent data is concatenated in a string available by calling #string.
|
3
|
+
class EmSendMock
|
4
|
+
attr_reader :string
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@string = ''
|
8
|
+
end
|
9
|
+
|
10
|
+
def send_data(data)
|
11
|
+
@string << data
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Mocks the receiving end of an EventMachine connection.
|
16
|
+
# The data to be received is passed as an array of strings to the constructor.
|
17
|
+
# Calling #replay mocks receiving the data.
|
18
|
+
class EmReceiveMock
|
19
|
+
attr_accessor :strings
|
20
|
+
attr_accessor :objects
|
21
|
+
|
22
|
+
def initialize(strings = [''])
|
23
|
+
@strings = strings
|
24
|
+
@objects = []
|
25
|
+
end
|
26
|
+
|
27
|
+
def replay
|
28
|
+
@strings.each { |str| receive_data str }
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def receive_object(object)
|
33
|
+
@objects << object
|
34
|
+
end
|
35
|
+
|
36
|
+
# Declares the name of the object to be received. For instance, a frame
|
37
|
+
# protocol would use :frame for name. This generates a receive_frame method,
|
38
|
+
# and a frames accessor.
|
39
|
+
def self.object_name(name)
|
40
|
+
alias_method "receive_#{name}".to_sym, :receive_object
|
41
|
+
alias_method "#{name}s".to_sym, :objects
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'eventmachine'
|
3
|
+
|
4
|
+
# Event Machine connection that runs a fixed send-expect scenario.
|
5
|
+
class ScenarioConnection < EventMachine::Connection
|
6
|
+
def initialize(test_case, scenario = nil)
|
7
|
+
super()
|
8
|
+
|
9
|
+
@test_case = test_case
|
10
|
+
@scenario = scenario || yield
|
11
|
+
@ignore_unbind = false
|
12
|
+
next_step
|
13
|
+
end
|
14
|
+
|
15
|
+
def post_init
|
16
|
+
scenario_can_send
|
17
|
+
end
|
18
|
+
|
19
|
+
def receive_data(data)
|
20
|
+
scenario_received data
|
21
|
+
end
|
22
|
+
|
23
|
+
def unbind
|
24
|
+
return if @ignore_unbind
|
25
|
+
|
26
|
+
unless @step and @step.first == :unbind
|
27
|
+
scenario_fail "Received unexpected unbind\n"
|
28
|
+
end
|
29
|
+
next_step
|
30
|
+
while @step and @step.first == :proc
|
31
|
+
@step.last.call
|
32
|
+
next_step
|
33
|
+
end
|
34
|
+
if @step and @step.first == :stop
|
35
|
+
scenario_stop @step.last
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Called when data is received. Plays the connection scenario.
|
40
|
+
def scenario_received(data)
|
41
|
+
unless @step and @step.first == :recv
|
42
|
+
scenario_fail "Received unexpected data: #{data}\n"
|
43
|
+
end
|
44
|
+
|
45
|
+
@test_case.send :assert_equal, @step.last, data
|
46
|
+
next_step
|
47
|
+
scenario_can_send
|
48
|
+
end
|
49
|
+
|
50
|
+
# Called when data can be sent. Plays the connection scenario.
|
51
|
+
def scenario_can_send
|
52
|
+
while @step
|
53
|
+
case @step.first
|
54
|
+
when :proc
|
55
|
+
@step.last.call
|
56
|
+
when :send
|
57
|
+
send_data @step.last
|
58
|
+
else
|
59
|
+
break
|
60
|
+
end
|
61
|
+
next_step
|
62
|
+
end
|
63
|
+
|
64
|
+
unless @step
|
65
|
+
# EM might stifle this exception and reraise
|
66
|
+
msg = "Scenario completed prematurely"
|
67
|
+
$stderr.puts msg
|
68
|
+
fail msg
|
69
|
+
end
|
70
|
+
|
71
|
+
case @step.first
|
72
|
+
when :receive
|
73
|
+
# wait to receive
|
74
|
+
return
|
75
|
+
when :unbind
|
76
|
+
# wait for unbind
|
77
|
+
return
|
78
|
+
when :close
|
79
|
+
@ignore_unbind = true
|
80
|
+
close_connection_after_writing
|
81
|
+
next_step
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def scenario_fail(fail_message)
|
86
|
+
if @step
|
87
|
+
fail_message << "Expected to #{@step.inspect}"
|
88
|
+
else
|
89
|
+
fail_message << "Scenario was completed\n"
|
90
|
+
end
|
91
|
+
@test_case.send :fail, fail_message
|
92
|
+
end
|
93
|
+
|
94
|
+
def scenario_stop(stop_proc)
|
95
|
+
if stop_proc.kind_of? Proc
|
96
|
+
# call the proc, then give em time to stop all its servers
|
97
|
+
stop_proc.call
|
98
|
+
EventMachine.add_timer 0.1 do
|
99
|
+
EventMachine.stop_event_loop
|
100
|
+
end
|
101
|
+
else
|
102
|
+
EventMachine.stop_event_loop
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def next_step
|
107
|
+
@step = @scenario.shift
|
108
|
+
end
|
109
|
+
end
|
data/test/test_client.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rtunnel'
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
require 'resolv'
|
5
|
+
require 'test/unit'
|
6
|
+
|
7
|
+
class ClientTest < Test::Unit::TestCase
|
8
|
+
def setup
|
9
|
+
@client = RTunnel::Client.new(:control_address => 'localhost',
|
10
|
+
:remote_listen_address => '9199',
|
11
|
+
:tunnel_to_address => '4444',
|
12
|
+
:private_key => 'test_data/ssh_host_rsa_key',
|
13
|
+
:tunnel_timeout => 5)
|
14
|
+
@localhost_addr = Resolv.getaddress 'localhost'
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_options
|
18
|
+
client = RTunnel::Client
|
19
|
+
assert_equal "18.241.3.100:#{RTunnel::DEFAULT_CONTROL_PORT}",
|
20
|
+
client.extract_control_address('18.241.3.100')
|
21
|
+
assert_equal "18.241.3.100:9199",
|
22
|
+
client.extract_control_address('18.241.3.100:9199')
|
23
|
+
assert_equal "#{@localhost_addr}:#{RTunnel::DEFAULT_CONTROL_PORT}",
|
24
|
+
client.extract_control_address('localhost')
|
25
|
+
assert_equal "#{@localhost_addr}:9199",
|
26
|
+
client.extract_control_address('localhost:9199')
|
27
|
+
|
28
|
+
assert_equal "0.0.0.0:9199",
|
29
|
+
client.extract_remote_listen_address('9199')
|
30
|
+
assert_equal "18.241.3.100:9199",
|
31
|
+
client.extract_remote_listen_address('18.241.3.100:9199')
|
32
|
+
assert_equal "#{@localhost_addr}:9199",
|
33
|
+
client.extract_remote_listen_address('localhost:9199')
|
34
|
+
|
35
|
+
assert_equal "18.241.3.100:9199",
|
36
|
+
client.extract_tunnel_to_address('18.241.3.100:9199')
|
37
|
+
assert_equal "#{@localhost_addr}:9199",
|
38
|
+
client.extract_tunnel_to_address('9199')
|
39
|
+
|
40
|
+
assert_equal RTunnel::TUNNEL_TIMEOUT, client.extract_tunnel_timeout(nil)
|
41
|
+
assert_equal 29, client.extract_tunnel_timeout(29)
|
42
|
+
|
43
|
+
assert_equal nil, client.extract_private_key(nil)
|
44
|
+
key = client.extract_private_key 'test_data/ssh_host_rsa_key'
|
45
|
+
assert_equal OpenSSL::PKey::RSA, key.class
|
46
|
+
assert_equal File.read('test_data/ssh_host_rsa_key'), key.to_pem
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'rtunnel'
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
|
5
|
+
require 'test/command_stubs.rb'
|
6
|
+
require 'test/protocol_mocks.rb'
|
7
|
+
|
8
|
+
# Send mock for commands.
|
9
|
+
class EmSendCommandsMock < EmSendMock
|
10
|
+
include RTunnel::CommandProtocol
|
11
|
+
end
|
12
|
+
|
13
|
+
# Receive mock for commands.
|
14
|
+
class EmReceiveCommandsMock < EmReceiveMock
|
15
|
+
include RTunnel::CommandProtocol
|
16
|
+
object_name :command
|
17
|
+
end
|
18
|
+
|
19
|
+
class CommandProtocolTest < Test::Unit::TestCase
|
20
|
+
include CommandStubs
|
21
|
+
C = RTunnel::Crypto
|
22
|
+
|
23
|
+
def setup
|
24
|
+
super
|
25
|
+
@send_mock = EmSendCommandsMock.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def teardown
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
def commandset_test(names)
|
33
|
+
@send_mock.outgoing_command_hasher = @hasher if @hasher
|
34
|
+
names.each do |name|
|
35
|
+
command = self.send "generate_#{name}".to_sym
|
36
|
+
@send_mock.send_command command
|
37
|
+
end
|
38
|
+
receive_mock = EmReceiveCommandsMock.new([@send_mock.string])
|
39
|
+
receive_mock.incoming_command_hasher = C::Hasher.new(@hasher.key) if @hasher
|
40
|
+
o_commands = receive_mock.replay.commands
|
41
|
+
self.assert_equal names.length, o_commands.length
|
42
|
+
names.each_index do |i|
|
43
|
+
self.send "verify_#{names[i]}", o_commands[i]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
CommandStubs.command_names.each do |name|
|
48
|
+
define_method "test_#{name}".to_sym do
|
49
|
+
commandset_test [name]
|
50
|
+
end
|
51
|
+
|
52
|
+
define_method "test_signed_#{name}".to_sym do
|
53
|
+
@hasher = C::Hasher.new
|
54
|
+
commandset_test [name]
|
55
|
+
end
|
56
|
+
|
57
|
+
define_method "test_signed_#{name}_has_signature".to_sym do
|
58
|
+
sig_send_mock = EmSendCommandsMock.new
|
59
|
+
sig_send_mock.outgoing_command_hasher = C::Hasher.new
|
60
|
+
outputs = [@send_mock, sig_send_mock].map do |mock|
|
61
|
+
mock.send_command self.send("generate_#{name}".to_sym)
|
62
|
+
mock.string
|
63
|
+
end
|
64
|
+
assert outputs.first.length < outputs.last.length,
|
65
|
+
"No signature generated"
|
66
|
+
end
|
67
|
+
|
68
|
+
define_method "test_signed_#{name}_enforces_signature".to_sym do
|
69
|
+
@send_mock.outgoing_command_hasher = hasher = C::Hasher.new
|
70
|
+
@send_mock.send_command self.send("generate_#{name}".to_sym)
|
71
|
+
signed_str = @send_mock.string
|
72
|
+
|
73
|
+
0.upto(signed_str.length - 1) do |i|
|
74
|
+
bad_str = signed_str.dup
|
75
|
+
bad_str[i] ^= 0x01
|
76
|
+
recv_mock = EmReceiveCommandsMock.new([bad_str])
|
77
|
+
recv_mock.incoming_command_hasher = C::Hasher.new hasher.key
|
78
|
+
assert_equal [], recv_mock.replay.commands
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rtunnel'
|
2
|
+
|
3
|
+
require 'stringio'
|
4
|
+
require 'test/unit'
|
5
|
+
|
6
|
+
require 'test/command_stubs.rb'
|
7
|
+
|
8
|
+
class CommandsTest < Test::Unit::TestCase
|
9
|
+
include CommandStubs
|
10
|
+
|
11
|
+
def setup
|
12
|
+
super
|
13
|
+
@str = StringIO.new
|
14
|
+
end
|
15
|
+
|
16
|
+
CommandStubs.command_names.each do |cmd|
|
17
|
+
define_method "test_#{cmd}_encode" do
|
18
|
+
command = self.send "generate_#{cmd}"
|
19
|
+
command.encode @str
|
20
|
+
@str.rewind
|
21
|
+
decoded_command = RTunnel::Command.decode @str
|
22
|
+
self.send "verify_#{cmd}", decoded_command
|
23
|
+
assert_equal "", @str.read, "Command #{cmd} did not consume its entire outpt"
|
24
|
+
end
|
25
|
+
|
26
|
+
define_method "test_#{cmd}_to_encoded_str" do
|
27
|
+
command = self.send "generate_#{cmd}"
|
28
|
+
command.encode @str
|
29
|
+
@str.rewind
|
30
|
+
assert_equal @str.read, command.to_encoded_str
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_all_encodes
|
35
|
+
sequence = CommandStubs.command_test_sequence
|
36
|
+
sequence.each { |cmd| self.send("generate_#{cmd}").encode @str }
|
37
|
+
@str.rewind
|
38
|
+
sequence.each do |cmd|
|
39
|
+
command = RTunnel::Command.decode(@str)
|
40
|
+
self.send "verify_#{cmd}", command
|
41
|
+
end
|
42
|
+
assert_equal "", @str.read
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_codes
|
46
|
+
# TODO(not_me): it'd be nice to have more than a smoke test here
|
47
|
+
assert_equal String, RTunnel::Command.printable_codes.class
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rtunnel'
|
2
|
+
|
3
|
+
require 'test/unit'
|
4
|
+
|
5
|
+
class ConnectionIdTest < Test::Unit::TestCase
|
6
|
+
class CidWrapper
|
7
|
+
include RTunnel::ConnectionId
|
8
|
+
end
|
9
|
+
|
10
|
+
def setup
|
11
|
+
@server = CidWrapper.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_ids_are_unique
|
15
|
+
n_ids = 1024
|
16
|
+
ids = (0...n_ids).map { @server.new_connection_id }
|
17
|
+
assert_equal n_ids, ids.uniq.length
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_id_sequences_are_not_trivial
|
21
|
+
n_servers = 256
|
22
|
+
n_ids = 16
|
23
|
+
sequences = (0...n_servers).map { CidWrapper.new }.map do |server|
|
24
|
+
(0...n_ids).map { server.new_connection_id }
|
25
|
+
end
|
26
|
+
0.upto(n_ids - 1) do |i|
|
27
|
+
assert_equal n_servers, sequences.map { |seq| seq[i] }.uniq.length
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/test/test_crypto.rb
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'rtunnel'
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
require 'test/unit'
|
5
|
+
|
6
|
+
class CryptoTest < Test::Unit::TestCase
|
7
|
+
C = RTunnel::Crypto
|
8
|
+
|
9
|
+
@@rsa_key_path = 'test_data/ssh_host_rsa_key'
|
10
|
+
@@dsa_key_path = 'test_data/ssh_host_dsa_key'
|
11
|
+
@@known_hosts_path = 'test_data/known_hosts'
|
12
|
+
@@authorized_keys_path = 'test_data/authorized_keys2'
|
13
|
+
|
14
|
+
def test_read_private_key
|
15
|
+
key = C.read_private_key @@rsa_key_path
|
16
|
+
assert_equal File.read(@@rsa_key_path), key.to_pem
|
17
|
+
assert_equal OpenSSL::PKey::RSA, key.class
|
18
|
+
|
19
|
+
key = C.read_private_key @@dsa_key_path
|
20
|
+
assert_equal File.read(@@dsa_key_path), key.to_pem
|
21
|
+
assert_equal OpenSSL::PKey::DSA, key.class
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_read_known_hosts
|
25
|
+
keys = C.read_authorized_keys @@known_hosts_path
|
26
|
+
verify_authorized_keys keys
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_read_authorized_keys
|
30
|
+
keys = C.read_authorized_keys @@authorized_keys_path
|
31
|
+
verify_authorized_keys keys
|
32
|
+
end
|
33
|
+
|
34
|
+
def verify_authorized_keys(keys)
|
35
|
+
assert_equal 4, keys.length
|
36
|
+
assert_equal [OpenSSL::PKey::RSA] * 3 + [OpenSSL::PKey::DSA],
|
37
|
+
keys.map { |k| k.class }
|
38
|
+
assert_equal C.read_private_key(@@rsa_key_path).public_key.to_pem,
|
39
|
+
keys[1].to_pem
|
40
|
+
assert_equal C.read_private_key(@@dsa_key_path).public_key.to_pem,
|
41
|
+
keys[3].to_pem
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_key_fingerprint
|
45
|
+
keys = C.read_authorized_keys @@known_hosts_path
|
46
|
+
|
47
|
+
assert_equal 4, keys.map { |k| C.key_fingerprint k }.uniq.length
|
48
|
+
keys.each { |k| assert_equal C.key_fingerprint(k), C.key_fingerprint(k) }
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_load_public_keys
|
52
|
+
keyset = C.load_public_keys @@known_hosts_path
|
53
|
+
rsa_key = C.read_private_key @@rsa_key_path
|
54
|
+
dsa_key = C.read_private_key @@dsa_key_path
|
55
|
+
|
56
|
+
assert_equal rsa_key.public_key.to_pem,
|
57
|
+
keyset[C.key_fingerprint(rsa_key)].to_pem
|
58
|
+
assert_equal dsa_key.public_key.to_pem,
|
59
|
+
keyset[C.key_fingerprint(dsa_key)].to_pem
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_key_encryption
|
63
|
+
test_data = 'qwertyuiopasdfghjklzxcvbnm' * 2
|
64
|
+
rsa_key = C.read_private_key @@rsa_key_path
|
65
|
+
dsa_key = C.read_private_key @@rsa_key_path
|
66
|
+
|
67
|
+
[rsa_key, dsa_key].each do |key|
|
68
|
+
encrypted_data = C.encrypt_with_key key.public_key, test_data
|
69
|
+
decrypted_data = C.decrypt_with_key key, encrypted_data
|
70
|
+
|
71
|
+
assert_equal test_data, decrypted_data
|
72
|
+
0.upto(test_data.length - 4) do |i|
|
73
|
+
assert !encrypted_data.index(test_data[i, 4]),
|
74
|
+
'Encryption did not wipe the original pattern'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_encryption_depends_on_key
|
80
|
+
num_keys = 16
|
81
|
+
test_data = 'qwertyuiopasdfghjklzxcvbnm' * 2
|
82
|
+
keys = (0...num_keys).map { OpenSSL::PKey::RSA.generate 1024, 35 }
|
83
|
+
assert_equal num_keys, keys.map { |k| C.encrypt_with_key k, test_data }.
|
84
|
+
uniq.length
|
85
|
+
end
|
86
|
+
|
87
|
+
def test_hasher_random_keys_are_random
|
88
|
+
num_keys = 1024
|
89
|
+
assert_equal num_keys, (0...num_keys).map { C::Hasher.random_key }.
|
90
|
+
uniq.length
|
91
|
+
end
|
92
|
+
|
93
|
+
def test_hasher_random_keys_yield_random_results
|
94
|
+
num_keys = 1024
|
95
|
+
test_data = 'qwertyuiopasdfghjklzxcvbnm' * 2
|
96
|
+
assert_equal num_keys, (0...num_keys).map { C::Hasher.new.hash test_data }.
|
97
|
+
uniq.length
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_hasher_hashes_are_finite
|
101
|
+
num_blocks = 64
|
102
|
+
block = 'qwertyuiopasdfghjklzxcvbnm'
|
103
|
+
|
104
|
+
hasher = C::Hasher.new
|
105
|
+
hash_length = hasher.hash('').length
|
106
|
+
1.upto(num_blocks) { |n| assert_equal hash_length, hasher.hash(block * n).
|
107
|
+
length }
|
108
|
+
end
|
109
|
+
|
110
|
+
def test_hasher_has_state
|
111
|
+
num_blocks = 1024
|
112
|
+
block = 'qwertyuiopasdfghjklzxcvbnm'
|
113
|
+
hasher = C::Hasher.new
|
114
|
+
|
115
|
+
assert_equal num_blocks, (0...num_blocks).map { hasher.hash block}.uniq.
|
116
|
+
length
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_hasher_is_reproducible
|
120
|
+
hasher = C::Hasher.new
|
121
|
+
hasher2 = C::Hasher.new hasher.key
|
122
|
+
|
123
|
+
num_blocks = 128
|
124
|
+
block = 'qwertyuiopasdfghjklzxcvbnm'
|
125
|
+
1.upto(num_blocks) { assert_equal hasher.hash(block), hasher2.hash(block) }
|
126
|
+
end
|
127
|
+
end
|