expect-behaviors 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.ruby-version +1 -0
- data/.travis.yml +20 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +99 -0
- data/LICENSE +22 -0
- data/README.md +16 -0
- data/Rakefile +17 -0
- data/VERSION +1 -0
- data/examples/example_ssh_localhost.rb +36 -0
- data/expect-behaviors.gemspec +95 -0
- data/lib/expect/behavior.rb +134 -0
- data/lib/expect/match.rb +39 -0
- data/lib/expect/ssh.rb +161 -0
- data/lib/expect/timeout_error.rb +4 -0
- data/tasks/doc.rake +10 -0
- data/tasks/jeweler.rake +14 -0
- data/tasks/test.rake +32 -0
- data/test/acceptance/sshd/erb/ssh_config.erb +8 -0
- data/test/acceptance/sshd/erb/sshd_config.erb +65 -0
- data/test/acceptance/test_expect_behaviors_slow_tests.rb +109 -0
- data/test/acceptance/test_expect_ssh.rb +97 -0
- data/test/class_including_expect_behavior.rb +33 -0
- data/test/helper.rb +22 -0
- data/test/sshd.rb +139 -0
- data/test/unit/expect/test_expect_behaviors.rb +178 -0
- data/test/unit/expect/test_expect_match.rb +64 -0
- data/test/unit/expect/test_includer_class.rb +34 -0
- metadata +250 -0
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'sshd'
|
3
|
+
|
4
|
+
require 'expect/ssh'
|
5
|
+
|
6
|
+
class TestExpectSSH < Test::Unit::TestCase
|
7
|
+
|
8
|
+
def self.startup
|
9
|
+
@@hostname = '127.0.0.1'
|
10
|
+
@@username = ENV['USER']
|
11
|
+
@@sshd = SSHD.new
|
12
|
+
@@sshd.start
|
13
|
+
@@port = @@sshd.port
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
def self.shutdown
|
18
|
+
@@sshd.teardown
|
19
|
+
end
|
20
|
+
|
21
|
+
context :init_and_start do
|
22
|
+
|
23
|
+
setup do
|
24
|
+
@ssh = Expect::SSH.new(@@hostname, @@username, port: @@port, key_file: @@sshd.client_key_path, ignore_known_hosts: true)
|
25
|
+
end
|
26
|
+
|
27
|
+
should "return an options hash when calling #options" do
|
28
|
+
expected = {
|
29
|
+
:auth_methods => ["none", "publickey", "password"],
|
30
|
+
:keys => [@@sshd.client_key_path],
|
31
|
+
:logger => nil,
|
32
|
+
:port => @@port,
|
33
|
+
:user_known_hosts_file => "/dev/null"
|
34
|
+
}
|
35
|
+
result = @ssh.send(:options)
|
36
|
+
result[:logger] = nil
|
37
|
+
assert_equal(expected, result)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context :startup do
|
42
|
+
|
43
|
+
setup do
|
44
|
+
@ssh = Expect::SSH.new(@@hostname, @@username, port: @@port, key_file: @@sshd.client_key_path, ignore_known_hosts: true)
|
45
|
+
end
|
46
|
+
|
47
|
+
should "be able to authenticate" do
|
48
|
+
@ssh.start
|
49
|
+
@ssh.stop
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
context :expect do
|
55
|
+
|
56
|
+
setup do
|
57
|
+
@ssh = Expect::SSH.new(@@hostname, @@username, port: @@port, key_file: @@sshd.client_key_path, ignore_known_hosts: true)
|
58
|
+
@ssh.start
|
59
|
+
end
|
60
|
+
|
61
|
+
teardown do
|
62
|
+
@ssh.stop
|
63
|
+
end
|
64
|
+
|
65
|
+
should "be able to send a few commands" do
|
66
|
+
result = @ssh.expect do
|
67
|
+
when_matching(/.*\$.*/) { @exp_match }
|
68
|
+
end
|
69
|
+
assert_match(/Last login:/, result.to_s)
|
70
|
+
@ssh.send_data("cat /etc/resolv.conf")
|
71
|
+
result = @ssh.expect do
|
72
|
+
when_matching(/.*nameserver.*/) { @exp_match }
|
73
|
+
end
|
74
|
+
assert_match(/nameserver [\d\.]+.*/, result.exact_match_string)
|
75
|
+
@ssh.send_data("date")
|
76
|
+
result = @ssh.expect do
|
77
|
+
when_matching(/.*20.*/) { @exp_match }
|
78
|
+
end
|
79
|
+
assert_match(/20/, result.exact_match_string)
|
80
|
+
end
|
81
|
+
|
82
|
+
should "timeout as expected" do
|
83
|
+
result = @ssh.expect do
|
84
|
+
when_matching(/.*\$.*/) { @exp_match }
|
85
|
+
end
|
86
|
+
assert_match(/Last login:/, result.to_s)
|
87
|
+
@ssh.send_data("sleep 2")
|
88
|
+
result = @ssh.expect do
|
89
|
+
when_matching(/.*\$.*/) { @exp_match }
|
90
|
+
when_timeout(1) { "timeout" }
|
91
|
+
end
|
92
|
+
assert_match(/timeout/, result)
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'expect/behavior'
|
2
|
+
|
3
|
+
class ClassIncludingExpectBehavior
|
4
|
+
include Expect::Behavior
|
5
|
+
# :Required methods to be created by the class mixing Expect::Behaviors :
|
6
|
+
# #exp_process - should do one iteration of handle input and append buffer
|
7
|
+
# #exp_buffer - provide the current buffer contents and empty it
|
8
|
+
|
9
|
+
attr_accessor :exp_buffer_values, :wait_sec
|
10
|
+
|
11
|
+
def initialize(wait: nil, values: [])
|
12
|
+
@exp_buffer = ''
|
13
|
+
@exp_buffer_values = values
|
14
|
+
@wait_sec = wait
|
15
|
+
end
|
16
|
+
|
17
|
+
##
|
18
|
+
# exp_buffer - provide the current buffer contents and empty it
|
19
|
+
def exp_buffer
|
20
|
+
result = @exp_buffer
|
21
|
+
@exp_buffer = ''
|
22
|
+
result
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# exp_process - should do one iteration of handle input and append buffer
|
27
|
+
def exp_process
|
28
|
+
sleep(@wait_sec.to_f)
|
29
|
+
# handle input
|
30
|
+
@exp_buffer << @exp_buffer_values.shift.to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require "codeclimate-test-reporter"
|
2
|
+
CodeClimate::TestReporter.start
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'bundler'
|
6
|
+
gem 'mocha'
|
7
|
+
require 'mocha/test_unit'
|
8
|
+
begin
|
9
|
+
Bundler.setup(:default, :development)
|
10
|
+
rescue Bundler::BundlerError => e
|
11
|
+
$stderr.puts e.message
|
12
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
13
|
+
exit e.status_code
|
14
|
+
end
|
15
|
+
require 'test/unit'
|
16
|
+
require 'shoulda'
|
17
|
+
|
18
|
+
# add lib folder from parent
|
19
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
20
|
+
# ...and the test folder
|
21
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
22
|
+
require 'expect/behavior'
|
data/test/sshd.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
|
6
|
+
##
|
7
|
+
# A Helper to start a second instance of SSHD on an unprivilege port which allows for a custom client key
|
8
|
+
# All keys for this server are created just in time.
|
9
|
+
class SSHD
|
10
|
+
|
11
|
+
TEST_ROOT = File.dirname(__FILE__)
|
12
|
+
SSHD_CFG_ROOT = File.join(TEST_ROOT, 'acceptance', 'sshd')
|
13
|
+
SSHD_TMP_ROOT = File.join(TEST_ROOT, 'tmp', 'sshd')
|
14
|
+
PIDFILE_PATH = File.join(SSHD_TMP_ROOT, 'sshd.pid')
|
15
|
+
ERB_ROOT = File.join(SSHD_CFG_ROOT, 'erb')
|
16
|
+
SSHD_CONFIG_ERB_PATH = File.join(ERB_ROOT, 'sshd_config.erb')
|
17
|
+
SSH_CONFIG_ERB_PATH = File.join(ERB_ROOT, 'ssh_config.erb')
|
18
|
+
SSHD_CONFIG_PATH = File.join(SSHD_TMP_ROOT, 'sshd_config')
|
19
|
+
SSH_CONFIG_PATH = File.join(SSHD_TMP_ROOT, 'ssh_config')
|
20
|
+
SSHD_LOG_PATH = File.join(SSHD_TMP_ROOT, 'sshd.log')
|
21
|
+
|
22
|
+
KEY_ROOT = SSHD_TMP_ROOT
|
23
|
+
SSHD_CLIENT_KEY_PATH = File.join(KEY_ROOT, 'id_rsa')
|
24
|
+
SSHD_CLIENT_PUBKEY_PATH = File.join(KEY_ROOT, 'id_rsa.pub')
|
25
|
+
SSHD_RSA_HOST_KEY_PATH = File.join(KEY_ROOT, 'ssh_host_key_rsa')
|
26
|
+
SSHD_DSA_HOST_KEY_PATH = File.join(KEY_ROOT, 'ssh_host_key_dsa')
|
27
|
+
SSHD_AUTHORIZED_KEYS_PATH = SSHD_CLIENT_PUBKEY_PATH
|
28
|
+
|
29
|
+
attr_reader :port
|
30
|
+
|
31
|
+
def initialize(address = '127.0.0.1')
|
32
|
+
@tcpserver = nil
|
33
|
+
@address = address
|
34
|
+
@port = reserve_unprivileged_tcp_port
|
35
|
+
@sshd_filepath = %x(which sshd).chomp
|
36
|
+
@ssh_keygen_filepath = %x(which ssh-keygen).chomp
|
37
|
+
teardown
|
38
|
+
end
|
39
|
+
|
40
|
+
def client_key_path
|
41
|
+
SSHD_CLIENT_KEY_PATH
|
42
|
+
end
|
43
|
+
|
44
|
+
def start
|
45
|
+
unless openssh_files_found?
|
46
|
+
raise(RuntimeError, "[SSHD] Error: Unable to locate sshd or ssh-keygen.")
|
47
|
+
end
|
48
|
+
create_sshd_config
|
49
|
+
create_ssh_config
|
50
|
+
generate_keys
|
51
|
+
release_port
|
52
|
+
start_ssh_server
|
53
|
+
end
|
54
|
+
|
55
|
+
def stop
|
56
|
+
this_pid = pid
|
57
|
+
unless this_pid.nil?
|
58
|
+
$stdout.puts("[SSHD] [#{__method__}]: Killing SSHD, [pid=#{this_pid}]")
|
59
|
+
begin
|
60
|
+
Process.kill(0, this_pid)
|
61
|
+
rescue Errno::ESRCH
|
62
|
+
$stderr.puts("[SSHD] [#{__method__}]: No Action: Process not found, [pid=#{this_pid}]")
|
63
|
+
ensure
|
64
|
+
File.delete(PIDFILE_PATH) if File.exists?(PIDFILE_PATH)
|
65
|
+
end
|
66
|
+
else
|
67
|
+
$stderr.puts("[SSHD] [#{__method__}]: No Action: PIDFILE Doesnt exist: #{PIDFILE_PATH}")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def teardown
|
72
|
+
stop
|
73
|
+
clean_tmp_root
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
#####################
|
78
|
+
private
|
79
|
+
#####################
|
80
|
+
|
81
|
+
def clean_tmp_root
|
82
|
+
FileUtils.rmtree(SSHD_TMP_ROOT)
|
83
|
+
FileUtils.mkpath(SSHD_TMP_ROOT)
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
|
87
|
+
def create_ssh_config
|
88
|
+
erb = ERB.new(IO.read(SSH_CONFIG_ERB_PATH))
|
89
|
+
result = erb.result(binding)
|
90
|
+
File.open(SSH_CONFIG_PATH, 'w') {|f| f.write(result)}
|
91
|
+
result
|
92
|
+
end
|
93
|
+
|
94
|
+
def create_sshd_config
|
95
|
+
erb = ERB.new(IO.read(SSHD_CONFIG_ERB_PATH))
|
96
|
+
result = erb.result(binding)
|
97
|
+
File.open(SSHD_CONFIG_PATH, 'w') {|f| f.write(result)}
|
98
|
+
result
|
99
|
+
end
|
100
|
+
|
101
|
+
def generate_keys
|
102
|
+
[SSHD_CLIENT_KEY_PATH, SSHD_RSA_HOST_KEY_PATH, SSHD_DSA_HOST_KEY_PATH].each do |key_path|
|
103
|
+
%x(#{@ssh_keygen_filepath} -t rsa -b 4096 -C user@localhost -f #{key_path} -N '')
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def openssh_files_found?
|
108
|
+
not @ssh_keygen_filepath.empty? and not @sshd_filepath.empty?
|
109
|
+
end
|
110
|
+
|
111
|
+
def pid
|
112
|
+
pid = nil
|
113
|
+
if File.exists?(PIDFILE_PATH)
|
114
|
+
pid = IO.read(PIDFILE_PATH).chomp.to_i
|
115
|
+
end
|
116
|
+
pid
|
117
|
+
end
|
118
|
+
|
119
|
+
def reserve_unprivileged_tcp_port
|
120
|
+
@tcpserver ||= TCPServer.new(@host, 0)
|
121
|
+
@port = @tcpserver.addr[1]
|
122
|
+
end
|
123
|
+
|
124
|
+
def release_port
|
125
|
+
@tcpserver.close
|
126
|
+
@tcpserver = nil
|
127
|
+
@port
|
128
|
+
end
|
129
|
+
|
130
|
+
def start_ssh_server
|
131
|
+
%x(#{@sshd_filepath} -4 -f #{SSHD_CONFIG_PATH})
|
132
|
+
# -E #{SSHD_LOG_PATH}
|
133
|
+
$stdout.puts("[SSHD] [#{__method__}]: Starting on [port=#{@port}]")
|
134
|
+
sleep(0.5)
|
135
|
+
$stdout.puts("[SSHD] [#{__method__}]: Started on [port=#{@port}] [pid=#{pid}]")
|
136
|
+
pid
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
@@ -0,0 +1,178 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'expect/behavior'
|
3
|
+
require 'class_including_expect_behavior'
|
4
|
+
gem 'mocha'
|
5
|
+
require 'mocha/test_unit'
|
6
|
+
|
7
|
+
class TestExpectBehaviors < Test::Unit::TestCase
|
8
|
+
|
9
|
+
####################################
|
10
|
+
context "expect" do
|
11
|
+
setup do
|
12
|
+
@values = []
|
13
|
+
@values << "the sun is a mass"
|
14
|
+
@values << "\nof incandescent gas"
|
15
|
+
@values << "\nswitch-prompt#"
|
16
|
+
@values << "\nblip blip"
|
17
|
+
@values << "\nblah blah"
|
18
|
+
@values << "\nswitch-prompt2#"
|
19
|
+
@includer = ClassIncludingExpectBehavior.new(values: @values)
|
20
|
+
@wait_sec = 0.1
|
21
|
+
end
|
22
|
+
|
23
|
+
should "match up to first switch-prompt" do
|
24
|
+
result = @includer.expect do
|
25
|
+
when_matching(/switch-prompt#/) do
|
26
|
+
@exp_match
|
27
|
+
end
|
28
|
+
end
|
29
|
+
expected = "the sun is a mass\nof incandescent gas\nswitch-prompt#"
|
30
|
+
assert_equal(expected, result.to_s)
|
31
|
+
end
|
32
|
+
|
33
|
+
should "timeout for switch-prompt2#" do
|
34
|
+
@includer.wait_sec = @wait_sec
|
35
|
+
@includer.exp_timeout_sec = @wait_sec * 4
|
36
|
+
result = @includer.expect do
|
37
|
+
when_matching(/switch-prompt2#/) do
|
38
|
+
@exp_match
|
39
|
+
end
|
40
|
+
when_timeout(@wait_sec * 3) do
|
41
|
+
"timed out"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
assert_equal('timed out', result.to_s)
|
45
|
+
end
|
46
|
+
|
47
|
+
should "not timeout for switch-prompt2# if expect_continue for blip" do
|
48
|
+
omit("See acceptance version of this test... doesn't work for such a short interval: (#{@wait_sec}")
|
49
|
+
end
|
50
|
+
|
51
|
+
should "timeout for switch-prompt2# if expect_continue for incandescent" do
|
52
|
+
omit("See acceptance version of this test... doesn't work for such a short interval: (#{@wait_sec}")
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
should "handle subsequent expect statements" do
|
57
|
+
@includer.wait_sec = @wait_sec
|
58
|
+
@includer.expect do
|
59
|
+
when_matching(/incandescent/) do
|
60
|
+
@exp_match
|
61
|
+
end
|
62
|
+
when_timeout(@wait_sec * 3) do
|
63
|
+
"timed out"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
result = @includer.expect do
|
67
|
+
when_matching(/switch-prompt#/) do
|
68
|
+
@exp_match
|
69
|
+
end
|
70
|
+
when_timeout(@wait_sec * 3) do
|
71
|
+
"timed out"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
expected = "\nswitch-prompt#"
|
75
|
+
assert_equal(expected, result.to_s)
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
####################################
|
82
|
+
context "initialize_expect" do
|
83
|
+
setup do
|
84
|
+
@includer = ClassIncludingExpectBehavior.new(values: ["bobert"])
|
85
|
+
end
|
86
|
+
|
87
|
+
should "be run when calling expect" do
|
88
|
+
@includer.expects(:initialize_expect)
|
89
|
+
@includer.expects(:execute_expect_loop)
|
90
|
+
@includer.expect do
|
91
|
+
when_matching(/bob/) do
|
92
|
+
return_value
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
should "key instance variables undefined before first expect" do
|
98
|
+
# undefined/nil before first expect
|
99
|
+
assert_equal(nil, @includer.instance_variable_get(:@exp_match_registry))
|
100
|
+
assert_equal(nil, @includer.instance_variable_get(:@exp_timeout_sec))
|
101
|
+
assert_equal(nil, @includer.instance_variable_get(:@exp_match))
|
102
|
+
assert_equal(nil, @includer.instance_variable_get(:@exp_timeout_block))
|
103
|
+
assert_equal(nil, @includer.instance_variable_get(:@__exp_buffer))
|
104
|
+
end
|
105
|
+
|
106
|
+
should "setup instance variables prior to expect" do
|
107
|
+
@includer.expects(:execute_expect_loop) #stub out expect loop
|
108
|
+
@includer.expect do
|
109
|
+
"bob"
|
110
|
+
end
|
111
|
+
assert_equal({}, @includer.instance_variable_get(:@exp_match_registry))
|
112
|
+
assert_equal(10, @includer.instance_variable_get(:@exp_timeout_sec))
|
113
|
+
assert_equal(nil, @includer.instance_variable_get(:@exp_match))
|
114
|
+
assert_equal(nil, @includer.instance_variable_get(:@exp_timeout_block))
|
115
|
+
assert_equal('', @includer.instance_variable_get(:@__exp_buffer))
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
####################################
|
121
|
+
context "match registry" do
|
122
|
+
setup do
|
123
|
+
@includer = ClassIncludingExpectBehavior.new
|
124
|
+
end
|
125
|
+
|
126
|
+
should "be populated by when_matching statements" do
|
127
|
+
@includer.stubs(:execute_expect_loop)
|
128
|
+
return_value = "Matched BOB"
|
129
|
+
@includer.expect do
|
130
|
+
when_matching(/bob/) do
|
131
|
+
return_value
|
132
|
+
end
|
133
|
+
when_matching(/joe/) do
|
134
|
+
"Matched JOE"
|
135
|
+
end
|
136
|
+
when_timeout do
|
137
|
+
"TIMEOUT"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
assert_equal(/bob/, @includer.instance_variable_get(:@exp_match_registry).keys.first)
|
141
|
+
assert_equal(2, @includer.instance_variable_get(:@exp_match_registry).length)
|
142
|
+
assert_equal(return_value, @includer.instance_variable_get(:@exp_match_registry).values.first.call)
|
143
|
+
assert_equal("TIMEOUT", @includer.instance_variable_get(:@exp_timeout_block).call)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
####################################
|
149
|
+
context "timeout" do
|
150
|
+
setup do
|
151
|
+
@includer = ClassIncludingExpectBehavior.new(wait: 0.2)
|
152
|
+
end
|
153
|
+
|
154
|
+
should "raise TimeoutError when timeout is reached before match is found" do
|
155
|
+
@includer.exp_timeout_sec = 0.1
|
156
|
+
assert_raises(Expect::TimeoutError) do
|
157
|
+
@includer.expect do
|
158
|
+
when_matching(/bob/) do
|
159
|
+
return_value
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
should "execute arbitrary block on timeout with override" do
|
166
|
+
result = @includer.expect do
|
167
|
+
when_matching(/bob/) do
|
168
|
+
return_value
|
169
|
+
end
|
170
|
+
when_timeout(0.1) do
|
171
|
+
"timeout"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
assert_equal('timeout', result)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|