opinionated_http 0.0.6 → 0.1.0

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: 5c63d8fd6c65797a7afcef26ff0ef72507dcf75f8dbbd4bc93e768cf0ddffc9a
4
- data.tar.gz: 0a56d817ad5a0a988886faf17b034df697712c3958719cbcd72558ebb1bd8ee1
3
+ metadata.gz: e0b33ff41de4de2487017dcb16c8af1bd049d8f59b99af148c2782f5aadbf90f
4
+ data.tar.gz: fbb4ddce12a6aabb67827d124fd579e91d4a2b4def78d74c893dc82687519c96
5
5
  SHA512:
6
- metadata.gz: f38a9f47681824038a271244c560639b119ff9ff138210c93eeb2e9d2e0955b05198f0956d5fba2f1b60b965bf4084e8f3aca0b89af759d6ecf78419d9f05903
7
- data.tar.gz: 4b882e2ec2025a9d626c1a3e6d52edff1a20db6fb6dc48bc10203f14117bb1856764f373c4a0ca0bd81f97fc75a4798a68a57b2cdeafd1c3ad7d6b36ef93a14d
6
+ metadata.gz: a3b31d0083666203057f05a38bdc0e7a5519af9757df935c9c772ca514bb7f8777c9b52a24edb960b201e5816bca356508d9470bfcd42fdcf83ee3bd622ec51b
7
+ data.tar.gz: b6b9ba368403e795fc19b81a759e30cd03de38fd436c52222db7f7e73f79d4ad38fb1054c7ad6a23a2cddf5c6f55654127e334c76b0f5fd5f0ab3deab606163d
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,83 @@ 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
41
45
  )
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)
46
+ @metric_prefix = metric_prefix
47
+ @logger = logger || SemanticLogger[self]
48
+ @error_class = error_class
49
+ @format = format
50
+ @after_connect = after_connect
51
+ SecretConfig.configure(secret_config_prefix) do |config|
52
+ @retry_count = config.fetch("retry_count", type: :integer, default: retry_count)
53
+ @retry_interval = config.fetch("retry_interval", type: :float, default: retry_interval)
54
+ @retry_multiplier = config.fetch("retry_multiplier", type: :float, default: retry_multiplier)
55
+ @max_redirects = config.fetch("max_redirects", type: :integer, default: max_redirects)
56
+ http_retry_codes = config.fetch("http_retry_codes", type: :string, default: http_retry_codes)
57
+ @http_retry_codes = http_retry_codes.split(",").collect(&:strip)
58
+
59
+ @url = url.nil? ? config.fetch("url") : config.fetch("url", default: url)
60
+
61
+ @pool_size = config.fetch("pool_size", type: :integer, default: pool_size)
62
+ @open_timeout = config.fetch("open_timeout", type: :float, default: open_timeout)
63
+ @read_timeout = config.fetch("read_timeout", type: :float, default: read_timeout)
64
+ @idle_timeout = config.fetch("idle_timeout", type: :float, default: idle_timeout)
65
+ @keep_alive = config.fetch("keep_alive", type: :float, default: keep_alive)
66
+ @pool_timeout = config.fetch("pool_timeout", type: :float, default: pool_timeout)
67
+ @warn_timeout = config.fetch("warn_timeout", type: :float, default: warn_timeout)
68
+ @proxy = config.fetch("proxy", type: :symbol, default: proxy)
69
+ @force_retry = config.fetch("force_retry", type: :boolean, default: force_retry)
70
+ @certificate = config.fetch("certificate", type: :string, default: certificate)
71
+ @private_key = config.fetch("private_key", type: :string, default: private_key)
72
+ @verify_peer = config.fetch("verify_peer", type: :boolean, default: verify_peer)
73
+ end
63
74
 
64
75
  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))
76
+ @driver = PersistentHTTP.new(
77
+ url: @url,
78
+ logger: internal_logger,
79
+ debug_output: internal_logger,
80
+ name: "",
81
+ pool_size: @pool_size,
82
+ open_timeout: @open_timeout,
83
+ read_timeout: @read_timeout,
84
+ idle_timeout: @idle_timeout,
85
+ keep_alive: @keep_alive,
86
+ pool_timeout: @pool_timeout,
87
+ warn_timeout: @warn_timeout,
88
+ proxy: @proxy,
89
+ force_retry: @force_retry,
90
+ after_connect: @after_connect,
91
+ certificate: @certificate,
92
+ private_key: @private_key,
93
+ verify_mode: @verify_peer ? OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT : OpenSSL::SSL::VERIFY_NONE
94
+ )
83
95
  end
84
96
 
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)
97
+ def get(request: nil, **args)
98
+ request ||= Request.new(**args)
99
+ request.verb = "Get"
100
+ http_response = request(request)
101
+ Response.new(http_response, request)
90
102
  end
91
103
 
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)
104
+ def post(request: nil, **args)
105
+ request ||= Request.new(**args)
106
+ request.verb = "Post"
107
+ http_response = request(request)
108
+ Response.new(http_response, request)
97
109
  end
98
110
 
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")
102
- 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)
109
-
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
114
-
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
119
- end
120
-
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)
111
+ # Returns [Response] after submitting the [Request]
112
+ def request(request)
113
+ request.metric_prefix ||= metric_prefix
114
+ request.format ||= format
115
+ request.error_class ||= error_class
116
+ request.logger ||= logger
117
+ request_with_retry(action: request.action, request: request.http_request)
127
118
  end
128
119
 
129
120
  private
@@ -134,12 +125,14 @@ module OpinionatedHTTP
134
125
  begin
135
126
  payload = {}
136
127
  if logger.trace?
137
- payload[:parameters] = parameters
138
- payload[:path] = request.path
128
+ # payload[:parameters] = parameters
129
+ payload[:path] = request.path
139
130
  end
140
131
  message = "HTTP #{http_method}: #{action}" if logger.debug?
141
132
 
142
- logger.benchmark_info(message: message, metric: "#{metric_prefix}/#{action}", payload: payload) { driver.request(request) }
133
+ logger.benchmark_info(message: message, metric: "#{metric_prefix}/#{action}", payload: payload) do
134
+ driver.request(request)
135
+ end
143
136
  rescue StandardError => e
144
137
  message = "HTTP #{http_method}: #{action} Failure: #{e.class.name}: #{e.message}"
145
138
  logger.error(message: message, metric: "#{metric_prefix}/exception", exception: e)
@@ -147,33 +140,19 @@ module OpinionatedHTTP
147
140
  end
148
141
 
149
142
  # 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)
143
+ return response unless http_retry_codes.include?(response.code)
169
144
 
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
145
+ if try_count >= retry_count
146
+ message = "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}. Retries Exhausted"
147
+ logger.error(message: message, metric: "#{metric_prefix}/exception")
148
+ raise(error_class, message)
149
+ end
174
150
 
175
- def prefix_path(path)
176
- path.start_with?("/") ? path : "/#{path}"
151
+ try_count += 1
152
+ duration = retry_sleep_interval(try_count)
153
+ logger.warn(message: "HTTP #{http_method}: #{action} Failure: (#{response.code}) #{response.message}. Retry: #{try_count}", metric: "#{metric_prefix}/retry", duration: duration * 1_000)
154
+ sleep(duration)
155
+ request_with_retry(action: action, request: request, try_count: try_count)
177
156
  end
178
157
 
179
158
  # First retry is immediate, next retry is after `retry_interval`,
@@ -181,13 +160,7 @@ module OpinionatedHTTP
181
160
  def retry_sleep_interval(retry_count)
182
161
  return 0 if retry_count <= 1
183
162
 
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")
163
+ (retry_multiplier ** (retry_count - 1)) * retry_interval
191
164
  end
192
165
  end
193
166
  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")
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.6".freeze
2
+ VERSION = "0.1.0".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,92 @@ 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
+ header: {"Content-Type" => "application/json"}
17
+ )
18
+ end
30
19
 
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
20
+ describe "get" do
21
+ it "succeeds" do
22
+ output = {zip: "12345", population: 54_321}
23
+ body = output.to_json
24
+ stub_request(Net::HTTPSuccess, 200, "OK", body) do
25
+ response = http.get(action: "lookup", parameters: {zip: "12345"})
26
+ assert_equal body, response.body!
39
27
  end
40
28
  end
41
29
 
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)
30
+ it "fails" do
31
+ message = "HTTP GET: lookup Failure: (403) Forbidden"
32
+ error = assert_raises ServiceError do
33
+ stub_request(Net::HTTPForbidden, 403, "Forbidden", "") do
34
+ response = http.get(action: "lookup", parameters: {zip: "12345"})
35
+ response.body!
57
36
  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
80
- end
81
- assert_equal message, error.message
82
37
  end
38
+ assert_equal message, error.message
83
39
  end
40
+ end
84
41
 
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
111
- end
112
-
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"]
42
+ describe "post" do
43
+ it "succeeds with body" do
44
+ output = {zip: "12345", population: 54_321}
45
+ body = output.to_json
46
+ stub_request(Net::HTTPSuccess, 200, "OK", body) do
47
+ response = http.post(action: "lookup", body: body)
48
+ assert_equal body, response.body!
120
49
  end
50
+ end
121
51
 
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"]
52
+ it "with form data" do
53
+ output = {zip: "12345", population: 54_321}
54
+ body = output.to_json
55
+ stub_request(Net::HTTPSuccess, 200, "OK", body) do
56
+ response = http.post(action: "lookup", form_data: output)
57
+ assert_equal body, response.body!
130
58
  end
59
+ end
131
60
 
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)
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
+ response = http.post(action: "lookup", body: body)
68
+ response.body!
144
69
  end
145
-
146
- assert http.build_request(path: path, verb: post_verb, headers: no_conflict_headers, form_data: form_data)
147
70
  end
71
+ assert_equal message, error.message
72
+ end
148
73
 
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)
74
+ it "fails with form data" do
75
+ output = {zip: "12345", population: 54_321}
76
+ message = "HTTP POST: lookup Failure: (403) Forbidden"
77
+ error = assert_raises ServiceError do
78
+ stub_request(Net::HTTPForbidden, 403, "Forbidden", "") do
79
+ response = http.post(action: "lookup", form_data: output)
80
+ response.body!
155
81
  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
82
  end
83
+ assert_equal message, error.message
160
84
  end
85
+ end
161
86
 
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
87
+ def stub_request(klass, code, msg, body, &block)
88
+ response = klass.new("1.1", code, msg)
89
+ response.stub(:body, body) do
90
+ http.driver.stub(:request, response, &block)
167
91
  end
168
92
  end
169
93
  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.6
4
+ version: 0.1.0
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-11 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