hugs 2.1.0 → 2.2.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.
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