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.
Files changed (201) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +241 -41
  3. data/LICENSE.txt +1 -1
  4. data/README.md +110 -13
  5. data/UPGRADING.md +491 -0
  6. data/http.gemspec +32 -29
  7. data/lib/http/base64.rb +11 -1
  8. data/lib/http/chainable/helpers.rb +62 -0
  9. data/lib/http/chainable/verbs.rb +136 -0
  10. data/lib/http/chainable.rb +232 -136
  11. data/lib/http/client.rb +158 -127
  12. data/lib/http/connection/internals.rb +141 -0
  13. data/lib/http/connection.rb +126 -97
  14. data/lib/http/content_type.rb +61 -6
  15. data/lib/http/errors.rb +25 -1
  16. data/lib/http/feature.rb +65 -5
  17. data/lib/http/features/auto_deflate.rb +124 -17
  18. data/lib/http/features/auto_inflate.rb +38 -15
  19. data/lib/http/features/caching/entry.rb +178 -0
  20. data/lib/http/features/caching/in_memory_store.rb +63 -0
  21. data/lib/http/features/caching.rb +216 -0
  22. data/lib/http/features/digest_auth.rb +234 -0
  23. data/lib/http/features/instrumentation.rb +97 -17
  24. data/lib/http/features/logging.rb +183 -5
  25. data/lib/http/features/normalize_uri.rb +17 -0
  26. data/lib/http/features/raise_error.rb +18 -3
  27. data/lib/http/form_data/composite_io.rb +106 -0
  28. data/lib/http/form_data/file.rb +95 -0
  29. data/lib/http/form_data/multipart/param.rb +62 -0
  30. data/lib/http/form_data/multipart.rb +106 -0
  31. data/lib/http/form_data/part.rb +52 -0
  32. data/lib/http/form_data/readable.rb +58 -0
  33. data/lib/http/form_data/urlencoded.rb +175 -0
  34. data/lib/http/form_data/version.rb +8 -0
  35. data/lib/http/form_data.rb +102 -0
  36. data/lib/http/headers/known.rb +3 -0
  37. data/lib/http/headers/normalizer.rb +17 -36
  38. data/lib/http/headers.rb +172 -65
  39. data/lib/http/mime_type/adapter.rb +24 -9
  40. data/lib/http/mime_type/json.rb +19 -4
  41. data/lib/http/mime_type.rb +21 -3
  42. data/lib/http/options/definitions.rb +189 -0
  43. data/lib/http/options.rb +172 -125
  44. data/lib/http/redirector.rb +80 -75
  45. data/lib/http/request/body.rb +87 -6
  46. data/lib/http/request/builder.rb +184 -0
  47. data/lib/http/request/proxy.rb +83 -0
  48. data/lib/http/request/writer.rb +76 -16
  49. data/lib/http/request.rb +214 -98
  50. data/lib/http/response/body.rb +103 -18
  51. data/lib/http/response/inflater.rb +35 -7
  52. data/lib/http/response/parser.rb +98 -4
  53. data/lib/http/response/status/reasons.rb +2 -4
  54. data/lib/http/response/status.rb +141 -31
  55. data/lib/http/response.rb +219 -61
  56. data/lib/http/retriable/delay_calculator.rb +38 -11
  57. data/lib/http/retriable/errors.rb +21 -0
  58. data/lib/http/retriable/performer.rb +82 -38
  59. data/lib/http/session.rb +280 -0
  60. data/lib/http/timeout/global.rb +147 -34
  61. data/lib/http/timeout/null.rb +155 -9
  62. data/lib/http/timeout/per_operation.rb +139 -18
  63. data/lib/http/uri/normalizer.rb +82 -0
  64. data/lib/http/uri/parsing.rb +182 -0
  65. data/lib/http/uri.rb +289 -124
  66. data/lib/http/version.rb +2 -1
  67. data/lib/http.rb +11 -2
  68. data/sig/deps.rbs +122 -0
  69. data/sig/http.rbs +1619 -0
  70. data/test/http/base64_test.rb +28 -0
  71. data/test/http/client_test.rb +739 -0
  72. data/test/http/connection_test.rb +1533 -0
  73. data/test/http/content_type_test.rb +190 -0
  74. data/test/http/errors_test.rb +28 -0
  75. data/test/http/feature_test.rb +49 -0
  76. data/test/http/features/auto_deflate_test.rb +317 -0
  77. data/test/http/features/auto_inflate_test.rb +213 -0
  78. data/test/http/features/caching_test.rb +942 -0
  79. data/test/http/features/digest_auth_test.rb +996 -0
  80. data/test/http/features/instrumentation_test.rb +246 -0
  81. data/test/http/features/logging_test.rb +654 -0
  82. data/test/http/features/normalize_uri_test.rb +41 -0
  83. data/test/http/features/raise_error_test.rb +77 -0
  84. data/test/http/form_data/composite_io_test.rb +215 -0
  85. data/test/http/form_data/file_test.rb +255 -0
  86. data/test/http/form_data/fixtures/the-http-gem.info +1 -0
  87. data/test/http/form_data/multipart_test.rb +303 -0
  88. data/test/http/form_data/part_test.rb +90 -0
  89. data/test/http/form_data/urlencoded_test.rb +164 -0
  90. data/test/http/form_data_test.rb +232 -0
  91. data/test/http/headers/normalizer_test.rb +93 -0
  92. data/test/http/headers_test.rb +888 -0
  93. data/test/http/mime_type/json_test.rb +39 -0
  94. data/test/http/mime_type_test.rb +150 -0
  95. data/test/http/options/base_uri_test.rb +148 -0
  96. data/test/http/options/body_test.rb +21 -0
  97. data/test/http/options/features_test.rb +38 -0
  98. data/test/http/options/form_test.rb +21 -0
  99. data/test/http/options/headers_test.rb +32 -0
  100. data/test/http/options/json_test.rb +21 -0
  101. data/test/http/options/merge_test.rb +78 -0
  102. data/test/http/options/new_test.rb +37 -0
  103. data/test/http/options/proxy_test.rb +32 -0
  104. data/test/http/options_test.rb +575 -0
  105. data/test/http/redirector_test.rb +639 -0
  106. data/test/http/request/body_test.rb +318 -0
  107. data/test/http/request/builder_test.rb +623 -0
  108. data/test/http/request/writer_test.rb +391 -0
  109. data/test/http/request_test.rb +1733 -0
  110. data/test/http/response/body_test.rb +292 -0
  111. data/test/http/response/parser_test.rb +105 -0
  112. data/test/http/response/status_test.rb +322 -0
  113. data/test/http/response_test.rb +502 -0
  114. data/test/http/retriable/delay_calculator_test.rb +194 -0
  115. data/test/http/retriable/errors_test.rb +71 -0
  116. data/test/http/retriable/performer_test.rb +551 -0
  117. data/test/http/session_test.rb +424 -0
  118. data/test/http/timeout/global_test.rb +239 -0
  119. data/test/http/timeout/null_test.rb +218 -0
  120. data/test/http/timeout/per_operation_test.rb +220 -0
  121. data/test/http/uri/normalizer_test.rb +89 -0
  122. data/test/http/uri_test.rb +1140 -0
  123. data/test/http/version_test.rb +15 -0
  124. data/test/http_test.rb +818 -0
  125. data/test/regression_tests.rb +27 -0
  126. data/test/support/dummy_server/encoding_routes.rb +47 -0
  127. data/test/support/dummy_server/routes.rb +201 -0
  128. data/test/support/dummy_server/servlet.rb +81 -0
  129. data/test/support/dummy_server.rb +200 -0
  130. data/{spec → test}/support/fakeio.rb +2 -2
  131. data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
  132. data/test/support/http_handling_shared/timeout_tests.rb +134 -0
  133. data/test/support/http_handling_shared.rb +11 -0
  134. data/test/support/proxy_server.rb +207 -0
  135. data/test/support/servers/runner.rb +67 -0
  136. data/{spec → test}/support/simplecov.rb +11 -2
  137. data/test/support/ssl_helper.rb +108 -0
  138. data/test/test_helper.rb +38 -0
  139. metadata +108 -168
  140. data/.github/workflows/ci.yml +0 -67
  141. data/.gitignore +0 -15
  142. data/.rspec +0 -1
  143. data/.rubocop/layout.yml +0 -8
  144. data/.rubocop/metrics.yml +0 -4
  145. data/.rubocop/rspec.yml +0 -9
  146. data/.rubocop/style.yml +0 -32
  147. data/.rubocop.yml +0 -11
  148. data/.rubocop_todo.yml +0 -219
  149. data/.yardopts +0 -2
  150. data/CHANGES_OLD.md +0 -1002
  151. data/Gemfile +0 -51
  152. data/Guardfile +0 -18
  153. data/Rakefile +0 -64
  154. data/lib/http/headers/mixin.rb +0 -34
  155. data/lib/http/retriable/client.rb +0 -37
  156. data/logo.png +0 -0
  157. data/spec/lib/http/client_spec.rb +0 -556
  158. data/spec/lib/http/connection_spec.rb +0 -88
  159. data/spec/lib/http/content_type_spec.rb +0 -47
  160. data/spec/lib/http/features/auto_deflate_spec.rb +0 -77
  161. data/spec/lib/http/features/auto_inflate_spec.rb +0 -86
  162. data/spec/lib/http/features/instrumentation_spec.rb +0 -81
  163. data/spec/lib/http/features/logging_spec.rb +0 -65
  164. data/spec/lib/http/features/raise_error_spec.rb +0 -62
  165. data/spec/lib/http/headers/mixin_spec.rb +0 -36
  166. data/spec/lib/http/headers/normalizer_spec.rb +0 -52
  167. data/spec/lib/http/headers_spec.rb +0 -527
  168. data/spec/lib/http/options/body_spec.rb +0 -15
  169. data/spec/lib/http/options/features_spec.rb +0 -33
  170. data/spec/lib/http/options/form_spec.rb +0 -15
  171. data/spec/lib/http/options/headers_spec.rb +0 -24
  172. data/spec/lib/http/options/json_spec.rb +0 -15
  173. data/spec/lib/http/options/merge_spec.rb +0 -68
  174. data/spec/lib/http/options/new_spec.rb +0 -30
  175. data/spec/lib/http/options/proxy_spec.rb +0 -20
  176. data/spec/lib/http/options_spec.rb +0 -13
  177. data/spec/lib/http/redirector_spec.rb +0 -530
  178. data/spec/lib/http/request/body_spec.rb +0 -211
  179. data/spec/lib/http/request/writer_spec.rb +0 -121
  180. data/spec/lib/http/request_spec.rb +0 -234
  181. data/spec/lib/http/response/body_spec.rb +0 -85
  182. data/spec/lib/http/response/parser_spec.rb +0 -74
  183. data/spec/lib/http/response/status_spec.rb +0 -253
  184. data/spec/lib/http/response_spec.rb +0 -262
  185. data/spec/lib/http/retriable/delay_calculator_spec.rb +0 -69
  186. data/spec/lib/http/retriable/performer_spec.rb +0 -302
  187. data/spec/lib/http/uri/normalizer_spec.rb +0 -95
  188. data/spec/lib/http/uri_spec.rb +0 -71
  189. data/spec/lib/http_spec.rb +0 -535
  190. data/spec/regression_specs.rb +0 -24
  191. data/spec/spec_helper.rb +0 -89
  192. data/spec/support/black_hole.rb +0 -13
  193. data/spec/support/dummy_server/servlet.rb +0 -203
  194. data/spec/support/dummy_server.rb +0 -44
  195. data/spec/support/fuubar.rb +0 -21
  196. data/spec/support/http_handling_shared.rb +0 -190
  197. data/spec/support/proxy_server.rb +0 -39
  198. data/spec/support/servers/config.rb +0 -11
  199. data/spec/support/servers/runner.rb +0 -19
  200. data/spec/support/ssl_helper.rb +0 -104
  201. /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