winrm-fs 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ # encoding: UTF-8
2
+ require_relative '../../lib/winrm-fs/core/tmp_zip'
3
+
4
+ describe WinRM::FS::Core::TmpZip do
5
+ let(:winrm_fs_dir) { File.expand_path('../../lib/winrm-fs', File.dirname(__FILE__)) }
6
+
7
+ subject { WinRM::FS::Core::TmpZip.new(winrm_fs_dir) }
8
+
9
+ context 'temp file creation' do
10
+ it 'should create a temp file on disk' do
11
+ path = subject.path
12
+ expect(File.exist?(path)).to be true
13
+ subject.unlink
14
+ expect(File.exist?(path)).to be false
15
+ end
16
+ end
17
+
18
+ context 'create zip' do
19
+ it 'should add all files in directory to the zip recursively' do
20
+ expect(subject).to contain_zip_entries([
21
+ 'exceptions.rb',
22
+ 'core/tmp_zip.rb',
23
+ 'scripts/checksum.ps1.erb'])
24
+ end
25
+ end
26
+ end
data/spec/spec_helper.rb CHANGED
@@ -7,12 +7,36 @@ require_relative 'matchers'
7
7
 
8
8
  # Creates a WinRM connection for integration tests
9
9
  module ConnectionHelper
10
+ # rubocop:disable AbcSize
10
11
  def winrm_connection
11
- config = symbolize_keys(YAML.load(File.read(winrm_config_path)))
12
- config[:options].merge!(basic_auth_only: true) unless config[:auth_type].eql? :kerberos
13
- winrm = WinRM::WinRMWebService.new(
12
+ WinRM::WinRMWebService.new(
14
13
  config[:endpoint], config[:auth_type].to_sym, config[:options])
15
- winrm
14
+ end
15
+ # rubocop:enable AbcSize
16
+
17
+ def config
18
+ @config ||= begin
19
+ cfg = symbolize_keys(YAML.load(File.read(winrm_config_path)))
20
+ cfg[:options].merge!(basic_auth_only: true) unless cfg[:auth_type].eql? :kerberos
21
+ merge_environment!(cfg)
22
+ cfg
23
+ end
24
+ end
25
+
26
+ def merge_environment!(config)
27
+ merge_config_option_from_environment(config, 'user')
28
+ merge_config_option_from_environment(config, 'pass')
29
+ merge_config_option_from_environment(config, 'no_ssl_peer_verification')
30
+ if ENV['use_ssl_peer_fingerprint']
31
+ config[:options][:ssl_peer_fingerprint] = ENV['winrm_cert']
32
+ end
33
+ config[:endpoint] = ENV['winrm_endpoint'] if ENV['winrm_endpoint']
34
+ config[:auth_type] = ENV['winrm_auth_type'] if ENV['winrm_auth_type']
35
+ end
36
+
37
+ def merge_config_option_from_environment(config, key)
38
+ env_key = 'winrm_' + key
39
+ config[:options][key.to_sym] = ENV[env_key] if ENV[env_key]
16
40
  end
17
41
 
18
42
  def winrm_config_path
@@ -0,0 +1,819 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Fletcher (<fnichol@nichol.ca>)
4
+ #
5
+ # Copyright (C) 2015, Fletcher Nichol
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 'base64'
20
+ require 'csv'
21
+ require 'stringio'
22
+ require 'logger'
23
+ require 'winrm'
24
+
25
+ require 'winrm-fs/core/file_transporter'
26
+
27
+ describe WinRM::FS::Core::FileTransporter do
28
+ CheckEntry = Struct.new(
29
+ :chk_exists, :src_md5, :dst_md5, :chk_dirty, :verifies)
30
+ DecodeEntry = Struct.new(
31
+ :dst, :verifies, :src_md5, :dst_md5, :tmpfile, :tmpzip)
32
+
33
+ let(:logged_output) { StringIO.new }
34
+ let(:logger) { Logger.new(logged_output) }
35
+
36
+ let(:randomness) { %w(alpha beta charlie delta).each }
37
+ let(:id_generator) { -> { randomness.next } }
38
+ let(:winrm_service) { double('winrm_service', logger: logger) }
39
+ let(:service) { double('command_executor', service: winrm_service) }
40
+ let(:transporter) do
41
+ WinRM::FS::Core::FileTransporter.new(
42
+ service,
43
+ id_generator: id_generator
44
+ )
45
+ end
46
+
47
+ before { @tempfiles = [] }
48
+
49
+ after { @tempfiles.each(&:unlink) }
50
+
51
+ describe 'when uploading a single file' do
52
+ let(:content) { '.' * 12_003 }
53
+ let(:local) { create_tempfile('input.txt', content) }
54
+ let(:remote) { 'C:\\dest' }
55
+ let(:dst) { "#{remote}/#{File.basename(local)}" }
56
+ let(:src_md5) { md5sum(local) }
57
+ let(:size) { File.size(local) }
58
+ let(:cmd_tmpfile) { "%TEMP%\\b64-#{src_md5}.txt" }
59
+ let(:ps_tmpfile) { "$env:TEMP\\b64-#{src_md5}.txt" }
60
+
61
+ let(:upload) { transporter.upload(local, remote) }
62
+
63
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
64
+ def self.common_specs_for_all_single_file_types
65
+ it 'truncates a zero-byte hash_file for check_files' do
66
+ expect(service).to receive(:run_cmd).with(
67
+ regexify(%(echo|set /p=>"%TEMP%\\hash-alpha.txt")))
68
+ .and_return(cmd_output)
69
+
70
+ upload
71
+ end
72
+
73
+ it 'uploads the hash_file in chunks for check_files' do
74
+ hash = outdent!(<<-HASH.chomp)
75
+ @{
76
+ "#{dst}" = "#{src_md5}"
77
+ }
78
+ HASH
79
+
80
+ expect(service).to receive(:run_cmd)
81
+ .with(%(echo #{base64(hash)} >> "%TEMP%\\hash-alpha.txt"))
82
+ .and_return(cmd_output).once
83
+
84
+ upload
85
+ end
86
+
87
+ it 'sets hash_file and runs the check_files powershell script' do
88
+ expect(service).to receive(:run_powershell_script).with(
89
+ regexify(%($hash_file = "$env:TEMP\\hash-alpha.txt")) &&
90
+ regexify(
91
+ 'Check-Files (Invoke-Input $hash_file) | ' \
92
+ 'ConvertTo-Csv -NoTypeInformation')
93
+ ).and_return(check_output)
94
+
95
+ upload
96
+ end
97
+ end
98
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
99
+
100
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
101
+ def self.common_specs_for_all_single_dirty_file_types
102
+ it 'truncates a zero-byte tempfile' do
103
+ expect(service).to receive(:run_cmd).with(
104
+ regexify(%(echo|set /p=>"#{cmd_tmpfile}"))
105
+ ).and_return(cmd_output)
106
+
107
+ upload
108
+ end
109
+
110
+ it 'ploads the file in 8k chunks' do
111
+ expect(service).to receive(:run_cmd)
112
+ .with(%(echo #{base64('.' * 6000)} >> "#{cmd_tmpfile}"))
113
+ .and_return(cmd_output).twice
114
+ expect(service).to receive(:run_cmd)
115
+ .with(%(echo #{base64('.' * 3)} >> "#{cmd_tmpfile}"))
116
+ .and_return(cmd_output).once
117
+
118
+ upload
119
+ end
120
+
121
+ describe 'with a small file' do
122
+ let(:content) { 'hello, world' }
123
+
124
+ it 'uploads the file in base64 encoding' do
125
+ expect(service).to receive(:run_cmd)
126
+ .with(%(echo #{base64(content)} >> "#{cmd_tmpfile}"))
127
+ .and_return(cmd_output)
128
+
129
+ upload
130
+ end
131
+ end
132
+
133
+ it 'truncates a zero-byte hash_file for decode_files' do
134
+ expect(service).to receive(:run_cmd).with(
135
+ regexify(%(echo|set /p=>"%TEMP%\\hash-beta.txt"))
136
+ ).and_return(cmd_output)
137
+
138
+ upload
139
+ end
140
+
141
+ it 'uploads the hash_file in chunks for decode_files' do
142
+ hash = outdent!(<<-HASH.chomp)
143
+ @{
144
+ "#{ps_tmpfile}" = @{
145
+ "dst" = "#{dst}"
146
+ }
147
+ }
148
+ HASH
149
+
150
+ expect(service).to receive(:run_cmd)
151
+ .with(%(echo #{base64(hash)} >> "%TEMP%\\hash-beta.txt"))
152
+ .and_return(cmd_output).once
153
+
154
+ upload
155
+ end
156
+
157
+ it 'sets hash_file and runs the decode_files powershell script' do
158
+ expect(service).to receive(:run_powershell_script).with(
159
+ regexify(%($hash_file = "$env:TEMP\\hash-beta.txt")) &&
160
+ regexify(
161
+ 'Decode-Files (Invoke-Input $hash_file) | ' \
162
+ 'ConvertTo-Csv -NoTypeInformation')
163
+ ).and_return(check_output)
164
+
165
+ upload
166
+ end
167
+ end
168
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
169
+
170
+ describe 'for a new file' do
171
+ # let(:check_output) do
172
+ def check_output
173
+ create_check_output([
174
+ CheckEntry.new('False', src_md5, nil, 'True', 'False')
175
+ ])
176
+ end
177
+
178
+ let(:cmd_output) do
179
+ o = ::WinRM::Output.new
180
+ o[:exitcode] = 0
181
+ o
182
+ end
183
+
184
+ # let(:decode_output) do
185
+ def decode_output
186
+ create_decode_output([
187
+ DecodeEntry.new(dst, 'True', src_md5, src_md5, ps_tmpfile, nil)
188
+ ])
189
+ end
190
+
191
+ before do
192
+ allow(service).to receive(:run_cmd)
193
+ .and_return(cmd_output)
194
+
195
+ allow(service).to receive(:run_powershell_script)
196
+ .with(/^Check-Files .+ \| ConvertTo-Csv/)
197
+ .and_return(check_output)
198
+
199
+ allow(service).to receive(:run_powershell_script)
200
+ .with(/^Decode-Files .+ \| ConvertTo-Csv/)
201
+ .and_return(decode_output)
202
+ end
203
+
204
+ common_specs_for_all_single_file_types
205
+
206
+ common_specs_for_all_single_dirty_file_types
207
+
208
+ it 'returns a report hash' do
209
+ expect(upload[1]).to eq(
210
+ src_md5 => {
211
+ 'src' => local,
212
+ 'dst' => dst,
213
+ 'tmpfile' => ps_tmpfile,
214
+ 'tmpzip' => nil,
215
+ 'src_md5' => src_md5,
216
+ 'dst_md5' => src_md5,
217
+ 'chk_exists' => 'False',
218
+ 'chk_dirty' => 'True',
219
+ 'verifies' => 'True',
220
+ 'size' => size,
221
+ 'xfered' => size / 3 * 4,
222
+ 'chunks' => (size / 6000.to_f).ceil
223
+ }
224
+ )
225
+ end
226
+
227
+ describe 'when a failed check command is returned' do
228
+ def check_output
229
+ o = ::WinRM::Output.new
230
+ o[:exitcode] = 10
231
+ o[:data].concat([{ stderr: 'Oh noes\n' }])
232
+ o
233
+ end
234
+
235
+ it 'raises a FileTransporterFailed error' do
236
+ expect { upload }.to raise_error(
237
+ WinRM::FS::Core::FileTransporterFailed, /Upload failed \(exitcode: 10\)/)
238
+ end
239
+ end
240
+
241
+ describe 'when a failed decode command is returned' do
242
+ def decode_output
243
+ o = ::WinRM::Output.new
244
+ o[:exitcode] = 10
245
+ o[:data].concat([{ stderr: 'Oh noes\n' }])
246
+ o
247
+ end
248
+
249
+ it 'raises a FileTransporterFailed error' do
250
+ expect { upload }.to raise_error(
251
+ WinRM::FS::Core::FileTransporterFailed, /Upload failed \(exitcode: 10\)/)
252
+ end
253
+ end
254
+ end
255
+
256
+ describe 'for an out of date (dirty) file' do
257
+ let(:check_output) do
258
+ create_check_output([
259
+ CheckEntry.new('True', src_md5, 'aabbcc', 'True', 'False')
260
+ ])
261
+ end
262
+
263
+ let(:cmd_output) do
264
+ o = ::WinRM::Output.new
265
+ o[:exitcode] = 0
266
+ o
267
+ end
268
+
269
+ let(:decode_output) do
270
+ create_decode_output([
271
+ DecodeEntry.new(dst, 'True', src_md5, src_md5, ps_tmpfile, nil)
272
+ ])
273
+ end
274
+
275
+ before do
276
+ allow(service).to receive(:run_cmd)
277
+ .and_return(cmd_output)
278
+
279
+ allow(service).to receive(:run_powershell_script)
280
+ .with(/^Check-Files .+ \| ConvertTo-Csv/)
281
+ .and_return(check_output)
282
+
283
+ allow(service).to receive(:run_powershell_script)
284
+ .with(/^Decode-Files .+ \| ConvertTo-Csv/)
285
+ .and_return(decode_output)
286
+ end
287
+
288
+ common_specs_for_all_single_file_types
289
+
290
+ common_specs_for_all_single_dirty_file_types
291
+
292
+ it 'returns a report hash' do
293
+ expect(upload[1]).to eq(
294
+ src_md5 => {
295
+ 'src' => local,
296
+ 'dst' => dst,
297
+ 'tmpfile' => ps_tmpfile,
298
+ 'tmpzip' => nil,
299
+ 'src_md5' => src_md5,
300
+ 'dst_md5' => src_md5,
301
+ 'chk_exists' => 'True',
302
+ 'chk_dirty' => 'True',
303
+ 'verifies' => 'True',
304
+ 'size' => size,
305
+ 'xfered' => size / 3 * 4,
306
+ 'chunks' => (size / 6000.to_f).ceil
307
+ }
308
+ )
309
+ end
310
+ end
311
+
312
+ describe 'for an up to date (clean) file' do
313
+ let(:check_output) do
314
+ create_check_output([
315
+ CheckEntry.new('True', src_md5, src_md5, 'False', 'True')
316
+ ])
317
+ end
318
+
319
+ let(:cmd_output) do
320
+ o = ::WinRM::Output.new
321
+ o[:exitcode] = 0
322
+ o
323
+ end
324
+
325
+ before do
326
+ allow(service).to receive(:run_cmd)
327
+ .and_return(cmd_output)
328
+
329
+ allow(service).to receive(:run_powershell_script)
330
+ .with(/^Check-Files .+ \| ConvertTo-Csv/)
331
+ .and_return(check_output)
332
+ end
333
+
334
+ common_specs_for_all_single_file_types
335
+
336
+ it 'uploads nothing' do
337
+ expect(service).not_to receive(:run_cmd).with(/#{remote}/)
338
+
339
+ upload
340
+ end
341
+
342
+ it 'skips the decode_files powershell script' do
343
+ expect(service).not_to receive(:run_powershell_script).with(regexify(
344
+ 'Decode-Files $files | ConvertTo-Csv -NoTypeInformation')
345
+ )
346
+
347
+ upload
348
+ end
349
+
350
+ it 'returns a report hash' do
351
+ expect(upload[1]).to eq(
352
+ src_md5 => {
353
+ 'src' => local,
354
+ 'dst' => dst,
355
+ 'size' => size,
356
+ 'src_md5' => src_md5,
357
+ 'dst_md5' => src_md5,
358
+ 'chk_exists' => 'True',
359
+ 'chk_dirty' => 'False',
360
+ 'verifies' => 'True'
361
+ }
362
+ )
363
+ end
364
+ end
365
+ end
366
+
367
+ describe 'when uploading a single directory' do
368
+ let(:content) { "I'm a fake zip file" }
369
+ let(:local) { Dir.mktmpdir('input') }
370
+ let(:remote) { 'C:\\dest' }
371
+ let(:src_zip) { create_tempfile('fake.zip', content) }
372
+ let(:dst) { remote }
373
+ let(:src_md5) { md5sum(src_zip) }
374
+ let(:size) { File.size(src_zip) }
375
+ let(:cmd_tmpfile) { "%TEMP%\\b64-#{src_md5}.txt" }
376
+ let(:ps_tmpfile) { "$env:TEMP\\b64-#{src_md5}.txt" }
377
+ let(:ps_tmpzip) { "$env:TEMP\\winrm-upload\\tmpzip-#{src_md5}.zip" }
378
+
379
+ let(:tmp_zip) { double('tmp_zip') }
380
+
381
+ let(:cmd_output) do
382
+ o = ::WinRM::Output.new
383
+ o[:exitcode] = 0
384
+ o
385
+ end
386
+
387
+ let(:check_output) do
388
+ create_check_output([
389
+ CheckEntry.new('False', src_md5, nil, 'True', 'False')
390
+ ])
391
+ end
392
+
393
+ let(:decode_output) do
394
+ create_decode_output([
395
+ DecodeEntry.new(dst, 'True', src_md5, src_md5, ps_tmpfile, ps_tmpzip)
396
+ ])
397
+ end
398
+
399
+ before do
400
+ allow(tmp_zip).to receive(:path).and_return(Pathname(src_zip))
401
+ allow(tmp_zip).to receive(:unlink)
402
+ allow(WinRM::FS::Core::TmpZip).to receive(:new).with("#{local}/", logger)
403
+ .and_return(tmp_zip)
404
+
405
+ allow(service).to receive(:run_cmd)
406
+ .and_return(cmd_output)
407
+
408
+ allow(service).to receive(:run_powershell_script)
409
+ .with(/^Check-Files .+ \| ConvertTo-Csv/)
410
+ .and_return(check_output)
411
+
412
+ allow(service).to receive(:run_powershell_script)
413
+ .with(/^Decode-Files .+ \| ConvertTo-Csv/)
414
+ .and_return(decode_output)
415
+ end
416
+
417
+ after do
418
+ FileUtils.rm_rf(local)
419
+ end
420
+
421
+ let(:upload) { transporter.upload("#{local}/", remote) }
422
+
423
+ it 'truncates a zero-byte hash_file for check_files' do
424
+ expect(service).to receive(:run_cmd).with(regexify(%(echo|set /p=>"%TEMP%\\hash-alpha.txt"))
425
+ ).and_return(cmd_output)
426
+
427
+ upload
428
+ end
429
+
430
+ it 'uploads the hash_file in chunks for check_files' do
431
+ hash = outdent!(<<-HASH.chomp)
432
+ @{
433
+ "#{ps_tmpzip}" = "#{src_md5}"
434
+ }
435
+ HASH
436
+
437
+ expect(service).to receive(:run_cmd)
438
+ .with(%(echo #{base64(hash)} >> "%TEMP%\\hash-alpha.txt"))
439
+ .and_return(cmd_output).once
440
+
441
+ upload
442
+ end
443
+
444
+ it 'sets hash_file and runs the check_files powershell script' do
445
+ expect(service).to receive(:run_powershell_script).with(
446
+ regexify(%($hash_file = "$env:TEMP\\hash-alpha.txt")) &&
447
+ regexify(
448
+ 'Check-Files (Invoke-Input $hash_file) | ' \
449
+ 'ConvertTo-Csv -NoTypeInformation')
450
+ ).and_return(check_output)
451
+
452
+ upload
453
+ end
454
+
455
+ it 'truncates a zero-byte tempfile' do
456
+ expect(service).to receive(:run_cmd).with(regexify(%(echo|set /p=>"#{cmd_tmpfile}"))
457
+ ).and_return(cmd_output)
458
+
459
+ upload
460
+ end
461
+
462
+ it 'uploads the zip file in base64 encoding' do
463
+ expect(service).to receive(:run_cmd)
464
+ .with(%(echo #{base64(content)} >> "#{cmd_tmpfile}"))
465
+ .and_return(cmd_output)
466
+
467
+ upload
468
+ end
469
+
470
+ it 'truncates a zero-byte hash_file for decode_files' do
471
+ expect(service).to receive(:run_cmd).with(regexify(%(echo|set /p=>"%TEMP%\\hash-beta.txt"))
472
+ ).and_return(cmd_output)
473
+
474
+ upload
475
+ end
476
+
477
+ it 'uploads the hash_file in chunks for decode_files' do
478
+ hash = outdent!(<<-HASH.chomp)
479
+ @{
480
+ "#{ps_tmpfile}" = @{
481
+ "dst" = "#{dst}\\#{File.basename(local)}";
482
+ "tmpzip" = "#{ps_tmpzip}"
483
+ }
484
+ }
485
+ HASH
486
+
487
+ expect(service).to receive(:run_cmd)
488
+ .with(%(echo #{base64(hash)} >> "%TEMP%\\hash-beta.txt"))
489
+ .and_return(cmd_output).once
490
+
491
+ upload
492
+ end
493
+
494
+ it 'sets hash_file and runs the decode_files powershell script' do
495
+ expect(service).to receive(:run_powershell_script).with(
496
+ regexify(%($hash_file = "$env:TEMP\\hash-beta.txt")) &&
497
+ regexify(
498
+ 'Decode-Files (Invoke-Input $hash_file) | ' \
499
+ 'ConvertTo-Csv -NoTypeInformation')
500
+ ).and_return(check_output)
501
+
502
+ upload
503
+ end
504
+
505
+ it 'returns a report hash' do
506
+ expect(upload[1]).to eq(
507
+ src_md5 => {
508
+ 'src' => "#{local}/",
509
+ 'src_zip' => src_zip,
510
+ 'dst' => dst,
511
+ 'tmpfile' => ps_tmpfile,
512
+ 'tmpzip' => ps_tmpzip,
513
+ 'src_md5' => src_md5,
514
+ 'dst_md5' => src_md5,
515
+ 'chk_exists' => 'False',
516
+ 'chk_dirty' => 'True',
517
+ 'verifies' => 'True',
518
+ 'size' => size,
519
+ 'xfered' => size / 3 * 4,
520
+ 'chunks' => (size / 6000.to_f).ceil
521
+ }
522
+ )
523
+ end
524
+
525
+ it 'cleans up the zip file' do
526
+ expect(tmp_zip).to receive(:unlink)
527
+
528
+ upload
529
+ end
530
+
531
+ describe 'when a failed check command is returned' do
532
+ def check_output
533
+ o = ::WinRM::Output.new
534
+ o[:exitcode] = 10
535
+ o[:data].concat([{ stderr: 'Oh noes\n' }])
536
+ o
537
+ end
538
+
539
+ it 'raises a FileTransporterFailed error' do
540
+ expect { upload }.to raise_error(
541
+ WinRM::FS::Core::FileTransporterFailed, /Upload failed \(exitcode: 10\)/)
542
+ end
543
+ end
544
+
545
+ describe 'when a failed decode command is returned' do
546
+ def decode_output
547
+ o = ::WinRM::Output.new
548
+ o[:exitcode] = 10
549
+ o[:data].concat([{ stderr: 'Oh noes\n' }])
550
+ o
551
+ end
552
+
553
+ it 'raises a FileTransporterFailed error' do
554
+ expect { upload }.to raise_error(
555
+ WinRM::FS::Core::FileTransporterFailed, /Upload failed \(exitcode: 10\)/)
556
+ end
557
+ end
558
+ end
559
+
560
+ describe 'when uploading multiple files' do
561
+ let(:remote) { 'C:\\Program Files' }
562
+
563
+ 1.upto(3).each do |i|
564
+ let(:"local#{i}") { create_tempfile("input#{i}.txt", "input#{i}") }
565
+ let(:"src#{i}_md5") { md5sum(send("local#{i}")) }
566
+ let(:"dst#{i}") { "#{remote}/#{File.basename(send("local#{i}"))}" }
567
+ let(:"size#{i}") { File.size(send("local#{i}")) }
568
+ let(:"cmd#{i}_tmpfile") { "%TEMP%\\b64-#{send("src#{i}_md5")}.txt" }
569
+ let(:"ps#{i}_tmpfile") { "$env:TEMP\\b64-#{send("src#{i}_md5")}.txt" }
570
+ end
571
+
572
+ let(:check_output) do
573
+ create_check_output([
574
+ # new
575
+ CheckEntry.new('False', src1_md5, nil, 'True', 'False'),
576
+ # out-of-date
577
+ CheckEntry.new('True', src2_md5, 'aabbcc', 'True', 'False'),
578
+ # current
579
+ CheckEntry.new('True', src3_md5, src3_md5, 'False', 'True')
580
+ ])
581
+ end
582
+
583
+ let(:cmd_output) do
584
+ o = ::WinRM::Output.new
585
+ o[:exitcode] = 0
586
+ o
587
+ end
588
+
589
+ let(:decode_output) do
590
+ create_decode_output([
591
+ DecodeEntry.new(dst1, 'True', src1_md5, src1_md5, ps1_tmpfile, nil),
592
+ DecodeEntry.new(dst2, 'True', src2_md5, src2_md5, ps2_tmpfile, nil)
593
+ ])
594
+ end
595
+
596
+ let(:upload) { transporter.upload([local1, local2, local3], remote) }
597
+
598
+ before do
599
+ allow(service).to receive(:run_cmd)
600
+ .and_return(cmd_output)
601
+
602
+ allow(service).to receive(:run_powershell_script)
603
+ .with(/^Check-Files .+ \| ConvertTo-Csv/)
604
+ .and_return(check_output)
605
+
606
+ allow(service).to receive(:run_powershell_script)
607
+ .with(/^Decode-Files .+ \| ConvertTo-Csv/)
608
+ .and_return(decode_output)
609
+ end
610
+
611
+ it 'truncates a zero-byte hash_file for check_files' do
612
+ expect(service).to receive(:run_cmd).with(regexify(%(echo|set /p=>"%TEMP%\\hash-alpha.txt"))
613
+ ).and_return(cmd_output)
614
+
615
+ upload
616
+ end
617
+
618
+ it 'uploads the hash_file in chunks for check_files' do
619
+ hash = outdent!(<<-HASH.chomp)
620
+ @{
621
+ "#{dst1}" = "#{src1_md5}";
622
+ "#{dst2}" = "#{src2_md5}";
623
+ "#{dst3}" = "#{src3_md5}"
624
+ }
625
+ HASH
626
+
627
+ expect(service).to receive(:run_cmd)
628
+ .with(%(echo #{base64(hash)} >> "%TEMP%\\hash-alpha.txt"))
629
+ .and_return(cmd_output).once
630
+
631
+ upload
632
+ end
633
+
634
+ it 'sets hash_file and runs the check_files powershell script' do
635
+ expect(service).to receive(:run_powershell_script).with(
636
+ regexify(%($hash_file = "$env:TEMP\\hash-alpha.txt")) &&
637
+ regexify(
638
+ 'Check-Files (Invoke-Input $hash_file) | ' \
639
+ 'ConvertTo-Csv -NoTypeInformation')
640
+ ).and_return(check_output)
641
+
642
+ upload
643
+ end
644
+
645
+ it 'only uploads dirty files' do
646
+ expect(service).to receive(:run_cmd)
647
+ .with(%(echo #{base64(IO.read(local1))} >> "#{cmd1_tmpfile}"))
648
+ expect(service).to receive(:run_cmd)
649
+ .with(%(echo #{base64(IO.read(local2))} >> "#{cmd2_tmpfile}"))
650
+ expect(service).not_to receive(:run_cmd)
651
+ .with(%(echo #{base64(IO.read(local3))} >> "#{cmd3_tmpfile}"))
652
+
653
+ upload
654
+ end
655
+
656
+ it 'truncates a zero-byte hash_file for decode_files' do
657
+ expect(service).to receive(:run_cmd).with(regexify(%(echo|set /p=>"%TEMP%\\hash-beta.txt"))
658
+ ).and_return(cmd_output)
659
+
660
+ upload
661
+ end
662
+
663
+ it 'uploads the hash_file in chunks for decode_files' do
664
+ hash = outdent!(<<-HASH.chomp)
665
+ @{
666
+ "#{ps1_tmpfile}" = @{
667
+ "dst" = "#{dst1}"
668
+ };
669
+ "#{ps2_tmpfile}" = @{
670
+ "dst" = "#{dst2}"
671
+ }
672
+ }
673
+ HASH
674
+
675
+ expect(service).to receive(:run_cmd)
676
+ .with(%(echo #{base64(hash)} >> "%TEMP%\\hash-beta.txt"))
677
+ .and_return(cmd_output).once
678
+
679
+ upload
680
+ end
681
+
682
+ it 'sets hash_file and runs the decode_files powershell script' do
683
+ expect(service).to receive(:run_powershell_script).with(
684
+ regexify(%($hash_file = '$env:TEMP\\hash-beta.txt')) &&
685
+ regexify(
686
+ 'Decode-Files (Invoke-Input $hash_file) | ' \
687
+ 'ConvertTo-Csv -NoTypeInformation')
688
+ ).and_return(check_output)
689
+
690
+ upload
691
+ end
692
+
693
+ it 'returns a report hash' do
694
+ report = upload[1]
695
+
696
+ expect(report.fetch(src1_md5)).to eq(
697
+ 'src' => local1,
698
+ 'dst' => dst1,
699
+ 'tmpfile' => ps1_tmpfile,
700
+ 'tmpzip' => nil,
701
+ 'src_md5' => src1_md5,
702
+ 'dst_md5' => src1_md5,
703
+ 'chk_exists' => 'False',
704
+ 'chk_dirty' => 'True',
705
+ 'verifies' => 'True',
706
+ 'size' => size1,
707
+ 'xfered' => size1 / 3 * 4,
708
+ 'chunks' => (size1 / 6000.to_f).ceil
709
+ )
710
+ expect(report.fetch(src2_md5)).to eq(
711
+ 'src' => local2,
712
+ 'dst' => dst2,
713
+ 'tmpfile' => ps2_tmpfile,
714
+ 'tmpzip' => nil,
715
+ 'src_md5' => src2_md5,
716
+ 'dst_md5' => src2_md5,
717
+ 'chk_exists' => 'True',
718
+ 'chk_dirty' => 'True',
719
+ 'verifies' => 'True',
720
+ 'size' => size2,
721
+ 'xfered' => size2 / 3 * 4,
722
+ 'chunks' => (size2 / 6000.to_f).ceil
723
+ )
724
+ expect(report.fetch(src3_md5)).to eq(
725
+ 'src' => local3,
726
+ 'dst' => dst3,
727
+ 'src_md5' => src3_md5,
728
+ 'dst_md5' => src3_md5,
729
+ 'chk_exists' => 'True',
730
+ 'chk_dirty' => 'False',
731
+ 'verifies' => 'True',
732
+ 'size' => size3
733
+ )
734
+ end
735
+
736
+ describe 'when a failed check command is returned' do
737
+ def check_output
738
+ o = ::WinRM::Output.new
739
+ o[:exitcode] = 10
740
+ o[:data].concat([{ stderr: "Oh noes\n" }])
741
+ o
742
+ end
743
+
744
+ it 'raises a FileTransporterFailed error' do
745
+ expect { upload }.to raise_error(
746
+ WinRM::FS::Core::FileTransporterFailed, /Upload failed \(exitcode: 10\)/)
747
+ end
748
+ end
749
+
750
+ describe 'when a failed decode command is returned' do
751
+ def decode_output
752
+ o = ::WinRM::Output.new
753
+ o[:exitcode] = 10
754
+ o[:data].concat([{ stderr: "Oh noes\n" }])
755
+ o
756
+ end
757
+
758
+ it 'raises a FileTransporterFailed error' do
759
+ expect { upload }.to raise_error(
760
+ WinRM::FS::Core::FileTransporterFailed, /Upload failed \(exitcode: 10\)/)
761
+ end
762
+ end
763
+ end
764
+
765
+ it 'raises an exception when local file or directory is not found' do
766
+ expect { transporter.upload('/a/b/c/nope', 'C:\\nopeland') }.to raise_error Errno::ENOENT
767
+ end
768
+
769
+ def base64(string)
770
+ Base64.strict_encode64(string)
771
+ end
772
+
773
+ def create_check_output(entries)
774
+ csv = CSV.generate(force_quotes: true) do |rows|
775
+ rows << CheckEntry.new.members.map(&:to_s)
776
+ entries.each { |entry| rows << entry.to_a }
777
+ end
778
+
779
+ o = ::WinRM::Output.new
780
+ o[:exitcode] = 0
781
+ o[:data].concat(csv.lines.map { |line| { stdout: line } })
782
+ o
783
+ end
784
+
785
+ def create_decode_output(entries)
786
+ csv = CSV.generate(force_quotes: true) do |rows|
787
+ rows << DecodeEntry.new.members.map(&:to_s)
788
+ entries.each { |entry| rows << entry.to_a }
789
+ end
790
+
791
+ o = ::WinRM::Output.new
792
+ o[:exitcode] = 0
793
+ o[:data].concat(csv.lines.map { |line| { stdout: line } })
794
+ o
795
+ end
796
+
797
+ def create_tempfile(name, content)
798
+ pre, _, ext = name.rpartition('.')
799
+ file = Tempfile.open(["#{pre}-", ".#{ext}"])
800
+ @tempfiles << file
801
+ file.write(content)
802
+ file.close
803
+ file.path
804
+ end
805
+
806
+ def md5sum(local)
807
+ Digest::MD5.file(local).hexdigest
808
+ end
809
+
810
+ def outdent!(string)
811
+ string.gsub!(/^ {#{string.index(/[^ ]/)}}/, '')
812
+ end
813
+
814
+ def regexify(str, line = :whole_line)
815
+ r = Regexp.escape(str)
816
+ r = "^#{r}$" if line == :whole_line
817
+ Regexp.new(r)
818
+ end
819
+ end