winrm-fs 0.3.2 → 0.4.0

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,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
- "#{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
+ "#{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