opinionated_http 0.0.5 → 0.1.1

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
2
  SHA256:
3
- metadata.gz: e42aeb5b1a1b2647f48288e3b34b54064b327f72b0cce291ea75a1c0a563eb79
4
- data.tar.gz: 533c7d1bf33a594ebb5b910e25a4ebf0b8420d9e5a24d0aac5f99e83e779fbe4
3
+ metadata.gz: 70883413762ba2277b74061e20f9fd5faa73fccee8a762595a40793a93ed2689
4
+ data.tar.gz: 45bf3291006a3577a29f5fd603ad27c34b1d6948c54e67f71efb20862c0af613
5
5
  SHA512:
6
- metadata.gz: e2555937900d00a7b139a1bee2b76de642065b8b0469aeebb7783934c588bd032dc9b72b211e7ba5b04f06d01f55ece6f7a45d44439f14303c051e71cb4207bc
7
- data.tar.gz: c78cfe4681bfe65cba9d570878dcf78328059d5835e9de0cbff7d416d7ee5954cc4d4fd831f459373027bff9cf37f096ad12f5108ae40fd32db5502fd02ebd12
6
+ metadata.gz: eec7f0a191186d29a970284e04dc571a2c8329587a414fa41576852e59d08503928594e14fc8e65f9ebea1570f90e15ad5e878a7b26259eae4adc605e7e9063f
7
+ data.tar.gz: ac1ce6b6f65d720cf7ddac4d93621c608f609ad1e113ead056e710e0cfe37df70792459940cdd3ca9effe0d8a123e60df11e89cf2a713ab7855c1148ea29e86d
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)
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)
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
@@ -134,12 +149,14 @@ module OpinionatedHTTP
134
149
  begin
135
150
  payload = {}
136
151
  if logger.trace?
137
- payload[:parameters] = parameters
138
- payload[:path] = request.path
152
+ # payload[:parameters] = parameters
153
+ payload[:path] = request.path
139
154
  end
140
155
  message = "HTTP #{http_method}: #{action}" if logger.debug?
141
156
 
142
- logger.benchmark_info(message: message, metric: "#{metric_prefix}/#{action}", payload: payload) { driver.request(request) }
157
+ logger.benchmark_info(message: message, metric: "#{metric_prefix}/#{action}", payload: payload) do
158
+ driver.request(request)
159
+ end
143
160
  rescue StandardError => e
144
161
  message = "HTTP #{http_method}: #{action} Failure: #{e.class.name}: #{e.message}"
145
162
  logger.error(message: message, metric: "#{metric_prefix}/exception", exception: e)
@@ -147,33 +164,19 @@ module OpinionatedHTTP
147
164
  end
148
165
 
149
166
  # 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)
168
- return response.body if response.is_a?(Net::HTTPSuccess)
167
+ return response unless http_retry_codes.include?(response.code)
169
168
 
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
169
+ if try_count >= retry_count
170
+ message = "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}. Retries Exhausted"
171
+ logger.error(message: message, metric: "#{metric_prefix}/exception")
172
+ raise(error_class, message)
173
+ end
174
174
 
175
- def prefix_path(path)
176
- path.start_with?("/") ? path : "/#{path}"
175
+ try_count += 1
176
+ duration = retry_sleep_interval(try_count)
177
+ logger.warn(message: "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}. Retry: #{try_count}", metric: "#{metric_prefix}/retry", duration: duration * 1_000)
178
+ sleep(duration)
179
+ request_with_retry(action: action, request: request, try_count: try_count)
177
180
  end
178
181
 
179
182
  # First retry is immediate, next retry is after `retry_interval`,
@@ -181,13 +184,7 @@ module OpinionatedHTTP
181
184
  def retry_sleep_interval(retry_count)
182
185
  return 0 if retry_count <= 1
183
186
 
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")
187
+ (retry_multiplier ** (retry_count - 1)) * retry_interval
191
188
  end
192
189
  end
193
190
  end
@@ -0,0 +1,77 @@
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
+ return if body.nil?
55
+
56
+ case format
57
+ when :json
58
+ headers["Content-Type"] = "application/json"
59
+ body.to_json unless body.is_a?(String) || body.nil?
60
+ when nil
61
+ body
62
+ else
63
+ raise(ArgumentError, "Unknown format: #{format.inspect}")
64
+ end
65
+ rescue StandardError => exc
66
+ message = "Failed to serialize request body. #{exc.class.name}: #{exc.message}"
67
+ logger.error(message: message, metric: "#{metric_prefix}/exception", exception: exc)
68
+ raise(error_class, message)
69
+ end
70
+
71
+ def headers_and_form_data_compatible?(headers, form_data)
72
+ return true if headers.empty? || form_data.nil?
73
+
74
+ !headers.keys.map(&:downcase).include?("content-type")
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,55 @@
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
+ error_message = "HTTP #{verb.upcase}: #{action} Failure: (#{code}) #{message}"
29
+ logger.error(message: error_message, metric: "#{metric_prefix}/exception", payload: {body: body})
30
+ raise(error_class, error_message)
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :http_response, :request
36
+
37
+ def parse_body
38
+ return unless http_response.class.body_permitted?
39
+
40
+ case format
41
+ when :json
42
+ JSON.parse(http_response.body)
43
+ when nil
44
+ http_response.body
45
+ else
46
+ raise(ArgumentError, "Unknown format: #{format.inspect}")
47
+ end
48
+
49
+ rescue StandardError => exc
50
+ message = "Failed to parse response body. #{exc.class.name}: #{exc.message}"
51
+ logger.error(message: message, metric: "#{metric_prefix}/exception", exception: exc)
52
+ raise(error_class, message)
53
+ end
54
+ end
55
+ end
@@ -1,3 +1,3 @@
1
1
  module OpinionatedHTTP
2
- VERSION = "0.0.5".freeze
2
+ VERSION = "0.1.1".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,135 +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
5
+ class ClientTest < Minitest::Test
6
+ describe OpinionatedHTTP::Client do
7
+ class ServiceError < StandardError
8
+ end
10
9
 
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
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
20
18
 
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
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!
29
26
  end
30
27
  end
31
28
 
32
- describe "post" do
33
- it "succeeds with body" do
34
- output = {zip: "12345", population: 54_321}
35
- body = output.to_json
36
- response = stub_request(Net::HTTPSuccess, 200, "OK", body) do
37
- http.post(action: "lookup", body: body)
38
- end
39
- assert_equal body, response
40
- end
41
-
42
- it "with form data" do
43
- output = {zip: "12345", population: 54_321}
44
- body = output.to_json
45
- response = stub_request(Net::HTTPSuccess, 200, "OK", body) do
46
- http.post(action: "lookup", form_data: output)
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!
47
35
  end
48
- assert_equal body, response
49
36
  end
37
+ assert_equal message, error.message
50
38
  end
39
+ end
51
40
 
52
- describe "build_request" do
53
- let(:path) { "/fake_action" }
54
- let(:post_verb) { "Post" }
55
- let(:get_verb) { "Get" }
56
-
57
- it "creates a request corresponding to the supplied verb" do
58
- req = http.build_request(path: path, verb: post_verb)
59
- req2 = http.build_request(path: path, verb: get_verb)
60
-
61
- assert_kind_of Net::HTTP::Post, req
62
- assert_kind_of Net::HTTP::Get, req2
63
- end
64
-
65
- it "returns a request with supplied headers" do
66
- test_headers = {"test1" => "yes_test_1", "test2" => "yes_test_2"}
67
- req = http.build_request(path: path, verb: get_verb, headers: test_headers)
68
-
69
- assert_equal test_headers["test1"], req["test1"]
70
- assert_equal test_headers["test2"], req["test2"]
71
- end
72
-
73
- it "returns a request with supplied body" do
74
- test_body = "nice bod"
75
- req = http.build_request(path: path, verb: post_verb, body: test_body)
76
-
77
- 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!
78
48
  end
49
+ end
79
50
 
80
- it "returns a request with supplied form data in x-www-form-urlencoded Content-Type" do
81
- test_data = {test1: "yes", test2: "no"}
82
- expected_string = "test1=yes&test2=no"
83
- req = http.build_request(path: path, verb: post_verb, form_data: test_data)
84
-
85
- assert_equal expected_string, req.body
86
- 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!
87
57
  end
58
+ end
88
59
 
89
- it "add supplied authentication to the request" do
90
- test_un = "admin"
91
- test_pw = "hunter2"
92
- req = http.build_request(path: path, verb: get_verb, username: test_un, password: test_pw)
93
- req2 = Net::HTTP::Get.new(path)
94
- req2.basic_auth test_un, test_pw
95
-
96
- 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)
97
66
  end
67
+ end
98
68
 
99
- it "raise an error if supplied content-type header would be overwritten by setting form_data" do
100
- downcase_headers = {"unimportant" => "blank", "content-type" => "application/json"}
101
- capitalized_headers = {"Unimportant" => "blank", "Content-Type" => "application/json"}
102
- no_conflict_headers = {"whatever" => "blank", "irrelevant" => "test"}
103
- form_data = {thing1: 1, thing2: 2}
104
-
105
- assert_raises ArgumentError do
106
- http.build_request(path: path, verb: post_verb, headers: downcase_headers, form_data: form_data)
107
- end
108
-
109
- assert_raises ArgumentError do
110
- 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!
111
77
  end
112
-
113
- assert http.build_request(path: path, verb: post_verb, headers: no_conflict_headers, form_data: form_data)
114
78
  end
79
+ assert_equal message, error.message
80
+ end
115
81
 
116
- it "raise an error if there is a collision between supplied body and form_data" do
117
- form_data = {thing1: 1, thing2: 2}
118
- body = "not form data"
119
-
120
- assert_raises ArgumentError do
121
- 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!
122
89
  end
123
-
124
- assert http.build_request(path: path, verb: post_verb, body: body)
125
- assert http.build_request(path: path, verb: post_verb, form_data: form_data)
126
90
  end
91
+ assert_equal message, error.message
127
92
  end
93
+ end
128
94
 
129
- def stub_request(klass, code, msg, body, &block)
130
- response = klass.new("1.1", code, msg)
131
- response.stub(:body, body) do
132
- http.driver.stub(:request, response, &block)
133
- 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)
134
99
  end
135
100
  end
136
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,95 @@
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"]
29
+ end
30
+
31
+ it "returns a request with supplied headers" do
32
+ test_headers = {"test1" => "yes_test_1", "test2" => "yes_test_2"}
33
+ req = OpinionatedHTTP::Request.new(action: action, path: path, verb: get_verb, headers: test_headers).http_request
34
+
35
+ assert_equal test_headers["test1"], req["test1"]
36
+ assert_equal test_headers["test2"], req["test2"]
37
+ end
38
+
39
+ it "returns a request with supplied body" do
40
+ test_body = "nice bod"
41
+ req = OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, body: test_body).http_request
42
+
43
+ assert_equal test_body, req.body
44
+ end
45
+
46
+ it "returns a request with supplied form data in x-www-form-urlencoded Content-Type" do
47
+ test_data = {test1: "yes", test2: "no"}
48
+ expected_string = "test1=yes&test2=no"
49
+ req = OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, form_data: test_data).http_request
50
+
51
+ assert_equal expected_string, req.body
52
+ assert_equal "application/x-www-form-urlencoded", req["Content-Type"]
53
+ end
54
+
55
+ it "add supplied authentication to the request" do
56
+ test_un = "admin"
57
+ test_pw = "hunter2"
58
+ req = OpinionatedHTTP::Request.new(action: action, path: path, verb: get_verb, username: test_un, password: test_pw).http_request
59
+ req2 = Net::HTTP::Get.new(path)
60
+ req2.basic_auth test_un, test_pw
61
+
62
+ assert_equal req2["authorization"], req["authorization"]
63
+ end
64
+
65
+ it "raise an error if supplied content-type header would be overwritten by setting form_data" do
66
+ downcase_headers = {"unimportant" => "blank", "content-type" => "application/json"}
67
+ capitalized_headers = {"Unimportant" => "blank", "Content-Type" => "application/json"}
68
+ no_conflict_headers = {"whatever" => "blank", "irrelevant" => "test"}
69
+ form_data = {thing1: 1, thing2: 2}
70
+
71
+ assert_raises ArgumentError do
72
+ OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, headers: downcase_headers, form_data: form_data).http_request
73
+ end
74
+
75
+ assert_raises ArgumentError do
76
+ OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, headers: capitalized_headers, form_data: form_data).http_request
77
+ end
78
+
79
+ assert OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, headers: no_conflict_headers, form_data: form_data).http_request
80
+ end
81
+
82
+ it "raise an error if there is a collision between supplied body and form_data" do
83
+ form_data = {thing1: 1, thing2: 2}
84
+ body = "not form data"
85
+
86
+ assert_raises ArgumentError do
87
+ OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, body: body, form_data: form_data).http_request
88
+ end
89
+
90
+ assert OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, body: body).http_request
91
+ assert OpinionatedHTTP::Request.new(action: action, path: path, verb: post_verb, form_data: form_data).http_request
92
+ end
93
+ end
94
+ end
95
+ 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.5
4
+ version: 0.1.1
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-04-29 00:00:00.000000000 Z
11
+ date: 2022-03-15 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