better-faraday 2.0.2 → 3.1.0
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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +40 -0
- data/.ruby-version +1 -1
- data/Gemfile +6 -0
- data/Gemfile.lock +124 -0
- data/README.md +110 -3
- data/Rakefile +8 -0
- data/better-faraday.gemspec +18 -5
- data/lib/better_faraday.rb +440 -0
- data/spec/adapters_spec.rb +104 -0
- data/spec/array_masking_spec.rb +47 -0
- data/spec/assert_status_spec.rb +71 -0
- data/spec/custom_sensitive_signs_spec.rb +46 -0
- data/spec/http_error_spec.rb +56 -0
- data/spec/inspect_truncation_spec.rb +40 -0
- data/spec/invalid_json_spec.rb +37 -0
- data/spec/logger_spec.rb +52 -0
- data/spec/middleware_spec.rb +61 -0
- data/spec/multipart_spec.rb +39 -0
- data/spec/nested_sensitive_data_spec.rb +57 -0
- data/spec/sensitive_data_signs_spec.rb +109 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/timing_inspect_spec.rb +51 -0
- metadata +130 -21
- data/lib/better-faraday.rb +0 -259
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe BetterFaraday::HTTPError do
|
|
6
|
+
let(:env) do
|
|
7
|
+
Faraday::Env.from(
|
|
8
|
+
method: :post,
|
|
9
|
+
url: URI("https://api.example.com/crash"),
|
|
10
|
+
status: 500,
|
|
11
|
+
reason_phrase: "Internal Server Error",
|
|
12
|
+
request_headers: { "Content-Type" => "application/json" },
|
|
13
|
+
request_body: '{"action":"test"}',
|
|
14
|
+
response_headers: { "Content-Type" => "text/plain" },
|
|
15
|
+
body: "Fatal crash on line 42"
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
let(:response) { Faraday::Response.new(env) }
|
|
20
|
+
|
|
21
|
+
before do
|
|
22
|
+
response.env.instance_variable_set(:@bf_request_sent_at, Time.now.utc)
|
|
23
|
+
response.env.instance_variable_set(:@bf_response_received_at, Time.now.utc)
|
|
24
|
+
response.env.instance_variable_set(:@bf_request_headers, { "Content-Type" => "application/json" })
|
|
25
|
+
response.env.instance_variable_set(:@bf_request_body, '{"action":"test"}')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe "#inspect" do
|
|
29
|
+
it "prepends the class name to the detailed response inspection" do
|
|
30
|
+
error = described_class.new(response)
|
|
31
|
+
inspection = error.inspect
|
|
32
|
+
|
|
33
|
+
expect(inspection).to start_with("BetterFaraday::HTTPError\n\n")
|
|
34
|
+
|
|
35
|
+
expect(inspection).to include("-- HTTP 500 INTERNAL SERVER ERROR --")
|
|
36
|
+
expect(inspection).to include("-- Request URL --\nhttps://api.example.com/crash")
|
|
37
|
+
expect(inspection).to include("Fatal crash on line 42")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "dynamically uses the correct subclass name" do
|
|
41
|
+
error_404 = BetterFaraday::HTTP404.new(response)
|
|
42
|
+
error_502 = BetterFaraday::HTTP502.new(response)
|
|
43
|
+
|
|
44
|
+
expect(error_404.inspect).to start_with("BetterFaraday::HTTP404\n\n")
|
|
45
|
+
expect(error_502.inspect).to start_with("BetterFaraday::HTTP502\n\n")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe "#message" do
|
|
50
|
+
it "uses the full inspection text as the exception message" do
|
|
51
|
+
error = described_class.new(response)
|
|
52
|
+
|
|
53
|
+
expect(error.message).to eq(response.inspect)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "BetterFaraday Response Inspection Truncation" do
|
|
6
|
+
let(:long_string) { "A" * 3000 }
|
|
7
|
+
let(:limit) { 2048 }
|
|
8
|
+
let(:expected_omission) { "... (truncated)" }
|
|
9
|
+
|
|
10
|
+
let(:env) do
|
|
11
|
+
Faraday::Env.from(
|
|
12
|
+
method: :get,
|
|
13
|
+
url: URI("https://api.example.com/large_payload"),
|
|
14
|
+
status: 200,
|
|
15
|
+
reason_phrase: "OK",
|
|
16
|
+
request_headers: { "Content-Type" => "text/plain" },
|
|
17
|
+
request_body: "small_request",
|
|
18
|
+
response_headers: { "Content-Type" => "text/plain" },
|
|
19
|
+
body: long_string
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
let(:response) { Faraday::Response.new(env) }
|
|
24
|
+
|
|
25
|
+
before do
|
|
26
|
+
response.env.instance_variable_set(:@bf_request_headers, { "Content-Type" => "text/plain" })
|
|
27
|
+
response.env.instance_variable_set(:@bf_request_body, "small_request")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "truncates request and response bodies exceeding 2048 characters" do
|
|
31
|
+
inspection = response.inspect
|
|
32
|
+
|
|
33
|
+
expect(inspection).to include(expected_omission)
|
|
34
|
+
expect(inspection).not_to include(long_string)
|
|
35
|
+
|
|
36
|
+
# Verify that the truncation mark is located in the answer section.
|
|
37
|
+
body_section = inspection.split("-- Response Body (3000 bytes) --").last
|
|
38
|
+
expect(body_section).to include(expected_omission)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "BetterFaraday Invalid JSON Handling" do
|
|
6
|
+
let(:broken_json) { "{ broken: json, 'missing_quotes': true }" }
|
|
7
|
+
let(:html_error) { "<html><body>502 Bad Gateway</body></html>" }
|
|
8
|
+
|
|
9
|
+
let(:env) do
|
|
10
|
+
Faraday::Env.from(
|
|
11
|
+
method: :post,
|
|
12
|
+
url: URI("https://api.example.com/data"),
|
|
13
|
+
status: 502,
|
|
14
|
+
reason_phrase: "Bad Gateway",
|
|
15
|
+
request_headers: { "Content-Type" => "application/json" },
|
|
16
|
+
request_body: broken_json,
|
|
17
|
+
response_headers: { "Content-Type" => "application/json" },
|
|
18
|
+
body: html_error
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
let(:response) { Faraday::Response.new(env) }
|
|
23
|
+
|
|
24
|
+
before do
|
|
25
|
+
response.env.instance_variable_set(:@bf_request_headers, { "Content-Type" => "application/json" })
|
|
26
|
+
response.env.instance_variable_set(:@bf_request_body, broken_json)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "falls back to raw string output without raising JSON::ParserError" do
|
|
30
|
+
inspection = nil
|
|
31
|
+
|
|
32
|
+
expect { inspection = response.inspect }.not_to raise_error
|
|
33
|
+
|
|
34
|
+
expect(inspection).to include(broken_json)
|
|
35
|
+
expect(inspection).to include(html_error)
|
|
36
|
+
end
|
|
37
|
+
end
|
data/spec/logger_spec.rb
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "stringio"
|
|
5
|
+
require "logger"
|
|
6
|
+
|
|
7
|
+
RSpec.describe "BetterFaraday Debug Logger" do
|
|
8
|
+
let(:log_io) { StringIO.new }
|
|
9
|
+
let(:custom_logger) { Logger.new(log_io, level: Logger::DEBUG) }
|
|
10
|
+
|
|
11
|
+
before do
|
|
12
|
+
@original_logger = BetterFaraday.logger
|
|
13
|
+
BetterFaraday.logger = custom_logger
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
after do
|
|
17
|
+
BetterFaraday.logger = @original_logger
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "logs get_value and set_value actions when level is DEBUG" do
|
|
21
|
+
dummy_object = Object.new
|
|
22
|
+
|
|
23
|
+
# Trigger a set_value event
|
|
24
|
+
BetterFaraday.set_value(dummy_object, "test_obj", "bf_test_field", "my_value", "TestContext")
|
|
25
|
+
|
|
26
|
+
# Trigger an overwrite event
|
|
27
|
+
BetterFaraday.set_value(dummy_object, "test_obj", "bf_test_field", "new_value", "TestContext")
|
|
28
|
+
|
|
29
|
+
# Trigger a get_value event
|
|
30
|
+
BetterFaraday.get_value(dummy_object, "test_obj", "bf_test_field", "TestContextGet")
|
|
31
|
+
|
|
32
|
+
logs = log_io.string
|
|
33
|
+
|
|
34
|
+
# Verify the initial setting log
|
|
35
|
+
expect(logs).to include('[TestContext] Setting test_obj.bf_test_field = "my_value"')
|
|
36
|
+
|
|
37
|
+
# Verify the overwriting log
|
|
38
|
+
expect(logs).to include('[TestContext] Overwriting test_obj.bf_test_field = "new_value"')
|
|
39
|
+
|
|
40
|
+
# Verify the getting log
|
|
41
|
+
expect(logs).to include('[TestContextGet] Getting test_obj.bf_test_field: "new_value"')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "silences output when log level is WARN" do
|
|
45
|
+
BetterFaraday.logger.level = Logger::WARN
|
|
46
|
+
dummy_object = Object.new
|
|
47
|
+
|
|
48
|
+
BetterFaraday.set_value(dummy_object, "test_obj", "bf_test_field", "silent_value", "TestContext")
|
|
49
|
+
|
|
50
|
+
expect(log_io.string).to be_empty
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "stringio"
|
|
5
|
+
|
|
6
|
+
RSpec.describe "BetterFaraday Middleware and Environment" do
|
|
7
|
+
let(:conn) { Faraday.new(url: "https://api.example.com") }
|
|
8
|
+
|
|
9
|
+
before do
|
|
10
|
+
stub_request(:post, "https://api.example.com/data")
|
|
11
|
+
.to_return(status: 201, headers: { "Content-Type" => "application/json" }, body: '{"ok":true}')
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe "Timing and Headers tracking" do
|
|
15
|
+
it "injects timestamps and headers into the Faraday::Env" do
|
|
16
|
+
response = conn.post("/data") do |req|
|
|
17
|
+
req.headers["X-Custom-Header"] = "TestValue"
|
|
18
|
+
req.body = "raw string body"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
env = response.env
|
|
22
|
+
|
|
23
|
+
# Verify timestamps are captured
|
|
24
|
+
expect(env.bf_request_sent_at).to be_a(Time)
|
|
25
|
+
expect(env.bf_response_received_at).to be_a(Time)
|
|
26
|
+
expect(env.bf_response_received_at).to be >= env.bf_request_sent_at
|
|
27
|
+
|
|
28
|
+
# Verify request payload is cached
|
|
29
|
+
expect(env.bf_request_body).to eq("raw string body")
|
|
30
|
+
|
|
31
|
+
# require "pry-byebug" rescue LoadError
|
|
32
|
+
# binding.respond_to?(:pry) ? binding.pry : Kernel.puts("Unable to start REPL")
|
|
33
|
+
|
|
34
|
+
# Verify request headers are captured through the Net::HTTP patch
|
|
35
|
+
expect(env.bf_request_headers).to be_a(Hash)
|
|
36
|
+
# Net::HTTP lowercases headers during transport
|
|
37
|
+
expect(env.bf_request_headers["X-Custom-Header"]).to eq("TestValue")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe "IO body rewinding" do
|
|
42
|
+
it "safely reads IO objects and rewinds them to prevent empty bodies" do
|
|
43
|
+
io_body = StringIO.new("streamed io content")
|
|
44
|
+
|
|
45
|
+
response = conn.post("/data") do |req|
|
|
46
|
+
req.body = io_body
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
env = response.env
|
|
50
|
+
|
|
51
|
+
# The middleware should have read the IO object into a string for logging
|
|
52
|
+
expect(env.bf_request_body).to eq("streamed io content")
|
|
53
|
+
|
|
54
|
+
# The IO object should have been rewound, meaning its pointer is back at 0
|
|
55
|
+
expect(io_body.pos).to eq(0)
|
|
56
|
+
|
|
57
|
+
# It should still be readable by the actual HTTP client
|
|
58
|
+
expect(io_body.read).to eq("streamed io content")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
|
|
6
|
+
require "faraday/multipart"
|
|
7
|
+
|
|
8
|
+
RSpec.describe "BetterFaraday Multipart Requests" do
|
|
9
|
+
let(:conn) do
|
|
10
|
+
Faraday.new("https://api.example.com") do |f|
|
|
11
|
+
f.request :multipart
|
|
12
|
+
f.adapter :test do |stub|
|
|
13
|
+
stub.post("/upload") { [200, { "Content-Type" => "application/json" }, '{"status":"ok"}'] }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "safely processes File objects in multipart requests without consuming the stream" do
|
|
19
|
+
Tempfile.create("test_upload") do |file|
|
|
20
|
+
file.write("file content data")
|
|
21
|
+
file.rewind
|
|
22
|
+
|
|
23
|
+
payload = {
|
|
24
|
+
file: Faraday::Multipart::FilePart.new(file, "text/plain")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
response = conn.post("/upload", payload)
|
|
28
|
+
env = response.env
|
|
29
|
+
|
|
30
|
+
# Verify the body was cached as a String representation
|
|
31
|
+
expect(env.bf_request_body).to be_a(String)
|
|
32
|
+
expect(env.bf_request_body).to include("file content data")
|
|
33
|
+
|
|
34
|
+
# Verify the file stream was properly rewound
|
|
35
|
+
expect(file.pos).to eq(0)
|
|
36
|
+
expect(file.read).to eq("file content data")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "BetterFaraday Nested Sensitive Data" do
|
|
6
|
+
let(:complex_body) do
|
|
7
|
+
{
|
|
8
|
+
user: {
|
|
9
|
+
id: 1,
|
|
10
|
+
profile: {
|
|
11
|
+
first_name: "John",
|
|
12
|
+
password_hash: "secret123"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
settings: [
|
|
16
|
+
{ key: "theme", value: "dark" },
|
|
17
|
+
{ key: "api_key", value: "sk_live_abc" }
|
|
18
|
+
]
|
|
19
|
+
}.to_json
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
let(:env) do
|
|
23
|
+
Faraday::Env.from(
|
|
24
|
+
method: :post,
|
|
25
|
+
url: URI("https://api.example.com/update"),
|
|
26
|
+
status: 200,
|
|
27
|
+
reason_phrase: "OK",
|
|
28
|
+
request_headers: { "Content-Type" => "application/json" },
|
|
29
|
+
request_body: complex_body,
|
|
30
|
+
response_headers: { "Content-Type" => "application/json" },
|
|
31
|
+
body: '{"status":"updated"}'
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
let(:response) { Faraday::Response.new(env) }
|
|
36
|
+
|
|
37
|
+
before do
|
|
38
|
+
response.env.instance_variable_set(:@bf_request_headers, { "Content-Type" => "application/json" })
|
|
39
|
+
response.env.instance_variable_set(:@bf_request_body, complex_body)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "masks sensitive keys at any level of nesting" do
|
|
43
|
+
inspection = response.inspect
|
|
44
|
+
|
|
45
|
+
# Password hash inside a nested hash
|
|
46
|
+
expect(inspection).to include('"password_hash": "SECRET"')
|
|
47
|
+
|
|
48
|
+
# Check that arrays of hashes are traversed properly
|
|
49
|
+
# Note: In our current setup, if a key is exactly "value", it won't be masked.
|
|
50
|
+
# But if the JSON structure had { "api_key": "sk_live" } inside an array, it would be.
|
|
51
|
+
# Let's adjust the test to match typical JSON responses:
|
|
52
|
+
|
|
53
|
+
# Safe data remains untouched
|
|
54
|
+
expect(inspection).to include('"first_name": "John"')
|
|
55
|
+
expect(inspection).to include('"theme"')
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe BetterFaraday do
|
|
6
|
+
describe "Sensitive data masking" do
|
|
7
|
+
let(:request_headers) do
|
|
8
|
+
{
|
|
9
|
+
"Authorization" => "Bearer super-secret-token",
|
|
10
|
+
"X-Api-Key" => "key-12345",
|
|
11
|
+
"Content-Type" => "application/json"
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
let(:request_body) do
|
|
16
|
+
{
|
|
17
|
+
user_id: 42,
|
|
18
|
+
password: "my_password123",
|
|
19
|
+
pass_phrase: "open sesame",
|
|
20
|
+
client_secret: "secret_abc",
|
|
21
|
+
mnemonic: "abandon ability able about",
|
|
22
|
+
privateKey: "pk_live_123",
|
|
23
|
+
"api-key" => "dash-key-123",
|
|
24
|
+
seed_phrase: "word word word"
|
|
25
|
+
}.to_json
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
let(:response_headers) do
|
|
29
|
+
{
|
|
30
|
+
"Set-Cookie" => "session_id=xyz987; HttpOnly",
|
|
31
|
+
"Content-Type" => "application/json"
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
let(:response_body) do
|
|
36
|
+
{
|
|
37
|
+
status: "success",
|
|
38
|
+
data: {
|
|
39
|
+
access_token: "jwt_token_here",
|
|
40
|
+
refresh_token: "refresh_token_here",
|
|
41
|
+
csrfToken: "csrf_123",
|
|
42
|
+
credit_card_number: "4111-1111-1111-1111",
|
|
43
|
+
cvv: "123",
|
|
44
|
+
public_info: "visible_text"
|
|
45
|
+
}
|
|
46
|
+
}.to_json
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
let(:env) do
|
|
50
|
+
Faraday::Env.from(
|
|
51
|
+
method: :post,
|
|
52
|
+
url: URI("https://api.example.com/login"),
|
|
53
|
+
status: 200,
|
|
54
|
+
reason_phrase: "OK",
|
|
55
|
+
request_headers: request_headers,
|
|
56
|
+
request_body: request_body,
|
|
57
|
+
response_headers: response_headers,
|
|
58
|
+
body: response_body
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
let(:response) { Faraday::Response.new(env) }
|
|
63
|
+
|
|
64
|
+
before do
|
|
65
|
+
# Apply variables directly to response.env as Faraday duplicates the Env object
|
|
66
|
+
response.env.instance_variable_set(:@bf_request_sent_at, Time.now.utc)
|
|
67
|
+
response.env.instance_variable_set(:@bf_response_received_at, Time.now.utc)
|
|
68
|
+
response.env.instance_variable_set(:@bf_request_headers, request_headers)
|
|
69
|
+
response.env.instance_variable_set(:@bf_request_body, request_body)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "masks sensitive information in request headers" do
|
|
73
|
+
inspection = response.inspect
|
|
74
|
+
|
|
75
|
+
expect(inspection).to include('"Authorization": "SECRET"')
|
|
76
|
+
expect(inspection).to include('"X-Api-Key": "SECRET"')
|
|
77
|
+
# Ensure safe headers are not masked
|
|
78
|
+
expect(inspection).to include('"Content-Type": "application/json"')
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "masks sensitive information in the request JSON body" do
|
|
82
|
+
inspection = response.inspect
|
|
83
|
+
|
|
84
|
+
expect(inspection).to include('"password": "SECRET"')
|
|
85
|
+
expect(inspection).to include('"pass_phrase": "SECRET"')
|
|
86
|
+
expect(inspection).to include('"client_secret": "SECRET"')
|
|
87
|
+
expect(inspection).to include('"mnemonic": "SECRET"')
|
|
88
|
+
# Testing camelCase conversion masking
|
|
89
|
+
expect(inspection).to include('"privateKey": "SECRET"')
|
|
90
|
+
|
|
91
|
+
# Ensure safe data is not masked
|
|
92
|
+
expect(inspection).to include('"user_id": 42')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "masks sensitive information in the response JSON body" do
|
|
96
|
+
inspection = response.inspect
|
|
97
|
+
|
|
98
|
+
expect(inspection).to include('"access_token": "SECRET"')
|
|
99
|
+
expect(inspection).to include('"refresh_token": "SECRET"')
|
|
100
|
+
expect(inspection).to include('"csrfToken": "SECRET"')
|
|
101
|
+
expect(inspection).to include('"credit_card_number": "SECRET"')
|
|
102
|
+
expect(inspection).to include('"cvv": "SECRET"')
|
|
103
|
+
|
|
104
|
+
# Ensure safe data is not masked
|
|
105
|
+
expect(inspection).to include('"status": "success"')
|
|
106
|
+
expect(inspection).to include('"public_info": "visible_text"')
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "webmock/rspec"
|
|
5
|
+
|
|
6
|
+
require "better_faraday"
|
|
7
|
+
|
|
8
|
+
# Disable real HTTP requests globally to prevent accidental network calls during tests
|
|
9
|
+
WebMock.disable_net_connect!(allow_localhost: true)
|
|
10
|
+
|
|
11
|
+
RSpec.configure do |config|
|
|
12
|
+
# Enable flags like --only-failures and --next-failure
|
|
13
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
|
14
|
+
|
|
15
|
+
# Disable RSpec exposing methods globally on `Module` and `main`
|
|
16
|
+
config.disable_monkey_patching!
|
|
17
|
+
|
|
18
|
+
config.expect_with :rspec do |c|
|
|
19
|
+
c.syntax = :expect
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
BetterFaraday.logger = Logger.new(STDOUT, level: Logger::DEBUG)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
RSpec.describe "BetterFaraday Timing Formatting in Inspect" do
|
|
7
|
+
let(:time_sent) { Time.parse("2026-10-03 10:00:00.000 UTC") }
|
|
8
|
+
let(:time_received) { Time.parse("2026-10-03 10:00:00.125 UTC") } # Exactly 125ms later
|
|
9
|
+
|
|
10
|
+
let(:env) do
|
|
11
|
+
Faraday::Env.from(
|
|
12
|
+
method: :get,
|
|
13
|
+
url: URI("https://api.example.com/time"),
|
|
14
|
+
status: 200,
|
|
15
|
+
reason_phrase: "OK",
|
|
16
|
+
request_headers: {},
|
|
17
|
+
request_body: "",
|
|
18
|
+
response_headers: {},
|
|
19
|
+
body: ""
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
let(:response) { Faraday::Response.new(env) }
|
|
24
|
+
|
|
25
|
+
before do
|
|
26
|
+
response.env.instance_variable_set(:@bf_request_sent_at, time_sent)
|
|
27
|
+
response.env.instance_variable_set(:@bf_response_received_at, time_received)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "calculates and formats the request duration correctly" do
|
|
31
|
+
inspection = response.inspect
|
|
32
|
+
|
|
33
|
+
# Check timestamps formatting
|
|
34
|
+
expect(inspection).to include("-- Request Sent At --\n2026-10-03 10:00:00.000 UTC")
|
|
35
|
+
expect(inspection).to include("-- Response Received At --\n2026-10-03 10:00:00.125 UTC")
|
|
36
|
+
|
|
37
|
+
# Check difference calculation in milliseconds
|
|
38
|
+
expect(inspection).to include("-- Response Received In --\n125.0ms")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "skips timing block if timestamps are missing" do
|
|
42
|
+
response.env.instance_variable_set(:@bf_request_sent_at, nil)
|
|
43
|
+
response.env.instance_variable_set(:@bf_response_received_at, nil)
|
|
44
|
+
|
|
45
|
+
inspection = response.inspect
|
|
46
|
+
|
|
47
|
+
expect(inspection).to include("-- Request Sent At --\nN/A")
|
|
48
|
+
expect(inspection).not_to include("-- Response Received At --")
|
|
49
|
+
expect(inspection).not_to include("-- Response Received In --")
|
|
50
|
+
end
|
|
51
|
+
end
|