hussh 0.1.0 → 0.2.1
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.rdoc +1 -0
- data/lib/hussh/channel.rb +66 -23
- data/lib/hussh/configuration.rb +12 -3
- data/lib/hussh/session.rb +21 -43
- data/lib/hussh/version.rb +1 -1
- data/spec/hussh_channel_spec.rb +119 -105
- data/spec/hussh_configuration_spec.rb +74 -1
- data/spec/hussh_functional_spec.rb +377 -0
- data/spec/hussh_session_spec.rb +22 -59
- data/spec/spec_helper.rb +4 -1
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a257554c5698eec071106e3d07d3ba27ac8e4d68
|
4
|
+
data.tar.gz: 9898be5599601f68c93b161e37ca5569752b9e69
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 500700b7c98b007ccfabecc971b2939a829621e616dd84fe8248c18b5f89ecfc2f2d8d3ff291458f4ef7453c5f2834a0a8c85fb445dab6c95faecc34ffa966bc
|
7
|
+
data.tar.gz: df7f6353a10afea92a5d643cf6403e648f9a94d91d3824c85a942bf4a3cbf9a6963861f70055b0aca1ef3e690fb481c0fc6faac8c75ed8cadfb2327b7c3784e1
|
data/README.rdoc
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
A mocking library for <tt>Net::SSH</tt> which allows testers to specify
|
4
4
|
responses and record real-life responses for later use.
|
5
5
|
|
6
|
+
{<img src="https://badge.fury.io/rb/hussh.svg" alt="Gem Version" />}[http://badge.fury.io/rb/hussh]
|
6
7
|
{<img src="https://travis-ci.org/moneyadviceservice/hussh.svg?branch=master" alt="Build Status" />}[https://travis-ci.org/moneyadviceservice/hussh]
|
7
8
|
|
8
9
|
== Installation
|
data/lib/hussh/channel.rb
CHANGED
@@ -5,47 +5,90 @@ module Hussh
|
|
5
5
|
@request_pty = false
|
6
6
|
end
|
7
7
|
|
8
|
-
def
|
9
|
-
|
8
|
+
def have_real_channel?
|
9
|
+
!!@real_channel
|
10
10
|
end
|
11
11
|
|
12
|
-
def
|
13
|
-
@
|
12
|
+
def open_real_channel
|
13
|
+
@real_channel ||= @session.real_session.open_channel
|
14
14
|
end
|
15
15
|
|
16
16
|
attr :command
|
17
17
|
attr :exec_block
|
18
18
|
def exec(command, &block)
|
19
19
|
@command = command
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
20
|
+
Hussh.commands_run << @command
|
21
|
+
if !@session.has_response?(@command)
|
22
|
+
open_real_channel
|
23
|
+
request_pty(&@request_pty_callback) if @request_pty
|
24
|
+
on_data(&@on_data_callback) if @on_data_callback
|
25
|
+
if @on_extended_data_callback
|
26
|
+
on_extended_data(&@on_extended_data_callback)
|
27
|
+
end
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
@real_channel.exec(command) do |ch, success|
|
30
|
+
@exec_result = success
|
31
|
+
block.call(self, success) if block
|
32
|
+
end
|
33
|
+
elsif block_given?
|
34
|
+
yield(self, true)
|
35
|
+
end
|
31
36
|
end
|
32
37
|
|
33
|
-
attr :request_pty_block
|
34
38
|
def request_pty(&block)
|
35
|
-
|
36
|
-
|
39
|
+
if have_real_channel?
|
40
|
+
@real_channel.request_pty do |ch, success|
|
41
|
+
block.call(ch, success) if block
|
42
|
+
end
|
43
|
+
else
|
44
|
+
@request_pty = true
|
45
|
+
@request_pty_callback = block
|
46
|
+
end
|
37
47
|
end
|
38
48
|
|
39
49
|
def requested_pty?
|
40
50
|
@request_pty
|
41
51
|
end
|
42
52
|
|
43
|
-
|
44
|
-
|
45
|
-
|
53
|
+
def on_data(&block)
|
54
|
+
if have_real_channel?
|
55
|
+
@real_channel.on_data do |ch, output|
|
56
|
+
@stdout ||= ''
|
57
|
+
@stdout << output
|
58
|
+
block.call(ch, output) if block
|
59
|
+
end
|
60
|
+
else
|
61
|
+
@on_data_callback = block
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def on_extended_data(&block)
|
66
|
+
if have_real_channel?
|
67
|
+
@real_channel.on_extended_data do |ch, output|
|
68
|
+
@stderr ||= ''
|
69
|
+
@stderr << output
|
70
|
+
block.call(ch, output) if block
|
71
|
+
end
|
72
|
+
else
|
73
|
+
@on_extended_data_callback = block
|
74
|
+
end
|
75
|
+
end
|
46
76
|
|
47
|
-
|
48
|
-
|
49
|
-
|
77
|
+
def wait
|
78
|
+
if @real_channel
|
79
|
+
@real_channel.wait
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def close
|
84
|
+
if have_real_channel?
|
85
|
+
@real_channel.close
|
86
|
+
@session.update_recording(@command, @stdout) if @stdout
|
87
|
+
else
|
88
|
+
stdout = @session.response_for(@command)
|
89
|
+
@on_data_callback.call(self, stdout) if stdout && @on_data_callback
|
90
|
+
@on_extended_data_callback.call(self, @stderr) if @stderr && @on_extended_data_callback
|
91
|
+
end
|
92
|
+
end
|
50
93
|
end
|
51
94
|
end
|
data/lib/hussh/configuration.rb
CHANGED
@@ -20,9 +20,18 @@ module Hussh
|
|
20
20
|
|
21
21
|
config.before(:each, hussh: lambda { |v| !!v }) do |example|
|
22
22
|
options = example.metadata[:hussh]
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
if options.is_a?(Hash)
|
24
|
+
options = options.dup
|
25
|
+
recording_name = options.delete(:recording_name) ||
|
26
|
+
recording_name_for[example.metadata]
|
27
|
+
elsif options.is_a?(String)
|
28
|
+
recording_name = options
|
29
|
+
options = {}
|
30
|
+
else
|
31
|
+
recording_name = recording_name_for[example.metadata]
|
32
|
+
options = {}
|
33
|
+
end
|
34
|
+
|
26
35
|
Hussh.load_recording(recording_name)
|
27
36
|
Hussh.clear_stubbed_responses
|
28
37
|
end
|
data/lib/hussh/session.rb
CHANGED
@@ -5,12 +5,17 @@ module Hussh
|
|
5
5
|
def initialize(host, user)
|
6
6
|
@host = host
|
7
7
|
@user = user
|
8
|
+
@channel_id_counter = 0
|
8
9
|
end
|
9
10
|
|
10
11
|
def real_session
|
11
12
|
@real_session ||= Net::SSH.start_without_hussh(@host, @user)
|
12
13
|
end
|
13
14
|
|
15
|
+
def have_real_session?
|
16
|
+
!!@real_session
|
17
|
+
end
|
18
|
+
|
14
19
|
def has_response?(command)
|
15
20
|
Hussh.stubbed_responses.fetch(@host, {}).fetch(@user, {})
|
16
21
|
.has_key?(command) ||
|
@@ -43,54 +48,27 @@ module Hussh
|
|
43
48
|
end
|
44
49
|
end
|
45
50
|
|
46
|
-
def
|
47
|
-
@
|
48
|
-
|
49
|
-
Hussh.commands_run << @channel.command
|
50
|
-
if self.has_response?(@channel.command)
|
51
|
-
if @channel.exec_block.respond_to?(:call)
|
52
|
-
@channel.exec_block.call(@channel, true)
|
53
|
-
end
|
54
|
-
|
55
|
-
if @channel.on_data_block.respond_to?(:call)
|
56
|
-
@channel.on_data_block.call(@channel, self.response_for(@channel.command))
|
57
|
-
end
|
58
|
-
else
|
59
|
-
self.real_session.open_channel do |ch|
|
60
|
-
|
61
|
-
if @channel.requested_pty?
|
62
|
-
ch.request_pty do |ch, success|
|
63
|
-
if @channel.request_pty_block.respond_to?(:call)
|
64
|
-
@channel.request_pty_block.call(ch, success)
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
ch.exec(@channel.command) do |ch, success|
|
70
|
-
@channel.exec_block.call(@channel, success) if @channel.exec_block
|
71
|
-
|
72
|
-
ch.on_data do |ch, output|
|
73
|
-
if @channel.on_data_block.respond_to?(:call)
|
74
|
-
@channel.on_data_block.call(@channel, output)
|
75
|
-
@on_data = output
|
76
|
-
self.update_recording(@channel.command, @on_data)
|
77
|
-
end
|
78
|
-
end
|
51
|
+
def channels
|
52
|
+
@channels ||= {}
|
53
|
+
end
|
79
54
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
end
|
84
|
-
end
|
55
|
+
def get_next_channel_id
|
56
|
+
@channel_id_counter += 1
|
57
|
+
end
|
85
58
|
|
86
|
-
|
87
|
-
|
88
|
-
|
59
|
+
def open_channel(&block)
|
60
|
+
channel = Channel.new(self)
|
61
|
+
yield(channel) if block_given?
|
62
|
+
channels[get_next_channel_id] = channel
|
89
63
|
end
|
90
64
|
|
91
65
|
def close
|
92
|
-
|
93
|
-
|
66
|
+
channels.each do |id, channel|
|
67
|
+
channel.close
|
68
|
+
end
|
69
|
+
if have_real_session?
|
70
|
+
real_session.close
|
71
|
+
end
|
94
72
|
end
|
95
73
|
end
|
96
74
|
end
|
data/lib/hussh/version.rb
CHANGED
data/spec/hussh_channel_spec.rb
CHANGED
@@ -7,142 +7,156 @@ require 'hussh'
|
|
7
7
|
RSpec.describe Hussh do
|
8
8
|
include FakeFS::SpecHelpers
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
end
|
10
|
+
let(:session) { spy(Hussh::Session) }
|
11
|
+
let(:channel) { Hussh::Channel.new(session) }
|
12
|
+
let(:real_channel) do
|
13
|
+
channel.instance_eval { @real_channel }
|
14
|
+
end
|
16
15
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
allow(channel).to receive(:exec) do |cmd, &blk|
|
23
|
-
@command = cmd
|
24
|
-
@exec = blk
|
25
|
-
end
|
26
|
-
allow(channel).to receive(:request_pty) { |&block| @request_pty = block }
|
27
|
-
allow(channel).to receive(:on_data) { |&block| @on_data = block }
|
28
|
-
allow(channel).to receive(:on_extended_data) do |&block|
|
29
|
-
@on_extended_data = block
|
30
|
-
end
|
31
|
-
# TODO: loop and wait should also end up calling the callbacks
|
32
|
-
allow(channel).to receive(:close) do
|
33
|
-
# Allow the test to specify a command that fails.
|
34
|
-
@exec.call(channel, !@command.match(/fail/)) if @exec
|
35
|
-
# Allow the test to specify a failed pty request.
|
36
|
-
@request_pty.call(channel, !@command.match(/nopty/)) if @request_pty
|
37
|
-
@on_data.call(channel, "#{@command} output") if @on_data
|
38
|
-
if @on_extended_data
|
39
|
-
@on_extended_data.call(channel, "#{@command} stderr output")
|
40
|
-
end
|
41
|
-
end
|
42
|
-
allow(channel).to receive(:wait) do
|
43
|
-
channel.close
|
44
|
-
end
|
45
|
-
channel
|
46
|
-
end
|
16
|
+
let(:mock_block) do
|
17
|
+
block = Proc.new {}
|
18
|
+
allow(block).to receive(:call)
|
19
|
+
block
|
20
|
+
end
|
47
21
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
blk.call(channel)
|
53
|
-
channel.close
|
54
|
-
end
|
55
|
-
allow(Net::SSH).to receive(:start_without_hussh).and_return(session)
|
56
|
-
session
|
22
|
+
context 'real channel opened' do
|
23
|
+
before do
|
24
|
+
spy = instance_spy('Net::SSH::Connection::Channel')
|
25
|
+
channel.instance_eval { @real_channel = spy }
|
57
26
|
end
|
58
27
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
# Simulate how we would use Hussh, which sits between the
|
66
|
-
# application code (the code below) and the mocked-out Net::SSH
|
67
|
-
# code.
|
68
|
-
Net::SSH.start('host', 'user') do |session|
|
69
|
-
session.open_channel do |ch|
|
70
|
-
ch.request_pty
|
71
|
-
ch.exec command do |ch, success|
|
72
|
-
@exec_success = success
|
73
|
-
ch.on_data { |c, data| @data = data }
|
74
|
-
ch.on_extended_data { |c, data| @extended_data = data }
|
75
|
-
end
|
28
|
+
describe :exec do
|
29
|
+
context 'where there is no saved response' do
|
30
|
+
before do
|
31
|
+
allow(real_channel).to receive(:exec) do |command, &block|
|
32
|
+
@command = command
|
33
|
+
@hussh_block = block
|
76
34
|
end
|
77
35
|
end
|
78
|
-
end
|
79
36
|
|
80
|
-
|
81
|
-
|
37
|
+
before do
|
38
|
+
allow(session).to receive(:has_response?).and_return(false)
|
39
|
+
end
|
82
40
|
|
83
|
-
|
84
|
-
|
41
|
+
it 'records that the command was run' do
|
42
|
+
channel.exec('record-command')
|
43
|
+
expect(Hussh.commands_run.last).to eql('record-command')
|
44
|
+
end
|
85
45
|
|
86
|
-
|
87
|
-
|
88
|
-
end
|
46
|
+
context 'when a pty has been requested' do
|
47
|
+
before { channel.instance_eval { @request_pty = true } }
|
89
48
|
|
90
|
-
it '
|
91
|
-
|
49
|
+
it 'requests a pty' do
|
50
|
+
channel.exec('request-pty')
|
51
|
+
expect(real_channel).to have_received(:request_pty)
|
92
52
|
end
|
53
|
+
end
|
93
54
|
|
94
|
-
|
95
|
-
|
96
|
-
end
|
55
|
+
context 'when a pty has not been requested' do
|
56
|
+
before { channel.instance_eval { @request_pty = false } }
|
97
57
|
|
98
|
-
it '
|
99
|
-
|
58
|
+
it 'does not request a pty' do
|
59
|
+
channel.exec('no-request-pty')
|
60
|
+
expect(real_channel).to_not have_received(:request_pty)
|
100
61
|
end
|
62
|
+
end
|
101
63
|
|
102
|
-
|
103
|
-
|
104
|
-
end
|
64
|
+
context 'when an on_data block has been previously defined' do
|
65
|
+
before { channel.instance_eval { @on_data_callback = Proc.new {} } }
|
105
66
|
|
106
|
-
it '
|
107
|
-
|
67
|
+
it 'sets up an on_data callback' do
|
68
|
+
channel.exec('on-data')
|
69
|
+
expect(real_channel).to have_received(:on_data)
|
108
70
|
end
|
71
|
+
end
|
109
72
|
|
110
|
-
|
111
|
-
|
112
|
-
.to eq 'test output'
|
113
|
-
end
|
73
|
+
context 'when an on_data block has not been previously defined' do
|
74
|
+
before { channel.instance_eval { @on_data_callback = nil } }
|
114
75
|
|
115
|
-
it '
|
116
|
-
|
76
|
+
it 'does not setup an on_data callback' do
|
77
|
+
channel.exec('no-on-data')
|
78
|
+
expect(real_channel).to_not have_received(:on_data)
|
117
79
|
end
|
118
80
|
end
|
119
81
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
82
|
+
it 'calls our block' do
|
83
|
+
channel.exec('block-command', &mock_block)
|
84
|
+
@hussh_block.call(channel, 'test-for-success')
|
85
|
+
expect(mock_block).to have_received(:call)
|
86
|
+
.with(channel, 'test-for-success')
|
124
87
|
end
|
125
88
|
end
|
126
89
|
|
127
|
-
context '
|
128
|
-
|
129
|
-
|
130
|
-
'host' => { 'user' => { 'test' => 'recorded test output' } }
|
131
|
-
}.to_yaml
|
90
|
+
context 'where there is a saved response' do
|
91
|
+
before do
|
92
|
+
allow(session).to receive(:has_response?).and_return(true)
|
132
93
|
end
|
133
94
|
|
134
|
-
|
135
|
-
|
95
|
+
it 'calls our block' do
|
96
|
+
channel.exec('block-command') { @block_called = true }
|
97
|
+
expect(@block_called).to eql(true)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
136
101
|
|
137
|
-
|
138
|
-
|
139
|
-
|
102
|
+
describe :request_pty do
|
103
|
+
before do
|
104
|
+
allow(real_channel).to receive(:request_pty) { |&b| @hussh_block = b }
|
105
|
+
end
|
140
106
|
|
141
|
-
|
142
|
-
|
143
|
-
|
107
|
+
it 'calls request_pty on the real channel' do
|
108
|
+
channel.request_pty
|
109
|
+
expect(real_channel).to have_received(:request_pty)
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'calls our block' do
|
113
|
+
channel.request_pty(&mock_block)
|
114
|
+
@hussh_block.call(channel, :status)
|
115
|
+
expect(mock_block).to have_received(:call).with(channel, :status)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe :on_data do
|
120
|
+
before do
|
121
|
+
allow(real_channel).to receive(:on_data) { |&blk| @hussh_block = blk }
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'calls on_data on the real channel' do
|
125
|
+
channel.on_data {}
|
126
|
+
expect(real_channel).to have_received(:on_data)
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'calls our block' do
|
130
|
+
channel.on_data(&mock_block)
|
131
|
+
@hussh_block.call(channel, 'stdout')
|
132
|
+
expect(mock_block).to have_received(:call).with(channel, 'stdout')
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
describe :on_extended_data do
|
137
|
+
before do
|
138
|
+
allow(real_channel).to receive(:on_extended_data) do |&block|
|
139
|
+
@hussh_block = block
|
144
140
|
end
|
145
141
|
end
|
142
|
+
|
143
|
+
it 'calls on_extended_data on the real channel' do
|
144
|
+
channel.on_extended_data {}
|
145
|
+
expect(real_channel).to have_received(:on_extended_data)
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'calls our block' do
|
149
|
+
channel.on_extended_data(&mock_block)
|
150
|
+
@hussh_block.call(channel, 'stderr')
|
151
|
+
expect(mock_block).to have_received(:call).with(channel, 'stderr')
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
describe :wait do
|
156
|
+
it 'calls wait on the real channel' do
|
157
|
+
channel.wait
|
158
|
+
expect(real_channel).to have_received(:wait)
|
159
|
+
end
|
146
160
|
end
|
147
161
|
end
|
148
162
|
end
|
@@ -29,7 +29,80 @@ RSpec.describe Hussh do
|
|
29
29
|
})
|
30
30
|
end
|
31
31
|
|
32
|
-
|
32
|
+
describe 'before block' do
|
33
|
+
let(:recorded_responses) do
|
34
|
+
{ 'host' => { 'user' => { 'cmd' => 'output' } } }
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'with no params' do
|
38
|
+
before do
|
39
|
+
allow(@example).to receive(:metadata).and_return(
|
40
|
+
{
|
41
|
+
hussh: true,
|
42
|
+
description: 'some spec',
|
43
|
+
example_group: {
|
44
|
+
description: 'example group',
|
45
|
+
parent_example_group: {
|
46
|
+
description: 'parent group'
|
47
|
+
}
|
48
|
+
}
|
49
|
+
}
|
50
|
+
)
|
51
|
+
FileUtils.mkdir_p('fixtures/hussh/parent group/example group')
|
52
|
+
File.write(
|
53
|
+
'fixtures/hussh/parent group/example group/some spec.yaml',
|
54
|
+
recorded_responses.to_yaml
|
55
|
+
)
|
56
|
+
@before.call(@example)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'loads a recording with a generated name' do
|
60
|
+
expect(Hussh.recorded_responses).to eq(recorded_responses)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'with a string param' do
|
65
|
+
before do
|
66
|
+
allow(@example).to receive(:metadata).and_return(
|
67
|
+
{ hussh: 'group/spec' }
|
68
|
+
)
|
69
|
+
FileUtils.mkdir_p('fixtures/hussh/group')
|
70
|
+
File.write(
|
71
|
+
'fixtures/hussh/group/spec.yaml',
|
72
|
+
recorded_responses.to_yaml
|
73
|
+
)
|
74
|
+
@before.call(@example)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'uses the string as the recording name' do
|
78
|
+
expect(Hussh.recorded_responses).to eq(recorded_responses)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context 'with a hash param' do
|
83
|
+
before do
|
84
|
+
allow(@example).to receive(:metadata).and_return(
|
85
|
+
{
|
86
|
+
hussh: {
|
87
|
+
recording_name: 'parent/spec'
|
88
|
+
}
|
89
|
+
}
|
90
|
+
)
|
91
|
+
FileUtils.mkdir_p('fixtures/hussh/parent')
|
92
|
+
File.write(
|
93
|
+
'fixtures/hussh/parent/spec.yaml',
|
94
|
+
recorded_responses.to_yaml
|
95
|
+
)
|
96
|
+
@before.call(@example)
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'gets the recording_name from the hash' do
|
100
|
+
expect(Hussh.recorded_responses).to eq(recorded_responses)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe 'after block' do
|
33
106
|
before do
|
34
107
|
Hussh.recorded_responses = {
|
35
108
|
'host' => { 'user' => { 'cmd' => 'output' } }
|
@@ -0,0 +1,377 @@
|
|
1
|
+
require 'hussh'
|
2
|
+
require 'fakefs/spec_helpers'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
RSpec.describe Hussh do
|
6
|
+
include FakeFS::SpecHelpers
|
7
|
+
|
8
|
+
# Setup a fake Channel object which will hopefully behave like a Net::SSH
|
9
|
+
# channel, i.e. register the command and callbacks, and then call them all
|
10
|
+
# with the appropriate data and in the right order.
|
11
|
+
let!(:real_channel) do
|
12
|
+
channel = instance_spy('Net::SSH::Connection::Channel')
|
13
|
+
|
14
|
+
allow(channel).to receive(:exec) do |cmd, &block|
|
15
|
+
@command = cmd
|
16
|
+
# Allow the test to specify a command that fails.
|
17
|
+
#
|
18
|
+
# So ... in Ruby 1.9.3, `@command.match(/fail/)` returns nil even when it
|
19
|
+
# matches, inside this block to RSpec::Mocks::ExampleMethods#receive, but
|
20
|
+
# not outside of it. There's some deep dark voodoo going on here, so flip
|
21
|
+
# the Regexp and the String for now to make tests pass.
|
22
|
+
#
|
23
|
+
# See https://github.com/rspec/rspec-expectations/issues/781
|
24
|
+
@success = !/fail/.match(@command)
|
25
|
+
block.call(channel, @success) if block
|
26
|
+
output = "#{cmd} output"
|
27
|
+
error_output = "#{cmd} error output"
|
28
|
+
if cmd.match(/pty/) && !@request_pty
|
29
|
+
output = nil
|
30
|
+
error_output = 'no pty'
|
31
|
+
end
|
32
|
+
|
33
|
+
if output
|
34
|
+
if @on_data
|
35
|
+
@on_data.call(channel, output)
|
36
|
+
else
|
37
|
+
@on_data_pending = output
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
if error_output
|
42
|
+
if @on_extended_data
|
43
|
+
@on_extended_data.call(channel, error_output)
|
44
|
+
else
|
45
|
+
@on_extended_data_pending = error_output
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
allow(channel).to receive(:request_pty) do |&block|
|
51
|
+
block.call(channel, true) if block
|
52
|
+
@request_pty = true
|
53
|
+
end
|
54
|
+
|
55
|
+
allow(channel).to receive(:on_data) do |&block|
|
56
|
+
@on_data = block
|
57
|
+
if @on_data_pending
|
58
|
+
block.call(channel, @on_data_pending)
|
59
|
+
@on_data_pending = nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
allow(channel).to receive(:on_extended_data) do |&block|
|
64
|
+
@on_extended_data = block
|
65
|
+
if @on_extended_data_pending
|
66
|
+
block.call(channel, @on_extended_data_pending)
|
67
|
+
@on_extended_data_pending = nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
channel
|
72
|
+
end
|
73
|
+
|
74
|
+
# Inject our "real_channel" above into code that uses Net::SSH
|
75
|
+
let!(:real_session) do
|
76
|
+
session = instance_spy('Net::SSH::Connection::Session')
|
77
|
+
allow(session).to receive(:exec!) { |c| "#{c} output" }
|
78
|
+
allow(session).to receive(:open_channel) do |&blk|
|
79
|
+
|
80
|
+
blk.call(real_channel) if blk
|
81
|
+
real_channel
|
82
|
+
end
|
83
|
+
allow(Net::SSH).to receive(:start_without_hussh).and_return(session)
|
84
|
+
session
|
85
|
+
end
|
86
|
+
|
87
|
+
let(:saved_responses) { {}.to_yaml }
|
88
|
+
|
89
|
+
|
90
|
+
before do
|
91
|
+
Hussh.commands_run.clear
|
92
|
+
Hussh.clear_stubbed_responses
|
93
|
+
|
94
|
+
FileUtils.mkdir_p 'fixtures/hussh'
|
95
|
+
File.write('fixtures/hussh/saved_responses.yaml', saved_responses)
|
96
|
+
Hussh.load_recording('saved_responses')
|
97
|
+
end
|
98
|
+
|
99
|
+
describe :exec! do
|
100
|
+
context 'with a command that has not been run before' do
|
101
|
+
before do
|
102
|
+
Net::SSH.start('host', 'user') do |s|
|
103
|
+
@output = s.exec!('id')
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'runs the command via ssh' do
|
108
|
+
expect(real_session).to have_received(:exec!).with('id')
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'records that the command was run' do
|
112
|
+
expect(Hussh.commands_run).to include('id')
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'returns the result of the command' do
|
116
|
+
expect(@output).to eql("id output")
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'saves the result of the command' do
|
120
|
+
expect(Hussh.recorded_responses['host']['user']['id'])
|
121
|
+
.to eql("id output")
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'flags the recording as changed' do
|
125
|
+
expect(Hussh.recording_changed?).to eql(true)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context 'with a command that has been run before' do
|
130
|
+
before do
|
131
|
+
FileUtils.mkdir_p 'fixtures/hussh'
|
132
|
+
File.write(
|
133
|
+
'fixtures/hussh/saved_responses.yaml',
|
134
|
+
{
|
135
|
+
'host' => { 'user' => { 'hostname' => "subsix\n" } }
|
136
|
+
}.to_yaml
|
137
|
+
)
|
138
|
+
Hussh.load_recording('saved_responses')
|
139
|
+
Net::SSH.start('host', 'user') { |s| s.exec!('hostname') }
|
140
|
+
end
|
141
|
+
|
142
|
+
it "doesn't run the command via ssh" do
|
143
|
+
expect(Net::SSH).to_not have_received(:start_without_hussh)
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'records that the command was run' do
|
147
|
+
expect(Hussh.commands_run).to include('hostname')
|
148
|
+
end
|
149
|
+
|
150
|
+
it "doesn't flags the recording as changed" do
|
151
|
+
expect(Hussh.recording_changed?).to eql(false)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
describe 'using callbacks defined after exec and no pty' do
|
157
|
+
before do
|
158
|
+
# Simulate how we would use Hussh, which sits between the
|
159
|
+
# application code (the code below) and the mocked-out Net::SSH
|
160
|
+
# code.
|
161
|
+
Net::SSH.start('host', 'user') do |session|
|
162
|
+
session.open_channel do |ch|
|
163
|
+
ch.exec command
|
164
|
+
ch.on_data { |c, data| @data = data }
|
165
|
+
ch.on_extended_data { |c, data| @extended_data = data }
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
let(:command) { 'callbacks-after' }
|
171
|
+
|
172
|
+
it 'runs the command via ssh' do
|
173
|
+
expect(real_channel).to have_received(:exec).with(command)
|
174
|
+
end
|
175
|
+
|
176
|
+
it 'records the command was run' do
|
177
|
+
expect(Hussh.commands_run).to include(command)
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'gives us the stdout' do
|
181
|
+
expect(@data).to eq "#{command} output"
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'gives us the stderr' do
|
185
|
+
expect(@extended_data).to eq "#{command} error output"
|
186
|
+
end
|
187
|
+
|
188
|
+
context 'with a command that requires a pty' do
|
189
|
+
let(:command) { 'test-pty-fail' }
|
190
|
+
|
191
|
+
it 'has no stdout' do
|
192
|
+
expect(@data).to eq nil
|
193
|
+
end
|
194
|
+
|
195
|
+
it 'signals error on stderr' do
|
196
|
+
expect(@extended_data).to eq 'no pty'
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
context 'with saved responses' do
|
201
|
+
let(:saved_responses) do
|
202
|
+
{
|
203
|
+
'host' => { 'user' => { command => "recorded #{command} output" } }
|
204
|
+
}.to_yaml
|
205
|
+
end
|
206
|
+
|
207
|
+
it 'gives us the recorded stdout' do
|
208
|
+
expect(@data).to eq "recorded #{command} output"
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
describe 'callbacks defined before exec and no pty' do
|
214
|
+
before do
|
215
|
+
# Callbacks defined before the exec should still be called.
|
216
|
+
Net::SSH.start('host', 'user') do |session|
|
217
|
+
session.open_channel do |ch|
|
218
|
+
ch.on_data { |c, data| @data = data }
|
219
|
+
ch.on_extended_data { |c, data| @extended_data = data }
|
220
|
+
ch.exec 'callbacks-before'
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
it 'runs the command via ssh' do
|
226
|
+
expect(real_channel).to have_received(:exec).with('callbacks-before')
|
227
|
+
end
|
228
|
+
|
229
|
+
it 'records the command was run' do
|
230
|
+
expect(Hussh.commands_run).to include('callbacks-before')
|
231
|
+
end
|
232
|
+
|
233
|
+
it 'gives us the stdout' do
|
234
|
+
expect(@data).to eq 'callbacks-before output'
|
235
|
+
end
|
236
|
+
|
237
|
+
it 'gives us the stderr' do
|
238
|
+
expect(@extended_data).to eq 'callbacks-before error output'
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
describe 'requesting a pty' do
|
243
|
+
before do
|
244
|
+
# Request a pty before we run exec.
|
245
|
+
Net::SSH.start('host', 'user') do |session|
|
246
|
+
session.open_channel do |ch|
|
247
|
+
ch.request_pty
|
248
|
+
ch.exec 'test-pty'
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
it 'requests a pty' do
|
254
|
+
expect(real_channel).to have_received(:request_pty)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
describe 'using an exec block' do
|
259
|
+
before do
|
260
|
+
# Use Net::SSH and our callbacks defined in the exec block.
|
261
|
+
Net::SSH.start('host', 'user') do |session|
|
262
|
+
session.open_channel do |ch|
|
263
|
+
ch.exec command do |ch, success|
|
264
|
+
@exec_success = success
|
265
|
+
ch.on_data { |c, data| @data = data }
|
266
|
+
ch.on_extended_data { |c, data| @extended_data = data }
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
let(:command) { 'block-test' }
|
273
|
+
|
274
|
+
it 'runs the command via ssh' do
|
275
|
+
expect(real_channel).to have_received(:exec).with(command)
|
276
|
+
end
|
277
|
+
|
278
|
+
it 'records that the command was run' do
|
279
|
+
expect(Hussh.commands_run).to include(command)
|
280
|
+
end
|
281
|
+
|
282
|
+
it 'passes command status to exec' do
|
283
|
+
expect(@exec_success).to eql(true)
|
284
|
+
end
|
285
|
+
|
286
|
+
it 'gives us the stdout' do
|
287
|
+
expect(@data).to eq "#{command} output"
|
288
|
+
end
|
289
|
+
|
290
|
+
it 'gives us the stderr' do
|
291
|
+
expect(@extended_data).to eq "#{command} error output"
|
292
|
+
end
|
293
|
+
|
294
|
+
it 'saves the result of the command' do
|
295
|
+
expect(Hussh.recorded_responses['host']['user'][command])
|
296
|
+
.to eq "#{command} output"
|
297
|
+
end
|
298
|
+
|
299
|
+
it 'flags the recording as changed' do
|
300
|
+
expect(Hussh.recording_changed?).to eql(true)
|
301
|
+
end
|
302
|
+
|
303
|
+
context 'with a failed connection' do
|
304
|
+
let(:command) { 'test-fail' }
|
305
|
+
subject { @exec_success }
|
306
|
+
it { is_expected.to eq false }
|
307
|
+
end
|
308
|
+
|
309
|
+
context 'with a command that has recorded results' do
|
310
|
+
let(:command) { 'recorded-test' }
|
311
|
+
let(:saved_responses) do
|
312
|
+
{
|
313
|
+
'host' => { 'user' => { 'test-recorded' => "#{command} output" } }
|
314
|
+
}.to_yaml
|
315
|
+
end
|
316
|
+
|
317
|
+
it 'gives us the stdout' do
|
318
|
+
expect(@data).to eq "#{command} output"
|
319
|
+
end
|
320
|
+
|
321
|
+
it 'gives us the success status' do
|
322
|
+
expect(@exec_success).to eql(true)
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
describe 'using exec wthout a block' do
|
328
|
+
before do
|
329
|
+
# Use Net::SSH and our callbacks defined in the exec block.
|
330
|
+
Net::SSH.start('host', 'user') do |session|
|
331
|
+
ch = session.open_channel
|
332
|
+
ch.on_data { |c, data| @data = data }
|
333
|
+
ch.on_extended_data { |c, data| @extended_data = data }
|
334
|
+
ch.exec command
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
let(:command) { 'exec-no-block' }
|
339
|
+
|
340
|
+
it 'runs the command via ssh' do
|
341
|
+
expect(real_channel).to have_received(:exec).with(command)
|
342
|
+
end
|
343
|
+
|
344
|
+
it 'records that the command was run' do
|
345
|
+
expect(Hussh.commands_run).to include(command)
|
346
|
+
end
|
347
|
+
|
348
|
+
it 'gives us the stdout' do
|
349
|
+
expect(@data).to eq "#{command} output"
|
350
|
+
end
|
351
|
+
|
352
|
+
it 'gives us the stderr' do
|
353
|
+
expect(@extended_data).to eq "#{command} error output"
|
354
|
+
end
|
355
|
+
|
356
|
+
it 'saves the result of the command' do
|
357
|
+
expect(Hussh.recorded_responses['host']['user'][command])
|
358
|
+
.to eq "#{command} output"
|
359
|
+
end
|
360
|
+
|
361
|
+
it 'flags the recording as changed' do
|
362
|
+
expect(Hussh.recording_changed?).to eql(true)
|
363
|
+
end
|
364
|
+
|
365
|
+
context 'with a command that has recorded results' do
|
366
|
+
let(:saved_responses) do
|
367
|
+
{
|
368
|
+
'host' => { 'user' => { 'exec-no-block' => "#{command} output" } }
|
369
|
+
}.to_yaml
|
370
|
+
end
|
371
|
+
|
372
|
+
it 'gives us the stdout' do
|
373
|
+
expect(@data).to eq "#{command} output"
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
data/spec/hussh_session_spec.rb
CHANGED
@@ -6,72 +6,35 @@ require 'hussh'
|
|
6
6
|
|
7
7
|
RSpec.describe Hussh do
|
8
8
|
include FakeFS::SpecHelpers
|
9
|
-
describe Hussh::Session do
|
10
|
-
describe :exec! do
|
11
|
-
let(:real_session) do
|
12
|
-
spy = instance_spy('Net::SSH::Connection::Session')
|
13
|
-
allow(spy).to receive(:exec!) { |c| "#{c} output" }
|
14
|
-
spy
|
15
|
-
end
|
16
|
-
before do
|
17
|
-
Hussh.commands_run.clear
|
18
|
-
Hussh.clear_recorded_responses
|
19
|
-
Hussh.clear_stubbed_responses
|
20
|
-
allow(Net::SSH).to receive(:start_without_hussh)
|
21
|
-
.and_return(real_session)
|
22
|
-
end
|
23
|
-
|
24
|
-
context 'with a command that has not been run before' do
|
25
|
-
before do
|
26
|
-
Net::SSH.start('host', 'user') { |s| @output = s.exec!('hostname') }
|
27
|
-
end
|
28
9
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
end
|
10
|
+
describe Hussh::Session do
|
11
|
+
let(:real_session) do
|
12
|
+
spy = instance_spy('Net::SSH::Connection::Session')
|
13
|
+
allow(spy).to receive(:exec!) { |c| "#{c} output" }
|
14
|
+
spy
|
15
|
+
end
|
36
16
|
|
37
|
-
|
38
|
-
|
39
|
-
|
17
|
+
before do
|
18
|
+
Hussh.commands_run.clear
|
19
|
+
Hussh.clear_recorded_responses
|
20
|
+
Hussh.clear_stubbed_responses
|
21
|
+
allow(Net::SSH).to receive(:start_without_hussh)
|
22
|
+
.and_return(real_session)
|
23
|
+
end
|
40
24
|
|
41
|
-
|
42
|
-
|
43
|
-
.to eql('hostname output')
|
44
|
-
end
|
25
|
+
describe :open_channel do
|
26
|
+
let(:session) { Hussh::Session.new('host', 'user') }
|
45
27
|
|
46
|
-
|
47
|
-
|
48
|
-
|
28
|
+
it 'returns a new channel' do
|
29
|
+
@channel = session.open_channel
|
30
|
+
expect(@channel).to be_a(Hussh::Channel)
|
49
31
|
end
|
50
32
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
File.write(
|
55
|
-
'fixtures/hussh/saved_responses.yaml',
|
56
|
-
{
|
57
|
-
'host' => { 'user' => { 'hostname' => "subsix\n" } }
|
58
|
-
}.to_yaml
|
59
|
-
)
|
60
|
-
Hussh.load_recording('saved_responses')
|
61
|
-
Net::SSH.start('host', 'user') { |s| s.exec!('hostname') }
|
62
|
-
end
|
63
|
-
|
64
|
-
it "doesn't run the command via ssh" do
|
65
|
-
expect(Net::SSH).to_not have_received(:start_without_hussh)
|
66
|
-
end
|
67
|
-
|
68
|
-
it 'records that the command was run' do
|
69
|
-
expect(Hussh.commands_run).to include('hostname')
|
70
|
-
end
|
71
|
-
|
72
|
-
it "doesn't flags the recording as changed" do
|
73
|
-
expect(Hussh.recording_changed?).to eql(false)
|
33
|
+
it 'runs the given block' do
|
34
|
+
session.open_channel do |ch|
|
35
|
+
@channel = ch
|
74
36
|
end
|
37
|
+
expect(@channel).to be_a(Hussh::Channel)
|
75
38
|
end
|
76
39
|
end
|
77
40
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
require "codeclimate-test-reporter"
|
2
|
+
CodeClimate::TestReporter.start
|
3
|
+
|
1
4
|
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
5
|
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
6
|
# The generated `.rspec` file contains `--require spec_helper` which will cause
|
@@ -42,7 +45,6 @@ RSpec.configure do |config|
|
|
42
45
|
|
43
46
|
# The settings below are suggested to provide a good initial experience
|
44
47
|
# with RSpec, but feel free to customize to your heart's content.
|
45
|
-
=begin
|
46
48
|
# These two settings work together to allow you to limit a spec run
|
47
49
|
# to individual examples or groups you care about by tagging them with
|
48
50
|
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
|
@@ -50,6 +52,7 @@ RSpec.configure do |config|
|
|
50
52
|
config.filter_run :focus
|
51
53
|
config.run_all_when_everything_filtered = true
|
52
54
|
|
55
|
+
=begin
|
53
56
|
# Limits the available syntax to the non-monkey patched syntax that is
|
54
57
|
# recommended. For more details, see:
|
55
58
|
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hussh
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Misha Gorodnitzky
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-05-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -136,6 +136,20 @@ dependencies:
|
|
136
136
|
- - "~>"
|
137
137
|
- !ruby/object:Gem::Version
|
138
138
|
version: '2.9'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: codeclimate-test-reporter
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
139
153
|
description: Session-recording library for Net::SSH to make testing easy
|
140
154
|
email: misaka@pobox.com
|
141
155
|
executables: []
|
@@ -151,6 +165,7 @@ files:
|
|
151
165
|
- lib/hussh/version.rb
|
152
166
|
- spec/hussh_channel_spec.rb
|
153
167
|
- spec/hussh_configuration_spec.rb
|
168
|
+
- spec/hussh_functional_spec.rb
|
154
169
|
- spec/hussh_session_spec.rb
|
155
170
|
- spec/spec_helper.rb
|
156
171
|
homepage: http://github.com/moneyadviceservice/hussh
|
@@ -180,5 +195,6 @@ summary: Session-recording library for Net::SSH to make testing easy
|
|
180
195
|
test_files:
|
181
196
|
- spec/hussh_channel_spec.rb
|
182
197
|
- spec/hussh_configuration_spec.rb
|
198
|
+
- spec/hussh_functional_spec.rb
|
183
199
|
- spec/hussh_session_spec.rb
|
184
200
|
- spec/spec_helper.rb
|