hugs 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -5,7 +5,7 @@ gem "nokogiri", "~> 1.4.4"
5
5
  gem "net-http-persistent", "~> 1.4.1"
6
6
  gem "multipart-post", "~> 1.0.1"
7
7
 
8
- group :development do
8
+ group :development, :test do
9
9
  gem "rake"
10
10
  gem "jeweler", "~> 1.5.1"
11
11
  gem "minitest", "~> 2.0.0"
@@ -4,11 +4,11 @@ GEM
4
4
  addressable (2.2.2)
5
5
  crack (0.1.8)
6
6
  git (1.2.5)
7
- jeweler (1.5.1)
7
+ jeweler (1.5.2)
8
8
  bundler (~> 1.0.0)
9
9
  git (>= 1.2.5)
10
10
  rake
11
- minitest (2.0.0)
11
+ minitest (2.0.2)
12
12
  multipart-post (1.0.1)
13
13
  net-http-persistent (1.4.1)
14
14
  nokogiri (1.4.4)
data/README.md CHANGED
@@ -10,7 +10,8 @@ Opted to write this gem for four reasons:
10
10
  * Wanted a [fast](http://blog.segment7.net/articles/2010/05/07/net-http-is-not-slow),
11
11
  thread-safe, and persistent client.
12
12
  * [Excon](https://github.com/geemus/excon) does most everything right, but is not
13
- compatible with [VCR](https://github.com/myronmarston/vcr).
13
+ compatible with [VCR](https://github.com/myronmarston/vcr) (more specifically
14
+ [webmock](https://github.com/bblimke/webmock) and [fakeweb](https://github.com/chrisk/fakeweb)).
14
15
  * Wanted to learn how to handle this pattern.
15
16
 
16
17
  The XML usage of this gem will probably change. In the next couple of weeks Hugs
@@ -19,7 +20,7 @@ will be implemented against an XML OCCI API.
19
20
  ## Assumptions
20
21
 
21
22
  * The webservice returns JSON or XML.
22
- * You want to objectify the returned JSON.
23
+ * To objectify the returned JSON or hand back a Nokogiri::XML::Document.
23
24
 
24
25
  ## Usage
25
26
 
@@ -33,4 +34,10 @@ See the 'Examples' section in the [wiki](http://github.com/retr0h/hugs/wiki/).
33
34
 
34
35
  ## Testing
35
36
 
37
+ Tests can run offline thanks to [webmock](https://github.com/bblimke/webmock).
38
+
36
39
  $ bundle exec rake
40
+
41
+ or
42
+
43
+ $ rake
data/Rakefile CHANGED
@@ -18,7 +18,7 @@ end
18
18
  require "rake/testtask"
19
19
  Rake::TestTask.new(:test) do |test|
20
20
  test.libs << "lib" << "test"
21
- test.pattern = "test/**/test_*.rb"
21
+ test.pattern = "test/**/*_test.rb"
22
22
  test.verbose = true
23
23
  end
24
24
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.1.0
1
+ 2.2.0
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{hugs}
8
- s.version = "2.1.0"
8
+ s.version = "2.2.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["retr0h"]
12
- s.date = %q{2010-12-23}
12
+ s.date = %q{2011-01-04}
13
13
  s.email = %q{john@dewey.ws}
14
14
  s.extra_rdoc_files = [
15
15
  "LICENSE",
@@ -25,16 +25,20 @@ Gem::Specification.new do |s|
25
25
  "VERSION",
26
26
  "hugs.gemspec",
27
27
  "lib/hugs.rb",
28
- "test/support.rb",
29
- "test/test_hugs.rb"
28
+ "lib/hugs/client.rb",
29
+ "lib/hugs/errors.rb",
30
+ "test/lib/hugs/client_test.rb",
31
+ "test/lib/hugs/errors_test.rb",
32
+ "test/test_helper.rb"
30
33
  ]
31
34
  s.homepage = %q{http://github.com/retr0h/hugs}
32
35
  s.require_paths = ["lib"]
33
36
  s.rubygems_version = %q{1.3.7}
34
37
  s.summary = %q{Hugs net-http-persistent with convenient get, delete, post, and put methods.}
35
38
  s.test_files = [
36
- "test/support.rb",
37
- "test/test_hugs.rb"
39
+ "test/lib/hugs/client_test.rb",
40
+ "test/lib/hugs/errors_test.rb",
41
+ "test/test_helper.rb"
38
42
  ]
39
43
 
40
44
  if s.respond_to? :specification_version then
@@ -1,133 +1 @@
1
- %w(net/http/persistent net/http/post/multipart yajl nokogiri).each { |r| require r }
2
-
3
- class Hugs
4
- Headers = {
5
- :json => "application/json",
6
- :xml => "application/xml",
7
- }.freeze
8
-
9
- Classes = [
10
- Net::HTTP::Get,
11
- Net::HTTP::Delete,
12
- Net::HTTP::Post,
13
- Net::HTTP::Put,
14
- ].freeze
15
-
16
- ##
17
- # Required options:
18
- # +host+: A String with the host to connect.
19
- # Optional:
20
- # +user+: A String containing the username for use in HTTP Basic auth.
21
- # +password+: A String containing the password for use in HTTP Basic auth.
22
- # +port+: An Integer containing the port to connect.
23
- # +scheme+: A String containing the HTTP scheme.
24
-
25
- def initialize options
26
- @user = options[:user]
27
- @password = options[:password]
28
- @host = options[:host]
29
- @port = options[:port] || 80
30
- @scheme = options[:scheme] || "https"
31
- @type = options[:type] || :json
32
- end
33
-
34
- ##
35
- # Perform an HTTP get, delete, post, or put.
36
- # +path+: A String with the path to the HTTP resource.
37
- # +params+: A Hash with the following keys:
38
- # - +:query+: Query String in the format "foo=bar"
39
- # - +:body+: A sub Hash to be JSON encoded, and posted in
40
- # the message body.
41
-
42
- Classes.each do |clazz|
43
- verb = clazz.to_s.split("::")[-1].tr 'A-Z', 'a-z'
44
-
45
- define_method verb do |*args|
46
- path = args[0]
47
- params = args[1] || {}
48
-
49
- response = response_for(clazz, path, params)
50
- response.body = parse response.body
51
- response
52
- end
53
- end
54
-
55
- ##
56
- # :method: get
57
-
58
- ##
59
- # :method: delete
60
-
61
- ##
62
- # :method: post
63
-
64
- ##
65
- # :method: put
66
-
67
- private
68
- ##
69
- # Worker method to be called by #get, #delete, #post, #put.
70
- # Method arguments have been documented in the callers.
71
-
72
- def response_for request, path, params
73
- query = params[:query] && params.delete(:query)
74
- body = params[:body] && params.delete(:body)
75
- upload = params[:upload] && params.delete(:upload)
76
-
77
- @http ||= Net::HTTP::Persistent.new
78
- @url ||= URI.parse "#{@scheme}://#{@host}:#{@port}"
79
-
80
- if upload && request.class === Net::HTTP::Post
81
- parts = upload[:parts] || {}
82
- parts[:file] = UploadIO.new(parts[:file], upload[:content_type]) if parts[:file]
83
-
84
- request = Net::HTTP::Post::Multipart.new path_with_query(path, query), parts
85
- else
86
- request = request.new path_with_query path, query
87
- request.body = encode(body) if body
88
-
89
- common_headers request
90
- end
91
-
92
- request.basic_auth(@user, @password) if requires_authentication?
93
- @http.request(@url, request)
94
- end
95
-
96
- def path_with_query path, query
97
- [path, query].compact.join "?"
98
- end
99
-
100
- def common_headers request
101
- case request
102
- when Net::HTTP::Get, Net::HTTP::Delete
103
- request.add_field "Accept", Headers[@type]
104
- when Net::HTTP::Post, Net::HTTP::Put
105
- request.add_field "Accept", Headers[@type]
106
- request.add_field "Content-Type", Headers[@type]
107
- end
108
- end
109
-
110
- def parse data
111
- if is_json?
112
- Yajl::Parser.parse data
113
- elsif is_xml?
114
- Nokogiri::XML.parse data
115
- end
116
- end
117
-
118
- def encode body
119
- is_json? ? (Yajl::Encoder.encode body) : body
120
- end
121
-
122
- def requires_authentication?
123
- @user && @password
124
- end
125
-
126
- def is_xml?
127
- @type == :xml
128
- end
129
-
130
- def is_json?
131
- @type == :json
132
- end
133
- end
1
+ require "hugs/client"
@@ -0,0 +1,152 @@
1
+ %w(hugs/errors net/http/persistent net/http/post/multipart yajl nokogiri).each { |r| require r }
2
+
3
+ module Hugs
4
+ class Client
5
+ attr_accessor :headers
6
+ attr_accessor :raise_4xx, :raise_5xx
7
+ attr_writer :raise_4xx, :raise_5xx
8
+
9
+ Headers = {
10
+ :json => "application/json",
11
+ :xml => "application/xml",
12
+ }.freeze
13
+
14
+ Classes = [
15
+ Net::HTTP::Get,
16
+ Net::HTTP::Delete,
17
+ Net::HTTP::Post,
18
+ Net::HTTP::Put,
19
+ ].freeze
20
+
21
+ ##
22
+ # Required options:
23
+ # +host+: A String with the host to connect.
24
+ # Optional:
25
+ # +user+: A String containing the username for use in HTTP Basic auth.
26
+ # +password+: A String containing the password for use in HTTP Basic auth.
27
+ # +port+: An Integer containing the port to connect.
28
+ # +scheme+: A String containing the HTTP scheme.
29
+
30
+ def initialize options
31
+ @user = options[:user]
32
+ @password = options[:password]
33
+ @host = options[:host]
34
+ @port = options[:port] || 80
35
+ @scheme = options[:scheme] || "http"
36
+ @type = options[:type] || :json
37
+ @headers = options[:headers] || {}
38
+ end
39
+
40
+ ##
41
+ # Perform an HTTP get, delete, post, or put.
42
+ # +path+: A String with the path to the HTTP resource.
43
+ # +params+: A Hash with the following keys:
44
+ # - +:query+: Query String in the format "foo=bar"
45
+ # - +:body+: A sub Hash to be JSON encoded, and posted in
46
+ # the message body.
47
+
48
+ Classes.each do |clazz|
49
+ verb = clazz.to_s.split("::").last.downcase
50
+
51
+ define_method verb do |*args|
52
+ path = args[0]
53
+ params = args[1] || {}
54
+
55
+ response_for clazz, path, params
56
+ end
57
+ end
58
+
59
+ ##
60
+ # :method: get
61
+
62
+ ##
63
+ # :method: delete
64
+
65
+ ##
66
+ # :method: post
67
+
68
+ ##
69
+ # :method: put
70
+
71
+ private
72
+ ##
73
+ # Worker method to be called by #get, #delete, #post, #put.
74
+ # Method arguments have been documented in the callers.
75
+
76
+ def response_for request, path, params
77
+ query = params[:query] && params.delete(:query)
78
+ body = params[:body] && params.delete(:body)
79
+ upload = params[:upload] && params.delete(:upload)
80
+
81
+ @http ||= Net::HTTP::Persistent.new
82
+ @url ||= URI.parse "#{@scheme}://#{@host}:#{@port}"
83
+
84
+ full_path = path_with_query path, query
85
+
86
+ if upload && request.class === Net::HTTP::Post
87
+ parts = upload[:parts] || {}
88
+ parts[:file] = UploadIO.new(parts[:file], upload[:content_type]) if parts[:file]
89
+
90
+ request = Net::HTTP::Post::Multipart.new full_path, parts
91
+ else
92
+ request = request.new full_path
93
+ request.body = encode body
94
+
95
+ add_headers request
96
+ end
97
+
98
+ request.basic_auth(@user, @password) if requires_authentication?
99
+ handle_response request
100
+ end
101
+
102
+ def handle_response request
103
+ resp = @http.request @url, request
104
+ Hugs::Errors::status_error resp, @raise_4xx, @raise_5xx
105
+ resp.body = parse resp.body
106
+
107
+ resp
108
+ end
109
+
110
+ def path_with_query path, query
111
+ [path, query].compact.join "?"
112
+ end
113
+
114
+ def add_headers request
115
+ request.add_field "Accept", Headers[@type]
116
+ if [Net::HTTP::Post, Net::HTTP::Put].include? request.class
117
+ request.add_field "Content-Type", Headers[@type]
118
+ end
119
+
120
+ @headers.each do |header, value|
121
+ request.add_field header, value
122
+ end
123
+ end
124
+
125
+ def parse data
126
+ if is_json?
127
+ Yajl::Parser.parse data
128
+ elsif is_xml?
129
+ Nokogiri::XML.parse data
130
+ else
131
+ data
132
+ end
133
+ end
134
+
135
+ def encode body
136
+ return unless body
137
+ is_json? ? (Yajl::Encoder.encode body) : body
138
+ end
139
+
140
+ def requires_authentication?
141
+ @user && @password
142
+ end
143
+
144
+ def is_xml?
145
+ @type == :xml
146
+ end
147
+
148
+ def is_json?
149
+ @type == :json
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,80 @@
1
+ module Hugs
2
+ module Errors
3
+ class Error < StandardError; end
4
+
5
+ class HTTPStatusError < Error
6
+ def initialize msg
7
+ super msg
8
+ end
9
+ end
10
+
11
+ ##
12
+ # Taken from geemus:excon/lib/excon/errors.rb
13
+
14
+ class BadRequest < HTTPStatusError; end # 400
15
+ class Unauthorized < HTTPStatusError; end # 401
16
+ class PaymentRequired < HTTPStatusError; end # 402
17
+ class Forbidden < HTTPStatusError; end # 403
18
+ class NotFound < HTTPStatusError; end # 404
19
+ class MethodNotAllowed < HTTPStatusError; end # 405
20
+ class NotAcceptable < HTTPStatusError; end # 406
21
+ class ProxyAuthenticationRequired < HTTPStatusError; end # 407
22
+ class RequestTimeout < HTTPStatusError; end # 408
23
+ class Conflict < HTTPStatusError; end # 409
24
+ class Gone < HTTPStatusError; end # 410
25
+ class LengthRequired < HTTPStatusError; end # 411
26
+ class PreconditionFailed < HTTPStatusError; end # 412
27
+ class RequestEntityTooLarge < HTTPStatusError; end # 413
28
+ class RequestURITooLong < HTTPStatusError; end # 414
29
+ class UnsupportedMediaType < HTTPStatusError; end # 415
30
+ class RequestedRangeNotSatisfiable < HTTPStatusError; end # 416
31
+ class ExpectationFailed < HTTPStatusError; end # 417
32
+ class UnprocessableEntity < HTTPStatusError; end # 422
33
+ class InternalServerError < HTTPStatusError; end # 500
34
+ class NotImplemented < HTTPStatusError; end # 501
35
+ class BadGateway < HTTPStatusError; end # 502
36
+ class ServiceUnavailable < HTTPStatusError; end # 503
37
+ class GatewayTimeout < HTTPStatusError; end # 504
38
+
39
+
40
+ def self.status_error response, raise_4xx, raise_5xx
41
+ @errors ||= {
42
+ 400 => [Hugs::Errors::BadRequest, 'Bad Request'],
43
+ 401 => [Hugs::Errors::Unauthorized, 'Unauthorized'],
44
+ 402 => [Hugs::Errors::PaymentRequired, 'Payment Required'],
45
+ 403 => [Hugs::Errors::Forbidden, 'Forbidden'],
46
+ 404 => [Hugs::Errors::NotFound, 'Not Found'],
47
+ 405 => [Hugs::Errors::MethodNotAllowed, 'Method Not Allowed'],
48
+ 406 => [Hugs::Errors::NotAcceptable, 'Not Acceptable'],
49
+ 407 => [Hugs::Errors::ProxyAuthenticationRequired, 'Proxy Authentication Required'],
50
+ 408 => [Hugs::Errors::RequestTimeout, 'Request Timeout'],
51
+ 409 => [Hugs::Errors::Conflict, 'Conflict'],
52
+ 410 => [Hugs::Errors::Gone, 'Gone'],
53
+ 411 => [Hugs::Errors::LengthRequired, 'Length Required'],
54
+ 412 => [Hugs::Errors::PreconditionFailed, 'Precondition Failed'],
55
+ 413 => [Hugs::Errors::RequestEntityTooLarge, 'Request Entity Too Large'],
56
+ 414 => [Hugs::Errors::RequestURITooLong, 'Request-URI Too Long'],
57
+ 415 => [Hugs::Errors::UnsupportedMediaType, 'Unsupported Media Type'],
58
+ 416 => [Hugs::Errors::RequestedRangeNotSatisfiable, 'Request Range Not Satisfiable'],
59
+ 417 => [Hugs::Errors::ExpectationFailed, 'Expectation Failed'],
60
+ 422 => [Hugs::Errors::UnprocessableEntity, 'Unprocessable Entity'],
61
+ 500 => [Hugs::Errors::InternalServerError, 'InternalServerError'],
62
+ 501 => [Hugs::Errors::NotImplemented, 'Not Implemented'],
63
+ 502 => [Hugs::Errors::BadGateway, 'Bad Gateway'],
64
+ 503 => [Hugs::Errors::ServiceUnavailable, 'Service Unavailable'],
65
+ 504 => [Hugs::Errors::GatewayTimeout, 'Gateway Timeout']
66
+ }
67
+
68
+ case response.code
69
+ when raise_4xx && %r{^4[0-9]{2}$} ; raise_for(response.code)
70
+ when raise_5xx && %r{^5[0-9]{2}$} ; raise_for(response.code)
71
+ end
72
+ end
73
+
74
+ private
75
+ def self.raise_for code
76
+ error, message = @errors[code.to_i]
77
+ raise error.new message
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,241 @@
1
+ %w(test_helper base64).each { |r| require r }
2
+
3
+ describe Hugs::Client do
4
+ before do
5
+ @scheme = "https"
6
+ @host = "example.com"
7
+ @port = 80
8
+ @base = "#{@host}:#{@port}"
9
+ @valid_options = {
10
+ :host => @host,
11
+ :port => @port,
12
+ :scheme => @scheme,
13
+ }
14
+
15
+ WebMock.reset!
16
+ @instance = Hugs::Client.new @valid_options
17
+ end
18
+
19
+ describe "#response_for" do
20
+ before do
21
+ @request = Net::HTTP::Get
22
+ end
23
+
24
+ describe "path" do
25
+ it "is valid" do
26
+ stub_request :get, "#{@scheme}://#{@base}/"
27
+
28
+ @instance.send :response_for, @request, "/", {}
29
+
30
+ assert_requested :get, "#{@scheme}://#{@base}/"
31
+ end
32
+
33
+ it "is valid when an invalid :query is supplied" do
34
+ stub_request :get, "#{@scheme}://#{@base}/"
35
+
36
+ @instance.send :response_for, @request, "/", :query => nil
37
+
38
+ assert_requested :get, "#{@scheme}://#{@base}/"
39
+ end
40
+
41
+ it "also has a query string" do
42
+ stub_request(:get, "#{@scheme}://#{@base}/").with:query => {"foo" => "bar"}
43
+
44
+ @instance.send :response_for, @request, "/", :query => "foo=bar"
45
+
46
+ assert_requested :get, "#{@scheme}://#{@base}/", :query => {"foo" => "bar"}
47
+ end
48
+ end
49
+
50
+ describe "multi-part" do
51
+ Content_Type_Matcher = %r{multipart/form-data}
52
+
53
+ before do
54
+ @request = Net::HTTP::Post
55
+ end
56
+
57
+ it "uploads a file" do
58
+ stub_request :post, "#{@scheme}://#{@base}/"
59
+ upload = {
60
+ :upload => {
61
+ :parts => { :file => "/dev/null" },
62
+ :content_type => "type/subtype"
63
+ }
64
+ }
65
+
66
+ @instance.send :response_for, @request, "/", upload
67
+
68
+ assert_requested :post, "#{@scheme}://#{@base}/", :body => %r{Content-Type: type/subtype}, :headers => {
69
+ "Content-Type" => Content_Type_Matcher
70
+ }
71
+ end
72
+
73
+ it "has parts" do
74
+ stub_request :post, "#{@scheme}://#{@base}/"
75
+ upload = {
76
+ :upload => {
77
+ :parts => { :foo => :bar, :baz => :xyzzy },
78
+ :content_type => "foo/bar"
79
+ }
80
+ }
81
+
82
+ @instance.send :response_for, @request, "/", upload
83
+
84
+ ### wtf can't use mx together.
85
+ content_disposition_matcher = %r{^Content-Disposition: form-data; name="foo".*^bar.*^Content-Disposition: form-data; name="baz".*^xyzzy.*}m
86
+ assert_requested :post, "#{@scheme}://#{@base}/", :body => content_disposition_matcher, :headers => {
87
+ "Content-Type" => Content_Type_Matcher
88
+ }
89
+ end
90
+ end
91
+
92
+ describe "body" do
93
+ describe "parses response" do
94
+ describe "json" do
95
+ it "objectifies and returns a hash" do
96
+ stub_request(:get, "#{@scheme}://#{@base}/").to_return :body => '{"foo":"bar"}'
97
+ instance = Hugs::Client.new @valid_options.merge(:type => :json)
98
+
99
+ response = instance.get "/"
100
+
101
+ response.body.must_be_kind_of Hash
102
+ end
103
+ end
104
+
105
+ describe "xml" do
106
+ it "parses and returns a Nokogiri object" do
107
+ stub_request(:get, "#{@scheme}://#{@base}/").to_return :body => "<STORAGE></STORAGE>"
108
+ instance = Hugs::Client.new @valid_options.merge(:type => :xml)
109
+
110
+ response = instance.get "/"
111
+
112
+ response.body.must_be_kind_of Nokogiri::XML::Document
113
+ end
114
+ end
115
+ end
116
+
117
+ describe "encodes request" do
118
+ before do
119
+ stub_request :get, "#{@scheme}://#{@base}/"
120
+ end
121
+
122
+ it "is not set when :body invalid" do
123
+ @instance.send :response_for, @request, "/", :body => nil
124
+
125
+ assert_requested :get, "#{@scheme}://#{@base}/", :body => nil
126
+ end
127
+
128
+ it "is not set when :body is missing" do
129
+ @instance.send :response_for, @request, "/", {}
130
+
131
+ assert_requested :get, "#{@scheme}://#{@base}/", {}
132
+ end
133
+
134
+ describe "json" do
135
+ it "is valid" do
136
+ instance = Hugs::Client.new @valid_options.merge(:type => :json)
137
+
138
+ instance.send :response_for, @request, "/", :body => {:foo => :bar}
139
+
140
+ assert_requested :get, "#{@scheme}://#{@base}/", :body => '{"foo":"bar"}'
141
+ end
142
+ end
143
+
144
+ describe "xml" do
145
+ it "is valid" do
146
+ instance = Hugs::Client.new @valid_options.merge(:type => :xml)
147
+
148
+ instance.send :response_for, @request, "/", :body => "foo bar"
149
+
150
+ assert_requested :get, "#{@scheme}://#{@base}/", :body => "foo bar"
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+
157
+ describe "headers" do
158
+ describe "authentication" do
159
+ it "uses basic auth when providing user and password" do
160
+ stub_request :get, "#{@scheme}://user:credentials@#{@base}/"
161
+ instance = Hugs::Client.new @valid_options.merge(:user => "user", :password => "credentials")
162
+
163
+ instance.send :response_for, @request, "/", {}
164
+ assert_requested :get, "#{@scheme}://user:credentials@#{@base}/"
165
+ end
166
+
167
+ it "doesn't use basic auth without a user" do
168
+ assert_doesnt_use_basic_auth_without :user
169
+ end
170
+
171
+ it "doesn't use basic auth without a password" do
172
+ assert_doesnt_use_basic_auth_without :password
173
+ end
174
+ end
175
+
176
+ describe "JSON" do
177
+ it "supports json GET" do
178
+ assert_supports_http_verb :json, :get
179
+ end
180
+
181
+ it "supports json DELETE" do
182
+ assert_supports_http_verb :json, :delete
183
+ end
184
+
185
+ it "supports json POST" do
186
+ assert_supports_http_verb :json, :post
187
+ end
188
+
189
+ it "supports json PUT" do
190
+ assert_supports_http_verb :json, :put
191
+ end
192
+ end
193
+
194
+ describe "XML" do
195
+ it "supports xml GET" do
196
+ assert_supports_http_verb :xml, :get
197
+ end
198
+
199
+ it "supports xml DELETE" do
200
+ assert_supports_http_verb :xml, :delete
201
+ end
202
+
203
+ it "supports xml POST" do
204
+ assert_supports_http_verb :xml, :post
205
+ end
206
+
207
+ it "supports xml PUT" do
208
+ assert_supports_http_verb :xml, :put
209
+ end
210
+ end
211
+ end
212
+
213
+ def assert_supports_http_verb type, verb
214
+ mimetype = {:xml => 'application/xml',
215
+ :json => 'application/json'}[type]
216
+
217
+ assert mimetype, "Unsupported Mimetype '#{type}'"
218
+
219
+ clazz = eval "Net::HTTP::#{verb.capitalize}"
220
+
221
+ stub_request verb, "#{@scheme}://#{@base}/"
222
+ instance = Hugs::Client.new @valid_options.merge(:type => type)
223
+
224
+ instance.send :response_for, clazz, "/", {}
225
+
226
+ headers = { "Accept" => ["*/*", mimetype] }
227
+ headers["Content-Type"] = mimetype if [:put, :put].include? type
228
+
229
+ assert_requested verb, "#{@scheme}://#{@base}/", :headers => headers
230
+ end
231
+
232
+ def assert_doesnt_use_basic_auth_without option
233
+ stub_request :get, "#{@scheme}://#{@base}/"
234
+ instance = Hugs::Client.new @valid_options.merge(option => "value")
235
+
236
+ instance.send :response_for, @request, "/", {}
237
+
238
+ assert_requested :get, "#{@scheme}://#{@base}/"
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,74 @@
1
+ require "test_helper"
2
+
3
+ describe Hugs::Errors do
4
+ before do
5
+ @scheme = "https"
6
+ @host = "example.com"
7
+ @port = 80
8
+ @base = "#{@host}:#{@port}"
9
+ @valid_options = {
10
+ :host => @host,
11
+ :port => @port,
12
+ :scheme => @scheme,
13
+ }
14
+
15
+ @instance = Hugs::Client.new @valid_options
16
+ WebMock.reset!
17
+ end
18
+
19
+ Error_4xx = {
20
+ 400 => [Hugs::Errors::BadRequest, 'Bad Request'],
21
+ 401 => [Hugs::Errors::Unauthorized, 'Unauthorized'],
22
+ 402 => [Hugs::Errors::PaymentRequired, 'Payment Required'],
23
+ 403 => [Hugs::Errors::Forbidden, 'Forbidden'],
24
+ 404 => [Hugs::Errors::NotFound, 'Not Found'],
25
+ 405 => [Hugs::Errors::MethodNotAllowed, 'Method Not Allowed'],
26
+ 406 => [Hugs::Errors::NotAcceptable, 'Not Acceptable'],
27
+ 407 => [Hugs::Errors::ProxyAuthenticationRequired, 'Proxy Authentication Required'],
28
+ 408 => [Hugs::Errors::RequestTimeout, 'Request Timeout'],
29
+ 409 => [Hugs::Errors::Conflict, 'Conflict'],
30
+ 410 => [Hugs::Errors::Gone, 'Gone'],
31
+ 411 => [Hugs::Errors::LengthRequired, 'Length Required'],
32
+ 412 => [Hugs::Errors::PreconditionFailed, 'Precondition Failed'],
33
+ 413 => [Hugs::Errors::RequestEntityTooLarge, 'Request Entity Too Large'],
34
+ 414 => [Hugs::Errors::RequestURITooLong, 'Request-URI Too Long'],
35
+ 415 => [Hugs::Errors::UnsupportedMediaType, 'Unsupported Media Type'],
36
+ 416 => [Hugs::Errors::RequestedRangeNotSatisfiable, 'Request Range Not Satisfiable'],
37
+ 417 => [Hugs::Errors::ExpectationFailed, 'Expectation Failed'],
38
+ 422 => [Hugs::Errors::UnprocessableEntity, 'Unprocessable Entity'],
39
+ }
40
+
41
+ Error_5xx = {
42
+ 500 => [Hugs::Errors::InternalServerError, 'InternalServerError'],
43
+ 501 => [Hugs::Errors::NotImplemented, 'Not Implemented'],
44
+ 502 => [Hugs::Errors::BadGateway, 'Bad Gateway'],
45
+ 503 => [Hugs::Errors::ServiceUnavailable, 'Service Unavailable'],
46
+ 504 => [Hugs::Errors::GatewayTimeout, 'Gateway Timeout']
47
+ }
48
+
49
+ describe "error codes" do
50
+ before do
51
+ @instance.raise_4xx = false
52
+ @instance.raise_5xx = false
53
+ end
54
+
55
+ (Error_4xx.merge(Error_5xx)).each_pair do |code, errors|
56
+ error, message = errors
57
+
58
+ it "raises" do
59
+ (code.to_s =~ %r{^4[0-9]{2}$}) ? (@instance.raise_4xx = true) : (@instance.raise_5xx = true)
60
+ stub_request(:get, "#{@scheme}://#{@base}/").to_return :status => [code, message]
61
+
62
+ lambda { @instance.send :response_for, Net::HTTP::Get, "/", {} }.must_raise error
63
+ end
64
+
65
+ it "doesn't raise" do
66
+ stub_request(:get, "#{@scheme}://#{@base}/").to_return :status => [code, message]
67
+
68
+ response = @instance.send :response_for, Net::HTTP::Get, "/", {}
69
+
70
+ response.code.must_equal code.to_s
71
+ end
72
+ end
73
+ end
74
+ end
@@ -1,3 +1,4 @@
1
+ require "bundler"
1
2
  Bundler.setup :default, :test
2
3
 
3
4
  %w(minitest/spec webmock ./lib/hugs).each { |r| require r }
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 2
7
- - 1
7
+ - 2
8
8
  - 0
9
- version: 2.1.0
9
+ version: 2.2.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - retr0h
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-12-23 00:00:00 -08:00
17
+ date: 2011-01-04 00:00:00 -08:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -154,8 +154,11 @@ files:
154
154
  - VERSION
155
155
  - hugs.gemspec
156
156
  - lib/hugs.rb
157
- - test/support.rb
158
- - test/test_hugs.rb
157
+ - lib/hugs/client.rb
158
+ - lib/hugs/errors.rb
159
+ - test/lib/hugs/client_test.rb
160
+ - test/lib/hugs/errors_test.rb
161
+ - test/test_helper.rb
159
162
  has_rdoc: true
160
163
  homepage: http://github.com/retr0h/hugs
161
164
  licenses: []
@@ -189,5 +192,6 @@ signing_key:
189
192
  specification_version: 3
190
193
  summary: Hugs net-http-persistent with convenient get, delete, post, and put methods.
191
194
  test_files:
192
- - test/support.rb
193
- - test/test_hugs.rb
195
+ - test/lib/hugs/client_test.rb
196
+ - test/lib/hugs/errors_test.rb
197
+ - test/test_helper.rb
@@ -1,228 +0,0 @@
1
- %w(support base64 hugs).each { |r| require r }
2
-
3
- describe Hugs do
4
- before do
5
- @scheme = "https"
6
- @host = "example.com"
7
- @port = 80
8
- @base = "#{@host}:#{@port}"
9
- @valid_options = {
10
- :host => @host,
11
- :port => @port,
12
- :scheme => @scheme,
13
- }
14
-
15
- WebMock.reset!
16
- @instance = Hugs.new @valid_options
17
- end
18
-
19
- describe "#response_for" do
20
- before do
21
- @request = Net::HTTP::Get
22
- end
23
-
24
- describe "path" do
25
- it "is valid" do
26
- stub_request :get, "#{@scheme}://#{@base}/"
27
-
28
- @instance.send :response_for, @request, "/", {}
29
-
30
- assert_requested :get, "#{@scheme}://#{@base}/"
31
- end
32
-
33
- it "is valid when an invalid :query is supplied" do
34
- stub_request :get, "#{@scheme}://#{@base}/"
35
-
36
- @instance.send :response_for, @request, "/", :query => nil
37
-
38
- assert_requested :get, "#{@scheme}://#{@base}/"
39
- end
40
-
41
- it "also has a query string" do
42
- stub_request(:get, "#{@scheme}://#{@base}/").with:query => {"foo" => "bar"}
43
-
44
- @instance.send :response_for, @request, "/", :query => "foo=bar"
45
-
46
- assert_requested :get, "#{@scheme}://#{@base}/", :query => {"foo" => "bar"}
47
- end
48
- end
49
-
50
- describe "multi-part" do
51
- Content_Type_Matcher = %r{multipart/form-data}
52
-
53
- before do
54
- @request = Net::HTTP::Post
55
- end
56
-
57
- it "uploads a file" do
58
- stub_request :post, "#{@scheme}://#{@base}/"
59
-
60
- upload = {
61
- :upload => {
62
- :parts => { :file => "/dev/null" },
63
- :content_type => "type/subtype"
64
- }
65
- }
66
-
67
- @instance.send :response_for, @request, "/", upload
68
-
69
- assert_requested :post, "#{@scheme}://#{@base}/", :body => %r{Content-Type: type/subtype}, :headers => {
70
- "Content-Type" => Content_Type_Matcher
71
- }
72
- end
73
-
74
- it "has parts" do
75
- stub_request :post, "#{@scheme}://#{@base}/"
76
-
77
- upload = {
78
- :upload => {
79
- :parts => { :foo => :bar, :baz => :xyzzy },
80
- :content_type => "foo/bar"
81
- }
82
- }
83
-
84
- @instance.send :response_for, @request, "/", upload
85
-
86
- ### wtf can't use mx together.
87
- content_disposition_matcher = %r{^Content-Disposition: form-data; name="foo".*^bar.*^Content-Disposition: form-data; name="baz".*^xyzzy.*}m
88
- assert_requested :post, "#{@scheme}://#{@base}/", :body => content_disposition_matcher, :headers => {
89
- "Content-Type" => Content_Type_Matcher
90
- }
91
- end
92
- end
93
-
94
- describe "body" do
95
- before do
96
- stub_request :get, "#{@scheme}://#{@base}/"
97
- end
98
-
99
- it "is not set when :body invalid" do
100
- @instance.send :response_for, @request, "/", :body => nil
101
-
102
- assert_requested :get, "#{@scheme}://#{@base}/", :body => nil
103
- end
104
-
105
- it "is not set when :body is missing" do
106
- @instance.send :response_for, @request, "/", {}
107
-
108
- assert_requested :get, "#{@scheme}://#{@base}/", {}
109
- end
110
-
111
- describe "json" do
112
- before do
113
- @instance = Hugs.new @valid_options.merge(:type => :json)
114
- end
115
-
116
- it "is valid" do
117
- @instance.send :response_for, @request, "/", :body => {:foo => :bar}
118
-
119
- assert_requested :get, "#{@scheme}://#{@base}/", :body => '{"foo":"bar"}'
120
- end
121
- end
122
-
123
- describe "xml" do
124
- before do
125
- @instance = Hugs.new @valid_options.merge(:type => :xml)
126
- end
127
-
128
- it "is valid" do
129
- @instance.send :response_for, @request, "/", :body => "foo bar"
130
-
131
- assert_requested :get, "#{@scheme}://#{@base}/", :body => "foo bar"
132
- end
133
- end
134
- end
135
-
136
- describe "headers" do
137
- it "authenticates" do
138
- stub_request :get, "#{@scheme}://user:credentials@#{@base}/"
139
-
140
- @instance = Hugs.new @valid_options.merge(:user => "user", :password => "credentials")
141
-
142
- @instance.send :response_for, @request, "/", {}
143
-
144
- assert_requested :get, "#{@scheme}://user:credentials@#{@base}/"
145
- end
146
-
147
- [:user, :password].each do |option|
148
- it "doesn't authenticate when '#{option}' missing" do
149
- stub_request :get, "#{@scheme}://#{@base}/"
150
-
151
- invalid_options = @valid_options.reject { |k,v| k == option }
152
- @instance = Hugs.new invalid_options
153
-
154
- @instance.send :response_for, @request, "/", {}
155
-
156
- assert_requested :get, "#{@scheme}://#{@base}/"
157
- end
158
- end
159
-
160
- [
161
- { :json => "application/json" },
162
- { :xml => "application/xml" },
163
- ].each do |pair|
164
- pair.each_pair do |type, subtype|
165
- [:post, :put].each do |verb|
166
- clazz = eval "Net::HTTP::#{verb.capitalize}"
167
-
168
- it "has '#{subtype}' Content-Type and Accept for '#{clazz}'" do
169
- stub_request verb, "#{@scheme}://#{@base}/"
170
-
171
- @instance = Hugs.new @valid_options.merge(:type => type)
172
-
173
- @instance.send :response_for, clazz, "/", {}
174
-
175
- assert_requested verb, "#{@scheme}://#{@base}/", :headers => {
176
- "Accept" => ["*/*", subtype], "Content-Type" => subtype }
177
- end
178
- end
179
-
180
- [:get, :delete].each do |verb|
181
- clazz = eval "Net::HTTP::#{verb.capitalize}"
182
-
183
- it "has '#{subtype}' Accept for '#{clazz}'" do
184
- stub_request verb, "#{@scheme}://#{@base}/"
185
-
186
- @instance = Hugs.new @valid_options.merge(:type => type)
187
-
188
- @instance.send :response_for, clazz, "/", {}
189
-
190
- assert_requested verb, "#{@scheme}://#{@base}/", :headers => {
191
- "Accept" => ["*/*", subtype] }
192
- end
193
- end
194
- end
195
- end
196
- end
197
- end
198
-
199
- describe "HTTP methods" do
200
- describe "json" do
201
- before do
202
- @instance = Hugs.new @valid_options.merge(:type => :json)
203
- end
204
-
205
- it "objectifies and returns a hash" do
206
- stub_request(:get, "#{@scheme}://#{@base}/").to_return :body => '{"foo":"bar"}'
207
-
208
- response = @instance.get "/", :body => { :foo => :bar }
209
-
210
- response.body.must_be_kind_of Hash
211
- end
212
- end
213
-
214
- describe "xml" do
215
- before do
216
- @instance = Hugs.new @valid_options.merge(:type => :xml)
217
- end
218
-
219
- it "parses and returns a Nokogiri object" do
220
- stub_request(:get, "#{@scheme}://#{@base}/").to_return :body => "<STORAGE></STORAGE>"
221
-
222
- response = @instance.get "/", :body => { :foo => :bar }
223
-
224
- response.body.must_be_kind_of Nokogiri::XML::Document
225
- end
226
- end
227
- end
228
- end