faraday 2.7.10 → 2.14.1

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -1
  3. data/README.md +22 -11
  4. data/Rakefile +3 -0
  5. data/lib/faraday/adapter/test.rb +1 -1
  6. data/lib/faraday/adapter.rb +1 -1
  7. data/lib/faraday/connection.rb +17 -7
  8. data/lib/faraday/encoders/flat_params_encoder.rb +2 -2
  9. data/lib/faraday/encoders/nested_params_encoder.rb +1 -1
  10. data/lib/faraday/error.rb +50 -5
  11. data/lib/faraday/logging/formatter.rb +8 -8
  12. data/lib/faraday/middleware.rb +40 -1
  13. data/lib/faraday/options/env.rb +2 -2
  14. data/lib/faraday/options/proxy_options.rb +4 -2
  15. data/lib/faraday/options/ssl_options.rb +8 -2
  16. data/lib/faraday/options.rb +3 -3
  17. data/lib/faraday/rack_builder.rb +21 -25
  18. data/lib/faraday/request/json.rb +8 -2
  19. data/lib/faraday/response/json.rb +21 -1
  20. data/lib/faraday/response/logger.rb +5 -3
  21. data/lib/faraday/response/raise_error.rb +36 -19
  22. data/lib/faraday/response.rb +7 -3
  23. data/lib/faraday/utils/headers.rb +8 -2
  24. data/lib/faraday/utils.rb +3 -4
  25. data/lib/faraday/version.rb +1 -1
  26. data/lib/faraday.rb +2 -1
  27. data/spec/faraday/connection_spec.rb +35 -2
  28. data/spec/faraday/error_spec.rb +93 -3
  29. data/spec/faraday/middleware_spec.rb +143 -0
  30. data/spec/faraday/options/proxy_options_spec.rb +27 -0
  31. data/spec/faraday/params_encoders/nested_spec.rb +2 -1
  32. data/spec/faraday/request/json_spec.rb +64 -0
  33. data/spec/faraday/response/json_spec.rb +89 -0
  34. data/spec/faraday/response/logger_spec.rb +45 -4
  35. data/spec/faraday/response/raise_error_spec.rb +115 -13
  36. data/spec/faraday/response_spec.rb +7 -0
  37. data/spec/faraday/utils/headers_spec.rb +9 -0
  38. data/spec/faraday/utils_spec.rb +3 -1
  39. data/spec/faraday_spec.rb +10 -4
  40. data/spec/spec_helper.rb +6 -5
  41. data/spec/support/faraday_middleware_subclasses.rb +18 -0
  42. metadata +26 -14
@@ -10,11 +10,13 @@ module Faraday
10
10
  # lifecycle to a given Logger object. By default, this logs to STDOUT. See
11
11
  # Faraday::Logging::Formatter to see specifically what is logged.
12
12
  class Logger < Middleware
13
+ DEFAULT_OPTIONS = { formatter: Logging::Formatter }.merge(Logging::Formatter::DEFAULT_OPTIONS).freeze
14
+
13
15
  def initialize(app, logger = nil, options = {})
14
- super(app)
16
+ super(app, options)
15
17
  logger ||= ::Logger.new($stdout)
16
- formatter_class = options.delete(:formatter) || Logging::Formatter
17
- @formatter = formatter_class.new(logger: logger, options: options)
18
+ formatter_class = @options.delete(:formatter)
19
+ @formatter = formatter_class.new(logger: logger, options: @options)
18
20
  yield @formatter if block_given?
19
21
  end
20
22
 
@@ -6,30 +6,32 @@ module Faraday
6
6
  # client or server error responses.
7
7
  class RaiseError < Middleware
8
8
  # rubocop:disable Naming/ConstantName
9
- ClientErrorStatuses = (400...500).freeze
10
- ServerErrorStatuses = (500...600).freeze
9
+ ClientErrorStatuses = (400...500)
10
+ ServerErrorStatuses = (500...600)
11
+ ClientErrorStatusesWithCustomExceptions = {
12
+ 400 => Faraday::BadRequestError,
13
+ 401 => Faraday::UnauthorizedError,
14
+ 403 => Faraday::ForbiddenError,
15
+ 404 => Faraday::ResourceNotFound,
16
+ 408 => Faraday::RequestTimeoutError,
17
+ 409 => Faraday::ConflictError,
18
+ 422 => Faraday::UnprocessableContentError,
19
+ 429 => Faraday::TooManyRequestsError
20
+ }.freeze
11
21
  # rubocop:enable Naming/ConstantName
12
22
 
23
+ DEFAULT_OPTIONS = { include_request: true, allowed_statuses: [] }.freeze
24
+
13
25
  def on_complete(env)
26
+ return if Array(options[:allowed_statuses]).include?(env[:status])
27
+
14
28
  case env[:status]
15
- when 400
16
- raise Faraday::BadRequestError, response_values(env)
17
- when 401
18
- raise Faraday::UnauthorizedError, response_values(env)
19
- when 403
20
- raise Faraday::ForbiddenError, response_values(env)
21
- when 404
22
- raise Faraday::ResourceNotFound, response_values(env)
29
+ when *ClientErrorStatusesWithCustomExceptions.keys
30
+ raise ClientErrorStatusesWithCustomExceptions[env[:status]], response_values(env)
23
31
  when 407
24
32
  # mimic the behavior that we get with proxy requests with HTTPS
25
33
  msg = %(407 "Proxy Authentication Required")
26
34
  raise Faraday::ProxyAuthError.new(msg, response_values(env))
27
- when 408
28
- raise Faraday::RequestTimeoutError, response_values(env)
29
- when 409
30
- raise Faraday::ConflictError, response_values(env)
31
- when 422
32
- raise Faraday::UnprocessableEntityError, response_values(env)
33
35
  when ClientErrorStatuses
34
36
  raise Faraday::ClientError, response_values(env)
35
37
  when ServerErrorStatuses
@@ -39,11 +41,26 @@ module Faraday
39
41
  end
40
42
  end
41
43
 
44
+ # Returns a hash of response data with the following keys:
45
+ # - status
46
+ # - headers
47
+ # - body
48
+ # - request
49
+ #
50
+ # The `request` key is omitted when the middleware is explicitly
51
+ # configured with the option `include_request: false`.
42
52
  def response_values(env)
43
- {
53
+ response = {
44
54
  status: env.status,
45
55
  headers: env.response_headers,
46
- body: env.body,
56
+ body: env.body
57
+ }
58
+
59
+ # Include the request data by default. If the middleware was explicitly
60
+ # configured to _not_ include request data, then omit it.
61
+ return response unless options[:include_request]
62
+
63
+ response.merge(
47
64
  request: {
48
65
  method: env.method,
49
66
  url: env.url,
@@ -52,7 +69,7 @@ module Faraday
52
69
  headers: env.request_headers,
53
70
  body: env.request_body
54
71
  }
55
- }
72
+ )
56
73
  end
57
74
 
58
75
  def query_params(env)
@@ -33,6 +33,10 @@ module Faraday
33
33
  finished? ? env.body : nil
34
34
  end
35
35
 
36
+ def url
37
+ finished? ? env.url : nil
38
+ end
39
+
36
40
  def finished?
37
41
  !!env
38
42
  end
@@ -60,9 +64,9 @@ module Faraday
60
64
 
61
65
  def to_hash
62
66
  {
63
- status: env.status, body: env.body,
64
- response_headers: env.response_headers,
65
- url: env.url
67
+ status: status, body: body,
68
+ response_headers: headers,
69
+ url: url
66
70
  }
67
71
  end
68
72
 
@@ -62,10 +62,10 @@ module Faraday
62
62
  super(key, val)
63
63
  end
64
64
 
65
- def fetch(key, *args, &block)
65
+ def fetch(key, ...)
66
66
  key = KeyMap[key]
67
67
  key = @names.fetch(key.downcase, key)
68
- super(key, *args, &block)
68
+ super(key, ...)
69
69
  end
70
70
 
71
71
  def delete(key)
@@ -77,6 +77,12 @@ module Faraday
77
77
  super(key)
78
78
  end
79
79
 
80
+ def dig(key, *rest)
81
+ key = KeyMap[key]
82
+ key = @names.fetch(key.downcase, key)
83
+ super(key, *rest)
84
+ end
85
+
80
86
  def include?(key)
81
87
  @names.include? key.downcase
82
88
  end
data/lib/faraday/utils.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'base64'
4
3
  require 'uri'
5
4
  require 'faraday/utils/headers'
6
5
  require 'faraday/utils/params_hash'
@@ -26,7 +25,7 @@ module Faraday
26
25
  attr_writer :default_space_encoding
27
26
  end
28
27
 
29
- ESCAPE_RE = /[^a-zA-Z0-9 .~_-]/.freeze
28
+ ESCAPE_RE = /[^a-zA-Z0-9 .~_-]/
30
29
 
31
30
  def escape(str)
32
31
  str.to_s.gsub(ESCAPE_RE) do |match|
@@ -38,7 +37,7 @@ module Faraday
38
37
  CGI.unescape str.to_s
39
38
  end
40
39
 
41
- DEFAULT_SEP = /[&;] */n.freeze
40
+ DEFAULT_SEP = /[&;] */n
42
41
 
43
42
  # Adapted from Rack
44
43
  def parse_query(query)
@@ -54,7 +53,7 @@ module Faraday
54
53
  end
55
54
 
56
55
  def basic_header_from(login, pass)
57
- value = Base64.encode64("#{login}:#{pass}")
56
+ value = ["#{login}:#{pass}"].pack('m') # Base64 encoding
58
57
  value.delete!("\n")
59
58
  "Basic #{value}"
60
59
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Faraday
4
- VERSION = '2.7.10'
4
+ VERSION = '2.14.1'
5
5
  end
data/lib/faraday.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'cgi'
3
+ require 'cgi/escape'
4
+ require 'cgi/util' if RUBY_VERSION < '3.5'
4
5
  require 'date'
5
6
  require 'set'
6
7
  require 'forwardable'
@@ -300,14 +300,47 @@ RSpec.describe Faraday::Connection do
300
300
  it 'joins url to base when used relative path' do
301
301
  conn = Faraday.new(url: url)
302
302
  uri = conn.build_exclusive_url('service:search?limit=400')
303
- expect(uri.to_s).to eq('http://service.com/service%3Asearch?limit=400')
303
+ expect(uri.to_s).to eq('http://service.com/service:search?limit=400')
304
304
  end
305
305
 
306
306
  it 'joins url to base when used with path prefix' do
307
307
  conn = Faraday.new(url: url)
308
308
  conn.path_prefix = '/api'
309
309
  uri = conn.build_exclusive_url('service:search?limit=400')
310
- expect(uri.to_s).to eq('http://service.com/api/service%3Asearch?limit=400')
310
+ expect(uri.to_s).to eq('http://service.com/api/service:search?limit=400')
311
+ end
312
+ end
313
+
314
+ context 'with protocol-relative URL (GHSA-33mh-2634-fwr2)' do
315
+ it 'does not allow host override with //evil.com/path' do
316
+ conn.url_prefix = 'http://httpbingo.org/api'
317
+ uri = conn.build_exclusive_url('//evil.com/path')
318
+ expect(uri.host).to eq('httpbingo.org')
319
+ end
320
+
321
+ it 'does not allow host override with //evil.com:8080/path' do
322
+ conn.url_prefix = 'http://httpbingo.org/api'
323
+ uri = conn.build_exclusive_url('//evil.com:8080/path')
324
+ expect(uri.host).to eq('httpbingo.org')
325
+ end
326
+
327
+ it 'does not allow host override with //user:pass@evil.com/path' do
328
+ conn.url_prefix = 'http://httpbingo.org/api'
329
+ uri = conn.build_exclusive_url('//user:pass@evil.com/path')
330
+ expect(uri.host).to eq('httpbingo.org')
331
+ end
332
+
333
+ it 'does not allow host override with ///evil.com' do
334
+ conn.url_prefix = 'http://httpbingo.org/api'
335
+ uri = conn.build_exclusive_url('///evil.com')
336
+ expect(uri.host).to eq('httpbingo.org')
337
+ end
338
+
339
+ it 'still allows single-slash absolute paths' do
340
+ conn.url_prefix = 'http://httpbingo.org/api'
341
+ uri = conn.build_exclusive_url('/safe/path')
342
+ expect(uri.host).to eq('httpbingo.org')
343
+ expect(uri.path).to eq('/safe/path')
311
344
  end
312
345
  end
313
346
 
@@ -23,8 +23,12 @@ RSpec.describe Faraday::Error do
23
23
 
24
24
  it { expect(subject.wrapped_exception).to be_nil }
25
25
  it { expect(subject.response).to eq(exception) }
26
- it { expect(subject.message).to eq('the server responded with status 400') }
27
- it { expect(subject.inspect).to eq('#<Faraday::Error response={:status=>400}>') }
26
+ it { expect(subject.message).to eq('the server responded with status 400 - method and url are not available due to include_request: false on Faraday::Response::RaiseError middleware') }
27
+ if RUBY_VERSION >= '3.4'
28
+ it { expect(subject.inspect).to eq('#<Faraday::Error response={status: 400}>') }
29
+ else
30
+ it { expect(subject.inspect).to eq('#<Faraday::Error response={:status=>400}>') }
31
+ end
28
32
  it { expect(subject.response_status).to eq(400) }
29
33
  it { expect(subject.response_headers).to be_nil }
30
34
  it { expect(subject.response_body).to be_nil }
@@ -61,7 +65,11 @@ RSpec.describe Faraday::Error do
61
65
  it { expect(subject.wrapped_exception).to be_nil }
62
66
  it { expect(subject.response).to eq(response) }
63
67
  it { expect(subject.message).to eq('custom message') }
64
- it { expect(subject.inspect).to eq('#<Faraday::Error response={:status=>400}>') }
68
+ if RUBY_VERSION >= '3.4'
69
+ it { expect(subject.inspect).to eq('#<Faraday::Error response={status: 400}>') }
70
+ else
71
+ it { expect(subject.inspect).to eq('#<Faraday::Error response={:status=>400}>') }
72
+ end
65
73
  it { expect(subject.response_status).to eq(400) }
66
74
  it { expect(subject.response_headers).to be_nil }
67
75
  it { expect(subject.response_body).to be_nil }
@@ -81,5 +89,87 @@ RSpec.describe Faraday::Error do
81
89
  it { expect(subject.response_headers).to eq(headers) }
82
90
  it { expect(subject.response_body).to eq(body) }
83
91
  end
92
+
93
+ context 'with hash missing status key' do
94
+ let(:exception) { { body: 'error body' } }
95
+
96
+ it { expect(subject.wrapped_exception).to be_nil }
97
+ it { expect(subject.response).to eq(exception) }
98
+ it { expect(subject.message).to eq('the server responded with status - method and url are not available due to include_request: false on Faraday::Response::RaiseError middleware') }
99
+ end
100
+
101
+ context 'with hash with status but missing request data' do
102
+ let(:exception) { { status: 404, body: 'not found' } } # missing request key
103
+
104
+ it { expect(subject.wrapped_exception).to be_nil }
105
+ it { expect(subject.response).to eq(exception) }
106
+ it { expect(subject.message).to eq('the server responded with status 404 - method and url are not available due to include_request: false on Faraday::Response::RaiseError middleware') }
107
+ end
108
+
109
+ context 'with hash with status and request but missing method in request' do
110
+ let(:exception) { { status: 404, body: 'not found', request: { url: 'http://example.com/test' } } } # missing method
111
+
112
+ it { expect(subject.wrapped_exception).to be_nil }
113
+ it { expect(subject.response).to eq(exception) }
114
+ it { expect(subject.message).to eq('the server responded with status 404 for http://example.com/test') }
115
+ end
116
+
117
+ context 'with hash with status and request but missing url in request' do
118
+ let(:exception) { { status: 404, body: 'not found', request: { method: :get } } } # missing url
119
+
120
+ it { expect(subject.wrapped_exception).to be_nil }
121
+ it { expect(subject.response).to eq(exception) }
122
+ it { expect(subject.message).to eq('the server responded with status 404 for GET ') }
123
+ end
124
+
125
+ context 'with properly formed Faraday::Env' do
126
+ # This represents the normal case - a well-formed Faraday::Env object
127
+ # with all the standard properties populated as they would be during
128
+ # a typical HTTP request/response cycle
129
+ let(:exception) { Faraday::Env.new }
130
+
131
+ before do
132
+ exception.status = 500
133
+ exception.method = :post
134
+ exception.url = URI('https://api.example.com/users')
135
+ exception.request = Faraday::RequestOptions.new
136
+ exception.response_headers = { 'content-type' => 'application/json' }
137
+ exception.response_body = '{"error": "Internal server error"}'
138
+ exception.request_headers = { 'authorization' => 'Bearer token123' }
139
+ exception.request_body = '{"name": "John"}'
140
+ end
141
+
142
+ it { expect(subject.wrapped_exception).to be_nil }
143
+ it { expect(subject.response).to eq(exception) }
144
+ it { expect(subject.message).to eq('the server responded with status 500 for POST https://api.example.com/users') }
145
+ end
146
+
147
+ context 'with Faraday::Env missing status key' do
148
+ let(:exception) { Faraday::Env.new }
149
+
150
+ before do
151
+ exception[:body] = 'error body'
152
+ # Intentionally not setting status
153
+ end
154
+
155
+ it { expect(subject.wrapped_exception).to be_nil }
156
+ it { expect(subject.response).to eq(exception) }
157
+ it { expect(subject.message).to eq('the server responded with status for ') }
158
+ end
159
+
160
+ context 'with Faraday::Env with direct method and url properties' do
161
+ let(:exception) { Faraday::Env.new }
162
+
163
+ before do
164
+ exception.status = 404
165
+ exception.method = :get
166
+ exception.url = URI('http://example.com/test')
167
+ exception[:body] = 'not found'
168
+ end
169
+
170
+ it { expect(subject.wrapped_exception).to be_nil }
171
+ it { expect(subject.response).to eq(exception) }
172
+ it { expect(subject.message).to eq('the server responded with status 404 for GET http://example.com/test') }
173
+ end
84
174
  end
85
175
  end
@@ -67,4 +67,147 @@ RSpec.describe Faraday::Middleware do
67
67
  end
68
68
  end
69
69
  end
70
+
71
+ describe '::default_options' do
72
+ let(:subclass_no_options) { FaradayMiddlewareSubclasses::SubclassNoOptions }
73
+ let(:subclass_one_option) { FaradayMiddlewareSubclasses::SubclassOneOption }
74
+ let(:subclass_two_options) { FaradayMiddlewareSubclasses::SubclassTwoOptions }
75
+
76
+ def build_conn(resp_middleware)
77
+ Faraday.new do |c|
78
+ c.adapter :test do |stub|
79
+ stub.get('/success') { [200, {}, 'ok'] }
80
+ end
81
+ c.response resp_middleware
82
+ end
83
+ end
84
+
85
+ RSpec.shared_context 'reset @default_options' do
86
+ before(:each) do
87
+ FaradayMiddlewareSubclasses::SubclassNoOptions.instance_variable_set(:@default_options, nil)
88
+ FaradayMiddlewareSubclasses::SubclassOneOption.instance_variable_set(:@default_options, nil)
89
+ FaradayMiddlewareSubclasses::SubclassTwoOptions.instance_variable_set(:@default_options, nil)
90
+ Faraday::Middleware.instance_variable_set(:@default_options, nil)
91
+ end
92
+ end
93
+
94
+ after(:all) do
95
+ FaradayMiddlewareSubclasses::SubclassNoOptions.instance_variable_set(:@default_options, nil)
96
+ FaradayMiddlewareSubclasses::SubclassOneOption.instance_variable_set(:@default_options, nil)
97
+ FaradayMiddlewareSubclasses::SubclassTwoOptions.instance_variable_set(:@default_options, nil)
98
+ Faraday::Middleware.instance_variable_set(:@default_options, nil)
99
+ end
100
+
101
+ context 'with subclass DEFAULT_OPTIONS defined' do
102
+ include_context 'reset @default_options'
103
+
104
+ context 'and without application options configured' do
105
+ let(:resp1) { build_conn(:one_option).get('/success') }
106
+
107
+ it 'has only subclass defaults' do
108
+ expect(Faraday::Middleware.default_options).to eq(Faraday::Middleware::DEFAULT_OPTIONS)
109
+ expect(subclass_no_options.default_options).to eq(subclass_no_options::DEFAULT_OPTIONS)
110
+ expect(subclass_one_option.default_options).to eq(subclass_one_option::DEFAULT_OPTIONS)
111
+ expect(subclass_two_options.default_options).to eq(subclass_two_options::DEFAULT_OPTIONS)
112
+ end
113
+
114
+ it { expect(resp1.body).to eq('ok') }
115
+ end
116
+
117
+ context "and with one application's options changed" do
118
+ let(:resp2) { build_conn(:two_options).get('/success') }
119
+
120
+ before(:each) do
121
+ FaradayMiddlewareSubclasses::SubclassTwoOptions.default_options = { some_option: false }
122
+ end
123
+
124
+ it 'only updates default options of target subclass' do
125
+ expect(Faraday::Middleware.default_options).to eq(Faraday::Middleware::DEFAULT_OPTIONS)
126
+ expect(subclass_no_options.default_options).to eq(subclass_no_options::DEFAULT_OPTIONS)
127
+ expect(subclass_one_option.default_options).to eq(subclass_one_option::DEFAULT_OPTIONS)
128
+ expect(subclass_two_options.default_options).to eq({ some_option: false, some_other_option: false })
129
+ end
130
+
131
+ it { expect(resp2.body).to eq('ok') }
132
+ end
133
+
134
+ context "and with two applications' options changed" do
135
+ let(:resp1) { build_conn(:one_option).get('/success') }
136
+ let(:resp2) { build_conn(:two_options).get('/success') }
137
+
138
+ before(:each) do
139
+ FaradayMiddlewareSubclasses::SubclassOneOption.default_options = { some_other_option: true }
140
+ FaradayMiddlewareSubclasses::SubclassTwoOptions.default_options = { some_option: false }
141
+ end
142
+
143
+ it 'updates subclasses and parent independent of each other' do
144
+ expect(Faraday::Middleware.default_options).to eq(Faraday::Middleware::DEFAULT_OPTIONS)
145
+ expect(subclass_no_options.default_options).to eq(subclass_no_options::DEFAULT_OPTIONS)
146
+ expect(subclass_one_option.default_options).to eq({ some_other_option: true })
147
+ expect(subclass_two_options.default_options).to eq({ some_option: false, some_other_option: false })
148
+ end
149
+
150
+ it { expect(resp1.body).to eq('ok') }
151
+ it { expect(resp2.body).to eq('ok') }
152
+ end
153
+ end
154
+
155
+ context 'with FARADAY::MIDDLEWARE DEFAULT_OPTIONS and with Subclass DEFAULT_OPTIONS' do
156
+ before(:each) do
157
+ stub_const('Faraday::Middleware::DEFAULT_OPTIONS', { its_magic: false })
158
+ end
159
+
160
+ # Must stub Faraday::Middleware::DEFAULT_OPTIONS before resetting default options
161
+ include_context 'reset @default_options'
162
+
163
+ context 'and without application options configured' do
164
+ let(:resp1) { build_conn(:one_option).get('/success') }
165
+
166
+ it 'has only subclass defaults' do
167
+ expect(Faraday::Middleware.default_options).to eq(Faraday::Middleware::DEFAULT_OPTIONS)
168
+ expect(FaradayMiddlewareSubclasses::SubclassNoOptions.default_options).to eq({ its_magic: false })
169
+ expect(FaradayMiddlewareSubclasses::SubclassOneOption.default_options).to eq({ its_magic: false, some_other_option: false })
170
+ expect(FaradayMiddlewareSubclasses::SubclassTwoOptions.default_options).to eq({ its_magic: false, some_option: true, some_other_option: false })
171
+ end
172
+
173
+ it { expect(resp1.body).to eq('ok') }
174
+ end
175
+
176
+ context "and with two applications' options changed" do
177
+ let(:resp1) { build_conn(:one_option).get('/success') }
178
+ let(:resp2) { build_conn(:two_options).get('/success') }
179
+
180
+ before(:each) do
181
+ FaradayMiddlewareSubclasses::SubclassOneOption.default_options = { some_other_option: true }
182
+ FaradayMiddlewareSubclasses::SubclassTwoOptions.default_options = { some_option: false }
183
+ end
184
+
185
+ it 'updates subclasses and parent independent of each other' do
186
+ expect(Faraday::Middleware.default_options).to eq(Faraday::Middleware::DEFAULT_OPTIONS)
187
+ expect(FaradayMiddlewareSubclasses::SubclassNoOptions.default_options).to eq({ its_magic: false })
188
+ expect(FaradayMiddlewareSubclasses::SubclassOneOption.default_options).to eq({ its_magic: false, some_other_option: true })
189
+ expect(FaradayMiddlewareSubclasses::SubclassTwoOptions.default_options).to eq({ its_magic: false, some_option: false, some_other_option: false })
190
+ end
191
+
192
+ it { expect(resp1.body).to eq('ok') }
193
+ it { expect(resp2.body).to eq('ok') }
194
+ end
195
+ end
196
+
197
+ describe 'default_options input validation' do
198
+ include_context 'reset @default_options'
199
+
200
+ it 'raises error if Faraday::Middleware option does not exist' do
201
+ expect { Faraday::Middleware.default_options = { something_special: true } }.to raise_error(Faraday::InitializationError) do |e|
202
+ expect(e.message).to eq('Invalid options provided. Keys not found in Faraday::Middleware::DEFAULT_OPTIONS: something_special')
203
+ end
204
+ end
205
+
206
+ it 'raises error if subclass option does not exist' do
207
+ expect { subclass_one_option.default_options = { this_is_a_typo: true } }.to raise_error(Faraday::InitializationError) do |e|
208
+ expect(e.message).to eq('Invalid options provided. Keys not found in FaradayMiddlewareSubclasses::SubclassOneOption::DEFAULT_OPTIONS: this_is_a_typo')
209
+ end
210
+ end
211
+ end
212
+ end
70
213
  end
@@ -27,6 +27,33 @@ RSpec.describe Faraday::ProxyOptions do
27
27
  expect(options.inspect).to eq('#<Faraday::ProxyOptions (empty)>')
28
28
  end
29
29
 
30
+ it 'works with hash' do
31
+ hash = { user: 'user', password: 'pass', uri: 'http://@example.org' }
32
+ options = Faraday::ProxyOptions.from(hash)
33
+ expect(options.user).to eq('user')
34
+ expect(options.password).to eq('pass')
35
+ expect(options.uri).to be_a_kind_of(URI)
36
+ expect(options.path).to eq('')
37
+ expect(options.port).to eq(80)
38
+ expect(options.host).to eq('example.org')
39
+ expect(options.scheme).to eq('http')
40
+ expect(options.inspect).to match('#<Faraday::ProxyOptions uri=')
41
+ end
42
+
43
+ it 'works with option' do
44
+ opt_arg = { user: 'user', password: 'pass', uri: 'http://@example.org' }
45
+ option = Faraday::ConnectionOptions.from(proxy: opt_arg)
46
+ options = Faraday::ProxyOptions.from(option.proxy)
47
+ expect(options.user).to eq('user')
48
+ expect(options.password).to eq('pass')
49
+ expect(options.uri).to be_a_kind_of(URI)
50
+ expect(options.path).to eq('')
51
+ expect(options.port).to eq(80)
52
+ expect(options.host).to eq('example.org')
53
+ expect(options.scheme).to eq('http')
54
+ expect(options.inspect).to match('#<Faraday::ProxyOptions uri=')
55
+ end
56
+
30
57
  it 'works with no auth' do
31
58
  proxy = Faraday::ProxyOptions.from 'http://example.org'
32
59
  expect(proxy.user).to be_nil
@@ -62,7 +62,8 @@ RSpec.describe Faraday::NestedParamsEncoder do
62
62
  it 'encodes rack compat' do
63
63
  params = { a: [{ one: '1', two: '2' }, '3', ''] }
64
64
  result = Faraday::Utils.unescape(Faraday::NestedParamsEncoder.encode(params)).split('&')
65
- expected = Rack::Utils.build_nested_query(params).split('&')
65
+ escaped = Rack::Utils.build_nested_query(params)
66
+ expected = Rack::Utils.unescape(escaped).split('&')
66
67
  expect(result).to match_array(expected)
67
68
  end
68
69
 
@@ -132,4 +132,68 @@ RSpec.describe Faraday::Request::Json do
132
132
  expect(result_type).to eq('application/xml; charset=utf-8')
133
133
  end
134
134
  end
135
+
136
+ context 'with encoder' do
137
+ let(:encoder) do
138
+ double('Encoder').tap do |e|
139
+ allow(e).to receive(:dump) { |s, opts| JSON.generate(s, opts) }
140
+ end
141
+ end
142
+
143
+ let(:result) { process(a: 1) }
144
+
145
+ context 'when encoder is passed as object' do
146
+ let(:middleware) { described_class.new(->(env) { Faraday::Response.new(env) }, { encoder: encoder }) }
147
+
148
+ it 'calls specified JSON encoder\'s dump method' do
149
+ expect(encoder).to receive(:dump).with({ a: 1 })
150
+
151
+ result
152
+ end
153
+
154
+ it 'encodes body' do
155
+ expect(result_body).to eq('{"a":1}')
156
+ end
157
+
158
+ it 'adds content type' do
159
+ expect(result_type).to eq('application/json')
160
+ end
161
+ end
162
+
163
+ context 'when encoder is passed as an object-method pair' do
164
+ let(:middleware) { described_class.new(->(env) { Faraday::Response.new(env) }, { encoder: [encoder, :dump] }) }
165
+
166
+ it 'calls specified JSON encoder' do
167
+ expect(encoder).to receive(:dump).with({ a: 1 })
168
+
169
+ result
170
+ end
171
+
172
+ it 'encodes body' do
173
+ expect(result_body).to eq('{"a":1}')
174
+ end
175
+
176
+ it 'adds content type' do
177
+ expect(result_type).to eq('application/json')
178
+ end
179
+ end
180
+
181
+ context 'when encoder is not passed' do
182
+ let(:middleware) { described_class.new(->(env) { Faraday::Response.new(env) }) }
183
+
184
+ it 'calls JSON.generate' do
185
+ expect(JSON).to receive(:generate).with({ a: 1 })
186
+
187
+ result
188
+ end
189
+
190
+ it 'encodes body' do
191
+ expect(result_body).to eq('{"a":1}')
192
+ end
193
+
194
+ it 'adds content type' do
195
+ expect(result_type).to eq('application/json')
196
+ end
197
+ end
198
+ end
135
199
  end