test-kitchen 1.6.0 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (180) hide show
  1. checksums.yaml +4 -4
  2. data/.cane +8 -7
  3. data/.github/ISSUE_TEMPLATE.md +56 -0
  4. data/.gitignore +28 -27
  5. data/.kitchen.ci.yml +23 -0
  6. data/.kitchen.proxy.yml +27 -0
  7. data/.rubocop.yml +3 -3
  8. data/.travis.yml +70 -53
  9. data/.yardopts +3 -3
  10. data/Berksfile +3 -0
  11. data/CHANGELOG.md +1083 -1051
  12. data/CONTRIBUTING.md +14 -14
  13. data/Gemfile +19 -14
  14. data/Gemfile.proxy_tests +4 -5
  15. data/Guardfile +42 -42
  16. data/LICENSE +15 -15
  17. data/MAINTAINERS.md +23 -24
  18. data/README.md +135 -135
  19. data/Rakefile +61 -76
  20. data/appveyor.yml +44 -34
  21. data/features/kitchen_action_commands.feature +164 -164
  22. data/features/kitchen_command.feature +16 -16
  23. data/features/kitchen_console_command.feature +34 -34
  24. data/features/kitchen_defaults.feature +38 -38
  25. data/features/kitchen_diagnose_command.feature +96 -96
  26. data/features/kitchen_driver_create_command.feature +64 -64
  27. data/features/kitchen_driver_discover_command.feature +25 -25
  28. data/features/kitchen_help_command.feature +16 -16
  29. data/features/kitchen_init_command.feature +274 -274
  30. data/features/kitchen_list_command.feature +104 -104
  31. data/features/kitchen_login_command.feature +62 -62
  32. data/features/kitchen_sink_command.feature +30 -30
  33. data/features/kitchen_test_command.feature +88 -88
  34. data/features/step_definitions/gem_steps.rb +36 -36
  35. data/features/step_definitions/git_steps.rb +5 -5
  36. data/features/step_definitions/output_steps.rb +5 -5
  37. data/features/support/env.rb +75 -75
  38. data/lib/kitchen.rb +150 -150
  39. data/lib/kitchen/base64_stream.rb +55 -55
  40. data/lib/kitchen/cli.rb +419 -419
  41. data/lib/kitchen/collection.rb +55 -55
  42. data/lib/kitchen/color.rb +65 -65
  43. data/lib/kitchen/command.rb +185 -185
  44. data/lib/kitchen/command/action.rb +45 -45
  45. data/lib/kitchen/command/console.rb +58 -58
  46. data/lib/kitchen/command/diagnose.rb +92 -92
  47. data/lib/kitchen/command/driver_discover.rb +105 -105
  48. data/lib/kitchen/command/exec.rb +41 -41
  49. data/lib/kitchen/command/list.rb +119 -119
  50. data/lib/kitchen/command/login.rb +43 -43
  51. data/lib/kitchen/command/sink.rb +54 -54
  52. data/lib/kitchen/command/test.rb +51 -51
  53. data/lib/kitchen/config.rb +322 -322
  54. data/lib/kitchen/configurable.rb +529 -529
  55. data/lib/kitchen/data_munger.rb +959 -960
  56. data/lib/kitchen/diagnostic.rb +141 -141
  57. data/lib/kitchen/driver.rb +56 -56
  58. data/lib/kitchen/driver/base.rb +134 -134
  59. data/lib/kitchen/driver/dummy.rb +108 -108
  60. data/lib/kitchen/driver/proxy.rb +72 -72
  61. data/lib/kitchen/driver/ssh_base.rb +357 -357
  62. data/lib/kitchen/errors.rb +229 -229
  63. data/lib/kitchen/generator/driver_create.rb +177 -177
  64. data/lib/kitchen/generator/init.rb +296 -296
  65. data/lib/kitchen/instance.rb +662 -662
  66. data/lib/kitchen/lazy_hash.rb +142 -142
  67. data/lib/kitchen/loader/yaml.rb +349 -349
  68. data/lib/kitchen/logger.rb +423 -423
  69. data/lib/kitchen/logging.rb +56 -56
  70. data/lib/kitchen/login_command.rb +52 -52
  71. data/lib/kitchen/metadata_chopper.rb +52 -52
  72. data/lib/kitchen/platform.rb +67 -67
  73. data/lib/kitchen/provisioner.rb +54 -54
  74. data/lib/kitchen/provisioner/base.rb +236 -236
  75. data/lib/kitchen/provisioner/chef/berkshelf.rb +114 -114
  76. data/lib/kitchen/provisioner/chef/common_sandbox.rb +322 -322
  77. data/lib/kitchen/provisioner/chef/librarian.rb +112 -112
  78. data/lib/kitchen/provisioner/chef_apply.rb +124 -125
  79. data/lib/kitchen/provisioner/chef_base.rb +341 -294
  80. data/lib/kitchen/provisioner/chef_solo.rb +88 -89
  81. data/lib/kitchen/provisioner/chef_zero.rb +245 -245
  82. data/lib/kitchen/provisioner/dummy.rb +79 -79
  83. data/lib/kitchen/provisioner/shell.rb +138 -138
  84. data/lib/kitchen/rake_tasks.rb +63 -63
  85. data/lib/kitchen/shell_out.rb +93 -93
  86. data/lib/kitchen/ssh.rb +276 -276
  87. data/lib/kitchen/state_file.rb +120 -120
  88. data/lib/kitchen/suite.rb +51 -51
  89. data/lib/kitchen/thor_tasks.rb +66 -66
  90. data/lib/kitchen/transport.rb +54 -54
  91. data/lib/kitchen/transport/base.rb +176 -176
  92. data/lib/kitchen/transport/dummy.rb +79 -79
  93. data/lib/kitchen/transport/ssh.rb +364 -364
  94. data/lib/kitchen/transport/winrm.rb +486 -486
  95. data/lib/kitchen/util.rb +147 -147
  96. data/lib/kitchen/verifier.rb +55 -55
  97. data/lib/kitchen/verifier/base.rb +235 -235
  98. data/lib/kitchen/verifier/busser.rb +277 -277
  99. data/lib/kitchen/verifier/dummy.rb +79 -79
  100. data/lib/kitchen/verifier/shell.rb +101 -101
  101. data/lib/kitchen/version.rb +21 -21
  102. data/lib/vendor/hash_recursive_merge.rb +82 -82
  103. data/spec/kitchen/base64_stream_spec.rb +77 -77
  104. data/spec/kitchen/cli_spec.rb +56 -56
  105. data/spec/kitchen/collection_spec.rb +80 -80
  106. data/spec/kitchen/color_spec.rb +54 -54
  107. data/spec/kitchen/config_spec.rb +408 -408
  108. data/spec/kitchen/configurable_spec.rb +1095 -1062
  109. data/spec/kitchen/data_munger_spec.rb +2694 -2383
  110. data/spec/kitchen/diagnostic_spec.rb +129 -129
  111. data/spec/kitchen/driver/base_spec.rb +121 -121
  112. data/spec/kitchen/driver/dummy_spec.rb +199 -199
  113. data/spec/kitchen/driver/proxy_spec.rb +138 -138
  114. data/spec/kitchen/driver/ssh_base_spec.rb +1115 -1115
  115. data/spec/kitchen/driver_spec.rb +112 -112
  116. data/spec/kitchen/errors_spec.rb +309 -309
  117. data/spec/kitchen/instance_spec.rb +1419 -1419
  118. data/spec/kitchen/lazy_hash_spec.rb +117 -117
  119. data/spec/kitchen/loader/yaml_spec.rb +774 -774
  120. data/spec/kitchen/logger_spec.rb +429 -429
  121. data/spec/kitchen/logging_spec.rb +59 -59
  122. data/spec/kitchen/login_command_spec.rb +68 -68
  123. data/spec/kitchen/metadata_chopper_spec.rb +82 -82
  124. data/spec/kitchen/platform_spec.rb +89 -89
  125. data/spec/kitchen/provisioner/base_spec.rb +386 -386
  126. data/spec/kitchen/provisioner/chef_apply_spec.rb +136 -136
  127. data/spec/kitchen/provisioner/chef_base_spec.rb +1161 -1067
  128. data/spec/kitchen/provisioner/chef_solo_spec.rb +557 -557
  129. data/spec/kitchen/provisioner/chef_zero_spec.rb +1001 -1001
  130. data/spec/kitchen/provisioner/dummy_spec.rb +99 -99
  131. data/spec/kitchen/provisioner/shell_spec.rb +566 -566
  132. data/spec/kitchen/provisioner_spec.rb +107 -107
  133. data/spec/kitchen/shell_out_spec.rb +150 -150
  134. data/spec/kitchen/ssh_spec.rb +693 -693
  135. data/spec/kitchen/state_file_spec.rb +129 -129
  136. data/spec/kitchen/suite_spec.rb +62 -62
  137. data/spec/kitchen/transport/base_spec.rb +89 -89
  138. data/spec/kitchen/transport/ssh_spec.rb +1255 -1255
  139. data/spec/kitchen/transport/winrm_spec.rb +1143 -1143
  140. data/spec/kitchen/transport_spec.rb +112 -112
  141. data/spec/kitchen/util_spec.rb +165 -165
  142. data/spec/kitchen/verifier/base_spec.rb +362 -362
  143. data/spec/kitchen/verifier/busser_spec.rb +610 -610
  144. data/spec/kitchen/verifier/dummy_spec.rb +99 -99
  145. data/spec/kitchen/verifier/shell_spec.rb +160 -158
  146. data/spec/kitchen/verifier_spec.rb +120 -120
  147. data/spec/kitchen_spec.rb +114 -114
  148. data/spec/spec_helper.rb +85 -85
  149. data/spec/support/powershell_max_size_spec.rb +40 -40
  150. data/support/busser_install_command.ps1 +14 -14
  151. data/support/busser_install_command.sh +14 -14
  152. data/support/chef-client-zero.rb +77 -77
  153. data/support/chef_base_init_command.ps1 +18 -18
  154. data/support/chef_base_init_command.sh +2 -2
  155. data/support/chef_base_install_command.ps1 +85 -85
  156. data/support/chef_base_install_command.sh +229 -229
  157. data/support/chef_zero_prepare_command_legacy.ps1 +9 -9
  158. data/support/chef_zero_prepare_command_legacy.sh +10 -10
  159. data/support/download_helpers.sh +109 -109
  160. data/support/dummy-validation.pem +27 -27
  161. data/templates/driver/CHANGELOG.md.erb +3 -3
  162. data/templates/driver/Gemfile.erb +3 -3
  163. data/templates/driver/README.md.erb +64 -64
  164. data/templates/driver/Rakefile.erb +21 -21
  165. data/templates/driver/driver.rb.erb +23 -23
  166. data/templates/driver/gemspec.erb +29 -29
  167. data/templates/driver/gitignore.erb +17 -17
  168. data/templates/driver/license_apachev2.erb +15 -15
  169. data/templates/driver/license_lgplv3.erb +16 -16
  170. data/templates/driver/license_mit.erb +22 -22
  171. data/templates/driver/license_reserved.erb +5 -5
  172. data/templates/driver/tailor.erb +4 -4
  173. data/templates/driver/travis.yml.erb +11 -11
  174. data/templates/driver/version.rb.erb +12 -12
  175. data/templates/init/chefignore.erb +1 -1
  176. data/templates/init/kitchen.yml.erb +18 -18
  177. data/test-kitchen.gemspec +62 -62
  178. data/test/integration/default/default_spec.rb +3 -0
  179. data/testing_windows.md +37 -37
  180. metadata +23 -11
@@ -1,1143 +1,1143 @@
1
- # -*- encoding: utf-8 -*-
2
- #
3
- # Author:: Matt Wrock (<matt@mattwrock.com>)
4
- #
5
- # Copyright (C) 2014, Matt Wrock
6
- #
7
- # Licensed under the Apache License, Version 2.0 (the "License");
8
- # you may not use this file except in compliance with the License.
9
- # You may obtain a copy of the License at
10
- #
11
- # http://www.apache.org/licenses/LICENSE-2.0
12
- #
13
- # Unless required by applicable law or agreed to in writing, software
14
- # distributed under the License is distributed on an "AS IS" BASIS,
15
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- # See the License for the specific language governing permissions and
17
- # limitations under the License.
18
-
19
- require_relative "../../spec_helper"
20
-
21
- require "kitchen/transport/winrm"
22
- require "winrm"
23
- require "winrm-fs"
24
-
25
- module Kitchen
26
-
27
- module Transport
28
-
29
- class WinRMConnectionDummy < Kitchen::Transport::Winrm::Connection
30
-
31
- attr_reader :saved_command, :remote_path, :local_path
32
-
33
- def upload(locals, remote)
34
- @saved_command = IO.read(locals)
35
- @local_path = locals
36
- @remote_path = remote
37
- end
38
- end
39
- end
40
- end
41
-
42
- describe Kitchen::Transport::Winrm do
43
-
44
- before do
45
- RbConfig::CONFIG.stubs(:[]).with("host_os").returns("blah")
46
- end
47
-
48
- let(:logged_output) { StringIO.new }
49
- let(:logger) { Logger.new(logged_output) }
50
- let(:config) { Hash.new }
51
- let(:state) { Hash.new }
52
-
53
- let(:instance) do
54
- stub(:name => "coolbeans", :logger => logger, :to_str => "instance")
55
- end
56
-
57
- let(:transport) do
58
- t = Kitchen::Transport::Winrm.new(config)
59
- # :load_winrm_s! is not cross-platform safe
60
- # and gets initialized too early in the pipeline
61
- t.stubs(:load_winrm_s!)
62
- t.finalize_config!(instance)
63
- end
64
-
65
- it "provisioner api_version is 1" do
66
- transport.diagnose_plugin[:api_version].must_equal 1
67
- end
68
-
69
- it "plugin_version is set to Kitchen::VERSION" do
70
- transport.diagnose_plugin[:version].must_equal Kitchen::VERSION
71
- end
72
-
73
- describe "default_config" do
74
-
75
- it "sets :port to 5985 by default" do
76
- transport[:port].must_equal 5985
77
- end
78
-
79
- it "sets :username to administrator by default" do
80
- transport[:username].must_equal "administrator"
81
- end
82
-
83
- it "sets :password to nil by default" do
84
- transport[:password].must_equal nil
85
- end
86
-
87
- it "sets a default :endpoint_template value" do
88
- transport[:endpoint_template].
89
- must_equal "http://%{hostname}:%{port}/wsman"
90
- end
91
-
92
- it "sets :rdp_port to 3389 by default" do
93
- transport[:rdp_port].must_equal 3389
94
- end
95
-
96
- it "sets :connection_retries to 5 by default" do
97
- transport[:connection_retries].must_equal 5
98
- end
99
-
100
- it "sets :connection_retry_sleep to 1 by default" do
101
- transport[:connection_retry_sleep].must_equal 1
102
- end
103
-
104
- it "sets :max_wait_until_ready to 600 by default" do
105
- transport[:max_wait_until_ready].must_equal 600
106
- end
107
-
108
- it "sets :winrm_transport to :negotiate" do
109
- transport[:winrm_transport].must_equal :negotiate
110
- end
111
- end
112
-
113
- describe "#connection" do
114
-
115
- let(:klass) { Kitchen::Transport::Winrm::Connection }
116
-
117
- # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
118
- def self.common_connection_specs
119
- before do
120
- config[:hostname] = "here"
121
- config[:kitchen_root] = "/i/am/root"
122
- end
123
-
124
- it "returns a Kitchen::Transport::Winrm::Connection object" do
125
- transport.connection(state).must_be_kind_of klass
126
- end
127
-
128
- it "sets :instance_name to the instance's name" do
129
- klass.expects(:new).with do |hash|
130
- hash[:instance_name] == "coolbeans"
131
- end
132
-
133
- make_connection
134
- end
135
- it "sets :kitchen_root to the transport's kitchen_root" do
136
- klass.expects(:new).with do |hash|
137
- hash[:kitchen_root] == "/i/am/root"
138
- end
139
-
140
- make_connection
141
- end
142
-
143
- it "sets the :logger to the transport's logger" do
144
- klass.expects(:new).with do |hash|
145
- hash[:logger] == logger
146
- end
147
-
148
- make_connection
149
- end
150
-
151
- it "sets the :winrm_transport to :negotiate" do
152
- klass.expects(:new).with do |hash|
153
- hash[:winrm_transport] == :negotiate
154
- end
155
-
156
- make_connection
157
- end
158
-
159
- it "sets the :disable_sspi to false" do
160
- klass.expects(:new).with do |hash|
161
- hash[:disable_sspi] == false
162
- end
163
-
164
- make_connection
165
- end
166
-
167
- it "sets :endpoint from data in config" do
168
- config[:hostname] = "host_from_config"
169
- config[:port] = "port_from_config"
170
- config[:winrm_transport] = "ssl"
171
-
172
- klass.expects(:new).with do |hash|
173
- hash[:endpoint] == "https://host_from_config:port_from_config/wsman"
174
- end
175
-
176
- make_connection
177
- end
178
-
179
- it "sets :endpoint from data in state over config data" do
180
- state[:hostname] = "host_from_state"
181
- config[:hostname] = "host_from_config"
182
- state[:port] = "port_from_state"
183
- config[:port] = "port_from_config"
184
- config[:winrm_transport] = "ssl"
185
-
186
- klass.expects(:new).with do |hash|
187
- hash[:endpoint] == "https://host_from_state:port_from_state/wsman"
188
- end
189
-
190
- make_connection
191
- end
192
-
193
- it "sets :user from :username in config" do
194
- config[:username] = "user_from_config"
195
-
196
- klass.expects(:new).with do |hash|
197
- hash[:user] == "user_from_config"
198
- end
199
-
200
- make_connection
201
- end
202
-
203
- it "sets :user from :username in state over config data" do
204
- state[:username] = "user_from_state"
205
- config[:username] = "user_from_config"
206
-
207
- klass.expects(:new).with do |hash|
208
- hash[:user] == "user_from_state"
209
- end
210
-
211
- make_connection
212
- end
213
-
214
- it "sets :pass from :password in config" do
215
- config[:password] = "pass_from_config"
216
-
217
- klass.expects(:new).with do |hash|
218
- hash[:pass] == "pass_from_config"
219
- end
220
-
221
- make_connection
222
- end
223
-
224
- it "sets :pass from :password in state over config data" do
225
- state[:password] = "pass_from_state"
226
- config[:password] = "pass_from_config"
227
-
228
- klass.expects(:new).with do |hash|
229
- hash[:pass] == "pass_from_state"
230
- end
231
-
232
- make_connection
233
- end
234
-
235
- it "sets :rdp_port from config" do
236
- config[:rdp_port] = "rdp_from_config"
237
-
238
- klass.expects(:new).with do |hash|
239
- hash[:rdp_port] == "rdp_from_config"
240
- end
241
-
242
- make_connection
243
- end
244
-
245
- it "sets :rdp_port from state over config data" do
246
- state[:rdp_port] = "rdp_from_state"
247
- config[:rdp_port] = "rdp_from_config"
248
-
249
- klass.expects(:new).with do |hash|
250
- hash[:rdp_port] == "rdp_from_state"
251
- end
252
-
253
- make_connection
254
- end
255
-
256
- it "sets :connection_retries from config" do
257
- config[:connection_retries] = "retries_from_config"
258
-
259
- klass.expects(:new).with do |hash|
260
- hash[:connection_retries] == "retries_from_config"
261
- end
262
-
263
- make_connection
264
- end
265
-
266
- it "sets :connection_retries from state over config data" do
267
- state[:connection_retries] = "retries_from_state"
268
- config[:connection_retries] = "retries_from_config"
269
-
270
- klass.expects(:new).with do |hash|
271
- hash[:connection_retries] == "retries_from_state"
272
- end
273
-
274
- make_connection
275
- end
276
-
277
- it "sets :connection_retry_sleep from config" do
278
- config[:connection_retry_sleep] = "sleep_from_config"
279
-
280
- klass.expects(:new).with do |hash|
281
- hash[:connection_retry_sleep] == "sleep_from_config"
282
- end
283
-
284
- make_connection
285
- end
286
-
287
- it "sets :connection_retry_sleep from state over config data" do
288
- state[:connection_retry_sleep] = "sleep_from_state"
289
- config[:connection_retry_sleep] = "sleep_from_config"
290
-
291
- klass.expects(:new).with do |hash|
292
- hash[:connection_retry_sleep] == "sleep_from_state"
293
- end
294
-
295
- make_connection
296
- end
297
-
298
- it "sets :max_wait_until_ready from config" do
299
- config[:max_wait_until_ready] = "max_from_config"
300
-
301
- klass.expects(:new).with do |hash|
302
- hash[:max_wait_until_ready] == "max_from_config"
303
- end
304
-
305
- make_connection
306
- end
307
-
308
- it "sets :max_wait_until_ready from state over config data" do
309
- state[:max_wait_until_ready] = "max_from_state"
310
- config[:max_wait_until_ready] = "max_from_config"
311
-
312
- klass.expects(:new).with do |hash|
313
- hash[:max_wait_until_ready] == "max_from_state"
314
- end
315
-
316
- make_connection
317
- end
318
-
319
- it "sets :winrm_transport from config data" do
320
- config[:winrm_transport] = "ssl"
321
-
322
- klass.expects(:new).with do |hash|
323
- hash[:winrm_transport] == :ssl
324
- end
325
-
326
- make_connection
327
- end
328
-
329
- describe "when negotiate is set in config" do
330
- before do
331
- config[:winrm_transport] = "negotiate"
332
- end
333
-
334
- it "sets :winrm_transport to negotiate" do
335
-
336
- klass.expects(:new).with do |hash|
337
- hash[:winrm_transport] == :negotiate &&
338
- hash[:disable_sspi] == false &&
339
- hash[:basic_auth_only] == false
340
- end
341
-
342
- make_connection
343
- end
344
- end
345
-
346
- it "returns the same connection when called again with same state" do
347
- first_connection = make_connection(state)
348
- second_connection = make_connection(state)
349
-
350
- first_connection.object_id.must_equal second_connection.object_id
351
- end
352
-
353
- it "logs a debug message when the connection is reused" do
354
- make_connection(state)
355
- make_connection(state)
356
-
357
- logged_output.string.lines.count { |l|
358
- l =~ debug_line_with("[WinRM] reusing existing connection ")
359
- }.must_equal 1
360
- end
361
-
362
- it "returns a new connection when called again if state differs" do
363
- first_connection = make_connection(state)
364
- second_connection = make_connection(state.merge(:port => 9000))
365
-
366
- first_connection.object_id.wont_equal second_connection.object_id
367
- end
368
-
369
- it "closes first connection when a second is created" do
370
- first_connection = make_connection(state)
371
- first_connection.expects(:close)
372
-
373
- make_connection(state.merge(:port => 9000))
374
- end
375
-
376
- it "logs a debug message a second connection is created" do
377
- make_connection(state)
378
- make_connection(state.merge(:port => 9000))
379
-
380
- logged_output.string.lines.count { |l|
381
- l =~ debug_line_with("[WinRM] shutting previous connection ")
382
- }.must_equal 1
383
- end
384
- end
385
- # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
386
-
387
- describe "called without a block" do
388
-
389
- def make_connection(s = state)
390
- transport.connection(s)
391
- end
392
-
393
- common_connection_specs
394
- end
395
-
396
- describe "called with a block" do
397
-
398
- def make_connection(s = state)
399
- transport.connection(s) do |conn|
400
- conn
401
- end
402
- end
403
-
404
- common_connection_specs
405
- end
406
- end
407
-
408
- describe "#load_needed_dependencies" do
409
- describe "winrm-fs" do
410
- before do
411
- # force loading of winrm-fs to get the version constant
412
- require "winrm-fs"
413
- end
414
-
415
- it "logs a message to debug that code will be loaded" do
416
- transport
417
-
418
- logged_output.string.must_match debug_line_with(
419
- "winrm-fs requested, loading winrm-fs gem")
420
- end
421
-
422
- it "logs a message to debug when library is initially loaded" do
423
- transport = Kitchen::Transport::Winrm.new(config)
424
- transport.stubs(:require).with("winrm", anything)
425
- transport.stubs(:require).with("winrm-fs").returns(true)
426
- transport.finalize_config!(instance)
427
-
428
- logged_output.string.must_match(
429
- /winrm-fs is loaded/
430
- )
431
- end
432
-
433
- it "logs a message to debug when library is previously loaded" do
434
- transport = Kitchen::Transport::Winrm.new(config)
435
- transport.stubs(:require).with("winrm", anything)
436
- transport.stubs(:require).with("winrm-fs").returns(false)
437
- transport.finalize_config!(instance)
438
-
439
- logged_output.string.must_match(
440
- /winrm-fs was already loaded/
441
- )
442
- end
443
-
444
- it "logs a message to fatal when libraries cannot be loaded" do
445
- transport = Kitchen::Transport::Winrm.new(config)
446
- transport.stubs(:require).with("winrm", anything)
447
- transport.stubs(:require).with("winrm-fs").
448
- raises(LoadError, "uh oh")
449
- begin
450
- transport.finalize_config!(instance)
451
- rescue # rubocop:disable Lint/HandleExceptions
452
- # we are interested in the log output, not this exception
453
- end
454
-
455
- logged_output.string.must_match fatal_line_with(
456
- "The `winrm-fs` gem is missing and must be installed")
457
- end
458
-
459
- it "raises a UserError when libraries cannot be loaded" do
460
- transport = Kitchen::Transport::Winrm.new(config)
461
- transport.stubs(:require).with("winrm", anything)
462
- transport.stubs(:require).with("winrm-fs").
463
- raises(LoadError, "uh oh")
464
-
465
- err = proc {
466
- transport.finalize_config!(instance)
467
- }.must_raise Kitchen::UserError
468
- err.message.must_match(/^Could not load or activate winrm-fs\. /)
469
- end
470
- end
471
-
472
- describe "winrm" do
473
- it "logs a message to debug that code will be loaded" do
474
- transport = Kitchen::Transport::Winrm.new(config)
475
- transport.stubs(:require).with("winrm-fs", anything)
476
- transport.stubs(:require)
477
- transport.finalize_config!(instance)
478
-
479
- logged_output.string.must_match debug_line_with(
480
- "winrm requested, loading winrm gem")
481
- end
482
-
483
- it "logs a message to debug when library is initially loaded" do
484
- transport = Kitchen::Transport::Winrm.new(config)
485
- transport.stubs(:require).with("winrm-fs", anything)
486
- transport.stubs(:require).returns(true)
487
-
488
- transport.finalize_config!(instance)
489
-
490
- logged_output.string.must_match(
491
- /winrm is loaded/
492
- )
493
- end
494
-
495
- it "logs a message to debug when library is previously loaded" do
496
- transport = Kitchen::Transport::Winrm.new(config)
497
- transport.stubs(:require).with("winrm-fs", anything)
498
- transport.stubs(:require).returns(false)
499
-
500
- transport.finalize_config!(instance)
501
-
502
- logged_output.string.must_match(
503
- /winrm was already loaded/
504
- )
505
- end
506
-
507
- it "logs a message to fatal when libraries cannot be loaded" do
508
- transport = Kitchen::Transport::Winrm.new(config)
509
- transport.stubs(:require).with("winrm-fs", anything)
510
- transport.stubs(:require).raises(LoadError, "uh oh")
511
- begin
512
- transport.finalize_config!(instance)
513
- rescue # rubocop:disable Lint/HandleExceptions
514
- # we are interested in the log output, not this exception
515
- end
516
-
517
- logged_output.string.must_match fatal_line_with(
518
- "The `winrm` gem is missing and must be installed")
519
- end
520
-
521
- it "raises a UserError when libraries cannot be loaded" do
522
- transport = Kitchen::Transport::Winrm.new(config)
523
- transport.stubs(:require).with("winrm-fs", anything)
524
- transport.stubs(:require).raises(LoadError, "uh oh")
525
-
526
- err = proc {
527
- transport.finalize_config!(instance)
528
- }.must_raise Kitchen::UserError
529
- err.message.must_match(/^Could not load or activate winrm\. /)
530
- end
531
- end
532
- end
533
-
534
- def debug_line_with(msg)
535
- %r{^D, .* : #{Regexp.escape(msg)}}
536
- end
537
-
538
- def fatal_line_with(msg)
539
- %r{^F, .* : #{Regexp.escape(msg)}}
540
- end
541
- end
542
-
543
- describe Kitchen::Transport::Winrm::Connection do
544
-
545
- let(:logged_output) { StringIO.new }
546
- let(:logger) { Logger.new(logged_output) }
547
-
548
- let(:options) do
549
- { :logger => logger, :user => "me", :pass => "haha",
550
- :endpoint => "http://foo:5985/wsman", :winrm_transport => :plaintext,
551
- :kitchen_root => "/i/am/root", :instance_name => "coolbeans",
552
- :rdp_port => "rdpyeah" }
553
- end
554
-
555
- let(:info) do
556
- copts = { :user => "me", :pass => "haha" }
557
- "plaintext::http://foo:5985/wsman<#{copts}>"
558
- end
559
-
560
- let(:winrm_session) do
561
- s = mock("winrm_session")
562
- s.responds_like_instance_of(::WinRM::WinRMWebService)
563
- s
564
- end
565
-
566
- let(:executor) do
567
- s = mock("command_executor")
568
- s.responds_like_instance_of(WinRM::CommandExecutor)
569
- s
570
- end
571
-
572
- let(:connection) do
573
- Kitchen::Transport::Winrm::Connection.new(options)
574
- end
575
-
576
- before do
577
- WinRM::WinRMWebService.stubs(:new).returns(winrm_session)
578
- winrm_session.stubs(:logger=)
579
- logger.level = Logger::DEBUG
580
- end
581
-
582
- describe "#close" do
583
-
584
- let(:response) do
585
- o = WinRM::Output.new
586
- o[:exitcode] = 0
587
- o[:data].concat([{ :stdout => "ok\r\n" }])
588
- o
589
- end
590
-
591
- before do
592
- winrm_session.stubs(:create_executor).returns(executor)
593
- executor.stubs(:close)
594
- executor.stubs(:run_powershell_script).
595
- with("doit").yields("ok\n", nil).returns(response)
596
- end
597
-
598
- it "only closes the shell once for multiple calls" do
599
- executor.expects(:close).once
600
-
601
- connection.execute("doit")
602
- connection.close
603
- connection.close
604
- connection.close
605
- end
606
- end
607
-
608
- describe "#execute" do
609
-
610
- before do
611
- winrm_session.stubs(:create_executor).returns(executor)
612
- end
613
-
614
- describe "for a successful command" do
615
-
616
- let(:response) do
617
- o = WinRM::Output.new
618
- o[:exitcode] = 0
619
- o[:data].concat([
620
- { :stdout => "ok\r\n" },
621
- { :stderr => "congrats\r\n" }
622
- ])
623
- o
624
- end
625
-
626
- before do
627
- executor.expects(:run_powershell_script).
628
- with("doit").yields("ok\n", nil).returns(response)
629
- end
630
-
631
- it "logger displays command on debug" do
632
- connection.execute("doit")
633
-
634
- logged_output.string.must_match debug_line(
635
- "[WinRM] #{info} (doit)")
636
- end
637
-
638
- it "logger captures stdout" do
639
- connection.execute("doit")
640
-
641
- logged_output.string.must_match(/^ok$/)
642
- end
643
-
644
- it "logger captures stderr on warn if logger is at debug level" do
645
- logger.level = Logger::DEBUG
646
- connection.execute("doit")
647
-
648
- logged_output.string.must_match warn_line("congrats")
649
- end
650
-
651
- it "logger does not log stderr on warn if logger is below debug level" do
652
- logger.level = Logger::INFO
653
- connection.execute("doit")
654
-
655
- logged_output.string.wont_match warn_line("congrats")
656
- end
657
- end
658
-
659
- describe "long command" do
660
- let(:command) { %{Write-Host "#{"a" * 4000}"} }
661
-
662
- let(:connection) do
663
- Kitchen::Transport::WinRMConnectionDummy.new(options)
664
- end
665
-
666
- let(:response) do
667
- o = WinRM::Output.new
668
- o[:exitcode] = 0
669
- o[:data].concat([
670
- { :stdout => "ok\r\n" },
671
- { :stderr => "congrats\r\n" }
672
- ])
673
- o
674
- end
675
-
676
- before do
677
- executor.expects(:run_powershell_script).with(
678
- %{powershell -ExecutionPolicy Bypass -File "$env:TEMP/kitchen/coolbeans-long_script.ps1"}
679
- ).yields("ok\n", nil).returns(response)
680
- end
681
-
682
- it "uploads the long command" do
683
- with_fake_fs do
684
- connection.execute(command)
685
-
686
- connection.saved_command.must_equal command
687
- end
688
- end
689
- end
690
-
691
- describe "for a failed command" do
692
-
693
- let(:response) do
694
- o = WinRM::Output.new
695
- o[:exitcode] = 1
696
- o[:data].concat([
697
- { :stderr => "#< CLIXML\r\n" },
698
- { :stderr => "<Objs Version=\"1.1.0.1\" xmlns=\"http://schemas." },
699
- { :stderr => "microsoft.com/powershell/2004/04\"><S S=\"Error\">" },
700
- { :stderr => "doit : The term 'doit' is not recognized as the " },
701
- { :stderr => "name of a cmdlet, function, _x000D__x000A_</S>" },
702
- { :stderr => "<S S=\"Error\">script file, or operable program. " },
703
- { :stderr => "Check the spelling of" },
704
- { :stderr => "the name, or if a path _x000D__x000A_</S><S S=\"E" },
705
- { :stderr => "rror\">was included, verify that the path is corr" },
706
- { :stderr => "ect and try again._x000D__x000A_</S><S S=\"Error" },
707
- { :stderr => "\">At line:1 char:1_x000D__x000A_</S><S S=\"Error" },
708
- { :stderr => "\">+ doit_x000D__x000A_</S><S S=\"Error\">+ ~~~~_" },
709
- { :stderr => "x000D__x000A_</S><S S=\"Error\"> + CategoryInf" },
710
- { :stderr => "o : ObjectNotFound: (doit:String) [], Co" },
711
- { :stderr => "mmandNotFoun _x000D__x000A_</S><S S=\"Error\"> " },
712
- { :stderr => "dException_x000D__x000A_</S><S S=\"Error\"> + " },
713
- { :stderr => "FullyQualifiedErrorId : CommandNotFoundException_" },
714
- { :stderr => "x000D__x000A_</S><S S=\"Error\"> _x000D__x000A_</" },
715
- { :stderr => "S></Objs>" }
716
- ])
717
- o
718
- end
719
-
720
- before do
721
- executor.expects(:run_powershell_script).
722
- with("doit").yields("nope\n", nil).returns(response)
723
- end
724
-
725
- # rubocop:disable Metrics/MethodLength
726
- def self.common_failed_command_specs
727
- it "logger displays command on debug" do
728
- begin
729
- connection.execute("doit")
730
- rescue # rubocop:disable Lint/HandleExceptions
731
- # the raise is not what is being tested here, rather its side-effect
732
- end
733
-
734
- logged_output.string.must_match debug_line(
735
- "[WinRM] #{info} (doit)"
736
- )
737
- end
738
-
739
- it "logger captures stdout" do
740
- begin
741
- connection.execute("doit")
742
- rescue # rubocop:disable Lint/HandleExceptions
743
- # the raise is not what is being tested here, rather its side-effect
744
- end
745
-
746
- logged_output.string.must_match(/^nope$/)
747
- end
748
-
749
- it "stderr is printed on logger warn level" do
750
- begin
751
- connection.execute("doit")
752
- rescue # rubocop:disable Lint/HandleExceptions
753
- # the raise is not what is being tested here, rather its side-effect
754
- end
755
-
756
- message = <<'MSG'.chomp!
757
- doit : The term 'doit' is not recognized as the name of a cmdlet, function,
758
- script file, or operable program. Check the spelling ofthe name, or if a path
759
- was included, verify that the path is correct and try again.
760
- At line:1 char:1
761
- + doit
762
- + ~~~~
763
- + CategoryInfo : ObjectNotFound: (doit:String) [], CommandNotFoun
764
- dException
765
- + FullyQualifiedErrorId : CommandNotFoundException
766
- MSG
767
-
768
- message.lines.each do |line|
769
- logged_output.string.must_match warn_line(line.chomp)
770
- end
771
- end
772
- end
773
- # rubocop:enable Metrics/MethodLength
774
-
775
- describe "when a non-zero exit code is returned" do
776
-
777
- common_failed_command_specs
778
-
779
- it "raises a WinrmFailed exception" do
780
- err = proc {
781
- connection.execute("doit")
782
- }.must_raise Kitchen::Transport::WinrmFailed
783
- err.message.must_equal "WinRM exited (1) for command: [doit]"
784
- end
785
- end
786
- end
787
-
788
- describe "for a nil command" do
789
-
790
- it "does not log on debug" do
791
- executor.expects(:open).never
792
- connection.execute(nil)
793
-
794
- logged_output.string.must_equal ""
795
- end
796
- end
797
-
798
- [
799
- Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED,
800
- Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH,
801
- ::WinRM::WinRMHTTPTransportError, ::WinRM::WinRMAuthorizationError,
802
- HTTPClient::KeepAliveDisconnected, HTTPClient::ConnectTimeoutError
803
- ].each do |klass|
804
- describe "raising #{klass}" do
805
-
806
- before do
807
- k = if klass == ::WinRM::WinRMHTTPTransportError
808
- # this exception takes 2 args in its constructor, which is not stock
809
- klass.new("dang", 200)
810
- else
811
- klass
812
- end
813
-
814
- options[:connection_retries] = 3
815
- options[:connection_retry_sleep] = 7
816
- winrm_session.stubs(:create_executor).raises(k)
817
- end
818
-
819
- it "reraises the #{klass} exception" do
820
- proc { connection.execute("nope") }.must_raise klass
821
- end
822
- end
823
- end
824
- end
825
-
826
- describe "#login_command" do
827
-
828
- let(:login_command) { connection.login_command }
829
- let(:args) { login_command.arguments.join(" ") }
830
- let(:exec_args) { login_command.exec_args }
831
-
832
- let(:rdp_doc) do
833
- File.join(File.join(options[:kitchen_root], ".kitchen", "coolbeans.rdp"))
834
- end
835
-
836
- describe "for Mac-based workstations" do
837
-
838
- before do
839
- RbConfig::CONFIG.stubs(:[]).with("host_os").returns("darwin14")
840
- end
841
-
842
- it "returns a LoginCommand" do
843
- with_fake_fs do
844
- FileUtils.mkdir_p(File.dirname(rdp_doc))
845
- login_command.must_be_instance_of Kitchen::LoginCommand
846
- end
847
- end
848
-
849
- it "creates an rdp document" do
850
- actual = nil
851
- with_fake_fs do
852
- FileUtils.mkdir_p(File.dirname(rdp_doc))
853
- login_command
854
- actual = IO.read(rdp_doc)
855
- end
856
-
857
- actual.must_equal Kitchen::Util.outdent!(<<-RDP)
858
- drivestoredirect:s:*
859
- full address:s:foo:rdpyeah
860
- prompt for credentials:i:1
861
- username:s:me
862
- RDP
863
- end
864
-
865
- it "prints the rdp document on debug" do
866
- with_fake_fs do
867
- FileUtils.mkdir_p(File.dirname(rdp_doc))
868
- login_command
869
- end
870
-
871
- expected = Kitchen::Util.outdent!(<<-OUTPUT)
872
- Creating RDP document for coolbeans (/i/am/root/.kitchen/coolbeans.rdp)
873
- ------------
874
- drivestoredirect:s:*
875
- full address:s:foo:rdpyeah
876
- prompt for credentials:i:1
877
- username:s:me
878
- ------------
879
- OUTPUT
880
- debug_output(logged_output.string).must_match expected
881
- end
882
-
883
- it "returns a LoginCommand which calls open on the rdp document" do
884
- actual = nil
885
- with_fake_fs do
886
- FileUtils.mkdir_p(File.dirname(rdp_doc))
887
- actual = login_command
888
- end
889
-
890
- actual.exec_args.must_equal ["open", rdp_doc, {}]
891
- end
892
- end
893
-
894
- describe "for Windows-based workstations" do
895
-
896
- before do
897
- RbConfig::CONFIG.stubs(:[]).with("host_os").returns("mingw32")
898
- end
899
-
900
- it "returns a LoginCommand" do
901
- with_fake_fs do
902
- FileUtils.mkdir_p(File.dirname(rdp_doc))
903
- login_command.must_be_instance_of Kitchen::LoginCommand
904
- end
905
- end
906
-
907
- it "creates an rdp document" do
908
- actual = nil
909
- with_fake_fs do
910
- FileUtils.mkdir_p(File.dirname(rdp_doc))
911
- login_command
912
- actual = IO.read(rdp_doc)
913
- end
914
-
915
- actual.must_equal Kitchen::Util.outdent!(<<-RDP)
916
- full address:s:foo:rdpyeah
917
- prompt for credentials:i:1
918
- username:s:me
919
- RDP
920
- end
921
-
922
- it "prints the rdp document on debug" do
923
- with_fake_fs do
924
- FileUtils.mkdir_p(File.dirname(rdp_doc))
925
- login_command
926
- end
927
-
928
- expected = Kitchen::Util.outdent!(<<-OUTPUT)
929
- Creating RDP document for coolbeans (/i/am/root/.kitchen/coolbeans.rdp)
930
- ------------
931
- full address:s:foo:rdpyeah
932
- prompt for credentials:i:1
933
- username:s:me
934
- ------------
935
- OUTPUT
936
- debug_output(logged_output.string).must_match expected
937
- end
938
-
939
- it "returns a LoginCommand which calls mstsc on the rdp document" do
940
- actual = nil
941
- with_fake_fs do
942
- FileUtils.mkdir_p(File.dirname(rdp_doc))
943
- actual = login_command
944
- end
945
-
946
- actual.exec_args.must_equal ["mstsc", rdp_doc, {}]
947
- end
948
- end
949
-
950
- describe "for Linux-based workstations" do
951
-
952
- before do
953
- RbConfig::CONFIG.stubs(:[]).with("host_os").returns("linux-gnu")
954
- end
955
-
956
- it "returns a LoginCommand" do
957
- login_command.must_be_instance_of Kitchen::LoginCommand
958
- end
959
-
960
- it "is an rdesktop command" do
961
- login_command.command.must_equal "rdesktop"
962
- args.must_match %r{ foo:rdpyeah$}
963
- end
964
-
965
- it "sets the user" do
966
- args.must_match regexify("-u me ")
967
- end
968
-
969
- it "sets the pass if given" do
970
- args.must_match regexify(" -p haha ")
971
- end
972
-
973
- it "won't set the pass if not given" do
974
- options.delete(:pass)
975
-
976
- args.wont_match regexify(" -p haha ")
977
- end
978
- end
979
-
980
- describe "for unknown workstation platforms" do
981
-
982
- before do
983
- RbConfig::CONFIG.stubs(:[]).with("host_os").returns("cray")
984
- end
985
-
986
- it "raises an ActionFailed error" do
987
- err = proc { login_command }.must_raise Kitchen::ActionFailed
988
- err.message.must_equal "Remote login not supported in " \
989
- "Kitchen::Transport::Winrm::Connection from host OS 'cray'."
990
- end
991
- end
992
- end
993
-
994
- describe "#upload" do
995
-
996
- let(:transporter) do
997
- t = mock("file_transporter")
998
- t.responds_like_instance_of(WinRM::FS::Core::FileTransporter)
999
- t
1000
- end
1001
-
1002
- before do
1003
- winrm_session.stubs(:create_executor).returns(executor)
1004
-
1005
- WinRM::FS::Core::FileTransporter.stubs(:new).
1006
- with(executor).returns(transporter)
1007
- transporter.stubs(:upload)
1008
- end
1009
-
1010
- def self.common_specs_for_upload
1011
- it "builds a Winrm::FileTransporter" do
1012
- WinRM::FS::Core::FileTransporter.unstub(:new)
1013
-
1014
- WinRM::FS::Core::FileTransporter.expects(:new).
1015
- with(executor).returns(transporter)
1016
-
1017
- upload
1018
- end
1019
-
1020
- it "reuses the Winrm::FileTransporter" do
1021
- WinRM::FS::Core::FileTransporter.unstub(:new)
1022
-
1023
- WinRM::FS::Core::FileTransporter.expects(:new).
1024
- with(executor).returns(transporter).once
1025
-
1026
- upload
1027
- upload
1028
- upload
1029
- end
1030
- end
1031
-
1032
- describe "for a file" do
1033
-
1034
- def upload # execute every time, not lazily once
1035
- connection.upload("/tmp/file.txt", "C:\\dest")
1036
- end
1037
-
1038
- common_specs_for_upload
1039
- end
1040
-
1041
- describe "for a collection of files" do
1042
-
1043
- def upload # execute every time, not lazily once
1044
- connection.upload(%W[/tmp/file1.txt /tmp/file2.txt], "C:\\dest")
1045
- end
1046
-
1047
- common_specs_for_upload
1048
- end
1049
- end
1050
-
1051
- describe "#wait_until_ready" do
1052
-
1053
- before do
1054
- winrm_session.stubs(:create_executor).returns(executor)
1055
- options[:max_wait_until_ready] = 300
1056
- end
1057
-
1058
- describe "when connection is successful" do
1059
-
1060
- let(:response) do
1061
- o = WinRM::Output.new
1062
- o[:exitcode] = 0
1063
- o[:data].concat([{ :stdout => "[WinRM] Established\r\n" }])
1064
- o
1065
- end
1066
-
1067
- before do
1068
- executor.expects(:run_powershell_script).
1069
- with("Write-Host '[WinRM] Established\n'").returns(response)
1070
- end
1071
-
1072
- it "executes an empty command string to ensure working" do
1073
- connection.wait_until_ready
1074
- end
1075
- end
1076
-
1077
- describe "when connection suceeds but command fails, sad panda" do
1078
-
1079
- let(:response) do
1080
- o = WinRM::Output.new
1081
- o[:exitcode] = 42
1082
- o[:data].concat([{ :stderr => "Ah crap.\r\n" }])
1083
- o
1084
- end
1085
-
1086
- before do
1087
- executor.expects(:run_powershell_script).
1088
- with("Write-Host '[WinRM] Established\n'").returns(response)
1089
- end
1090
-
1091
- it "executes an empty command string to ensure working" do
1092
- err = proc {
1093
- connection.wait_until_ready
1094
- }.must_raise Kitchen::Transport::WinrmFailed
1095
- err.message.must_equal "WinRM exited (42) for command: " \
1096
- "[Write-Host '[WinRM] Established\n']"
1097
- end
1098
-
1099
- it "stderr is printed on logger warn level" do
1100
- begin
1101
- connection.wait_until_ready
1102
- rescue # rubocop:disable Lint/HandleExceptions
1103
- # the raise is not what is being tested here, rather its side-effect
1104
- end
1105
-
1106
- logged_output.string.must_match warn_line("Ah crap.\n")
1107
- end
1108
- end
1109
- end
1110
-
1111
- def debug_output(output)
1112
- regexp = %r{^D, .* DEBUG -- : }
1113
- output.lines.grep(%r{^D, .* DEBUG -- : }).map { |l| l.sub(regexp, "") }.join
1114
- end
1115
-
1116
- def debug_line(msg)
1117
- %r{^D, .* : #{Regexp.escape(msg)}$}
1118
- end
1119
-
1120
- def debug_line_with(msg)
1121
- %r{^D, .* : #{Regexp.escape(msg)}}
1122
- end
1123
-
1124
- def info_line(msg)
1125
- %r{^I, .* : #{Regexp.escape(msg)}$}
1126
- end
1127
-
1128
- def info_line_with(msg)
1129
- %r{^I, .* : #{Regexp.escape(msg)}}
1130
- end
1131
-
1132
- def regexify(string)
1133
- Regexp.new(Regexp.escape(string))
1134
- end
1135
-
1136
- def warn_line(msg)
1137
- %r{^W, .* : #{Regexp.escape(msg)}$}
1138
- end
1139
-
1140
- def warn_line_with(msg)
1141
- %r{^W, .* : #{Regexp.escape(msg)}}
1142
- end
1143
- end
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Matt Wrock (<matt@mattwrock.com>)
4
+ #
5
+ # Copyright (C) 2014, Matt Wrock
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require_relative "../../spec_helper"
20
+
21
+ require "kitchen/transport/winrm"
22
+ require "winrm"
23
+ require "winrm-fs"
24
+
25
+ module Kitchen
26
+
27
+ module Transport
28
+
29
+ class WinRMConnectionDummy < Kitchen::Transport::Winrm::Connection
30
+
31
+ attr_reader :saved_command, :remote_path, :local_path
32
+
33
+ def upload(locals, remote)
34
+ @saved_command = IO.read(locals)
35
+ @local_path = locals
36
+ @remote_path = remote
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ describe Kitchen::Transport::Winrm do
43
+
44
+ before do
45
+ RbConfig::CONFIG.stubs(:[]).with("host_os").returns("blah")
46
+ end
47
+
48
+ let(:logged_output) { StringIO.new }
49
+ let(:logger) { Logger.new(logged_output) }
50
+ let(:config) { Hash.new }
51
+ let(:state) { Hash.new }
52
+
53
+ let(:instance) do
54
+ stub(:name => "coolbeans", :logger => logger, :to_str => "instance")
55
+ end
56
+
57
+ let(:transport) do
58
+ t = Kitchen::Transport::Winrm.new(config)
59
+ # :load_winrm_s! is not cross-platform safe
60
+ # and gets initialized too early in the pipeline
61
+ t.stubs(:load_winrm_s!)
62
+ t.finalize_config!(instance)
63
+ end
64
+
65
+ it "provisioner api_version is 1" do
66
+ transport.diagnose_plugin[:api_version].must_equal 1
67
+ end
68
+
69
+ it "plugin_version is set to Kitchen::VERSION" do
70
+ transport.diagnose_plugin[:version].must_equal Kitchen::VERSION
71
+ end
72
+
73
+ describe "default_config" do
74
+
75
+ it "sets :port to 5985 by default" do
76
+ transport[:port].must_equal 5985
77
+ end
78
+
79
+ it "sets :username to administrator by default" do
80
+ transport[:username].must_equal "administrator"
81
+ end
82
+
83
+ it "sets :password to nil by default" do
84
+ transport[:password].must_equal nil
85
+ end
86
+
87
+ it "sets a default :endpoint_template value" do
88
+ transport[:endpoint_template].
89
+ must_equal "http://%{hostname}:%{port}/wsman"
90
+ end
91
+
92
+ it "sets :rdp_port to 3389 by default" do
93
+ transport[:rdp_port].must_equal 3389
94
+ end
95
+
96
+ it "sets :connection_retries to 5 by default" do
97
+ transport[:connection_retries].must_equal 5
98
+ end
99
+
100
+ it "sets :connection_retry_sleep to 1 by default" do
101
+ transport[:connection_retry_sleep].must_equal 1
102
+ end
103
+
104
+ it "sets :max_wait_until_ready to 600 by default" do
105
+ transport[:max_wait_until_ready].must_equal 600
106
+ end
107
+
108
+ it "sets :winrm_transport to :negotiate" do
109
+ transport[:winrm_transport].must_equal :negotiate
110
+ end
111
+ end
112
+
113
+ describe "#connection" do
114
+
115
+ let(:klass) { Kitchen::Transport::Winrm::Connection }
116
+
117
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
118
+ def self.common_connection_specs
119
+ before do
120
+ config[:hostname] = "here"
121
+ config[:kitchen_root] = "/i/am/root"
122
+ end
123
+
124
+ it "returns a Kitchen::Transport::Winrm::Connection object" do
125
+ transport.connection(state).must_be_kind_of klass
126
+ end
127
+
128
+ it "sets :instance_name to the instance's name" do
129
+ klass.expects(:new).with do |hash|
130
+ hash[:instance_name] == "coolbeans"
131
+ end
132
+
133
+ make_connection
134
+ end
135
+ it "sets :kitchen_root to the transport's kitchen_root" do
136
+ klass.expects(:new).with do |hash|
137
+ hash[:kitchen_root] == "/i/am/root"
138
+ end
139
+
140
+ make_connection
141
+ end
142
+
143
+ it "sets the :logger to the transport's logger" do
144
+ klass.expects(:new).with do |hash|
145
+ hash[:logger] == logger
146
+ end
147
+
148
+ make_connection
149
+ end
150
+
151
+ it "sets the :winrm_transport to :negotiate" do
152
+ klass.expects(:new).with do |hash|
153
+ hash[:winrm_transport] == :negotiate
154
+ end
155
+
156
+ make_connection
157
+ end
158
+
159
+ it "sets the :disable_sspi to false" do
160
+ klass.expects(:new).with do |hash|
161
+ hash[:disable_sspi] == false
162
+ end
163
+
164
+ make_connection
165
+ end
166
+
167
+ it "sets :endpoint from data in config" do
168
+ config[:hostname] = "host_from_config"
169
+ config[:port] = "port_from_config"
170
+ config[:winrm_transport] = "ssl"
171
+
172
+ klass.expects(:new).with do |hash|
173
+ hash[:endpoint] == "https://host_from_config:port_from_config/wsman"
174
+ end
175
+
176
+ make_connection
177
+ end
178
+
179
+ it "sets :endpoint from data in state over config data" do
180
+ state[:hostname] = "host_from_state"
181
+ config[:hostname] = "host_from_config"
182
+ state[:port] = "port_from_state"
183
+ config[:port] = "port_from_config"
184
+ config[:winrm_transport] = "ssl"
185
+
186
+ klass.expects(:new).with do |hash|
187
+ hash[:endpoint] == "https://host_from_state:port_from_state/wsman"
188
+ end
189
+
190
+ make_connection
191
+ end
192
+
193
+ it "sets :user from :username in config" do
194
+ config[:username] = "user_from_config"
195
+
196
+ klass.expects(:new).with do |hash|
197
+ hash[:user] == "user_from_config"
198
+ end
199
+
200
+ make_connection
201
+ end
202
+
203
+ it "sets :user from :username in state over config data" do
204
+ state[:username] = "user_from_state"
205
+ config[:username] = "user_from_config"
206
+
207
+ klass.expects(:new).with do |hash|
208
+ hash[:user] == "user_from_state"
209
+ end
210
+
211
+ make_connection
212
+ end
213
+
214
+ it "sets :pass from :password in config" do
215
+ config[:password] = "pass_from_config"
216
+
217
+ klass.expects(:new).with do |hash|
218
+ hash[:pass] == "pass_from_config"
219
+ end
220
+
221
+ make_connection
222
+ end
223
+
224
+ it "sets :pass from :password in state over config data" do
225
+ state[:password] = "pass_from_state"
226
+ config[:password] = "pass_from_config"
227
+
228
+ klass.expects(:new).with do |hash|
229
+ hash[:pass] == "pass_from_state"
230
+ end
231
+
232
+ make_connection
233
+ end
234
+
235
+ it "sets :rdp_port from config" do
236
+ config[:rdp_port] = "rdp_from_config"
237
+
238
+ klass.expects(:new).with do |hash|
239
+ hash[:rdp_port] == "rdp_from_config"
240
+ end
241
+
242
+ make_connection
243
+ end
244
+
245
+ it "sets :rdp_port from state over config data" do
246
+ state[:rdp_port] = "rdp_from_state"
247
+ config[:rdp_port] = "rdp_from_config"
248
+
249
+ klass.expects(:new).with do |hash|
250
+ hash[:rdp_port] == "rdp_from_state"
251
+ end
252
+
253
+ make_connection
254
+ end
255
+
256
+ it "sets :connection_retries from config" do
257
+ config[:connection_retries] = "retries_from_config"
258
+
259
+ klass.expects(:new).with do |hash|
260
+ hash[:connection_retries] == "retries_from_config"
261
+ end
262
+
263
+ make_connection
264
+ end
265
+
266
+ it "sets :connection_retries from state over config data" do
267
+ state[:connection_retries] = "retries_from_state"
268
+ config[:connection_retries] = "retries_from_config"
269
+
270
+ klass.expects(:new).with do |hash|
271
+ hash[:connection_retries] == "retries_from_state"
272
+ end
273
+
274
+ make_connection
275
+ end
276
+
277
+ it "sets :connection_retry_sleep from config" do
278
+ config[:connection_retry_sleep] = "sleep_from_config"
279
+
280
+ klass.expects(:new).with do |hash|
281
+ hash[:connection_retry_sleep] == "sleep_from_config"
282
+ end
283
+
284
+ make_connection
285
+ end
286
+
287
+ it "sets :connection_retry_sleep from state over config data" do
288
+ state[:connection_retry_sleep] = "sleep_from_state"
289
+ config[:connection_retry_sleep] = "sleep_from_config"
290
+
291
+ klass.expects(:new).with do |hash|
292
+ hash[:connection_retry_sleep] == "sleep_from_state"
293
+ end
294
+
295
+ make_connection
296
+ end
297
+
298
+ it "sets :max_wait_until_ready from config" do
299
+ config[:max_wait_until_ready] = "max_from_config"
300
+
301
+ klass.expects(:new).with do |hash|
302
+ hash[:max_wait_until_ready] == "max_from_config"
303
+ end
304
+
305
+ make_connection
306
+ end
307
+
308
+ it "sets :max_wait_until_ready from state over config data" do
309
+ state[:max_wait_until_ready] = "max_from_state"
310
+ config[:max_wait_until_ready] = "max_from_config"
311
+
312
+ klass.expects(:new).with do |hash|
313
+ hash[:max_wait_until_ready] == "max_from_state"
314
+ end
315
+
316
+ make_connection
317
+ end
318
+
319
+ it "sets :winrm_transport from config data" do
320
+ config[:winrm_transport] = "ssl"
321
+
322
+ klass.expects(:new).with do |hash|
323
+ hash[:winrm_transport] == :ssl
324
+ end
325
+
326
+ make_connection
327
+ end
328
+
329
+ describe "when negotiate is set in config" do
330
+ before do
331
+ config[:winrm_transport] = "negotiate"
332
+ end
333
+
334
+ it "sets :winrm_transport to negotiate" do
335
+
336
+ klass.expects(:new).with do |hash|
337
+ hash[:winrm_transport] == :negotiate &&
338
+ hash[:disable_sspi] == false &&
339
+ hash[:basic_auth_only] == false
340
+ end
341
+
342
+ make_connection
343
+ end
344
+ end
345
+
346
+ it "returns the same connection when called again with same state" do
347
+ first_connection = make_connection(state)
348
+ second_connection = make_connection(state)
349
+
350
+ first_connection.object_id.must_equal second_connection.object_id
351
+ end
352
+
353
+ it "logs a debug message when the connection is reused" do
354
+ make_connection(state)
355
+ make_connection(state)
356
+
357
+ logged_output.string.lines.count { |l|
358
+ l =~ debug_line_with("[WinRM] reusing existing connection ")
359
+ }.must_equal 1
360
+ end
361
+
362
+ it "returns a new connection when called again if state differs" do
363
+ first_connection = make_connection(state)
364
+ second_connection = make_connection(state.merge(:port => 9000))
365
+
366
+ first_connection.object_id.wont_equal second_connection.object_id
367
+ end
368
+
369
+ it "closes first connection when a second is created" do
370
+ first_connection = make_connection(state)
371
+ first_connection.expects(:close)
372
+
373
+ make_connection(state.merge(:port => 9000))
374
+ end
375
+
376
+ it "logs a debug message a second connection is created" do
377
+ make_connection(state)
378
+ make_connection(state.merge(:port => 9000))
379
+
380
+ logged_output.string.lines.count { |l|
381
+ l =~ debug_line_with("[WinRM] shutting previous connection ")
382
+ }.must_equal 1
383
+ end
384
+ end
385
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
386
+
387
+ describe "called without a block" do
388
+
389
+ def make_connection(s = state)
390
+ transport.connection(s)
391
+ end
392
+
393
+ common_connection_specs
394
+ end
395
+
396
+ describe "called with a block" do
397
+
398
+ def make_connection(s = state)
399
+ transport.connection(s) do |conn|
400
+ conn
401
+ end
402
+ end
403
+
404
+ common_connection_specs
405
+ end
406
+ end
407
+
408
+ describe "#load_needed_dependencies" do
409
+ describe "winrm-fs" do
410
+ before do
411
+ # force loading of winrm-fs to get the version constant
412
+ require "winrm-fs"
413
+ end
414
+
415
+ it "logs a message to debug that code will be loaded" do
416
+ transport
417
+
418
+ logged_output.string.must_match debug_line_with(
419
+ "winrm-fs requested, loading winrm-fs gem")
420
+ end
421
+
422
+ it "logs a message to debug when library is initially loaded" do
423
+ transport = Kitchen::Transport::Winrm.new(config)
424
+ transport.stubs(:require).with("winrm", anything)
425
+ transport.stubs(:require).with("winrm-fs").returns(true)
426
+ transport.finalize_config!(instance)
427
+
428
+ logged_output.string.must_match(
429
+ /winrm-fs is loaded/
430
+ )
431
+ end
432
+
433
+ it "logs a message to debug when library is previously loaded" do
434
+ transport = Kitchen::Transport::Winrm.new(config)
435
+ transport.stubs(:require).with("winrm", anything)
436
+ transport.stubs(:require).with("winrm-fs").returns(false)
437
+ transport.finalize_config!(instance)
438
+
439
+ logged_output.string.must_match(
440
+ /winrm-fs was already loaded/
441
+ )
442
+ end
443
+
444
+ it "logs a message to fatal when libraries cannot be loaded" do
445
+ transport = Kitchen::Transport::Winrm.new(config)
446
+ transport.stubs(:require).with("winrm", anything)
447
+ transport.stubs(:require).with("winrm-fs").
448
+ raises(LoadError, "uh oh")
449
+ begin
450
+ transport.finalize_config!(instance)
451
+ rescue # rubocop:disable Lint/HandleExceptions
452
+ # we are interested in the log output, not this exception
453
+ end
454
+
455
+ logged_output.string.must_match fatal_line_with(
456
+ "The `winrm-fs` gem is missing and must be installed")
457
+ end
458
+
459
+ it "raises a UserError when libraries cannot be loaded" do
460
+ transport = Kitchen::Transport::Winrm.new(config)
461
+ transport.stubs(:require).with("winrm", anything)
462
+ transport.stubs(:require).with("winrm-fs").
463
+ raises(LoadError, "uh oh")
464
+
465
+ err = proc {
466
+ transport.finalize_config!(instance)
467
+ }.must_raise Kitchen::UserError
468
+ err.message.must_match(/^Could not load or activate winrm-fs\. /)
469
+ end
470
+ end
471
+
472
+ describe "winrm" do
473
+ it "logs a message to debug that code will be loaded" do
474
+ transport = Kitchen::Transport::Winrm.new(config)
475
+ transport.stubs(:require).with("winrm-fs", anything)
476
+ transport.stubs(:require)
477
+ transport.finalize_config!(instance)
478
+
479
+ logged_output.string.must_match debug_line_with(
480
+ "winrm requested, loading winrm gem")
481
+ end
482
+
483
+ it "logs a message to debug when library is initially loaded" do
484
+ transport = Kitchen::Transport::Winrm.new(config)
485
+ transport.stubs(:require).with("winrm-fs", anything)
486
+ transport.stubs(:require).returns(true)
487
+
488
+ transport.finalize_config!(instance)
489
+
490
+ logged_output.string.must_match(
491
+ /winrm is loaded/
492
+ )
493
+ end
494
+
495
+ it "logs a message to debug when library is previously loaded" do
496
+ transport = Kitchen::Transport::Winrm.new(config)
497
+ transport.stubs(:require).with("winrm-fs", anything)
498
+ transport.stubs(:require).returns(false)
499
+
500
+ transport.finalize_config!(instance)
501
+
502
+ logged_output.string.must_match(
503
+ /winrm was already loaded/
504
+ )
505
+ end
506
+
507
+ it "logs a message to fatal when libraries cannot be loaded" do
508
+ transport = Kitchen::Transport::Winrm.new(config)
509
+ transport.stubs(:require).with("winrm-fs", anything)
510
+ transport.stubs(:require).raises(LoadError, "uh oh")
511
+ begin
512
+ transport.finalize_config!(instance)
513
+ rescue # rubocop:disable Lint/HandleExceptions
514
+ # we are interested in the log output, not this exception
515
+ end
516
+
517
+ logged_output.string.must_match fatal_line_with(
518
+ "The `winrm` gem is missing and must be installed")
519
+ end
520
+
521
+ it "raises a UserError when libraries cannot be loaded" do
522
+ transport = Kitchen::Transport::Winrm.new(config)
523
+ transport.stubs(:require).with("winrm-fs", anything)
524
+ transport.stubs(:require).raises(LoadError, "uh oh")
525
+
526
+ err = proc {
527
+ transport.finalize_config!(instance)
528
+ }.must_raise Kitchen::UserError
529
+ err.message.must_match(/^Could not load or activate winrm\. /)
530
+ end
531
+ end
532
+ end
533
+
534
+ def debug_line_with(msg)
535
+ %r{^D, .* : #{Regexp.escape(msg)}}
536
+ end
537
+
538
+ def fatal_line_with(msg)
539
+ %r{^F, .* : #{Regexp.escape(msg)}}
540
+ end
541
+ end
542
+
543
+ describe Kitchen::Transport::Winrm::Connection do
544
+
545
+ let(:logged_output) { StringIO.new }
546
+ let(:logger) { Logger.new(logged_output) }
547
+
548
+ let(:options) do
549
+ { :logger => logger, :user => "me", :pass => "haha",
550
+ :endpoint => "http://foo:5985/wsman", :winrm_transport => :plaintext,
551
+ :kitchen_root => "/i/am/root", :instance_name => "coolbeans",
552
+ :rdp_port => "rdpyeah" }
553
+ end
554
+
555
+ let(:info) do
556
+ copts = { :user => "me", :pass => "haha" }
557
+ "plaintext::http://foo:5985/wsman<#{copts}>"
558
+ end
559
+
560
+ let(:winrm_session) do
561
+ s = mock("winrm_session")
562
+ s.responds_like_instance_of(::WinRM::WinRMWebService)
563
+ s
564
+ end
565
+
566
+ let(:executor) do
567
+ s = mock("command_executor")
568
+ s.responds_like_instance_of(WinRM::CommandExecutor)
569
+ s
570
+ end
571
+
572
+ let(:connection) do
573
+ Kitchen::Transport::Winrm::Connection.new(options)
574
+ end
575
+
576
+ before do
577
+ WinRM::WinRMWebService.stubs(:new).returns(winrm_session)
578
+ winrm_session.stubs(:logger=)
579
+ logger.level = Logger::DEBUG
580
+ end
581
+
582
+ describe "#close" do
583
+
584
+ let(:response) do
585
+ o = WinRM::Output.new
586
+ o[:exitcode] = 0
587
+ o[:data].concat([{ :stdout => "ok\r\n" }])
588
+ o
589
+ end
590
+
591
+ before do
592
+ winrm_session.stubs(:create_executor).returns(executor)
593
+ executor.stubs(:close)
594
+ executor.stubs(:run_powershell_script).
595
+ with("doit").yields("ok\n", nil).returns(response)
596
+ end
597
+
598
+ it "only closes the shell once for multiple calls" do
599
+ executor.expects(:close).once
600
+
601
+ connection.execute("doit")
602
+ connection.close
603
+ connection.close
604
+ connection.close
605
+ end
606
+ end
607
+
608
+ describe "#execute" do
609
+
610
+ before do
611
+ winrm_session.stubs(:create_executor).returns(executor)
612
+ end
613
+
614
+ describe "for a successful command" do
615
+
616
+ let(:response) do
617
+ o = WinRM::Output.new
618
+ o[:exitcode] = 0
619
+ o[:data].concat([
620
+ { :stdout => "ok\r\n" },
621
+ { :stderr => "congrats\r\n" }
622
+ ])
623
+ o
624
+ end
625
+
626
+ before do
627
+ executor.expects(:run_powershell_script).
628
+ with("doit").yields("ok\n", nil).returns(response)
629
+ end
630
+
631
+ it "logger displays command on debug" do
632
+ connection.execute("doit")
633
+
634
+ logged_output.string.must_match debug_line(
635
+ "[WinRM] #{info} (doit)")
636
+ end
637
+
638
+ it "logger captures stdout" do
639
+ connection.execute("doit")
640
+
641
+ logged_output.string.must_match(/^ok$/)
642
+ end
643
+
644
+ it "logger captures stderr on warn if logger is at debug level" do
645
+ logger.level = Logger::DEBUG
646
+ connection.execute("doit")
647
+
648
+ logged_output.string.must_match warn_line("congrats")
649
+ end
650
+
651
+ it "logger does not log stderr on warn if logger is below debug level" do
652
+ logger.level = Logger::INFO
653
+ connection.execute("doit")
654
+
655
+ logged_output.string.wont_match warn_line("congrats")
656
+ end
657
+ end
658
+
659
+ describe "long command" do
660
+ let(:command) { %{Write-Host "#{"a" * 4000}"} }
661
+
662
+ let(:connection) do
663
+ Kitchen::Transport::WinRMConnectionDummy.new(options)
664
+ end
665
+
666
+ let(:response) do
667
+ o = WinRM::Output.new
668
+ o[:exitcode] = 0
669
+ o[:data].concat([
670
+ { :stdout => "ok\r\n" },
671
+ { :stderr => "congrats\r\n" }
672
+ ])
673
+ o
674
+ end
675
+
676
+ before do
677
+ executor.expects(:run_powershell_script).with(
678
+ %{powershell -ExecutionPolicy Bypass -File "$env:TEMP/coolbeans-long_script.ps1"}
679
+ ).yields("ok\n", nil).returns(response)
680
+ end
681
+
682
+ it "uploads the long command" do
683
+ with_fake_fs do
684
+ connection.execute(command)
685
+
686
+ connection.saved_command.must_equal command
687
+ end
688
+ end
689
+ end
690
+
691
+ describe "for a failed command" do
692
+
693
+ let(:response) do
694
+ o = WinRM::Output.new
695
+ o[:exitcode] = 1
696
+ o[:data].concat([
697
+ { :stderr => "#< CLIXML\r\n" },
698
+ { :stderr => "<Objs Version=\"1.1.0.1\" xmlns=\"http://schemas." },
699
+ { :stderr => "microsoft.com/powershell/2004/04\"><S S=\"Error\">" },
700
+ { :stderr => "doit : The term 'doit' is not recognized as the " },
701
+ { :stderr => "name of a cmdlet, function, _x000D__x000A_</S>" },
702
+ { :stderr => "<S S=\"Error\">script file, or operable program. " },
703
+ { :stderr => "Check the spelling of" },
704
+ { :stderr => "the name, or if a path _x000D__x000A_</S><S S=\"E" },
705
+ { :stderr => "rror\">was included, verify that the path is corr" },
706
+ { :stderr => "ect and try again._x000D__x000A_</S><S S=\"Error" },
707
+ { :stderr => "\">At line:1 char:1_x000D__x000A_</S><S S=\"Error" },
708
+ { :stderr => "\">+ doit_x000D__x000A_</S><S S=\"Error\">+ ~~~~_" },
709
+ { :stderr => "x000D__x000A_</S><S S=\"Error\"> + CategoryInf" },
710
+ { :stderr => "o : ObjectNotFound: (doit:String) [], Co" },
711
+ { :stderr => "mmandNotFoun _x000D__x000A_</S><S S=\"Error\"> " },
712
+ { :stderr => "dException_x000D__x000A_</S><S S=\"Error\"> + " },
713
+ { :stderr => "FullyQualifiedErrorId : CommandNotFoundException_" },
714
+ { :stderr => "x000D__x000A_</S><S S=\"Error\"> _x000D__x000A_</" },
715
+ { :stderr => "S></Objs>" }
716
+ ])
717
+ o
718
+ end
719
+
720
+ before do
721
+ executor.expects(:run_powershell_script).
722
+ with("doit").yields("nope\n", nil).returns(response)
723
+ end
724
+
725
+ # rubocop:disable Metrics/MethodLength
726
+ def self.common_failed_command_specs
727
+ it "logger displays command on debug" do
728
+ begin
729
+ connection.execute("doit")
730
+ rescue # rubocop:disable Lint/HandleExceptions
731
+ # the raise is not what is being tested here, rather its side-effect
732
+ end
733
+
734
+ logged_output.string.must_match debug_line(
735
+ "[WinRM] #{info} (doit)"
736
+ )
737
+ end
738
+
739
+ it "logger captures stdout" do
740
+ begin
741
+ connection.execute("doit")
742
+ rescue # rubocop:disable Lint/HandleExceptions
743
+ # the raise is not what is being tested here, rather its side-effect
744
+ end
745
+
746
+ logged_output.string.must_match(/^nope$/)
747
+ end
748
+
749
+ it "stderr is printed on logger warn level" do
750
+ begin
751
+ connection.execute("doit")
752
+ rescue # rubocop:disable Lint/HandleExceptions
753
+ # the raise is not what is being tested here, rather its side-effect
754
+ end
755
+
756
+ message = <<'MSG'.chomp!
757
+ doit : The term 'doit' is not recognized as the name of a cmdlet, function,
758
+ script file, or operable program. Check the spelling ofthe name, or if a path
759
+ was included, verify that the path is correct and try again.
760
+ At line:1 char:1
761
+ + doit
762
+ + ~~~~
763
+ + CategoryInfo : ObjectNotFound: (doit:String) [], CommandNotFoun
764
+ dException
765
+ + FullyQualifiedErrorId : CommandNotFoundException
766
+ MSG
767
+
768
+ message.lines.each do |line|
769
+ logged_output.string.must_match warn_line(line.chomp)
770
+ end
771
+ end
772
+ end
773
+ # rubocop:enable Metrics/MethodLength
774
+
775
+ describe "when a non-zero exit code is returned" do
776
+
777
+ common_failed_command_specs
778
+
779
+ it "raises a WinrmFailed exception" do
780
+ err = proc {
781
+ connection.execute("doit")
782
+ }.must_raise Kitchen::Transport::WinrmFailed
783
+ err.message.must_equal "WinRM exited (1) for command: [doit]"
784
+ end
785
+ end
786
+ end
787
+
788
+ describe "for a nil command" do
789
+
790
+ it "does not log on debug" do
791
+ executor.expects(:open).never
792
+ connection.execute(nil)
793
+
794
+ logged_output.string.must_equal ""
795
+ end
796
+ end
797
+
798
+ [
799
+ Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED,
800
+ Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH,
801
+ ::WinRM::WinRMHTTPTransportError, ::WinRM::WinRMAuthorizationError,
802
+ HTTPClient::KeepAliveDisconnected, HTTPClient::ConnectTimeoutError
803
+ ].each do |klass|
804
+ describe "raising #{klass}" do
805
+
806
+ before do
807
+ k = if klass == ::WinRM::WinRMHTTPTransportError
808
+ # this exception takes 2 args in its constructor, which is not stock
809
+ klass.new("dang", 200)
810
+ else
811
+ klass
812
+ end
813
+
814
+ options[:connection_retries] = 3
815
+ options[:connection_retry_sleep] = 7
816
+ winrm_session.stubs(:create_executor).raises(k)
817
+ end
818
+
819
+ it "reraises the #{klass} exception" do
820
+ proc { connection.execute("nope") }.must_raise klass
821
+ end
822
+ end
823
+ end
824
+ end
825
+
826
+ describe "#login_command" do
827
+
828
+ let(:login_command) { connection.login_command }
829
+ let(:args) { login_command.arguments.join(" ") }
830
+ let(:exec_args) { login_command.exec_args }
831
+
832
+ let(:rdp_doc) do
833
+ File.join(File.join(options[:kitchen_root], ".kitchen", "coolbeans.rdp"))
834
+ end
835
+
836
+ describe "for Mac-based workstations" do
837
+
838
+ before do
839
+ RbConfig::CONFIG.stubs(:[]).with("host_os").returns("darwin14")
840
+ end
841
+
842
+ it "returns a LoginCommand" do
843
+ with_fake_fs do
844
+ FileUtils.mkdir_p(File.dirname(rdp_doc))
845
+ login_command.must_be_instance_of Kitchen::LoginCommand
846
+ end
847
+ end
848
+
849
+ it "creates an rdp document" do
850
+ actual = nil
851
+ with_fake_fs do
852
+ FileUtils.mkdir_p(File.dirname(rdp_doc))
853
+ login_command
854
+ actual = IO.read(rdp_doc)
855
+ end
856
+
857
+ actual.must_equal Kitchen::Util.outdent!(<<-RDP)
858
+ drivestoredirect:s:*
859
+ full address:s:foo:rdpyeah
860
+ prompt for credentials:i:1
861
+ username:s:me
862
+ RDP
863
+ end
864
+
865
+ it "prints the rdp document on debug" do
866
+ with_fake_fs do
867
+ FileUtils.mkdir_p(File.dirname(rdp_doc))
868
+ login_command
869
+ end
870
+
871
+ expected = Kitchen::Util.outdent!(<<-OUTPUT)
872
+ Creating RDP document for coolbeans (/i/am/root/.kitchen/coolbeans.rdp)
873
+ ------------
874
+ drivestoredirect:s:*
875
+ full address:s:foo:rdpyeah
876
+ prompt for credentials:i:1
877
+ username:s:me
878
+ ------------
879
+ OUTPUT
880
+ debug_output(logged_output.string).must_match expected
881
+ end
882
+
883
+ it "returns a LoginCommand which calls open on the rdp document" do
884
+ actual = nil
885
+ with_fake_fs do
886
+ FileUtils.mkdir_p(File.dirname(rdp_doc))
887
+ actual = login_command
888
+ end
889
+
890
+ actual.exec_args.must_equal ["open", rdp_doc, {}]
891
+ end
892
+ end
893
+
894
+ describe "for Windows-based workstations" do
895
+
896
+ before do
897
+ RbConfig::CONFIG.stubs(:[]).with("host_os").returns("mingw32")
898
+ end
899
+
900
+ it "returns a LoginCommand" do
901
+ with_fake_fs do
902
+ FileUtils.mkdir_p(File.dirname(rdp_doc))
903
+ login_command.must_be_instance_of Kitchen::LoginCommand
904
+ end
905
+ end
906
+
907
+ it "creates an rdp document" do
908
+ actual = nil
909
+ with_fake_fs do
910
+ FileUtils.mkdir_p(File.dirname(rdp_doc))
911
+ login_command
912
+ actual = IO.read(rdp_doc)
913
+ end
914
+
915
+ actual.must_equal Kitchen::Util.outdent!(<<-RDP)
916
+ full address:s:foo:rdpyeah
917
+ prompt for credentials:i:1
918
+ username:s:me
919
+ RDP
920
+ end
921
+
922
+ it "prints the rdp document on debug" do
923
+ with_fake_fs do
924
+ FileUtils.mkdir_p(File.dirname(rdp_doc))
925
+ login_command
926
+ end
927
+
928
+ expected = Kitchen::Util.outdent!(<<-OUTPUT)
929
+ Creating RDP document for coolbeans (/i/am/root/.kitchen/coolbeans.rdp)
930
+ ------------
931
+ full address:s:foo:rdpyeah
932
+ prompt for credentials:i:1
933
+ username:s:me
934
+ ------------
935
+ OUTPUT
936
+ debug_output(logged_output.string).must_match expected
937
+ end
938
+
939
+ it "returns a LoginCommand which calls mstsc on the rdp document" do
940
+ actual = nil
941
+ with_fake_fs do
942
+ FileUtils.mkdir_p(File.dirname(rdp_doc))
943
+ actual = login_command
944
+ end
945
+
946
+ actual.exec_args.must_equal ["mstsc", rdp_doc, {}]
947
+ end
948
+ end
949
+
950
+ describe "for Linux-based workstations" do
951
+
952
+ before do
953
+ RbConfig::CONFIG.stubs(:[]).with("host_os").returns("linux-gnu")
954
+ end
955
+
956
+ it "returns a LoginCommand" do
957
+ login_command.must_be_instance_of Kitchen::LoginCommand
958
+ end
959
+
960
+ it "is an rdesktop command" do
961
+ login_command.command.must_equal "rdesktop"
962
+ args.must_match %r{ foo:rdpyeah$}
963
+ end
964
+
965
+ it "sets the user" do
966
+ args.must_match regexify("-u me ")
967
+ end
968
+
969
+ it "sets the pass if given" do
970
+ args.must_match regexify(" -p haha ")
971
+ end
972
+
973
+ it "won't set the pass if not given" do
974
+ options.delete(:pass)
975
+
976
+ args.wont_match regexify(" -p haha ")
977
+ end
978
+ end
979
+
980
+ describe "for unknown workstation platforms" do
981
+
982
+ before do
983
+ RbConfig::CONFIG.stubs(:[]).with("host_os").returns("cray")
984
+ end
985
+
986
+ it "raises an ActionFailed error" do
987
+ err = proc { login_command }.must_raise Kitchen::ActionFailed
988
+ err.message.must_equal "Remote login not supported in " \
989
+ "Kitchen::Transport::Winrm::Connection from host OS 'cray'."
990
+ end
991
+ end
992
+ end
993
+
994
+ describe "#upload" do
995
+
996
+ let(:transporter) do
997
+ t = mock("file_transporter")
998
+ t.responds_like_instance_of(WinRM::FS::Core::FileTransporter)
999
+ t
1000
+ end
1001
+
1002
+ before do
1003
+ winrm_session.stubs(:create_executor).returns(executor)
1004
+
1005
+ WinRM::FS::Core::FileTransporter.stubs(:new).
1006
+ with(executor).returns(transporter)
1007
+ transporter.stubs(:upload)
1008
+ end
1009
+
1010
+ def self.common_specs_for_upload
1011
+ it "builds a Winrm::FileTransporter" do
1012
+ WinRM::FS::Core::FileTransporter.unstub(:new)
1013
+
1014
+ WinRM::FS::Core::FileTransporter.expects(:new).
1015
+ with(executor).returns(transporter)
1016
+
1017
+ upload
1018
+ end
1019
+
1020
+ it "reuses the Winrm::FileTransporter" do
1021
+ WinRM::FS::Core::FileTransporter.unstub(:new)
1022
+
1023
+ WinRM::FS::Core::FileTransporter.expects(:new).
1024
+ with(executor).returns(transporter).once
1025
+
1026
+ upload
1027
+ upload
1028
+ upload
1029
+ end
1030
+ end
1031
+
1032
+ describe "for a file" do
1033
+
1034
+ def upload # execute every time, not lazily once
1035
+ connection.upload("/tmp/file.txt", "C:\\dest")
1036
+ end
1037
+
1038
+ common_specs_for_upload
1039
+ end
1040
+
1041
+ describe "for a collection of files" do
1042
+
1043
+ def upload # execute every time, not lazily once
1044
+ connection.upload(%W[/tmp/file1.txt /tmp/file2.txt], "C:\\dest")
1045
+ end
1046
+
1047
+ common_specs_for_upload
1048
+ end
1049
+ end
1050
+
1051
+ describe "#wait_until_ready" do
1052
+
1053
+ before do
1054
+ winrm_session.stubs(:create_executor).returns(executor)
1055
+ options[:max_wait_until_ready] = 300
1056
+ end
1057
+
1058
+ describe "when connection is successful" do
1059
+
1060
+ let(:response) do
1061
+ o = WinRM::Output.new
1062
+ o[:exitcode] = 0
1063
+ o[:data].concat([{ :stdout => "[WinRM] Established\r\n" }])
1064
+ o
1065
+ end
1066
+
1067
+ before do
1068
+ executor.expects(:run_powershell_script).
1069
+ with("Write-Host '[WinRM] Established\n'").returns(response)
1070
+ end
1071
+
1072
+ it "executes an empty command string to ensure working" do
1073
+ connection.wait_until_ready
1074
+ end
1075
+ end
1076
+
1077
+ describe "when connection suceeds but command fails, sad panda" do
1078
+
1079
+ let(:response) do
1080
+ o = WinRM::Output.new
1081
+ o[:exitcode] = 42
1082
+ o[:data].concat([{ :stderr => "Ah crap.\r\n" }])
1083
+ o
1084
+ end
1085
+
1086
+ before do
1087
+ executor.expects(:run_powershell_script).
1088
+ with("Write-Host '[WinRM] Established\n'").returns(response)
1089
+ end
1090
+
1091
+ it "executes an empty command string to ensure working" do
1092
+ err = proc {
1093
+ connection.wait_until_ready
1094
+ }.must_raise Kitchen::Transport::WinrmFailed
1095
+ err.message.must_equal "WinRM exited (42) for command: " \
1096
+ "[Write-Host '[WinRM] Established\n']"
1097
+ end
1098
+
1099
+ it "stderr is printed on logger warn level" do
1100
+ begin
1101
+ connection.wait_until_ready
1102
+ rescue # rubocop:disable Lint/HandleExceptions
1103
+ # the raise is not what is being tested here, rather its side-effect
1104
+ end
1105
+
1106
+ logged_output.string.must_match warn_line("Ah crap.\n")
1107
+ end
1108
+ end
1109
+ end
1110
+
1111
+ def debug_output(output)
1112
+ regexp = %r{^D, .* DEBUG -- : }
1113
+ output.lines.grep(%r{^D, .* DEBUG -- : }).map { |l| l.sub(regexp, "") }.join
1114
+ end
1115
+
1116
+ def debug_line(msg)
1117
+ %r{^D, .* : #{Regexp.escape(msg)}$}
1118
+ end
1119
+
1120
+ def debug_line_with(msg)
1121
+ %r{^D, .* : #{Regexp.escape(msg)}}
1122
+ end
1123
+
1124
+ def info_line(msg)
1125
+ %r{^I, .* : #{Regexp.escape(msg)}$}
1126
+ end
1127
+
1128
+ def info_line_with(msg)
1129
+ %r{^I, .* : #{Regexp.escape(msg)}}
1130
+ end
1131
+
1132
+ def regexify(string)
1133
+ Regexp.new(Regexp.escape(string))
1134
+ end
1135
+
1136
+ def warn_line(msg)
1137
+ %r{^W, .* : #{Regexp.escape(msg)}$}
1138
+ end
1139
+
1140
+ def warn_line_with(msg)
1141
+ %r{^W, .* : #{Regexp.escape(msg)}}
1142
+ end
1143
+ end