opinionated_http 0.0.1 → 0.0.6

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: 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