test-kitchen 1.7.0 → 1.7.1.dev

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.
Files changed (181) hide show
  1. checksums.yaml +4 -4
  2. data/.cane +8 -8
  3. data/.gitattributes +3 -0
  4. data/.github/ISSUE_TEMPLATE.md +55 -55
  5. data/.gitignore +28 -28
  6. data/.kitchen.ci.yml +23 -23
  7. data/.kitchen.proxy.yml +27 -27
  8. data/.rubocop.yml +3 -3
  9. data/.travis.yml +70 -70
  10. data/.yardopts +3 -3
  11. data/Berksfile +3 -3
  12. data/CHANGELOG.md +1090 -1083
  13. data/CONTRIBUTING.md +14 -14
  14. data/Gemfile +19 -19
  15. data/Gemfile.proxy_tests +4 -4
  16. data/Guardfile +42 -42
  17. data/LICENSE +15 -15
  18. data/MAINTAINERS.md +23 -23
  19. data/README.md +135 -135
  20. data/Rakefile +61 -61
  21. data/appveyor.yml +44 -44
  22. data/features/kitchen_action_commands.feature +164 -164
  23. data/features/kitchen_command.feature +16 -16
  24. data/features/kitchen_console_command.feature +34 -34
  25. data/features/kitchen_defaults.feature +38 -38
  26. data/features/kitchen_diagnose_command.feature +96 -96
  27. data/features/kitchen_driver_create_command.feature +64 -64
  28. data/features/kitchen_driver_discover_command.feature +25 -25
  29. data/features/kitchen_help_command.feature +16 -16
  30. data/features/kitchen_init_command.feature +274 -274
  31. data/features/kitchen_list_command.feature +104 -104
  32. data/features/kitchen_login_command.feature +62 -62
  33. data/features/kitchen_sink_command.feature +30 -30
  34. data/features/kitchen_test_command.feature +88 -88
  35. data/features/step_definitions/gem_steps.rb +36 -36
  36. data/features/step_definitions/git_steps.rb +5 -5
  37. data/features/step_definitions/output_steps.rb +5 -5
  38. data/features/support/env.rb +75 -75
  39. data/lib/kitchen.rb +150 -150
  40. data/lib/kitchen/base64_stream.rb +55 -55
  41. data/lib/kitchen/cli.rb +419 -419
  42. data/lib/kitchen/collection.rb +55 -55
  43. data/lib/kitchen/color.rb +65 -65
  44. data/lib/kitchen/command.rb +185 -185
  45. data/lib/kitchen/command/action.rb +45 -45
  46. data/lib/kitchen/command/console.rb +58 -58
  47. data/lib/kitchen/command/diagnose.rb +92 -92
  48. data/lib/kitchen/command/driver_discover.rb +105 -105
  49. data/lib/kitchen/command/exec.rb +41 -41
  50. data/lib/kitchen/command/list.rb +119 -119
  51. data/lib/kitchen/command/login.rb +43 -43
  52. data/lib/kitchen/command/sink.rb +54 -54
  53. data/lib/kitchen/command/test.rb +51 -51
  54. data/lib/kitchen/config.rb +322 -322
  55. data/lib/kitchen/configurable.rb +529 -529
  56. data/lib/kitchen/data_munger.rb +959 -959
  57. data/lib/kitchen/diagnostic.rb +141 -141
  58. data/lib/kitchen/driver.rb +56 -56
  59. data/lib/kitchen/driver/base.rb +134 -134
  60. data/lib/kitchen/driver/dummy.rb +108 -108
  61. data/lib/kitchen/driver/proxy.rb +72 -72
  62. data/lib/kitchen/driver/ssh_base.rb +357 -357
  63. data/lib/kitchen/errors.rb +229 -229
  64. data/lib/kitchen/generator/driver_create.rb +177 -177
  65. data/lib/kitchen/generator/init.rb +296 -296
  66. data/lib/kitchen/instance.rb +662 -662
  67. data/lib/kitchen/lazy_hash.rb +142 -142
  68. data/lib/kitchen/loader/yaml.rb +349 -349
  69. data/lib/kitchen/logger.rb +423 -423
  70. data/lib/kitchen/logging.rb +56 -56
  71. data/lib/kitchen/login_command.rb +52 -52
  72. data/lib/kitchen/metadata_chopper.rb +52 -52
  73. data/lib/kitchen/platform.rb +67 -67
  74. data/lib/kitchen/provisioner.rb +54 -54
  75. data/lib/kitchen/provisioner/base.rb +236 -236
  76. data/lib/kitchen/provisioner/chef/berkshelf.rb +114 -114
  77. data/lib/kitchen/provisioner/chef/common_sandbox.rb +322 -322
  78. data/lib/kitchen/provisioner/chef/librarian.rb +112 -112
  79. data/lib/kitchen/provisioner/chef_apply.rb +124 -124
  80. data/lib/kitchen/provisioner/chef_base.rb +341 -341
  81. data/lib/kitchen/provisioner/chef_solo.rb +88 -88
  82. data/lib/kitchen/provisioner/chef_zero.rb +245 -245
  83. data/lib/kitchen/provisioner/dummy.rb +79 -79
  84. data/lib/kitchen/provisioner/shell.rb +138 -138
  85. data/lib/kitchen/rake_tasks.rb +63 -63
  86. data/lib/kitchen/shell_out.rb +93 -93
  87. data/lib/kitchen/ssh.rb +276 -276
  88. data/lib/kitchen/state_file.rb +120 -120
  89. data/lib/kitchen/suite.rb +51 -51
  90. data/lib/kitchen/thor_tasks.rb +66 -66
  91. data/lib/kitchen/transport.rb +54 -54
  92. data/lib/kitchen/transport/base.rb +176 -176
  93. data/lib/kitchen/transport/dummy.rb +79 -79
  94. data/lib/kitchen/transport/ssh.rb +364 -364
  95. data/lib/kitchen/transport/winrm.rb +486 -486
  96. data/lib/kitchen/util.rb +147 -147
  97. data/lib/kitchen/verifier.rb +55 -55
  98. data/lib/kitchen/verifier/base.rb +235 -235
  99. data/lib/kitchen/verifier/busser.rb +277 -277
  100. data/lib/kitchen/verifier/dummy.rb +79 -79
  101. data/lib/kitchen/verifier/shell.rb +101 -101
  102. data/lib/kitchen/version.rb +21 -21
  103. data/lib/vendor/hash_recursive_merge.rb +82 -82
  104. data/spec/kitchen/base64_stream_spec.rb +77 -77
  105. data/spec/kitchen/cli_spec.rb +56 -56
  106. data/spec/kitchen/collection_spec.rb +80 -80
  107. data/spec/kitchen/color_spec.rb +54 -54
  108. data/spec/kitchen/config_spec.rb +408 -408
  109. data/spec/kitchen/configurable_spec.rb +1095 -1095
  110. data/spec/kitchen/data_munger_spec.rb +2694 -2694
  111. data/spec/kitchen/diagnostic_spec.rb +129 -129
  112. data/spec/kitchen/driver/base_spec.rb +121 -121
  113. data/spec/kitchen/driver/dummy_spec.rb +199 -199
  114. data/spec/kitchen/driver/proxy_spec.rb +138 -138
  115. data/spec/kitchen/driver/ssh_base_spec.rb +1115 -1115
  116. data/spec/kitchen/driver_spec.rb +112 -112
  117. data/spec/kitchen/errors_spec.rb +309 -309
  118. data/spec/kitchen/instance_spec.rb +1419 -1419
  119. data/spec/kitchen/lazy_hash_spec.rb +117 -117
  120. data/spec/kitchen/loader/yaml_spec.rb +774 -774
  121. data/spec/kitchen/logger_spec.rb +429 -429
  122. data/spec/kitchen/logging_spec.rb +59 -59
  123. data/spec/kitchen/login_command_spec.rb +68 -68
  124. data/spec/kitchen/metadata_chopper_spec.rb +82 -82
  125. data/spec/kitchen/platform_spec.rb +89 -89
  126. data/spec/kitchen/provisioner/base_spec.rb +386 -386
  127. data/spec/kitchen/provisioner/chef_apply_spec.rb +136 -136
  128. data/spec/kitchen/provisioner/chef_base_spec.rb +1161 -1161
  129. data/spec/kitchen/provisioner/chef_solo_spec.rb +557 -557
  130. data/spec/kitchen/provisioner/chef_zero_spec.rb +1001 -1001
  131. data/spec/kitchen/provisioner/dummy_spec.rb +99 -99
  132. data/spec/kitchen/provisioner/shell_spec.rb +566 -566
  133. data/spec/kitchen/provisioner_spec.rb +107 -107
  134. data/spec/kitchen/shell_out_spec.rb +150 -150
  135. data/spec/kitchen/ssh_spec.rb +693 -693
  136. data/spec/kitchen/state_file_spec.rb +129 -129
  137. data/spec/kitchen/suite_spec.rb +62 -62
  138. data/spec/kitchen/transport/base_spec.rb +89 -89
  139. data/spec/kitchen/transport/ssh_spec.rb +1255 -1255
  140. data/spec/kitchen/transport/winrm_spec.rb +1143 -1143
  141. data/spec/kitchen/transport_spec.rb +112 -112
  142. data/spec/kitchen/util_spec.rb +165 -165
  143. data/spec/kitchen/verifier/base_spec.rb +362 -362
  144. data/spec/kitchen/verifier/busser_spec.rb +610 -610
  145. data/spec/kitchen/verifier/dummy_spec.rb +99 -99
  146. data/spec/kitchen/verifier/shell_spec.rb +160 -160
  147. data/spec/kitchen/verifier_spec.rb +120 -120
  148. data/spec/kitchen_spec.rb +114 -114
  149. data/spec/spec_helper.rb +85 -85
  150. data/spec/support/powershell_max_size_spec.rb +40 -40
  151. data/support/busser_install_command.ps1 +14 -14
  152. data/support/busser_install_command.sh +14 -14
  153. data/support/chef-client-zero.rb +77 -77
  154. data/support/chef_base_init_command.ps1 +18 -18
  155. data/support/chef_base_init_command.sh +2 -2
  156. data/support/chef_base_install_command.ps1 +85 -85
  157. data/support/chef_base_install_command.sh +229 -229
  158. data/support/chef_zero_prepare_command_legacy.ps1 +9 -9
  159. data/support/chef_zero_prepare_command_legacy.sh +10 -10
  160. data/support/download_helpers.sh +109 -109
  161. data/support/dummy-validation.pem +27 -27
  162. data/templates/driver/CHANGELOG.md.erb +3 -3
  163. data/templates/driver/Gemfile.erb +3 -3
  164. data/templates/driver/README.md.erb +64 -64
  165. data/templates/driver/Rakefile.erb +21 -21
  166. data/templates/driver/driver.rb.erb +23 -23
  167. data/templates/driver/gemspec.erb +29 -29
  168. data/templates/driver/gitignore.erb +17 -17
  169. data/templates/driver/license_apachev2.erb +15 -15
  170. data/templates/driver/license_lgplv3.erb +16 -16
  171. data/templates/driver/license_mit.erb +22 -22
  172. data/templates/driver/license_reserved.erb +5 -5
  173. data/templates/driver/tailor.erb +4 -4
  174. data/templates/driver/travis.yml.erb +11 -11
  175. data/templates/driver/version.rb.erb +12 -12
  176. data/templates/init/chefignore.erb +1 -1
  177. data/templates/init/kitchen.yml.erb +18 -18
  178. data/test-kitchen.gemspec +62 -62
  179. data/test/integration/default/default_spec.rb +3 -3
  180. data/testing_windows.md +37 -37
  181. metadata +5 -4
@@ -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/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