right_cloud_api_base 0.1.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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/HISTORY +2 -0
  3. data/LICENSE +19 -0
  4. data/README.md +14 -0
  5. data/Rakefile +37 -0
  6. data/lib/base/api_manager.rb +707 -0
  7. data/lib/base/helpers/cloud_api_logger.rb +214 -0
  8. data/lib/base/helpers/http_headers.rb +239 -0
  9. data/lib/base/helpers/http_parent.rb +103 -0
  10. data/lib/base/helpers/http_request.rb +173 -0
  11. data/lib/base/helpers/http_response.rb +122 -0
  12. data/lib/base/helpers/net_http_patch.rb +31 -0
  13. data/lib/base/helpers/query_api_patterns.rb +862 -0
  14. data/lib/base/helpers/support.rb +270 -0
  15. data/lib/base/helpers/support.xml.rb +306 -0
  16. data/lib/base/helpers/utils.rb +380 -0
  17. data/lib/base/manager.rb +122 -0
  18. data/lib/base/parsers/json.rb +38 -0
  19. data/lib/base/parsers/plain.rb +36 -0
  20. data/lib/base/parsers/rexml.rb +83 -0
  21. data/lib/base/parsers/sax.rb +200 -0
  22. data/lib/base/routines/cache_validator.rb +184 -0
  23. data/lib/base/routines/connection_proxies/net_http_persistent_proxy.rb +194 -0
  24. data/lib/base/routines/connection_proxies/right_http_connection_proxy.rb +224 -0
  25. data/lib/base/routines/connection_proxy.rb +66 -0
  26. data/lib/base/routines/request_analyzer.rb +122 -0
  27. data/lib/base/routines/request_generator.rb +48 -0
  28. data/lib/base/routines/request_initializer.rb +52 -0
  29. data/lib/base/routines/response_analyzer.rb +152 -0
  30. data/lib/base/routines/response_parser.rb +79 -0
  31. data/lib/base/routines/result_wrapper.rb +75 -0
  32. data/lib/base/routines/retry_manager.rb +106 -0
  33. data/lib/base/routines/routine.rb +98 -0
  34. data/lib/right_cloud_api_base.rb +72 -0
  35. data/lib/right_cloud_api_base_version.rb +37 -0
  36. data/right_cloud_api_base.gemspec +63 -0
  37. data/spec/helpers/query_api_pattern_spec.rb +312 -0
  38. data/spec/helpers/support_spec.rb +211 -0
  39. data/spec/helpers/support_xml_spec.rb +207 -0
  40. data/spec/helpers/utils_spec.rb +179 -0
  41. data/spec/routines/connection_proxies/test_net_http_persistent_proxy_spec.rb +143 -0
  42. data/spec/routines/test_cache_validator_spec.rb +152 -0
  43. data/spec/routines/test_connection_proxy_spec.rb +44 -0
  44. data/spec/routines/test_request_analyzer_spec.rb +106 -0
  45. data/spec/routines/test_response_analyzer_spec.rb +132 -0
  46. data/spec/routines/test_response_parser_spec.rb +228 -0
  47. data/spec/routines/test_result_wrapper_spec.rb +63 -0
  48. data/spec/routines/test_retry_manager_spec.rb +84 -0
  49. data/spec/spec_helper.rb +15 -0
  50. metadata +215 -0
@@ -0,0 +1,207 @@
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 NONINFRINGEMENT.
18
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ require File.expand_path(File.dirname(__FILE__)) + "/../spec_helper"
25
+
26
+ describe "support_xml.rb" do
27
+
28
+ # --- Object ---
29
+
30
+ context "Object#_xml_escale" do
31
+ it "escapes non-xml symbols" do
32
+ expect("Hello <'world'> & \"the Universe\""._xml_escape).to eq(
33
+ "Hello &lt;&apos;world&apos;&gt; &amp; &quot;the Universe&quot;"
34
+ )
35
+ end
36
+ end
37
+
38
+ context "Object#_xml_unescale" do
39
+ it "unescapes non-xml symbols" do
40
+ expect("Hello &lt;&apos;world&apos;&gt; &amp; &quot;the Universe&quot;"._xml_unescape).to eq(
41
+ "Hello <'world'> & \"the Universe\""
42
+ )
43
+ end
44
+ end
45
+
46
+ context "Object#_to_xml" do
47
+ it "returns a simple XML string" do
48
+ expect("hahaha"._to_xml).to eq 'hahaha'
49
+ end
50
+ end
51
+
52
+ # --- Array ---
53
+
54
+ context "Aray#_to_xml" do
55
+ it "builds a one-line XML by default" do
56
+ expect(['a', 2, [3, 4], 'string', :symbol, { 3 => 4 }, { 5 => { '@i:type' => '13', '@@text' => 555 }}].\
57
+ _to_xml).to eq(
58
+ "<item>a</item><item>2</item><item><item>3</item><item>4</item></item>" +
59
+ "<item>string</item><item>symbol</item><item><3>4</3></item><item><5 i:type=\"13\">555</5></item>"
60
+ )
61
+ end
62
+ it "builds a multi-line XML when :indent is set" do
63
+ expect(['a', 2, [3, 4], 'string', :symbol,
64
+ { 3 => 4 }, { 5 => { '@i:type' => '13', '@@text' => 555 }}]._to_xml(:tag => 'item', :indent => ' ')).to eq(
65
+ "<item>a</item>\n" +
66
+ "<item>2</item>\n" +
67
+ "<item>\n" +
68
+ " <item>3</item>\n" +
69
+ " <item>4</item>\n" +
70
+ "</item>\n" +
71
+ "<item>string</item>\n" +
72
+ "<item>symbol</item>\n" +
73
+ "<item>\n" +
74
+ " <3>4</3>\n" +
75
+ "</item>\n" +
76
+ "<item>\n" +
77
+ " <5 i:type=\"13\">555</5>\n" +
78
+ "</item>\n"
79
+ )
80
+ end
81
+ end
82
+
83
+ # --- Hash ---
84
+
85
+ context "Hash#_to_xml" do
86
+
87
+ it "builds a simple XML from a single key hash" do
88
+ expect(({ 'a' => [ 1, { :c => 'd' } ] })._to_xml).to eq "<a>1</a><a><c>d</c></a>"
89
+ end
90
+
91
+ it "understands attributes as keys starting with @ and text defined as @@text" do
92
+ expect({ 'screen' => { '@width' => 1080, '@@text' => 'HD' } }._to_xml).to eq(
93
+ "<screen width=\"1080\">HD</screen>"
94
+ )
95
+ end
96
+
97
+ # Ruby 1.8 keeps hash keys in unpredictable order and the order on the tags
98
+ # in the resulting XMl is also not easy to predict.
99
+ if RUBY_VERSION >= '1.9'
100
+
101
+ it "builds a one-line hash by default" do
102
+ expect({ 'a' => 2, :b => [1, 3, 4, { :c => { 'd' => 'something' } } ], 5 => { '@i:type' => '13', '@@text' => 555 } }._to_xml).to eq(
103
+ '<a>2</a><b>1</b><b>3</b><b>4</b><b><c><d>something</d></c></b><5 i:type="13">555</5>'
104
+ )
105
+ end
106
+ it "builds a multi-line hash when :indent is set" do
107
+ expect({ 'a' => 2, :b => [1, 3, 4, { :c => { 'd' => 'something' } } ] }._to_xml(:indent => ' ')).to eq(
108
+ "<a>2</a>" + "\n" +
109
+ "<b>1</b>" + "\n" +
110
+ "<b>3</b>" + "\n" +
111
+ "<b>4</b>" + "\n" +
112
+ "<b>" + "\n" +
113
+ " <c>" + "\n" +
114
+ " <d>something</d>" + "\n" +
115
+ " </c>" + "\n" +
116
+ "</b>" + "\n"
117
+ )
118
+ end
119
+
120
+ it "understands attributes as keys starting with @ and text defined as @@text (more complex "+
121
+ "example for ruby 1.9)" do
122
+ expect({ 'screen' => {
123
+ '@width' => 1080,
124
+ '@hight' => 720,
125
+ '@@text' => 'HD',
126
+ 'color' => {
127
+ '@max-colors' => 65535,
128
+ '@dinamic-resolution' => '1:1000000',
129
+ '@@text' => '<"PAL">',
130
+ 'brightness' => {
131
+ 'bright' => true
132
+ }
133
+ }
134
+ }
135
+ }._to_xml(:indent => ' ', :escape => true)).to eq(
136
+ "<screen width=\"1080\" hight=\"720\">" + "\n" +
137
+ " HD" + "\n" +
138
+ " <color max-colors=\"65535\" dinamic-resolution=\"1:1000000\">" + "\n" +
139
+ " &lt;&quot;PAL&quot;&gt;" + "\n" +
140
+ " <brightness>" + "\n" +
141
+ " <bright>true</bright>" + "\n" +
142
+ " </brightness>" + "\n" +
143
+ " </color>" + "\n" +
144
+ "</screen>" + "\n"
145
+ )
146
+ end
147
+
148
+ end
149
+
150
+ it "can mix ordering ID into Strings" do
151
+ key1 = Hash::_order('my-item')
152
+ key2 = Hash::_order('my-item')
153
+
154
+ expect(!!key1[Hash::RIGHTXMLSUPPORT_SORTORDERREGEXP]).to be true
155
+ expect(!!key2[Hash::RIGHTXMLSUPPORT_SORTORDERREGEXP]).to be true
156
+
157
+ expect(key1).to be < key2
158
+ end
159
+
160
+ it "XML-text has all the keys sorted accordingly to the given order" do
161
+ Hash::instance_variable_set('@_next_ordered_key_id', 0)
162
+ hash = {
163
+ Hash::_order('foo') => 34,
164
+ Hash::_order('boo') => 45,
165
+ Hash::_order('zoo') => 53,
166
+ Hash::_order('poo') => 10,
167
+ Hash::_order('moo') => {
168
+ Hash::_order('noo') => 101,
169
+ Hash::_order('too') => 113,
170
+ Hash::_order('koo') => 102,
171
+ },
172
+ Hash::_order('woo') => 03,
173
+ Hash::_order('hoo') => 1
174
+ }
175
+
176
+ expect(hash).to eq(
177
+ "foo{#1}" => 34,
178
+ "boo{#2}" => 45,
179
+ "zoo{#3}" => 53,
180
+ "poo{#4}" => 10,
181
+ "moo{#5}" => {
182
+ "noo{#6}" => 101,
183
+ "too{#7}" => 113,
184
+ "koo{#8}" => 102,
185
+ },
186
+ "woo{#9}" => 3,
187
+ "hoo{#10}" => 1
188
+ )
189
+
190
+ expect(hash._to_xml(:indent => ' ')).to eq(
191
+ "<foo>34</foo>" + "\n" +
192
+ "<boo>45</boo>" + "\n" +
193
+ "<zoo>53</zoo>" + "\n" +
194
+ "<poo>10</poo>" + "\n" +
195
+ "<moo>" + "\n" +
196
+ " <noo>101</noo>" + "\n" +
197
+ " <too>113</too>" + "\n" +
198
+ " <koo>102</koo>" + "\n" +
199
+ "</moo>" + "\n" +
200
+ "<woo>3</woo>" + "\n" +
201
+ "<hoo>1</hoo>" + "\n"
202
+ )
203
+ end
204
+
205
+ end
206
+
207
+ end
@@ -0,0 +1,179 @@
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 NONINFRINGEMENT.
18
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ describe "Utils" do
25
+ context "RightScale::CloudApi::Utils" do
26
+
27
+ context "self.url_encode" do
28
+ it "uses CGI::escape to escape" do
29
+ str = 'hahaha'
30
+ expect(CGI).to receive(:escape).once.and_return(str)
31
+ RightScale::CloudApi::Utils.url_encode(str)
32
+ end
33
+ it "replaces spaces with '%20'" do
34
+ expect(RightScale::CloudApi::Utils.url_encode('ha ha ha')).to eq 'ha%20ha%20ha'
35
+ end
36
+ end
37
+
38
+ context "self.params_to_urn" do
39
+ it "converts a Hash into a string" do
40
+ expect(RightScale::CloudApi::Utils.params_to_urn('a' => 'b', 'c' => '', 'd' => nil)).to eq 'a=b&c=&d'
41
+ end
42
+ it "auto escapes values" do
43
+ expect(RightScale::CloudApi::Utils.params_to_urn('a' => 'ha ha')).to eq 'a=ha%20%20ha'
44
+ end
45
+ it "uses a provided block to escape values" do
46
+ expect(RightScale::CloudApi::Utils.params_to_urn('a' => 'ha ha'){|val| val.gsub(' ','-') }).to eq 'a=ha--ha'
47
+ end
48
+ end
49
+
50
+ context "self.join_urn" do
51
+ it "joins pathes" do
52
+ expect(RightScale::CloudApi::Utils.join_urn('/first', 'second', 'third')).to eq '/first/second/third'
53
+ end
54
+ it "knows how to deal with empty pathes or slashes" do
55
+ expect(RightScale::CloudApi::Utils.join_urn('/first', '', '1/', '1.1/', 'second', 'third')).to eq '/first/1/1.1/second/third'
56
+ end
57
+ it "drops strips left when it sees a path starting with forward slash (root sign)" do
58
+ expect(RightScale::CloudApi::Utils.join_urn('/first', '', '1/', '1.1/', '/second', 'third')).to eq '/second/third'
59
+ end
60
+ it "adds URL params" do
61
+ expect(RightScale::CloudApi::Utils.join_urn('/first','second', {'a' => 'b', 'c' => '', 'd' => nil})).to eq "/first/second?a=b&c=&d"
62
+ end
63
+
64
+ context "self.extract_url_params" do
65
+ end
66
+
67
+ context "self.pattern_matches?" do
68
+ it "returns a blank Hash when there are no any params in the provided URL" do
69
+ expect(RightScale::CloudApi::Utils.extract_url_params('https://ec2.amazonaws.com')).to eq({})
70
+ end
71
+ it "returns parsed URL params when they are in the provided URL" do
72
+ expect(RightScale::CloudApi::Utils.extract_url_params('https://ec2.amazonaws.com/?w=1&x=3&y&z')).to eq(
73
+ {"z"=>nil, "y"=>nil, "x"=>"3", "w"=>"1"}
74
+ )
75
+ end
76
+ end
77
+
78
+ context "self.contentify_body" do
79
+ before(:each) do
80
+ @body = { '1' => '2' }
81
+ end
82
+ it "returns JSON when content type says it should be json" do
83
+ expect(RightScale::CloudApi::Utils.contentify_body(@body,'json')).to eq '{"1":"2"}'
84
+ end
85
+ it "returns XML when content type says it should be json" do
86
+ expect(RightScale::CloudApi::Utils.contentify_body(@body,'xml')).to eq "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<1>2</1>"
87
+ end
88
+ it "fails if there is an unsupported content-type" do
89
+ expect { RightScale::CloudApi::Utils.contentify_body(@body,'unsupported-smething') }.to raise_error(RightScale::CloudApi::Error)
90
+ end
91
+ end
92
+
93
+ context "self.generate_token" do
94
+ context "UUID" do
95
+ before(:each) do
96
+ @expectation = 'something-random-from-UUID.generate'
97
+ UUID = double('UUID', :new => double(:generate => @expectation))
98
+ end
99
+ it "uses UUID when UUID is loaded" do
100
+ RightScale::CloudApi::Utils.generate_token == @expectation
101
+ end
102
+ end
103
+
104
+ context "self.random" do
105
+ before(:each) do
106
+ @expectation = 'something-random-from-self.random'
107
+ UUID = double('UUID', :new => double(:generate => @expectation))
108
+ expect(UUID).to receive(:respond_to?).with(:new).and_return(false)
109
+ end
110
+ it "uses self.random when UUID is not loaded" do
111
+ expect(RightScale::CloudApi::Utils).to receive(:random).and_return(@expectation)
112
+ RightScale::CloudApi::Utils.generate_token == @expectation
113
+ end
114
+ end
115
+ end
116
+
117
+ context "self.random" do
118
+ it "generates a single random HEX digit by default" do
119
+ expect(RightScale::CloudApi::Utils.random[/^[0-9a-f]{1}$/]).to_not be(nil)
120
+ end
121
+ it "generates 'size' random HEX digits when size is set" do
122
+ expect(RightScale::CloudApi::Utils.random(13)[/^[0-9a-f]{13}$/]).to_not be(nil)
123
+ end
124
+ it "generates random decimal digits when :base is 10" do
125
+ expect(RightScale::CloudApi::Utils.random(13, :base => 10)[/^[0-9]{13}$/]).to_not be(nil)
126
+ end
127
+ it "generates random alpha symbols when :base is 26 and :offset is 10" do
128
+ expect(RightScale::CloudApi::Utils.random(13, :base => 26, :offset => 10)[/^[a-z]{13}$/]).to_not be(nil)
129
+ end
130
+ end
131
+
132
+ context "self.arrayify" do
133
+ it "does not change Array instances" do
134
+ expect(RightScale::CloudApi::Utils.arrayify([])).to eq []
135
+ expect(RightScale::CloudApi::Utils.arrayify([1,2,3])).to eq([1,2,3])
136
+ end
137
+ it "wraps all the other objects into Array" do
138
+ expect(RightScale::CloudApi::Utils.arrayify(nil)).to eq [nil]
139
+ expect(RightScale::CloudApi::Utils.arrayify(1)).to eq [1]
140
+ expect(RightScale::CloudApi::Utils.arrayify('something')).to eq ['something']
141
+ expect(RightScale::CloudApi::Utils.arrayify({1=>2})).to eq( [{1=>2}])
142
+ end
143
+ end
144
+
145
+ context "self.dearrayify" do
146
+ it "returns input if the input is not an Array instance" do
147
+ expect(RightScale::CloudApi::Utils.dearrayify(nil)).to be(nil)
148
+ expect(RightScale::CloudApi::Utils.dearrayify(1)).to eq 1
149
+ expect(RightScale::CloudApi::Utils.dearrayify('something')).to eq 'something'
150
+ expect(RightScale::CloudApi::Utils.dearrayify({1=>2})).to eq({1=>2})
151
+ end
152
+ it "returns the first element of the input if the input is an Array instance" do
153
+ expect(RightScale::CloudApi::Utils.dearrayify([])).to be(nil)
154
+ expect(RightScale::CloudApi::Utils.dearrayify([1])).to eq 1
155
+ expect(RightScale::CloudApi::Utils.dearrayify([1,2,3])).to eq 1
156
+ end
157
+ end
158
+
159
+ context "self.get_xml_parser_class" do
160
+ it "returns RightScale::CloudApiParser::Sax by default" do
161
+ expect(RightScale::CloudApi::Utils.get_xml_parser_class(nil)).to eq RightScale::CloudApi::Parser::Sax
162
+ end
163
+ it "returns RightScale::CloudApiParser::Sax by its name" do
164
+ expect(RightScale::CloudApi::Utils.get_xml_parser_class('sax')).to eq RightScale::CloudApi::Parser::Sax
165
+ end
166
+ it "returns RightScale::CloudApiParser::ReXml by its name" do
167
+ expect(RightScale::CloudApi::Utils.get_xml_parser_class('rexml')).to eq RightScale::CloudApi::Parser::ReXml
168
+ end
169
+ it "fails when an unknown parser is requested" do
170
+ expect { RightScale::CloudApi::Utils.get_xml_parser_class('something-unknown') }.to raise_error(RightScale::CloudApi::Error)
171
+ end
172
+ end
173
+
174
+ context "self.inheritance_chain" do
175
+ end
176
+
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,143 @@
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 NONINFRINGEMENT.
18
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ require File.expand_path(File.dirname(__FILE__)) + "/../../spec_helper"
25
+ require "net/http/persistent"
26
+
27
+ describe "RightScale::CloudApi::ConnectionProxy::NetHTTPPersistentProxy" do
28
+ before(:each) do
29
+ logger = Logger.new(STDOUT)
30
+ logger.level = Logger::INFO
31
+ @proxy = RightScale::CloudApi::ConnectionProxy::NetHttpPersistentProxy.new
32
+ @uri = double(:host => 'host.com',
33
+ :port => '777',
34
+ :scheme => 'scheme',
35
+ :dup => @uri)
36
+ @test_data = {
37
+ :options => {:user_agent => 'user_agent_data',
38
+ :connection_retry_count => 0,
39
+ :cloud_api_logger => RightScale::CloudApi::CloudApiLogger.new({:logger => logger})},
40
+ :credentials => {},
41
+ :callbacks => {},
42
+ :vars => {:system => {:block => 'block'}},
43
+ :request => {:instance => double(:verb => 'get',
44
+ :path => 'some/path',
45
+ :body => 'body',
46
+ :is_io? => false,
47
+ :headers => {'header1' => 'val1',
48
+ 'header2' => 'val2'},
49
+ :raw= => nil)},
50
+ :connection => {:uri => @uri}
51
+ }
52
+ @response = double(:code => '200', :body => 'body', :to_hash => {:code => '200', :body => 'body'})
53
+ end
54
+
55
+
56
+ context "when request succeeds" do
57
+ before :each do
58
+ @connection = double(
59
+ :request => @response,
60
+ :retry_change_requests= => true )
61
+ expect(Net::HTTP::Persistent).to receive(:new).and_return(@connection)
62
+ end
63
+
64
+ it "works" do
65
+ @proxy.request(@test_data)
66
+ end
67
+ end
68
+
69
+
70
+ context "when there is a connection issue" do
71
+ before :each do
72
+ @connection = double( :retry_change_requests= => true )
73
+ expect(Net::HTTP::Persistent).to receive(:new).and_return(@connection)
74
+ # failure in the connection should finish and reraise the error
75
+ expect(@connection).to receive(:request).and_raise(StandardError.new("Banana"))
76
+ expect(@connection).to receive(:shutdown)
77
+ end
78
+
79
+ it "works" do
80
+ expect { @proxy.request(@test_data) }.to raise_error(Exception)
81
+ end
82
+ end
83
+
84
+
85
+ context "low level connection retries" do
86
+ before :each do
87
+ @connection = double(
88
+ :retry_change_requests= => true,
89
+ :shutdown => true
90
+ )
91
+ expect(Net::HTTP::Persistent).to receive(:new).and_return(@connection)
92
+ end
93
+
94
+ context "when retries are disabled" do
95
+ before:each do
96
+ expect(@connection).to receive(:request).and_raise( Timeout::Error)
97
+ @test_data[:options][:connection_retry_count] = 0
98
+ expect(@proxy).to receive(:sleep).never
99
+ end
100
+
101
+ it "makes no retries" do
102
+ expect { @proxy.request(@test_data) }.to raise_error(RightScale::CloudApi::ConnectionError)
103
+ end
104
+ end
105
+
106
+
107
+ context "when retries are enabled" do
108
+ context "timeouts are enabled" do
109
+ before:each do
110
+ @retries_count = 3
111
+ expect(@connection).to receive(:request).exactly(@retries_count+1).times.and_raise( Timeout::Error)
112
+ @test_data[:options][:connection_retry_count] = @retries_count
113
+ expect(@proxy).to receive(:sleep).exactly(@retries_count).times
114
+ end
115
+
116
+ it "makes no retries" do
117
+ expect { @proxy.request(@test_data) }.to raise_error(RightScale::CloudApi::ConnectionError)
118
+ end
119
+ end
120
+
121
+
122
+ context "but timeouts are disabled" do
123
+ before:each do
124
+ @retries_count = 3
125
+ @test_data[:options][:connection_retry_count] = @retries_count
126
+ @test_data[:options][:abort_on_timeout] = true
127
+ end
128
+
129
+ it "makes no retries on timeout" do
130
+ expect(@connection).to receive(:request).and_raise(Timeout::Error)
131
+ expect(@proxy).to receive(:sleep).never
132
+ expect { @proxy.request(@test_data) }.to raise_error(RightScale::CloudApi::ConnectionError)
133
+ end
134
+
135
+ it "makes retries on non timeout errors" do
136
+ expect(@connection).to receive(:request).exactly(@retries_count+1).times.and_raise(SocketError)
137
+ expect(@proxy).to receive(:sleep).exactly(@retries_count).times
138
+ expect { @proxy.request(@test_data) }.to raise_error(RightScale::CloudApi::ConnectionError)
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,152 @@
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 NONINFRINGEMENT.
18
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ require File.expand_path(File.dirname(__FILE__)) + "/../spec_helper"
25
+
26
+ describe "RightScale::CloudApi::CacheValidator" do
27
+ before(:each) do
28
+ logger = Logger.new(STDOUT)
29
+ logger.level = Logger::INFO
30
+ @api_manager = RightScale::CloudApi::ApiManager.new({'x' => 'y'},'endpoint', {:logger => logger})
31
+ @api_manager.class.set_routine RightScale::CloudApi::CacheValidator
32
+ @api_manager.class.options[:error_patterns] = []
33
+
34
+ @cachevalidator = RightScale::CloudApi::CacheValidator.new
35
+ @test_data = {}
36
+ @test_data[:request] = { :verb => 'some_verb', :orig_params => {}, :instance => 'some_request'}
37
+ @test_data[:options] = {:error_patterns => []}
38
+ @callback = double
39
+ @test_data[:options][:cache] = {}
40
+ @test_data[:callbacks] = {:close_current_connection => @callback}
41
+ @test_data[:connection] = {}
42
+ @test_data[:response] = {}
43
+ @test_data[:vars] = {:system => {:storage => {}}}
44
+ @test_data[:response][:instance] = double(:is_io? => false)
45
+ allow(@cachevalidator).to receive(:log)
46
+ end
47
+
48
+ context "cache_pattern" do
49
+ it "fails when is has unexpected inputs" do
50
+ # non hash input
51
+ expect { @api_manager.class.cache_pattern(true) }.to raise_error(RightScale::CloudApi::CacheValidator::Error)
52
+ # blank pattern
53
+ expect { @api_manager.class.cache_pattern({}) }.to raise_error(RightScale::CloudApi::CacheValidator::Error)
54
+ end
55
+
56
+ context "pattern keys" do
57
+ before(:each) do
58
+ # test all pattern keys
59
+ @cache_pattern = RightScale::CloudApi::CacheValidator::ClassMethods::CACHE_PATTERN_KEYS.inject({}) { |result, k| result.merge(k => k.to_s)}
60
+ @api_manager.class.cache_pattern(@cache_pattern)
61
+ end
62
+ it "stores all the cache patterns keys properly" do
63
+ expect(@api_manager.class.options[:cache_patterns]).to eq([@cache_pattern])
64
+ end
65
+ it "complains when a mandatory key is missing" do
66
+ @cache_pattern.delete(:key)
67
+ expect { @api_manager.class.cache_pattern(@cache_pattern) }.to raise_error(RightScale::CloudApi::CacheValidator::Error)
68
+ end
69
+ it "complains when an unsupported key is passed" do
70
+ bad_keys = { :bad_key1 => "bad_key1", :bad_key2 => "bad_key2" }
71
+ @cache_pattern.merge!(bad_keys)
72
+ expect { @api_manager.class.cache_pattern(@cache_pattern) }.to raise_error(RightScale::CloudApi::CacheValidator::Error)
73
+ end
74
+ end
75
+ end
76
+
77
+ context "basic cache validation" do
78
+ it "returns true when it performed validation" do
79
+ expect(@cachevalidator.execute(@test_data)).to be(true)
80
+ end
81
+ it "returns nil if there is no way to parse a response object" do
82
+ @test_data[:response][:instance] = double(:is_io? => true)
83
+ expect(@cachevalidator.execute(@test_data)).to be(nil)
84
+ end
85
+ it "returns nil if caching is disabled" do
86
+ @test_data[:options].delete(:cache)
87
+ expect(@cachevalidator.execute(@test_data)).to be(nil)
88
+ end
89
+ end
90
+
91
+ context "cache validation with match" do
92
+ before(:each) do
93
+ expect(RightScale::CloudApi::Utils).to receive(:pattern_matches?).at_least(1).and_return(true)
94
+ @cache_pattern = RightScale::CloudApi::CacheValidator::ClassMethods::CACHE_PATTERN_KEYS.inject({}) { |result, k| result.merge(k => k.to_s)}
95
+ @cache_pattern[:sign] = double(:call => "body_to_sign")
96
+ @test_data[:options][:cache_patterns] = [@cache_pattern]
97
+ @response = double(:code => '501', :body => 'body', :headers => 'headers', :is_io? => false)
98
+ @test_data[:response] = {:instance => @response}
99
+ end
100
+ it "fails if there is a missing key" do
101
+ @cache_pattern.delete(:key)
102
+ expect { @cachevalidator.execute(@test_data) }.to raise_error(RightScale::CloudApi::CacheValidator::Error)
103
+ end
104
+
105
+ context "and one record cached" do
106
+ before(:each) do
107
+ expect(@cachevalidator).to receive(:build_cache_key).at_least(1).and_return(["some_key","some_response_body"])
108
+ expect(@cachevalidator).to receive(:log).with("New cache record created")
109
+ @cachevalidator.execute(@test_data)
110
+ end
111
+ it "succeeds when it builds a cache key for the first time" do
112
+ expect(@test_data[:vars][:cache][:key]).to eq "some_key"
113
+ end
114
+ it "raises CacheHit and increments a counter when cache hits" do
115
+ expect { @cachevalidator.execute(@test_data) }.to raise_error(RightScale::CloudApi::CacheHit)
116
+ expect(@test_data[:vars][:system][:storage][:cache]['some_key'][:hits]).to eq 1
117
+ expect { @cachevalidator.execute(@test_data) }.to raise_error(RightScale::CloudApi::CacheHit)
118
+ expect(@test_data[:vars][:system][:storage][:cache]['some_key'][:hits]).to eq 2
119
+ end
120
+ it "replaces a record if the same request gets a different response" do
121
+ @test_data[:vars][:system][:storage][:cache]['some_key'][:md5] = 'corrupted'
122
+ expect(@cachevalidator).to receive(:log).with("Missed. Record is replaced")
123
+ @cachevalidator.execute(@test_data)
124
+ expect(@test_data[:vars][:system][:storage][:cache]['some_key'][:hits]).to eq 0
125
+ end
126
+ end
127
+ end
128
+
129
+ context "build_cache_key" do
130
+ before(:each) do
131
+ # use send since this is a private method
132
+ @opts = {:response => double(:body => nil)}
133
+ end
134
+ it "fails when it cannot create a key" do
135
+ expect { @cachevalidator.__send__(:build_cache_key, {}, @opts) }.to raise_error((RightScale::CloudApi::CacheValidator::Error))
136
+ end
137
+ it "fails when it cannot create body" do
138
+ expect { @cachevalidator.__send__(:build_cache_key, {:key => 'normal_key'}, @opts) }.to raise_error(RightScale::CloudApi::CacheValidator::Error)
139
+ end
140
+ it "creates key and body from given inputs" do
141
+ pattern = {:key => 'normal_key'}
142
+ opts = {:response => double(:body => "normal_body")}
143
+ expect(@cachevalidator.__send__(:build_cache_key, pattern, opts)).to eq(['normal_key', 'normal_body'])
144
+ end
145
+ it "creates key and body from given procs" do
146
+ proc = double(:is_a? => true)
147
+ expect(proc).to receive(:call).and_return("proc_key")
148
+ proc_pattern = { :key => proc, :sign => double(:call => "proc_sign_call") }
149
+ expect(@cachevalidator.__send__(:build_cache_key, proc_pattern, @opts)).to eq(['proc_key', 'proc_sign_call'])
150
+ end
151
+ end
152
+ end