faraday 0.15.4 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +380 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +16 -344
  5. data/Rakefile +7 -0
  6. data/examples/client_spec.rb +65 -0
  7. data/examples/client_test.rb +79 -0
  8. data/lib/faraday.rb +127 -190
  9. data/lib/faraday/adapter.rb +72 -22
  10. data/lib/faraday/adapter/test.rb +86 -53
  11. data/lib/faraday/adapter/typhoeus.rb +4 -1
  12. data/lib/faraday/adapter_registry.rb +30 -0
  13. data/lib/faraday/autoload.rb +39 -36
  14. data/lib/faraday/connection.rb +320 -183
  15. data/lib/faraday/dependency_loader.rb +37 -0
  16. data/lib/faraday/encoders/flat_params_encoder.rb +105 -0
  17. data/lib/faraday/encoders/nested_params_encoder.rb +176 -0
  18. data/lib/faraday/error.rb +123 -37
  19. data/lib/faraday/file_part.rb +128 -0
  20. data/lib/faraday/logging/formatter.rb +105 -0
  21. data/lib/faraday/methods.rb +6 -0
  22. data/lib/faraday/middleware.rb +19 -25
  23. data/lib/faraday/middleware_registry.rb +129 -0
  24. data/lib/faraday/options.rb +39 -194
  25. data/lib/faraday/options/connection_options.rb +22 -0
  26. data/lib/faraday/options/env.rb +181 -0
  27. data/lib/faraday/options/proxy_options.rb +32 -0
  28. data/lib/faraday/options/request_options.rb +22 -0
  29. data/lib/faraday/options/ssl_options.rb +59 -0
  30. data/lib/faraday/param_part.rb +53 -0
  31. data/lib/faraday/parameters.rb +4 -197
  32. data/lib/faraday/rack_builder.rb +77 -65
  33. data/lib/faraday/request.rb +86 -44
  34. data/lib/faraday/request/authorization.rb +44 -30
  35. data/lib/faraday/request/basic_authentication.rb +14 -7
  36. data/lib/faraday/request/instrumentation.rb +45 -27
  37. data/lib/faraday/request/multipart.rb +86 -48
  38. data/lib/faraday/request/retry.rb +198 -169
  39. data/lib/faraday/request/token_authentication.rb +15 -10
  40. data/lib/faraday/request/url_encoded.rb +43 -23
  41. data/lib/faraday/response.rb +27 -23
  42. data/lib/faraday/response/logger.rb +22 -69
  43. data/lib/faraday/response/raise_error.rb +49 -14
  44. data/lib/faraday/utils.rb +38 -247
  45. data/lib/faraday/utils/headers.rb +139 -0
  46. data/lib/faraday/utils/params_hash.rb +61 -0
  47. data/lib/faraday/version.rb +5 -0
  48. data/spec/external_adapters/faraday_specs_setup.rb +14 -0
  49. data/spec/faraday/adapter/em_http_spec.rb +49 -0
  50. data/spec/faraday/adapter/em_synchrony_spec.rb +18 -0
  51. data/spec/faraday/adapter/excon_spec.rb +49 -0
  52. data/spec/faraday/adapter/httpclient_spec.rb +73 -0
  53. data/spec/faraday/adapter/net_http_spec.rb +64 -0
  54. data/spec/faraday/adapter/patron_spec.rb +18 -0
  55. data/spec/faraday/adapter/rack_spec.rb +8 -0
  56. data/spec/faraday/adapter/test_spec.rb +260 -0
  57. data/spec/faraday/adapter/typhoeus_spec.rb +7 -0
  58. data/spec/faraday/adapter_registry_spec.rb +28 -0
  59. data/spec/faraday/adapter_spec.rb +55 -0
  60. data/spec/faraday/composite_read_io_spec.rb +80 -0
  61. data/spec/faraday/connection_spec.rb +736 -0
  62. data/spec/faraday/error_spec.rb +60 -0
  63. data/spec/faraday/middleware_spec.rb +52 -0
  64. data/spec/faraday/options/env_spec.rb +70 -0
  65. data/spec/faraday/options/options_spec.rb +297 -0
  66. data/spec/faraday/options/proxy_options_spec.rb +44 -0
  67. data/spec/faraday/options/request_options_spec.rb +19 -0
  68. data/spec/faraday/params_encoders/flat_spec.rb +42 -0
  69. data/spec/faraday/params_encoders/nested_spec.rb +142 -0
  70. data/spec/faraday/rack_builder_spec.rb +345 -0
  71. data/spec/faraday/request/authorization_spec.rb +88 -0
  72. data/spec/faraday/request/instrumentation_spec.rb +76 -0
  73. data/spec/faraday/request/multipart_spec.rb +302 -0
  74. data/spec/faraday/request/retry_spec.rb +242 -0
  75. data/spec/faraday/request/url_encoded_spec.rb +83 -0
  76. data/spec/faraday/request_spec.rb +120 -0
  77. data/spec/faraday/response/logger_spec.rb +220 -0
  78. data/spec/faraday/response/middleware_spec.rb +68 -0
  79. data/spec/faraday/response/raise_error_spec.rb +169 -0
  80. data/spec/faraday/response_spec.rb +75 -0
  81. data/spec/faraday/utils/headers_spec.rb +82 -0
  82. data/spec/faraday/utils_spec.rb +56 -0
  83. data/spec/faraday_spec.rb +37 -0
  84. data/spec/spec_helper.rb +132 -0
  85. data/spec/support/disabling_stub.rb +14 -0
  86. data/spec/support/fake_safe_buffer.rb +15 -0
  87. data/spec/support/helper_methods.rb +133 -0
  88. data/spec/support/shared_examples/adapter.rb +105 -0
  89. data/spec/support/shared_examples/params_encoder.rb +18 -0
  90. data/spec/support/shared_examples/request_method.rb +262 -0
  91. data/spec/support/streaming_response_checker.rb +35 -0
  92. data/spec/support/webmock_rack_app.rb +68 -0
  93. metadata +206 -19
  94. data/lib/faraday/adapter/em_http.rb +0 -243
  95. data/lib/faraday/adapter/em_http_ssl_patch.rb +0 -56
  96. data/lib/faraday/adapter/em_synchrony.rb +0 -106
  97. data/lib/faraday/adapter/em_synchrony/parallel_manager.rb +0 -66
  98. data/lib/faraday/adapter/excon.rb +0 -82
  99. data/lib/faraday/adapter/httpclient.rb +0 -128
  100. data/lib/faraday/adapter/net_http.rb +0 -152
  101. data/lib/faraday/adapter/net_http_persistent.rb +0 -68
  102. data/lib/faraday/adapter/patron.rb +0 -95
  103. data/lib/faraday/adapter/rack.rb +0 -58
  104. data/lib/faraday/upload_io.rb +0 -67
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Faraday::Request::Retry do
4
+ let(:calls) { [] }
5
+ let(:times_called) { calls.size }
6
+ let(:options) { [] }
7
+ let(:conn) do
8
+ Faraday.new do |b|
9
+ b.request :retry, *options
10
+
11
+ b.adapter :test do |stub|
12
+ %w[get post].each do |method|
13
+ stub.send(method, '/unstable') do |env|
14
+ calls << env.dup
15
+ env[:body] = nil # simulate blanking out response body
16
+ callback.call
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ context 'when an unexpected error happens' do
24
+ let(:callback) { -> { raise 'boom!' } }
25
+
26
+ before { expect { conn.get('/unstable') }.to raise_error(RuntimeError) }
27
+
28
+ it { expect(times_called).to eq(1) }
29
+
30
+ context 'and this is passed as a custom exception' do
31
+ let(:options) { [{ exceptions: StandardError }] }
32
+
33
+ it { expect(times_called).to eq(3) }
34
+ end
35
+ end
36
+
37
+ context 'when an expected error happens' do
38
+ let(:callback) { -> { raise Errno::ETIMEDOUT } }
39
+
40
+ before do
41
+ @started = Time.now
42
+ expect { conn.get('/unstable') }.to raise_error(Errno::ETIMEDOUT)
43
+ end
44
+
45
+ it { expect(times_called).to eq(3) }
46
+
47
+ context 'and legacy max_retry set to 1' do
48
+ let(:options) { [1] }
49
+
50
+ it { expect(times_called).to eq(2) }
51
+ end
52
+
53
+ context 'and legacy max_retry set to -9' do
54
+ let(:options) { [-9] }
55
+
56
+ it { expect(times_called).to eq(1) }
57
+ end
58
+
59
+ context 'and new max_retry set to 3' do
60
+ let(:options) { [{ max: 3 }] }
61
+
62
+ it { expect(times_called).to eq(4) }
63
+ end
64
+
65
+ context 'and new max_retry set to -9' do
66
+ let(:options) { [{ max: -9 }] }
67
+
68
+ it { expect(times_called).to eq(1) }
69
+ end
70
+
71
+ context 'and both max_retry and interval are set' do
72
+ let(:options) { [{ max: 2, interval: 0.1 }] }
73
+
74
+ it { expect(Time.now - @started).to be_within(0.04).of(0.2) }
75
+ end
76
+ end
77
+
78
+ context 'when no exception raised' do
79
+ let(:options) { [{ max: 1, retry_statuses: 429 }] }
80
+
81
+ before { conn.get('/unstable') }
82
+
83
+ context 'and response code is in retry_statuses' do
84
+ let(:callback) { -> { [429, {}, ''] } }
85
+
86
+ it { expect(times_called).to eq(2) }
87
+ end
88
+
89
+ context 'and response code is not in retry_statuses' do
90
+ let(:callback) { -> { [503, {}, ''] } }
91
+
92
+ it { expect(times_called).to eq(1) }
93
+ end
94
+ end
95
+
96
+ describe '#calculate_retry_interval' do
97
+ context 'with exponential backoff' do
98
+ let(:options) { { max: 5, interval: 0.1, backoff_factor: 2 } }
99
+ let(:middleware) { Faraday::Request::Retry.new(nil, options) }
100
+
101
+ it { expect(middleware.send(:calculate_retry_interval, 5)).to eq(0.1) }
102
+ it { expect(middleware.send(:calculate_retry_interval, 4)).to eq(0.2) }
103
+ it { expect(middleware.send(:calculate_retry_interval, 3)).to eq(0.4) }
104
+ end
105
+
106
+ context 'with exponential backoff and max_interval' do
107
+ let(:options) { { max: 5, interval: 0.1, backoff_factor: 2, max_interval: 0.3 } }
108
+ let(:middleware) { Faraday::Request::Retry.new(nil, options) }
109
+
110
+ it { expect(middleware.send(:calculate_retry_interval, 5)).to eq(0.1) }
111
+ it { expect(middleware.send(:calculate_retry_interval, 4)).to eq(0.2) }
112
+ it { expect(middleware.send(:calculate_retry_interval, 3)).to eq(0.3) }
113
+ it { expect(middleware.send(:calculate_retry_interval, 2)).to eq(0.3) }
114
+ end
115
+
116
+ context 'with exponential backoff and interval_randomness' do
117
+ let(:options) { { max: 2, interval: 0.1, interval_randomness: 0.05 } }
118
+ let(:middleware) { Faraday::Request::Retry.new(nil, options) }
119
+
120
+ it { expect(middleware.send(:calculate_retry_interval, 2)).to be_between(0.1, 0.105) }
121
+ end
122
+ end
123
+
124
+ context 'when method is not idempotent' do
125
+ let(:callback) { -> { raise Errno::ETIMEDOUT } }
126
+
127
+ before { expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT) }
128
+
129
+ it { expect(times_called).to eq(1) }
130
+ end
131
+
132
+ describe 'retry_if option' do
133
+ let(:callback) { -> { raise Errno::ETIMEDOUT } }
134
+ let(:options) { [{ retry_if: @check }] }
135
+
136
+ it 'retries if retry_if block always returns true' do
137
+ body = { foo: :bar }
138
+ @check = ->(_, _) { true }
139
+ expect { conn.post('/unstable', body) }.to raise_error(Errno::ETIMEDOUT)
140
+ expect(times_called).to eq(3)
141
+ expect(calls.all? { |env| env[:body] == body }).to be_truthy
142
+ end
143
+
144
+ it 'does not retry if retry_if block returns false checking env' do
145
+ @check = ->(env, _) { env[:method] != :post }
146
+ expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT)
147
+ expect(times_called).to eq(1)
148
+ end
149
+
150
+ it 'does not retry if retry_if block returns false checking exception' do
151
+ @check = ->(_, exception) { !exception.is_a?(Errno::ETIMEDOUT) }
152
+ expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT)
153
+ expect(times_called).to eq(1)
154
+ end
155
+
156
+ it 'FilePart: should rewind files on retry' do
157
+ io = StringIO.new('Test data')
158
+ filepart = Faraday::FilePart.new(io, 'application/octet/stream')
159
+
160
+ rewound = 0
161
+ rewind = -> { rewound += 1 }
162
+
163
+ @check = ->(_, _) { true }
164
+ allow(filepart).to receive(:rewind, &rewind)
165
+ expect { conn.post('/unstable', file: filepart) }.to raise_error(Errno::ETIMEDOUT)
166
+ expect(times_called).to eq(3)
167
+ expect(rewound).to eq(2)
168
+ end
169
+
170
+ it 'UploadIO: should rewind files on retry' do
171
+ io = StringIO.new('Test data')
172
+ upload_io = Faraday::UploadIO.new(io, 'application/octet/stream')
173
+
174
+ rewound = 0
175
+ rewind = -> { rewound += 1 }
176
+
177
+ @check = ->(_, _) { true }
178
+ allow(upload_io).to receive(:rewind, &rewind)
179
+ expect { conn.post('/unstable', file: upload_io) }.to raise_error(Errno::ETIMEDOUT)
180
+ expect(times_called).to eq(3)
181
+ expect(rewound).to eq(2)
182
+ end
183
+
184
+ context 'when explicitly specifying methods to retry' do
185
+ let(:options) { [{ retry_if: @check, methods: [:post] }] }
186
+
187
+ it 'does not call retry_if for specified methods' do
188
+ @check = ->(_, _) { raise 'this should have never been called' }
189
+ expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT)
190
+ expect(times_called).to eq(3)
191
+ end
192
+ end
193
+
194
+ context 'with empty list of methods to retry' do
195
+ let(:options) { [{ retry_if: @check, methods: [] }] }
196
+
197
+ it 'calls retry_if for all methods' do
198
+ @check = ->(_, _) { calls.size < 2 }
199
+ expect { conn.get('/unstable') }.to raise_error(Errno::ETIMEDOUT)
200
+ expect(times_called).to eq(2)
201
+ end
202
+ end
203
+ end
204
+
205
+ describe 'retry_after header support' do
206
+ let(:callback) { -> { [504, headers, ''] } }
207
+ let(:elapsed) { Time.now - @started }
208
+
209
+ before do
210
+ @started = Time.now
211
+ conn.get('/unstable')
212
+ end
213
+
214
+ context 'when retry_after bigger than interval' do
215
+ let(:headers) { { 'Retry-After' => '0.5' } }
216
+ let(:options) { [{ max: 1, interval: 0.1, retry_statuses: 504 }] }
217
+
218
+ it { expect(elapsed).to be > 0.5 }
219
+ end
220
+
221
+ context 'when retry_after smaller than interval' do
222
+ let(:headers) { { 'Retry-After' => '0.1' } }
223
+ let(:options) { [{ max: 1, interval: 0.2, retry_statuses: 504 }] }
224
+
225
+ it { expect(elapsed).to be > 0.2 }
226
+ end
227
+
228
+ context 'when retry_after is a timestamp' do
229
+ let(:headers) { { 'Retry-After' => (Time.now.utc + 2).strftime('%a, %d %b %Y %H:%M:%S GMT') } }
230
+ let(:options) { [{ max: 1, interval: 0.1, retry_statuses: 504 }] }
231
+
232
+ it { expect(elapsed).to be > 1 }
233
+ end
234
+
235
+ context 'when retry_after is bigger than max_interval' do
236
+ let(:headers) { { 'Retry-After' => (Time.now.utc + 20).strftime('%a, %d %b %Y %H:%M:%S GMT') } }
237
+ let(:options) { [{ max: 2, interval: 0.1, max_interval: 5, retry_statuses: 504 }] }
238
+
239
+ it { expect(times_called).to eq(1) }
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Faraday::Request::UrlEncoded do
4
+ let(:conn) do
5
+ Faraday.new do |b|
6
+ b.request :multipart
7
+ b.request :url_encoded
8
+ b.adapter :test do |stub|
9
+ stub.post('/echo') do |env|
10
+ posted_as = env[:request_headers]['Content-Type']
11
+ [200, { 'Content-Type' => posted_as }, env[:body]]
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ it 'does nothing without payload' do
18
+ response = conn.post('/echo')
19
+ expect(response.headers['Content-Type']).to be_nil
20
+ expect(response.body.empty?).to be_truthy
21
+ end
22
+
23
+ it 'ignores custom content type' do
24
+ response = conn.post('/echo', { some: 'data' }, 'content-type' => 'application/x-foo')
25
+ expect(response.headers['Content-Type']).to eq('application/x-foo')
26
+ expect(response.body).to eq(some: 'data')
27
+ end
28
+
29
+ it 'works with no headers' do
30
+ response = conn.post('/echo', fruit: %w[apples oranges])
31
+ expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded')
32
+ expect(response.body).to eq('fruit%5B%5D=apples&fruit%5B%5D=oranges')
33
+ end
34
+
35
+ it 'works with with headers' do
36
+ response = conn.post('/echo', { 'a' => 123 }, 'content-type' => 'application/x-www-form-urlencoded')
37
+ expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded')
38
+ expect(response.body).to eq('a=123')
39
+ end
40
+
41
+ it 'works with nested params' do
42
+ response = conn.post('/echo', user: { name: 'Mislav', web: 'mislav.net' })
43
+ expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded')
44
+ expected = { 'user' => { 'name' => 'Mislav', 'web' => 'mislav.net' } }
45
+ expect(Faraday::Utils.parse_nested_query(response.body)).to eq(expected)
46
+ end
47
+
48
+ it 'works with non nested params' do
49
+ response = conn.post('/echo', dimensions: %w[date location]) do |req|
50
+ req.options.params_encoder = Faraday::FlatParamsEncoder
51
+ end
52
+ expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded')
53
+ expected = { 'dimensions' => %w[date location] }
54
+ expect(Faraday::Utils.parse_query(response.body)).to eq(expected)
55
+ expect(response.body).to eq('dimensions=date&dimensions=location')
56
+ end
57
+
58
+ it 'works with unicode' do
59
+ err = capture_warnings do
60
+ response = conn.post('/echo', str: 'eé cç aã aâ')
61
+ expect(response.body).to eq('str=e%C3%A9+c%C3%A7+a%C3%A3+a%C3%A2')
62
+ end
63
+ expect(err.empty?).to be_truthy
64
+ end
65
+
66
+ it 'works with nested keys' do
67
+ response = conn.post('/echo', 'a' => { 'b' => { 'c' => ['d'] } })
68
+ expect(response.body).to eq('a%5Bb%5D%5Bc%5D%5B%5D=d')
69
+ end
70
+
71
+ context 'customising default_space_encoding' do
72
+ around do |example|
73
+ Faraday::Utils.default_space_encoding = '%20'
74
+ example.run
75
+ Faraday::Utils.default_space_encoding = nil
76
+ end
77
+
78
+ it 'uses the custom character to encode spaces' do
79
+ response = conn.post('/echo', str: 'apple banana')
80
+ expect(response.body).to eq('str=apple%20banana')
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Faraday::Request do
4
+ let(:conn) do
5
+ Faraday.new(url: 'http://sushi.com/api',
6
+ headers: { 'Mime-Version' => '1.0' },
7
+ request: { oauth: { consumer_key: 'anonymous' } })
8
+ end
9
+ let(:http_method) { :get }
10
+ let(:block) { nil }
11
+
12
+ subject { conn.build_request(http_method, &block) }
13
+
14
+ context 'when nothing particular is configured' do
15
+ it { expect(subject.http_method).to eq(:get) }
16
+ it { expect(subject.to_env(conn).ssl.verify).to be_falsey }
17
+ end
18
+
19
+ context 'when HTTP method is post' do
20
+ let(:http_method) { :post }
21
+
22
+ it { expect(subject.http_method).to eq(:post) }
23
+ end
24
+
25
+ describe 'deprecate method for HTTP method' do
26
+ let(:http_method) { :post }
27
+ let(:expected_warning) do
28
+ %r{WARNING: `Faraday::Request#method` is deprecated; use `#http_method` instead. It will be removed in or after version 2.0.\n`Faraday::Request#method` called from .+/spec/faraday/request_spec.rb:\d+.}
29
+ end
30
+
31
+ it { expect(subject.method).to eq(:post) }
32
+
33
+ it { expect { subject.method }.to output(expected_warning).to_stderr }
34
+ end
35
+
36
+ context 'when setting the url on setup with a URI' do
37
+ let(:block) { proc { |req| req.url URI.parse('foo.json?a=1') } }
38
+
39
+ it { expect(subject.path).to eq(URI.parse('foo.json')) }
40
+ it { expect(subject.params).to eq('a' => '1') }
41
+ it { expect(subject.to_env(conn).url.to_s).to eq('http://sushi.com/api/foo.json?a=1') }
42
+ end
43
+
44
+ context 'when setting the url on setup with a string path and params' do
45
+ let(:block) { proc { |req| req.url 'foo.json', 'a' => 1 } }
46
+
47
+ it { expect(subject.path).to eq('foo.json') }
48
+ it { expect(subject.params).to eq('a' => 1) }
49
+ it { expect(subject.to_env(conn).url.to_s).to eq('http://sushi.com/api/foo.json?a=1') }
50
+ end
51
+
52
+ context 'when setting the url on setup with a path including params' do
53
+ let(:block) { proc { |req| req.url 'foo.json?b=2&a=1#qqq' } }
54
+
55
+ it { expect(subject.path).to eq('foo.json') }
56
+ it { expect(subject.params).to eq('a' => '1', 'b' => '2') }
57
+ it { expect(subject.to_env(conn).url.to_s).to eq('http://sushi.com/api/foo.json?a=1&b=2') }
58
+ end
59
+
60
+ context 'when setting a header on setup with []= syntax' do
61
+ let(:block) { proc { |req| req['Server'] = 'Faraday' } }
62
+ let(:headers) { subject.to_env(conn).request_headers }
63
+
64
+ it { expect(subject.headers['Server']).to eq('Faraday') }
65
+ it { expect(headers['mime-version']).to eq('1.0') }
66
+ it { expect(headers['server']).to eq('Faraday') }
67
+ end
68
+
69
+ context 'when setting the body on setup' do
70
+ let(:block) { proc { |req| req.body = 'hi' } }
71
+
72
+ it { expect(subject.body).to eq('hi') }
73
+ it { expect(subject.to_env(conn).body).to eq('hi') }
74
+ end
75
+
76
+ context 'with global request options set' do
77
+ let(:env_request) { subject.to_env(conn).request }
78
+
79
+ before do
80
+ conn.options.timeout = 3
81
+ conn.options.open_timeout = 5
82
+ conn.ssl.verify = false
83
+ conn.proxy = 'http://proxy.com'
84
+ end
85
+
86
+ it { expect(subject.options.timeout).to eq(3) }
87
+ it { expect(subject.options.open_timeout).to eq(5) }
88
+ it { expect(env_request.timeout).to eq(3) }
89
+ it { expect(env_request.open_timeout).to eq(5) }
90
+
91
+ context 'and per-request options set' do
92
+ let(:block) do
93
+ proc do |req|
94
+ req.options.timeout = 10
95
+ req.options.boundary = 'boo'
96
+ req.options.oauth[:consumer_secret] = 'xyz'
97
+ req.options.context = {
98
+ foo: 'foo',
99
+ bar: 'bar'
100
+ }
101
+ end
102
+ end
103
+
104
+ it { expect(subject.options.timeout).to eq(10) }
105
+ it { expect(subject.options.open_timeout).to eq(5) }
106
+ it { expect(env_request.timeout).to eq(10) }
107
+ it { expect(env_request.open_timeout).to eq(5) }
108
+ it { expect(env_request.boundary).to eq('boo') }
109
+ it { expect(env_request.context).to eq(foo: 'foo', bar: 'bar') }
110
+ it do
111
+ oauth_expected = { consumer_secret: 'xyz', consumer_key: 'anonymous' }
112
+ expect(env_request.oauth).to eq(oauth_expected)
113
+ end
114
+ end
115
+ end
116
+
117
+ it 'supports marshal serialization' do
118
+ expect(Marshal.load(Marshal.dump(subject))).to eq(subject)
119
+ end
120
+ end
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+ require 'logger'
5
+
6
+ RSpec.describe Faraday::Response::Logger do
7
+ let(:string_io) { StringIO.new }
8
+ let(:logger) { Logger.new(string_io) }
9
+ let(:logger_options) { {} }
10
+ let(:conn) do
11
+ rubbles = ['Barney', 'Betty', 'Bam Bam']
12
+
13
+ Faraday.new do |b|
14
+ b.response :logger, logger, logger_options do |logger|
15
+ logger.filter(/(soylent green is) (.+)/, '\1 tasty')
16
+ logger.filter(/(api_key:).*"(.+)."/, '\1[API_KEY]')
17
+ logger.filter(/(password)=(.+)/, '\1=[HIDDEN]')
18
+ end
19
+ b.adapter :test do |stubs|
20
+ stubs.get('/hello') { [200, { 'Content-Type' => 'text/html' }, 'hello'] }
21
+ stubs.post('/ohai') { [200, { 'Content-Type' => 'text/html' }, 'fred'] }
22
+ stubs.post('/ohyes') { [200, { 'Content-Type' => 'text/html' }, 'pebbles'] }
23
+ stubs.get('/rubbles') { [200, { 'Content-Type' => 'application/json' }, rubbles] }
24
+ stubs.get('/filtered_body') { [200, { 'Content-Type' => 'text/html' }, 'soylent green is people'] }
25
+ stubs.get('/filtered_headers') { [200, { 'Content-Type' => 'text/html' }, 'headers response'] }
26
+ stubs.get('/filtered_params') { [200, { 'Content-Type' => 'text/html' }, 'params response'] }
27
+ stubs.get('/filtered_url') { [200, { 'Content-Type' => 'text/html' }, 'url response'] }
28
+ end
29
+ end
30
+ end
31
+
32
+ before do
33
+ logger.level = Logger::DEBUG
34
+ end
35
+
36
+ it 'still returns output' do
37
+ resp = conn.get '/hello', nil, accept: 'text/html'
38
+ expect(resp.body).to eq('hello')
39
+ end
40
+
41
+ context 'without configuration' do
42
+ let(:conn) do
43
+ Faraday.new do |b|
44
+ b.response :logger
45
+ b.adapter :test do |stubs|
46
+ stubs.get('/hello') { [200, { 'Content-Type' => 'text/html' }, 'hello'] }
47
+ end
48
+ end
49
+ end
50
+
51
+ it 'defaults to stdout' do
52
+ expect(Logger).to receive(:new).with($stdout).and_return(Logger.new(nil))
53
+ conn.get('/hello')
54
+ end
55
+ end
56
+
57
+ context 'with default formatter' do
58
+ let(:formatter) { instance_double(Faraday::Logging::Formatter, request: true, response: true, filter: []) }
59
+
60
+ before { allow(Faraday::Logging::Formatter).to receive(:new).and_return(formatter) }
61
+
62
+ it 'delegates logging to the formatter' do
63
+ expect(formatter).to receive(:request).with(an_instance_of(Faraday::Env))
64
+ expect(formatter).to receive(:response).with(an_instance_of(Faraday::Env))
65
+ conn.get '/hello'
66
+ end
67
+ end
68
+
69
+ context 'with custom formatter' do
70
+ let(:formatter_class) do
71
+ Class.new(Faraday::Logging::Formatter) do
72
+ def request(_env)
73
+ info 'Custom log formatter request'
74
+ end
75
+
76
+ def response(_env)
77
+ info 'Custom log formatter response'
78
+ end
79
+ end
80
+ end
81
+
82
+ let(:logger_options) { { formatter: formatter_class } }
83
+
84
+ it 'logs with custom formatter' do
85
+ conn.get '/hello'
86
+
87
+ expect(string_io.string).to match('Custom log formatter request')
88
+ expect(string_io.string).to match('Custom log formatter response')
89
+ end
90
+ end
91
+
92
+ it 'logs method and url' do
93
+ conn.get '/hello', nil, accept: 'text/html'
94
+ expect(string_io.string).to match('GET http:/hello')
95
+ end
96
+
97
+ it 'logs request headers by default' do
98
+ conn.get '/hello', nil, accept: 'text/html'
99
+ expect(string_io.string).to match(%(Accept: "text/html))
100
+ end
101
+
102
+ it 'logs response headers by default' do
103
+ conn.get '/hello', nil, accept: 'text/html'
104
+ expect(string_io.string).to match(%(Content-Type: "text/html))
105
+ end
106
+
107
+ it 'does not log request body by default' do
108
+ conn.post '/ohai', 'name=Unagi', accept: 'text/html'
109
+ expect(string_io.string).not_to match(%(name=Unagi))
110
+ end
111
+
112
+ it 'does not log response body by default' do
113
+ conn.post '/ohai', 'name=Toro', accept: 'text/html'
114
+ expect(string_io.string).not_to match(%(fred))
115
+ end
116
+
117
+ it 'logs filter headers' do
118
+ conn.headers = { 'api_key' => 'ABC123' }
119
+ conn.get '/filtered_headers', nil, accept: 'text/html'
120
+ expect(string_io.string).to match(%(api_key:))
121
+ expect(string_io.string).to match(%([API_KEY]))
122
+ expect(string_io.string).not_to match(%(ABC123))
123
+ end
124
+
125
+ it 'logs filter url' do
126
+ conn.get '/filtered_url?password=hunter2', nil, accept: 'text/html'
127
+ expect(string_io.string).to match(%([HIDDEN]))
128
+ expect(string_io.string).not_to match(%(hunter2))
129
+ end
130
+
131
+ context 'when not logging request headers' do
132
+ let(:logger_options) { { headers: { request: false } } }
133
+
134
+ it 'does not log request headers if option is false' do
135
+ conn.get '/hello', nil, accept: 'text/html'
136
+ expect(string_io.string).not_to match(%(Accept: "text/html))
137
+ end
138
+ end
139
+
140
+ context 'when not logging response headers' do
141
+ let(:logger_options) { { headers: { response: false } } }
142
+
143
+ it 'does not log response headers if option is false' do
144
+ conn.get '/hello', nil, accept: 'text/html'
145
+ expect(string_io.string).not_to match(%(Content-Type: "text/html))
146
+ end
147
+ end
148
+
149
+ context 'when logging request body' do
150
+ let(:logger_options) { { bodies: { request: true } } }
151
+
152
+ it 'log only request body' do
153
+ conn.post '/ohyes', 'name=Tamago', accept: 'text/html'
154
+ expect(string_io.string).to match(%(name=Tamago))
155
+ expect(string_io.string).not_to match(%(pebbles))
156
+ end
157
+ end
158
+
159
+ context 'when logging response body' do
160
+ let(:logger_options) { { bodies: { response: true } } }
161
+
162
+ it 'log only response body' do
163
+ conn.post '/ohyes', 'name=Hamachi', accept: 'text/html'
164
+ expect(string_io.string).to match(%(pebbles))
165
+ expect(string_io.string).not_to match(%(name=Hamachi))
166
+ end
167
+ end
168
+
169
+ context 'when logging request and response bodies' do
170
+ let(:logger_options) { { bodies: true } }
171
+
172
+ it 'log request and response body' do
173
+ conn.post '/ohyes', 'name=Ebi', accept: 'text/html'
174
+ expect(string_io.string).to match(%(name=Ebi))
175
+ expect(string_io.string).to match(%(pebbles))
176
+ end
177
+
178
+ it 'log response body object' do
179
+ conn.get '/rubbles', nil, accept: 'text/html'
180
+ expect(string_io.string).to match(%([\"Barney\", \"Betty\", \"Bam Bam\"]\n))
181
+ end
182
+
183
+ it 'logs filter body' do
184
+ conn.get '/filtered_body', nil, accept: 'text/html'
185
+ expect(string_io.string).to match(%(soylent green is))
186
+ expect(string_io.string).to match(%(tasty))
187
+ expect(string_io.string).not_to match(%(people))
188
+ end
189
+ end
190
+
191
+ context 'when using log_level' do
192
+ let(:logger_options) { { bodies: true, log_level: :debug } }
193
+
194
+ it 'logs request/request body on the specified level (debug)' do
195
+ logger.level = Logger::DEBUG
196
+ conn.post '/ohyes', 'name=Ebi', accept: 'text/html'
197
+ expect(string_io.string).to match(%(name=Ebi))
198
+ expect(string_io.string).to match(%(pebbles))
199
+ end
200
+
201
+ it 'logs headers on the debug level' do
202
+ logger.level = Logger::DEBUG
203
+ conn.get '/hello', nil, accept: 'text/html'
204
+ expect(string_io.string).to match(%(Content-Type: "text/html))
205
+ end
206
+
207
+ it 'does not log request/response body on the info level' do
208
+ logger.level = Logger::INFO
209
+ conn.post '/ohyes', 'name=Ebi', accept: 'text/html'
210
+ expect(string_io.string).not_to match(%(name=Ebi))
211
+ expect(string_io.string).not_to match(%(pebbles))
212
+ end
213
+
214
+ it 'does not log headers on the info level' do
215
+ logger.level = Logger::INFO
216
+ conn.get '/hello', nil, accept: 'text/html'
217
+ expect(string_io.string).not_to match(%(Content-Type: "text/html))
218
+ end
219
+ end
220
+ end