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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 21c5b626468b387d5b10f055e3f45b399d1b7c0e91a3962a31de1914f582848e
4
- data.tar.gz: 4190403fda9fd6bb3853f8d552aa53f22e1ddf89ead008ee4fde7d9f184c7eaa
3
+ metadata.gz: 5c63d8fd6c65797a7afcef26ff0ef72507dcf75f8dbbd4bc93e768cf0ddffc9a
4
+ data.tar.gz: 0a56d817ad5a0a988886faf17b034df697712c3958719cbcd72558ebb1bd8ee1
5
5
  SHA512:
6
- metadata.gz: ff4c0ec5fadca2ac6a6cc6bbabb1e0893aa7064da3adbbfb33ef1c3561df302e2408b983ad1598360fab213ca5c35f5cc14765bcde59da95c8684d4f7f32894a
7
- data.tar.gz: 274ff50c9ab8770f292ac37f2341480e01384ceddf8ef08a2ae644c139b5ecb965e0621309dd381f64e2e0a5d44bc2ee06b8037ada96c8731b8cdf645d8ab035
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 'rubygems'
3
- require 'bundler/setup'
2
+ require "rubygems"
3
+ require "bundler/setup"
4
4
 
5
- require 'rake/testtask'
6
- require_relative 'lib/opinionated_http/version'
5
+ require "rake/testtask"
6
+ require_relative "lib/opinionated_http/version"
7
7
 
8
8
  task :gem do
9
- system 'gem build opinionated_http.gemspec'
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 'git push --tags'
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 = 'test/**/*_test.rb'
20
+ t.pattern = "test/**/*_test.rb"
21
21
  t.verbose = true
22
22
  t.warning = false
23
23
  end
@@ -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, 'opinionated_http/client'
25
- autoload :Logger, 'opinionated_http/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
- # Configuration:
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
- # Metrics:
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 'persistent_http'
2
- require 'secret_config'
3
- require 'semantic_logger'
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
- attr_reader :secret_config_prefix, :logger, :metric_prefix, :error_class, :driver
10
+ # 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout
11
+ HTTP_RETRY_CODES = %w[502 503 504].freeze
10
12
 
11
- def initialize(secret_config_prefix:, logger: nil, metric_prefix:, error_class:, **options)
12
- @metric_prefix = metric_prefix
13
- @logger = logger || SemanticLogger[self]
14
- @error_class = error_class
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: SecretConfig.fetch("#{secret_config_prefix}/pool_size", type: :integer, default: 100),
22
- open_timeout: SecretConfig.fetch("#{secret_config_prefix}/open_timeout", type: :float, default: 10),
23
- read_timeout: SecretConfig.fetch("#{secret_config_prefix}/read_timeout", type: :float, default: 10),
24
- idle_timeout: SecretConfig.fetch("#{secret_config_prefix}/idle_timeout", type: :float, default: 300),
25
- keep_alive: SecretConfig.fetch("#{secret_config_prefix}/keep_alive", type: :float, default: 300),
26
- pool_timeout: SecretConfig.fetch("#{secret_config_prefix}/pool_timeout", type: :float, default: 5),
27
- warn_timeout: SecretConfig.fetch("#{secret_config_prefix}/warn_timeout", type: :float, default: 0.25),
28
- proxy: SecretConfig.fetch("#{secret_config_prefix}/proxy", type: :symbol, default: :ENV),
29
- force_retry: SecretConfig.fetch("#{secret_config_prefix}/force_retry", type: :boolean, default: true),
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}", parameters: nil)
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 = Net::HTTP::Get.new(path)
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
- unless response.is_a?(Net::HTTPSuccess)
60
- message = "HTTP GET: #{action} Failure: (#{response.code}) #{response.message}"
61
- logger.error(message: message, metric: "#{metric_prefix}/exception")
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
- response.body
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
- def post(action:, path: "/#{action}", parameters: nil)
69
- path = "/#{path}" unless path.start_with?("/")
70
- request = Net::HTTP::Post.new(path)
71
- request.set_form_data(*parameters) if parameters
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
- response =
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 POST: #{action}" if logger.debug?
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 => exc
84
- message = "HTTP POST: #{action} Failure: #{exc.class.name}: #{exc.message}"
85
- logger.error(message: message, metric: "#{metric_prefix}/exception", exception: exc)
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
- unless response.is_a?(Net::HTTPSuccess)
90
- message = "HTTP POST: #{action} Failure: (#{response.code}) #{response.message}"
91
- logger.error(message: message, metric: "#{metric_prefix}/exception")
92
- raise(error_class, message)
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.body
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
@@ -1,3 +1,3 @@
1
1
  module OpinionatedHTTP
2
- VERSION = "0.0.1".freeze
2
+ VERSION = "0.0.6".freeze
3
3
  end
@@ -1,6 +1,6 @@
1
- require 'net/http'
2
- require 'json'
3
- require_relative 'test_helper'
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: 'fake_service',
14
- metric_prefix: 'FakeService',
13
+ secret_config_prefix: "fake_service",
14
+ metric_prefix: "FakeService",
15
15
  logger: SemanticLogger["FakeService"],
16
16
  error_class: ServiceError,
17
- header: {'Content-Type' => 'application/json'}
17
+ header: {"Content-Type" => "application/json"}
18
18
  )
19
19
  end
20
20
 
21
21
  describe "get" do
22
- it 'success' do
23
- output = {zip: '12345', population: 54321}
22
+ it "succeeds" do
23
+ output = {zip: "12345", population: 54_321}
24
24
  body = output.to_json
25
- response = Net::HTTPSuccess.new(200, 'OK', body)
26
- http.driver.stub(:request, response) do
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
@@ -1,14 +1,14 @@
1
- $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
- ENV['TZ'] = 'America/New_York'
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + "/../lib"
2
+ ENV["TZ"] = "America/New_York"
3
3
 
4
- require 'yaml'
5
- require 'minitest/autorun'
6
- require 'awesome_print'
7
- require 'secret_config'
8
- require 'semantic_logger'
9
- require 'opinionated_http'
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: 'test.log', formatter: :color)
11
+ SemanticLogger.add_appender(file_name: "test.log", formatter: :color)
12
12
  SemanticLogger.default_level = :debug
13
13
 
14
- SecretConfig.use :file, path: 'test', file_name: File.expand_path('config/application.yml', __dir__)
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.1
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-02-20 00:00:00.000000000 Z
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.6
90
+ rubygems_version: 3.0.8
91
91
  signing_key:
92
92
  specification_version: 4
93
93
  summary: Opinionated HTTP Client