winrm-fs 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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