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,303 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class FormDataMultipartTest < Minitest::Test
6
+ cover "HTTP::FormData::Multipart*"
7
+
8
+ BOUNDARY_PATTERN = /-{21}[a-f0-9]{42}/
9
+ CRLF = "\r\n"
10
+
11
+ private
12
+
13
+ def fixture_path
14
+ @fixture_path ||= Pathname.new(__dir__).join("fixtures/the-http-gem.info").realpath
15
+ end
16
+
17
+ def file
18
+ @file ||= HTTP::FormData::File.new(fixture_path)
19
+ end
20
+
21
+ def params
22
+ { foo: :bar, baz: file }
23
+ end
24
+
25
+ def form_data
26
+ @form_data ||= HTTP::FormData::Multipart.new(params)
27
+ end
28
+
29
+ def disposition(params)
30
+ params = params.map { |k, v| "#{k}=#{v.inspect}" }.join("; ")
31
+ "Content-Disposition: form-data; #{params}"
32
+ end
33
+
34
+ public
35
+
36
+ # --- Multipart body generation ---
37
+
38
+ def test_generates_multipart_data
39
+ b = form_data.boundary
40
+ expected = [
41
+ "--#{b}#{CRLF}",
42
+ "#{disposition 'name' => 'foo'}#{CRLF}",
43
+ "#{CRLF}bar#{CRLF}",
44
+ "--#{b}#{CRLF}",
45
+ "#{disposition 'name' => 'baz', 'filename' => file.filename}#{CRLF}",
46
+ "Content-Type: #{file.content_type}#{CRLF}",
47
+ "#{CRLF}#{file}#{CRLF}",
48
+ "--#{b}--#{CRLF}"
49
+ ].join
50
+
51
+ assert_equal expected, form_data.to_s
52
+ end
53
+
54
+ def test_user_defined_boundary
55
+ fd = HTTP::FormData::Multipart.new(params, boundary: "my-boundary")
56
+ expected = [
57
+ "--my-boundary#{CRLF}",
58
+ "#{disposition 'name' => 'foo'}#{CRLF}",
59
+ "#{CRLF}bar#{CRLF}",
60
+ "--my-boundary#{CRLF}",
61
+ "#{disposition 'name' => 'baz', 'filename' => file.filename}#{CRLF}",
62
+ "Content-Type: #{file.content_type}#{CRLF}",
63
+ "#{CRLF}#{file}#{CRLF}",
64
+ "--my-boundary--#{CRLF}"
65
+ ].join
66
+
67
+ assert_equal expected, fd.to_s
68
+ end
69
+
70
+ def test_part_without_filename
71
+ part = HTTP::FormData::Part.new("s", content_type: "mime/type")
72
+ fd = HTTP::FormData::Multipart.new({ foo: part })
73
+ b = fd.content_type[/(#{BOUNDARY_PATTERN})$/o, 1]
74
+
75
+ expected = [
76
+ "--#{b}#{CRLF}",
77
+ "#{disposition 'name' => 'foo'}#{CRLF}",
78
+ "Content-Type: #{part.content_type}#{CRLF}",
79
+ "#{CRLF}s#{CRLF}",
80
+ "--#{b}--#{CRLF}"
81
+ ].join
82
+
83
+ assert_equal expected, fd.to_s
84
+ end
85
+
86
+ def test_part_without_content_type
87
+ part = HTTP::FormData::Part.new("s")
88
+ fd = HTTP::FormData::Multipart.new({ foo: part })
89
+ b = fd.content_type[/(#{BOUNDARY_PATTERN})$/o, 1]
90
+
91
+ expected = [
92
+ "--#{b}#{CRLF}",
93
+ "#{disposition 'name' => 'foo'}#{CRLF}",
94
+ "#{CRLF}s#{CRLF}",
95
+ "--#{b}--#{CRLF}"
96
+ ].join
97
+
98
+ assert_equal expected, fd.to_s
99
+ end
100
+
101
+ def test_supports_enumerable_of_pairs
102
+ enum = Enumerator.new { |y| y << %i[foo bar] << %i[foo baz] }
103
+ fd = HTTP::FormData::Multipart.new(enum)
104
+ b = fd.boundary
105
+
106
+ expected = [
107
+ "--#{b}#{CRLF}",
108
+ "#{disposition 'name' => 'foo'}#{CRLF}",
109
+ "#{CRLF}bar#{CRLF}",
110
+ "--#{b}#{CRLF}",
111
+ "#{disposition 'name' => 'foo'}#{CRLF}",
112
+ "#{CRLF}baz#{CRLF}",
113
+ "--#{b}--#{CRLF}"
114
+ ].join
115
+
116
+ assert_equal expected, fd.to_s
117
+ end
118
+
119
+ def test_array_of_pairs_with_duplicate_names
120
+ data = [
121
+ ["metadata", %(filename="first.txt")],
122
+ ["file", HTTP::FormData::File.new(StringIO.new("uno"), content_type: "plain/text", filename: "abc")],
123
+ ["metadata", %(filename="second.txt")],
124
+ ["file", HTTP::FormData::File.new(StringIO.new("dos"), content_type: "plain/text", filename: "xyz")],
125
+ ["metadata", %w[question=why question=not]]
126
+ ]
127
+ fd = HTTP::FormData::Multipart.new(data)
128
+ b = fd.boundary
129
+
130
+ expected = [
131
+ %(--#{b}\r\n),
132
+ %(Content-Disposition: form-data; name="metadata"\r\n),
133
+ %(\r\nfilename="first.txt"\r\n),
134
+ %(--#{b}\r\n),
135
+ %(Content-Disposition: form-data; name="file"; filename="abc"\r\n),
136
+ %(Content-Type: plain/text\r\n),
137
+ %(\r\nuno\r\n),
138
+ %(--#{b}\r\n),
139
+ %(Content-Disposition: form-data; name="metadata"\r\n),
140
+ %(\r\nfilename="second.txt"\r\n),
141
+ %(--#{b}\r\n),
142
+ %(Content-Disposition: form-data; name="file"; filename="xyz"\r\n),
143
+ %(Content-Type: plain/text\r\n),
144
+ %(\r\ndos\r\n),
145
+ %(--#{b}\r\n),
146
+ %(Content-Disposition: form-data; name="metadata"\r\n),
147
+ %(\r\nquestion=why\r\n),
148
+ %(--#{b}\r\n),
149
+ %(Content-Disposition: form-data; name="metadata"\r\n),
150
+ %(\r\nquestion=not\r\n),
151
+ %(--#{b}--\r\n)
152
+ ].join
153
+
154
+ assert_equal expected, fd.to_s
155
+ end
156
+
157
+ # --- size / read / rewind ---
158
+
159
+ def test_size_returns_bytesize
160
+ assert_equal form_data.to_s.bytesize, form_data.size
161
+ end
162
+
163
+ def test_read_returns_multipart_data
164
+ assert_equal form_data.to_s, form_data.read
165
+ end
166
+
167
+ def test_rewind
168
+ form_data.read
169
+ form_data.rewind
170
+
171
+ assert_equal form_data.to_s, form_data.read
172
+ end
173
+
174
+ def test_content_length
175
+ assert_equal form_data.to_s.bytesize, form_data.content_length
176
+ end
177
+
178
+ def test_to_s_rewinds_content
179
+ content = form_data.read
180
+
181
+ assert_equal content, form_data.to_s
182
+ assert_equal content, form_data.read
183
+ end
184
+
185
+ # --- Content type ---
186
+
187
+ def test_content_type_matches_pattern
188
+ assert_match(%r{^multipart/form-data; boundary=#{BOUNDARY_PATTERN}$}o, form_data.content_type)
189
+ end
190
+
191
+ def test_content_type_with_user_defined_boundary
192
+ fd = HTTP::FormData::Multipart.new(params, boundary: "my-boundary")
193
+
194
+ assert_equal "multipart/form-data; boundary=my-boundary", fd.content_type
195
+ end
196
+
197
+ def test_content_type_with_custom_type
198
+ fd = HTTP::FormData::Multipart.new(params, boundary: "b", content_type: "multipart/related")
199
+
200
+ assert_equal "multipart/related; boundary=b", fd.content_type
201
+ end
202
+
203
+ def test_content_type_with_multipart_mixed
204
+ fd = HTTP::FormData::Multipart.new(params, boundary: "b", content_type: "multipart/mixed")
205
+
206
+ assert_equal "multipart/mixed; boundary=b", fd.content_type
207
+ end
208
+
209
+ def test_content_type_default_is_form_data
210
+ assert_equal "multipart/form-data", HTTP::FormData::Multipart::DEFAULT_CONTENT_TYPE
211
+ end
212
+
213
+ def test_content_type_converts_to_string
214
+ fd = HTTP::FormData::Multipart.new(params, boundary: "b", content_type: :"multipart/related")
215
+
216
+ assert_equal "multipart/related; boundary=b", fd.content_type
217
+ assert_instance_of String, fd.content_type
218
+ end
219
+
220
+ # --- Boundary ---
221
+
222
+ def test_boundary_matches_pattern
223
+ assert_match(BOUNDARY_PATTERN, form_data.boundary)
224
+ end
225
+
226
+ def test_boundary_with_user_defined_value
227
+ fd = HTTP::FormData::Multipart.new(params, boundary: "my-boundary")
228
+
229
+ assert_equal "my-boundary", fd.boundary
230
+ end
231
+
232
+ def test_boundary_is_frozen_string
233
+ assert_predicate form_data.boundary, :frozen?
234
+ assert_instance_of String, form_data.boundary
235
+ end
236
+
237
+ def test_boundary_with_symbol_value
238
+ fd = HTTP::FormData::Multipart.new({ foo: "bar" }, boundary: :"my-sym-boundary")
239
+
240
+ assert_equal "my-sym-boundary", fd.boundary
241
+ assert_instance_of String, fd.boundary
242
+ end
243
+
244
+ def test_generate_boundary
245
+ assert_match(BOUNDARY_PATTERN, HTTP::FormData::Multipart.generate_boundary)
246
+ end
247
+
248
+ # --- Param ---
249
+
250
+ def test_parts_with_nil_data
251
+ form = HTTP::FormData::Multipart.new(nil, boundary: "test-boundary")
252
+
253
+ assert_equal "--test-boundary--\r\n", form.to_s
254
+ end
255
+
256
+ def test_param_converts_name_to_string
257
+ part = HTTP::FormData::Part.new("val")
258
+ body = HTTP::FormData::Multipart.new({ 123 => part }, boundary: "b").to_s
259
+
260
+ assert_includes body, 'name="123"'
261
+ end
262
+
263
+ def test_param_wraps_non_part_value
264
+ body = HTTP::FormData::Multipart.new({ foo: "raw_string" }, boundary: "b").to_s
265
+
266
+ assert_includes body, "raw_string"
267
+ refute_includes body, "Content-Type:"
268
+ end
269
+
270
+ def test_param_uses_part_directly
271
+ part = HTTP::FormData::Part.new("part_body", content_type: "text/plain")
272
+ body = HTTP::FormData::Multipart.new({ foo: part }, boundary: "b").to_s
273
+
274
+ assert_includes body, "part_body"
275
+ assert_includes body, "Content-Type: text/plain"
276
+ end
277
+
278
+ def test_param_header_contains_content_disposition
279
+ body = HTTP::FormData::Multipart.new({ myfield: "val" }, boundary: "b").to_s
280
+
281
+ assert_includes body, "Content-Disposition: form-data; name=\"myfield\""
282
+ end
283
+
284
+ def test_param_includes_content_type_when_present
285
+ part = HTTP::FormData::Part.new("val", content_type: "application/json")
286
+ body = HTTP::FormData::Multipart.new({ f: part }, boundary: "b").to_s
287
+
288
+ assert_includes body, "Content-Type: application/json\r\n"
289
+ end
290
+
291
+ def test_param_excludes_content_type_when_nil
292
+ part = HTTP::FormData::Part.new("val")
293
+ body = HTTP::FormData::Multipart.new({ f: part }, boundary: "b").to_s
294
+
295
+ refute_includes body, "Content-Type:"
296
+ end
297
+
298
+ def test_param_footer_is_crlf
299
+ body = HTTP::FormData::Multipart.new({ f: "v" }, boundary: "b").to_s
300
+
301
+ assert_includes body, "v\r\n--b--"
302
+ end
303
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class FormDataPartTest < Minitest::Test
6
+ cover "HTTP::FormData::Part*"
7
+
8
+ def test_size
9
+ assert_equal 20, HTTP::FormData::Part.new("привет мир!").size
10
+ end
11
+
12
+ def test_to_s
13
+ assert_equal "привет мир!", HTTP::FormData::Part.new("привет мир!").to_s
14
+ end
15
+
16
+ def test_to_s_rewinds_content
17
+ part = HTTP::FormData::Part.new("привет мир!")
18
+ part.to_s
19
+ content = part.read
20
+
21
+ assert_equal content, part.to_s
22
+ assert_equal content, part.read
23
+ end
24
+
25
+ def test_read
26
+ assert_equal "привет мир!", HTTP::FormData::Part.new("привет мир!").read
27
+ end
28
+
29
+ def test_read_with_length
30
+ part = HTTP::FormData::Part.new("hello world")
31
+
32
+ assert_equal "hello", part.read(5)
33
+ assert_equal " worl", part.read(5)
34
+ assert_equal "d", part.read(5)
35
+ assert_nil part.read(5)
36
+ end
37
+
38
+ def test_read_with_nil_length
39
+ assert_equal "hello", HTTP::FormData::Part.new("hello").read(nil)
40
+ end
41
+
42
+ def test_read_with_outbuf
43
+ part = HTTP::FormData::Part.new("hello")
44
+ buf = +""
45
+ result = part.read(3, buf)
46
+
47
+ assert_equal "hel", result
48
+ assert_equal "hel", buf
49
+ end
50
+
51
+ def test_rewind
52
+ part = HTTP::FormData::Part.new("привет мир!")
53
+ part.read
54
+ part.rewind
55
+
56
+ assert_equal "привет мир!", part.read
57
+ end
58
+
59
+ def test_filename_defaults_to_nil
60
+ assert_nil HTTP::FormData::Part.new("").filename
61
+ end
62
+
63
+ def test_filename_with_option
64
+ assert_equal "foobar.txt", HTTP::FormData::Part.new("", filename: "foobar.txt").filename
65
+ end
66
+
67
+ def test_filename_stores_exact_value
68
+ assert_equal "test.txt", HTTP::FormData::Part.new("body", filename: "test.txt").filename
69
+ end
70
+
71
+ def test_content_type_defaults_to_nil
72
+ assert_nil HTTP::FormData::Part.new("").content_type
73
+ end
74
+
75
+ def test_content_type_with_option
76
+ assert_equal "application/json", HTTP::FormData::Part.new("", content_type: "application/json").content_type
77
+ end
78
+
79
+ def test_content_type_stores_exact_value
80
+ assert_equal "text/plain", HTTP::FormData::Part.new("body", content_type: "text/plain").content_type
81
+ end
82
+
83
+ def test_initialize_converts_body_to_string
84
+ assert_equal "42", HTTP::FormData::Part.new(42).to_s
85
+ end
86
+
87
+ def test_initialize_with_symbol_body
88
+ assert_equal "hello", HTTP::FormData::Part.new(:hello).to_s
89
+ end
90
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class FormDataUrlencodedTest < Minitest::Test
6
+ cover "HTTP::FormData::Urlencoded*"
7
+
8
+ def test_raises_error_for_non_enumerable_input
9
+ assert_raises(HTTP::FormData::Error) { HTTP::FormData::Urlencoded.new(42) }
10
+ end
11
+
12
+ def test_raises_argument_error_for_non_hash_top_level
13
+ assert_raises(ArgumentError) { HTTP::FormData::Urlencoded.encoder.call(42) }
14
+ end
15
+
16
+ def test_supports_enumerables_of_pairs
17
+ form_data = HTTP::FormData::Urlencoded.new([%w[foo bar], ["foo", %w[baz moo]]])
18
+
19
+ assert_equal "foo=bar&foo[]=baz&foo[]=moo", form_data.to_s
20
+ end
21
+
22
+ def test_content_type
23
+ assert_equal "application/x-www-form-urlencoded", HTTP::FormData::Urlencoded.new({ foo: "bar" }).content_type
24
+ end
25
+
26
+ def test_content_length
27
+ form_data = HTTP::FormData::Urlencoded.new({ "foo[bar]" => "test" })
28
+
29
+ assert_equal form_data.to_s.bytesize, form_data.content_length
30
+ end
31
+
32
+ def test_content_length_with_unicode
33
+ form_data = HTTP::FormData::Urlencoded.new({ "foo[bar]" => "тест" })
34
+
35
+ assert_equal form_data.to_s.bytesize, form_data.content_length
36
+ end
37
+
38
+ def test_to_s
39
+ assert_equal "foo%5Bbar%5D=test", HTTP::FormData::Urlencoded.new({ "foo[bar]" => "test" }).to_s
40
+ end
41
+
42
+ def test_to_s_with_unicode
43
+ assert_equal "foo%5Bbar%5D=%D1%82%D0%B5%D1%81%D1%82",
44
+ HTTP::FormData::Urlencoded.new({ "foo[bar]" => "тест" }).to_s
45
+ end
46
+
47
+ def test_to_s_with_nested_hashes
48
+ assert_equal "foo[bar]=test", HTTP::FormData::Urlencoded.new({ "foo" => { "bar" => "test" } }).to_s
49
+ end
50
+
51
+ def test_to_s_with_nil_value
52
+ assert_equal "foo", HTTP::FormData::Urlencoded.new({ "foo" => nil }).to_s
53
+ end
54
+
55
+ def test_to_s_rewinds_content
56
+ form_data = HTTP::FormData::Urlencoded.new({ "foo[bar]" => "test" })
57
+ content = form_data.read
58
+
59
+ assert_equal content, form_data.to_s
60
+ assert_equal content, form_data.read
61
+ end
62
+
63
+ def test_size
64
+ form_data = HTTP::FormData::Urlencoded.new({ "foo[bar]" => "test" })
65
+
66
+ assert_equal form_data.to_s.bytesize, form_data.size
67
+ end
68
+
69
+ def test_read
70
+ form_data = HTTP::FormData::Urlencoded.new({ "foo[bar]" => "test" })
71
+
72
+ assert_equal form_data.to_s, form_data.read
73
+ end
74
+
75
+ def test_rewind
76
+ form_data = HTTP::FormData::Urlencoded.new({ "foo[bar]" => "test" })
77
+ form_data.read
78
+ form_data.rewind
79
+
80
+ assert_equal form_data.to_s, form_data.read
81
+ end
82
+
83
+ # --- Custom encoders ---
84
+
85
+ def test_custom_class_level_encoder
86
+ original_encoder = HTTP::FormData::Urlencoded.encoder
87
+ HTTP::FormData::Urlencoded.encoder = JSON.method(:dump)
88
+ form_data = HTTP::FormData::Urlencoded.new({ "foo[bar]" => "test" })
89
+
90
+ assert_equal '{"foo[bar]":"test"}', form_data.to_s
91
+ ensure
92
+ HTTP::FormData::Urlencoded.encoder = original_encoder
93
+ end
94
+
95
+ def test_encoder_rejects_non_callable
96
+ assert_raises(ArgumentError) { HTTP::FormData::Urlencoded.encoder = "not callable" }
97
+ end
98
+
99
+ def test_custom_instance_level_encoder
100
+ encoder = proc { |data| JSON.dump(data) }
101
+ form_data = HTTP::FormData::Urlencoded.new({ "foo[bar]" => "test" }, encoder: encoder)
102
+
103
+ assert_equal '{"foo[bar]":"test"}', form_data.to_s
104
+ end
105
+
106
+ def test_default_encoder_returns_callable
107
+ assert_respond_to HTTP::FormData::Urlencoded.encoder, :call
108
+ end
109
+
110
+ def test_default_encoder_encodes_correctly
111
+ assert_equal "key=value", HTTP::FormData::Urlencoded.encoder.call({ "key" => "value" })
112
+ end
113
+
114
+ def test_nil_encoder_uses_class_default
115
+ form_data = HTTP::FormData::Urlencoded.new({ foo: "bar" }, encoder: nil)
116
+
117
+ assert_equal "foo=bar", form_data.to_s
118
+ end
119
+
120
+ def test_custom_encoder_is_called
121
+ calls = []
122
+ custom = proc { |data|
123
+ calls << data
124
+ "custom"
125
+ }
126
+ form_data = HTTP::FormData::Urlencoded.new({ a: "b" }, encoder: custom)
127
+
128
+ assert_equal "custom", form_data.to_s
129
+ refute_empty calls
130
+ end
131
+
132
+ # --- Initialize edge cases ---
133
+
134
+ def test_initialize_with_nil_data
135
+ assert_equal "", HTTP::FormData::Urlencoded.new(nil).to_s
136
+ end
137
+
138
+ def test_initialize_with_to_h_object
139
+ obj = Object.new
140
+ def obj.to_h = { x: "y" }
141
+
142
+ assert_equal "x=y", HTTP::FormData::Urlencoded.new(obj).to_s
143
+ end
144
+
145
+ def test_initialize_stores_encoded_content
146
+ form_data = HTTP::FormData::Urlencoded.new({ a: "1", b: "2" })
147
+
148
+ assert_equal "a=1&b=2", form_data.to_s
149
+ assert_equal 7, form_data.size
150
+ end
151
+
152
+ def test_read_with_length
153
+ form_data = HTTP::FormData::Urlencoded.new({ foo: "bar" })
154
+
155
+ assert_equal "fo", form_data.read(2)
156
+ assert_equal "o=b", form_data.read(3)
157
+ assert_equal "ar", form_data.read(5)
158
+ assert_nil form_data.read(1)
159
+ end
160
+
161
+ def test_read_with_nil_length
162
+ assert_equal "foo=bar", HTTP::FormData::Urlencoded.new({ foo: "bar" }).read(nil)
163
+ end
164
+ end