simple-http 0.1.5 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 629d4ae6ecbf63473054f8cdc95b88bdae1a5251
4
- data.tar.gz: 782dab2622b3d481d9dd5eabc77231707a5b0abe
2
+ SHA256:
3
+ metadata.gz: c92784b07cf1f548d0c40078b19609308a45788bbc550a42305c1bf26c6e32a0
4
+ data.tar.gz: e9c7ad66790380696674c6a6566581e87eef1735b2f7dd6656918908600df04c
5
5
  SHA512:
6
- metadata.gz: 6b6b5133cd3ad03c1f2bf29a96e8c21bfdfd5df97de3c57aaa3dc1da6da3e59dc964085f1e86cf8255ca4126aad3d5871f715f9061f47a36277a53634c0c1a2a
7
- data.tar.gz: d6c270201e14f28b6edde03a28571a9bcc46aa792a0751ac07b61a5144331a9f5d07c4c6e548f3f9abc3aa9ee0c48abb2495c3dc38e5b24b81e9bc3525ca78ad
6
+ metadata.gz: eb4bbedf1338cc0c657e3e1c7ac38d3e243656d5128c25d2f8252eaa8bd5a228e50bc17cf967eac469a5f15db873899dcee187962effe812a15ff05dca007bf0
7
+ data.tar.gz: ce18010f695ae2be693552cfc41ed001490d47490ecd1be6fc22fa6d6847cc19506c007e3741ef1a8522590529f090247479d8e802aa5e30389d78f229d44898
@@ -10,197 +10,147 @@ module Simple; end
10
10
  class Simple::HTTP; end
11
11
 
12
12
  require_relative "http/version"
13
- require_relative "http/result"
14
- require_relative "http/expires_in"
13
+ require_relative "http/caching"
15
14
  require_relative "http/errors"
15
+ require_relative "http/headers"
16
+ require_relative "http/request"
17
+ require_relative "http/response"
18
+
19
+ require "openssl"
16
20
 
17
21
  #
18
- # A very simple, Net::HTTP-based HTTP client.
22
+ # A very simple, Net::HTTP-based HTTP client.
19
23
  #
20
24
  # Has some support for transferring JSON data: all data in PUT and POST
21
25
  # requests are jsonized, and all data in responses are parsed as JSON if
22
26
  # the Content-Type header is set to "application/json".
23
27
  class Simple::HTTP
24
-
25
28
  #
26
29
  # The logger instance.
27
- attr :logger, true
30
+ attr_accessor :logger
28
31
 
29
32
  #
30
- # The base URL: when set, all requests that do not start with http: or
31
- # https: are done relative to this base URL.
32
- attr :base_url, true
33
-
33
+ # The base URL when set, all requests that do not start with http: or
34
+ # https: are done relative to this base URL.
35
+ attr_accessor :base_url
36
+
34
37
  #
35
- # When set (default), redirections are followed. Note: When
38
+ # When set (default), redirections are followed. Note: When
36
39
  # follows_redirections is not set, a HTTP redirection would raise an
37
40
  # error - which is probably only useful when testing an interface.
38
- attr :follows_redirections, true
41
+ attr_accessor :follows_redirections
39
42
 
40
43
  #
41
44
  # When set, appends this to all request URLs
42
- attr :default_params, true
45
+ attr_accessor :default_params
43
46
 
44
47
  #
45
48
  # When set, sets Authorization headers to all requests. Must be an
46
49
  # array [ username, password ].
47
- attr :basic_auth, true
50
+ attr_accessor :basic_auth
48
51
 
49
52
  def initialize
50
53
  self.follows_redirections = true
51
- self.logger = Logger.new(STDERR)
52
- self.logger.level = Logger::WARN
54
+ self.logger = Logger.new(STDERR, level: :warn)
53
55
  end
54
56
 
55
- def get(url, headers = {}); http :GET, url, nil, headers; end
56
- def post(url, body = nil, headers = {}); http :POST, url, body, headers; end
57
- def put(url, body = nil, headers = {}); http :PUT, url, body, headers; end
58
- def delete(url, headers = {}); http :DELETE, url, nil, headers; end
57
+ def head(url, headers = {})
58
+ request :HEAD, url, nil, headers
59
+ end
59
60
 
60
- #
61
- # -- Caching ----------------------------------------------------------------
61
+ def get(url, headers = {})
62
+ request :GET, url, nil, headers
63
+ end
62
64
 
63
- # When set, and a response is cacheable (as it returns a valid expires_in
64
- # value), the cache object is used to cache responses.
65
- attr :cache, true
65
+ def post(url, body = nil, headers = {})
66
+ request :POST, url, body, headers
67
+ end
66
68
 
67
- #
68
- # when does the response expire? By default, calculates expiration based
69
- # on response headers. Override as needed.
70
- def expires_in(response)
71
- response.expires_in
69
+ def put(url, body = nil, headers = {})
70
+ request :PUT, url, body, headers
72
71
  end
73
72
 
74
- private
73
+ def delete(url, headers = {})
74
+ request :DELETE, url, nil, headers
75
+ end
76
+
77
+ #
78
+ # -- Caching ----------------------------------------------------------------
79
+
80
+ # When set, and a response is cacheable (as it returns a valid expires_in
81
+ # value), the cache object is used to cache responses.
82
+ attr_accessor :cache
75
83
 
76
84
  # -- HTTP request -----------------------------------------------------------
77
85
 
78
- def http(method, url, body = nil, headers)
86
+ def request(verb, url, body, headers)
79
87
  #
80
88
  # normalize url; i.e. prepend base_url if the url itself is incomplete.
81
89
  unless url =~ /^(http|https):/
82
- url = File.join(base_url, url)
90
+ url = File.join(base_url, url)
83
91
  end
84
-
92
+
93
+ uri = URI.parse(url)
94
+ unless uri.is_a?(URI::HTTP)
95
+ raise ArgumentError, "Invalid URL: #{url}"
96
+ end
97
+
85
98
  # append default_params, if set
86
99
  if default_params
87
100
  url.concat(url.include?("?") ? "&" : "?")
88
101
  url.concat default_params
89
102
  end
90
103
 
91
- # "raw" execute request
92
- http_ method, url, body, headers
104
+ request = Request.new(verb: verb, url: url, body: body, headers: headers)
105
+
106
+ execute_request(request)
93
107
  end
94
108
 
95
- #
96
- # do a HTTP request, return its response or, when not successful,
97
- # raise an error.
98
- def http_(method, url, body, headers, max_redirections = 10)
99
- if method == :GET && cache && result = cache.read(url)
100
- logger.debug "#{method} #{url}: using cached result"
101
- return result
102
- end
109
+ private
103
110
 
104
- uri = URI.parse(url)
105
- unless uri.is_a?(URI::HTTP)
106
- raise ArgumentError, "Invalid URL: #{url}"
107
- end
111
+ def execute_request(request, max_redirections: 10)
112
+ response = execute_request_w_caching(request)
108
113
 
109
- http = Net::HTTP.new(uri.host, uri.port)
110
- if uri.scheme == "https"
111
- http.use_ssl = true
112
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
113
- end
114
+ return response unless response.status >= 300 && response.status <= 399
115
+ return response unless follows_redirections
114
116
 
115
- #
116
- # build request
117
- request = build_request method, uri, body, headers
117
+ raise ::Simple::HTTP::TooManyRedirections, response if max_redirections <= 0
118
118
 
119
- #
120
- # execute request
121
- started_at = Time.now
122
- response = http.request(request)
123
- logger.info "#{method} #{url}: #{response.body.bytesize} byte, #{"%.3f secs" % (Time.now - started_at)}"
119
+ request = ::Simple::HTTP::Request.new(verb: :GET, url: response.headers["Location"], headers: {})
120
+ execute_request(request, max_redirections: max_redirections - 1)
121
+ end
124
122
 
125
- #
126
- # Most of the times Net::HTTP#request returns a response with the :uri
127
- # attribute set, but sometimes not. We make sure that the uri is set
128
- # everytime.
129
- response.uri = uri
123
+ def execute_request_w_caching(request)
124
+ is_cacheable = request.verb == :GET && cache
130
125
 
131
- #
132
- # handle successful responses.
133
- if response.is_a?(Net::HTTPSuccess)
134
- result = response.result
135
- if cache && method == :GET && expires_in = self.expires_in(response)
136
- if response.expires_in == expires_in
137
- logger.debug "#{method} #{url}: store in cache, w/expiration of #{expires_in}"
138
- else
139
- logger.debug "#{method} #{url}: store in cache, w/custom expiration of #{expires_in} (instead #{response.expires_in.inspect})"
140
- end
141
- cache.write(url, result, expires_in: expires_in)
142
- end
143
-
144
- return result
126
+ if is_cacheable && (cached = Caching.fetch(request))
127
+ return cached
145
128
  end
146
129
 
147
- #
148
- # handle redirections.
149
- if response.is_a?(Net::HTTPRedirection) && self.follows_redirections
150
- if max_redirections <= 0
151
- raise TooManyRedirections.new(method, request, respons)
152
- end
153
-
154
- target_url = response["Location"]
155
- unless target_url =~ /^(http|https):\/\//
156
- base_url = "#{uri.scheme}://#{uri.host}"
157
- File.join base_url, target_url
158
- end
159
-
160
- return http_(:GET, target_url, nil, {}, max_redirections - 1)
130
+ response = execute_request_w_logging(request)
131
+
132
+ expires_in = Caching.determine_expires_in(response)
133
+ if is_cacheable && expires_in
134
+ Caching.store(request, response, expires_in: expires_in)
161
135
  end
162
136
 
163
- #
164
- # raise an error in any other case.
165
- raise Error.new(method, request, response)
137
+ response
166
138
  end
167
139
 
168
- private
140
+ def execute_request_w_logging(request)
141
+ started_at = Time.now
169
142
 
170
- REQUEST_CLASSES = {
171
- :GET => Net::HTTP::Get,
172
- :POST => Net::HTTP::Post,
173
- :PUT => Net::HTTP::Put,
174
- :DELETE => Net::HTTP::Delete
175
- }.freeze #:nodoc:
143
+ response = driver.execute_request(request, client: self)
176
144
 
177
- #
178
- # build a HTTP request object.
179
- def build_request(method, uri, body, headers)
180
- klass = REQUEST_CLASSES.fetch(method)
181
- request = klass.new(uri.request_uri)
182
-
183
- if uri.user
184
- request.basic_auth(uri.user, uri.password)
185
- elsif basic_auth
186
- request.basic_auth(*basic_auth)
145
+ logger.info do
146
+ "#{request}: #{response}, #{"%.3f secs" % (Time.now - started_at)}"
187
147
  end
188
148
 
189
- # set request headers
190
- # unless headers && !headers.empty?
191
- # # TODO: set headers
192
- # # set_request_headers request, headers
193
- # end
194
-
195
- # set request body
196
- if request.request_body_permitted? && body
197
- request.content_type = "application/json"
198
- if body.is_a?(Hash) || body.is_a?(Array)
199
- body = JSON.generate(body)
200
- end
201
- request.body = body
202
- end
149
+ response
150
+ end
203
151
 
204
- request
152
+ def driver
153
+ require "simple/http/driver/default"
154
+ ::Simple::HTTP::Driver::Default
205
155
  end
206
156
  end
@@ -0,0 +1,71 @@
1
+ # This module originally was taken from rack/media_type.rb and modified:
2
+ # we don't want to have a rack (which is a server toolset) dependency
3
+ # in simple-http, which is a client after all.
4
+
5
+ # parses content_type to return media_type and charset, and can reencode bodies.
6
+ class Simple::HTTP::BodyBuilder
7
+ SPLIT_PATTERN = %r{\s*[;,]\s*}
8
+
9
+ def initialize(content_type)
10
+ @media_type = content_type.split(SPLIT_PATTERN, 2).first.downcase if content_type
11
+ @charset = fetch_content_type_param(content_type, "charset", default: nil)
12
+ end
13
+
14
+ # The media type (type/subtype) portion of the CONTENT_TYPE header
15
+ # without any media type parameters. e.g., when CONTENT_TYPE is
16
+ # "text/plain;charset=utf-8", the media-type is "text/plain".
17
+ #
18
+ # For more information on the use of media types in HTTP, see:
19
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
20
+ attr_reader :media_type
21
+
22
+ # The charset as embedded on the Content-Type header
23
+ attr_reader :charset
24
+
25
+ # returns the body
26
+ #
27
+ # This method reencodes the text body into UTF-8. Non-text bodies should be
28
+ # encoded as ASCII-8BIT (a.k.a. "BINARY")
29
+ def reencode(body)
30
+ body&.encode(best_encoding)
31
+ end
32
+
33
+ private
34
+
35
+ BINARY = ::Encoding.find "ASCII-8BIT"
36
+ UTF8 = ::Encoding.find "UTF-8"
37
+
38
+ # returns the encoding we want for the body.
39
+ #
40
+ # This method makes sure to reencode text bodies into UTF-8, and non-text
41
+ # bodies as ASCII-8BIT (a.k.a. "BINARY")
42
+ def best_encoding
43
+ case media_type
44
+ when /^application\/json/, /^application\/xml/, /^text\// then UTF8
45
+ else BINARY
46
+ end
47
+ end
48
+
49
+ # The media type parameters provided in CONTENT_TYPE as a Hash, or
50
+ # an empty Hash if no CONTENT_TYPE or media-type parameters were
51
+ # provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8",
52
+ # this method responds with the following Hash:
53
+ # { 'charset' => 'utf-8' }
54
+ def fetch_content_type_param(content_type, parameter_name, default:)
55
+ return default if content_type.nil?
56
+
57
+ parameter_name = parameter_name.downcase
58
+
59
+ content_type.split(SPLIT_PATTERN)[1..-1]
60
+ .each do |s|
61
+ k, v = s.split("=", 2)
62
+ return strip_doublequotes(v) if k.downcase == parameter_name
63
+ end
64
+
65
+ default
66
+ end
67
+
68
+ def strip_doublequotes(str)
69
+ str[0] == '"' && str[-1] == '"' ? str[1..-2] : str
70
+ end
71
+ end
@@ -0,0 +1,46 @@
1
+ # This file is part of the simple-http ruby gem.
2
+ #
3
+ # Copyright (c) 2011 - 2015 @radiospiel
4
+ # Distributed under the terms of the modified BSD license, see LICENSE.BSD
5
+
6
+ module Simple::HTTP::Caching
7
+ extend self
8
+
9
+ #
10
+ # returns expiration information, in seconds from now.
11
+ def determine_expires_in(response)
12
+ return nil unless cacheable_status?(response.status)
13
+
14
+ expires_in = parse_cache_control response
15
+ expires_in ||= parse_expires response
16
+
17
+ return expires_in if expires_in && expires_in > 0
18
+ end
19
+
20
+ private
21
+
22
+ def cacheable_status?(status)
23
+ (status >= 200 && status <= 299) || (status >= 400 && status <= 499)
24
+ end
25
+
26
+ # Returns the expiration setting from the "Cache-Control"'s max-age entry
27
+ def parse_cache_control(response)
28
+ cache_control = response.headers["Cache-Control"]
29
+ return unless cache_control
30
+
31
+ cache_control.split(/,\s+/).each do |part|
32
+ next unless part =~ /max-age=(\d+)/
33
+
34
+ return Integer($1)
35
+ end
36
+ nil
37
+ end
38
+
39
+ # Returns the expiration setting from the "Expires" header
40
+ def parse_expires(response)
41
+ expires = response.headers["Expires"]
42
+ return unless expires
43
+
44
+ Time.parse(expires) - Time.now
45
+ end
46
+ end
@@ -0,0 +1,81 @@
1
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
2
+
3
+ module Simple::HTTP::Driver
4
+ end
5
+
6
+ module Simple::HTTP::Driver::Default
7
+ extend self
8
+
9
+ #
10
+ # do a HTTP request, return its response or, when not successful,
11
+ # raise an error.
12
+ def execute_request(request, client:)
13
+ verb, url, body, headers =
14
+ request.verb, request.url, request.body, request.headers
15
+
16
+ uri = URI.parse(url)
17
+
18
+ # build Net::HTTP request
19
+ request = build_request verb, uri, body, headers, client: client
20
+
21
+ # execute request
22
+ net_http = load_net_http(uri.scheme, uri.host, uri.port)
23
+ resp = net_http.request(request)
24
+
25
+ ::Simple::HTTP::Response.new request: request,
26
+ status: Integer(resp.code),
27
+ message: resp.message,
28
+ headers: Simple::HTTP::Headers.new(resp),
29
+ body: resp.body
30
+ end
31
+
32
+ private
33
+
34
+ # rubocop:disable Style/HashSyntax, Layout/AlignHash
35
+ REQUEST_CLASSES = {
36
+ :HEAD => Net::HTTP::Head,
37
+ :GET => Net::HTTP::Get,
38
+ :POST => Net::HTTP::Post,
39
+ :PUT => Net::HTTP::Put,
40
+ :DELETE => Net::HTTP::Delete
41
+ }.freeze #:nodoc:
42
+
43
+ #
44
+ # build a HTTP request object.
45
+ def build_request(method, uri, body, _headers, client:)
46
+ klass = REQUEST_CLASSES.fetch(method)
47
+ request = klass.new(uri.request_uri)
48
+
49
+ if uri.user
50
+ request.basic_auth(uri.user, uri.password)
51
+ elsif client.basic_auth
52
+ request.basic_auth(*client.basic_auth)
53
+ end
54
+
55
+ # set request headers
56
+ # unless headers && !headers.empty?
57
+ # # TODO: set headers
58
+ # # set_request_headers request, headers
59
+ # end
60
+
61
+ # set request body
62
+ if request.request_body_permitted? && body
63
+ request.content_type = "application/json"
64
+ if body.is_a?(Hash) || body.is_a?(Array)
65
+ body = JSON.generate(body)
66
+ end
67
+ request.body = body
68
+ end
69
+
70
+ request
71
+ end
72
+
73
+ def load_net_http(scheme, host, port)
74
+ net_http = Net::HTTP.new(host, port)
75
+ if scheme == "https"
76
+ net_http.use_ssl = true
77
+ net_http.verify_mode = OpenSSL::SSL::VERIFY_PEER
78
+ end
79
+ net_http
80
+ end
81
+ end
@@ -7,27 +7,35 @@ module Simple; end
7
7
  class Simple::HTTP; end
8
8
 
9
9
  class Simple::HTTP::Error < RuntimeError
10
- attr :method, :request, :response
10
+ attr_reader :response
11
11
 
12
- def initialize(method, request, response)
13
- @method, @request, @response = method, request, response
12
+ def initialize(response)
13
+ @response = response
14
14
  end
15
15
 
16
- def code
17
- response.code.to_i
16
+ def status
17
+ response.status
18
+ end
19
+
20
+ def request
21
+ @response.request
22
+ end
23
+
24
+ def verb
25
+ request.class::METHOD
18
26
  end
19
27
 
20
28
  def message
21
- message = "#{method} #{response.uri} ##{response.code} #{response.message}"
22
- if response.is_a?(Net::HTTPRedirection)
23
- message += " To #{response["Location"]}"
24
- end
25
- message
29
+ "#{verb} #{request.uri} ##{status} #{response.message}"
26
30
  end
27
31
  end
28
32
 
29
33
  class Simple::HTTP::TooManyRedirections < Simple::HTTP::Error
34
+ def location
35
+ response["Location"]
36
+ end
37
+
30
38
  def message
31
- "Too many redirections; after\n#{super}"
39
+ "#{super}: too many redirections (latest to #{location})"
32
40
  end
33
41
  end
@@ -0,0 +1,34 @@
1
+ class Simple::HTTP::Headers < Hash
2
+ def initialize(headers)
3
+ case headers
4
+ when Net::HTTPHeader
5
+ headers.each_name.map do |name|
6
+ values = headers.get_fields(name)
7
+ self[name] = values.length == 1 ? values.first : values
8
+ end
9
+ else
10
+ headers.each do |name, value|
11
+ self[name] = value
12
+ end
13
+ end
14
+ end
15
+
16
+ def [](key)
17
+ super key.downcase
18
+ end
19
+
20
+ def value(key)
21
+ values(key)&.first
22
+ end
23
+
24
+ def values(key)
25
+ v = self[key]
26
+ Array(v) if v
27
+ end
28
+
29
+ private
30
+
31
+ def []=(key, value)
32
+ super key.downcase, value
33
+ end
34
+ end
@@ -0,0 +1,12 @@
1
+ class Simple::HTTP::Request
2
+ attr_reader :verb, :url, :body, :headers
3
+
4
+ def initialize(verb:, url:, body: nil, headers:)
5
+ @verb, @url, @body, @headers = verb, url, body, headers
6
+ end
7
+
8
+ def to_s
9
+ scrubbed_url = url.gsub(/\/\/(.*):(.*)@/) { |_| "//#{$1}:xxxxxx@" }
10
+ "#{verb} #{scrubbed_url}"
11
+ end
12
+ end
@@ -0,0 +1,63 @@
1
+ require_relative "./body_builder"
2
+
3
+ class Simple::HTTP::Response
4
+ BodyBuilder = Simple::HTTP::BodyBuilder
5
+
6
+ attr_reader :request
7
+ attr_reader :status
8
+ attr_reader :message
9
+ attr_reader :headers
10
+ attr_reader :original_body
11
+
12
+ def initialize(request:, body:, headers:, status:, message:)
13
+ @request, @headers = request, headers
14
+ @status, @message = status, message
15
+ @original_body = body
16
+
17
+ # adjust encoding on original_body
18
+ @body_builder = BodyBuilder.new(headers["Content-Type"])
19
+
20
+ if @original_body && (charset = @body_builder.charset)
21
+ @original_body.force_encoding(charset)
22
+ end
23
+ end
24
+
25
+ # e.g "text/plain"
26
+ def media_type
27
+ @body_builder.media_type
28
+ end
29
+
30
+ # returns the body
31
+ #
32
+ # This method reencodes the text body into UTF-8. Non-text bodies should be
33
+ # encoded as ASCII-8BIT (a.k.a. "BINARY")
34
+ def body
35
+ @body ||= @body_builder.reencode(@original_body)
36
+ end
37
+
38
+ def bytes
39
+ @original_body&.byte_size || 0
40
+ end
41
+
42
+ def to_s
43
+ "#{status} #{message.gsub(/\s+$/, "")} (#{bytes} byte)"
44
+ end
45
+
46
+ # evaluate and potentially parses response body.
47
+ # raises an Simple::Http::Error if the result code is not a 2xx
48
+ def content!
49
+ raise Error, self unless status >= 200 && status <= 299
50
+
51
+ content
52
+ end
53
+
54
+ # evaluate and potentially parse response
55
+ def content
56
+ case media_type
57
+ when "application/json"
58
+ JSON.parse(body) unless body.empty?
59
+ else
60
+ body
61
+ end
62
+ end
63
+ end
@@ -5,5 +5,26 @@
5
5
 
6
6
  module Simple; end
7
7
  class Simple::HTTP
8
- VERSION = "0.1.5"
8
+
9
+ module GemHelper
10
+ extend self
11
+
12
+ def version(name)
13
+ spec = Gem.loaded_specs[name]
14
+ version = spec ? spec.version.to_s : "0.0.0"
15
+ # version += "+unreleased" if !spec || unreleased?(spec)
16
+ version
17
+ end
18
+
19
+ private
20
+
21
+ def unreleased?(spec)
22
+ return false unless defined?(Bundler::Source::Gemspec)
23
+ return true if spec.source.is_a?(::Bundler::Source::Gemspec)
24
+ return true if spec.source.is_a?(::Bundler::Source::Path)
25
+ false
26
+ end
27
+ end
28
+
29
+ VERSION = GemHelper.version "simple-http"
9
30
  end
@@ -7,14 +7,14 @@ require_relative 'test_helper'
7
7
  class SimpleHttpTest < Simple::HTTP::TestCase
8
8
  def test_assert_http_error
9
9
  assert_http_error(456) do
10
- http.get "http://eu.httpbin.org/status/456"
10
+ http.get "/status/456"
11
11
  end
12
12
  end
13
13
 
14
14
  def test_assert_http_redirects_to
15
15
  http.follows_redirections = false
16
16
  assert_http_redirects_to "http://eu.httpbin.org" do
17
- http.get "http://eu.httpbin.org/redirect-to?url=http://eu.httpbin.org"
17
+ http.get "/redirect-to?url=http://eu.httpbin.org"
18
18
  end
19
19
  end
20
20
  end
@@ -6,35 +6,35 @@ require_relative 'test_helper'
6
6
 
7
7
  class SimpleHttpTest < Simple::HTTP::TestCase
8
8
  def test_basic_auth
9
- http.basic_auth = [ "user", "passwd" ]
10
- http.get "http://eu.httpbin.org/basic-auth/user/passwd"
9
+ http.basic_auth = [ "admin", "secret" ]
10
+ http.get "/basic-auth/user/passwd"
11
11
  end
12
12
 
13
13
  def test_basic_auth_invalid
14
14
  http.basic_auth = [ "wronguser", "passwd" ]
15
15
  assert_http_error 401 do
16
- http.get "http://eu.httpbin.org/basic-auth/user/passwd"
16
+ http.get "/basic-auth/user/passwd"
17
17
  end
18
18
  end
19
19
 
20
20
  def test_basic_auth_missing
21
21
  http.basic_auth = [ "wronguser", "passwd" ]
22
22
  assert_http_error 401 do
23
- http.get "http://eu.httpbin.org/basic-auth/user/passwd"
23
+ http.get "/basic-auth/user/passwd"
24
24
  end
25
25
  end
26
26
 
27
27
  def test_url_auth
28
- http.get "http://user:passwd@eu.httpbin.org/basic-auth/user/passwd"
28
+ http.get "http://admin:secret@#{HOST}:#{PORT}/basic-auth/user/passwd"
29
29
  end
30
30
 
31
31
  def test_url_auth_overrides_basic_auth
32
32
  http.basic_auth = [ "wronguser", "passwd" ]
33
- http.get "http://user:passwd@eu.httpbin.org/basic-auth/user/passwd"
33
+ http.get "http://admin:secret@#{HOST}:#{PORT}/basic-auth/user/passwd"
34
34
 
35
- http.basic_auth = [ "user", "passwd" ]
35
+ http.basic_auth = [ "admin", "secret" ]
36
36
  assert_http_error 401 do
37
- http.get "http://wronguser:passwd@eu.httpbin.org/basic-auth/user/passwd"
37
+ http.get "http://wrongadmin:secret@#{HOST}:#{PORT}/basic-auth/user/passwd"
38
38
  end
39
39
  end
40
40
  end
@@ -0,0 +1,67 @@
1
+ # Author:: radiospiel (mailto:eno@radiospiel.org)
2
+ # Copyright:: Copyright (c) 2011-2015 radiospiel
3
+ # License:: Distributes under the terms of the Modified BSD License, see LICENSE.BSD for details.
4
+
5
+ require_relative 'test_helper'
6
+
7
+ class EchoTest < Simple::HTTP::TestCase
8
+ def test_get
9
+ response = http.get "/echo?a=1&b=2"
10
+ expected = <<~MSG
11
+ GET
12
+ a: 1
13
+ b: 2
14
+ MSG
15
+
16
+ assert_equal(expected, response.content)
17
+ end
18
+
19
+ def test_head
20
+ response = http.head "/echo?a=1&b=2"
21
+ assert_equal(nil, response.content)
22
+ assert_equal(200, response.status)
23
+ end
24
+
25
+ def body_example
26
+ {
27
+ foo: "bar"
28
+ }
29
+ end
30
+
31
+ def test_post
32
+ response = http.post "/echo?a=1&b=2", body_example
33
+ expected = <<~MSG
34
+ POST
35
+ a: 1
36
+ b: 2
37
+ CONTENT_TYPE: application/json
38
+ {"foo":"bar"}
39
+ MSG
40
+
41
+ assert_equal(expected, response.content)
42
+ end
43
+
44
+ def test_put
45
+ response = http.put "/echo?a=1&b=2", body_example
46
+ expected = <<~MSG
47
+ PUT
48
+ a: 1
49
+ b: 2
50
+ CONTENT_TYPE: application/json
51
+ {"foo":"bar"}
52
+ MSG
53
+
54
+ assert_equal(expected, response.content)
55
+ end
56
+
57
+ def test_delete
58
+ response = http.delete "/echo?a=1&b=2"
59
+ expected = <<~MSG
60
+ DELETE
61
+ a: 1
62
+ b: 2
63
+ MSG
64
+
65
+ assert_equal(expected, response.content)
66
+ end
67
+ end
@@ -0,0 +1,7 @@
1
+ use Rack::Auth::Basic do |username, password|
2
+ [username, password] == %w(admin secret)
3
+ end
4
+
5
+ get "/user/passwd" do
6
+ "ok"
7
+ end
@@ -0,0 +1,54 @@
1
+ helpers do
2
+ def debug_params
3
+ params.map { |k, v| "#{k}: #{v}" }.sort.join("\n")
4
+ end
5
+
6
+ def interesting_header(name)
7
+ return true if name == "CONTENT_TYPE"
8
+ return true if name =~ /X_/
9
+ false
10
+ end
11
+
12
+ def debug_headers
13
+ headers = request.each_header.select { |header, _| interesting_header(header) }
14
+ return if headers.empty?
15
+ headers.map { |k, v| "#{k}: #{v}" }.sort.join("\n")
16
+ end
17
+
18
+ def debug_body
19
+ verb = request.request_method
20
+ return unless verb == "POST" || verb == "PUT"
21
+
22
+ request.body.rewind
23
+ request.body.read
24
+ end
25
+
26
+ def debug
27
+ [
28
+ request.request_method,
29
+ debug_params,
30
+ debug_headers,
31
+ debug_body
32
+ ].compact.map { |s| "#{s}\n" }.join("")
33
+ end
34
+ end
35
+
36
+ get "/" do
37
+ debug
38
+ end
39
+
40
+ head "/" do
41
+ debug
42
+ end
43
+
44
+ post "/" do
45
+ debug
46
+ end
47
+
48
+ put "/" do
49
+ debug
50
+ end
51
+
52
+ delete "/" do
53
+ debug
54
+ end
@@ -0,0 +1,20 @@
1
+ get "/" do
2
+ end
3
+
4
+ get "/status/:status" do
5
+ status params[:status]
6
+ "No content"
7
+ end
8
+
9
+ get "/redirect-to" do
10
+ redirect params[:url]
11
+ end
12
+
13
+ get "/redirect-to-self" do
14
+ redirect "/redirect-to-self"
15
+ end
16
+
17
+ get "/redirection-target" do
18
+ content_type :html
19
+ "I am the redirection target"
20
+ end
@@ -5,14 +5,22 @@
5
5
  require_relative 'test_helper'
6
6
 
7
7
  class RedirectionTest < Simple::HTTP::TestCase
8
+ def test_limits_redirections
9
+ assert_raise(Simple::HTTP::TooManyRedirections) {
10
+ http.get "/redirect-to-self"
11
+ }
12
+ end
13
+
8
14
  def test_follows_redirections
9
- html = http.get "http://eu.httpbin.org/redirect-to?url=http://eu.httpbin.org"
10
- assert_match(/title.*httpbin/, html)
15
+ response = http.get "/redirect-to?url=http://#{HOST}:#{PORT}/redirection-target"
16
+ assert_match(/I am the redirection target/, response.content)
11
17
  end
12
18
 
13
- def test_raises_error_on_invalid_redirect_location
19
+ # We have a hard time to convince the test application to redirect
20
+ # to an invalid location.. we therefore skip this test.
21
+ def skipped_test_raises_error_on_invalid_redirect_location
14
22
  assert_raise(ArgumentError) {
15
- http.get "http://eu.httpbin.org/redirect-to?url=foo"
23
+ http.get "/redirect-to?url=foo"
16
24
  }
17
25
  end
18
26
  end
@@ -0,0 +1,14 @@
1
+ # Author:: radiospiel (mailto:eno@radiospiel.org)
2
+ # Copyright:: Copyright (c) 2011-2015 radiospiel
3
+ # License:: Distributes under the terms of the Modified BSD License, see LICENSE.BSD for details.
4
+
5
+ ENV['SSL_CERT_FILE'] = '/usr/local/etc/openssl/certs/cert.pem'
6
+
7
+ require_relative 'test_helper'
8
+
9
+ class SimpleHttpsTest < Simple::HTTP::TestCase
10
+ def skipped_test_https
11
+ response = http.get "https://google.com"
12
+ assert_match(/doctype/, response)
13
+ end
14
+ end
@@ -1,22 +1,58 @@
1
1
  require 'rubygems'
2
2
  require 'bundler/setup'
3
3
 
4
- # require 'simplecov'
4
+ # -- setup a test server serving ./test/fixtures/httpd ------------------------
5
+ #
6
+ # This server uses simple-httpd to serve some static and dynamic test content.
7
+ # Note: for purity reasons this should be reimplemented via a rack middleware.
8
+ #
9
+ $: << "../rspec-httpd/lib"
10
+ require "rspec/httpd/server"
11
+
5
12
  require 'test/unit'
6
13
 
14
+ # require 'simplecov'
15
+ #
7
16
  # SimpleCov.start do
8
17
  # add_filter "test/*.rb"
9
18
  # end
10
19
 
11
20
  require 'simple/http'
12
- require 'simple/http/assertions'
13
21
 
14
22
  class Simple::HTTP::TestCase < Test::Unit::TestCase
15
- include Simple::HTTP::Assertions
23
+ HOST = "0.0.0.0"
24
+ PORT = 12345
16
25
 
17
26
  attr :http
18
27
 
28
+ # start a test server. Note that this starts the server only once, and
29
+ # automatically cleans up the server when this process finishes.
30
+ def start_server!
31
+ httpd_root = "#{__dir__}/fixtures/httpd"
32
+
33
+ # The command to start the server must reset the BUNDLE_GEMFILE environment
34
+ # setting.
35
+ command = "cd ../simple-httpd/ && BUNDLE_GEMFILE= PORT=12345 bin/simple-httpd #{httpd_root} -q"
36
+ command = "#{command} 2> log/simple-httpd.log"
37
+
38
+ ::RSpec::Httpd::Server.start! port: PORT, command: command
39
+ end
40
+
19
41
  def setup
42
+ start_server!
43
+
20
44
  @http = Simple::HTTP.new
45
+ @http.base_url = "http://#{HOST}:#{PORT}"
46
+ end
47
+
48
+ def assert_http_error(expected_status, &block)
49
+ response = yield
50
+ assert_equal(expected_status, response.status)
51
+ end
52
+
53
+ def assert_http_redirects_to(expected_location, &block)
54
+ response = yield
55
+ assert_includes([301, 302], response.status)
56
+ assert_equal(expected_location, response.headers["Location"])
21
57
  end
22
58
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple-http
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - radiospiel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-03-25 00:00:00.000000000 Z
11
+ date: 2019-11-16 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Simple code for simple HTTP requests
14
14
  email: eno@radiospiel.org
@@ -18,15 +18,22 @@ extra_rdoc_files: []
18
18
  files:
19
19
  - README.md
20
20
  - lib/simple/http.rb
21
- - lib/simple/http/assertions.rb
21
+ - lib/simple/http/body_builder.rb
22
+ - lib/simple/http/caching.rb
23
+ - lib/simple/http/driver/default.rb
22
24
  - lib/simple/http/errors.rb
23
- - lib/simple/http/expires_in.rb
24
- - lib/simple/http/result.rb
25
+ - lib/simple/http/headers.rb
26
+ - lib/simple/http/request.rb
27
+ - lib/simple/http/response.rb
25
28
  - lib/simple/http/version.rb
26
29
  - test/assertions_test.rb
27
30
  - test/basic_auth_test.rb
31
+ - test/echo_test.rb
32
+ - test/fixtures/httpd/basic-auth.rb
33
+ - test/fixtures/httpd/echo.rb
34
+ - test/fixtures/httpd/root.rb
28
35
  - test/redirection_test.rb
29
- - test/simple_http_test.rb
36
+ - test/simple_https_test.rb
30
37
  - test/test_helper.rb
31
38
  homepage: http://github.com/radiospiel/simple-http
32
39
  licenses: []
@@ -46,8 +53,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
46
53
  - !ruby/object:Gem::Version
47
54
  version: '0'
48
55
  requirements: []
49
- rubyforge_project:
50
- rubygems_version: 2.5.1
56
+ rubygems_version: 3.0.4
51
57
  signing_key:
52
58
  specification_version: 4
53
59
  summary: Simple code for simple HTTP requests
@@ -1,16 +0,0 @@
1
- # Author:: radiospiel (mailto:eno@radiospiel.org)
2
- # Copyright:: Copyright (c) 2011-2015 radiospiel
3
- # License:: Distributes under the terms of the Modified BSD License, see LICENSE.BSD for details.
4
-
5
- module Simple::HTTP::Assertions
6
- def assert_http_error(expected_status, &block)
7
- http_error = assert_raises(Simple::HTTP::Error, &block)
8
- assert_equal(expected_status, http_error.code)
9
- end
10
-
11
- def assert_http_redirects_to(expected_location, &block)
12
- http_error = assert_raises(Simple::HTTP::Error, &block)
13
- assert_includes([301, 302], http_error.code)
14
- assert_equal(expected_location, http_error.response["Location"])
15
- end
16
- end
@@ -1,34 +0,0 @@
1
- # This file is part of the simple-http ruby gem.
2
- #
3
- # Copyright (c) 2011 - 2015 @radiospiel
4
- # Distributed under the terms of the modified BSD license, see LICENSE.BSD
5
-
6
- require "net/http"
7
-
8
- class Net::HTTPResponse
9
- private
10
-
11
- #
12
- # The "max-age" value, in seconds, from the Cache-Control header.
13
- def max_age
14
- return unless cache_control = header["Cache-Control"]
15
- cache_control.split(/, /).each do |part|
16
- next unless part =~ /max-age=(\d+)/
17
- return Integer($1)
18
- end
19
- nil
20
- end
21
-
22
- public
23
-
24
- #
25
- # returns expiration information, in seconds from now.
26
- def expires_in
27
- expires_in = max_age
28
- if !expires_in && expires = header["Expires"]
29
- expires_in = Time.parse(expires) - Time.now
30
- end
31
-
32
- return expires_in if expires_in && expires_in > 0
33
- end
34
- end
@@ -1,41 +0,0 @@
1
- # This file is part of the simple-http ruby gem.
2
- #
3
- # Copyright (c) 2011 - 2015 @radiospiel
4
- # Distributed under the terms of the modified BSD license, see LICENSE.BSD
5
-
6
- require "net/http"
7
-
8
- class Net::HTTPResponse
9
- #
10
- # evaluate and potentially parse response
11
- def result
12
- case response.content_type
13
- when "application/json"
14
- JSON.parse(body) unless body.empty?
15
- else
16
- body_string
17
- end
18
- end
19
-
20
- private
21
-
22
- #
23
- # returns a body. The resulting string is encoded in ASCII-8BIT, if the
24
- # content type is binary, and encoded in UTF8 otherwise.
25
- def body_string
26
- default_encoding = content_is_binary? ? "ASCII-8BIT" : "UTF-8"
27
-
28
- body = self.body
29
- if charset = type_params["charset"]
30
- body = body.force_encoding(charset)
31
- end
32
- body.encode(default_encoding)
33
- end
34
-
35
- def content_is_binary?
36
- case content_type
37
- when /^image/ then true
38
- else false
39
- end
40
- end
41
- end
@@ -1,17 +0,0 @@
1
- # Author:: radiospiel (mailto:eno@radiospiel.org)
2
- # Copyright:: Copyright (c) 2011-2015 radiospiel
3
- # License:: Distributes under the terms of the Modified BSD License, see LICENSE.BSD for details.
4
-
5
- require_relative 'test_helper'
6
-
7
- class SimpleHttpTest < Simple::HTTP::TestCase
8
- def test_http_google
9
- google = http.get "http://google.com"
10
- assert_match(/doctype/, google)
11
- end
12
-
13
- def test_https_google
14
- google = http.get "https://google.com"
15
- assert_match(/doctype/, google)
16
- end
17
- end