opinionated_http 0.0.6 → 0.1.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
2
  SHA256:
3
- metadata.gz: 5c63d8fd6c65797a7afcef26ff0ef72507dcf75f8dbbd4bc93e768cf0ddffc9a
4
- data.tar.gz: 0a56d817ad5a0a988886faf17b034df697712c3958719cbcd72558ebb1bd8ee1
3
+ metadata.gz: cbd58bf8946567e33344521051ead26e88ad8d6d083f820c1528ac644c6ac7ae
4
+ data.tar.gz: 4dd8282607215174ef3b261f262e9bbbd99396a0f5992df1a6dc652bc58b4008
5
5
  SHA512:
6
- metadata.gz: f38a9f47681824038a271244c560639b119ff9ff138210c93eeb2e9d2e0955b05198f0956d5fba2f1b60b965bf4084e8f3aca0b89af759d6ecf78419d9f05903
7
- data.tar.gz: 4b882e2ec2025a9d626c1a3e6d52edff1a20db6fb6dc48bc10203f14117bb1856764f373c4a0ca0bd81f97fc75a4798a68a57b2cdeafd1c3ad7d6b36ef93a14d
6
+ metadata.gz: 37c91ccf2069493d1cea1243d5673d3fbbd7e7caed6542eb3b61678e1f4f2f34fd964b5521b17042f297482f42921983a5019c788b5b3de10d6739a1d9569239
7
+ data.tar.gz: fd93c9925f575faabc06809cd0ed665469cd8e2531c91b9a96446e00b64fb1be1b50c3a065378ea73697a0c42f563a352c234dc42f9b8ac309e5413e2accc253
data/README.md CHANGED
@@ -36,6 +36,10 @@ Parameters:
36
36
  Required.
37
37
  logger:
38
38
  Default: SemanticLogger[OpinionatedHTTP]
39
+ format: [:json | nil]
40
+ Optionally set the format for http requests.
41
+ Currently only supports `:json`, which will format the request and response to/from JSON.
42
+ It will also set the `Content-Type` http header to `application/json`.
39
43
  Other options as supported by PersistentHTTP
40
44
  #TODO: Expand PersistentHTTP options here
41
45
 
data/Rakefile CHANGED
@@ -21,3 +21,5 @@ Rake::TestTask.new(:test) do |t|
21
21
  t.verbose = true
22
22
  t.warning = false
23
23
  end
24
+
25
+ task default: :test
@@ -10,18 +10,19 @@ module OpinionatedHTTP
10
10
  # 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout
11
11
  HTTP_RETRY_CODES = %w[502 503 504].freeze
12
12
 
13
- attr_reader :secret_config_prefix, :logger, :metric_prefix, :error_class, :driver,
13
+ attr_reader :secret_config_prefix, :logger, :format, :metric_prefix, :error_class, :driver, :format,
14
14
  :retry_count, :retry_interval, :retry_multiplier, :http_retry_codes,
15
15
  :url, :pool_size, :keep_alive, :proxy, :force_retry, :max_redirects,
16
- :open_timeout, :read_timeout, :idle_timeout, :pool_timeout, :warn_timeout
16
+ :open_timeout, :read_timeout, :idle_timeout, :pool_timeout, :warn_timeout,
17
+ :after_connect
17
18
 
18
19
  # Any option supplied here can be overridden if that corresponding value is set in Secret Config.
19
- # Except for any values passed directly to Persistent HTTP under `**options`.
20
20
  def initialize(
21
21
  secret_config_prefix:,
22
22
  metric_prefix:,
23
23
  error_class:,
24
24
  logger: nil,
25
+ format: nil,
25
26
  retry_count: 11,
26
27
  retry_interval: 0.01,
27
28
  retry_multiplier: 1.8,
@@ -37,93 +38,107 @@ module OpinionatedHTTP
37
38
  proxy: :ENV,
38
39
  force_retry: true,
39
40
  max_redirects: 10,
40
- **options
41
+ after_connect: nil,
42
+ verify_peer: false, # TODO: PersistentHTTP keeps returning cert expired even when it is valid.
43
+ certificate: nil,
44
+ private_key: nil,
45
+ header: nil
41
46
  )
42
- @metric_prefix = metric_prefix
43
- @logger = logger || SemanticLogger[self]
44
- @error_class = error_class
45
- @retry_count = SecretConfig.fetch("#{secret_config_prefix}/retry_count", type: :integer, default: retry_count)
46
- @retry_interval = SecretConfig.fetch("#{secret_config_prefix}/retry_interval", type: :float, default: retry_interval)
47
- @retry_multiplier = SecretConfig.fetch("#{secret_config_prefix}/retry_multiplier", type: :float, default: retry_multiplier)
48
- @max_redirects = SecretConfig.fetch("#{secret_config_prefix}/max_redirects", type: :integer, default: max_redirects)
49
- http_retry_codes = SecretConfig.fetch("#{secret_config_prefix}/http_retry_codes", type: :string, default: http_retry_codes)
50
- @http_retry_codes = http_retry_codes.split(",").collect(&:strip)
51
-
52
- @url = url.nil? ? SecretConfig["#{secret_config_prefix}/url"] : SecretConfig.fetch("#{secret_config_prefix}/url", default: url)
53
-
54
- @pool_size = SecretConfig.fetch("#{secret_config_prefix}/pool_size", type: :integer, default: pool_size)
55
- @open_timeout = SecretConfig.fetch("#{secret_config_prefix}/open_timeout", type: :float, default: open_timeout)
56
- @read_timeout = SecretConfig.fetch("#{secret_config_prefix}/read_timeout", type: :float, default: read_timeout)
57
- @idle_timeout = SecretConfig.fetch("#{secret_config_prefix}/idle_timeout", type: :float, default: idle_timeout)
58
- @keep_alive = SecretConfig.fetch("#{secret_config_prefix}/keep_alive", type: :float, default: keep_alive)
59
- @pool_timeout = SecretConfig.fetch("#{secret_config_prefix}/pool_timeout", type: :float, default: pool_timeout)
60
- @warn_timeout = SecretConfig.fetch("#{secret_config_prefix}/warn_timeout", type: :float, default: warn_timeout)
61
- @proxy = SecretConfig.fetch("#{secret_config_prefix}/proxy", type: :symbol, default: proxy)
62
- @force_retry = SecretConfig.fetch("#{secret_config_prefix}/force_retry", type: :boolean, default: force_retry)
47
+ @metric_prefix = metric_prefix
48
+ @logger = logger || SemanticLogger[self]
49
+ @error_class = error_class
50
+ @format = format
51
+ @after_connect = after_connect
52
+ SecretConfig.configure(secret_config_prefix) do |config|
53
+ @retry_count = config.fetch("retry_count", type: :integer, default: retry_count)
54
+ @retry_interval = config.fetch("retry_interval", type: :float, default: retry_interval)
55
+ @retry_multiplier = config.fetch("retry_multiplier", type: :float, default: retry_multiplier)
56
+ @max_redirects = config.fetch("max_redirects", type: :integer, default: max_redirects)
57
+ http_retry_codes = config.fetch("http_retry_codes", type: :string, default: http_retry_codes)
58
+ @http_retry_codes = http_retry_codes.split(",").collect(&:strip)
59
+
60
+ @url = url.nil? ? config.fetch("url") : config.fetch("url", default: url)
61
+
62
+ @pool_size = config.fetch("pool_size", type: :integer, default: pool_size)
63
+ @open_timeout = config.fetch("open_timeout", type: :float, default: open_timeout)
64
+ @read_timeout = config.fetch("read_timeout", type: :float, default: read_timeout)
65
+ @idle_timeout = config.fetch("idle_timeout", type: :float, default: idle_timeout)
66
+ @keep_alive = config.fetch("keep_alive", type: :float, default: keep_alive)
67
+ @pool_timeout = config.fetch("pool_timeout", type: :float, default: pool_timeout)
68
+ @warn_timeout = config.fetch("warn_timeout", type: :float, default: warn_timeout)
69
+ @proxy = config.fetch("proxy", type: :symbol, default: proxy)
70
+ @force_retry = config.fetch("force_retry", type: :boolean, default: force_retry)
71
+ @certificate = config.fetch("certificate", type: :string, default: certificate)
72
+ @private_key = config.fetch("private_key", type: :string, default: private_key)
73
+ @verify_peer = config.fetch("verify_peer", type: :boolean, default: verify_peer)
74
+ end
63
75
 
64
76
  internal_logger = OpinionatedHTTP::Logger.new(@logger)
65
- new_options = {
66
- logger: internal_logger,
67
- debug_output: internal_logger,
68
- name: "",
69
- pool_size: @pool_size,
70
- open_timeout: @open_timeout,
71
- read_timeout: @read_timeout,
72
- idle_timeout: @idle_timeout,
73
- keep_alive: @keep_alive,
74
- pool_timeout: @pool_timeout,
75
- warn_timeout: @warn_timeout,
76
- proxy: @proxy,
77
- force_retry: @force_retry
78
- }
79
-
80
- url = SecretConfig["#{secret_config_prefix}/url"]
81
- new_options[:url] = url if url
82
- @driver = PersistentHTTP.new(new_options.merge(options))
77
+ @driver = PersistentHTTP.new(
78
+ url: @url,
79
+ logger: internal_logger,
80
+ debug_output: internal_logger,
81
+ name: "",
82
+ pool_size: @pool_size,
83
+ open_timeout: @open_timeout,
84
+ read_timeout: @read_timeout,
85
+ idle_timeout: @idle_timeout,
86
+ keep_alive: @keep_alive,
87
+ pool_timeout: @pool_timeout,
88
+ warn_timeout: @warn_timeout,
89
+ proxy: @proxy,
90
+ force_retry: @force_retry,
91
+ after_connect: @after_connect,
92
+ certificate: @certificate,
93
+ private_key: @private_key,
94
+ verify_mode: @verify_peer ? OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT : OpenSSL::SSL::VERIFY_NONE,
95
+ header: header
96
+ )
83
97
  end
84
98
 
85
- # Perform an HTTP Get against the supplied path
86
- def get(action:, path: "/#{action}", **args)
87
- request = build_request(path: path, verb: "Get", **args)
88
- response = request(action: action, request: request)
89
- extract_body(response, 'GET', action)
99
+ def get(request: nil, **args)
100
+ request ||= Request.new(**args)
101
+ request.verb = "Get"
102
+ http_response = request(request)
103
+ Response.new(http_response, request)
90
104
  end
91
105
 
92
- def post(action:, path: "/#{action}", **args)
93
- request = build_request(path: path, verb: "Post", **args)
94
-
95
- response = request(action: action, request: request)
96
- extract_body(response, 'POST', action)
97
- end
106
+ def post(request: nil, json: nil, body: nil, **args)
107
+ raise(ArgumentError, "Either set :json or :body") if json && body
98
108
 
99
- def build_request(verb:, path:, headers: nil, body: nil, form_data: nil, username: nil, password: nil, parameters: nil)
100
- unless headers_and_form_data_compatible?(headers, form_data)
101
- raise(ArgumentError, "Setting form data will overwrite supplied content-type")
109
+ request ||= Request.new(**args)
110
+ if json
111
+ request.format = :json
112
+ request.body = json
113
+ else
114
+ request.body = body
102
115
  end
103
- raise(ArgumentError, "Cannot supply both form_data and a body") if body && form_data
104
-
105
- path = "/#{path}" unless path.start_with?("/")
106
- path = "#{path}?#{URI.encode_www_form(parameters)}" if parameters
107
-
108
- request = Net::HTTP.const_get(verb).new(path, headers)
116
+ request.verb = "Post"
117
+ http_response = request(request)
118
+ Response.new(http_response, request)
119
+ end
109
120
 
110
- raise(ArgumentError, "#{request.class.name} does not support a request body") if body && !request.request_body_permitted?
111
- if parameters && !request.response_body_permitted?
112
- raise(ArgumentError, ":parameters cannot be supplied for #{request.class.name}")
113
- end
121
+ def delete(request: nil, **args)
122
+ request ||= Request.new(**args)
123
+ request.verb = "Delete"
124
+ http_response = request(request)
125
+ Response.new(http_response, request)
126
+ end
114
127
 
115
- request.body = body if body
116
- request.set_form_data form_data if form_data
117
- request.basic_auth(username, password) if username && password
118
- request
128
+ def patch(request: nil, **args)
129
+ request ||= Request.new(**args)
130
+ request.verb = "Patch"
131
+ http_response = request(request)
132
+ Response.new(http_response, request)
119
133
  end
120
134
 
121
- # Returns [HTTP Response] after submitting the request
122
- #
123
- # Notes:
124
- # - Does not raise an exception when the http response is not an HTTP OK (200.
125
- def request(action:, request:)
126
- request_with_retry(action: action, request: request)
135
+ # Returns [Response] after submitting the [Request]
136
+ def request(request)
137
+ request.metric_prefix ||= metric_prefix
138
+ request.format ||= format
139
+ request.error_class ||= error_class
140
+ request.logger ||= logger
141
+ request_with_retry(action: request.action, request: request.http_request)
127
142
  end
128
143
 
129
144
  private
@@ -132,14 +147,10 @@ module OpinionatedHTTP
132
147
  http_method = request.method.upcase
133
148
  response =
134
149
  begin
135
- payload = {}
136
- if logger.trace?
137
- payload[:parameters] = parameters
138
- payload[:path] = request.path
150
+ message = "HTTP #{http_method}: #{action} #{request.path}"
151
+ logger.benchmark_info(message: message, metric: "#{metric_prefix}/#{action}") do
152
+ driver.request(request)
139
153
  end
140
- message = "HTTP #{http_method}: #{action}" if logger.debug?
141
-
142
- logger.benchmark_info(message: message, metric: "#{metric_prefix}/#{action}", payload: payload) { driver.request(request) }
143
154
  rescue StandardError => e
144
155
  message = "HTTP #{http_method}: #{action} Failure: #{e.class.name}: #{e.message}"
145
156
  logger.error(message: message, metric: "#{metric_prefix}/exception", exception: e)
@@ -147,33 +158,19 @@ module OpinionatedHTTP
147
158
  end
148
159
 
149
160
  # Retry on http 5xx errors except 500 which means internal server error.
150
- if http_retry_codes.include?(response.code)
151
- if try_count < retry_count
152
- try_count += 1
153
- duration = retry_sleep_interval(try_count)
154
- logger.warn(message: "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}. Retry: #{try_count}", metric: "#{metric_prefix}/retry", duration: duration * 1_000)
155
- sleep(duration)
156
- response = request_with_retry(action: action, request: request, try_count: try_count)
157
- else
158
- message = "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}. Retries Exhausted"
159
- logger.error(message: message, metric: "#{metric_prefix}/exception")
160
- raise(error_class, message)
161
- end
162
- end
163
-
164
- response
165
- end
166
-
167
- def extract_body(response, http_method, action)
168
- return response.body if response.is_a?(Net::HTTPSuccess)
161
+ return response unless http_retry_codes.include?(response.code)
169
162
 
170
- message = "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}"
171
- logger.error(message: message, metric: "#{metric_prefix}/exception")
172
- raise(error_class, message)
173
- end
163
+ if try_count >= retry_count
164
+ message = "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}. Retries Exhausted"
165
+ logger.error(message: message, metric: "#{metric_prefix}/exception")
166
+ raise(error_class, message)
167
+ end
174
168
 
175
- def prefix_path(path)
176
- path.start_with?("/") ? path : "/#{path}"
169
+ try_count += 1
170
+ duration = retry_sleep_interval(try_count)
171
+ logger.warn(message: "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}. Retry: #{try_count}", metric: "#{metric_prefix}/retry", duration: duration * 1_000)
172
+ sleep(duration)
173
+ request_with_retry(action: action, request: request, try_count: try_count)
177
174
  end
178
175
 
179
176
  # First retry is immediate, next retry is after `retry_interval`,
@@ -181,13 +178,7 @@ module OpinionatedHTTP
181
178
  def retry_sleep_interval(retry_count)
182
179
  return 0 if retry_count <= 1
183
180
 
184
- (retry_multiplier**(retry_count - 1)) * retry_interval
185
- end
186
-
187
- def headers_and_form_data_compatible?(headers, form_data)
188
- return true if headers.nil? || form_data.nil?
189
-
190
- !headers.keys.map(&:downcase).include?("content-type")
181
+ (retry_multiplier ** (retry_count - 1)) * retry_interval
191
182
  end
192
183
  end
193
184
  end
@@ -0,0 +1,76 @@
1
+ module OpinionatedHTTP
2
+ # The request object
3
+ class Request
4
+ attr_accessor :action, :verb, :format, :path, :headers, :body, :form_data, :username, :password, :parameters,
5
+ :metric_prefix, :error_class, :logger
6
+
7
+ def initialize(action:, verb: nil, path: nil, format: nil, headers: {}, body: nil, form_data: nil, username: nil, password: nil, parameters: nil)
8
+ @action = action
9
+ @path =
10
+ if path.nil?
11
+ "/#{action}"
12
+ elsif path.start_with?("/")
13
+ path
14
+ else
15
+ "/#{path}"
16
+ end
17
+ @verb = verb
18
+ @format = format
19
+ @headers = headers
20
+ @body = body
21
+ @form_data = form_data
22
+ @username = username
23
+ @password = password
24
+ @parameters = parameters
25
+ end
26
+
27
+ def http_request
28
+ unless headers_and_form_data_compatible?(headers, form_data)
29
+ raise(ArgumentError, "Setting form data will overwrite supplied content-type")
30
+ end
31
+ raise(ArgumentError, "Cannot supply both form_data and a body") if body && form_data
32
+
33
+ path_with_params = parameters ? "#{path}?#{URI.encode_www_form(parameters)}" : path
34
+ body = format_body if self.body
35
+ request = Net::HTTP.const_get(verb).new(path_with_params, headers)
36
+
37
+ if body && !request.request_body_permitted?
38
+ raise(ArgumentError, "#{request.class.name} does not support a request body")
39
+ end
40
+
41
+ if parameters && !request.response_body_permitted?
42
+ raise(ArgumentError, ":parameters cannot be supplied for #{request.class.name}")
43
+ end
44
+
45
+ request.body = body if body
46
+ request.set_form_data form_data if form_data
47
+ request.basic_auth(username, password) if username && password
48
+ request
49
+ end
50
+
51
+ private
52
+
53
+ def format_body
54
+ case format
55
+ when :json
56
+ headers["Content-Type"] = "application/json"
57
+ headers["Accept"] = "application/json"
58
+ body.is_a?(String) || body.nil? ? body : body.to_json
59
+ when nil
60
+ body
61
+ else
62
+ raise(ArgumentError, "Unknown format: #{format.inspect}")
63
+ end
64
+ rescue StandardError => exc
65
+ message = "Failed to serialize request body. #{exc.class.name}: #{exc.message}"
66
+ logger.error(message: message, metric: "#{metric_prefix}/exception", exception: exc)
67
+ raise(error_class, message)
68
+ end
69
+
70
+ def headers_and_form_data_compatible?(headers, form_data)
71
+ return true if headers.empty? || form_data.nil?
72
+
73
+ !headers.keys.map(&:downcase).include?("content-type")
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,59 @@
1
+ require "forwardable"
2
+ module OpinionatedHTTP
3
+ # The response object
4
+ class Response
5
+ extend Forwardable
6
+
7
+ def_instance_delegators :@http_response, :code, :message
8
+ def_instance_delegators :@request, :verb, :action, :metric_prefix, :format, :path, :error_class, :logger
9
+
10
+ # :action used for logging the action in the error message
11
+ def initialize(http_response, request)
12
+ @http_response = http_response
13
+ @request = request
14
+ end
15
+
16
+ def success?
17
+ @http_response.is_a?(Net::HTTPSuccess)
18
+ end
19
+
20
+ def body
21
+ @body ||= parse_body
22
+ end
23
+
24
+ # Raises an exception when the HTTP Response is not a success
25
+ def body!
26
+ return body if success?
27
+
28
+ exception!
29
+ end
30
+
31
+ def exception!
32
+ error_message = "HTTP #{verb.upcase}: #{action} Failure: (#{code}) #{message}"
33
+ logger.error(message: error_message, metric: "#{metric_prefix}/exception", payload: {body: body})
34
+ raise(error_class, error_message)
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :http_response, :request
40
+
41
+ def parse_body
42
+ return unless http_response.class.body_permitted?
43
+
44
+ case format
45
+ when :json
46
+ JSON.parse(http_response.body)
47
+ when nil
48
+ http_response.body
49
+ else
50
+ raise(ArgumentError, "Unknown format: #{format.inspect}")
51
+ end
52
+
53
+ rescue StandardError => exc
54
+ message = "Failed to parse response body. #{exc.class.name}: #{exc.message}"
55
+ logger.error(message: message, metric: "#{metric_prefix}/exception", exception: exc)
56
+ raise(error_class, message)
57
+ end
58
+ end
59
+ end
@@ -1,3 +1,3 @@
1
1
  module OpinionatedHTTP
2
- VERSION = "0.0.6".freeze
2
+ VERSION = "0.1.2".freeze
3
3
  end
@@ -7,6 +7,8 @@ require "opinionated_http/version"
7
7
  module OpinionatedHTTP
8
8
  autoload :Client, "opinionated_http/client"
9
9
  autoload :Logger, "opinionated_http/logger"
10
+ autoload :Request, "opinionated_http/request"
11
+ autoload :Response, "opinionated_http/response"
10
12
 
11
13
  #
12
14
  # Create a new Opinionated HTTP instance.
data/test/client_test.rb CHANGED
@@ -2,168 +2,100 @@ require "net/http"
2
2
  require "json"
3
3
  require_relative "test_helper"
4
4
 
5
- module OpinionatedHTTP
6
- class ClientTest < Minitest::Test
7
- describe OpinionatedHTTP::Client do
8
- class ServiceError < StandardError
9
- end
10
-
11
- let :http do
12
- OpinionatedHTTP.new(
13
- secret_config_prefix: "fake_service",
14
- metric_prefix: "FakeService",
15
- logger: SemanticLogger["FakeService"],
16
- error_class: ServiceError,
17
- header: {"Content-Type" => "application/json"}
18
- )
19
- end
5
+ class ClientTest < Minitest::Test
6
+ describe OpinionatedHTTP::Client do
7
+ class ServiceError < StandardError
8
+ end
20
9
 
21
- describe "get" do
22
- it "succeeds" do
23
- output = {zip: "12345", population: 54_321}
24
- body = output.to_json
25
- response = stub_request(Net::HTTPSuccess, 200, "OK", body) do
26
- http.get(action: "lookup", parameters: {zip: "12345"})
27
- end
28
- assert_equal body, response
29
- end
10
+ let :http do
11
+ OpinionatedHTTP.new(
12
+ secret_config_prefix: "fake_service",
13
+ metric_prefix: "FakeService",
14
+ logger: SemanticLogger["FakeService"],
15
+ error_class: ServiceError
16
+ )
17
+ end
30
18
 
31
- it "fails" do
32
- message = "HTTP GET: lookup Failure: (403) Forbidden"
33
- error = assert_raises ServiceError do
34
- stub_request(Net::HTTPForbidden, 403, "Forbidden", "") do
35
- http.get(action: "lookup", parameters: {zip: "12345"})
36
- end
37
- end
38
- assert_equal message, error.message
19
+ describe "get" do
20
+ it "succeeds" do
21
+ output = {zip: "12345", population: 54_321}
22
+ body = output.to_json
23
+ stub_request(Net::HTTPSuccess, 200, "OK", body) do
24
+ response = http.get(action: "lookup", parameters: {zip: "12345"})
25
+ assert_equal body, response.body!
39
26
  end
40
27
  end
41
28
 
42
- describe "post" do
43
- it "succeeds with body" do
44
- output = {zip: "12345", population: 54_321}
45
- body = output.to_json
46
- response = stub_request(Net::HTTPSuccess, 200, "OK", body) do
47
- http.post(action: "lookup", body: body)
48
- end
49
- assert_equal body, response
50
- end
51
-
52
- it "with form data" do
53
- output = {zip: "12345", population: 54_321}
54
- body = output.to_json
55
- response = stub_request(Net::HTTPSuccess, 200, "OK", body) do
56
- http.post(action: "lookup", form_data: output)
57
- end
58
- assert_equal body, response
59
- end
60
-
61
- it "fails with body" do
62
- message = "HTTP POST: lookup Failure: (403) Forbidden"
63
- output = {zip: "12345", population: 54_321}
64
- body = output.to_json
65
- error = assert_raises ServiceError do
66
- stub_request(Net::HTTPForbidden, 403, "Forbidden", "") do
67
- http.post(action: "lookup", body: body)
68
- end
69
- end
70
- assert_equal message, error.message
71
- end
72
-
73
- it "fails with form data" do
74
- output = {zip: "12345", population: 54_321}
75
- message = "HTTP POST: lookup Failure: (403) Forbidden"
76
- error = assert_raises ServiceError do
77
- stub_request(Net::HTTPForbidden, 403, "Forbidden", "") do
78
- http.post(action: "lookup", form_data: output)
79
- end
29
+ it "fails" do
30
+ message = "HTTP GET: lookup Failure: (403) Forbidden"
31
+ error = assert_raises ServiceError do
32
+ stub_request(Net::HTTPForbidden, 403, "Forbidden", "") do
33
+ response = http.get(action: "lookup", parameters: {zip: "12345"})
34
+ response.body!
80
35
  end
81
- assert_equal message, error.message
82
36
  end
37
+ assert_equal message, error.message
83
38
  end
39
+ end
84
40
 
85
- describe "build_request" do
86
- let(:path) { "/fake_action" }
87
- let(:post_verb) { "Post" }
88
- let(:get_verb) { "Get" }
89
-
90
- it "creates a request corresponding to the supplied verb" do
91
- req = http.build_request(path: path, verb: post_verb)
92
- req2 = http.build_request(path: path, verb: get_verb)
93
-
94
- assert_kind_of Net::HTTP::Post, req
95
- assert_kind_of Net::HTTP::Get, req2
96
- end
97
-
98
- it "returns a request with supplied headers" do
99
- test_headers = {"test1" => "yes_test_1", "test2" => "yes_test_2"}
100
- req = http.build_request(path: path, verb: get_verb, headers: test_headers)
101
-
102
- assert_equal test_headers["test1"], req["test1"]
103
- assert_equal test_headers["test2"], req["test2"]
104
- end
105
-
106
- it "returns a request with supplied body" do
107
- test_body = "nice bod"
108
- req = http.build_request(path: path, verb: post_verb, body: test_body)
109
-
110
- assert_equal test_body, req.body
41
+ describe "post" do
42
+ it "succeeds with body" do
43
+ output = {zip: "12345", population: 54_321}
44
+ body = output.to_json
45
+ stub_request(Net::HTTPSuccess, 200, "OK", body) do
46
+ response = http.post(action: "lookup", body: body)
47
+ assert_equal body, response.body!
111
48
  end
49
+ end
112
50
 
113
- it "returns a request with supplied form data in x-www-form-urlencoded Content-Type" do
114
- test_data = {test1: "yes", test2: "no"}
115
- expected_string = "test1=yes&test2=no"
116
- req = http.build_request(path: path, verb: post_verb, form_data: test_data)
117
-
118
- assert_equal expected_string, req.body
119
- assert_equal "application/x-www-form-urlencoded", req["Content-Type"]
51
+ it "with form data" do
52
+ output = {zip: "12345", population: 54_321}
53
+ body = output.to_json
54
+ stub_request(Net::HTTPSuccess, 200, "OK", body) do
55
+ response = http.post(action: "lookup", form_data: output)
56
+ assert_equal body, response.body!
120
57
  end
58
+ end
121
59
 
122
- it "add supplied authentication to the request" do
123
- test_un = "admin"
124
- test_pw = "hunter2"
125
- req = http.build_request(path: path, verb: get_verb, username: test_un, password: test_pw)
126
- req2 = Net::HTTP::Get.new(path)
127
- req2.basic_auth test_un, test_pw
128
-
129
- assert_equal req2["authorization"], req["authorization"]
60
+ it "with JSON data" do
61
+ request = {zip: "12345", population: 54_321}
62
+ body = request.to_json
63
+ stub_request(Net::HTTPSuccess, 200, "OK", body) do
64
+ response = http.post(action: "lookup", json: request)
65
+ assert_equal request, response.body!.transform_keys!(&:to_sym)
130
66
  end
67
+ end
131
68
 
132
- it "raise an error if supplied content-type header would be overwritten by setting form_data" do
133
- downcase_headers = {"unimportant" => "blank", "content-type" => "application/json"}
134
- capitalized_headers = {"Unimportant" => "blank", "Content-Type" => "application/json"}
135
- no_conflict_headers = {"whatever" => "blank", "irrelevant" => "test"}
136
- form_data = {thing1: 1, thing2: 2}
137
-
138
- assert_raises ArgumentError do
139
- http.build_request(path: path, verb: post_verb, headers: downcase_headers, form_data: form_data)
140
- end
141
-
142
- assert_raises ArgumentError do
143
- http.build_request(path: path, verb: post_verb, headers: capitalized_headers, form_data: form_data)
69
+ it "fails with body" do
70
+ message = "HTTP POST: lookup Failure: (403) Forbidden"
71
+ output = {zip: "12345", population: 54_321}
72
+ body = output.to_json
73
+ error = assert_raises ServiceError do
74
+ stub_request(Net::HTTPForbidden, 403, "Forbidden", "") do
75
+ response = http.post(action: "lookup", body: body)
76
+ response.body!
144
77
  end
145
-
146
- assert http.build_request(path: path, verb: post_verb, headers: no_conflict_headers, form_data: form_data)
147
78
  end
79
+ assert_equal message, error.message
80
+ end
148
81
 
149
- it "raise an error if there is a collision between supplied body and form_data" do
150
- form_data = {thing1: 1, thing2: 2}
151
- body = "not form data"
152
-
153
- assert_raises ArgumentError do
154
- http.build_request(path: path, verb: post_verb, body: body, form_data: form_data)
82
+ it "fails with form data" do
83
+ output = {zip: "12345", population: 54_321}
84
+ message = "HTTP POST: lookup Failure: (403) Forbidden"
85
+ error = assert_raises ServiceError do
86
+ stub_request(Net::HTTPForbidden, 403, "Forbidden", "") do
87
+ response = http.post(action: "lookup", form_data: output)
88
+ response.body!
155
89
  end
156
-
157
- assert http.build_request(path: path, verb: post_verb, body: body)
158
- assert http.build_request(path: path, verb: post_verb, form_data: form_data)
159
90
  end
91
+ assert_equal message, error.message
160
92
  end
93
+ end
161
94
 
162
- def stub_request(klass, code, msg, body, &block)
163
- response = klass.new("1.1", code, msg)
164
- response.stub(:body, body) do
165
- http.driver.stub(:request, response, &block)
166
- end
95
+ def stub_request(klass, code, msg, body, &block)
96
+ response = klass.new("1.1", code, msg)
97
+ response.stub(:body, body) do
98
+ http.driver.stub(:request, response, &block)
167
99
  end
168
100
  end
169
101
  end
@@ -1,4 +1,4 @@
1
1
  test:
2
- fake_supplier:
2
+ fake_service:
3
3
  url: https://example.org
4
4
 
@@ -0,0 +1,96 @@
1
+ require "net/http"
2
+ require "json"
3
+ require_relative "test_helper"
4
+
5
+ class RequestTest < Minitest::Test
6
+ describe OpinionatedHTTP::Request do
7
+ describe "http_request" do
8
+ let(:path) { "/fake_action" }
9
+ let(:post_verb) { "Post" }
10
+ let(:get_verb) { "Get" }
11
+ let(:action) { "fake_action" }
12
+ let :json_request do
13
+ {zip: "12345", population: 54_321}
14
+ end
15
+
16
+ it "creates a request corresponding to the supplied verb" do
17
+ req = OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb).http_request
18
+ req2 = OpinionatedHTTP::Request.new(action: action, path: path, verb: get_verb).http_request
19
+
20
+ assert_kind_of Net::HTTP::Post, req
21
+ assert_kind_of Net::HTTP::Get, req2
22
+ end
23
+
24
+ it "creates a JSON request" do
25
+ req = OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, format: :json, body: json_request).http_request
26
+
27
+ assert_equal json_request.to_json, req.body
28
+ assert_equal "application/json", req["content-type"], -> { req.to_hash.ai }
29
+ assert_equal "application/json", req["accept"], -> { req.to_hash.ai }
30
+ end
31
+
32
+ it "returns a request with supplied headers" do
33
+ test_headers = {"test1" => "yes_test_1", "test2" => "yes_test_2"}
34
+ req = OpinionatedHTTP::Request.new(action: action, path: path, verb: get_verb, headers: test_headers).http_request
35
+
36
+ assert_equal test_headers["test1"], req["test1"]
37
+ assert_equal test_headers["test2"], req["test2"]
38
+ end
39
+
40
+ it "returns a request with supplied body" do
41
+ test_body = "nice bod"
42
+ req = OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, body: test_body).http_request
43
+
44
+ assert_equal test_body, req.body
45
+ end
46
+
47
+ it "returns a request with supplied form data in x-www-form-urlencoded Content-Type" do
48
+ test_data = {test1: "yes", test2: "no"}
49
+ expected_string = "test1=yes&test2=no"
50
+ req = OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, form_data: test_data).http_request
51
+
52
+ assert_equal expected_string, req.body
53
+ assert_equal "application/x-www-form-urlencoded", req["Content-Type"]
54
+ end
55
+
56
+ it "add supplied authentication to the request" do
57
+ test_un = "admin"
58
+ test_pw = "hunter2"
59
+ req = OpinionatedHTTP::Request.new(action: action, path: path, verb: get_verb, username: test_un, password: test_pw).http_request
60
+ req2 = Net::HTTP::Get.new(path)
61
+ req2.basic_auth test_un, test_pw
62
+
63
+ assert_equal req2["authorization"], req["authorization"]
64
+ end
65
+
66
+ it "raise an error if supplied content-type header would be overwritten by setting form_data" do
67
+ downcase_headers = {"unimportant" => "blank", "content-type" => "application/json"}
68
+ capitalized_headers = {"Unimportant" => "blank", "Content-Type" => "application/json"}
69
+ no_conflict_headers = {"whatever" => "blank", "irrelevant" => "test"}
70
+ form_data = {thing1: 1, thing2: 2}
71
+
72
+ assert_raises ArgumentError do
73
+ OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, headers: downcase_headers, form_data: form_data).http_request
74
+ end
75
+
76
+ assert_raises ArgumentError do
77
+ OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, headers: capitalized_headers, form_data: form_data).http_request
78
+ end
79
+
80
+ assert OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, headers: no_conflict_headers, form_data: form_data).http_request
81
+ end
82
+
83
+ it "raise an error if there is a collision between supplied body and form_data" do
84
+ form_data = {thing1: 1, thing2: 2}
85
+ body = "not form data"
86
+
87
+ assert_raises ArgumentError do
88
+ OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, body: body, form_data: form_data).http_request
89
+ end
90
+
91
+ assert OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, body: body).http_request
92
+ assert OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, form_data: form_data).http_request
93
+ end
94
+ end
95
+ end
96
+ end
data/test/test_helper.rb CHANGED
@@ -3,7 +3,7 @@ ENV["TZ"] = "America/New_York"
3
3
 
4
4
  require "yaml"
5
5
  require "minitest/autorun"
6
- require "awesome_print"
6
+ require "amazing_print"
7
7
  require "secret_config"
8
8
  require "semantic_logger"
9
9
  require "opinionated_http"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opinionated_http
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Reid Morrison
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-10 00:00:00.000000000 Z
11
+ date: 2022-03-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: persistent_http
@@ -55,7 +55,6 @@ dependencies:
55
55
  description: HTTP Client with retries. Uses PersistentHTTP for http connection pooling,
56
56
  Semantic Logger for logging and metrics, and uses Secret Config for its configuration.
57
57
  email:
58
- - reidmo@gmail.com
59
58
  executables: []
60
59
  extensions: []
61
60
  extra_rdoc_files: []
@@ -65,14 +64,17 @@ files:
65
64
  - lib/opinionated_http.rb
66
65
  - lib/opinionated_http/client.rb
67
66
  - lib/opinionated_http/logger.rb
67
+ - lib/opinionated_http/request.rb
68
+ - lib/opinionated_http/response.rb
68
69
  - lib/opinionated_http/version.rb
69
70
  - test/client_test.rb
70
71
  - test/config/application.yml
72
+ - test/request_test.rb
71
73
  - test/test_helper.rb
72
- homepage:
74
+ homepage:
73
75
  licenses: []
74
76
  metadata: {}
75
- post_install_message:
77
+ post_install_message:
76
78
  rdoc_options: []
77
79
  require_paths:
78
80
  - lib
@@ -87,11 +89,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
87
89
  - !ruby/object:Gem::Version
88
90
  version: '0'
89
91
  requirements: []
90
- rubygems_version: 3.0.8
91
- signing_key:
92
+ rubygems_version: 3.3.7
93
+ signing_key:
92
94
  specification_version: 4
93
95
  summary: Opinionated HTTP Client
94
96
  test_files:
95
97
  - test/client_test.rb
96
98
  - test/config/application.yml
99
+ - test/request_test.rb
97
100
  - test/test_helper.rb