httpotato 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. data/.gitignore +7 -0
  2. data/History +216 -0
  3. data/MIT-LICENSE +20 -0
  4. data/Manifest +47 -0
  5. data/README.rdoc +52 -0
  6. data/Rakefile +90 -0
  7. data/VERSION +1 -0
  8. data/bin/httpotato +108 -0
  9. data/cucumber.yml +1 -0
  10. data/examples/aaws.rb +32 -0
  11. data/examples/basic.rb +11 -0
  12. data/examples/custom_parsers.rb +67 -0
  13. data/examples/delicious.rb +37 -0
  14. data/examples/google.rb +16 -0
  15. data/examples/rubyurl.rb +14 -0
  16. data/examples/twitter.rb +31 -0
  17. data/examples/whoismyrep.rb +10 -0
  18. data/features/basic_authentication.feature +20 -0
  19. data/features/command_line.feature +7 -0
  20. data/features/deals_with_http_error_codes.feature +26 -0
  21. data/features/digest_authentication.feature +20 -0
  22. data/features/handles_compressed_responses.feature +19 -0
  23. data/features/handles_multiple_formats.feature +34 -0
  24. data/features/steps/env.rb +23 -0
  25. data/features/steps/httpotato_response_steps.rb +26 -0
  26. data/features/steps/httpotato_steps.rb +27 -0
  27. data/features/steps/mongrel_helper.rb +94 -0
  28. data/features/steps/remote_service_steps.rb +69 -0
  29. data/features/supports_redirection.feature +22 -0
  30. data/features/supports_timeout_option.feature +13 -0
  31. data/httpotato.gemspec +150 -0
  32. data/lib/httpotato.rb +371 -0
  33. data/lib/httpotato/cookie_hash.rb +22 -0
  34. data/lib/httpotato/core_extensions.rb +31 -0
  35. data/lib/httpotato/exceptions.rb +26 -0
  36. data/lib/httpotato/module_inheritable_attributes.rb +34 -0
  37. data/lib/httpotato/net_digest_auth.rb +35 -0
  38. data/lib/httpotato/parser.rb +146 -0
  39. data/lib/httpotato/request.rb +231 -0
  40. data/lib/httpotato/response.rb +79 -0
  41. data/spec/fixtures/delicious.xml +23 -0
  42. data/spec/fixtures/empty.xml +0 -0
  43. data/spec/fixtures/google.html +3 -0
  44. data/spec/fixtures/ssl/generate.sh +29 -0
  45. data/spec/fixtures/ssl/generated/1fe462c2.0 +15 -0
  46. data/spec/fixtures/ssl/generated/bogushost.crt +13 -0
  47. data/spec/fixtures/ssl/generated/ca.crt +15 -0
  48. data/spec/fixtures/ssl/generated/ca.key +15 -0
  49. data/spec/fixtures/ssl/generated/selfsigned.crt +14 -0
  50. data/spec/fixtures/ssl/generated/server.crt +13 -0
  51. data/spec/fixtures/ssl/generated/server.key +15 -0
  52. data/spec/fixtures/ssl/openssl-exts.cnf +9 -0
  53. data/spec/fixtures/twitter.json +1 -0
  54. data/spec/fixtures/twitter.xml +403 -0
  55. data/spec/fixtures/undefined_method_add_node_for_nil.xml +2 -0
  56. data/spec/httpotato/cookie_hash_spec.rb +71 -0
  57. data/spec/httpotato/parser_spec.rb +155 -0
  58. data/spec/httpotato/request_spec.rb +430 -0
  59. data/spec/httpotato/response_spec.rb +188 -0
  60. data/spec/httpotato/ssl_spec.rb +54 -0
  61. data/spec/httpotato_spec.rb +570 -0
  62. data/spec/spec.opts +3 -0
  63. data/spec/spec_helper.rb +20 -0
  64. data/spec/support/ssl_test_helper.rb +25 -0
  65. data/spec/support/ssl_test_server.rb +69 -0
  66. data/spec/support/stub_response.rb +30 -0
  67. data/test.rb +39 -0
  68. data/website/css/common.css +47 -0
  69. data/website/index.html +66 -0
  70. metadata +260 -0
@@ -0,0 +1,188 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2
+
3
+ describe HTTPotato::Response do
4
+ before do
5
+ @last_modified = Date.new(2010, 1, 15).to_s
6
+ @content_length = '1024'
7
+ @response_object = {'foo' => 'bar'}
8
+ @response_object = Net::HTTPOK.new('1.1', 200, 'OK')
9
+ @response_object.stub(:body => "{foo:'bar'}")
10
+ @response_object['last-modified'] = @last_modified
11
+ @response_object['content-length'] = @content_length
12
+ @parsed_response = {"foo" => "bar"}
13
+ @response = HTTPotato::Response.new(@response_object, @parsed_response)
14
+ end
15
+
16
+ describe ".underscore" do
17
+ it "works with one capitalized word" do
18
+ HTTPotato::Response.underscore("Accepted").should == "accepted"
19
+ end
20
+
21
+ it "works with titlecase" do
22
+ HTTPotato::Response.underscore("BadGateway").should == "bad_gateway"
23
+ end
24
+
25
+ it "works with all caps" do
26
+ HTTPotato::Response.underscore("OK").should == "ok"
27
+ end
28
+ end
29
+
30
+ describe "initialization" do
31
+ it "should set the Net::HTTP Response" do
32
+ @response.response.should == @response_object
33
+ end
34
+
35
+ it "should set body" do
36
+ @response.body.should == @response_object.body
37
+ end
38
+
39
+ it "should set code" do
40
+ @response.code.should.to_s == @response_object.code
41
+ end
42
+
43
+ it "should set code as a Fixnum" do
44
+ @response.code.should be_an_instance_of(Fixnum)
45
+ end
46
+ end
47
+
48
+ it "returns response headers" do
49
+ response = HTTPotato::Response.new(@response_object, @parsed_response)
50
+ response.headers.should == {'last-modified' => [@last_modified], 'content-length' => [@content_length]}
51
+ end
52
+
53
+ it "should send missing methods to delegate" do
54
+ response = HTTPotato::Response.new(@response_object, {'foo' => 'bar'})
55
+ response['foo'].should == 'bar'
56
+ end
57
+
58
+ it "should be able to iterate if it is array" do
59
+ response = HTTPotato::Response.new(@response_object, [{'foo' => 'bar'}, {'foo' => 'baz'}])
60
+ response.size.should == 2
61
+ expect {
62
+ response.each { |item| }
63
+ }.to_not raise_error
64
+ end
65
+
66
+ it "allows headers to be accessed by mixed-case names in hash notation" do
67
+ response = HTTPotato::Response.new(@response_object, @parsed_response)
68
+ response.headers['Content-LENGTH'].should == @content_length
69
+ end
70
+
71
+ it "returns a comma-delimited value when multiple values exist" do
72
+ @response_object.add_field 'set-cookie', 'csrf_id=12345; path=/'
73
+ @response_object.add_field 'set-cookie', '_github_ses=A123CdE; path=/'
74
+ response = HTTPotato::Response.new(@response_object, @parsed_response)
75
+ response.headers['set-cookie'].should == "csrf_id=12345; path=/, _github_ses=A123CdE; path=/"
76
+ end
77
+
78
+ # Backwards-compatibility - previously, #headers returned a Hash
79
+ it "responds to hash methods" do
80
+ response = HTTPotato::Response.new(@response_object, @parsed_response)
81
+ hash_methods = {}.methods - response.headers.methods
82
+ hash_methods.each do |method_name|
83
+ response.headers.respond_to?(method_name).should be_true
84
+ end
85
+ end
86
+
87
+ xit "should allow hashes to be accessed with dot notation" do
88
+ response = HTTPotato::Response.new({'foo' => 'bar'}, "{foo:'bar'}", 200, 'OK')
89
+ response.foo.should == 'bar'
90
+ end
91
+
92
+ xit "should allow nested hashes to be accessed with dot notation" do
93
+ response = HTTPotato::Response.new({'foo' => {'bar' => 'baz'}}, "{foo: {bar:'baz'}}", 200, 'OK')
94
+ response.foo.should == {'bar' => 'baz'}
95
+ response.foo.bar.should == 'baz'
96
+ end
97
+
98
+ describe "semantic methods for response codes" do
99
+ def response_mock(klass)
100
+ r = klass.new('', '', '')
101
+ r.stub(:body)
102
+ r
103
+ end
104
+
105
+ context "major codes" do
106
+ it "is information" do
107
+ net_response = response_mock(Net::HTTPInformation)
108
+ response = HTTPotato::Response.new(net_response, '')
109
+ response.information?.should be_true
110
+ end
111
+
112
+ it "is success" do
113
+ net_response = response_mock(Net::HTTPSuccess)
114
+ response = HTTPotato::Response.new(net_response, '')
115
+ response.success?.should be_true
116
+ end
117
+
118
+ it "is redirection" do
119
+ net_response = response_mock(Net::HTTPRedirection)
120
+ response = HTTPotato::Response.new(net_response, '')
121
+ response.redirection?.should be_true
122
+ end
123
+
124
+ it "is client error" do
125
+ net_response = response_mock(Net::HTTPClientError)
126
+ response = HTTPotato::Response.new(net_response, '')
127
+ response.client_error?.should be_true
128
+ end
129
+
130
+ it "is server error" do
131
+ net_response = response_mock(Net::HTTPServerError)
132
+ response = HTTPotato::Response.new(net_response, '')
133
+ response.server_error?.should be_true
134
+ end
135
+ end
136
+
137
+ context "for specific codes" do
138
+ SPECIFIC_CODES = {
139
+ :accepted? => Net::HTTPAccepted,
140
+ :bad_gateway? => Net::HTTPBadGateway,
141
+ :bad_request? => Net::HTTPBadRequest,
142
+ :conflict? => Net::HTTPConflict,
143
+ :continue? => Net::HTTPContinue,
144
+ :created? => Net::HTTPCreated,
145
+ :expectation_failed? => Net::HTTPExpectationFailed,
146
+ :forbidden? => Net::HTTPForbidden,
147
+ :found? => Net::HTTPFound,
148
+ :gateway_time_out? => Net::HTTPGatewayTimeOut,
149
+ :gone? => Net::HTTPGone,
150
+ :internal_server_error? => Net::HTTPInternalServerError,
151
+ :length_required? => Net::HTTPLengthRequired,
152
+ :method_not_allowed? => Net::HTTPMethodNotAllowed,
153
+ :moved_permanently? => Net::HTTPMovedPermanently,
154
+ :multiple_choice? => Net::HTTPMultipleChoice,
155
+ :no_content? => Net::HTTPNoContent,
156
+ :non_authoritative_information? => Net::HTTPNonAuthoritativeInformation,
157
+ :not_acceptable? => Net::HTTPNotAcceptable,
158
+ :not_found? => Net::HTTPNotFound,
159
+ :not_implemented? => Net::HTTPNotImplemented,
160
+ :not_modified? => Net::HTTPNotModified,
161
+ :ok? => Net::HTTPOK,
162
+ :partial_content? => Net::HTTPPartialContent,
163
+ :payment_required? => Net::HTTPPaymentRequired,
164
+ :precondition_failed? => Net::HTTPPreconditionFailed,
165
+ :proxy_authentication_required? => Net::HTTPProxyAuthenticationRequired,
166
+ :request_entity_too_large? => Net::HTTPRequestEntityTooLarge,
167
+ :request_time_out? => Net::HTTPRequestTimeOut,
168
+ :request_uri_too_long? => Net::HTTPRequestURITooLong,
169
+ :requested_range_not_satisfiable? => Net::HTTPRequestedRangeNotSatisfiable,
170
+ :reset_content? => Net::HTTPResetContent,
171
+ :see_other? => Net::HTTPSeeOther,
172
+ :service_unavailable? => Net::HTTPServiceUnavailable,
173
+ :switch_protocol? => Net::HTTPSwitchProtocol,
174
+ :temporary_redirect? => Net::HTTPTemporaryRedirect,
175
+ :unauthorized? => Net::HTTPUnauthorized,
176
+ :unsupported_media_type? => Net::HTTPUnsupportedMediaType,
177
+ :use_proxy? => Net::HTTPUseProxy,
178
+ :version_not_supported? => Net::HTTPVersionNotSupported
179
+ }.each do |method, klass|
180
+ it "responds to #{method}" do
181
+ net_response = response_mock(klass)
182
+ response = HTTPotato::Response.new(net_response, '')
183
+ response.__send__(method).should be_true
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,54 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2
+
3
+ describe HTTPotato::Request do
4
+ context "SSL certificate verification" do
5
+ before do
6
+ FakeWeb.allow_net_connect = true # enable network connections just for this test
7
+ end
8
+
9
+ after do
10
+ FakeWeb.allow_net_connect = false # Restore allow_net_connect value for testing
11
+ end
12
+
13
+ it "should work with when no trusted CA list is specified" do
14
+ ssl_verify_test(nil, nil, "selfsigned.crt").should == {'success' => true}
15
+ end
16
+
17
+ it "should work with when no trusted CA list is specified, even with a bogus hostname" do
18
+ ssl_verify_test(nil, nil, "bogushost.crt").should == {'success' => true}
19
+ end
20
+
21
+ it "should work when using ssl_ca_file with a self-signed CA" do
22
+ ssl_verify_test(:ssl_ca_file, "selfsigned.crt", "selfsigned.crt").should == {'success' => true}
23
+ end
24
+
25
+ it "should work when using ssl_ca_file with a certificate authority" do
26
+ ssl_verify_test(:ssl_ca_file, "ca.crt", "server.crt").should == {'success' => true}
27
+ end
28
+ it "should work when using ssl_ca_path with a certificate authority" do
29
+ ssl_verify_test(:ssl_ca_path, ".", "server.crt").should == {'success' => true}
30
+ end
31
+
32
+ it "should fail when using ssl_ca_file and the server uses an unrecognized certificate authority" do
33
+ lambda do
34
+ ssl_verify_test(:ssl_ca_file, "ca.crt", "selfsigned.crt")
35
+ end.should raise_error(OpenSSL::SSL::SSLError)
36
+ end
37
+ it "should fail when using ssl_ca_path and the server uses an unrecognized certificate authority" do
38
+ lambda do
39
+ ssl_verify_test(:ssl_ca_path, ".", "selfsigned.crt")
40
+ end.should raise_error(OpenSSL::SSL::SSLError)
41
+ end
42
+
43
+ it "should fail when using ssl_ca_file and the server uses a bogus hostname" do
44
+ lambda do
45
+ ssl_verify_test(:ssl_ca_file, "ca.crt", "bogushost.crt")
46
+ end.should raise_error(OpenSSL::SSL::SSLError)
47
+ end
48
+ it "should fail when using ssl_ca_path and the server uses a bogus hostname" do
49
+ lambda do
50
+ ssl_verify_test(:ssl_ca_path, ".", "bogushost.crt")
51
+ end.should raise_error(OpenSSL::SSL::SSLError)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,570 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
2
+
3
+ describe HTTPotato do
4
+ before(:each) do
5
+ @klass = Class.new
6
+ @klass.instance_eval { include HTTPotato }
7
+ end
8
+
9
+ describe "AllowedFormats deprecated" do
10
+ before do
11
+ Kernel.stub(:warn)
12
+ end
13
+ it "warns with a deprecation message" do
14
+ Kernel.should_receive(:warn).with("Deprecated: Use HTTPotato::Parser::SupportedFormats")
15
+ HTTPotato::AllowedFormats
16
+ end
17
+
18
+ it "returns HTTPart::Parser::SupportedFormats" do
19
+ HTTPotato::AllowedFormats.should == HTTPotato::Parser::SupportedFormats
20
+ end
21
+ end
22
+
23
+ describe "base uri" do
24
+ before(:each) do
25
+ @klass.base_uri('api.foo.com/v1')
26
+ end
27
+
28
+ it "should have reader" do
29
+ @klass.base_uri.should == 'http://api.foo.com/v1'
30
+ end
31
+
32
+ it 'should have writer' do
33
+ @klass.base_uri('http://api.foobar.com')
34
+ @klass.base_uri.should == 'http://api.foobar.com'
35
+ end
36
+
37
+ it 'should not modify the parameter during assignment' do
38
+ uri = 'http://api.foobar.com'
39
+ @klass.base_uri(uri)
40
+ uri.should == 'http://api.foobar.com'
41
+ end
42
+ end
43
+
44
+ describe "#normalize_base_uri" do
45
+ it "should add http if not present for non ssl requests" do
46
+ uri = HTTPotato.normalize_base_uri('api.foobar.com')
47
+ uri.should == 'http://api.foobar.com'
48
+ end
49
+
50
+ it "should add https if not present for ssl requests" do
51
+ uri = HTTPotato.normalize_base_uri('api.foo.com/v1:443')
52
+ uri.should == 'https://api.foo.com/v1:443'
53
+ end
54
+
55
+ it "should not remove https for ssl requests" do
56
+ uri = HTTPotato.normalize_base_uri('https://api.foo.com/v1:443')
57
+ uri.should == 'https://api.foo.com/v1:443'
58
+ end
59
+
60
+ it 'should not modify the parameter' do
61
+ uri = 'http://api.foobar.com'
62
+ HTTPotato.normalize_base_uri(uri)
63
+ uri.should == 'http://api.foobar.com'
64
+ end
65
+ end
66
+
67
+ describe "headers" do
68
+ def expect_headers(header={})
69
+ HTTPotato::Request.should_receive(:new) \
70
+ .with(anything, anything, hash_including({ :headers => header })) \
71
+ .and_return(mock("mock response", :perform => nil))
72
+ end
73
+
74
+ it "should default to empty hash" do
75
+ @klass.headers.should == {}
76
+ end
77
+
78
+ it "should be able to be updated" do
79
+ init_headers = {:foo => 'bar', :baz => 'spax'}
80
+ @klass.headers init_headers
81
+ @klass.headers.should == init_headers
82
+ end
83
+
84
+ it "uses the class headers when sending a request" do
85
+ expect_headers(:foo => 'bar')
86
+ @klass.headers(:foo => 'bar')
87
+ @klass.get('')
88
+ end
89
+
90
+ it "overwrites class headers when passing in headers" do
91
+ expect_headers(:baz => 'spax')
92
+ @klass.headers(:foo => 'bar')
93
+ @klass.get('', :headers => {:baz => 'spax'})
94
+ end
95
+
96
+ context "with cookies" do
97
+ it 'utilizes the class-level cookies' do
98
+ expect_headers(:foo => 'bar', 'cookie' => 'type=snickerdoodle')
99
+ @klass.headers(:foo => 'bar')
100
+ @klass.cookies(:type => 'snickerdoodle')
101
+ @klass.get('')
102
+ end
103
+
104
+ it 'adds cookies to the headers' do
105
+ expect_headers(:foo => 'bar', 'cookie' => 'type=snickerdoodle')
106
+ @klass.headers(:foo => 'bar')
107
+ @klass.get('', :cookies => {:type => 'snickerdoodle'})
108
+ end
109
+
110
+ it 'adds optional cookies to the optional headers' do
111
+ expect_headers(:baz => 'spax', 'cookie' => 'type=snickerdoodle')
112
+ @klass.get('', :cookies => {:type => 'snickerdoodle'}, :headers => {:baz => 'spax'})
113
+ end
114
+ end
115
+ end
116
+
117
+ describe "cookies" do
118
+ def expect_cookie_header(s)
119
+ HTTPotato::Request.should_receive(:new) \
120
+ .with(anything, anything, hash_including({ :headers => { "cookie" => s } })) \
121
+ .and_return(mock("mock response", :perform => nil))
122
+ end
123
+
124
+ it "should not be in the headers by default" do
125
+ HTTPotato::Request.stub!(:new).and_return(stub(nil, :perform => nil))
126
+ @klass.get("")
127
+ @klass.headers.keys.should_not include("cookie")
128
+ end
129
+
130
+ it "should raise an ArgumentError if passed a non-Hash" do
131
+ lambda do
132
+ @klass.cookies("nonsense")
133
+ end.should raise_error(ArgumentError)
134
+ end
135
+
136
+ it "should allow a cookie to be specified with a one-off request" do
137
+ expect_cookie_header "type=snickerdoodle"
138
+ @klass.get("", :cookies => { :type => "snickerdoodle" })
139
+ end
140
+
141
+ describe "when a cookie is set at the class level" do
142
+ before(:each) do
143
+ @klass.cookies({ :type => "snickerdoodle" })
144
+ end
145
+
146
+ it "should include that cookie in the request" do
147
+ expect_cookie_header "type=snickerdoodle"
148
+ @klass.get("")
149
+ end
150
+
151
+ it "should pass the proper cookies when requested multiple times" do
152
+ 2.times do
153
+ expect_cookie_header "type=snickerdoodle"
154
+ @klass.get("")
155
+ end
156
+ end
157
+
158
+ it "should allow the class defaults to be overridden" do
159
+ expect_cookie_header "type=chocolate_chip"
160
+
161
+ @klass.get("", :cookies => { :type => "chocolate_chip" })
162
+ end
163
+ end
164
+
165
+ describe "in a class with multiple methods that use different cookies" do
166
+ before(:each) do
167
+ @klass.instance_eval do
168
+ def first_method
169
+ get("first_method", :cookies => { :first_method_cookie => "foo" })
170
+ end
171
+
172
+ def second_method
173
+ get("second_method", :cookies => { :second_method_cookie => "foo" })
174
+ end
175
+ end
176
+ end
177
+
178
+ it "should not allow cookies used in one method to carry over into other methods" do
179
+ expect_cookie_header "first_method_cookie=foo"
180
+ @klass.first_method
181
+
182
+ expect_cookie_header "second_method_cookie=foo"
183
+ @klass.second_method
184
+ end
185
+ end
186
+ end
187
+
188
+ describe "default params" do
189
+ it "should default to empty hash" do
190
+ @klass.default_params.should == {}
191
+ end
192
+
193
+ it "should be able to be updated" do
194
+ new_defaults = {:foo => 'bar', :baz => 'spax'}
195
+ @klass.default_params new_defaults
196
+ @klass.default_params.should == new_defaults
197
+ end
198
+ end
199
+
200
+ describe "default timeout" do
201
+ it "should default to nil" do
202
+ @klass.default_options[:timeout].should == nil
203
+ end
204
+
205
+ it "should support updating" do
206
+ @klass.default_timeout 10
207
+ @klass.default_options[:timeout].should == 10
208
+ end
209
+ end
210
+
211
+ describe "debug_output" do
212
+ it "stores the given stream as a default_option" do
213
+ @klass.debug_output $stdout
214
+ @klass.default_options[:debug_output].should == $stdout
215
+ end
216
+
217
+ it "stores the $stderr stream by default" do
218
+ @klass.debug_output
219
+ @klass.default_options[:debug_output].should == $stderr
220
+ end
221
+ end
222
+
223
+ describe "basic http authentication" do
224
+ it "should work" do
225
+ @klass.basic_auth 'foobar', 'secret'
226
+ @klass.default_options[:basic_auth].should == {:username => 'foobar', :password => 'secret'}
227
+ end
228
+ end
229
+
230
+ describe "digest http authentication" do
231
+ it "should work" do
232
+ @klass.digest_auth 'foobar', 'secret'
233
+ @klass.default_options[:digest_auth].should == {:username => 'foobar', :password => 'secret'}
234
+ end
235
+ end
236
+
237
+ describe "parser" do
238
+ class CustomParser
239
+ def self.parse(body)
240
+ return {:sexy => true}
241
+ end
242
+ end
243
+
244
+ let(:parser) do
245
+ Proc.new{ |data, format| CustomParser.parse(data) }
246
+ end
247
+
248
+ it "should set parser options" do
249
+ @klass.parser parser
250
+ @klass.default_options[:parser].should == parser
251
+ end
252
+
253
+ it "should be able parse response with custom parser" do
254
+ @klass.parser parser
255
+ FakeWeb.register_uri(:get, 'http://twitter.com/statuses/public_timeline.xml', :body => 'tweets')
256
+ custom_parsed_response = @klass.get('http://twitter.com/statuses/public_timeline.xml')
257
+ custom_parsed_response[:sexy].should == true
258
+ end
259
+
260
+ it "raises UnsupportedFormat when the parser cannot handle the format" do
261
+ @klass.format :json
262
+ class MyParser < HTTPotato::Parser
263
+ SupportedFormats = {}
264
+ end unless defined?(MyParser)
265
+ expect do
266
+ @klass.parser MyParser
267
+ end.to raise_error(HTTPotato::UnsupportedFormat)
268
+ end
269
+
270
+ it 'does not validate format whe custom parser is a proc' do
271
+ expect do
272
+ @klass.format :json
273
+ @klass.parser lambda {|body, format|}
274
+ end.to_not raise_error(HTTPotato::UnsupportedFormat)
275
+ end
276
+ end
277
+
278
+ describe "format" do
279
+ it "should allow xml" do
280
+ @klass.format :xml
281
+ @klass.default_options[:format].should == :xml
282
+ end
283
+
284
+ it "should allow json" do
285
+ @klass.format :json
286
+ @klass.default_options[:format].should == :json
287
+ end
288
+
289
+ it "should allow yaml" do
290
+ @klass.format :yaml
291
+ @klass.default_options[:format].should == :yaml
292
+ end
293
+
294
+ it "should allow plain" do
295
+ @klass.format :plain
296
+ @klass.default_options[:format].should == :plain
297
+ end
298
+
299
+ it 'should not allow funky format' do
300
+ lambda do
301
+ @klass.format :foobar
302
+ end.should raise_error(HTTPotato::UnsupportedFormat)
303
+ end
304
+
305
+ it 'should only print each format once with an exception' do
306
+ lambda do
307
+ @klass.format :foobar
308
+ end.should raise_error(HTTPotato::UnsupportedFormat, "':foobar' Must be one of: html, json, plain, xml, yaml")
309
+ end
310
+
311
+ it 'sets the default parser' do
312
+ @klass.default_options[:parser].should be_nil
313
+ @klass.format :json
314
+ @klass.default_options[:parser].should == HTTPotato::Parser
315
+ end
316
+
317
+ it 'does not reset parser to the default parser' do
318
+ my_parser = lambda {}
319
+ @klass.parser my_parser
320
+ @klass.format :json
321
+ @klass.parser.should == my_parser
322
+ end
323
+ end
324
+
325
+ describe "#no_follow" do
326
+ it "sets no_follow to false by default" do
327
+ @klass.no_follow
328
+ @klass.default_options[:no_follow].should be_false
329
+ end
330
+
331
+ it "sets the no_follow option to true" do
332
+ @klass.no_follow true
333
+ @klass.default_options[:no_follow].should be_true
334
+ end
335
+ end
336
+
337
+ describe "#maintain_method_across_redirects" do
338
+ it "sets maintain_method_across_redirects to true by default" do
339
+ @klass.maintain_method_across_redirects
340
+ @klass.default_options[:maintain_method_across_redirects].should be_true
341
+ end
342
+
343
+ it "sets the maintain_method_across_redirects option to false" do
344
+ @klass.maintain_method_across_redirects false
345
+ @klass.default_options[:maintain_method_across_redirects].should be_false
346
+ end
347
+ end
348
+
349
+ describe "with explicit override of automatic redirect handling" do
350
+ before do
351
+ @request = HTTPotato::Request.new(Net::HTTP::Get, 'http://api.foo.com/v1', :format => :xml, :no_follow => true)
352
+ @redirect = stub_response 'first redirect', 302
353
+ @redirect['location'] = 'http://foo.com/bar'
354
+ HTTPotato::Request.stub(:new => @request)
355
+ end
356
+
357
+ it "should fail with redirected GET" do
358
+ lambda do
359
+ @error = @klass.get('/foo', :no_follow => true)
360
+ end.should raise_error(HTTPotato::RedirectionTooDeep) {|e| e.response.body.should == 'first redirect'}
361
+ end
362
+
363
+ it "should fail with redirected POST" do
364
+ lambda do
365
+ @klass.post('/foo', :no_follow => true)
366
+ end.should raise_error(HTTPotato::RedirectionTooDeep) {|e| e.response.body.should == 'first redirect'}
367
+ end
368
+
369
+ it "should fail with redirected DELETE" do
370
+ lambda do
371
+ @klass.delete('/foo', :no_follow => true)
372
+ end.should raise_error(HTTPotato::RedirectionTooDeep) {|e| e.response.body.should == 'first redirect'}
373
+ end
374
+
375
+ it "should fail with redirected PUT" do
376
+ lambda do
377
+ @klass.put('/foo', :no_follow => true)
378
+ end.should raise_error(HTTPotato::RedirectionTooDeep) {|e| e.response.body.should == 'first redirect'}
379
+ end
380
+
381
+ it "should fail with redirected HEAD" do
382
+ lambda do
383
+ @klass.head('/foo', :no_follow => true)
384
+ end.should raise_error(HTTPotato::RedirectionTooDeep) {|e| e.response.body.should == 'first redirect'}
385
+ end
386
+
387
+ it "should fail with redirected OPTIONS" do
388
+ lambda do
389
+ @klass.options('/foo', :no_follow => true)
390
+ end.should raise_error(HTTPotato::RedirectionTooDeep) {|e| e.response.body.should == 'first redirect'}
391
+ end
392
+ end
393
+
394
+ describe "with multiple class definitions" do
395
+ before(:each) do
396
+ @klass.instance_eval do
397
+ base_uri "http://first.com"
398
+ default_params :one => 1
399
+ end
400
+
401
+ @additional_klass = Class.new
402
+ @additional_klass.instance_eval do
403
+ include HTTPotato
404
+ base_uri "http://second.com"
405
+ default_params :two => 2
406
+ end
407
+ end
408
+
409
+ it "should not run over each others options" do
410
+ @klass.default_options.should == { :base_uri => 'http://first.com', :default_params => { :one => 1 } }
411
+ @additional_klass.default_options.should == { :base_uri => 'http://second.com', :default_params => { :two => 2 } }
412
+ end
413
+ end
414
+
415
+ describe "two child classes inheriting from one parent" do
416
+ before(:each) do
417
+ @parent = Class.new do
418
+ include HTTPotato
419
+ def self.name
420
+ "Parent"
421
+ end
422
+ end
423
+
424
+ @child1 = Class.new(@parent)
425
+ @child2 = Class.new(@parent)
426
+ end
427
+
428
+ it "does not modify each others inherited attributes" do
429
+ @child1.default_params :joe => "alive"
430
+ @child2.default_params :joe => "dead"
431
+
432
+ @child1.default_options.should == { :default_params => {:joe => "alive"} }
433
+ @child2.default_options.should == { :default_params => {:joe => "dead"} }
434
+
435
+ @parent.default_options.should == { }
436
+ end
437
+
438
+ it "inherits default_options from the superclass" do
439
+ @parent.basic_auth 'user', 'password'
440
+ @child1.default_options.should == {:basic_auth => {:username => 'user', :password => 'password'}}
441
+ @child1.basic_auth 'u', 'p' # modifying child1 has no effect on child2
442
+ @child2.default_options.should == {:basic_auth => {:username => 'user', :password => 'password'}}
443
+ end
444
+
445
+ it "doesn't modify the parent's default options" do
446
+ @parent.basic_auth 'user', 'password'
447
+
448
+ @child1.basic_auth 'u', 'p'
449
+ @child1.default_options.should == {:basic_auth => {:username => 'u', :password => 'p'}}
450
+
451
+ @child1.basic_auth 'email', 'token'
452
+ @child1.default_options.should == {:basic_auth => {:username => 'email', :password => 'token'}}
453
+
454
+ @parent.default_options.should == {:basic_auth => {:username => 'user', :password => 'password'}}
455
+ end
456
+
457
+ it "inherits default_cookies from the parent class" do
458
+ @parent.cookies 'type' => 'chocolate_chip'
459
+ @child1.default_cookies.should == {"type" => "chocolate_chip"}
460
+ @child1.cookies 'type' => 'snickerdoodle'
461
+ @child1.default_cookies.should == {"type" => "snickerdoodle"}
462
+ @child2.default_cookies.should == {"type" => "chocolate_chip"}
463
+ end
464
+
465
+ it "doesn't modify the parent's default cookies" do
466
+ @parent.cookies 'type' => 'chocolate_chip'
467
+
468
+ @child1.cookies 'type' => 'snickerdoodle'
469
+ @child1.default_cookies.should == {"type" => "snickerdoodle"}
470
+
471
+ @parent.default_cookies.should == {"type" => "chocolate_chip"}
472
+ end
473
+ end
474
+
475
+ describe "grand parent with inherited callback" do
476
+ before do
477
+ @grand_parent = Class.new do
478
+ def self.inherited(subclass)
479
+ subclass.instance_variable_set(:@grand_parent, true)
480
+ end
481
+ end
482
+ @parent = Class.new(@grand_parent) do
483
+ include HTTPotato
484
+ end
485
+ end
486
+ it "continues running the #inherited on the parent" do
487
+ child = Class.new(@parent)
488
+ child.instance_variable_get(:@grand_parent).should be_true
489
+ end
490
+ end
491
+
492
+ describe "#get" do
493
+ it "should be able to get html" do
494
+ stub_http_response_with('google.html')
495
+ HTTPotato.get('http://www.google.com').should == file_fixture('google.html')
496
+ end
497
+
498
+ it "should be able parse response type json automatically" do
499
+ stub_http_response_with('twitter.json')
500
+ tweets = HTTPotato.get('http://twitter.com/statuses/public_timeline.json')
501
+ tweets.size.should == 20
502
+ tweets.first['user'].should == {
503
+ "name" => "Pyk",
504
+ "url" => nil,
505
+ "id" => "7694602",
506
+ "description" => nil,
507
+ "protected" => false,
508
+ "screen_name" => "Pyk",
509
+ "followers_count" => 1,
510
+ "location" => "Opera Plaza, California",
511
+ "profile_image_url" => "http://static.twitter.com/images/default_profile_normal.png"
512
+ }
513
+ end
514
+
515
+ it "should be able parse response type xml automatically" do
516
+ stub_http_response_with('twitter.xml')
517
+ tweets = HTTPotato.get('http://twitter.com/statuses/public_timeline.xml')
518
+ tweets['statuses'].size.should == 20
519
+ tweets['statuses'].first['user'].should == {
520
+ "name" => "Magic 8 Bot",
521
+ "url" => nil,
522
+ "id" => "17656026",
523
+ "description" => "ask me a question",
524
+ "protected" => "false",
525
+ "screen_name" => "magic8bot",
526
+ "followers_count" => "90",
527
+ "profile_image_url" => "http://s3.amazonaws.com/twitter_production/profile_images/65565851/8ball_large_normal.jpg",
528
+ "location" => nil
529
+ }
530
+ end
531
+
532
+ it "should not get undefined method add_node for nil class for the following xml" do
533
+ stub_http_response_with('undefined_method_add_node_for_nil.xml')
534
+ result = HTTPotato.get('http://foobar.com')
535
+ result.should == {"Entities"=>{"href"=>"https://s3-sandbox.parature.com/api/v1/5578/5633/Account", "results"=>"0", "total"=>"0", "page_size"=>"25", "page"=>"1"}}
536
+ end
537
+
538
+ it "should parse empty response fine" do
539
+ stub_http_response_with('empty.xml')
540
+ result = HTTPotato.get('http://foobar.com')
541
+ result.should be_nil
542
+ end
543
+
544
+ it "should accept http URIs" do
545
+ stub_http_response_with('google.html')
546
+ lambda do
547
+ HTTPotato.get('http://google.com')
548
+ end.should_not raise_error(HTTPotato::UnsupportedURIScheme)
549
+ end
550
+
551
+ it "should accept https URIs" do
552
+ stub_http_response_with('google.html')
553
+ lambda do
554
+ HTTPotato.get('https://google.com')
555
+ end.should_not raise_error(HTTPotato::UnsupportedURIScheme)
556
+ end
557
+
558
+ it "should raise an ArgumentError on URIs that are not http or https" do
559
+ lambda do
560
+ HTTPotato.get("file:///there_is_no_party_on/my/filesystem")
561
+ end.should raise_error(HTTPotato::UnsupportedURIScheme)
562
+ end
563
+
564
+ it "should raise an InvalidURIError on URIs that can't be parsed at all" do
565
+ lambda do
566
+ HTTPotato.get("It's the one that says 'Bad URI'")
567
+ end.should raise_error(URI::InvalidURIError)
568
+ end
569
+ end
570
+ end