simple-http 0.1.5 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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