costan-rtunnel 0.4.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.
- 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
|