chef-apply 0.1.17 → 0.1.18

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.
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