faraday 0.15.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +570 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +25 -351
  5. data/Rakefile +7 -0
  6. data/examples/client_spec.rb +97 -0
  7. data/examples/client_test.rb +118 -0
  8. data/lib/faraday/adapter/test.rb +127 -72
  9. data/lib/faraday/adapter.rb +69 -22
  10. data/lib/faraday/adapter_registry.rb +30 -0
  11. data/lib/faraday/connection.rb +309 -232
  12. data/lib/faraday/encoders/flat_params_encoder.rb +105 -0
  13. data/lib/faraday/encoders/nested_params_encoder.rb +176 -0
  14. data/lib/faraday/error.rb +119 -38
  15. data/lib/faraday/logging/formatter.rb +106 -0
  16. data/lib/faraday/methods.rb +6 -0
  17. data/lib/faraday/middleware.rb +18 -25
  18. data/lib/faraday/middleware_registry.rb +83 -0
  19. data/lib/faraday/options/connection_options.rb +22 -0
  20. data/lib/faraday/options/env.rb +181 -0
  21. data/lib/faraday/options/proxy_options.rb +32 -0
  22. data/lib/faraday/options/request_options.rb +22 -0
  23. data/lib/faraday/options/ssl_options.rb +59 -0
  24. data/lib/faraday/options.rb +41 -195
  25. data/lib/faraday/parameters.rb +4 -196
  26. data/lib/faraday/rack_builder.rb +91 -76
  27. data/lib/faraday/request/authorization.rb +37 -29
  28. data/lib/faraday/request/instrumentation.rb +47 -27
  29. data/lib/faraday/request/json.rb +55 -0
  30. data/lib/faraday/request/url_encoded.rb +45 -23
  31. data/lib/faraday/request.rb +74 -32
  32. data/lib/faraday/response/json.rb +54 -0
  33. data/lib/faraday/response/logger.rb +22 -69
  34. data/lib/faraday/response/raise_error.rb +57 -14
  35. data/lib/faraday/response.rb +26 -33
  36. data/lib/faraday/utils/headers.rb +139 -0
  37. data/lib/faraday/utils/params_hash.rb +61 -0
  38. data/lib/faraday/utils.rb +47 -251
  39. data/lib/faraday/version.rb +5 -0
  40. data/lib/faraday.rb +108 -199
  41. data/spec/external_adapters/faraday_specs_setup.rb +14 -0
  42. data/spec/faraday/adapter/test_spec.rb +377 -0
  43. data/spec/faraday/adapter_registry_spec.rb +28 -0
  44. data/spec/faraday/adapter_spec.rb +55 -0
  45. data/spec/faraday/connection_spec.rb +787 -0
  46. data/spec/faraday/error_spec.rb +60 -0
  47. data/spec/faraday/middleware_registry_spec.rb +31 -0
  48. data/spec/faraday/middleware_spec.rb +52 -0
  49. data/spec/faraday/options/env_spec.rb +70 -0
  50. data/spec/faraday/options/options_spec.rb +297 -0
  51. data/spec/faraday/options/proxy_options_spec.rb +44 -0
  52. data/spec/faraday/options/request_options_spec.rb +19 -0
  53. data/spec/faraday/params_encoders/flat_spec.rb +42 -0
  54. data/spec/faraday/params_encoders/nested_spec.rb +142 -0
  55. data/spec/faraday/rack_builder_spec.rb +317 -0
  56. data/spec/faraday/request/authorization_spec.rb +83 -0
  57. data/spec/faraday/request/instrumentation_spec.rb +74 -0
  58. data/spec/faraday/request/json_spec.rb +111 -0
  59. data/spec/faraday/request/url_encoded_spec.rb +82 -0
  60. data/spec/faraday/request_spec.rb +109 -0
  61. data/spec/faraday/response/json_spec.rb +117 -0
  62. data/spec/faraday/response/logger_spec.rb +220 -0
  63. data/spec/faraday/response/raise_error_spec.rb +172 -0
  64. data/spec/faraday/response_spec.rb +75 -0
  65. data/spec/faraday/utils/headers_spec.rb +82 -0
  66. data/spec/faraday/utils_spec.rb +117 -0
  67. data/spec/faraday_spec.rb +37 -0
  68. data/spec/spec_helper.rb +132 -0
  69. data/spec/support/disabling_stub.rb +14 -0
  70. data/spec/support/fake_safe_buffer.rb +15 -0
  71. data/spec/support/helper_methods.rb +96 -0
  72. data/spec/support/shared_examples/adapter.rb +104 -0
  73. data/spec/support/shared_examples/params_encoder.rb +18 -0
  74. data/spec/support/shared_examples/request_method.rb +249 -0
  75. data/spec/support/streaming_response_checker.rb +35 -0
  76. metadata +86 -34
  77. data/lib/faraday/adapter/em_http.rb +0 -243
  78. data/lib/faraday/adapter/em_http_ssl_patch.rb +0 -56
  79. data/lib/faraday/adapter/em_synchrony/parallel_manager.rb +0 -66
  80. data/lib/faraday/adapter/em_synchrony.rb +0 -106
  81. data/lib/faraday/adapter/excon.rb +0 -79
  82. data/lib/faraday/adapter/httpclient.rb +0 -128
  83. data/lib/faraday/adapter/net_http.rb +0 -137
  84. data/lib/faraday/adapter/net_http_persistent.rb +0 -63
  85. data/lib/faraday/adapter/patron.rb +0 -100
  86. data/lib/faraday/adapter/rack.rb +0 -58
  87. data/lib/faraday/adapter/typhoeus.rb +0 -12
  88. data/lib/faraday/autoload.rb +0 -84
  89. data/lib/faraday/request/basic_authentication.rb +0 -13
  90. data/lib/faraday/request/multipart.rb +0 -68
  91. data/lib/faraday/request/retry.rb +0 -211
  92. data/lib/faraday/request/token_authentication.rb +0 -15
  93. data/lib/faraday/upload_io.rb +0 -67
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Faraday::Request::Json do
4
+ let(:middleware) { described_class.new(->(env) { Faraday::Response.new(env) }) }
5
+
6
+ def process(body, content_type = nil)
7
+ env = { body: body, request_headers: Faraday::Utils::Headers.new }
8
+ env[:request_headers]['content-type'] = content_type if content_type
9
+ middleware.call(Faraday::Env.from(env)).env
10
+ end
11
+
12
+ def result_body
13
+ result[:body]
14
+ end
15
+
16
+ def result_type
17
+ result[:request_headers]['content-type']
18
+ end
19
+
20
+ context 'no body' do
21
+ let(:result) { process(nil) }
22
+
23
+ it "doesn't change body" do
24
+ expect(result_body).to be_nil
25
+ end
26
+
27
+ it "doesn't add content type" do
28
+ expect(result_type).to be_nil
29
+ end
30
+ end
31
+
32
+ context 'empty body' do
33
+ let(:result) { process('') }
34
+
35
+ it "doesn't change body" do
36
+ expect(result_body).to be_empty
37
+ end
38
+
39
+ it "doesn't add content type" do
40
+ expect(result_type).to be_nil
41
+ end
42
+ end
43
+
44
+ context 'string body' do
45
+ let(:result) { process('{"a":1}') }
46
+
47
+ it "doesn't change body" do
48
+ expect(result_body).to eq('{"a":1}')
49
+ end
50
+
51
+ it 'adds content type' do
52
+ expect(result_type).to eq('application/json')
53
+ end
54
+ end
55
+
56
+ context 'object body' do
57
+ let(:result) { process(a: 1) }
58
+
59
+ it 'encodes body' do
60
+ expect(result_body).to eq('{"a":1}')
61
+ end
62
+
63
+ it 'adds content type' do
64
+ expect(result_type).to eq('application/json')
65
+ end
66
+ end
67
+
68
+ context 'empty object body' do
69
+ let(:result) { process({}) }
70
+
71
+ it 'encodes body' do
72
+ expect(result_body).to eq('{}')
73
+ end
74
+ end
75
+
76
+ context 'object body with json type' do
77
+ let(:result) { process({ a: 1 }, 'application/json; charset=utf-8') }
78
+
79
+ it 'encodes body' do
80
+ expect(result_body).to eq('{"a":1}')
81
+ end
82
+
83
+ it "doesn't change content type" do
84
+ expect(result_type).to eq('application/json; charset=utf-8')
85
+ end
86
+ end
87
+
88
+ context 'object body with vendor json type' do
89
+ let(:result) { process({ a: 1 }, 'application/vnd.myapp.v1+json; charset=utf-8') }
90
+
91
+ it 'encodes body' do
92
+ expect(result_body).to eq('{"a":1}')
93
+ end
94
+
95
+ it "doesn't change content type" do
96
+ expect(result_type).to eq('application/vnd.myapp.v1+json; charset=utf-8')
97
+ end
98
+ end
99
+
100
+ context 'object body with incompatible type' do
101
+ let(:result) { process({ a: 1 }, 'application/xml; charset=utf-8') }
102
+
103
+ it "doesn't change body" do
104
+ expect(result_body).to eq(a: 1)
105
+ end
106
+
107
+ it "doesn't change content type" do
108
+ expect(result_type).to eq('application/xml; charset=utf-8')
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,82 @@
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 :url_encoded
7
+ b.adapter :test do |stub|
8
+ stub.post('/echo') do |env|
9
+ posted_as = env[:request_headers]['Content-Type']
10
+ [200, { 'Content-Type' => posted_as }, env[:body]]
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ it 'does nothing without payload' do
17
+ response = conn.post('/echo')
18
+ expect(response.headers['Content-Type']).to be_nil
19
+ expect(response.body.empty?).to be_truthy
20
+ end
21
+
22
+ it 'ignores custom content type' do
23
+ response = conn.post('/echo', { some: 'data' }, 'content-type' => 'application/x-foo')
24
+ expect(response.headers['Content-Type']).to eq('application/x-foo')
25
+ expect(response.body).to eq(some: 'data')
26
+ end
27
+
28
+ it 'works with no headers' do
29
+ response = conn.post('/echo', fruit: %w[apples oranges])
30
+ expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded')
31
+ expect(response.body).to eq('fruit%5B%5D=apples&fruit%5B%5D=oranges')
32
+ end
33
+
34
+ it 'works with with headers' do
35
+ response = conn.post('/echo', { 'a' => 123 }, 'content-type' => 'application/x-www-form-urlencoded')
36
+ expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded')
37
+ expect(response.body).to eq('a=123')
38
+ end
39
+
40
+ it 'works with nested params' do
41
+ response = conn.post('/echo', user: { name: 'Mislav', web: 'mislav.net' })
42
+ expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded')
43
+ expected = { 'user' => { 'name' => 'Mislav', 'web' => 'mislav.net' } }
44
+ expect(Faraday::Utils.parse_nested_query(response.body)).to eq(expected)
45
+ end
46
+
47
+ it 'works with non nested params' do
48
+ response = conn.post('/echo', dimensions: %w[date location]) do |req|
49
+ req.options.params_encoder = Faraday::FlatParamsEncoder
50
+ end
51
+ expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded')
52
+ expected = { 'dimensions' => %w[date location] }
53
+ expect(Faraday::Utils.parse_query(response.body)).to eq(expected)
54
+ expect(response.body).to eq('dimensions=date&dimensions=location')
55
+ end
56
+
57
+ it 'works with unicode' do
58
+ err = capture_warnings do
59
+ response = conn.post('/echo', str: 'eé cç aã aâ')
60
+ expect(response.body).to eq('str=e%C3%A9+c%C3%A7+a%C3%A3+a%C3%A2')
61
+ end
62
+ expect(err.empty?).to be_truthy
63
+ end
64
+
65
+ it 'works with nested keys' do
66
+ response = conn.post('/echo', 'a' => { 'b' => { 'c' => ['d'] } })
67
+ expect(response.body).to eq('a%5Bb%5D%5Bc%5D%5B%5D=d')
68
+ end
69
+
70
+ context 'customising default_space_encoding' do
71
+ around do |example|
72
+ Faraday::Utils.default_space_encoding = '%20'
73
+ example.run
74
+ Faraday::Utils.default_space_encoding = nil
75
+ end
76
+
77
+ it 'uses the custom character to encode spaces' do
78
+ response = conn.post('/echo', str: 'apple banana')
79
+ expect(response.body).to eq('str=apple%20banana')
80
+ end
81
+ end
82
+ 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://httpbingo.org/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
+ 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://httpbingo.org/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://httpbingo.org/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://httpbingo.org/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,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Faraday::Response::Json, type: :response do
4
+ let(:options) { {} }
5
+ let(:headers) { {} }
6
+ let(:middleware) do
7
+ described_class.new(lambda { |env|
8
+ Faraday::Response.new(env)
9
+ }, **options)
10
+ end
11
+
12
+ def process(body, content_type = 'application/json', options = {})
13
+ env = {
14
+ body: body, request: options,
15
+ request_headers: Faraday::Utils::Headers.new,
16
+ response_headers: Faraday::Utils::Headers.new(headers)
17
+ }
18
+ env[:response_headers]['content-type'] = content_type if content_type
19
+ yield(env) if block_given?
20
+ middleware.call(Faraday::Env.from(env))
21
+ end
22
+
23
+ context 'no type matching' do
24
+ it "doesn't change nil body" do
25
+ expect(process(nil).body).to be_nil
26
+ end
27
+
28
+ it 'nullifies empty body' do
29
+ expect(process('').body).to be_nil
30
+ end
31
+
32
+ it 'parses json body' do
33
+ response = process('{"a":1}')
34
+ expect(response.body).to eq('a' => 1)
35
+ expect(response.env[:raw_body]).to be_nil
36
+ end
37
+ end
38
+
39
+ context 'with preserving raw' do
40
+ let(:options) { { preserve_raw: true } }
41
+
42
+ it 'parses json body' do
43
+ response = process('{"a":1}')
44
+ expect(response.body).to eq('a' => 1)
45
+ expect(response.env[:raw_body]).to eq('{"a":1}')
46
+ end
47
+ end
48
+
49
+ context 'with default regexp type matching' do
50
+ it 'parses json body of correct type' do
51
+ response = process('{"a":1}', 'application/x-json')
52
+ expect(response.body).to eq('a' => 1)
53
+ end
54
+
55
+ it 'ignores json body of incorrect type' do
56
+ response = process('{"a":1}', 'text/json-xml')
57
+ expect(response.body).to eq('{"a":1}')
58
+ end
59
+ end
60
+
61
+ context 'with array type matching' do
62
+ let(:options) { { content_type: %w[a/b c/d] } }
63
+
64
+ it 'parses json body of correct type' do
65
+ expect(process('{"a":1}', 'a/b').body).to be_a(Hash)
66
+ expect(process('{"a":1}', 'c/d').body).to be_a(Hash)
67
+ end
68
+
69
+ it 'ignores json body of incorrect type' do
70
+ expect(process('{"a":1}', 'a/d').body).not_to be_a(Hash)
71
+ end
72
+ end
73
+
74
+ it 'chokes on invalid json' do
75
+ expect { process('{!') }.to raise_error(Faraday::ParsingError)
76
+ end
77
+
78
+ it 'includes the response on the ParsingError instance' do
79
+ process('{') { |env| env[:response] = Faraday::Response.new }
80
+ raise 'Parsing should have failed.'
81
+ rescue Faraday::ParsingError => e
82
+ expect(e.response).to be_a(Faraday::Response)
83
+ end
84
+
85
+ context 'HEAD responses' do
86
+ it "nullifies the body if it's only one space" do
87
+ response = process(' ')
88
+ expect(response.body).to be_nil
89
+ end
90
+
91
+ it "nullifies the body if it's two spaces" do
92
+ response = process(' ')
93
+ expect(response.body).to be_nil
94
+ end
95
+ end
96
+
97
+ context 'JSON options' do
98
+ let(:body) { '{"a": 1}' }
99
+ let(:result) { { a: 1 } }
100
+ let(:options) do
101
+ {
102
+ parser_options: {
103
+ symbolize_names: true
104
+ }
105
+ }
106
+ end
107
+
108
+ it 'passes relevant options to JSON parse' do
109
+ expect(::JSON).to receive(:parse)
110
+ .with(body, options[:parser_options])
111
+ .and_return(result)
112
+
113
+ response = process(body)
114
+ expect(response.body).to eq(result)
115
+ end
116
+ end
117
+ 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