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,888 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class HTTPHeadersTest < Minitest::Test
6
+ cover "HTTP::Headers*"
7
+
8
+ def headers
9
+ @headers ||= HTTP::Headers.new
10
+ end
11
+
12
+ def test_is_enumerable
13
+ assert_kind_of Enumerable, headers
14
+ end
15
+
16
+ # -- #set -------------------------------------------------------------------
17
+
18
+ def test_set_sets_header_value
19
+ headers.set "Accept", "application/json"
20
+
21
+ assert_equal "application/json", headers["Accept"]
22
+ end
23
+
24
+ def test_set_allows_retrieval_via_normalized_header_name
25
+ headers.set :content_type, "application/json"
26
+
27
+ assert_equal "application/json", headers["Content-Type"]
28
+ end
29
+
30
+ def test_set_overwrites_previous_value
31
+ headers.set :set_cookie, "hoo=ray"
32
+ headers.set :set_cookie, "woo=hoo"
33
+
34
+ assert_equal "woo=hoo", headers["Set-Cookie"]
35
+ end
36
+
37
+ def test_set_allows_set_multiple_values
38
+ headers.set :set_cookie, "hoo=ray"
39
+ headers.set :set_cookie, %w[hoo=ray woo=hoo]
40
+
41
+ assert_equal %w[hoo=ray woo=hoo], headers["Set-Cookie"]
42
+ end
43
+
44
+ def test_set_fails_with_empty_header_name
45
+ assert_raises(HTTP::HeaderError) { headers.set "", "foo bar" }
46
+ end
47
+
48
+ ["foo bar", "foo bar: ok\nfoo", "evil-header: evil-value\nfoo"].each do |name|
49
+ define_method :"test_set_fails_with_invalid_header_name_#{name.inspect}" do
50
+ assert_raises(HTTP::HeaderError) { headers.set name, "baz" }
51
+ end
52
+ end
53
+
54
+ def test_set_fails_with_invalid_header_value
55
+ assert_raises(HTTP::HeaderError) { headers.set "foo", "bar\nEvil-Header: evil-value" }
56
+ end
57
+
58
+ # -- #[]= ------------------------------------------------------------------
59
+
60
+ def test_bracket_assign_sets_header_value
61
+ headers["Accept"] = "application/json"
62
+
63
+ assert_equal "application/json", headers["Accept"]
64
+ end
65
+
66
+ def test_bracket_assign_allows_retrieval_via_normalized_header_name
67
+ headers[:content_type] = "application/json"
68
+
69
+ assert_equal "application/json", headers["Content-Type"]
70
+ end
71
+
72
+ def test_bracket_assign_overwrites_previous_value
73
+ headers[:set_cookie] = "hoo=ray"
74
+ headers[:set_cookie] = "woo=hoo"
75
+
76
+ assert_equal "woo=hoo", headers["Set-Cookie"]
77
+ end
78
+
79
+ def test_bracket_assign_allows_set_multiple_values
80
+ headers[:set_cookie] = "hoo=ray"
81
+ headers[:set_cookie] = %w[hoo=ray woo=hoo]
82
+
83
+ assert_equal %w[hoo=ray woo=hoo], headers["Set-Cookie"]
84
+ end
85
+
86
+ # -- #delete ----------------------------------------------------------------
87
+
88
+ def test_delete_removes_given_header
89
+ headers.set "Content-Type", "application/json"
90
+ headers.delete "Content-Type"
91
+
92
+ assert_nil headers["Content-Type"]
93
+ end
94
+
95
+ def test_delete_removes_header_that_matches_normalized_version_of_specified_name
96
+ headers.set "Content-Type", "application/json"
97
+ headers.delete :content_type
98
+
99
+ assert_nil headers["Content-Type"]
100
+ end
101
+
102
+ def test_delete_calls_to_s_on_non_string_name_argument
103
+ headers.set "Content-Type", "application/json"
104
+ name = fake(to_s: "Content-Type")
105
+ headers.delete name
106
+
107
+ assert_nil headers["Content-Type"]
108
+ end
109
+
110
+ def test_delete_fails_with_empty_header_name
111
+ headers.set "Content-Type", "application/json"
112
+
113
+ assert_raises(HTTP::HeaderError) { headers.delete "" }
114
+ end
115
+
116
+ ["foo bar", "foo bar: ok\nfoo"].each do |name|
117
+ define_method :"test_delete_fails_with_invalid_header_name_#{name.inspect}" do
118
+ headers.set "Content-Type", "application/json"
119
+
120
+ assert_raises(HTTP::HeaderError) { headers.delete name }
121
+ end
122
+ end
123
+
124
+ # -- #add -------------------------------------------------------------------
125
+
126
+ def test_add_sets_header_value
127
+ headers.add "Accept", "application/json"
128
+
129
+ assert_equal "application/json", headers["Accept"]
130
+ end
131
+
132
+ def test_add_allows_retrieval_via_normalized_header_name
133
+ headers.add :content_type, "application/json"
134
+
135
+ assert_equal "application/json", headers["Content-Type"]
136
+ end
137
+
138
+ def test_add_appends_new_value_if_header_exists
139
+ headers.add "Set-Cookie", "hoo=ray"
140
+ headers.add :set_cookie, "woo=hoo"
141
+
142
+ assert_equal %w[hoo=ray woo=hoo], headers["Set-Cookie"]
143
+ end
144
+
145
+ def test_add_allows_append_multiple_values
146
+ headers.add :set_cookie, "hoo=ray"
147
+ headers.add :set_cookie, %w[woo=hoo yup=pie]
148
+
149
+ assert_equal %w[hoo=ray woo=hoo yup=pie], headers["Set-Cookie"]
150
+ end
151
+
152
+ def test_add_fails_with_empty_header_name
153
+ assert_raises(HTTP::HeaderError) { headers.add("", "foobar") }
154
+ end
155
+
156
+ ["foo bar", "foo bar: ok\nfoo"].each do |name|
157
+ define_method :"test_add_fails_with_invalid_header_name_#{name.inspect}" do
158
+ assert_raises(HTTP::HeaderError) { headers.add name, "baz" }
159
+ end
160
+ end
161
+
162
+ def test_add_fails_with_invalid_header_value
163
+ assert_raises(HTTP::HeaderError) { headers.add "foo", "bar\nEvil-Header: evil-value" }
164
+ end
165
+
166
+ def test_add_fails_when_header_name_is_not_a_string_or_symbol
167
+ err = assert_raises(HTTP::HeaderError) { headers.add 2, "foo" }
168
+ assert_includes err.message, "2"
169
+ end
170
+
171
+ def test_add_includes_inspect_formatted_name_in_error_for_non_string_symbol
172
+ obj = Object.new
173
+ def obj.to_s = "plain"
174
+ def obj.inspect = "INSPECTED"
175
+
176
+ err = assert_raises(HTTP::HeaderError) { headers.add obj, "foo" }
177
+ assert_includes err.message, "INSPECTED"
178
+ end
179
+
180
+ def test_add_uses_normalized_name_as_wire_name_for_symbol_keys_in_to_a
181
+ headers.add :content_type, "application/json"
182
+
183
+ assert_equal [["Content-Type", "application/json"]], headers.to_a
184
+ end
185
+
186
+ def test_add_preserves_original_string_as_wire_name_for_string_keys_in_to_a
187
+ headers.add "auth_key", "secret"
188
+
189
+ assert_equal [%w[auth_key secret]], headers.to_a
190
+ end
191
+
192
+ def test_add_calls_to_s_on_symbol_name_for_normalization
193
+ headers.add :accept, "text/html"
194
+
195
+ assert_equal [["Accept", "text/html"]], headers.to_a
196
+ end
197
+
198
+ # -- #get -------------------------------------------------------------------
199
+
200
+ def test_get_returns_array_of_associated_values
201
+ headers.set("Content-Type", "application/json")
202
+
203
+ assert_equal %w[application/json], headers.get("Content-Type")
204
+ end
205
+
206
+ def test_get_normalizes_header_name
207
+ headers.set("Content-Type", "application/json")
208
+
209
+ assert_equal %w[application/json], headers.get(:content_type)
210
+ end
211
+
212
+ def test_get_when_header_does_not_exist_returns_empty_array
213
+ headers.set("Content-Type", "application/json")
214
+
215
+ assert_equal [], headers.get(:accept)
216
+ end
217
+
218
+ def test_get_calls_to_s_on_non_string_name_argument
219
+ headers.set("Content-Type", "application/json")
220
+ name = fake(to_s: "Content-Type")
221
+
222
+ assert_equal %w[application/json], headers.get(name)
223
+ end
224
+
225
+ def test_get_fails_with_empty_header_name
226
+ headers.set("Content-Type", "application/json")
227
+
228
+ assert_raises(HTTP::HeaderError) { headers.get("") }
229
+ end
230
+
231
+ ["foo bar", "foo bar: ok\nfoo"].each do |name|
232
+ define_method :"test_get_fails_with_invalid_header_name_#{name.inspect}" do
233
+ headers.set("Content-Type", "application/json")
234
+
235
+ assert_raises(HTTP::HeaderError) { headers.get name }
236
+ end
237
+ end
238
+
239
+ # -- #[] -------------------------------------------------------------------
240
+
241
+ def test_bracket_when_header_does_not_exist_returns_nil
242
+ assert_nil headers[:accept]
243
+ end
244
+
245
+ def test_bracket_single_value_normalizes_header_name
246
+ headers.set "Content-Type", "application/json"
247
+
248
+ refute_nil headers[:content_type]
249
+ end
250
+
251
+ def test_bracket_single_value_returns_single_value
252
+ headers.set "Content-Type", "application/json"
253
+
254
+ assert_equal "application/json", headers[:content_type]
255
+ end
256
+
257
+ def test_bracket_multiple_values_normalizes_header_name
258
+ headers.add :set_cookie, "hoo=ray"
259
+ headers.add :set_cookie, "woo=hoo"
260
+
261
+ refute_nil headers[:set_cookie]
262
+ end
263
+
264
+ def test_bracket_multiple_values_returns_array_of_associated_values
265
+ headers.add :set_cookie, "hoo=ray"
266
+ headers.add :set_cookie, "woo=hoo"
267
+
268
+ assert_equal %w[hoo=ray woo=hoo], headers[:set_cookie]
269
+ end
270
+
271
+ def test_bracket_returns_nil_for_missing_header
272
+ headers.set "Content-Type", "text/plain"
273
+ result = headers[:nonexistent]
274
+
275
+ assert_nil result
276
+ end
277
+
278
+ def test_bracket_returns_string_for_single_value
279
+ headers.set "Content-Type", "text/plain"
280
+
281
+ result = headers["Content-Type"]
282
+
283
+ assert_instance_of String, result
284
+ assert_equal "text/plain", result
285
+ end
286
+
287
+ def test_bracket_returns_array_for_multiple_values
288
+ headers.add :cookie, "a=1"
289
+ headers.add :cookie, "b=2"
290
+
291
+ result = headers[:cookie]
292
+
293
+ assert_instance_of Array, result
294
+ assert_equal %w[a=1 b=2], result
295
+ end
296
+
297
+ # -- #include? --------------------------------------------------------------
298
+
299
+ def test_include_tells_whenever_given_headers_is_set_or_not
300
+ headers.add :content_type, "application/json"
301
+ headers.add :set_cookie, "hoo=ray"
302
+ headers.add :set_cookie, "woo=hoo"
303
+
304
+ assert_includes headers, "Content-Type"
305
+ assert_includes headers, "Set-Cookie"
306
+ refute_includes headers, "Accept"
307
+ end
308
+
309
+ def test_include_normalizes_given_header_name
310
+ headers.add :content_type, "application/json"
311
+ headers.add :set_cookie, "hoo=ray"
312
+ headers.add :set_cookie, "woo=hoo"
313
+
314
+ assert_includes headers, :content_type
315
+ assert_includes headers, :set_cookie
316
+ refute_includes headers, :accept
317
+ end
318
+
319
+ def test_include_calls_to_s_on_non_string_name_argument
320
+ headers.add :content_type, "application/json"
321
+ headers.add :set_cookie, "hoo=ray"
322
+ headers.add :set_cookie, "woo=hoo"
323
+ name = fake(to_s: "Content-Type")
324
+
325
+ assert_includes headers, name
326
+ end
327
+
328
+ def test_include_finds_headers_added_with_non_canonical_string_keys
329
+ h = HTTP::Headers.new
330
+ h.add("x-custom", "value")
331
+
332
+ assert_includes h, "x-custom"
333
+ end
334
+
335
+ # -- #to_h ------------------------------------------------------------------
336
+
337
+ def test_to_h_returns_a_hash
338
+ headers.add :content_type, "application/json"
339
+ headers.add :set_cookie, "hoo=ray"
340
+ headers.add :set_cookie, "woo=hoo"
341
+
342
+ assert_kind_of Hash, headers.to_h
343
+ end
344
+
345
+ def test_to_h_returns_hash_with_normalized_keys
346
+ headers.add :content_type, "application/json"
347
+ headers.add :set_cookie, "hoo=ray"
348
+ headers.add :set_cookie, "woo=hoo"
349
+
350
+ assert_equal %w[Content-Type Set-Cookie].sort, headers.to_h.keys.sort
351
+ end
352
+
353
+ def test_to_h_single_value_provides_value_as_is
354
+ headers.add :content_type, "application/json"
355
+ headers.add :set_cookie, "hoo=ray"
356
+ headers.add :set_cookie, "woo=hoo"
357
+
358
+ assert_equal "application/json", headers.to_h["Content-Type"]
359
+ end
360
+
361
+ def test_to_h_multiple_values_provides_array_of_values
362
+ headers.add :content_type, "application/json"
363
+ headers.add :set_cookie, "hoo=ray"
364
+ headers.add :set_cookie, "woo=hoo"
365
+
366
+ assert_equal %w[hoo=ray woo=hoo], headers.to_h["Set-Cookie"]
367
+ end
368
+
369
+ # -- #to_a ------------------------------------------------------------------
370
+
371
+ def test_to_a_returns_an_array
372
+ headers.add :content_type, "application/json"
373
+ headers.add :set_cookie, "hoo=ray"
374
+ headers.add :set_cookie, "woo=hoo"
375
+
376
+ assert_kind_of Array, headers.to_a
377
+ end
378
+
379
+ def test_to_a_returns_array_of_key_value_pairs_with_normalized_keys
380
+ headers.add :content_type, "application/json"
381
+ headers.add :set_cookie, "hoo=ray"
382
+ headers.add :set_cookie, "woo=hoo"
383
+
384
+ assert_equal [
385
+ %w[Content-Type application/json],
386
+ %w[Set-Cookie hoo=ray],
387
+ %w[Set-Cookie woo=hoo]
388
+ ], headers.to_a
389
+ end
390
+
391
+ def test_to_a_returns_two_element_arrays
392
+ headers.add :content_type, "application/json"
393
+ headers.add :set_cookie, "hoo=ray"
394
+ headers.add :set_cookie, "woo=hoo"
395
+
396
+ headers.to_a.each do |pair|
397
+ assert_equal 2, pair.size, "Expected each element to be a [name, value] pair"
398
+ end
399
+ end
400
+
401
+ def test_to_a_returns_wire_name_as_first_element
402
+ h = HTTP::Headers.new
403
+ h.add "X_Custom", "val"
404
+
405
+ assert_equal [%w[X_Custom val]], h.to_a
406
+ end
407
+
408
+ # -- #deconstruct_keys ------------------------------------------------------
409
+
410
+ def test_deconstruct_keys_returns_all_keys_as_snake_case_symbols_when_given_nil
411
+ headers.add :content_type, "application/json"
412
+ headers.add :set_cookie, "hoo=ray"
413
+ headers.add :set_cookie, "woo=hoo"
414
+
415
+ result = headers.deconstruct_keys(nil)
416
+
417
+ assert_equal "application/json", result[:content_type]
418
+ assert_equal %w[hoo=ray woo=hoo], result[:set_cookie]
419
+ end
420
+
421
+ def test_deconstruct_keys_converts_header_names_to_snake_case_symbols
422
+ headers.add :content_type, "application/json"
423
+ headers.add :set_cookie, "hoo=ray"
424
+ headers.add :set_cookie, "woo=hoo"
425
+
426
+ assert_includes headers.deconstruct_keys(nil).keys, :content_type
427
+ assert_includes headers.deconstruct_keys(nil).keys, :set_cookie
428
+ end
429
+
430
+ def test_deconstruct_keys_returns_only_requested_keys
431
+ headers.add :content_type, "application/json"
432
+ headers.add :set_cookie, "hoo=ray"
433
+ headers.add :set_cookie, "woo=hoo"
434
+
435
+ result = headers.deconstruct_keys([:content_type])
436
+
437
+ assert_equal({ content_type: "application/json" }, result)
438
+ end
439
+
440
+ def test_deconstruct_keys_excludes_unrequested_keys
441
+ headers.add :content_type, "application/json"
442
+ headers.add :set_cookie, "hoo=ray"
443
+ headers.add :set_cookie, "woo=hoo"
444
+
445
+ refute_includes headers.deconstruct_keys([:content_type]).keys, :set_cookie
446
+ end
447
+
448
+ def test_deconstruct_keys_returns_empty_hash_for_empty_keys
449
+ headers.add :content_type, "application/json"
450
+ headers.add :set_cookie, "hoo=ray"
451
+ headers.add :set_cookie, "woo=hoo"
452
+
453
+ assert_equal({}, headers.deconstruct_keys([]))
454
+ end
455
+
456
+ def test_deconstruct_keys_supports_pattern_matching_with_case_in
457
+ headers.add :content_type, "application/json"
458
+ headers.add :set_cookie, "hoo=ray"
459
+ headers.add :set_cookie, "woo=hoo"
460
+
461
+ matched = case headers
462
+ in { content_type: /json/ }
463
+ true
464
+ else
465
+ false
466
+ end
467
+
468
+ assert matched
469
+ end
470
+
471
+ # -- #inspect ---------------------------------------------------------------
472
+
473
+ def test_inspect_returns_a_human_readable_representation
474
+ headers.set :set_cookie, %w[hoo=ray woo=hoo]
475
+
476
+ assert_equal "#<HTTP::Headers>", headers.inspect
477
+ end
478
+
479
+ # -- #keys ------------------------------------------------------------------
480
+
481
+ def test_keys_returns_uniq_keys_only
482
+ headers.add :content_type, "application/json"
483
+ headers.add :set_cookie, "hoo=ray"
484
+ headers.add :set_cookie, "woo=hoo"
485
+
486
+ assert_equal 2, headers.keys.size
487
+ end
488
+
489
+ def test_keys_normalizes_keys
490
+ headers.add :content_type, "application/json"
491
+ headers.add :set_cookie, "hoo=ray"
492
+ headers.add :set_cookie, "woo=hoo"
493
+
494
+ assert_includes headers.keys, "Content-Type"
495
+ assert_includes headers.keys, "Set-Cookie"
496
+ end
497
+
498
+ # -- #each ------------------------------------------------------------------
499
+
500
+ def test_each_yields_each_key_value_pair_separately
501
+ headers.add :set_cookie, "hoo=ray"
502
+ headers.add :content_type, "application/json"
503
+ headers.add :set_cookie, "woo=hoo"
504
+
505
+ yielded = headers.map { |pair| pair }
506
+
507
+ assert_equal 3, yielded.size
508
+ end
509
+
510
+ def test_each_yields_headers_in_the_same_order_they_were_added
511
+ headers.add :set_cookie, "hoo=ray"
512
+ headers.add :content_type, "application/json"
513
+ headers.add :set_cookie, "woo=hoo"
514
+
515
+ yielded = headers.map { |pair| pair }
516
+
517
+ assert_equal [
518
+ %w[Set-Cookie hoo=ray],
519
+ %w[Content-Type application/json],
520
+ %w[Set-Cookie woo=hoo]
521
+ ], yielded
522
+ end
523
+
524
+ def test_each_yields_header_keys_specified_as_symbols_in_normalized_form
525
+ headers.add :set_cookie, "hoo=ray"
526
+ headers.add :content_type, "application/json"
527
+ headers.add :set_cookie, "woo=hoo"
528
+
529
+ keys = headers.each.map(&:first)
530
+
531
+ assert_equal %w[Set-Cookie Content-Type Set-Cookie], keys
532
+ end
533
+
534
+ def test_each_yields_headers_specified_as_strings_without_conversion
535
+ headers.add :set_cookie, "hoo=ray"
536
+ headers.add :content_type, "application/json"
537
+ headers.add :set_cookie, "woo=hoo"
538
+ headers.add "X_kEy", "value"
539
+
540
+ keys = headers.each.map(&:first)
541
+
542
+ assert_equal %w[Set-Cookie Content-Type Set-Cookie X_kEy], keys
543
+ end
544
+
545
+ def test_each_returns_self_instance_if_block_given
546
+ assert_same(headers, headers.each { |*| nil })
547
+ end
548
+
549
+ def test_each_returns_enumerator_if_no_block_given
550
+ assert_kind_of Enumerator, headers.each
551
+ end
552
+
553
+ def test_each_yields_two_element_arrays
554
+ headers.add :set_cookie, "hoo=ray"
555
+ headers.add :content_type, "application/json"
556
+ headers.add :set_cookie, "woo=hoo"
557
+
558
+ headers.each do |pair|
559
+ assert_equal 2, pair.size
560
+ end
561
+ end
562
+
563
+ # -- .empty? ----------------------------------------------------------------
564
+
565
+ def test_empty_initially_is_true
566
+ assert_empty headers
567
+ end
568
+
569
+ def test_empty_when_header_exists_is_false
570
+ headers.add :accept, "text/plain"
571
+
572
+ refute_empty headers
573
+ end
574
+
575
+ def test_empty_when_last_header_was_removed_is_true
576
+ headers.add :accept, "text/plain"
577
+ headers.delete :accept
578
+
579
+ assert_empty headers
580
+ end
581
+
582
+ # -- #hash ------------------------------------------------------------------
583
+
584
+ def test_hash_equals_if_two_headers_equals
585
+ left = HTTP::Headers.new
586
+ right = HTTP::Headers.new
587
+
588
+ left.add :accept, "text/plain"
589
+ right.add :accept, "text/plain"
590
+
591
+ assert_equal left.hash, right.hash
592
+ end
593
+
594
+ # -- #== -------------------------------------------------------------------
595
+
596
+ def test_eq_compares_header_keys_and_values
597
+ left = HTTP::Headers.new
598
+ right = HTTP::Headers.new
599
+
600
+ left.add :accept, "text/plain"
601
+ right.add :accept, "text/plain"
602
+
603
+ assert_equal left, right
604
+ end
605
+
606
+ def test_eq_allows_comparison_with_array_of_key_value_pairs
607
+ left = HTTP::Headers.new
608
+ left.add :accept, "text/plain"
609
+
610
+ assert_equal [%w[Accept text/plain]], left.to_a
611
+ end
612
+
613
+ def test_eq_sensitive_to_headers_order
614
+ left = HTTP::Headers.new
615
+ right = HTTP::Headers.new
616
+
617
+ left.add :accept, "text/plain"
618
+ left.add :cookie, "woo=hoo"
619
+ right.add :cookie, "woo=hoo"
620
+ right.add :accept, "text/plain"
621
+
622
+ refute_equal left, right
623
+ end
624
+
625
+ def test_eq_sensitive_to_header_values_order
626
+ left = HTTP::Headers.new
627
+ right = HTTP::Headers.new
628
+
629
+ left.add :cookie, "hoo=ray"
630
+ left.add :cookie, "woo=hoo"
631
+ right.add :cookie, "woo=hoo"
632
+ right.add :cookie, "hoo=ray"
633
+
634
+ refute_equal left, right
635
+ end
636
+
637
+ def test_eq_returns_false_when_compared_to_object_without_to_a
638
+ left = HTTP::Headers.new
639
+ left.add :accept, "text/plain"
640
+
641
+ refute_equal left, 42
642
+ end
643
+
644
+ # -- #dup -------------------------------------------------------------------
645
+
646
+ def test_dup_returns_an_http_headers_instance
647
+ headers.set :content_type, "application/json"
648
+ dupped = headers.dup
649
+
650
+ assert_kind_of HTTP::Headers, dupped
651
+ end
652
+
653
+ def test_dup_is_not_the_same_object
654
+ headers.set :content_type, "application/json"
655
+ dupped = headers.dup
656
+
657
+ refute_same headers, dupped
658
+ end
659
+
660
+ def test_dup_has_headers_copied
661
+ headers.set :content_type, "application/json"
662
+ dupped = headers.dup
663
+
664
+ assert_equal "application/json", dupped[:content_type]
665
+ end
666
+
667
+ def test_dup_modifying_copy_modifies_dupped_copy
668
+ headers.set :content_type, "application/json"
669
+ dupped = headers.dup
670
+ dupped.set :content_type, "text/plain"
671
+
672
+ assert_equal "text/plain", dupped[:content_type]
673
+ end
674
+
675
+ def test_dup_modifying_copy_does_not_affect_original_headers
676
+ headers.set :content_type, "application/json"
677
+ dupped = headers.dup
678
+ dupped.set :content_type, "text/plain"
679
+
680
+ assert_equal "application/json", headers[:content_type]
681
+ end
682
+
683
+ def test_dup_deep_copies_internal_pile_entries
684
+ headers.set :content_type, "application/json"
685
+ dupped = headers.dup
686
+
687
+ original_pile = headers.instance_variable_get(:@pile)
688
+ dupped_pile = dupped.instance_variable_get(:@pile)
689
+
690
+ # The outer arrays should be different objects
691
+ refute_same original_pile, dupped_pile
692
+
693
+ # Each inner array should also be a different object
694
+ original_pile.each_with_index do |entry, i|
695
+ refute_same entry, dupped_pile[i]
696
+ end
697
+ end
698
+
699
+ # -- validate_value (via #add) ----------------------------------------------
700
+
701
+ def test_validate_value_raises_header_error_when_value_contains_newline
702
+ err = assert_raises(HTTP::HeaderError) { headers.add "X-Test", "foo\nbar" }
703
+ assert_includes err.message, "foo"
704
+ end
705
+
706
+ def test_validate_value_accepts_values_without_newlines
707
+ headers.add "X-Test", "foobar"
708
+
709
+ assert_equal "foobar", headers["X-Test"]
710
+ end
711
+
712
+ def test_validate_value_calls_to_s_on_non_string_values
713
+ numeric_value = 42
714
+ headers.add "X-Number", numeric_value
715
+
716
+ assert_equal "42", headers["X-Number"]
717
+ end
718
+
719
+ def test_validate_value_raises_header_error_when_to_s_result_contains_newline
720
+ evil = fake(to_s: "good\nevil")
721
+
722
+ assert_raises(HTTP::HeaderError) { headers.add "X-Evil", evil }
723
+ end
724
+
725
+ def test_validate_value_includes_inspected_value_in_error_message
726
+ err = assert_raises(HTTP::HeaderError) { headers.add "Test", "bad\nvalue" }
727
+
728
+ assert_includes err.message, '"bad\nvalue"'
729
+ end
730
+
731
+ def test_validate_value_raises_header_error_when_value_contains_carriage_return
732
+ assert_raises(HTTP::HeaderError) { headers.add "X-Test", "foo\rbar" }
733
+ end
734
+
735
+ def test_validate_value_raises_header_error_when_value_contains_crlf
736
+ assert_raises(HTTP::HeaderError) { headers.add "X-Test", "foo\r\nbar" }
737
+ end
738
+
739
+ # -- #merge! ----------------------------------------------------------------
740
+
741
+ def test_merge_bang_leaves_headers_not_presented_in_other_as_is
742
+ headers.set :host, "example.com"
743
+ headers.set :accept, "application/json"
744
+ headers.merge! accept: "plain/text", cookie: %w[hoo=ray woo=hoo]
745
+
746
+ assert_equal "example.com", headers[:host]
747
+ end
748
+
749
+ def test_merge_bang_overwrites_existing_values
750
+ headers.set :host, "example.com"
751
+ headers.set :accept, "application/json"
752
+ headers.merge! accept: "plain/text", cookie: %w[hoo=ray woo=hoo]
753
+
754
+ assert_equal "plain/text", headers[:accept]
755
+ end
756
+
757
+ def test_merge_bang_appends_other_headers_not_presented_in_base
758
+ headers.set :host, "example.com"
759
+ headers.set :accept, "application/json"
760
+ headers.merge! accept: "plain/text", cookie: %w[hoo=ray woo=hoo]
761
+
762
+ assert_equal %w[hoo=ray woo=hoo], headers[:cookie]
763
+ end
764
+
765
+ def test_merge_bang_accepts_an_http_headers_instance
766
+ other = HTTP::Headers.new
767
+ other.set :accept, "text/xml"
768
+
769
+ h = HTTP::Headers.new
770
+ h.set :accept, "application/json"
771
+ h.merge!(other)
772
+
773
+ assert_equal "text/xml", h[:accept]
774
+ end
775
+
776
+ def test_merge_bang_uses_set_so_existing_values_are_replaced
777
+ h = HTTP::Headers.new
778
+ h.add :accept, "text/html"
779
+ h.add :accept, "text/plain"
780
+ h[:accept] = "application/json"
781
+
782
+ assert_equal "application/json", h[:accept]
783
+ end
784
+
785
+ # -- #merge -----------------------------------------------------------------
786
+
787
+ def test_merge_returns_an_http_headers_instance
788
+ headers.set :host, "example.com"
789
+ headers.set :accept, "application/json"
790
+ merged = headers.merge accept: "plain/text", cookie: %w[hoo=ray woo=hoo]
791
+
792
+ assert_kind_of HTTP::Headers, merged
793
+ end
794
+
795
+ def test_merge_is_not_the_same_object
796
+ headers.set :host, "example.com"
797
+ headers.set :accept, "application/json"
798
+ merged = headers.merge accept: "plain/text", cookie: %w[hoo=ray woo=hoo]
799
+
800
+ refute_same headers, merged
801
+ end
802
+
803
+ def test_merge_does_not_affect_original_headers
804
+ headers.set :host, "example.com"
805
+ headers.set :accept, "application/json"
806
+ merged = headers.merge accept: "plain/text", cookie: %w[hoo=ray woo=hoo]
807
+
808
+ refute_equal merged.to_h, headers.to_h
809
+ end
810
+
811
+ def test_merge_leaves_headers_not_presented_in_other_as_is
812
+ headers.set :host, "example.com"
813
+ headers.set :accept, "application/json"
814
+ merged = headers.merge accept: "plain/text", cookie: %w[hoo=ray woo=hoo]
815
+
816
+ assert_equal "example.com", merged[:host]
817
+ end
818
+
819
+ def test_merge_overwrites_existing_values
820
+ headers.set :host, "example.com"
821
+ headers.set :accept, "application/json"
822
+ merged = headers.merge accept: "plain/text", cookie: %w[hoo=ray woo=hoo]
823
+
824
+ assert_equal "plain/text", merged[:accept]
825
+ end
826
+
827
+ def test_merge_appends_other_headers_not_presented_in_base
828
+ headers.set :host, "example.com"
829
+ headers.set :accept, "application/json"
830
+ merged = headers.merge accept: "plain/text", cookie: %w[hoo=ray woo=hoo]
831
+
832
+ assert_equal %w[hoo=ray woo=hoo], merged[:cookie]
833
+ end
834
+
835
+ # -- .coerce ----------------------------------------------------------------
836
+
837
+ def test_coerce_accepts_any_object_that_respond_to_to_hash
838
+ hashie = fake(to_hash: { "accept" => "json" })
839
+
840
+ assert_equal "json", HTTP::Headers.coerce(hashie)["accept"]
841
+ end
842
+
843
+ def test_coerce_accepts_any_object_that_respond_to_to_h
844
+ hashie = fake(to_h: { "accept" => "json" })
845
+
846
+ assert_equal "json", HTTP::Headers.coerce(hashie)["accept"]
847
+ end
848
+
849
+ def test_coerce_accepts_any_object_that_respond_to_to_a
850
+ hashie = fake(to_a: [%w[accept json]])
851
+
852
+ assert_equal "json", HTTP::Headers.coerce(hashie)["accept"]
853
+ end
854
+
855
+ def test_coerce_fails_if_given_object_cannot_be_coerced
856
+ obj = Object.new
857
+ def obj.respond_to?(*); end
858
+ def obj.inspect = "INSPECTED"
859
+ def obj.to_s = "plain"
860
+
861
+ err = assert_raises(HTTP::Error) { HTTP::Headers.coerce obj }
862
+ assert_includes err.message, "INSPECTED"
863
+ end
864
+
865
+ def test_coerce_with_duplicate_header_keys_adds_all_headers
866
+ hdrs = { "Set-Cookie" => "hoo=ray", "set_cookie" => "woo=hoo", :set_cookie => "ta=da" }
867
+ expected = [%w[Set-Cookie hoo=ray], %w[set_cookie woo=hoo], %w[Set-Cookie ta=da]]
868
+
869
+ assert_equal expected.sort, HTTP::Headers.coerce(hdrs).to_a.sort
870
+ end
871
+
872
+ def test_coerce_is_aliased_as_bracket
873
+ result = HTTP::Headers["Content-Type" => "text/plain"]
874
+
875
+ assert_instance_of HTTP::Headers, result
876
+ assert_equal "text/plain", result["Content-Type"]
877
+ end
878
+
879
+ # -- .normalizer ------------------------------------------------------------
880
+
881
+ def test_normalizer_returns_a_normalizer_instance
882
+ assert_instance_of HTTP::Headers::Normalizer, HTTP::Headers.normalizer
883
+ end
884
+
885
+ def test_normalizer_returns_the_same_instance_on_subsequent_calls
886
+ assert_same HTTP::Headers.normalizer, HTTP::Headers.normalizer
887
+ end
888
+ end