winrm-fs 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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