opinionated_http 0.0.1 → 0.0.6
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 +4 -4
- data/README.md +53 -0
- data/Rakefile +7 -7
- data/lib/opinionated_http.rb +5 -64
- data/lib/opinionated_http/client.rb +149 -54
- data/lib/opinionated_http/version.rb +1 -1
- data/test/client_test.rb +148 -11
- data/test/test_helper.rb +10 -10
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5c63d8fd6c65797a7afcef26ff0ef72507dcf75f8dbbd4bc93e768cf0ddffc9a
|
4
|
+
data.tar.gz: 0a56d817ad5a0a988886faf17b034df697712c3958719cbcd72558ebb1bd8ee1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f38a9f47681824038a271244c560639b119ff9ff138210c93eeb2e9d2e0955b05198f0956d5fba2f1b60b965bf4084e8f3aca0b89af759d6ecf78419d9f05903
|
7
|
+
data.tar.gz: 4b882e2ec2025a9d626c1a3e6d52edff1a20db6fb6dc48bc10203f14117bb1856764f373c4a0ca0bd81f97fc75a4798a68a57b2cdeafd1c3ad7d6b36ef93a14d
|
data/README.md
CHANGED
@@ -17,3 +17,56 @@ PersistentHTTP with the following enhancements:
|
|
17
17
|
* Standardized Service Exception.
|
18
18
|
* Retries on HTTP 5XX errors
|
19
19
|
|
20
|
+
# Example
|
21
|
+
|
22
|
+
# Configuration
|
23
|
+
|
24
|
+
# Usage
|
25
|
+
|
26
|
+
Create a new Opinionated HTTP instance.
|
27
|
+
|
28
|
+
Parameters:
|
29
|
+
secret_config_prefix:
|
30
|
+
Required
|
31
|
+
metric_prefix:
|
32
|
+
Required
|
33
|
+
error_class:
|
34
|
+
Whenever exceptions are raised it is important that every client gets its own exception / error class
|
35
|
+
so that failures to specific http servers can be easily identified.
|
36
|
+
Required.
|
37
|
+
logger:
|
38
|
+
Default: SemanticLogger[OpinionatedHTTP]
|
39
|
+
Other options as supported by PersistentHTTP
|
40
|
+
#TODO: Expand PersistentHTTP options here
|
41
|
+
|
42
|
+
Configuration:
|
43
|
+
Off of the `secret_config_path` path above, Opinionated HTTP uses specific configuration entry names
|
44
|
+
to configure the underlying HTTP setup:
|
45
|
+
url: [String]
|
46
|
+
The host url to the site to connect to.
|
47
|
+
Exclude any path, since that will be supplied when `#get` or `#post` is called.
|
48
|
+
Required.
|
49
|
+
Examples:
|
50
|
+
"https://example.com"
|
51
|
+
"https://example.com:8443/"
|
52
|
+
pool_size: [Integer]
|
53
|
+
default: 100
|
54
|
+
open_timeout: [Float]
|
55
|
+
default: 10
|
56
|
+
read_timeout: [Float]
|
57
|
+
default: 10
|
58
|
+
idle_timeout: [Float]
|
59
|
+
default: 300
|
60
|
+
keep_alive: [Float]
|
61
|
+
default: 300
|
62
|
+
pool_timeout: [Float]
|
63
|
+
default: 5
|
64
|
+
warn_timeout: [Float]
|
65
|
+
default: 0.25
|
66
|
+
proxy: [Symbol]
|
67
|
+
default: :ENV
|
68
|
+
force_retry: [true|false]
|
69
|
+
default: true
|
70
|
+
|
71
|
+
Metrics:
|
72
|
+
During each call to `#get` or `#put`, the following metrics are logged using the
|
data/Rakefile
CHANGED
@@ -1,23 +1,23 @@
|
|
1
1
|
# Setup bundler to avoid having to run bundle exec all the time.
|
2
|
-
require
|
3
|
-
require
|
2
|
+
require "rubygems"
|
3
|
+
require "bundler/setup"
|
4
4
|
|
5
|
-
require
|
6
|
-
require_relative
|
5
|
+
require "rake/testtask"
|
6
|
+
require_relative "lib/opinionated_http/version"
|
7
7
|
|
8
8
|
task :gem do
|
9
|
-
system
|
9
|
+
system "gem build opinionated_http.gemspec"
|
10
10
|
end
|
11
11
|
|
12
12
|
task publish: :gem do
|
13
13
|
system "git tag -a v#{OpinionatedHTTP::VERSION} -m 'Tagging #{OpinionatedHTTP::VERSION}'"
|
14
|
-
system
|
14
|
+
system "git push --tags"
|
15
15
|
system "gem push opinionated_http-#{OpinionatedHTTP::VERSION}.gem"
|
16
16
|
system "rm opinionated_http-#{OpinionatedHTTP::VERSION}.gem"
|
17
17
|
end
|
18
18
|
|
19
19
|
Rake::TestTask.new(:test) do |t|
|
20
|
-
t.pattern =
|
20
|
+
t.pattern = "test/**/*_test.rb"
|
21
21
|
t.verbose = true
|
22
22
|
t.warning = false
|
23
23
|
end
|
data/lib/opinionated_http.rb
CHANGED
@@ -1,76 +1,17 @@
|
|
1
|
+
require "opinionated_http/version"
|
1
2
|
#
|
2
3
|
# Opinionated HTTP
|
3
4
|
#
|
4
5
|
# An opinionated HTTP Client library using convention over configuration.
|
5
6
|
#
|
6
|
-
# Uses
|
7
|
-
# * PersistentHTTP for http connection pooling.
|
8
|
-
# * Semantic Logger for logging and metrics.
|
9
|
-
# * Secret Config for its configuration.
|
10
|
-
#
|
11
|
-
# By convention the following metrics are measured and logged:
|
12
|
-
# *
|
13
|
-
#
|
14
|
-
# PersistentHTTP with the following enhancements:
|
15
|
-
# * Read config from Secret Config, just supply the `secret_config_path`.
|
16
|
-
# * Redirect logging into standard Semantic Logger.
|
17
|
-
# * Implements metrics and measure call durations.
|
18
|
-
# * Standardized Service Exception.
|
19
|
-
# * Retries on HTTP 5XX errors
|
20
|
-
#
|
21
|
-
|
22
|
-
require 'opinionated_http/version'
|
23
7
|
module OpinionatedHTTP
|
24
|
-
autoload :Client,
|
25
|
-
autoload :Logger,
|
8
|
+
autoload :Client, "opinionated_http/client"
|
9
|
+
autoload :Logger, "opinionated_http/logger"
|
26
10
|
|
27
|
-
# Create a new Opinionated HTTP instance.
|
28
|
-
#
|
29
|
-
# Parameters:
|
30
|
-
# secret_config_prefix:
|
31
|
-
# Required
|
32
|
-
# metric_prefix:
|
33
|
-
# Required
|
34
|
-
# error_class:
|
35
|
-
# Whenever exceptions are raised it is important that every client gets its own exception / error class
|
36
|
-
# so that failures to specific http servers can be easily identified.
|
37
|
-
# Required.
|
38
|
-
# logger:
|
39
|
-
# Default: SemanticLogger[OpinionatedHTTP]
|
40
|
-
# Other options as supported by PersistentHTTP
|
41
|
-
# #TODO: Expand PersistentHTTP options here
|
42
11
|
#
|
43
|
-
#
|
44
|
-
# Off of the `secret_config_path` path above, Opinionated HTTP uses specific configuration entry names
|
45
|
-
# to configure the underlying HTTP setup:
|
46
|
-
# url: [String]
|
47
|
-
# The host url to the site to connect to.
|
48
|
-
# Exclude any path, since that will be supplied when `#get` or `#post` is called.
|
49
|
-
# Required.
|
50
|
-
# Examples:
|
51
|
-
# "https://example.com"
|
52
|
-
# "https://example.com:8443/"
|
53
|
-
# pool_size: [Integer]
|
54
|
-
# default: 100
|
55
|
-
# open_timeout: [Float]
|
56
|
-
# default: 10
|
57
|
-
# read_timeout: [Float]
|
58
|
-
# default: 10
|
59
|
-
# idle_timeout: [Float]
|
60
|
-
# default: 300
|
61
|
-
# keep_alive: [Float]
|
62
|
-
# default: 300
|
63
|
-
# pool_timeout: [Float]
|
64
|
-
# default: 5
|
65
|
-
# warn_timeout: [Float]
|
66
|
-
# default: 0.25
|
67
|
-
# proxy: [Symbol]
|
68
|
-
# default: :ENV
|
69
|
-
# force_retry: [true|false]
|
70
|
-
# default: true
|
12
|
+
# Create a new Opinionated HTTP instance.
|
71
13
|
#
|
72
|
-
#
|
73
|
-
# During each call to `#get` or `#put`, the following metrics are logged using the
|
14
|
+
# See README.md for more info.
|
74
15
|
def self.new(**args)
|
75
16
|
Client.new(**args)
|
76
17
|
end
|
@@ -1,32 +1,80 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
1
|
+
require "persistent_http"
|
2
|
+
require "secret_config"
|
3
|
+
require "semantic_logger"
|
4
4
|
#
|
5
5
|
# Client http implementation
|
6
6
|
#
|
7
|
+
# See README.md for more info.
|
7
8
|
module OpinionatedHTTP
|
8
9
|
class Client
|
9
|
-
|
10
|
+
# 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout
|
11
|
+
HTTP_RETRY_CODES = %w[502 503 504].freeze
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
attr_reader :secret_config_prefix, :logger, :metric_prefix, :error_class, :driver,
|
14
|
+
:retry_count, :retry_interval, :retry_multiplier, :http_retry_codes,
|
15
|
+
:url, :pool_size, :keep_alive, :proxy, :force_retry, :max_redirects,
|
16
|
+
:open_timeout, :read_timeout, :idle_timeout, :pool_timeout, :warn_timeout
|
17
|
+
|
18
|
+
# 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
|
+
def initialize(
|
21
|
+
secret_config_prefix:,
|
22
|
+
metric_prefix:,
|
23
|
+
error_class:,
|
24
|
+
logger: nil,
|
25
|
+
retry_count: 11,
|
26
|
+
retry_interval: 0.01,
|
27
|
+
retry_multiplier: 1.8,
|
28
|
+
http_retry_codes: HTTP_RETRY_CODES.join(","),
|
29
|
+
url: nil,
|
30
|
+
pool_size: 100,
|
31
|
+
open_timeout: 10,
|
32
|
+
read_timeout: 10,
|
33
|
+
idle_timeout: 300,
|
34
|
+
keep_alive: 300,
|
35
|
+
pool_timeout: 5,
|
36
|
+
warn_timeout: 0.25,
|
37
|
+
proxy: :ENV,
|
38
|
+
force_retry: true,
|
39
|
+
max_redirects: 10,
|
40
|
+
**options
|
41
|
+
)
|
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)
|
15
63
|
|
16
64
|
internal_logger = OpinionatedHTTP::Logger.new(@logger)
|
17
65
|
new_options = {
|
18
66
|
logger: internal_logger,
|
19
67
|
debug_output: internal_logger,
|
20
68
|
name: "",
|
21
|
-
pool_size:
|
22
|
-
open_timeout:
|
23
|
-
read_timeout:
|
24
|
-
idle_timeout:
|
25
|
-
keep_alive:
|
26
|
-
pool_timeout:
|
27
|
-
warn_timeout:
|
28
|
-
proxy:
|
29
|
-
force_retry:
|
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
|
30
78
|
}
|
31
79
|
|
32
80
|
url = SecretConfig["#{secret_config_prefix}/url"]
|
@@ -35,64 +83,111 @@ module OpinionatedHTTP
|
|
35
83
|
end
|
36
84
|
|
37
85
|
# Perform an HTTP Get against the supplied path
|
38
|
-
def get(action:, path: "/#{action}",
|
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)
|
90
|
+
end
|
91
|
+
|
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
|
98
|
+
|
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
|
+
|
39
105
|
path = "/#{path}" unless path.start_with?("/")
|
40
106
|
path = "#{path}?#{URI.encode_www_form(parameters)}" if parameters
|
41
107
|
|
42
|
-
request
|
43
|
-
response =
|
44
|
-
begin
|
45
|
-
payload = {}
|
46
|
-
if logger.trace?
|
47
|
-
payload[:parameters] = parameters
|
48
|
-
payload[:path] = path
|
49
|
-
end
|
50
|
-
message = "HTTP GET: #{action}" if logger.debug?
|
51
|
-
|
52
|
-
logger.benchmark_info(message: message, metric: "#{metric_prefix}/#{action}", payload: payload) { driver.request(request) }
|
53
|
-
rescue StandardError => exc
|
54
|
-
message = "HTTP GET: #{action} Failure: #{exc.class.name}: #{exc.message}"
|
55
|
-
logger.error(message: message, metric: "#{metric_prefix}/exception", exception: exc)
|
56
|
-
raise(error_class, message)
|
57
|
-
end
|
108
|
+
request = Net::HTTP.const_get(verb).new(path, headers)
|
58
109
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
raise(error_class, message)
|
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}")
|
63
113
|
end
|
64
114
|
|
65
|
-
|
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
|
66
119
|
end
|
67
120
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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)
|
127
|
+
end
|
72
128
|
|
73
|
-
|
129
|
+
private
|
130
|
+
|
131
|
+
def request_with_retry(action:, request:, try_count: 0)
|
132
|
+
http_method = request.method.upcase
|
133
|
+
response =
|
74
134
|
begin
|
75
135
|
payload = {}
|
76
136
|
if logger.trace?
|
77
137
|
payload[:parameters] = parameters
|
78
|
-
payload[:path] = path
|
138
|
+
payload[:path] = request.path
|
79
139
|
end
|
80
|
-
message = "HTTP
|
140
|
+
message = "HTTP #{http_method}: #{action}" if logger.debug?
|
81
141
|
|
82
142
|
logger.benchmark_info(message: message, metric: "#{metric_prefix}/#{action}", payload: payload) { driver.request(request) }
|
83
|
-
rescue StandardError =>
|
84
|
-
message = "HTTP
|
85
|
-
logger.error(message: message, metric: "#{metric_prefix}/exception", exception:
|
143
|
+
rescue StandardError => e
|
144
|
+
message = "HTTP #{http_method}: #{action} Failure: #{e.class.name}: #{e.message}"
|
145
|
+
logger.error(message: message, metric: "#{metric_prefix}/exception", exception: e)
|
86
146
|
raise(error_class, message)
|
87
147
|
end
|
88
148
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
149
|
+
# 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
|
93
162
|
end
|
94
163
|
|
95
|
-
response
|
164
|
+
response
|
165
|
+
end
|
166
|
+
|
167
|
+
def extract_body(response, http_method, action)
|
168
|
+
return response.body if response.is_a?(Net::HTTPSuccess)
|
169
|
+
|
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
|
174
|
+
|
175
|
+
def prefix_path(path)
|
176
|
+
path.start_with?("/") ? path : "/#{path}"
|
177
|
+
end
|
178
|
+
|
179
|
+
# First retry is immediate, next retry is after `retry_interval`,
|
180
|
+
# each subsequent retry interval is 100% longer than the prior interval.
|
181
|
+
def retry_sleep_interval(retry_count)
|
182
|
+
return 0 if retry_count <= 1
|
183
|
+
|
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")
|
96
191
|
end
|
97
192
|
end
|
98
193
|
end
|
data/test/client_test.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require_relative
|
1
|
+
require "net/http"
|
2
|
+
require "json"
|
3
|
+
require_relative "test_helper"
|
4
4
|
|
5
5
|
module OpinionatedHTTP
|
6
6
|
class ClientTest < Minitest::Test
|
@@ -10,22 +10,159 @@ module OpinionatedHTTP
|
|
10
10
|
|
11
11
|
let :http do
|
12
12
|
OpinionatedHTTP.new(
|
13
|
-
secret_config_prefix:
|
14
|
-
metric_prefix:
|
13
|
+
secret_config_prefix: "fake_service",
|
14
|
+
metric_prefix: "FakeService",
|
15
15
|
logger: SemanticLogger["FakeService"],
|
16
16
|
error_class: ServiceError,
|
17
|
-
header: {
|
17
|
+
header: {"Content-Type" => "application/json"}
|
18
18
|
)
|
19
19
|
end
|
20
20
|
|
21
21
|
describe "get" do
|
22
|
-
it
|
23
|
-
output = {zip:
|
22
|
+
it "succeeds" do
|
23
|
+
output = {zip: "12345", population: 54_321}
|
24
24
|
body = output.to_json
|
25
|
-
response = Net::HTTPSuccess
|
26
|
-
|
27
|
-
http.get(action: 'lookup', parameters: {zip: '12345'})
|
25
|
+
response = stub_request(Net::HTTPSuccess, 200, "OK", body) do
|
26
|
+
http.get(action: "lookup", parameters: {zip: "12345"})
|
28
27
|
end
|
28
|
+
assert_equal body, response
|
29
|
+
end
|
30
|
+
|
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
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
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
|
80
|
+
end
|
81
|
+
assert_equal message, error.message
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
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"]
|
120
|
+
end
|
121
|
+
|
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"]
|
130
|
+
end
|
131
|
+
|
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)
|
144
|
+
end
|
145
|
+
|
146
|
+
assert http.build_request(path: path, verb: post_verb, headers: no_conflict_headers, form_data: form_data)
|
147
|
+
end
|
148
|
+
|
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)
|
155
|
+
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
|
+
end
|
160
|
+
end
|
161
|
+
|
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)
|
29
166
|
end
|
30
167
|
end
|
31
168
|
end
|
data/test/test_helper.rb
CHANGED
@@ -1,14 +1,14 @@
|
|
1
|
-
$LOAD_PATH.unshift File.dirname(__FILE__) +
|
2
|
-
ENV[
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + "/../lib"
|
2
|
+
ENV["TZ"] = "America/New_York"
|
3
3
|
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
4
|
+
require "yaml"
|
5
|
+
require "minitest/autorun"
|
6
|
+
require "awesome_print"
|
7
|
+
require "secret_config"
|
8
|
+
require "semantic_logger"
|
9
|
+
require "opinionated_http"
|
10
10
|
|
11
|
-
SemanticLogger.add_appender(file_name:
|
11
|
+
SemanticLogger.add_appender(file_name: "test.log", formatter: :color)
|
12
12
|
SemanticLogger.default_level = :debug
|
13
13
|
|
14
|
-
SecretConfig.use :file, path:
|
14
|
+
SecretConfig.use :file, path: "test", file_name: File.expand_path("config/application.yml", __dir__)
|
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.
|
4
|
+
version: 0.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Reid Morrison
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-06-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: persistent_http
|
@@ -87,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
87
87
|
- !ruby/object:Gem::Version
|
88
88
|
version: '0'
|
89
89
|
requirements: []
|
90
|
-
rubygems_version: 3.0.
|
90
|
+
rubygems_version: 3.0.8
|
91
91
|
signing_key:
|
92
92
|
specification_version: 4
|
93
93
|
summary: Opinionated HTTP Client
|