chef-apply 0.1.17 → 0.1.18

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +0 -7
  3. data/Gemfile.lock +176 -84
  4. data/chef-apply.gemspec +2 -2
  5. data/lib/chef_apply/version.rb +1 -1
  6. data/spec/fixtures/custom_config.toml +2 -0
  7. data/spec/integration/chef-run_spec.rb +41 -0
  8. data/spec/integration/fixtures/chef_help.out +69 -0
  9. data/spec/integration/fixtures/chef_version.out +1 -0
  10. data/spec/integration/spec_helper.rb +55 -0
  11. data/spec/spec_helper.rb +114 -0
  12. data/spec/support/matchers/output_to_terminal.rb +36 -0
  13. data/spec/unit/action/base_spec.rb +89 -0
  14. data/spec/unit/action/converge_target_spec.rb +292 -0
  15. data/spec/unit/action/generate_local_policy_spec.rb +114 -0
  16. data/spec/unit/action/generate_temp_cookbook_spec.rb +75 -0
  17. data/spec/unit/action/install_chef/base_spec.rb +234 -0
  18. data/spec/unit/action/install_chef_spec.rb +69 -0
  19. data/spec/unit/cli/options_spec.rb +75 -0
  20. data/spec/unit/cli/validation_spec.rb +78 -0
  21. data/spec/unit/cli_spec.rb +440 -0
  22. data/spec/unit/config_spec.rb +70 -0
  23. data/spec/unit/errors/ccr_failure_mapper_spec.rb +103 -0
  24. data/spec/unit/file_fetcher_spec.rb +40 -0
  25. data/spec/unit/fixtures/multi-error.out +2 -0
  26. data/spec/unit/log_spec.rb +37 -0
  27. data/spec/unit/recipe_lookup_spec.rb +122 -0
  28. data/spec/unit/startup_spec.rb +283 -0
  29. data/spec/unit/target_host_spec.rb +231 -0
  30. data/spec/unit/target_resolver_spec.rb +380 -0
  31. data/spec/unit/telemeter/sender_spec.rb +140 -0
  32. data/spec/unit/telemeter_spec.rb +191 -0
  33. data/spec/unit/temp_cookbook_spec.rb +199 -0
  34. data/spec/unit/ui/error_printer_spec.rb +173 -0
  35. data/spec/unit/ui/terminal_spec.rb +109 -0
  36. data/spec/unit/version_spec.rb +31 -0
  37. data/warning.txt +3 -0
  38. metadata +34 -2
@@ -0,0 +1,231 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2018 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "spec_helper"
19
+ require "ostruct"
20
+ require "chef_apply/target_host"
21
+
22
+ RSpec.describe ChefApply::TargetHost do
23
+ let(:host) { "mock://user@example.com" }
24
+ let(:sudo) { true }
25
+ let(:logger) { nil }
26
+ let(:family) { "windows" }
27
+ let(:is_linux) { false }
28
+ let(:platform_mock) { double("platform", linux?: is_linux, family: family, name: "an os") }
29
+ subject do
30
+ s = ChefApply::TargetHost.new(host, sudo: sudo, logger: logger)
31
+ allow(s).to receive(:platform).and_return(platform_mock)
32
+ s
33
+ end
34
+
35
+ context "#base_os" do
36
+ context "for a windows os" do
37
+ it "reports :windows" do
38
+ expect(subject.base_os).to eq :windows
39
+ end
40
+ end
41
+
42
+ context "for a linux os" do
43
+ let(:family) { "debian" }
44
+ let(:is_linux) { true }
45
+ it "reports :linux" do
46
+ expect(subject.base_os).to eq :linux
47
+ end
48
+ end
49
+
50
+ context "for an unsupported OS" do
51
+ let(:family) { "other" }
52
+ let(:is_linux) { false }
53
+ it "raises UnsupportedTargetOS" do
54
+ expect { subject.base_os }.to raise_error(ChefApply::TargetHost::UnsupportedTargetOS)
55
+ end
56
+ end
57
+ end
58
+
59
+ context "#installed_chef_version" do
60
+ let(:manifest) { :not_found }
61
+ before do
62
+ allow(subject).to receive(:get_chef_version_manifest).and_return manifest
63
+ end
64
+
65
+ context "when no version manifest is present" do
66
+ it "raises ChefNotInstalled" do
67
+ expect { subject.installed_chef_version }.to raise_error(ChefApply::TargetHost::ChefNotInstalled)
68
+ end
69
+ end
70
+
71
+ context "when version manifest is present" do
72
+ let(:manifest) { { "build_version" => "14.0.1" } }
73
+ it "reports version based on the build_version field" do
74
+ expect(subject.installed_chef_version).to eq Gem::Version.new("14.0.1")
75
+ end
76
+ end
77
+ end
78
+
79
+ context "connect!" do
80
+ let(:train_connection_mock) { double("train connection") }
81
+ before do
82
+ allow(subject).to receive(:train_connection).and_return(train_connection_mock)
83
+ end
84
+ context "when an Train::UserError occurs" do
85
+ it "raises a ConnectionFailure" do
86
+ allow(train_connection_mock).to receive(:connection).and_raise Train::UserError
87
+ expect { subject.connect! }.to raise_error(ChefApply::TargetHost::ConnectionFailure)
88
+ end
89
+ end
90
+ context "when a Train::Error occurs" do
91
+ it "raises a ConnectionFailure" do
92
+ allow(train_connection_mock).to receive(:connection).and_raise Train::Error
93
+ expect { subject.connect! }.to raise_error(ChefApply::TargetHost::ConnectionFailure)
94
+ end
95
+ end
96
+ end
97
+
98
+ context "#user" do
99
+ before do
100
+ allow(subject).to receive(:config).and_return(user: user)
101
+ end
102
+ context "when a user has been configured" do
103
+ let(:user) { "testuser" }
104
+ it "returns that user" do
105
+ expect(subject.user).to eq user
106
+ end
107
+ end
108
+ context "when no user has been configured" do
109
+ let(:user) { nil }
110
+ it "returns the correct default from train" do
111
+ expect(subject.user).to eq Train::Transports::SSH.default_options[:user][:default]
112
+ end
113
+ end
114
+ end
115
+
116
+ context "#run_command!" do
117
+ let(:backend) { double("backend") }
118
+ let(:exit_status) { 0 }
119
+ let(:result) { RemoteExecResult.new(exit_status, "", "an error occurred") }
120
+ let(:command) { "cmd" }
121
+
122
+ before do
123
+ allow(subject).to receive(:backend).and_return(backend)
124
+ allow(backend).to receive(:run_command).with(command).and_return(result)
125
+ end
126
+
127
+ context "when no error occurs" do
128
+ let(:exit_status) { 0 }
129
+ it "returns the result" do
130
+ expect(subject.run_command!(command)).to eq result
131
+ end
132
+
133
+ context "when sudo_as_user is true" do
134
+ let(:family) { "debian" }
135
+ let(:is_linux) { true }
136
+ it "returns the result" do
137
+ expect(backend).to receive(:run_command).with("-u user #{command}").and_return(result)
138
+ expect(subject.run_command!(command, true)).to eq result
139
+ end
140
+ end
141
+ end
142
+
143
+ context "when an error occurs" do
144
+ let(:exit_status) { 1 }
145
+ it "raises a RemoteExecutionFailed error" do
146
+ expected_error = ChefApply::TargetHost::RemoteExecutionFailed
147
+ expect { subject.run_command!(command) }.to raise_error(expected_error)
148
+ end
149
+ end
150
+ end
151
+
152
+ context "#get_chef_version_manifest" do
153
+ let(:manifest_content) { '{"build_version" : "1.2.3"}' }
154
+ let(:expected_manifest_path) do
155
+ {
156
+ windows: "c:\\opscode\\chef\\version-manifest.json",
157
+ linux: "/opt/chef/version-manifest.json"
158
+ }
159
+ end
160
+ let(:base_os) { :unknown }
161
+ before do
162
+ remote_file_mock = double("remote_file", file?: manifest_exists, content: manifest_content)
163
+ backend_mock = double("backend")
164
+ expect(backend_mock).to receive(:file).
165
+ with(expected_manifest_path[base_os]).
166
+ and_return(remote_file_mock)
167
+ allow(subject).to receive(:backend).and_return backend_mock
168
+ allow(subject).to receive(:base_os).and_return base_os
169
+ end
170
+
171
+ context "when manifest is missing" do
172
+ let(:manifest_exists) { false }
173
+ context "on windows" do
174
+ let(:base_os) { :windows }
175
+ it "returns :not_found" do
176
+ expect(subject.get_chef_version_manifest).to eq :not_found
177
+ end
178
+
179
+ end
180
+ context "on linux" do
181
+ let(:base_os) { :linux }
182
+ it "returns :not_found" do
183
+ expect(subject.get_chef_version_manifest).to eq :not_found
184
+ end
185
+ end
186
+ end
187
+
188
+ context "when manifest is present" do
189
+ let(:manifest_exists) { true }
190
+ context "on windows" do
191
+ let(:base_os) { :windows }
192
+ it "should return the parsed manifest" do
193
+ expect(subject.get_chef_version_manifest).to eq({ "build_version" => "1.2.3" })
194
+ end
195
+ end
196
+
197
+ context "on linux" do
198
+ let(:base_os) { :linux }
199
+ it "should return the parsed manifest" do
200
+ expect(subject.get_chef_version_manifest).to eq({ "build_version" => "1.2.3" })
201
+ end
202
+ end
203
+ end
204
+ end
205
+
206
+ context "#apply_ssh_config" do
207
+ let(:ssh_host_config) { { user: "testuser", port: 1000, proxy: double("Net:SSH::Proxy::Command") } }
208
+ let(:connection_config) { { user: "user1", port: 8022, proxy: nil } }
209
+ before do
210
+ allow(subject).to receive(:ssh_config_for_host).and_return ssh_host_config
211
+ end
212
+
213
+ ChefApply::TargetHost::SSH_CONFIG_OVERRIDE_KEYS.each do |key|
214
+ context "when a value is not explicitly provided in options" do
215
+ it "replaces config config[:#{key}] with the ssh config value" do
216
+ subject.apply_ssh_config(connection_config, key => nil)
217
+ expect(connection_config[key]).to eq(ssh_host_config[key])
218
+ end
219
+ end
220
+
221
+ context "when a value is explicitly provided in options" do
222
+ it "the connection configuration isnot updated with a value from ssh config" do
223
+ original_config = connection_config.clone
224
+ subject.apply_ssh_config(connection_config, { key => "testvalue" } )
225
+ expect(connection_config[key]).to eq original_config[key]
226
+ end
227
+ end
228
+ end
229
+ end
230
+
231
+ end
@@ -0,0 +1,380 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2018 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "spec_helper"
19
+ require "chef_apply/target_resolver"
20
+
21
+ RSpec.describe ChefApply::TargetResolver do
22
+ let(:target_string) { "" }
23
+ let(:default_protocol) { "ssh" }
24
+ let(:connection_options) { {} }
25
+ subject { ChefApply::TargetResolver.new(target_string, default_protocol, connection_options) }
26
+
27
+ context "#targets" do
28
+ context "when no target is provided" do
29
+ let(:target_string) { "" }
30
+ it "returns an empty array" do
31
+ expect(subject.targets).to eq []
32
+ end
33
+ end
34
+
35
+ context "when a single target is provided" do
36
+ let(:target_string) { "ssh://localhost" }
37
+ it "returns any array with one target" do
38
+ actual_targets = subject.targets
39
+ expect(actual_targets[0].config[:host]).to eq "localhost"
40
+ end
41
+ end
42
+
43
+ context "when a comma-separated list of targets is provided" do
44
+ let(:target_string) { "ssh://node1.example.com,winrm://node2.example.com" }
45
+ it "returns an array with correct TargetHost instances" do
46
+ actual_targets = subject.targets
47
+ expect(actual_targets[0].config[:host]).to eq "node1.example.com"
48
+ expect(actual_targets[1].config[:host]).to eq "node2.example.com"
49
+ end
50
+ end
51
+ context "when a comma-separated list of targets that include ranges is provided" do
52
+ let(:target_string) { "ssh://node[0:1],ssh://machine[0:1]" }
53
+ it "returns an array with correct TargetHost instances" do
54
+ actual_targets = subject.targets
55
+
56
+ expect(actual_targets[0].config[:host]).to eq "node0"
57
+ expect(actual_targets[1].config[:host]).to eq "node1"
58
+ expect(actual_targets[2].config[:host]).to eq "machine0"
59
+ expect(actual_targets[3].config[:host]).to eq "machine1"
60
+ end
61
+ end
62
+
63
+ context "when a mixed list of targets containing user prefix and not are included" do
64
+
65
+ let(:target_string) { "test_user1@node1,node2,test_user2:password@node3" }
66
+
67
+ context "and the :user option is provided" do
68
+
69
+ let(:connection_options) { { user: "defaultuser" } }
70
+ it "should default to the given :user only for the host that does not include name" do
71
+ actual_targets = subject.targets
72
+ tc = actual_targets[0].config
73
+ expect(tc[:host]).to eq "node1"
74
+ expect(tc[:user]).to eq "test_user1"
75
+
76
+ tc = actual_targets[1].config
77
+ expect(tc[:host]).to eq "node2"
78
+ expect(tc[:user]).to eq "defaultuser"
79
+
80
+ tc = actual_targets[2].config
81
+ expect(tc[:host]).to eq "node3"
82
+ expect(tc[:user]).to eq "test_user2"
83
+ end
84
+ end
85
+ context "and the :user option is not provided" do
86
+ let(:opts) { {} }
87
+ it "should default to no user when user is not included with host" do
88
+ actual_targets = subject.targets
89
+ tc = actual_targets[0].config
90
+ expect(tc[:host]).to eq "node1"
91
+ expect(tc[:user]).to eq "test_user1"
92
+
93
+ tc = actual_targets[1].config
94
+ expect(tc[:host]).to eq "node2"
95
+ expect(tc[:user]).to eq nil
96
+
97
+ tc = actual_targets[2].config
98
+ expect(tc[:host]).to eq "node3"
99
+ expect(tc[:user]).to eq "test_user2"
100
+ end
101
+ end
102
+
103
+ end
104
+ end
105
+
106
+ context "#expand_targets" do
107
+ it "returns a single item when no expansion is required" do
108
+ expect(subject.expand_targets("one")).to eq ["one"]
109
+ end
110
+
111
+ it "expands single alphabetic range" do
112
+ expect(subject.expand_targets("host[a:h]")).to eq %w{
113
+ hosta hostb hostc hostd hoste hostf hostg hosth
114
+ }
115
+ end
116
+ it "expands single alphabetic range even if reverse ordering is given" do
117
+ expect(subject.expand_targets("host[h:a]")).to eq %w{
118
+ hosta hostb hostc hostd hoste hostf hostg hosth
119
+ }
120
+ end
121
+
122
+ it "expands a range when the target name is qualified with credentials" do
123
+ expect(subject.expand_targets("ssh://user:password@host[a:b]")).to eq %w{
124
+ ssh://user:password@hosta
125
+ ssh://user:password@hostb
126
+ }
127
+ end
128
+
129
+ it "expands a numeric range correctly when start/stop string values ASCII-sort in reverse" do
130
+ # eg: ["4", "10"].sort => ["10", "4"]
131
+ expect(subject.expand_targets("[4:10]")).to eq %w{ 4 5 6 7 8 9 10 }
132
+ end
133
+
134
+ it "expands a numeric range correctly when stop is higher than start" do
135
+ expect(subject.expand_targets("[10:8]")).to eq %w{ 8 9 10 }
136
+ end
137
+
138
+ it "expands a string range correctly when stop is higher than start" do
139
+ expect(subject.expand_targets("[z:y]")).to eq %w{ y z }
140
+ end
141
+
142
+ it "expands single numeric range" do
143
+ expect(subject.expand_targets("host[10:20]")).to eq %w{
144
+ host10 host11 host12 host13 host14 host15 host16
145
+ host17 host18 host19 host20
146
+ }
147
+ end
148
+
149
+ it "expands two included ranges" do
150
+ expect(subject.expand_targets("host[1:4].domain[a:c]").sort).to eq [
151
+ "host1.domaina", "host1.domainb", "host1.domainc",
152
+ "host2.domaina", "host2.domainb", "host2.domainc",
153
+ "host3.domaina", "host3.domainb", "host3.domainc",
154
+ "host4.domaina", "host4.domainb", "host4.domainc"
155
+ ].sort
156
+ end
157
+
158
+ it "raises InvalidRange if a range mixes alpha and numeric" do
159
+ expect { subject.expand_targets("host[a:9]") }.to raise_error(ChefApply::TargetResolver::InvalidRange)
160
+ end
161
+
162
+ it "raises TooManyRanges if more than two ranges are included" do
163
+ expect { subject.expand_targets("[0:1][5:10][10:11]") }.to raise_error(ChefApply::TargetResolver::TooManyRanges)
164
+ end
165
+
166
+ context "when the target resolves to more than #{ChefApply::TargetResolver::MAX_EXPANDED_TARGETS} names" do
167
+ it "raises TooManyTargets" do
168
+ expect { subject.expand_targets("[0:99999]") }.to raise_error(ChefApply::TargetResolver::TooManyTargets)
169
+ end
170
+ end
171
+ end
172
+
173
+ context "#make_credentials" do
174
+ let(:default_user) { nil }
175
+ let(:default_password) { nil }
176
+
177
+ let(:inline_user) { nil }
178
+ let(:inline_password) { nil }
179
+
180
+ subject do
181
+ opts = {}
182
+ opts[:user] = default_user unless default_user.nil?
183
+ opts[:password] = default_password unless default_password.nil?
184
+ resolver = ChefApply::TargetResolver.new("", default_protocol, opts)
185
+ Proc.new { resolver.make_credentials(inline_user, inline_password) }
186
+ end
187
+
188
+ context "when no default user or password is given" do
189
+ let(:default_user) { nil }
190
+ let(:default_password) { nil }
191
+
192
+ context "and only an inline user is provided" do
193
+ let(:inline_user) { "aninlineuser" }
194
+ let(:inline_password) { nil }
195
+ it "returns the decorated inline user with nil password" do
196
+ expect(subject.call).to eq [inline_user, nil]
197
+ end
198
+ end
199
+
200
+ context "and only an inline password is provided" do
201
+ let(:inline_user) { nil }
202
+ let(:inline_password) { "inlinepassword4u" }
203
+ it "returns the decorated inline password" do
204
+ expect(subject.call).to eq [nil, inline_password]
205
+ end
206
+ end
207
+
208
+ context "and neither inline user nor inline password is given" do
209
+ let(:inline_user) { nil }
210
+ let(:inline_password) { nil }
211
+ it "returns an empty string" do
212
+ expect(subject.call).to eq [nil, nil]
213
+ end
214
+ end
215
+
216
+ context "and both inline user and inline password are given" do
217
+ let(:inline_user) { "adefaultuser" }
218
+ let(:inline_password) { "inlinepassword4u" }
219
+ it "returns the decorated inline_user and inline password" do
220
+ expect(subject.call).to eq [inline_user, inline_password]
221
+ end
222
+ end
223
+ end
224
+
225
+ context "when only a default user is given" do
226
+ let(:default_user) { "defaultusername" }
227
+ let(:default_password) { nil }
228
+
229
+ context "and only an inline user is provided" do
230
+ let(:inline_user) { "aninlineuser" }
231
+ let(:inline_password) { nil }
232
+ it "returns the inline user with no password" do
233
+ expect(subject.call).to eq [inline_user, nil]
234
+ end
235
+ end
236
+
237
+ context "and only an inline password is provided" do
238
+ let(:inline_user) { nil }
239
+ let(:inline_password) { "inlinepassword4u" }
240
+ it "returns the default user and inline password" do
241
+ expect(subject.call).to eq [default_user, inline_password]
242
+ end
243
+ end
244
+
245
+ context "and neither inline user nor inline password is given" do
246
+ let(:inline_user) { nil }
247
+ let(:inline_password) { nil }
248
+ it "returns the default user" do
249
+ expect(subject.call).to eq [default_user, nil]
250
+ end
251
+ end
252
+
253
+ context "and both inline user and inline password are given" do
254
+ let(:inline_user) { "adefaultuser" }
255
+ let(:inline_password) { "inlinepassword4u" }
256
+ it "returns the decorated inline_user and inline password" do
257
+ expect(subject.call).to eq [inline_user, inline_password]
258
+ end
259
+ end
260
+ end
261
+
262
+ context "when only a default password is given" do
263
+ let(:default_user) { nil }
264
+ let(:default_password) { "ihasdefaultpassword" }
265
+
266
+ context "and only an inline user is provided" do
267
+ let(:inline_user) { "aninlineuser" }
268
+ let(:inline_password) { nil }
269
+ it "returns the decorated inline user and default password" do
270
+ expect(subject.call).to eq [inline_user, default_password]
271
+ end
272
+ end
273
+
274
+ context "and only an inline password is provided" do
275
+ let(:inline_user) { nil }
276
+ let(:inline_password) { "inlinepassword4u" }
277
+ it "returns nil user and inline password" do
278
+ expect(subject.call).to eq [nil, inline_password]
279
+ end
280
+ end
281
+
282
+ context "and neither inline user nor inline password is given" do
283
+ let(:inline_user) { nil }
284
+ let(:inline_password) { nil }
285
+ it "returns the nil user and default password" do
286
+ expect(subject.call).to eq [nil, default_password]
287
+ end
288
+ end
289
+
290
+ context "and both inline user and inline password are given" do
291
+ let(:inline_user) { "adefaultuser" }
292
+ let(:inline_password) { "inlinepassword4u" }
293
+ it "returns the inline_user and inline password" do
294
+ expect(subject.call).to eq [inline_user, inline_password]
295
+ end
296
+ end
297
+ end
298
+
299
+ context "when defaults for both user and password are given" do
300
+ let(:default_user) { "adefaultuser" }
301
+ let(:default_password) { "ihasdefaultpassword" }
302
+
303
+ context "and only an inline user is provided" do
304
+ let(:inline_user) { "aninlineuser" }
305
+ let(:inline_password) { nil }
306
+ it "returns the decorated inline user and default password" do
307
+ expect(subject.call).to eq [inline_user, default_password]
308
+ end
309
+ end
310
+
311
+ context "and only an inline password is provided" do
312
+ let(:inline_user) { nil }
313
+ let(:inline_password) { "inlinepassword4u" }
314
+ it "returns the decorated default user and inline password" do
315
+ expect(subject.call).to eq [default_user, inline_password]
316
+ end
317
+ end
318
+
319
+ context "and neither inline user nor inline password is given" do
320
+ let(:inline_user) { nil }
321
+ let(:inline_password) { nil }
322
+ it "returns the decorated default user and default password" do
323
+ expect(subject.call).to eq [default_user, default_password]
324
+ end
325
+ end
326
+
327
+ context "and both inline user and inline password are given" do
328
+ let(:inline_user) { "adefaultuser" }
329
+ let(:inline_password) { "inlinepassword4u" }
330
+ it "returns the decorated inline_user and inline password" do
331
+ expect(subject.call).to eq [inline_user, inline_password]
332
+ end
333
+ end
334
+ end
335
+ end
336
+
337
+ context "#config_for_target" do
338
+ { "example.com" => { password: nil, url: "ssh://example.com", user: nil },
339
+ "ssh://example.com" => { password: nil, url: "ssh://example.com", user: nil },
340
+ "ssh://user@example.com" => { password: nil, url: "ssh://example.com", user: "user" },
341
+ "ssh://user:password@example.com" => { password: "password", user: "user", url: "ssh://example.com" },
342
+ "ssh://user:pas:sw:ord@example.com" => { password: "pas:sw:ord", user: "user", url: "ssh://example.com" },
343
+ "ssh://user:!@#$%^&*()|\'\";:/?><.,{}[]+=`~@example.com" => { password: "!@#$%^&*()|\'\";:/?><.,{}[]+=`~", user: "user", url: "ssh://example.com" }
344
+ }.each do |values|
345
+ it "resolves #{values[0]} to #{values[1]}" do
346
+ expect(subject.config_for_target(values[0])).to eq values[1]
347
+ end
348
+ end
349
+
350
+ it "preserves range specifiers in the host portion while encoding in the password portion" do
351
+ input = "user:pas[1:2]!^@ho[a:b]s[t:z].com"
352
+ output = { password: "pas[1:2]!^", url: "ssh://ho[a:b]s[t:z].com", user: "user" }
353
+ expect(subject.config_for_target(input)).to eq output
354
+ end
355
+ end
356
+
357
+ context "#prefix_from_target" do
358
+ context "when no protocol is provided" do
359
+ let(:default_protocol) { "badproto" }
360
+ it "uses the default from configuration" do
361
+ expect(subject.prefix_from_target("host.com")).to eq %w{badproto:// host.com}
362
+ end
363
+ end
364
+
365
+ context "when protocol is provided" do
366
+ context "and it is valid" do
367
+ it "keeps the protocol" do
368
+ expect(subject.prefix_from_target("ssh://host.com")).to eq %w{ssh:// host.com}
369
+ end
370
+ end
371
+ context "and it is not valid" do
372
+ it "raises an error" do
373
+ expect { subject.prefix_from_target("bad://host.com") }.
374
+ to raise_error(ChefApply::TargetResolver::UnsupportedProtocol)
375
+ end
376
+ end
377
+ end
378
+ end
379
+
380
+ end