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.
- checksums.yaml +7 -0
- data/HISTORY +2 -0
- data/LICENSE +19 -0
- data/README.md +14 -0
- data/Rakefile +37 -0
- data/lib/base/api_manager.rb +707 -0
- data/lib/base/helpers/cloud_api_logger.rb +214 -0
- data/lib/base/helpers/http_headers.rb +239 -0
- data/lib/base/helpers/http_parent.rb +103 -0
- data/lib/base/helpers/http_request.rb +173 -0
- data/lib/base/helpers/http_response.rb +122 -0
- data/lib/base/helpers/net_http_patch.rb +31 -0
- data/lib/base/helpers/query_api_patterns.rb +862 -0
- data/lib/base/helpers/support.rb +270 -0
- data/lib/base/helpers/support.xml.rb +306 -0
- data/lib/base/helpers/utils.rb +380 -0
- data/lib/base/manager.rb +122 -0
- data/lib/base/parsers/json.rb +38 -0
- data/lib/base/parsers/plain.rb +36 -0
- data/lib/base/parsers/rexml.rb +83 -0
- data/lib/base/parsers/sax.rb +200 -0
- data/lib/base/routines/cache_validator.rb +184 -0
- data/lib/base/routines/connection_proxies/net_http_persistent_proxy.rb +194 -0
- data/lib/base/routines/connection_proxies/right_http_connection_proxy.rb +224 -0
- data/lib/base/routines/connection_proxy.rb +66 -0
- data/lib/base/routines/request_analyzer.rb +122 -0
- data/lib/base/routines/request_generator.rb +48 -0
- data/lib/base/routines/request_initializer.rb +52 -0
- data/lib/base/routines/response_analyzer.rb +152 -0
- data/lib/base/routines/response_parser.rb +79 -0
- data/lib/base/routines/result_wrapper.rb +75 -0
- data/lib/base/routines/retry_manager.rb +106 -0
- data/lib/base/routines/routine.rb +98 -0
- data/lib/right_cloud_api_base.rb +72 -0
- data/lib/right_cloud_api_base_version.rb +37 -0
- data/right_cloud_api_base.gemspec +63 -0
- data/spec/helpers/query_api_pattern_spec.rb +312 -0
- data/spec/helpers/support_spec.rb +211 -0
- data/spec/helpers/support_xml_spec.rb +207 -0
- data/spec/helpers/utils_spec.rb +179 -0
- data/spec/routines/connection_proxies/test_net_http_persistent_proxy_spec.rb +143 -0
- data/spec/routines/test_cache_validator_spec.rb +152 -0
- data/spec/routines/test_connection_proxy_spec.rb +44 -0
- data/spec/routines/test_request_analyzer_spec.rb +106 -0
- data/spec/routines/test_response_analyzer_spec.rb +132 -0
- data/spec/routines/test_response_parser_spec.rb +228 -0
- data/spec/routines/test_result_wrapper_spec.rb +63 -0
- data/spec/routines/test_retry_manager_spec.rb +84 -0
- data/spec/spec_helper.rb +15 -0
- 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 <'world'> & "the Universe""
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context "Object#_xml_unescale" do
|
39
|
+
it "unescapes non-xml symbols" do
|
40
|
+
expect("Hello <'world'> & "the Universe""._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
|
+
" <"PAL">" + "\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
|