winrm-fs 0.4.2 → 0.4.3

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