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,623 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class HTTPRequestBuilderTest < Minitest::Test
6
+ cover "HTTP::Request::Builder*"
7
+
8
+ def build_builder(**option_overrides)
9
+ HTTP::Request::Builder.new(HTTP::Options.new(**option_overrides))
10
+ end
11
+
12
+ # #build basics
13
+
14
+ def test_build_returns_an_http_request
15
+ builder = build_builder
16
+ request = builder.build(:get, "http://example.com/path")
17
+
18
+ assert_kind_of HTTP::Request, request
19
+ end
20
+
21
+ def test_build_sets_the_verb_on_the_request
22
+ builder = build_builder
23
+ request = builder.build(:get, "http://example.com/path")
24
+
25
+ assert_equal :get, request.verb
26
+ end
27
+
28
+ def test_build_sets_the_uri_on_the_request
29
+ builder = build_builder
30
+ request = builder.build(:get, "http://example.com/path")
31
+
32
+ assert_equal "/path", request.uri.path
33
+ end
34
+
35
+ def test_build_sets_connection_close_by_default
36
+ builder = build_builder
37
+ request = builder.build(:get, "http://example.com/path")
38
+
39
+ assert_equal HTTP::Connection::CLOSE, request.headers["Connection"]
40
+ end
41
+
42
+ def test_build_sets_the_proxy_from_options
43
+ opts = HTTP::Options.new(proxy: { proxy_address: "proxy.example.com" })
44
+ b = HTTP::Request::Builder.new(opts)
45
+ req = b.build(:get, "http://example.com/")
46
+
47
+ assert_equal({ proxy_address: "proxy.example.com" }, req.proxy)
48
+ end
49
+
50
+ def test_build_with_persistent_connection_sets_keep_alive
51
+ builder = build_builder(persistent: "http://example.com")
52
+ request = builder.build(:get, "http://example.com/path")
53
+
54
+ assert_equal HTTP::Connection::KEEP_ALIVE, request.headers["Connection"]
55
+ end
56
+
57
+ def test_build_when_uri_has_empty_path_sets_path_to_slash
58
+ builder = build_builder
59
+ request = builder.build(:get, "http://example.com")
60
+
61
+ assert_equal "/", request.uri.path
62
+ end
63
+
64
+ def test_build_when_uri_has_a_non_empty_path_preserves_it
65
+ builder = build_builder
66
+ request = builder.build(:get, "http://example.com/foo")
67
+
68
+ assert_equal "/foo", request.uri.path
69
+ end
70
+
71
+ def test_build_with_query_params_in_options_merges_into_uri_query
72
+ builder = build_builder(params: { "foo" => "bar" })
73
+ request = builder.build(:get, "http://example.com/path")
74
+
75
+ assert_includes request.uri.query, "foo=bar"
76
+ end
77
+
78
+ def test_build_with_query_params_and_existing_query_preserves_existing
79
+ builder = build_builder(params: { "extra" => "val" })
80
+ request = builder.build(:get, "http://example.com/path?existing=1")
81
+
82
+ assert_includes request.uri.query, "existing=1"
83
+ end
84
+
85
+ def test_build_with_query_params_and_existing_query_appends_new_params
86
+ builder = build_builder(params: { "extra" => "val" })
87
+ request = builder.build(:get, "http://example.com/path?existing=1")
88
+
89
+ assert_includes request.uri.query, "extra=val"
90
+ end
91
+
92
+ def test_build_with_body_in_options_uses_body
93
+ builder = build_builder(body: "raw body")
94
+ req = builder.build(:post, "http://example.com/")
95
+ chunks = req.body.enum_for(:each).map(&:dup)
96
+
97
+ assert_equal ["raw body"], chunks
98
+ end
99
+
100
+ def test_build_with_form_data_in_options_sets_content_type_header
101
+ builder = build_builder(form: { "key" => "value" })
102
+ req = builder.build(:post, "http://example.com/")
103
+
104
+ refute_nil req.headers["Content-Type"]
105
+ end
106
+
107
+ def test_build_with_form_data_in_options_includes_form_data_in_body
108
+ builder = build_builder(form: { "key" => "value" })
109
+ req = builder.build(:post, "http://example.com/")
110
+ chunks = req.body.enum_for(:each).map(&:dup)
111
+ body_str = chunks.join
112
+
113
+ assert_includes body_str, "key=value"
114
+ end
115
+
116
+ def test_build_with_json_in_options_encodes_json_body
117
+ builder = build_builder(json: { "key" => "value" })
118
+ req = builder.build(:post, "http://example.com/")
119
+ chunks = req.body.enum_for(:each).map(&:dup)
120
+
121
+ assert_equal [{ "key" => "value" }.to_json], chunks
122
+ end
123
+
124
+ def test_build_with_json_in_options_sets_content_type_to_application_json
125
+ builder = build_builder(json: { "key" => "value" })
126
+ req = builder.build(:post, "http://example.com/")
127
+
128
+ assert_match(%r{\Aapplication/json}, req.headers["Content-Type"])
129
+ end
130
+
131
+ def test_build_with_normalize_uri_feature_passes_custom_normalizer
132
+ custom_normalizer = ->(uri) { HTTP::URI::NORMALIZER.call(uri) }
133
+ builder = build_builder(features: { normalize_uri: { normalizer: custom_normalizer } })
134
+ req = builder.build(:get, "http://example.com/path")
135
+
136
+ assert_same custom_normalizer, req.uri_normalizer
137
+ end
138
+
139
+ def test_build_without_normalize_uri_feature_uses_default_normalizer
140
+ builder = build_builder
141
+ req = builder.build(:get, "http://example.com/path")
142
+
143
+ assert_equal HTTP::URI::NORMALIZER, req.uri_normalizer
144
+ end
145
+
146
+ def test_build_with_an_object_responding_to_to_s_as_uri_converts_it
147
+ builder = build_builder
148
+ uri_obj = Object.new
149
+ uri_obj.define_singleton_method(:to_s) { "http://example.com/converted" }
150
+ req = builder.build(:get, uri_obj)
151
+
152
+ assert_equal "/converted", req.uri.path
153
+ end
154
+
155
+ def test_build_with_uri_object_and_base_uri_converts_non_string_uri
156
+ builder = build_builder(base_uri: "http://example.com/api/")
157
+ uri_obj = Object.new
158
+ uri_obj.define_singleton_method(:to_s) { "users" }
159
+ req = builder.build(:get, uri_obj)
160
+
161
+ assert_equal "example.com", req.uri.host
162
+ assert_equal "/api/users", req.uri.path
163
+ end
164
+
165
+ def test_build_with_a_feature_that_wraps_the_request_returns_wrapped
166
+ wrapped = nil
167
+ feature_class = Class.new(HTTP::Feature) do
168
+ define_method(:wrap_request) do |req|
169
+ wrapped = HTTP::Request.new(
170
+ verb: req.verb,
171
+ uri: "http://wrapped.example.com/wrapped"
172
+ )
173
+ wrapped
174
+ end
175
+ end
176
+
177
+ HTTP::Options.register_feature(:test_build_wrap, feature_class)
178
+ begin
179
+ opts = HTTP::Options.new(features: { test_build_wrap: {} })
180
+ b = HTTP::Request::Builder.new(opts)
181
+ result = b.build(:get, "http://example.com/original")
182
+
183
+ assert_same wrapped, result
184
+ assert_equal "/wrapped", result.uri.path
185
+ ensure
186
+ HTTP::Options.available_features.delete(:test_build_wrap)
187
+ end
188
+ end
189
+
190
+ # #build with base_uri
191
+
192
+ def test_build_with_base_uri_when_uri_is_relative_resolves_against_base
193
+ builder = build_builder(base_uri: "http://example.com/api/")
194
+ req = builder.build(:get, "users")
195
+
196
+ assert_equal "example.com", req.uri.host
197
+ assert_match(%r{/api/users}, req.uri.path)
198
+ end
199
+
200
+ def test_build_with_base_uri_path_not_ending_with_slash_appends_slash
201
+ builder = build_builder(base_uri: "http://example.com/api")
202
+ req = builder.build(:get, "users")
203
+
204
+ assert_equal "example.com", req.uri.host
205
+ assert_match(%r{/api/users}, req.uri.path)
206
+ end
207
+
208
+ def test_build_with_base_uri_path_not_ending_with_slash_does_not_mutate_original
209
+ options = HTTP::Options.new(base_uri: "http://example.com/api")
210
+ builder = HTTP::Request::Builder.new(options)
211
+ original_path = options.base_uri.path.dup
212
+ builder.build(:get, "users")
213
+
214
+ assert_equal original_path, options.base_uri.path
215
+ end
216
+
217
+ def test_build_with_base_uri_path_ending_with_slash_does_not_double_it
218
+ builder = build_builder(base_uri: "http://example.com/api/")
219
+ req = builder.build(:get, "users")
220
+
221
+ assert_equal "/api/users", req.uri.path
222
+ end
223
+
224
+ def test_build_with_base_uri_and_absolute_http_uri_does_not_use_base
225
+ builder = build_builder(base_uri: "http://example.com/api/")
226
+ req = builder.build(:get, "http://other.com/path")
227
+
228
+ assert_equal "other.com", req.uri.host
229
+ assert_equal "/path", req.uri.path
230
+ end
231
+
232
+ def test_build_with_base_uri_and_absolute_https_uri_does_not_use_base
233
+ builder = build_builder(base_uri: "http://example.com/api/")
234
+ req = builder.build(:get, "https://secure.example.com/path")
235
+
236
+ assert_equal "secure.example.com", req.uri.host
237
+ end
238
+
239
+ def test_build_with_base_uri_resolves_correctly_when_base_is_set
240
+ opts = HTTP::Options.new(base_uri: "http://example.com/")
241
+ b = HTTP::Request::Builder.new(opts)
242
+ req = b.build(:get, "relative")
243
+
244
+ assert_equal "example.com", req.uri.host
245
+ end
246
+
247
+ # #build with persistent
248
+
249
+ def test_build_with_persistent_when_uri_is_relative_prepends_persistent_origin
250
+ builder = build_builder(persistent: "http://example.com")
251
+ req = builder.build(:get, "/path")
252
+
253
+ assert_equal "example.com", req.uri.host
254
+ assert_equal "/path", req.uri.path
255
+ end
256
+
257
+ def test_build_with_persistent_does_not_prepend_when_not_persistent
258
+ non_persistent_opts = HTTP::Options.new
259
+ b = HTTP::Request::Builder.new(non_persistent_opts)
260
+ req = b.build(:get, "http://fallback.com/path")
261
+
262
+ assert_equal "fallback.com", req.uri.host
263
+ end
264
+
265
+ def test_build_with_persistent_when_uri_is_absolute_uses_absolute_uri
266
+ builder = build_builder(persistent: "http://example.com")
267
+ req = builder.build(:get, "http://other.com/path")
268
+
269
+ assert_equal "other.com", req.uri.host
270
+ end
271
+
272
+ # #wrap
273
+
274
+ def test_wrap_returns_the_request_when_no_features_configured
275
+ builder = build_builder
276
+ req = HTTP::Request.new(verb: :get, uri: "http://example.com/")
277
+ result = builder.wrap(req)
278
+
279
+ assert_same req, result
280
+ end
281
+
282
+ def test_wrap_with_a_feature_applies_feature_wrapping
283
+ wrapped_request = nil
284
+ feature_class = Class.new(HTTP::Feature) do
285
+ define_method(:wrap_request) do |req|
286
+ wrapped_request = req
287
+ req
288
+ end
289
+ end
290
+
291
+ HTTP::Options.register_feature(:test_wrap_builder, feature_class)
292
+ begin
293
+ opts = HTTP::Options.new(features: { test_wrap_builder: {} })
294
+ b = HTTP::Request::Builder.new(opts)
295
+ req = HTTP::Request.new(verb: :get, uri: "http://example.com/")
296
+ b.wrap(req)
297
+
298
+ assert_same req, wrapped_request
299
+ ensure
300
+ HTTP::Options.available_features.delete(:test_wrap_builder)
301
+ end
302
+ end
303
+
304
+ def test_wrap_with_multiple_features_applies_in_order
305
+ call_order = []
306
+ feature_a = Class.new(HTTP::Feature) do
307
+ define_method(:wrap_request) do |req|
308
+ call_order << :a
309
+ req
310
+ end
311
+ end
312
+ feature_b = Class.new(HTTP::Feature) do
313
+ define_method(:wrap_request) do |req|
314
+ call_order << :b
315
+ req
316
+ end
317
+ end
318
+
319
+ HTTP::Options.register_feature(:test_wrap_a, feature_a)
320
+ HTTP::Options.register_feature(:test_wrap_b, feature_b)
321
+ begin
322
+ opts = HTTP::Options.new(features: { test_wrap_a: {}, test_wrap_b: {} })
323
+ b = HTTP::Request::Builder.new(opts)
324
+ req = HTTP::Request.new(verb: :get, uri: "http://example.com/")
325
+ b.wrap(req)
326
+
327
+ assert_equal %i[a b], call_order
328
+ ensure
329
+ HTTP::Options.available_features.delete(:test_wrap_a)
330
+ HTTP::Options.available_features.delete(:test_wrap_b)
331
+ end
332
+ end
333
+
334
+ # make_request_body (via #build)
335
+
336
+ def test_make_request_body_when_body_option_is_set_uses_body_directly
337
+ builder = build_builder(body: "raw")
338
+ req = builder.build(:post, "http://example.com/")
339
+ chunks = req.body.enum_for(:each).map(&:dup)
340
+
341
+ assert_equal ["raw"], chunks
342
+ end
343
+
344
+ def test_make_request_body_when_form_option_is_set_creates_form_data
345
+ builder = build_builder(form: { "name" => "test" })
346
+ req = builder.build(:post, "http://example.com/")
347
+
348
+ refute_nil req.headers["Content-Type"]
349
+ end
350
+
351
+ def test_make_request_body_when_form_option_is_set_returns_form_data_body_source
352
+ builder = build_builder(form: { "name" => "test" })
353
+ req = builder.build(:post, "http://example.com/")
354
+
355
+ refute_nil req.body.source
356
+ end
357
+
358
+ def test_make_request_body_form_does_not_override_existing_content_type
359
+ opts = HTTP::Options.new(
360
+ form: { "name" => "test" },
361
+ headers: { "Content-Type" => "custom/type" }
362
+ )
363
+ b = HTTP::Request::Builder.new(opts)
364
+ req = b.build(:post, "http://example.com/")
365
+
366
+ assert_equal "custom/type", req.headers["Content-Type"]
367
+ end
368
+
369
+ def test_make_request_body_when_json_option_is_set_encodes_as_json
370
+ builder = build_builder(json: { "key" => "val" })
371
+ req = builder.build(:post, "http://example.com/")
372
+ chunks = req.body.enum_for(:each).map(&:dup)
373
+
374
+ assert_equal [{ "key" => "val" }.to_json], chunks
375
+ end
376
+
377
+ def test_make_request_body_json_includes_charset_in_content_type
378
+ builder = build_builder(json: { "key" => "val" })
379
+ req = builder.build(:post, "http://example.com/")
380
+
381
+ assert_match(/charset=utf-8/, req.headers["Content-Type"])
382
+ end
383
+
384
+ def test_make_request_body_json_does_not_override_existing_content_type
385
+ opts = HTTP::Options.new(
386
+ json: { "key" => "val" },
387
+ headers: { "Content-Type" => "custom/json" }
388
+ )
389
+ b = HTTP::Request::Builder.new(opts)
390
+ req = b.build(:post, "http://example.com/")
391
+
392
+ assert_equal "custom/json", req.headers["Content-Type"]
393
+ end
394
+
395
+ def test_make_request_body_when_no_body_form_or_json_has_nil_body_source
396
+ builder = build_builder
397
+ req = builder.build(:get, "http://example.com/")
398
+
399
+ assert_nil req.body.source
400
+ end
401
+
402
+ # make_form_data (via #build)
403
+
404
+ def test_make_form_data_with_hash_form_creates_form_data
405
+ builder = build_builder(form: { "field" => "value" })
406
+ req = builder.build(:post, "http://example.com/")
407
+
408
+ refute_nil req.headers["Content-Type"]
409
+ end
410
+
411
+ def test_make_form_data_with_hash_form_passes_data_through_to_body
412
+ builder = build_builder(form: { "field" => "value" })
413
+ req = builder.build(:post, "http://example.com/")
414
+ chunks = req.body.enum_for(:each).map(&:dup)
415
+ body_str = chunks.join
416
+
417
+ assert_includes body_str, "field=value"
418
+ end
419
+
420
+ def test_make_form_data_with_multipart_form_passes_through
421
+ multipart = HTTP::FormData::Multipart.new({ "part" => HTTP::FormData::Part.new("val") })
422
+ opts = HTTP::Options.new(form: multipart)
423
+ b = HTTP::Request::Builder.new(opts)
424
+ req = b.build(:post, "http://example.com/")
425
+
426
+ assert_match(%r{\Amultipart/form-data}, req.headers["Content-Type"])
427
+ end
428
+
429
+ def test_make_form_data_with_urlencoded_form_passes_through
430
+ urlencoded = HTTP::FormData::Urlencoded.new({ "field" => "value" })
431
+ opts = HTTP::Options.new(form: urlencoded)
432
+ b = HTTP::Request::Builder.new(opts)
433
+ req = b.build(:post, "http://example.com/")
434
+
435
+ assert_equal "application/x-www-form-urlencoded", req.headers["Content-Type"]
436
+ end
437
+
438
+ # merge_query_params!
439
+
440
+ def test_merge_query_params_when_params_is_nil_does_not_add_query_string
441
+ builder = build_builder
442
+ req = builder.build(:get, "http://example.com/path")
443
+
444
+ assert_nil req.uri.query
445
+ end
446
+
447
+ def test_merge_query_params_when_params_is_empty_does_not_add_query_string
448
+ builder = build_builder(params: {})
449
+ req = builder.build(:get, "http://example.com/path")
450
+
451
+ assert_nil req.uri.query
452
+ end
453
+
454
+ def test_merge_query_params_when_params_has_values_and_no_query_sets_query
455
+ builder = build_builder(params: { "a" => "1" })
456
+ req = builder.build(:get, "http://example.com/path")
457
+
458
+ assert_equal "a=1", req.uri.query
459
+ end
460
+
461
+ def test_merge_query_params_when_params_has_values_and_existing_query_concatenates
462
+ builder = build_builder(params: { "b" => "2" })
463
+ req = builder.build(:get, "http://example.com/path?a=1")
464
+
465
+ assert_equal "a=1&b=2", req.uri.query
466
+ end
467
+
468
+ # empty path normalization (via #build)
469
+
470
+ def test_empty_path_normalization_normalizes_to_slash
471
+ builder = build_builder
472
+ req = builder.build(:get, "http://example.com")
473
+
474
+ assert_equal "/", req.uri.path
475
+ end
476
+
477
+ def test_empty_path_normalization_returns_http_uri_with_corrected_path
478
+ builder = build_builder
479
+ req = builder.build(:get, "http://example.com")
480
+
481
+ assert_instance_of HTTP::URI, req.uri
482
+ assert_equal "/", req.uri.path
483
+ end
484
+
485
+ # resolve_against_base error handling (via #build)
486
+
487
+ def test_resolve_against_base_raises_http_error
488
+ opts = HTTP::Options.new(base_uri: "http://example.com/")
489
+ b = HTTP::Request::Builder.new(opts)
490
+ opts.define_singleton_method(:base_uri) { nil }
491
+ opts.define_singleton_method(:base_uri?) { true }
492
+
493
+ err = assert_raises(HTTP::Error) { b.build(:get, "relative") }
494
+ assert_equal "base_uri is not set", err.message
495
+ end
496
+
497
+ # make_request_uri scheme guard with base_uri
498
+
499
+ def test_scheme_guard_with_base_uri_and_absolute_http_uses_absolute_uri
500
+ builder = build_builder(base_uri: "http://example.com/api/")
501
+ req = builder.build(:get, "http://other.com/path")
502
+
503
+ assert_equal "other.com", req.uri.host
504
+ assert_equal "/path", req.uri.path
505
+ end
506
+
507
+ def test_scheme_guard_with_base_uri_and_absolute_https_uses_absolute_uri
508
+ builder = build_builder(base_uri: "http://example.com/api/")
509
+ req = builder.build(:get, "https://secure.com/path")
510
+
511
+ assert_equal "secure.com", req.uri.host
512
+ assert_equal "/path", req.uri.path
513
+ end
514
+
515
+ def test_scheme_guard_with_base_uri_and_relative_uri_resolves_against_base
516
+ builder = build_builder(base_uri: "http://example.com/api/")
517
+ req = builder.build(:get, "users/1")
518
+
519
+ assert_equal "example.com", req.uri.host
520
+ assert_equal "/api/users/1", req.uri.path
521
+ end
522
+
523
+ # make_request_uri persistent guard
524
+
525
+ def test_persistent_guard_when_not_persistent_does_not_prepend_origin
526
+ builder = build_builder
527
+ req = builder.build(:get, "http://example.com/path")
528
+
529
+ assert_equal "example.com", req.uri.host
530
+ assert_equal "/path", req.uri.path
531
+ end
532
+
533
+ def test_persistent_guard_when_persistent_and_absolute_http_does_not_prepend
534
+ builder = build_builder(persistent: "http://example.com")
535
+ req = builder.build(:get, "http://other.com/path")
536
+
537
+ assert_equal "other.com", req.uri.host
538
+ end
539
+
540
+ # make_request_uri returns HTTP::URI
541
+
542
+ def test_make_request_uri_returns_http_uri_not_stdlib_uri
543
+ builder = build_builder
544
+ req = builder.build(:get, "http://example.com/path")
545
+
546
+ assert_instance_of HTTP::URI, req.uri
547
+ end
548
+
549
+ # make_request_uri empty path normalization
550
+
551
+ def test_make_request_uri_normalizes_empty_path_to_slash_for_bare_domain
552
+ builder = build_builder
553
+ req = builder.build(:get, "http://example.com")
554
+
555
+ assert_equal "/", req.uri.path
556
+ end
557
+
558
+ def test_make_request_uri_does_not_change_non_empty_path
559
+ builder = build_builder
560
+ req = builder.build(:get, "http://example.com/existing")
561
+
562
+ assert_equal "/existing", req.uri.path
563
+ end
564
+
565
+ def test_make_request_uri_normalizes_empty_path_when_using_persistent
566
+ opts = HTTP::Options.new(persistent: "http://example.com")
567
+ b = HTTP::Request::Builder.new(opts)
568
+ req = b.build(:get, "http://example.com")
569
+
570
+ assert_equal "/", req.uri.path
571
+ end
572
+
573
+ # resolve_against_base String conversion
574
+
575
+ def test_resolve_against_base_string_conversion_resolves_and_returns_valid_uri
576
+ builder = build_builder(base_uri: "http://example.com/api/")
577
+ req = builder.build(:get, "users")
578
+
579
+ assert_instance_of HTTP::URI, req.uri
580
+ assert_equal "/api/users", req.uri.path
581
+ assert_equal "example.com", req.uri.host
582
+ end
583
+
584
+ # #build uses HTTP::Request (not Request)
585
+
586
+ def test_build_creates_an_http_request_instance
587
+ builder = build_builder
588
+ req = builder.build(:get, "http://example.com/")
589
+
590
+ assert_instance_of HTTP::Request, req
591
+ end
592
+
593
+ # make_form_data type checks
594
+
595
+ def test_make_form_data_with_multipart_subclass_passes_through
596
+ multipart = HTTP::FormData::Multipart.new({ "part" => HTTP::FormData::Part.new("val") })
597
+ opts = HTTP::Options.new(form: multipart)
598
+ b = HTTP::Request::Builder.new(opts)
599
+ req = b.build(:post, "http://example.com/")
600
+
601
+ assert_match(%r{\Amultipart/form-data}, req.headers["Content-Type"])
602
+ end
603
+
604
+ def test_make_form_data_with_urlencoded_subclass_passes_through
605
+ urlencoded = HTTP::FormData::Urlencoded.new({ "a" => "1" })
606
+ opts = HTTP::Options.new(form: urlencoded)
607
+ b = HTTP::Request::Builder.new(opts)
608
+ req = b.build(:post, "http://example.com/")
609
+
610
+ assert_equal "application/x-www-form-urlencoded", req.headers["Content-Type"]
611
+ end
612
+
613
+ def test_make_form_data_with_plain_hash_creates_form_data_and_sets_content_type
614
+ opts = HTTP::Options.new(form: { "key" => "value" })
615
+ b = HTTP::Request::Builder.new(opts)
616
+ req = b.build(:post, "http://example.com/")
617
+
618
+ assert_equal "application/x-www-form-urlencoded", req.headers["Content-Type"]
619
+ chunks = req.body.enum_for(:each).map(&:dup)
620
+
621
+ assert_includes chunks.join, "key=value"
622
+ end
623
+ end