right_agent 1.0.1 → 2.0.7

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 (67) hide show
  1. data/README.rdoc +10 -8
  2. data/Rakefile +31 -5
  3. data/lib/right_agent.rb +6 -1
  4. data/lib/right_agent/actor.rb +4 -20
  5. data/lib/right_agent/actors/agent_manager.rb +1 -1
  6. data/lib/right_agent/agent.rb +357 -144
  7. data/lib/right_agent/agent_config.rb +7 -6
  8. data/lib/right_agent/agent_identity.rb +13 -11
  9. data/lib/right_agent/agent_tag_manager.rb +60 -64
  10. data/{spec/results_mock.rb → lib/right_agent/clients.rb} +10 -24
  11. data/lib/right_agent/clients/api_client.rb +383 -0
  12. data/lib/right_agent/clients/auth_client.rb +247 -0
  13. data/lib/right_agent/clients/balanced_http_client.rb +369 -0
  14. data/lib/right_agent/clients/base_retry_client.rb +495 -0
  15. data/lib/right_agent/clients/right_http_client.rb +279 -0
  16. data/lib/right_agent/clients/router_client.rb +493 -0
  17. data/lib/right_agent/command/command_io.rb +4 -4
  18. data/lib/right_agent/command/command_parser.rb +2 -2
  19. data/lib/right_agent/command/command_runner.rb +1 -1
  20. data/lib/right_agent/connectivity_checker.rb +179 -0
  21. data/lib/right_agent/core_payload_types/secure_document_location.rb +2 -2
  22. data/lib/right_agent/dispatcher.rb +12 -10
  23. data/lib/right_agent/enrollment_result.rb +16 -12
  24. data/lib/right_agent/exceptions.rb +34 -20
  25. data/lib/right_agent/history.rb +10 -5
  26. data/lib/right_agent/log.rb +5 -5
  27. data/lib/right_agent/minimal.rb +1 -0
  28. data/lib/right_agent/multiplexer.rb +1 -1
  29. data/lib/right_agent/offline_handler.rb +270 -0
  30. data/lib/right_agent/packets.rb +7 -7
  31. data/lib/right_agent/payload_formatter.rb +1 -1
  32. data/lib/right_agent/pending_requests.rb +128 -0
  33. data/lib/right_agent/platform.rb +1 -1
  34. data/lib/right_agent/protocol_version_mixin.rb +69 -0
  35. data/lib/right_agent/{idempotent_request.rb → retryable_request.rb} +7 -7
  36. data/lib/right_agent/scripts/agent_controller.rb +28 -26
  37. data/lib/right_agent/scripts/agent_deployer.rb +37 -22
  38. data/lib/right_agent/scripts/common_parser.rb +10 -3
  39. data/lib/right_agent/secure_identity.rb +1 -1
  40. data/lib/right_agent/sender.rb +299 -785
  41. data/lib/right_agent/serialize/secure_serializer.rb +3 -1
  42. data/lib/right_agent/serialize/secure_serializer_initializer.rb +2 -2
  43. data/lib/right_agent/serialize/serializable.rb +8 -3
  44. data/right_agent.gemspec +49 -18
  45. data/spec/agent_config_spec.rb +7 -7
  46. data/spec/agent_identity_spec.rb +7 -4
  47. data/spec/agent_spec.rb +43 -7
  48. data/spec/agent_tag_manager_spec.rb +72 -83
  49. data/spec/clients/api_client_spec.rb +423 -0
  50. data/spec/clients/auth_client_spec.rb +272 -0
  51. data/spec/clients/balanced_http_client_spec.rb +576 -0
  52. data/spec/clients/base_retry_client_spec.rb +635 -0
  53. data/spec/clients/router_client_spec.rb +594 -0
  54. data/spec/clients/spec_helper.rb +111 -0
  55. data/spec/command/command_io_spec.rb +1 -1
  56. data/spec/command/command_parser_spec.rb +1 -1
  57. data/spec/connectivity_checker_spec.rb +83 -0
  58. data/spec/dispatcher_spec.rb +3 -2
  59. data/spec/enrollment_result_spec.rb +2 -2
  60. data/spec/history_spec.rb +51 -39
  61. data/spec/offline_handler_spec.rb +340 -0
  62. data/spec/pending_requests_spec.rb +136 -0
  63. data/spec/{idempotent_request_spec.rb → retryable_request_spec.rb} +73 -73
  64. data/spec/sender_spec.rb +835 -1052
  65. data/spec/serialize/secure_serializer_spec.rb +3 -2
  66. data/spec/spec_helper.rb +54 -1
  67. metadata +71 -12
@@ -0,0 +1,423 @@
1
+ # encoding: utf-8
2
+
3
+ #--
4
+ # Copyright (c) 2013 RightScale Inc
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining
7
+ # a copy of this software and associated documentation files (the
8
+ # "Software"), to deal in the Software without restriction, including
9
+ # without limitation the rights to use, copy, modify, merge, publish,
10
+ # distribute, sublicense, and/or sell copies of the Software, and to
11
+ # permit persons to whom the Software is furnished to do so, subject to
12
+ # the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+ #++
25
+
26
+ require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
27
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'lib', 'right_agent', 'clients', 'api_client'))
28
+
29
+ describe RightScale::ApiClient do
30
+
31
+ include FlexMock::ArgumentTypes
32
+
33
+ before(:each) do
34
+ @log = flexmock(RightScale::Log)
35
+ @log.should_receive(:error).by_default.and_return { |m| raise RightScale::Log.format(*m) }
36
+ @log.should_receive(:warning).by_default.and_return { |m| raise RightScale::Log.format(*m) }
37
+ @timer = flexmock("timer", :cancel => true, :interval= => 0).by_default
38
+ flexmock(EM::PeriodicTimer).should_receive(:new).and_return(@timer).and_yield.by_default
39
+ @account_id = 123
40
+ @agent_id = "rs-instance-1-1"
41
+ @agent_href = "/api/clouds/1/instances/1"
42
+ @agent_href2 = "/api/clouds/2/instances/2"
43
+ @links = {"links" => [{"rel" => "self", "href" => @agent_href}]}
44
+ @http_client = flexmock("http client", :get => @links, :check_health => true).by_default
45
+ flexmock(RightScale::BalancedHttpClient).should_receive(:new).and_return(@http_client).by_default
46
+ @url = "http://test.com"
47
+ @auth_header = {"Authorization" => "Bearer <session>"}
48
+ @auth_client = AuthClientMock.new(@url, @auth_header, :authorized, @account_id, @agent_id)
49
+ @options = {}
50
+ @client = RightScale::ApiClient.new(@auth_client, @options)
51
+ @version = RightScale::AgentConfig.protocol_version
52
+ @payload = {:agent_identity => @agent_id}
53
+ @target = nil
54
+ @token = "random token"
55
+ end
56
+
57
+ context :initialize do
58
+ it "initializes options" do
59
+ @options = {
60
+ :open_timeout => 1,
61
+ :request_timeout => 2,
62
+ :listen_timeout => 3,
63
+ :retry_timeout => 4,
64
+ :retry_intervals => [1, 2, 3],
65
+ :reconnect_interval => 5 }
66
+ @client = RightScale::ApiClient.new(@auth_client, @options)
67
+ options = @client.instance_variable_get(:@options)
68
+ options[:server_name] = "RightApi"
69
+ options[:api_version] = "1.5"
70
+ options[:open_timeout] = 1
71
+ options[:request_timeout] = 2
72
+ options[:retry_timeout] = 3
73
+ options[:retry_intervals] = [1, 2, 3]
74
+ options[:reconnect_interval] = 4
75
+ end
76
+ end
77
+
78
+ context :push do
79
+ it "makes mapped request" do
80
+ flexmock(@client).should_receive(:make_request).with(:post, "/audit_entries/111/append", {:detail => "details"},
81
+ "update_entry", @token, Hash).and_return(nil).once
82
+ @client.push("/auditor/update_entry", @payload.merge(:audit_id => 111, :detail => "details"), @target, @token).should be_nil
83
+ end
84
+
85
+ it "does not require token" do
86
+ flexmock(@client).should_receive(:make_request).with(:post, "/audit_entries/111/append", {:detail => "details"},
87
+ "update_entry", nil, Hash).and_return(nil).once
88
+ @client.push("/auditor/update_entry", @payload.merge(:audit_id => 111, :detail => "details"), @target).should be_nil
89
+ end
90
+ end
91
+
92
+ context :request do
93
+ it "makes mapped request" do
94
+ flexmock(@client).should_receive(:make_request).with(:post, "/right_net/booter/declare", {:r_s_version => @version},
95
+ "declare", @token, Hash).and_return(nil).once
96
+ @client.push("/booter/declare", @payload.merge(:r_s_version => @version), @target, @token).should be_nil
97
+ end
98
+
99
+ it "maps query_tags request" do
100
+ flexmock(@client).should_receive(:map_query_tags).with(:post, {:tags => ["a:b=c"]}, "query_tags", @token, Hash).
101
+ and_return({}).once
102
+ @client.request("/router/query_tags", @payload.merge(:tags => ["a:b=c"]), @target, @token).should == {}
103
+ end
104
+
105
+ it "does not require token" do
106
+ flexmock(@client).should_receive(:make_request).with(:post, "/right_net/booter/declare", {:r_s_version => @version},
107
+ "declare", nil, Hash).and_return(nil).once
108
+ @client.push("/booter/declare", @payload.merge(:r_s_version => @version), @target).should be_nil
109
+ end
110
+ end
111
+
112
+ context :support? do
113
+ it "returns true if request type is supported" do
114
+ @client.support?("/booter/declare").should be_true
115
+ end
116
+
117
+ it "returns false if request type is not supported" do
118
+ @client.support?("/instance_scheduler/execute").should be_false
119
+ end
120
+ end
121
+
122
+ context :map_request do
123
+ it "raises if request type not supported" do
124
+ lambda { @client.send(:map_request, "/instance_scheduler/execute", @payload, @token) }.should \
125
+ raise_error(ArgumentError, "Unsupported request type: /instance_scheduler/execute")
126
+ end
127
+
128
+ it "makes request" do
129
+ flexmock(@client).should_receive(:make_request).with(:post, "/right_net/booter/declare", {:r_s_version => @version},
130
+ "declare", @token, Hash).and_return(nil).once
131
+ @client.send(:map_request, "/booter/declare", @payload.merge(:r_s_version => @version), @token).should be_nil
132
+ end
133
+
134
+ it "returns mapped response" do
135
+ flexmock(@client).should_receive(:make_request).with(:post, "/audit_entries",
136
+ {:audit_entry => {:auditee_href => @agent_href, :summary => "summary"}}, "create_entry", @token, Hash).
137
+ and_return("/api/audit_entries/111").once
138
+ @client.send(:map_request, "/auditor/create_entry", @payload.merge(:summary => "summary"), @token).should == "111"
139
+ end
140
+ end
141
+
142
+ context :map_response do
143
+ it "converts audit entry href in result to an audit ID" do
144
+ response = "/api/audit_entries/111"
145
+ @client.send(:map_response, response, "/audit_entries").should == "111"
146
+ end
147
+
148
+ it "converts tag query result to list of tags for resource" do
149
+ response = [
150
+ { "actions" => [],
151
+ "links" => [{"rel" => "resource", "href" => "/api/clouds/6/instances/CUPVAL7KUP7TF"}],
152
+ "tags" => [{"name" => "rs_agent_dev:log_level=debug"},
153
+ {"name" => "rs_login:state=restricted"},
154
+ {"name" => "rs_monitoring:state=active"}] },
155
+ { "actions" => [],
156
+ "links" => [{"rel" => "resource", "href" => "/api/servers/20"}],
157
+ "tags" => [{"name" => "server:ready=now"},
158
+ {"name" => "rs_agent_dev:log_level=debug"}] } ]
159
+ @client.send(:map_response, response, "/tags/by_resource").should ==
160
+ { "/api/clouds/6/instances/CUPVAL7KUP7TF" => { "tags" => ["rs_agent_dev:log_level=debug",
161
+ "rs_login:state=restricted",
162
+ "rs_monitoring:state=active"] } }
163
+ end
164
+ end
165
+
166
+ context "query tags" do
167
+ before(:each) do
168
+ @action = "query_tags"
169
+ @options = {}
170
+ @tags = ["a:b=c"]
171
+ @hrefs = [@agent_href2]
172
+ @response = [
173
+ { "actions" => [],
174
+ "links" => [{"rel" => "resource", "href" => @agent_href}],
175
+ "tags" => [{"name" => "a:b=c"}, {"name" => "x:y=z"}] } ]
176
+ end
177
+
178
+ context :map_query_tags do
179
+ it "retrieves resource hrefs for specified tags" do
180
+ params = {:tags => @tags}
181
+ params2 = params.merge(:match_all => false, :resource_type => "instances")
182
+ flexmock(@client).should_receive(:query_by_resource).never
183
+ flexmock(@client).should_receive(:make_request).with(:post, "/tags/by_tag", params2, @action, @token, @options).and_return({}).once
184
+ @client.send(:map_query_tags, :post, params, @action, @token, @options).should == {}
185
+ end
186
+
187
+ it "appends retrieved hrefs to any specified resource hrefs" do
188
+ params = {:tags => @tags, :resource_hrefs => @hrefs}
189
+ params2 = {:resource_hrefs => [@agent_href2, @agent_href]}
190
+ flexmock(@client).should_receive(:query_by_tag).with(:post, @tags, @action, @token, @options).and_return([@agent_href]).once
191
+ flexmock(@client).should_receive(:make_request).with(:post, "/tags/by_resource", params2, @action, @token, @options).and_return({}).once
192
+ @client.send(:map_query_tags, :post, params, @action, @token, @options).should == {}
193
+ end
194
+
195
+ it "queries for tags for each resource href" do
196
+ params = {:resource_hrefs => @hrefs}
197
+ flexmock(@client).should_receive(:query_by_tag).never
198
+ flexmock(@client).should_receive(:make_request).with(:post, "/tags/by_resource", params, @action, @token, @options).and_return(@response).once
199
+ @client.send(:map_query_tags, :post, params, @action, @token, @options).should == {@agent_href => {"tags" => ["a:b=c", "x:y=z"]}}
200
+ end
201
+ end
202
+
203
+ context :query_by_tag do
204
+ before(:each) do
205
+ @params = {:tags => @tags}
206
+ @params2 = @params.merge(:match_all => false, :resource_type => "instances")
207
+ end
208
+
209
+ it "queries for tags using specified tags" do
210
+ flexmock(@client).should_receive(:make_request).with(:post, "/tags/by_tag", @params2, @action, @token, @options).and_return({}).once
211
+ @client.send(:query_by_tag, :post, @tags, @action, @token, @options).should == []
212
+ end
213
+
214
+ it "maps response" do
215
+ flexmock(@client).should_receive(:make_request).with(:post, "/tags/by_tag", @params2, @action, @token, @options).and_return(@response).once
216
+ @client.send(:query_by_tag, :post, @tags, @action, @token, @options).should == [@agent_href]
217
+ end
218
+ end
219
+
220
+ context :query_by_resource do
221
+ before(:each) do
222
+ @params = {:resource_hrefs => @hrefs}
223
+ end
224
+
225
+ it "queries for tags using specified hrefs" do
226
+ flexmock(@client).should_receive(:make_request).with(:post, "/tags/by_resource", @params, @action, @token, @options).and_return({}).once
227
+ @client.send(:query_by_resource, :post, @hrefs, @action, @token, @options).should == {}
228
+ end
229
+
230
+ it "maps response" do
231
+ flexmock(@client).should_receive(:make_request).with(:post, "/tags/by_resource", @params, @action, @token, @options).and_return(@response).once
232
+ @client.send(:query_by_resource, :post, @hrefs, @action, @token, @options).should == {@agent_href => {"tags" => ["a:b=c", "x:y=z"]}}
233
+ end
234
+ end
235
+ end
236
+
237
+ context :parameterize do
238
+
239
+ context "for audits" do
240
+ before(:each) do
241
+ @path, @params, @options = @client.send(:parameterize, "auditor", "update_entry", @payload.merge(:audit_id => 111,
242
+ :detail => "details"), "/audit_entries/:id/append")
243
+ end
244
+
245
+ it "converts audit parameters" do
246
+ @params.should == {:detail => "details"}
247
+ end
248
+
249
+ it "substitutes audit ID into path" do
250
+ @path.should == "/audit_entries/111/append"
251
+ end
252
+
253
+ it "adds parameter filter to options" do
254
+ @options.should == {:filter_params => ["detail", "text"]}
255
+ end
256
+ end
257
+
258
+ context "for tags" do
259
+ before(:each) do
260
+ tags = ["a:b=c", nil, [nil, "x:y=z"]]
261
+ @path, @params, @options = @client.send(:parameterize, "router", "add_tags", @payload.merge(:tags => tags),
262
+ "/tags/multi_add")
263
+ end
264
+
265
+ it "adds default resource href to parameters" do
266
+ @params[:resource_hrefs].should == [@agent_href]
267
+ end
268
+
269
+ it "adds specified resource hrefs to parameters" do
270
+ hrefs = [@agent_href, @agent_href2]
271
+ _, @params, _ = @client.send(:parameterize, "router", "query_tags", @payload.merge(:hrefs => hrefs),
272
+ "/tags/by_resource")
273
+ @params[:resource_hrefs].should == hrefs
274
+ end
275
+
276
+ it "ensures that tags parameter is properly formed" do
277
+ @params[:tags].should == ["a:b=c", "x:y=z"]
278
+ end
279
+
280
+ it "does not add tags if not present" do
281
+ _, @params, _ = @client.send(:parameterize, "router", "query_tags", @payload, "/tags/multi_add")
282
+ @params.should_not have_key(:tags)
283
+ end
284
+ end
285
+
286
+ context "otherwise" do
287
+ before(:each) do
288
+ @path, @params, @options = @client.send(:parameterize, "booter", "declare", @payload.merge(:r_s_version => @version),
289
+ "/right_net/booter/declare")
290
+ end
291
+
292
+ it "removes :agent_identity parameter" do
293
+ @params[:agent_identity].should be_nil
294
+ end
295
+ end
296
+ end
297
+
298
+ context :parameterize_audit do
299
+
300
+ context "create_entry" do
301
+ it "stores instance href" do
302
+ @client.send(:parameterize_audit, "create_entry", @payload)[:audit_entry][:auditee_href].should == @agent_href
303
+ end
304
+
305
+ context "summary" do
306
+ it "stores summary if non-blank" do
307
+ @client.send(:parameterize_audit, "create_entry", :summary => "hello")[:audit_entry][:summary].should == "hello"
308
+ @client.send(:parameterize_audit, "create_entry", :summary => "")[:audit_entry].should_not have_key(:summary)
309
+ @client.send(:parameterize_audit, "create_entry", {})[:audit_entry].should_not have_key(:summary)
310
+ end
311
+
312
+ it "truncates summary if too long" do
313
+ @client.send(:parameterize_audit, "create_entry", :summary => "hello " * 50)[:audit_entry][:summary].size.should == 255
314
+ end
315
+ end
316
+
317
+ it "stores detail if non-blank" do
318
+ @client.send(:parameterize_audit, "create_entry", :detail => "details")[:audit_entry][:detail].should == "details"
319
+ @client.send(:parameterize_audit, "create_entry", :detail => "")[:audit_entry].should_not have_key(:detail)
320
+ @client.send(:parameterize_audit, "create_entry", {})[:audit_entry].should_not have_key(:detail)
321
+ end
322
+
323
+ it "stores user email if non-blank" do
324
+ @client.send(:parameterize_audit, "create_entry", :user_email => "email")[:user_email].should == "email"
325
+ @client.send(:parameterize_audit, "create_entry", :user_email => "").should_not have_key(:user_email)
326
+ @client.send(:parameterize_audit, "create_entry", {}).should_not have_key(:user_email)
327
+ end
328
+
329
+ it "stores notify category if present" do
330
+ @client.send(:parameterize_audit, "create_entry", :category => "Notification")[:notify].should == "Notification"
331
+ @client.send(:parameterize_audit, "create_entry", {}).should_not have_key(:notify)
332
+ end
333
+ end
334
+
335
+ context "update_entry" do
336
+ it "stores offset if present" do
337
+ @client.send(:parameterize_audit, "update_entry", :offset => 100)[:offset].should == 100
338
+ @client.send(:parameterize_audit, "update_entry", {}).should_not have_key(:offset)
339
+ end
340
+
341
+ context "summary" do
342
+ it "stores summary if non-blank" do
343
+ @client.send(:parameterize_audit, "update_entry", :summary => "hello")[:summary].should == "hello"
344
+ @client.send(:parameterize_audit, "update_entry", :summary => "").should_not have_key(:summary)
345
+ @client.send(:parameterize_audit, "update_entry", {}).should_not have_key(:summary)
346
+ end
347
+
348
+ it "truncates summary if too long" do
349
+ @client.send(:parameterize_audit, "update_entry", :summary => "hello " * 50)[:summary].size.should == 255
350
+ end
351
+
352
+ it "stores notify category if present and summary is non-blank" do
353
+ @client.send(:parameterize_audit, "update_entry", :summary => "hello", :category => "Notification")[:notify].should == "Notification"
354
+ @client.send(:parameterize_audit, "update_entry", :category => "Notification").should_not have_key(:notify)
355
+ end
356
+ end
357
+
358
+ it "stores detail if non-blank" do
359
+ @client.send(:parameterize_audit, "update_entry", :detail => "details")[:detail].should == "details"
360
+ @client.send(:parameterize_audit, "update_entry", :detail => "").should_not have_key(:detail)
361
+ @client.send(:parameterize_audit, "update_entry", {}).should_not have_key(:detail)
362
+ end
363
+ end
364
+
365
+ context "otherwise" do
366
+ it "raises unknown audit" do
367
+ lambda { @client.send(:parameterize_audit, "bogus", {}) }.should raise_error(ArgumentError)
368
+ end
369
+ end
370
+ end
371
+
372
+ context :truncate do
373
+ it "returns non-string as is" do
374
+ @client.send(:truncate, nil, 5).should == nil
375
+ end
376
+
377
+ it "returns strings shorter than limit as is" do
378
+ @client.send(:truncate, "hello", 5).should == "hello"
379
+ end
380
+
381
+ it "requires max length to be greater than 3" do
382
+ @client.send(:truncate, "hello", 4).should == "h..."
383
+ lambda { @client.send(:truncate, "hello", 3) }.should raise_error(ArgumentError)
384
+ end
385
+
386
+ it "truncates strings that are too long" do
387
+ @client.send(:truncate, "how you doing", 10).bytesize.should == 10
388
+ end
389
+
390
+ it "ends truncated string with an ellipsis" do
391
+ @client.send(:truncate, "how you doing", 10).should == "how you..."
392
+ end
393
+
394
+ it "accounts for multi-byte characters" do
395
+ @client.send(:truncate, "Schös Tägli wünschi", 20).should == "Schös Tägli wü..."
396
+ @client.send(:truncate, "Schös Tägli wünschi", 19).should == "Schös Tägli w..."
397
+ @client.send(:truncate, "Schös Tägli wünschi", 18).should == "Schös Tägli w..."
398
+ @client.send(:truncate, "Schös Tägli wünschi", 17).should == "Schös Tägli ..."
399
+ end
400
+ end
401
+
402
+ context :non_blank do
403
+ it "returns nil if value nil" do
404
+ @client.send(:non_blank, nil).should be_nil
405
+ end
406
+
407
+ it "returns nil if value is empty" do
408
+ @client.send(:non_blank, "").should be_nil
409
+ end
410
+
411
+ it "returns nil if value nil" do
412
+ @client.send(:non_blank, "hello").should == "hello"
413
+ end
414
+ end
415
+
416
+ context :enable_use do
417
+ it "makes API request to get links for setting instance href" do
418
+ flexmock(@client).should_receive(:make_request).with(:get, "/sessions/instance", {}, "instance").and_return(@links).once
419
+ @client.instance_variable_get(:@self_href).should == @agent_href
420
+ @client.send(:enable_use).should be_true
421
+ end
422
+ end
423
+ end
@@ -0,0 +1,272 @@
1
+ #--
2
+ # Copyright (c) 2013 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
25
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'lib', 'right_agent', 'clients', 'auth_client'))
26
+
27
+ class AuthClientTest < RightScale::AuthClient
28
+ def initialize(options = {})
29
+ @identity = options[:identity]
30
+ @api_url = options[:api_url]
31
+ @router_url = options[:router_url]
32
+ @account_id = options[:account_id]
33
+ @mode = options[:mode]
34
+ @access_token = options[:access_token]
35
+ @state = :pending
36
+ @status_callbacks = []
37
+ reset_stats
38
+ end
39
+ end
40
+
41
+ describe RightScale::AuthClient do
42
+
43
+ include FlexMock::ArgumentTypes
44
+
45
+ before(:each) do
46
+ @log = flexmock(RightScale::Log)
47
+ @log.should_receive(:error).by_default.and_return { |m| raise RightScale::Log.format(*m) }
48
+ @log.should_receive(:warning).by_default.and_return { |m| raise RightScale::Log.format(*m) }
49
+ @identity = "rs-agent-1-1"
50
+ @api_url = "http://api.com"
51
+ @router_url = "http://router.com"
52
+ @options = {
53
+ :identity => @identity,
54
+ :api_url => @api_url,
55
+ :router_url => @router_url,
56
+ :account_id => 123,
57
+ :mode => :http,
58
+ :access_token => "<test access token>" }
59
+ @client = AuthClientTest.new(@options)
60
+ end
61
+
62
+ context :initialize do
63
+ it "raises if abstract class" do
64
+ lambda { RightScale::AuthClient.new }.should raise_error(NotImplementedError, "RightScale::AuthClient is an abstract class")
65
+ end
66
+
67
+ it "does not raise for derived class" do
68
+ AuthClientTest.new
69
+ end
70
+ end
71
+
72
+ context :identity do
73
+ it "returns identity" do
74
+ @client.identity.should == @identity
75
+ end
76
+ end
77
+
78
+ context :headers do
79
+ it "raises if not authorized" do
80
+ lambda { @client.headers }.should raise_error(RightScale::Exceptions::Unauthorized)
81
+ end
82
+
83
+ it "returns headers" do
84
+ @client.send(:state=, :authorized)
85
+ @client.headers.should == {"Authorization" => "Bearer <test access token>"}
86
+ end
87
+ end
88
+
89
+ context :auth_header do
90
+ it "raises if not authorized" do
91
+ lambda { @client.auth_header }.should raise_error(RightScale::Exceptions::Unauthorized)
92
+ end
93
+
94
+ it "returns authorization header" do
95
+ @client.send(:state=, :authorized)
96
+ @client.auth_header.should == {"Authorization" => "Bearer <test access token>"}
97
+ end
98
+ end
99
+
100
+ context :account_id do
101
+ it "raises if not authorized" do
102
+ lambda { @client.account_id }.should raise_error(RightScale::Exceptions::Unauthorized)
103
+ end
104
+
105
+ it "returns auth header" do
106
+ @client.send(:state=, :authorized)
107
+ @client.account_id.should == 123
108
+ end
109
+ end
110
+
111
+ context :api_url do
112
+ it "raises if not authorized" do
113
+ lambda { @client.api_url }.should raise_error(RightScale::Exceptions::Unauthorized)
114
+ end
115
+
116
+ it "returns auth header" do
117
+ @client.send(:state=, :authorized)
118
+ @client.api_url.should == @api_url
119
+ end
120
+ end
121
+
122
+ context :router_url do
123
+ it "raises if not authorized" do
124
+ lambda { @client.router_url }.should raise_error(RightScale::Exceptions::Unauthorized)
125
+ end
126
+
127
+ it "returns auth header" do
128
+ @client.send(:state=, :authorized)
129
+ @client.router_url.should == @router_url
130
+ end
131
+ end
132
+
133
+ context :mode do
134
+ it "returns mode" do
135
+ @client.mode.should == :http
136
+ end
137
+ end
138
+
139
+ context :expired do
140
+ it "logs that authorization expired" do
141
+ @log.should_receive(:info).with(/Renewing authorization/).once
142
+ @client.send(:state=, :authorized)
143
+ @client.expired.should be_true
144
+ end
145
+
146
+ it "sets state to :expired" do
147
+ @client.send(:state=, :authorized)
148
+ @client.expired.should be_true
149
+ @client.state.should == :expired
150
+ end
151
+
152
+ it "should renew authorization" do
153
+ @client.send(:state=, :authorized)
154
+ flexmock(@client).should_receive(:renew_authorization).once
155
+ @client.expired.should be_true
156
+ end
157
+ end
158
+
159
+ context :redirect do
160
+ it "handles redirect" do
161
+ @client.redirect("location").should be_true
162
+ end
163
+ end
164
+
165
+ context :close do
166
+ it "should set state to :closed" do
167
+ @client.close.should be_true
168
+ @client.state.should == :closed
169
+ end
170
+ end
171
+
172
+ context :status do
173
+ it "stores callback" do
174
+ callback = lambda { |_, _| }
175
+ @client.instance_variable_get(:@status_callbacks).size.should == 0
176
+ @client.status(&callback)
177
+ @client.instance_variable_get(:@status_callbacks).size.should == 1
178
+ @client.instance_variable_get(:@status_callbacks)[0].should == callback
179
+ end
180
+
181
+ it "treats callback as optional" do
182
+ @client.instance_variable_get(:@status_callbacks).size.should == 0
183
+ @client.status
184
+ @client.instance_variable_get(:@status_callbacks).size.should == 0
185
+ end
186
+
187
+ it "returns current state" do
188
+ @client.status.should == :pending
189
+ end
190
+ end
191
+
192
+ context :check_authorized do
193
+ it "raises retryable error if state is :expired" do
194
+ @client.send(:state=, :authorized)
195
+ @client.send(:expired)
196
+ lambda { @client.send(:check_authorized) }.should raise_error(RightScale::Exceptions::RetryableError, "Authorization expired")
197
+ end
198
+
199
+ it "raises unauthorized if state is not :authorized" do
200
+ lambda { @client.send(:check_authorized) }.should raise_error(RightScale::Exceptions::Unauthorized, "Not authorized with RightScale")
201
+ end
202
+
203
+ it "returns true if authorized" do
204
+ @client.send(:state=, :authorized)
205
+ @client.send(:check_authorized).should be_true
206
+ end
207
+ end
208
+
209
+ context :renew_authorization do
210
+ it "renews authorization" do
211
+ @client.send(:renew_authorization).should be_true
212
+ end
213
+
214
+ it "waits to renew authorization" do
215
+ @client.send(:renew_authorization, 10).should be_true
216
+ end
217
+ end
218
+
219
+ context :state= do
220
+ it "raises exception if state transition is invalid" do
221
+ lambda { @client.send(:state=, :expired) }.should raise_error(ArgumentError, "Invalid state transition: :pending -> :expired")
222
+ end
223
+
224
+ [:pending, :closed].each do |state|
225
+ context state do
226
+ it "stores new state" do
227
+ @client.send(:state=, state)
228
+ @client.state.should == state
229
+ end
230
+ end
231
+ end
232
+
233
+ [:authorized, :unauthorized, :expired, :failed].each do |state|
234
+ context state do
235
+ before(:each) do
236
+ @client.send(:state=, :authorized) if state == :expired
237
+ end
238
+
239
+ it "stores new state" do
240
+ @client.send(:state=, state)
241
+ @client.state.should == state
242
+ end
243
+
244
+ it "stores new state" do
245
+ @client.send(:state=, state).should == state
246
+ end
247
+
248
+ context "when callbacks" do
249
+ it "makes callbacks with new state" do
250
+ callback_type = callback_state = nil
251
+ @client.status { |t, s| callback_type = t; callback_state = s }
252
+ @client.send(:state=, state)
253
+ callback_type.should == :auth
254
+ callback_state.should == state
255
+ end
256
+
257
+ it "log error if callback fails" do
258
+ @log.should_receive(:error).with("Failed status callback", StandardError).once
259
+ @client.status { |t, s| raise StandardError, "test" }
260
+ @client.send(:state=, state).should == state
261
+ end
262
+ end
263
+
264
+ it "does nothing if current state is the same" do
265
+ flexmock(@client.instance_variable_get(:@stats)["state"]).should_receive(:update).once
266
+ @client.send(:state=, state)
267
+ @client.send(:state=, state).should == state
268
+ end
269
+ end
270
+ end
271
+ end
272
+ end