winrm-fs 0.3.1 → 0.3.2

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/matchers.rb CHANGED
@@ -1,58 +1,58 @@
1
- # encoding: UTF-8
2
- require 'rspec/expectations'
3
-
4
- RSpec::Matchers.define :have_created do |remote_file|
5
- match do |file_manager|
6
- if @expected_content
7
- downloaded_file = Tempfile.new('downloaded')
8
- downloaded_file.close
9
-
10
- subject.download(remote_file, downloaded_file.path)
11
- @actual_content = File.read(downloaded_file.path)
12
- downloaded_file.delete
13
-
14
- file_manager.exists?(remote_file) && \
15
- @actual_content == @expected_content
16
- else
17
- file_manager.exists?(remote_file)
18
- end
19
- end
20
- chain :with_content do |expected_content|
21
- expected_content = File.read(expected_content) if File.file?(expected_content)
22
- @expected_content = expected_content
23
- end
24
- failure_message do
25
- if @expected_content
26
- <<-EOH
27
- Expected file '#{remote_file}' to exist with content:
28
-
29
- #{@expected_content}
30
-
31
- but instead got content:
32
-
33
- #{@actual_content}
34
- EOH
35
- else
36
- "Expected file '#{remote_file}' to exist"
37
- end
38
- end
39
- end
40
-
41
- RSpec::Matchers.define :contain_zip_entries do |zip_entries|
42
- match do |temp_zip_file|
43
- zip_entries = [zip_entries] if zip_entries.is_a? String
44
- @zip_file = Zip::File.open(temp_zip_file.path)
45
- @missing_entries = []
46
- zip_entries.each do |entry|
47
- @missing_entries << entry unless @zip_file.find_entry(entry)
48
- end
49
- @missing_entries.empty?
50
- end
51
- failure_message do |temp_zip_file|
52
- msg = "Expected #{temp_zip_file.path} to contain zip entries: #{@missing_entries}\n Got: "
53
- @zip_file.each do |entry|
54
- msg << entry.name << ', '
55
- end
56
- msg
57
- end
58
- end
1
+ # encoding: UTF-8
2
+ require 'rspec/expectations'
3
+
4
+ RSpec::Matchers.define :have_created do |remote_file|
5
+ match do |file_manager|
6
+ if @expected_content
7
+ downloaded_file = Tempfile.new('downloaded')
8
+ downloaded_file.close
9
+
10
+ subject.download(remote_file, downloaded_file.path)
11
+ @actual_content = File.read(downloaded_file.path)
12
+ downloaded_file.delete
13
+
14
+ file_manager.exists?(remote_file) && \
15
+ @actual_content == @expected_content
16
+ else
17
+ file_manager.exists?(remote_file)
18
+ end
19
+ end
20
+ chain :with_content do |expected_content|
21
+ expected_content = File.read(expected_content) if File.file?(expected_content)
22
+ @expected_content = expected_content
23
+ end
24
+ failure_message do
25
+ if @expected_content
26
+ <<-EOH
27
+ Expected file '#{remote_file}' to exist with content:
28
+
29
+ #{@expected_content}
30
+
31
+ but instead got content:
32
+
33
+ #{@actual_content}
34
+ EOH
35
+ else
36
+ "Expected file '#{remote_file}' to exist"
37
+ end
38
+ end
39
+ end
40
+
41
+ RSpec::Matchers.define :contain_zip_entries do |zip_entries|
42
+ match do |temp_zip_file|
43
+ zip_entries = [zip_entries] if zip_entries.is_a? String
44
+ @zip_file = Zip::File.open(temp_zip_file.path)
45
+ @missing_entries = []
46
+ zip_entries.each do |entry|
47
+ @missing_entries << entry unless @zip_file.find_entry(entry)
48
+ end
49
+ @missing_entries.empty?
50
+ end
51
+ failure_message do |temp_zip_file|
52
+ msg = "Expected #{temp_zip_file.path} to contain zip entries: #{@missing_entries}\n Got: "
53
+ @zip_file.each do |entry|
54
+ msg << entry.name << ', '
55
+ end
56
+ msg
57
+ end
58
+ 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,819 +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
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