faraday 0.17.6 → 1.0.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 (133) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -8
  3. data/LICENSE.md +1 -1
  4. data/README.md +18 -358
  5. data/Rakefile +1 -7
  6. data/examples/client_spec.rb +65 -0
  7. data/examples/client_test.rb +79 -0
  8. data/lib/faraday/adapter/em_http.rb +142 -99
  9. data/lib/faraday/adapter/em_http_ssl_patch.rb +24 -18
  10. data/lib/faraday/adapter/em_synchrony/parallel_manager.rb +18 -15
  11. data/lib/faraday/adapter/em_synchrony.rb +104 -60
  12. data/lib/faraday/adapter/excon.rb +98 -56
  13. data/lib/faraday/adapter/httpclient.rb +83 -59
  14. data/lib/faraday/adapter/net_http.rb +129 -63
  15. data/lib/faraday/adapter/net_http_persistent.rb +50 -27
  16. data/lib/faraday/adapter/patron.rb +80 -43
  17. data/lib/faraday/adapter/rack.rb +30 -13
  18. data/lib/faraday/adapter/test.rb +86 -53
  19. data/lib/faraday/adapter/typhoeus.rb +4 -1
  20. data/lib/faraday/adapter.rb +82 -22
  21. data/lib/faraday/adapter_registry.rb +30 -0
  22. data/lib/faraday/autoload.rb +47 -36
  23. data/lib/faraday/connection.rb +312 -182
  24. data/lib/faraday/dependency_loader.rb +37 -0
  25. data/lib/faraday/encoders/flat_params_encoder.rb +98 -0
  26. data/lib/faraday/encoders/nested_params_encoder.rb +171 -0
  27. data/lib/faraday/error.rb +9 -35
  28. data/lib/faraday/file_part.rb +128 -0
  29. data/lib/faraday/logging/formatter.rb +105 -0
  30. data/lib/faraday/middleware.rb +12 -28
  31. data/lib/faraday/middleware_registry.rb +129 -0
  32. data/lib/faraday/options/connection_options.rb +22 -0
  33. data/lib/faraday/options/env.rb +181 -0
  34. data/lib/faraday/options/proxy_options.rb +28 -0
  35. data/lib/faraday/options/request_options.rb +22 -0
  36. data/lib/faraday/options/ssl_options.rb +59 -0
  37. data/lib/faraday/options.rb +32 -183
  38. data/lib/faraday/param_part.rb +53 -0
  39. data/lib/faraday/parameters.rb +4 -197
  40. data/lib/faraday/rack_builder.rb +66 -55
  41. data/lib/faraday/request/authorization.rb +44 -30
  42. data/lib/faraday/request/basic_authentication.rb +14 -7
  43. data/lib/faraday/request/instrumentation.rb +45 -27
  44. data/lib/faraday/request/multipart.rb +79 -48
  45. data/lib/faraday/request/retry.rb +197 -171
  46. data/lib/faraday/request/token_authentication.rb +15 -10
  47. data/lib/faraday/request/url_encoded.rb +43 -23
  48. data/lib/faraday/request.rb +68 -38
  49. data/lib/faraday/response/logger.rb +22 -69
  50. data/lib/faraday/response/raise_error.rb +38 -18
  51. data/lib/faraday/response.rb +24 -14
  52. data/lib/faraday/utils/headers.rb +139 -0
  53. data/lib/faraday/utils/params_hash.rb +61 -0
  54. data/lib/faraday/utils.rb +36 -245
  55. data/lib/faraday.rb +94 -175
  56. data/spec/external_adapters/faraday_specs_setup.rb +14 -0
  57. data/spec/faraday/adapter/em_http_spec.rb +47 -0
  58. data/spec/faraday/adapter/em_synchrony_spec.rb +16 -0
  59. data/spec/faraday/adapter/excon_spec.rb +49 -0
  60. data/spec/faraday/adapter/httpclient_spec.rb +73 -0
  61. data/spec/faraday/adapter/net_http_persistent_spec.rb +57 -0
  62. data/spec/faraday/adapter/net_http_spec.rb +64 -0
  63. data/spec/faraday/adapter/patron_spec.rb +18 -0
  64. data/spec/faraday/adapter/rack_spec.rb +8 -0
  65. data/spec/faraday/adapter/typhoeus_spec.rb +7 -0
  66. data/spec/faraday/adapter_registry_spec.rb +28 -0
  67. data/spec/faraday/adapter_spec.rb +55 -0
  68. data/spec/faraday/composite_read_io_spec.rb +80 -0
  69. data/spec/faraday/connection_spec.rb +691 -0
  70. data/spec/faraday/error_spec.rb +0 -57
  71. data/spec/faraday/middleware_spec.rb +26 -0
  72. data/spec/faraday/options/env_spec.rb +70 -0
  73. data/spec/faraday/options/options_spec.rb +297 -0
  74. data/spec/faraday/options/proxy_options_spec.rb +37 -0
  75. data/spec/faraday/options/request_options_spec.rb +19 -0
  76. data/spec/faraday/params_encoders/flat_spec.rb +34 -0
  77. data/spec/faraday/params_encoders/nested_spec.rb +134 -0
  78. data/spec/faraday/rack_builder_spec.rb +196 -0
  79. data/spec/faraday/request/authorization_spec.rb +88 -0
  80. data/spec/faraday/request/instrumentation_spec.rb +76 -0
  81. data/spec/faraday/request/multipart_spec.rb +274 -0
  82. data/spec/faraday/request/retry_spec.rb +242 -0
  83. data/spec/faraday/request/url_encoded_spec.rb +83 -0
  84. data/spec/faraday/request_spec.rb +109 -0
  85. data/spec/faraday/response/logger_spec.rb +220 -0
  86. data/spec/faraday/response/middleware_spec.rb +68 -0
  87. data/spec/faraday/response/raise_error_spec.rb +15 -15
  88. data/spec/faraday/response_spec.rb +75 -0
  89. data/spec/faraday/utils/headers_spec.rb +82 -0
  90. data/spec/faraday/utils_spec.rb +56 -0
  91. data/spec/faraday_spec.rb +37 -0
  92. data/spec/spec_helper.rb +63 -36
  93. data/spec/support/disabling_stub.rb +14 -0
  94. data/spec/support/fake_safe_buffer.rb +15 -0
  95. data/spec/support/helper_methods.rb +133 -0
  96. data/spec/support/shared_examples/adapter.rb +104 -0
  97. data/spec/support/shared_examples/params_encoder.rb +18 -0
  98. data/spec/support/shared_examples/request_method.rb +234 -0
  99. data/spec/support/streaming_response_checker.rb +35 -0
  100. data/spec/support/webmock_rack_app.rb +68 -0
  101. metadata +66 -38
  102. data/lib/faraday/deprecate.rb +0 -109
  103. data/lib/faraday/upload_io.rb +0 -77
  104. data/spec/faraday/deprecate_spec.rb +0 -147
  105. data/test/adapters/default_test.rb +0 -14
  106. data/test/adapters/em_http_test.rb +0 -30
  107. data/test/adapters/em_synchrony_test.rb +0 -32
  108. data/test/adapters/excon_test.rb +0 -30
  109. data/test/adapters/httpclient_test.rb +0 -34
  110. data/test/adapters/integration.rb +0 -263
  111. data/test/adapters/logger_test.rb +0 -136
  112. data/test/adapters/net_http_persistent_test.rb +0 -114
  113. data/test/adapters/net_http_test.rb +0 -79
  114. data/test/adapters/patron_test.rb +0 -40
  115. data/test/adapters/rack_test.rb +0 -38
  116. data/test/adapters/test_middleware_test.rb +0 -157
  117. data/test/adapters/typhoeus_test.rb +0 -38
  118. data/test/authentication_middleware_test.rb +0 -65
  119. data/test/composite_read_io_test.rb +0 -109
  120. data/test/connection_test.rb +0 -738
  121. data/test/env_test.rb +0 -268
  122. data/test/helper.rb +0 -75
  123. data/test/live_server.rb +0 -67
  124. data/test/middleware/instrumentation_test.rb +0 -88
  125. data/test/middleware/retry_test.rb +0 -282
  126. data/test/middleware_stack_test.rb +0 -260
  127. data/test/multibyte.txt +0 -1
  128. data/test/options_test.rb +0 -333
  129. data/test/parameters_test.rb +0 -157
  130. data/test/request_middleware_test.rb +0 -126
  131. data/test/response_middleware_test.rb +0 -72
  132. data/test/strawberry.rb +0 -2
  133. data/test/utils_test.rb +0 -98
@@ -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.15) }
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,109 @@
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(:method) { :get }
10
+ let(:block) { nil }
11
+
12
+ subject { conn.build_request(method, &block) }
13
+
14
+ context 'when nothing particular is configured' do
15
+ it { expect(subject.method).to eq(:get) }
16
+ it { expect(subject.to_env(conn).ssl.verify).to be_falsey }
17
+ end
18
+
19
+ context 'when method is post' do
20
+ let(:method) { :post }
21
+
22
+ it { expect(subject.method).to eq(:post) }
23
+ end
24
+
25
+ context 'when setting the url on setup with a URI' do
26
+ let(:block) { proc { |req| req.url URI.parse('foo.json?a=1') } }
27
+
28
+ it { expect(subject.path).to eq(URI.parse('foo.json')) }
29
+ it { expect(subject.params).to eq('a' => '1') }
30
+ it { expect(subject.to_env(conn).url.to_s).to eq('http://sushi.com/api/foo.json?a=1') }
31
+ end
32
+
33
+ context 'when setting the url on setup with a string path and params' do
34
+ let(:block) { proc { |req| req.url 'foo.json', 'a' => 1 } }
35
+
36
+ it { expect(subject.path).to eq('foo.json') }
37
+ it { expect(subject.params).to eq('a' => 1) }
38
+ it { expect(subject.to_env(conn).url.to_s).to eq('http://sushi.com/api/foo.json?a=1') }
39
+ end
40
+
41
+ context 'when setting the url on setup with a path including params' do
42
+ let(:block) { proc { |req| req.url 'foo.json?b=2&a=1#qqq' } }
43
+
44
+ it { expect(subject.path).to eq('foo.json') }
45
+ it { expect(subject.params).to eq('a' => '1', 'b' => '2') }
46
+ it { expect(subject.to_env(conn).url.to_s).to eq('http://sushi.com/api/foo.json?a=1&b=2') }
47
+ end
48
+
49
+ context 'when setting a header on setup with []= syntax' do
50
+ let(:block) { proc { |req| req['Server'] = 'Faraday' } }
51
+ let(:headers) { subject.to_env(conn).request_headers }
52
+
53
+ it { expect(subject.headers['Server']).to eq('Faraday') }
54
+ it { expect(headers['mime-version']).to eq('1.0') }
55
+ it { expect(headers['server']).to eq('Faraday') }
56
+ end
57
+
58
+ context 'when setting the body on setup' do
59
+ let(:block) { proc { |req| req.body = 'hi' } }
60
+
61
+ it { expect(subject.body).to eq('hi') }
62
+ it { expect(subject.to_env(conn).body).to eq('hi') }
63
+ end
64
+
65
+ context 'with global request options set' do
66
+ let(:env_request) { subject.to_env(conn).request }
67
+
68
+ before do
69
+ conn.options.timeout = 3
70
+ conn.options.open_timeout = 5
71
+ conn.ssl.verify = false
72
+ conn.proxy = 'http://proxy.com'
73
+ end
74
+
75
+ it { expect(subject.options.timeout).to eq(3) }
76
+ it { expect(subject.options.open_timeout).to eq(5) }
77
+ it { expect(env_request.timeout).to eq(3) }
78
+ it { expect(env_request.open_timeout).to eq(5) }
79
+
80
+ context 'and per-request options set' do
81
+ let(:block) do
82
+ proc do |req|
83
+ req.options.timeout = 10
84
+ req.options.boundary = 'boo'
85
+ req.options.oauth[:consumer_secret] = 'xyz'
86
+ req.options.context = {
87
+ foo: 'foo',
88
+ bar: 'bar'
89
+ }
90
+ end
91
+ end
92
+
93
+ it { expect(subject.options.timeout).to eq(10) }
94
+ it { expect(subject.options.open_timeout).to eq(5) }
95
+ it { expect(env_request.timeout).to eq(10) }
96
+ it { expect(env_request.open_timeout).to eq(5) }
97
+ it { expect(env_request.boundary).to eq('boo') }
98
+ it { expect(env_request.context).to eq(foo: 'foo', bar: 'bar') }
99
+ it do
100
+ oauth_expected = { consumer_secret: 'xyz', consumer_key: 'anonymous' }
101
+ expect(env_request.oauth).to eq(oauth_expected)
102
+ end
103
+ end
104
+ end
105
+
106
+ it 'supports marshal serialization' do
107
+ expect(Marshal.load(Marshal.dump(subject))).to eq(subject)
108
+ end
109
+ 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