http 5.3.1 → 6.0.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/CHANGELOG.md +241 -41
- data/LICENSE.txt +1 -1
- data/README.md +110 -13
- data/UPGRADING.md +491 -0
- data/http.gemspec +32 -29
- data/lib/http/base64.rb +11 -1
- data/lib/http/chainable/helpers.rb +62 -0
- data/lib/http/chainable/verbs.rb +136 -0
- data/lib/http/chainable.rb +232 -136
- data/lib/http/client.rb +158 -127
- data/lib/http/connection/internals.rb +141 -0
- data/lib/http/connection.rb +126 -97
- data/lib/http/content_type.rb +61 -6
- data/lib/http/errors.rb +25 -1
- data/lib/http/feature.rb +65 -5
- data/lib/http/features/auto_deflate.rb +124 -17
- data/lib/http/features/auto_inflate.rb +38 -15
- data/lib/http/features/caching/entry.rb +178 -0
- data/lib/http/features/caching/in_memory_store.rb +63 -0
- data/lib/http/features/caching.rb +216 -0
- data/lib/http/features/digest_auth.rb +234 -0
- data/lib/http/features/instrumentation.rb +97 -17
- data/lib/http/features/logging.rb +183 -5
- data/lib/http/features/normalize_uri.rb +17 -0
- data/lib/http/features/raise_error.rb +18 -3
- data/lib/http/form_data/composite_io.rb +106 -0
- data/lib/http/form_data/file.rb +95 -0
- data/lib/http/form_data/multipart/param.rb +62 -0
- data/lib/http/form_data/multipart.rb +106 -0
- data/lib/http/form_data/part.rb +52 -0
- data/lib/http/form_data/readable.rb +58 -0
- data/lib/http/form_data/urlencoded.rb +175 -0
- data/lib/http/form_data/version.rb +8 -0
- data/lib/http/form_data.rb +102 -0
- data/lib/http/headers/known.rb +3 -0
- data/lib/http/headers/normalizer.rb +17 -36
- data/lib/http/headers.rb +172 -65
- data/lib/http/mime_type/adapter.rb +24 -9
- data/lib/http/mime_type/json.rb +19 -4
- data/lib/http/mime_type.rb +21 -3
- data/lib/http/options/definitions.rb +189 -0
- data/lib/http/options.rb +172 -125
- data/lib/http/redirector.rb +80 -75
- data/lib/http/request/body.rb +87 -6
- data/lib/http/request/builder.rb +184 -0
- data/lib/http/request/proxy.rb +83 -0
- data/lib/http/request/writer.rb +76 -16
- data/lib/http/request.rb +214 -98
- data/lib/http/response/body.rb +103 -18
- data/lib/http/response/inflater.rb +35 -7
- data/lib/http/response/parser.rb +98 -4
- data/lib/http/response/status/reasons.rb +2 -4
- data/lib/http/response/status.rb +141 -31
- data/lib/http/response.rb +219 -61
- data/lib/http/retriable/delay_calculator.rb +38 -11
- data/lib/http/retriable/errors.rb +21 -0
- data/lib/http/retriable/performer.rb +82 -38
- data/lib/http/session.rb +280 -0
- data/lib/http/timeout/global.rb +147 -34
- data/lib/http/timeout/null.rb +155 -9
- data/lib/http/timeout/per_operation.rb +139 -18
- data/lib/http/uri/normalizer.rb +82 -0
- data/lib/http/uri/parsing.rb +182 -0
- data/lib/http/uri.rb +289 -124
- data/lib/http/version.rb +2 -1
- data/lib/http.rb +11 -2
- data/sig/deps.rbs +122 -0
- data/sig/http.rbs +1619 -0
- data/test/http/base64_test.rb +28 -0
- data/test/http/client_test.rb +739 -0
- data/test/http/connection_test.rb +1533 -0
- data/test/http/content_type_test.rb +190 -0
- data/test/http/errors_test.rb +28 -0
- data/test/http/feature_test.rb +49 -0
- data/test/http/features/auto_deflate_test.rb +317 -0
- data/test/http/features/auto_inflate_test.rb +213 -0
- data/test/http/features/caching_test.rb +942 -0
- data/test/http/features/digest_auth_test.rb +996 -0
- data/test/http/features/instrumentation_test.rb +246 -0
- data/test/http/features/logging_test.rb +654 -0
- data/test/http/features/normalize_uri_test.rb +41 -0
- data/test/http/features/raise_error_test.rb +77 -0
- data/test/http/form_data/composite_io_test.rb +215 -0
- data/test/http/form_data/file_test.rb +255 -0
- data/test/http/form_data/fixtures/the-http-gem.info +1 -0
- data/test/http/form_data/multipart_test.rb +303 -0
- data/test/http/form_data/part_test.rb +90 -0
- data/test/http/form_data/urlencoded_test.rb +164 -0
- data/test/http/form_data_test.rb +232 -0
- data/test/http/headers/normalizer_test.rb +93 -0
- data/test/http/headers_test.rb +888 -0
- data/test/http/mime_type/json_test.rb +39 -0
- data/test/http/mime_type_test.rb +150 -0
- data/test/http/options/base_uri_test.rb +148 -0
- data/test/http/options/body_test.rb +21 -0
- data/test/http/options/features_test.rb +38 -0
- data/test/http/options/form_test.rb +21 -0
- data/test/http/options/headers_test.rb +32 -0
- data/test/http/options/json_test.rb +21 -0
- data/test/http/options/merge_test.rb +78 -0
- data/test/http/options/new_test.rb +37 -0
- data/test/http/options/proxy_test.rb +32 -0
- data/test/http/options_test.rb +575 -0
- data/test/http/redirector_test.rb +639 -0
- data/test/http/request/body_test.rb +318 -0
- data/test/http/request/builder_test.rb +623 -0
- data/test/http/request/writer_test.rb +391 -0
- data/test/http/request_test.rb +1733 -0
- data/test/http/response/body_test.rb +292 -0
- data/test/http/response/parser_test.rb +105 -0
- data/test/http/response/status_test.rb +322 -0
- data/test/http/response_test.rb +502 -0
- data/test/http/retriable/delay_calculator_test.rb +194 -0
- data/test/http/retriable/errors_test.rb +71 -0
- data/test/http/retriable/performer_test.rb +551 -0
- data/test/http/session_test.rb +424 -0
- data/test/http/timeout/global_test.rb +239 -0
- data/test/http/timeout/null_test.rb +218 -0
- data/test/http/timeout/per_operation_test.rb +220 -0
- data/test/http/uri/normalizer_test.rb +89 -0
- data/test/http/uri_test.rb +1140 -0
- data/test/http/version_test.rb +15 -0
- data/test/http_test.rb +818 -0
- data/test/regression_tests.rb +27 -0
- data/test/support/dummy_server/encoding_routes.rb +47 -0
- data/test/support/dummy_server/routes.rb +201 -0
- data/test/support/dummy_server/servlet.rb +81 -0
- data/test/support/dummy_server.rb +200 -0
- data/{spec → test}/support/fakeio.rb +2 -2
- data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
- data/test/support/http_handling_shared/timeout_tests.rb +134 -0
- data/test/support/http_handling_shared.rb +11 -0
- data/test/support/proxy_server.rb +207 -0
- data/test/support/servers/runner.rb +67 -0
- data/{spec → test}/support/simplecov.rb +11 -2
- data/test/support/ssl_helper.rb +108 -0
- data/test/test_helper.rb +38 -0
- metadata +108 -168
- data/.github/workflows/ci.yml +0 -67
- data/.gitignore +0 -15
- data/.rspec +0 -1
- data/.rubocop/layout.yml +0 -8
- data/.rubocop/metrics.yml +0 -4
- data/.rubocop/rspec.yml +0 -9
- data/.rubocop/style.yml +0 -32
- data/.rubocop.yml +0 -11
- data/.rubocop_todo.yml +0 -219
- data/.yardopts +0 -2
- data/CHANGES_OLD.md +0 -1002
- data/Gemfile +0 -51
- data/Guardfile +0 -18
- data/Rakefile +0 -64
- data/lib/http/headers/mixin.rb +0 -34
- data/lib/http/retriable/client.rb +0 -37
- data/logo.png +0 -0
- data/spec/lib/http/client_spec.rb +0 -556
- data/spec/lib/http/connection_spec.rb +0 -88
- data/spec/lib/http/content_type_spec.rb +0 -47
- data/spec/lib/http/features/auto_deflate_spec.rb +0 -77
- data/spec/lib/http/features/auto_inflate_spec.rb +0 -86
- data/spec/lib/http/features/instrumentation_spec.rb +0 -81
- data/spec/lib/http/features/logging_spec.rb +0 -65
- data/spec/lib/http/features/raise_error_spec.rb +0 -62
- data/spec/lib/http/headers/mixin_spec.rb +0 -36
- data/spec/lib/http/headers/normalizer_spec.rb +0 -52
- data/spec/lib/http/headers_spec.rb +0 -527
- data/spec/lib/http/options/body_spec.rb +0 -15
- data/spec/lib/http/options/features_spec.rb +0 -33
- data/spec/lib/http/options/form_spec.rb +0 -15
- data/spec/lib/http/options/headers_spec.rb +0 -24
- data/spec/lib/http/options/json_spec.rb +0 -15
- data/spec/lib/http/options/merge_spec.rb +0 -68
- data/spec/lib/http/options/new_spec.rb +0 -30
- data/spec/lib/http/options/proxy_spec.rb +0 -20
- data/spec/lib/http/options_spec.rb +0 -13
- data/spec/lib/http/redirector_spec.rb +0 -530
- data/spec/lib/http/request/body_spec.rb +0 -211
- data/spec/lib/http/request/writer_spec.rb +0 -121
- data/spec/lib/http/request_spec.rb +0 -234
- data/spec/lib/http/response/body_spec.rb +0 -85
- data/spec/lib/http/response/parser_spec.rb +0 -74
- data/spec/lib/http/response/status_spec.rb +0 -253
- data/spec/lib/http/response_spec.rb +0 -262
- data/spec/lib/http/retriable/delay_calculator_spec.rb +0 -69
- data/spec/lib/http/retriable/performer_spec.rb +0 -302
- data/spec/lib/http/uri/normalizer_spec.rb +0 -95
- data/spec/lib/http/uri_spec.rb +0 -71
- data/spec/lib/http_spec.rb +0 -535
- data/spec/regression_specs.rb +0 -24
- data/spec/spec_helper.rb +0 -89
- data/spec/support/black_hole.rb +0 -13
- data/spec/support/dummy_server/servlet.rb +0 -203
- data/spec/support/dummy_server.rb +0 -44
- data/spec/support/fuubar.rb +0 -21
- data/spec/support/http_handling_shared.rb +0 -190
- data/spec/support/proxy_server.rb +0 -39
- data/spec/support/servers/config.rb +0 -11
- data/spec/support/servers/runner.rb +0 -19
- data/spec/support/ssl_helper.rb +0 -104
- /data/{spec → test}/support/capture_warning.rb +0 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require "logger"
|
|
5
|
+
|
|
6
|
+
class HTTPFeaturesLoggingTest < Minitest::Test
|
|
7
|
+
cover "HTTP::Features::Logging*"
|
|
8
|
+
|
|
9
|
+
def logdev
|
|
10
|
+
@logdev ||= StringIO.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def feature
|
|
14
|
+
@feature ||= begin
|
|
15
|
+
logger = Logger.new(logdev)
|
|
16
|
+
logger.formatter = ->(severity, _, _, message) { format("** %s **\n%s\n", severity, message) }
|
|
17
|
+
HTTP::Features::Logging.new(logger: logger)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# -- NullLogger --
|
|
22
|
+
|
|
23
|
+
def test_null_logger_responds_to_log_level_methods
|
|
24
|
+
null_logger = HTTP::Features::Logging::NullLogger.new
|
|
25
|
+
|
|
26
|
+
%i[fatal error warn info debug].each do |level|
|
|
27
|
+
assert_nil null_logger.public_send(level, "msg")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_null_logger_reports_all_levels_as_enabled
|
|
32
|
+
null_logger = HTTP::Features::Logging::NullLogger.new
|
|
33
|
+
|
|
34
|
+
%i[fatal? error? warn? info? debug?].each do |level|
|
|
35
|
+
assert null_logger.public_send(level)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# -- default initialization --
|
|
40
|
+
|
|
41
|
+
def test_default_initialization_uses_null_logger
|
|
42
|
+
f = HTTP::Features::Logging.new
|
|
43
|
+
|
|
44
|
+
assert_instance_of HTTP::Features::Logging::NullLogger, f.logger
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# -- logging the request --
|
|
48
|
+
|
|
49
|
+
def test_logging_the_request_logs_the_request
|
|
50
|
+
req = HTTP::Request.new(
|
|
51
|
+
verb: :post,
|
|
52
|
+
uri: "https://example.com/",
|
|
53
|
+
headers: { accept: "application/json" },
|
|
54
|
+
body: '{"hello": "world!"}'
|
|
55
|
+
)
|
|
56
|
+
feature.wrap_request(req)
|
|
57
|
+
|
|
58
|
+
expected = <<~OUTPUT
|
|
59
|
+
** INFO **
|
|
60
|
+
> POST https://example.com/
|
|
61
|
+
** DEBUG **
|
|
62
|
+
Accept: application/json
|
|
63
|
+
Host: example.com
|
|
64
|
+
User-Agent: http.rb/#{HTTP::VERSION}
|
|
65
|
+
|
|
66
|
+
{"hello": "world!"}
|
|
67
|
+
OUTPUT
|
|
68
|
+
assert_equal expected, logdev.string
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_logging_the_request_returns_the_request
|
|
72
|
+
req = HTTP::Request.new(
|
|
73
|
+
verb: :post,
|
|
74
|
+
uri: "https://example.com/",
|
|
75
|
+
headers: { accept: "application/json" },
|
|
76
|
+
body: '{"hello": "world!"}'
|
|
77
|
+
)
|
|
78
|
+
result = feature.wrap_request(req)
|
|
79
|
+
|
|
80
|
+
assert_same req, result
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# -- logging request with string header names --
|
|
84
|
+
|
|
85
|
+
def test_logging_request_preserves_original_header_names_without_canonicalization
|
|
86
|
+
req = HTTP::Request.new(
|
|
87
|
+
verb: :post,
|
|
88
|
+
uri: "https://example.com/",
|
|
89
|
+
headers: { "X-Custom_Header" => "value1", "X-Another.Header" => "value2" },
|
|
90
|
+
body: "hello"
|
|
91
|
+
)
|
|
92
|
+
feature.wrap_request(req)
|
|
93
|
+
|
|
94
|
+
expected = <<~OUTPUT
|
|
95
|
+
** INFO **
|
|
96
|
+
> POST https://example.com/
|
|
97
|
+
** DEBUG **
|
|
98
|
+
X-Custom_Header: value1
|
|
99
|
+
X-Another.Header: value2
|
|
100
|
+
Host: example.com
|
|
101
|
+
User-Agent: http.rb/#{HTTP::VERSION}
|
|
102
|
+
|
|
103
|
+
hello
|
|
104
|
+
OUTPUT
|
|
105
|
+
assert_equal expected, logdev.string
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# -- logging request with non-loggable IO body --
|
|
109
|
+
|
|
110
|
+
def test_logging_request_with_io_body_logs_headers_without_body
|
|
111
|
+
req = HTTP::Request.new(
|
|
112
|
+
verb: :post,
|
|
113
|
+
uri: "https://example.com/upload",
|
|
114
|
+
headers: { content_type: "application/octet-stream" },
|
|
115
|
+
body: FakeIO.new("binary data")
|
|
116
|
+
)
|
|
117
|
+
feature.wrap_request(req)
|
|
118
|
+
|
|
119
|
+
expected = <<~OUTPUT
|
|
120
|
+
** INFO **
|
|
121
|
+
> POST https://example.com/upload
|
|
122
|
+
** DEBUG **
|
|
123
|
+
Content-Type: application/octet-stream
|
|
124
|
+
Host: example.com
|
|
125
|
+
User-Agent: http.rb/#{HTTP::VERSION}
|
|
126
|
+
OUTPUT
|
|
127
|
+
assert_equal expected, logdev.string
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# -- logging request with binary-encoded string body --
|
|
131
|
+
|
|
132
|
+
def test_logging_request_with_binary_body_logs_binary_stats
|
|
133
|
+
binary_data = String.new("\x89PNG\r\n", encoding: Encoding::BINARY)
|
|
134
|
+
req = HTTP::Request.new(
|
|
135
|
+
verb: :post,
|
|
136
|
+
uri: "https://example.com/upload",
|
|
137
|
+
headers: { content_type: "application/octet-stream" },
|
|
138
|
+
body: binary_data
|
|
139
|
+
)
|
|
140
|
+
feature.wrap_request(req)
|
|
141
|
+
|
|
142
|
+
assert_includes logdev.string, "BINARY DATA (6 bytes)"
|
|
143
|
+
refute_includes logdev.string, "\x89PNG"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# -- logging the response: with a string body --
|
|
147
|
+
|
|
148
|
+
def test_logging_response_with_string_body_logs_response_with_body
|
|
149
|
+
resp = HTTP::Response.new(
|
|
150
|
+
version: "1.1",
|
|
151
|
+
status: 200,
|
|
152
|
+
headers: { content_type: "application/json" },
|
|
153
|
+
body: '{"success": true}',
|
|
154
|
+
request: HTTP::Request.new(verb: :get, uri: "https://example.com")
|
|
155
|
+
)
|
|
156
|
+
feature.wrap_response(resp)
|
|
157
|
+
|
|
158
|
+
expected = <<~OUTPUT
|
|
159
|
+
** INFO **
|
|
160
|
+
< 200 OK
|
|
161
|
+
** DEBUG **
|
|
162
|
+
Content-Type: application/json
|
|
163
|
+
|
|
164
|
+
{"success": true}
|
|
165
|
+
OUTPUT
|
|
166
|
+
assert_equal expected, logdev.string
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def test_logging_response_with_string_body_returns_same_response_object
|
|
170
|
+
resp = HTTP::Response.new(
|
|
171
|
+
version: "1.1",
|
|
172
|
+
status: 200,
|
|
173
|
+
headers: { content_type: "application/json" },
|
|
174
|
+
body: '{"success": true}',
|
|
175
|
+
request: HTTP::Request.new(verb: :get, uri: "https://example.com")
|
|
176
|
+
)
|
|
177
|
+
result = feature.wrap_response(resp)
|
|
178
|
+
|
|
179
|
+
assert_same resp, result
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# -- logging the response: with a streaming body --
|
|
183
|
+
|
|
184
|
+
def build_streaming_response
|
|
185
|
+
chunks = %w[{"suc cess" :true}]
|
|
186
|
+
connection_obj = Object.new
|
|
187
|
+
stream = fake(
|
|
188
|
+
readpartial: proc { chunks.shift or raise EOFError },
|
|
189
|
+
close: nil,
|
|
190
|
+
closed?: true,
|
|
191
|
+
connection: connection_obj
|
|
192
|
+
)
|
|
193
|
+
body = HTTP::Response::Body.new(stream, encoding: Encoding::UTF_8)
|
|
194
|
+
request_obj = HTTP::Request.new(verb: :get, uri: "https://example.com")
|
|
195
|
+
response = HTTP::Response.new(
|
|
196
|
+
version: "1.1",
|
|
197
|
+
status: 200,
|
|
198
|
+
headers: { content_type: "application/json" },
|
|
199
|
+
proxy_headers: { "X-Via" => "proxy" },
|
|
200
|
+
body: body,
|
|
201
|
+
request: request_obj
|
|
202
|
+
)
|
|
203
|
+
[response, body, request_obj, connection_obj]
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def test_logging_streaming_response_does_not_consume_the_body
|
|
207
|
+
response, = build_streaming_response
|
|
208
|
+
wrapped = feature.wrap_response(response)
|
|
209
|
+
|
|
210
|
+
assert_nil wrapped.body.instance_variable_get(:@streaming)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def test_logging_streaming_response_logs_body_chunks_as_streamed
|
|
214
|
+
response, = build_streaming_response
|
|
215
|
+
wrapped = feature.wrap_response(response)
|
|
216
|
+
wrapped.body.to_s
|
|
217
|
+
|
|
218
|
+
assert_includes logdev.string, '{"suc'
|
|
219
|
+
assert_includes logdev.string, 'cess"'
|
|
220
|
+
assert_includes logdev.string, ":true}"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def test_logging_streaming_response_preserves_full_body_content
|
|
224
|
+
response, = build_streaming_response
|
|
225
|
+
wrapped = feature.wrap_response(response)
|
|
226
|
+
|
|
227
|
+
assert_equal '{"success":true}', wrapped.body.to_s
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def test_logging_streaming_response_returns_new_response_with_same_status
|
|
231
|
+
response, = build_streaming_response
|
|
232
|
+
wrapped = feature.wrap_response(response)
|
|
233
|
+
|
|
234
|
+
assert_equal response.status.code, wrapped.status.code
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def test_logging_streaming_response_returns_new_response_with_same_version
|
|
238
|
+
response, = build_streaming_response
|
|
239
|
+
wrapped = feature.wrap_response(response)
|
|
240
|
+
|
|
241
|
+
assert_equal "1.1", wrapped.version
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def test_logging_streaming_response_returns_new_response_with_same_headers
|
|
245
|
+
response, = build_streaming_response
|
|
246
|
+
wrapped = feature.wrap_response(response)
|
|
247
|
+
|
|
248
|
+
assert_equal response.headers.to_h, wrapped.headers.to_h
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def test_logging_streaming_response_returns_new_response_with_same_proxy_headers
|
|
252
|
+
response, = build_streaming_response
|
|
253
|
+
wrapped = feature.wrap_response(response)
|
|
254
|
+
|
|
255
|
+
assert_equal({ "X-Via" => "proxy" }, wrapped.proxy_headers.to_h)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def test_logging_streaming_response_returns_new_response_preserving_the_request
|
|
259
|
+
response, _, request_obj, = build_streaming_response
|
|
260
|
+
wrapped = feature.wrap_response(response)
|
|
261
|
+
|
|
262
|
+
assert_same request_obj, wrapped.request
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def test_logging_streaming_response_returns_different_response_object
|
|
266
|
+
response, = build_streaming_response
|
|
267
|
+
wrapped = feature.wrap_response(response)
|
|
268
|
+
|
|
269
|
+
refute_same response, wrapped
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def test_logging_streaming_response_preserves_body_encoding
|
|
273
|
+
response, = build_streaming_response
|
|
274
|
+
wrapped = feature.wrap_response(response)
|
|
275
|
+
|
|
276
|
+
assert_equal Encoding::UTF_8, wrapped.body.encoding
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def test_logging_streaming_response_wraps_underlying_stream_not_body_object
|
|
280
|
+
response, body, = build_streaming_response
|
|
281
|
+
wrapped = feature.wrap_response(response)
|
|
282
|
+
wrapped.body.to_s
|
|
283
|
+
|
|
284
|
+
assert_nil body.instance_variable_get(:@streaming)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def test_logging_streaming_response_logs_headers
|
|
288
|
+
response, = build_streaming_response
|
|
289
|
+
feature.wrap_response(response)
|
|
290
|
+
|
|
291
|
+
assert_includes logdev.string, "Content-Type: application/json"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def test_logging_streaming_response_preserves_connection_on_wrapped_response
|
|
295
|
+
response, _, _, connection_obj = build_streaming_response
|
|
296
|
+
wrapped = feature.wrap_response(response)
|
|
297
|
+
|
|
298
|
+
assert_same connection_obj, wrapped.connection
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# -- response with body that does not respond to :encoding --
|
|
302
|
+
|
|
303
|
+
def test_logging_response_with_non_encoding_body_logs_without_error
|
|
304
|
+
body_obj = Object.new
|
|
305
|
+
body_obj.define_singleton_method(:to_s) { "inline content" }
|
|
306
|
+
resp = HTTP::Response.new(
|
|
307
|
+
version: "1.1",
|
|
308
|
+
status: 200,
|
|
309
|
+
headers: { content_type: "text/plain" },
|
|
310
|
+
body: body_obj,
|
|
311
|
+
request: HTTP::Request.new(verb: :get, uri: "https://example.com")
|
|
312
|
+
)
|
|
313
|
+
feature.wrap_response(resp)
|
|
314
|
+
|
|
315
|
+
assert_includes logdev.string, "inline content"
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def test_logging_response_with_non_encoding_body_returns_same_response_object
|
|
319
|
+
body_obj = Object.new
|
|
320
|
+
body_obj.define_singleton_method(:to_s) { "inline content" }
|
|
321
|
+
resp = HTTP::Response.new(
|
|
322
|
+
version: "1.1",
|
|
323
|
+
status: 200,
|
|
324
|
+
headers: { content_type: "text/plain" },
|
|
325
|
+
body: body_obj,
|
|
326
|
+
request: HTTP::Request.new(verb: :get, uri: "https://example.com")
|
|
327
|
+
)
|
|
328
|
+
result = feature.wrap_response(resp)
|
|
329
|
+
|
|
330
|
+
assert_same resp, result
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# -- response with binary string body --
|
|
334
|
+
|
|
335
|
+
def test_logging_response_with_binary_string_body_logs_binary_stats
|
|
336
|
+
binary_data = String.new("\x89PNG\r\n\x1A\n", encoding: Encoding::BINARY)
|
|
337
|
+
resp = HTTP::Response.new(
|
|
338
|
+
version: "1.1",
|
|
339
|
+
status: 200,
|
|
340
|
+
headers: { content_type: "application/octet-stream" },
|
|
341
|
+
body: binary_data,
|
|
342
|
+
request: HTTP::Request.new(verb: :get, uri: "https://example.com")
|
|
343
|
+
)
|
|
344
|
+
feature.wrap_response(resp)
|
|
345
|
+
|
|
346
|
+
assert_includes logdev.string, "BINARY DATA (8 bytes)"
|
|
347
|
+
refute_includes logdev.string, "\x89PNG"
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def test_logging_response_with_binary_string_body_includes_headers
|
|
351
|
+
binary_data = String.new("\x89PNG\r\n\x1A\n", encoding: Encoding::BINARY)
|
|
352
|
+
resp = HTTP::Response.new(
|
|
353
|
+
version: "1.1",
|
|
354
|
+
status: 200,
|
|
355
|
+
headers: { content_type: "application/octet-stream" },
|
|
356
|
+
body: binary_data,
|
|
357
|
+
request: HTTP::Request.new(verb: :get, uri: "https://example.com")
|
|
358
|
+
)
|
|
359
|
+
feature.wrap_response(resp)
|
|
360
|
+
|
|
361
|
+
assert_includes logdev.string, "Content-Type: application/octet-stream"
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# -- response with binary streaming body --
|
|
365
|
+
|
|
366
|
+
def test_logging_response_with_binary_streaming_body_logs_binary_stats
|
|
367
|
+
chunks = [String.new("\x89PNG\r\n", encoding: Encoding::BINARY)]
|
|
368
|
+
stream = fake(
|
|
369
|
+
readpartial: proc { chunks.shift or raise EOFError },
|
|
370
|
+
close: nil,
|
|
371
|
+
closed?: true
|
|
372
|
+
)
|
|
373
|
+
body = HTTP::Response::Body.new(stream)
|
|
374
|
+
resp = HTTP::Response.new(
|
|
375
|
+
version: "1.1",
|
|
376
|
+
status: 200,
|
|
377
|
+
headers: { content_type: "application/octet-stream" },
|
|
378
|
+
body: body,
|
|
379
|
+
request: HTTP::Request.new(verb: :get, uri: "https://example.com")
|
|
380
|
+
)
|
|
381
|
+
wrapped = feature.wrap_response(resp)
|
|
382
|
+
wrapped.body.to_s
|
|
383
|
+
|
|
384
|
+
assert_includes logdev.string, "BINARY DATA (6 bytes)"
|
|
385
|
+
refute_includes logdev.string, "\x89PNG"
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def test_logging_response_with_binary_streaming_body_preserves_full_content
|
|
389
|
+
chunks = [String.new("\x89PNG\r\n", encoding: Encoding::BINARY)]
|
|
390
|
+
stream = fake(
|
|
391
|
+
readpartial: proc { chunks.shift or raise EOFError },
|
|
392
|
+
close: nil,
|
|
393
|
+
closed?: true
|
|
394
|
+
)
|
|
395
|
+
body = HTTP::Response::Body.new(stream)
|
|
396
|
+
resp = HTTP::Response.new(
|
|
397
|
+
version: "1.1",
|
|
398
|
+
status: 200,
|
|
399
|
+
headers: { content_type: "application/octet-stream" },
|
|
400
|
+
body: body,
|
|
401
|
+
request: HTTP::Request.new(verb: :get, uri: "https://example.com")
|
|
402
|
+
)
|
|
403
|
+
wrapped = feature.wrap_response(resp)
|
|
404
|
+
|
|
405
|
+
assert_equal String.new("\x89PNG\r\n", encoding: Encoding::BINARY), wrapped.body.to_s
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# -- response with Response::Body subclass --
|
|
409
|
+
|
|
410
|
+
def test_logging_response_with_body_subclass_treats_same_as_response_body
|
|
411
|
+
subclass = Class.new(HTTP::Response::Body)
|
|
412
|
+
chunks = %w[hello world]
|
|
413
|
+
connection_obj = Object.new
|
|
414
|
+
stream = fake(
|
|
415
|
+
readpartial: proc { chunks.shift or raise EOFError },
|
|
416
|
+
close: nil,
|
|
417
|
+
closed?: true,
|
|
418
|
+
connection: connection_obj
|
|
419
|
+
)
|
|
420
|
+
body = subclass.new(stream, encoding: Encoding::UTF_8)
|
|
421
|
+
resp = HTTP::Response.new(
|
|
422
|
+
version: "1.1",
|
|
423
|
+
status: 200,
|
|
424
|
+
headers: { content_type: "text/plain" },
|
|
425
|
+
body: body,
|
|
426
|
+
request: HTTP::Request.new(verb: :get, uri: "https://example.com")
|
|
427
|
+
)
|
|
428
|
+
wrapped = feature.wrap_response(resp)
|
|
429
|
+
|
|
430
|
+
refute_same resp, wrapped
|
|
431
|
+
assert_equal "helloworld", wrapped.body.to_s
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# -- when logger level is above debug --
|
|
435
|
+
|
|
436
|
+
def test_logging_when_logger_level_above_debug_does_not_wrap_body
|
|
437
|
+
dev = StringIO.new
|
|
438
|
+
logger = Logger.new(dev)
|
|
439
|
+
logger.formatter = ->(severity, _, _, message) { format("** %s **\n%s\n", severity, message) }
|
|
440
|
+
logger.level = Logger::INFO
|
|
441
|
+
|
|
442
|
+
feat = HTTP::Features::Logging.new(logger: logger)
|
|
443
|
+
stream = fake(
|
|
444
|
+
readpartial: proc { raise EOFError },
|
|
445
|
+
close: nil,
|
|
446
|
+
closed?: true
|
|
447
|
+
)
|
|
448
|
+
body = HTTP::Response::Body.new(stream)
|
|
449
|
+
resp = HTTP::Response.new(
|
|
450
|
+
version: "1.1",
|
|
451
|
+
status: 200,
|
|
452
|
+
headers: { content_type: "text/plain" },
|
|
453
|
+
body: body,
|
|
454
|
+
request: HTTP::Request.new(verb: :get, uri: "https://example.com")
|
|
455
|
+
)
|
|
456
|
+
wrapped = feat.wrap_response(resp)
|
|
457
|
+
|
|
458
|
+
assert_same resp, wrapped
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# -- binary_formatter validation --
|
|
462
|
+
|
|
463
|
+
def test_binary_formatter_raises_for_unsupported_values
|
|
464
|
+
err = assert_raises(ArgumentError) do
|
|
465
|
+
HTTP::Features::Logging.new(binary_formatter: :unsupported)
|
|
466
|
+
end
|
|
467
|
+
assert_includes err.message, "binary_formatter must be :stats, :base64, or a callable"
|
|
468
|
+
assert_includes err.message, ":unsupported"
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def test_binary_formatter_accepts_stats
|
|
472
|
+
HTTP::Features::Logging.new(binary_formatter: :stats)
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def test_binary_formatter_accepts_base64
|
|
476
|
+
HTTP::Features::Logging.new(binary_formatter: :base64)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def test_binary_formatter_accepts_a_callable
|
|
480
|
+
HTTP::Features::Logging.new(binary_formatter: ->(data) { data })
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# -- binary_formatter :base64 --
|
|
484
|
+
|
|
485
|
+
def test_binary_formatter_base64_logs_base64_encoded_body
|
|
486
|
+
dev = StringIO.new
|
|
487
|
+
logger = Logger.new(dev)
|
|
488
|
+
logger.formatter = ->(severity, _, _, message) { format("** %s **\n%s\n", severity, message) }
|
|
489
|
+
feat = HTTP::Features::Logging.new(logger: logger, binary_formatter: :base64)
|
|
490
|
+
|
|
491
|
+
binary_data = String.new("\x89PNG\r\n\x1A\n", encoding: Encoding::BINARY)
|
|
492
|
+
resp = HTTP::Response.new(
|
|
493
|
+
version: "1.1",
|
|
494
|
+
status: 200,
|
|
495
|
+
headers: { content_type: "image/png" },
|
|
496
|
+
body: binary_data,
|
|
497
|
+
request: HTTP::Request.new(verb: :get, uri: "https://example.com")
|
|
498
|
+
)
|
|
499
|
+
feat.wrap_response(resp)
|
|
500
|
+
|
|
501
|
+
assert_includes dev.string, "BINARY DATA (8 bytes)"
|
|
502
|
+
assert_includes dev.string, [binary_data].pack("m0")
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def test_binary_formatter_base64_encodes_streaming_binary_chunks
|
|
506
|
+
dev = StringIO.new
|
|
507
|
+
logger = Logger.new(dev)
|
|
508
|
+
logger.formatter = ->(severity, _, _, message) { format("** %s **\n%s\n", severity, message) }
|
|
509
|
+
feat = HTTP::Features::Logging.new(logger: logger, binary_formatter: :base64)
|
|
510
|
+
|
|
511
|
+
chunks = [String.new("\xFF\xD8\xFF", encoding: Encoding::BINARY)]
|
|
512
|
+
stream = fake(
|
|
513
|
+
readpartial: proc { chunks.shift or raise EOFError },
|
|
514
|
+
close: nil,
|
|
515
|
+
closed?: true
|
|
516
|
+
)
|
|
517
|
+
body = HTTP::Response::Body.new(stream)
|
|
518
|
+
resp = HTTP::Response.new(
|
|
519
|
+
version: "1.1",
|
|
520
|
+
status: 200,
|
|
521
|
+
headers: { content_type: "image/jpeg" },
|
|
522
|
+
body: body,
|
|
523
|
+
request: HTTP::Request.new(verb: :get, uri: "https://example.com")
|
|
524
|
+
)
|
|
525
|
+
wrapped = feat.wrap_response(resp)
|
|
526
|
+
wrapped.body.to_s
|
|
527
|
+
|
|
528
|
+
assert_includes dev.string, "BINARY DATA (3 bytes)"
|
|
529
|
+
assert_includes dev.string, ["\xFF\xD8\xFF"].pack("m0")
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# -- binary_formatter Proc --
|
|
533
|
+
|
|
534
|
+
def test_binary_formatter_proc_uses_custom_formatter
|
|
535
|
+
dev = StringIO.new
|
|
536
|
+
logger = Logger.new(dev)
|
|
537
|
+
logger.formatter = ->(severity, _, _, message) { format("** %s **\n%s\n", severity, message) }
|
|
538
|
+
formatter = ->(data) { "[#{data.bytesize} bytes hidden]" }
|
|
539
|
+
feat = HTTP::Features::Logging.new(logger: logger, binary_formatter: formatter)
|
|
540
|
+
|
|
541
|
+
binary_data = String.new("\x00\x01\x02", encoding: Encoding::BINARY)
|
|
542
|
+
resp = HTTP::Response.new(
|
|
543
|
+
version: "1.1",
|
|
544
|
+
status: 200,
|
|
545
|
+
headers: { content_type: "application/octet-stream" },
|
|
546
|
+
body: binary_data,
|
|
547
|
+
request: HTTP::Request.new(verb: :get, uri: "https://example.com")
|
|
548
|
+
)
|
|
549
|
+
feat.wrap_response(resp)
|
|
550
|
+
|
|
551
|
+
assert_includes dev.string, "[3 bytes hidden]"
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def test_binary_formatter_proc_uses_custom_formatter_for_streaming_chunks
|
|
555
|
+
dev = StringIO.new
|
|
556
|
+
logger = Logger.new(dev)
|
|
557
|
+
logger.formatter = ->(severity, _, _, message) { format("** %s **\n%s\n", severity, message) }
|
|
558
|
+
formatter = ->(data) { "[#{data.bytesize} bytes hidden]" }
|
|
559
|
+
feat = HTTP::Features::Logging.new(logger: logger, binary_formatter: formatter)
|
|
560
|
+
|
|
561
|
+
chunks = [String.new("\xDE\xAD", encoding: Encoding::BINARY)]
|
|
562
|
+
stream = fake(
|
|
563
|
+
readpartial: proc { chunks.shift or raise EOFError },
|
|
564
|
+
close: nil,
|
|
565
|
+
closed?: true
|
|
566
|
+
)
|
|
567
|
+
body = HTTP::Response::Body.new(stream)
|
|
568
|
+
resp = HTTP::Response.new(
|
|
569
|
+
version: "1.1",
|
|
570
|
+
status: 200,
|
|
571
|
+
headers: { content_type: "application/octet-stream" },
|
|
572
|
+
body: body,
|
|
573
|
+
request: HTTP::Request.new(verb: :get, uri: "https://example.com")
|
|
574
|
+
)
|
|
575
|
+
wrapped = feat.wrap_response(resp)
|
|
576
|
+
wrapped.body.to_s
|
|
577
|
+
|
|
578
|
+
assert_includes dev.string, "[2 bytes hidden]"
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
# -- BodyLogger --
|
|
582
|
+
|
|
583
|
+
def test_body_logger_passes_through_chunks_and_logs_them
|
|
584
|
+
dev = StringIO.new
|
|
585
|
+
logger = Logger.new(dev)
|
|
586
|
+
logger.formatter = ->(severity, _, _, message) { format("** %s **\n%s\n", severity, message) }
|
|
587
|
+
|
|
588
|
+
chunks = %w[hello world]
|
|
589
|
+
stream = fake(readpartial: proc { chunks.shift or raise EOFError })
|
|
590
|
+
body_logger = HTTP::Features::Logging::BodyLogger.new(stream, logger)
|
|
591
|
+
|
|
592
|
+
assert_equal "hello", body_logger.readpartial
|
|
593
|
+
assert_equal "world", body_logger.readpartial
|
|
594
|
+
assert_raises(EOFError) { body_logger.readpartial }
|
|
595
|
+
assert_includes dev.string, "hello"
|
|
596
|
+
assert_includes dev.string, "world"
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def test_body_logger_forwards_arguments_to_the_underlying_stream
|
|
600
|
+
dev = StringIO.new
|
|
601
|
+
logger = Logger.new(dev)
|
|
602
|
+
logger.formatter = ->(severity, _, _, message) { format("** %s **\n%s\n", severity, message) }
|
|
603
|
+
|
|
604
|
+
received_args = nil
|
|
605
|
+
stream = fake(readpartial: proc { |*args|
|
|
606
|
+
received_args = args
|
|
607
|
+
"data"
|
|
608
|
+
})
|
|
609
|
+
body_logger = HTTP::Features::Logging::BodyLogger.new(stream, logger)
|
|
610
|
+
body_logger.readpartial(1024)
|
|
611
|
+
|
|
612
|
+
assert_equal [1024], received_args
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def test_body_logger_applies_formatter_when_provided
|
|
616
|
+
dev = StringIO.new
|
|
617
|
+
logger = Logger.new(dev)
|
|
618
|
+
logger.formatter = ->(severity, _, _, message) { format("** %s **\n%s\n", severity, message) }
|
|
619
|
+
|
|
620
|
+
chunks = %w[hello world]
|
|
621
|
+
stream = fake(readpartial: proc { chunks.shift or raise EOFError })
|
|
622
|
+
formatter = ->(data) { "FORMATTED: #{data}" }
|
|
623
|
+
body_logger = HTTP::Features::Logging::BodyLogger.new(stream, logger, formatter: formatter)
|
|
624
|
+
|
|
625
|
+
assert_equal "hello", body_logger.readpartial
|
|
626
|
+
assert_includes dev.string, "FORMATTED: hello"
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
def test_body_logger_exposes_the_underlying_connection
|
|
630
|
+
dev = StringIO.new
|
|
631
|
+
logger = Logger.new(dev)
|
|
632
|
+
logger.formatter = ->(severity, _, _, message) { format("** %s **\n%s\n", severity, message) }
|
|
633
|
+
|
|
634
|
+
connection = Object.new
|
|
635
|
+
stream = fake(
|
|
636
|
+
readpartial: proc { raise EOFError },
|
|
637
|
+
connection: connection
|
|
638
|
+
)
|
|
639
|
+
body_logger = HTTP::Features::Logging::BodyLogger.new(stream, logger)
|
|
640
|
+
|
|
641
|
+
assert_same connection, body_logger.connection
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def test_body_logger_uses_stream_as_connection_when_stream_has_no_connection_method
|
|
645
|
+
dev = StringIO.new
|
|
646
|
+
logger = Logger.new(dev)
|
|
647
|
+
logger.formatter = ->(severity, _, _, message) { format("** %s **\n%s\n", severity, message) }
|
|
648
|
+
|
|
649
|
+
stream = fake(readpartial: proc { raise EOFError })
|
|
650
|
+
body_logger = HTTP::Features::Logging::BodyLogger.new(stream, logger)
|
|
651
|
+
|
|
652
|
+
assert_same stream, body_logger.connection
|
|
653
|
+
end
|
|
654
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class HTTPFeaturesNormalizeURITest < Minitest::Test
|
|
6
|
+
cover "HTTP::Features::NormalizeUri*"
|
|
7
|
+
|
|
8
|
+
# -- #initialize --
|
|
9
|
+
|
|
10
|
+
def test_initialize_defaults_normalizer_to_http_uri_normalizer
|
|
11
|
+
feature = HTTP::Features::NormalizeUri.new
|
|
12
|
+
|
|
13
|
+
assert_same HTTP::URI::NORMALIZER, feature.normalizer
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_initialize_accepts_a_custom_normalizer
|
|
17
|
+
custom = ->(uri) { uri }
|
|
18
|
+
feature = HTTP::Features::NormalizeUri.new(normalizer: custom)
|
|
19
|
+
|
|
20
|
+
assert_same custom, feature.normalizer
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def test_initialize_is_a_feature
|
|
24
|
+
assert_kind_of HTTP::Feature, HTTP::Features::NormalizeUri.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# -- #normalizer --
|
|
28
|
+
|
|
29
|
+
def test_normalizer_returns_the_normalizer
|
|
30
|
+
custom = ->(uri) { uri }
|
|
31
|
+
feature = HTTP::Features::NormalizeUri.new(normalizer: custom)
|
|
32
|
+
|
|
33
|
+
assert_same custom, feature.normalizer
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# -- .register_feature --
|
|
37
|
+
|
|
38
|
+
def test_register_feature_registers_as_normalize_uri
|
|
39
|
+
assert_equal HTTP::Features::NormalizeUri, HTTP::Options.available_features[:normalize_uri]
|
|
40
|
+
end
|
|
41
|
+
end
|