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,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class HTTPTimeoutNullTest < Minitest::Test
6
+ cover "HTTP::Timeout::Null*"
7
+
8
+ def setup
9
+ super
10
+ @io = fake(wait_readable: true, wait_writable: true)
11
+ @socket = fake(to_io: @io, closed?: false)
12
+ @timeout = HTTP::Timeout::Null.new
13
+ @timeout.instance_variable_set(:@socket, @socket)
14
+ end
15
+
16
+ # -- #initialize --
17
+
18
+ def test_initialize_stores_provided_options_compacted
19
+ t = HTTP::Timeout::Null.new(read_timeout: 5, write_timeout: 10)
20
+
21
+ assert_equal({ read_timeout: 5, write_timeout: 10 }, t.options)
22
+ end
23
+
24
+ # -- #start_tls --
25
+
26
+ def test_start_tls_skips_hostname_and_sync_close_when_not_responding
27
+ ssl_socket = fake(connect: nil)
28
+ ssl_socket_class = fake(new: ssl_socket)
29
+ ssl_context = OpenSSL::SSL::SSLContext.new
30
+ ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
31
+ @timeout.start_tls("example.com", ssl_socket_class, ssl_context)
32
+ end
33
+
34
+ def test_start_tls_skips_post_connection_check_when_verify_mode_not_verify_peer
35
+ post_connection_check_called = false
36
+ ssl_socket = fake(
37
+ connect: nil,
38
+ "hostname=": ->(*) {},
39
+ "sync_close=": ->(*) {},
40
+ post_connection_check: ->(*) { post_connection_check_called = true }
41
+ )
42
+ ssl_socket_class = fake(new: ssl_socket)
43
+ ssl_context = OpenSSL::SSL::SSLContext.new
44
+ ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
45
+ @timeout.start_tls("example.com", ssl_socket_class, ssl_context)
46
+
47
+ refute post_connection_check_called
48
+ end
49
+
50
+ def test_start_tls_calls_post_connection_check_when_verify_peer_and_verify_hostname
51
+ post_connection_check_called = false
52
+ post_connection_check_arg = nil
53
+ ssl_socket = fake(
54
+ connect: nil,
55
+ "hostname=": ->(*) {},
56
+ "sync_close=": ->(*) {},
57
+ post_connection_check: lambda { |host|
58
+ post_connection_check_called = true
59
+ post_connection_check_arg = host
60
+ }
61
+ )
62
+ ssl_socket_class = fake(new: ssl_socket)
63
+ ssl_context = OpenSSL::SSL::SSLContext.new
64
+ ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
65
+ ssl_context.verify_hostname = true
66
+ @timeout.start_tls("example.com", ssl_socket_class, ssl_context)
67
+
68
+ assert post_connection_check_called
69
+ assert_equal "example.com", post_connection_check_arg
70
+ end
71
+
72
+ def test_start_tls_skips_post_connection_check_when_verify_hostname_false
73
+ post_connection_check_called = false
74
+ ssl_socket = fake(
75
+ connect: nil,
76
+ "hostname=": ->(*) {},
77
+ "sync_close=": ->(*) {},
78
+ post_connection_check: ->(*) { post_connection_check_called = true }
79
+ )
80
+ ssl_socket_class = fake(new: ssl_socket)
81
+ ssl_context = OpenSSL::SSL::SSLContext.new
82
+ ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
83
+ ssl_context.verify_hostname = false
84
+ @timeout.start_tls("example.com", ssl_socket_class, ssl_context)
85
+
86
+ refute post_connection_check_called
87
+ end
88
+
89
+ # -- #rescue_readable (private) --
90
+
91
+ def test_rescue_readable_yields_the_block
92
+ assert_equal :ok, @timeout.send(:rescue_readable, 1) { :ok }
93
+ end
94
+
95
+ def test_rescue_readable_when_wait_readable_raised_and_wait_succeeds_retries
96
+ call_count = 0
97
+ result = @timeout.send(:rescue_readable, 1) do
98
+ raise IO::EAGAINWaitReadable if (call_count += 1) == 1
99
+
100
+ :done
101
+ end
102
+
103
+ assert_equal :done, result
104
+ end
105
+
106
+ def test_rescue_readable_when_wait_readable_raised_and_wait_times_out_raises_timeout_error
107
+ io_with_nil_wait = fake(wait_readable: nil, wait_writable: true)
108
+ socket_with_nil_wait = fake(to_io: io_with_nil_wait, closed?: false)
109
+ @timeout.instance_variable_set(:@socket, socket_with_nil_wait)
110
+
111
+ err = assert_raises(HTTP::TimeoutError) do
112
+ @timeout.send(:rescue_readable, 1) { raise IO::EAGAINWaitReadable }
113
+ end
114
+ assert_match(/Read timed out/, err.message)
115
+ end
116
+
117
+ # -- #rescue_writable (private) --
118
+
119
+ def test_rescue_writable_yields_the_block
120
+ assert_equal :ok, @timeout.send(:rescue_writable, 1) { :ok }
121
+ end
122
+
123
+ def test_rescue_writable_when_wait_writable_raised_and_wait_succeeds_retries
124
+ call_count = 0
125
+ result = @timeout.send(:rescue_writable, 1) do
126
+ raise IO::EAGAINWaitWritable if (call_count += 1) == 1
127
+
128
+ :done
129
+ end
130
+
131
+ assert_equal :done, result
132
+ end
133
+
134
+ def test_rescue_writable_when_wait_writable_raised_and_wait_times_out_raises_timeout_error
135
+ io_with_nil_wait = fake(wait_readable: true, wait_writable: nil)
136
+ socket_with_nil_wait = fake(to_io: io_with_nil_wait, closed?: false)
137
+ @timeout.instance_variable_set(:@socket, socket_with_nil_wait)
138
+
139
+ err = assert_raises(HTTP::TimeoutError) do
140
+ @timeout.send(:rescue_writable, 1) { raise IO::EAGAINWaitWritable }
141
+ end
142
+ assert_match(/Write timed out/, err.message)
143
+ end
144
+
145
+ # -- NATIVE_CONNECT_TIMEOUT --
146
+
147
+ def test_native_connect_timeout_is_true_on_ruby_3_4_plus
148
+ assert_equal RUBY_VERSION >= "3.4", HTTP::Timeout::Null::NATIVE_CONNECT_TIMEOUT
149
+ end
150
+
151
+ # -- #open_socket (private) --
152
+
153
+ def test_open_socket_opens_a_socket_without_timeout
154
+ tcp_socket = fake(closed?: false)
155
+ socket_class = fake(open: tcp_socket)
156
+ result = @timeout.send(:open_socket, socket_class, "example.com", 80)
157
+
158
+ assert_same tcp_socket, result
159
+ end
160
+
161
+ def test_open_socket_passes_connect_timeout_natively_when_supported
162
+ received_args = nil
163
+ stub_open = lambda do |*args, **kwargs|
164
+ received_args = [args, kwargs]
165
+ fake(closed?: false)
166
+ end
167
+
168
+ @timeout.stub(:native_timeout?, true) do
169
+ TCPSocket.stub(:open, stub_open) do
170
+ @timeout.send(:open_socket, TCPSocket, "127.0.0.1", 1, connect_timeout: 5)
171
+ end
172
+ end
173
+
174
+ assert_equal [["127.0.0.1", 1], { connect_timeout: 5 }], received_args
175
+ end
176
+
177
+ def test_open_socket_does_not_pass_connect_timeout_to_non_tcp_socket_classes
178
+ received_args = nil
179
+ tcp_socket = fake(closed?: false)
180
+ socket_class = fake(open: proc { |*args|
181
+ received_args = args
182
+ tcp_socket
183
+ })
184
+
185
+ @timeout.send(:open_socket, socket_class, "example.com", 80, connect_timeout: 5)
186
+
187
+ assert_equal ["example.com", 80], received_args
188
+ end
189
+
190
+ def test_open_socket_converts_io_timeout_error_to_connect_timeout_error
191
+ socket_class = fake(open: proc { |*| raise IO::TimeoutError, "Connect timed out!" })
192
+
193
+ err = assert_raises(HTTP::ConnectTimeoutError) do
194
+ @timeout.send(:open_socket, socket_class, "example.com", 80, connect_timeout: 5)
195
+ end
196
+ assert_match(/Connect timed out/, err.message)
197
+ end
198
+
199
+ # -- #native_timeout? (private) --
200
+
201
+ if RUBY_VERSION >= "3.4"
202
+ def test_native_timeout_returns_true_for_tcp_socket_on_ruby_3_4_plus
203
+ assert @timeout.send(:native_timeout?, TCPSocket)
204
+ end
205
+ else
206
+ def test_native_timeout_returns_false_for_tcp_socket_on_ruby_below_3_4
207
+ refute @timeout.send(:native_timeout?, TCPSocket)
208
+ end
209
+ end
210
+
211
+ def test_native_timeout_returns_false_for_non_tcp_socket_classes
212
+ refute @timeout.send(:native_timeout?, OpenSSL::SSL::SSLSocket)
213
+ end
214
+
215
+ def test_native_timeout_returns_false_for_non_class_objects
216
+ refute @timeout.send(:native_timeout?, fake(open: nil))
217
+ end
218
+ end
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class HTTPTimeoutPerOperationTest < Minitest::Test
6
+ cover "HTTP::Timeout::PerOperation*"
7
+
8
+ # -- instance tests --
9
+
10
+ def setup
11
+ super
12
+ @io = fake(wait_readable: true, wait_writable: true)
13
+ @socket = fake(to_io: @io, closed?: false)
14
+ @timeout = HTTP::Timeout::PerOperation.new(connect_timeout: 1, read_timeout: 1, write_timeout: 1)
15
+ @timeout.instance_variable_set(:@socket, @socket)
16
+ end
17
+ # -- .extract_global_timeout! --
18
+
19
+ def test_extract_global_timeout_extracts_short_global_key
20
+ opts = { global: 60, read: 5 }
21
+
22
+ assert_equal 60, HTTP::Timeout::PerOperation.send(:extract_global_timeout!, opts)
23
+ assert_equal({ read: 5 }, opts)
24
+ end
25
+
26
+ def test_extract_global_timeout_extracts_long_global_timeout_key
27
+ opts = { global_timeout: 60, read: 5 }
28
+
29
+ assert_equal 60, HTTP::Timeout::PerOperation.send(:extract_global_timeout!, opts)
30
+ assert_equal({ read: 5 }, opts)
31
+ end
32
+
33
+ def test_extract_global_timeout_returns_nil_when_no_global_key_present
34
+ opts = { read: 5 }
35
+
36
+ assert_nil HTTP::Timeout::PerOperation.send(:extract_global_timeout!, opts)
37
+ assert_equal({ read: 5 }, opts)
38
+ end
39
+
40
+ def test_extract_global_timeout_raises_when_both_global_and_global_timeout_given
41
+ assert_raises(ArgumentError) do
42
+ HTTP::Timeout::PerOperation.send(:extract_global_timeout!, global: 60, global_timeout: 60)
43
+ end
44
+ end
45
+
46
+ def test_extract_global_timeout_raises_for_non_numeric_global_value
47
+ assert_raises(ArgumentError) do
48
+ HTTP::Timeout::PerOperation.send(:extract_global_timeout!, global: "60")
49
+ end
50
+ end
51
+
52
+ # -- .normalize_options --
53
+
54
+ def test_normalize_options_normalizes_short_read_key
55
+ assert_equal({ read_timeout: 5 }, HTTP::Timeout::PerOperation.normalize_options(read: 5))
56
+ end
57
+
58
+ def test_normalize_options_normalizes_short_write_key
59
+ assert_equal({ write_timeout: 3 }, HTTP::Timeout::PerOperation.normalize_options(write: 3))
60
+ end
61
+
62
+ def test_normalize_options_normalizes_short_connect_key
63
+ assert_equal({ connect_timeout: 1 }, HTTP::Timeout::PerOperation.normalize_options(connect: 1))
64
+ end
65
+
66
+ def test_normalize_options_passes_through_long_form_keys
67
+ assert_equal({ read_timeout: 5 }, HTTP::Timeout::PerOperation.normalize_options(read_timeout: 5))
68
+ end
69
+
70
+ def test_normalize_options_normalizes_all_keys_together
71
+ result = HTTP::Timeout::PerOperation.normalize_options(read: 1, write: 2, connect: 3)
72
+
73
+ assert_equal({ read_timeout: 1, write_timeout: 2, connect_timeout: 3 }, result)
74
+ end
75
+
76
+ def test_normalize_options_accepts_float_values
77
+ assert_equal({ read_timeout: 1.5 }, HTTP::Timeout::PerOperation.normalize_options(read: 1.5))
78
+ end
79
+
80
+ def test_normalize_options_handles_frozen_hashes
81
+ result = HTTP::Timeout::PerOperation.normalize_options({ read: 5 }.freeze)
82
+
83
+ assert_equal({ read_timeout: 5 }, result)
84
+ end
85
+
86
+ def test_normalize_options_raises_when_both_short_and_long_form_given
87
+ assert_raises(ArgumentError) do
88
+ HTTP::Timeout::PerOperation.normalize_options(read: 1, read_timeout: 2)
89
+ end
90
+ end
91
+
92
+ def test_normalize_options_raises_for_non_numeric_values
93
+ assert_raises(ArgumentError) do
94
+ HTTP::Timeout::PerOperation.normalize_options(read: "5")
95
+ end
96
+ end
97
+
98
+ def test_normalize_options_raises_for_unknown_keys
99
+ assert_raises(ArgumentError) do
100
+ HTTP::Timeout::PerOperation.normalize_options(timeout: 5)
101
+ end
102
+ end
103
+
104
+ def test_normalize_options_raises_for_empty_hash
105
+ assert_raises(ArgumentError) do
106
+ HTTP::Timeout::PerOperation.normalize_options({})
107
+ end
108
+ end
109
+
110
+ # -- #connect --
111
+
112
+ def test_connect_sets_tcp_nodelay_when_nodelay_is_true
113
+ setsockopt_args = nil
114
+ tcp_socket = fake(
115
+ setsockopt: ->(*args) { setsockopt_args = args }
116
+ )
117
+
118
+ socket_class = fake(open: tcp_socket)
119
+ @timeout.connect(socket_class, "example.com", 80, nodelay: true)
120
+
121
+ assert_equal [Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1], setsockopt_args
122
+ end
123
+
124
+ # -- #connect_ssl --
125
+
126
+ def test_connect_ssl_completes_without_error
127
+ connected = Object.new
128
+ socket = fake(
129
+ to_io: @io,
130
+ closed?: false,
131
+ connect_nonblock: ->(*) { connected }
132
+ )
133
+ @timeout.instance_variable_set(:@socket, socket)
134
+ @timeout.connect_ssl
135
+ end
136
+
137
+ # -- #readpartial --
138
+
139
+ def test_readpartial_when_read_returns_nil_returns_eof
140
+ socket = fake(
141
+ to_io: @io,
142
+ closed?: false,
143
+ read_nonblock: nil
144
+ )
145
+ @timeout.instance_variable_set(:@socket, socket)
146
+
147
+ assert_equal :eof, @timeout.readpartial(10)
148
+ end
149
+
150
+ def test_readpartial_when_wait_writable_then_data_waits_and_retries
151
+ call_count = 0
152
+ socket = fake(
153
+ to_io: @io,
154
+ closed?: false,
155
+ read_nonblock: ->(*) { (call_count += 1) == 1 ? :wait_writable : "data" }
156
+ )
157
+ @timeout.instance_variable_set(:@socket, socket)
158
+
159
+ assert_equal "data", @timeout.readpartial(10)
160
+ end
161
+
162
+ def test_readpartial_when_wait_writable_and_times_out_raises_timeout_error
163
+ io_with_nil_wait = fake(wait_readable: nil, wait_writable: nil)
164
+ socket = fake(
165
+ to_io: io_with_nil_wait,
166
+ closed?: false,
167
+ read_nonblock: :wait_writable
168
+ )
169
+ @timeout.instance_variable_set(:@socket, socket)
170
+
171
+ err = assert_raises(HTTP::TimeoutError) do
172
+ @timeout.readpartial(10)
173
+ end
174
+ assert_match(/Read timed out/, err.message)
175
+ end
176
+
177
+ # -- #write --
178
+
179
+ def test_write_when_times_out_raises_timeout_error
180
+ io_with_nil_wait = fake(wait_readable: true, wait_writable: nil)
181
+ socket = fake(
182
+ to_io: io_with_nil_wait,
183
+ closed?: false,
184
+ write_nonblock: :wait_writable
185
+ )
186
+ @timeout.instance_variable_set(:@socket, socket)
187
+
188
+ err = assert_raises(HTTP::TimeoutError) do
189
+ @timeout.write("data")
190
+ end
191
+ assert_match(/Write timed out/, err.message)
192
+ end
193
+
194
+ def test_write_when_wait_readable_then_completes_waits_and_retries
195
+ call_count = 0
196
+ socket = fake(
197
+ to_io: @io,
198
+ closed?: false,
199
+ write_nonblock: ->(*) { (call_count += 1) == 1 ? :wait_readable : 4 }
200
+ )
201
+ @timeout.instance_variable_set(:@socket, socket)
202
+
203
+ assert_equal 4, @timeout.write("data")
204
+ end
205
+
206
+ def test_write_when_wait_readable_and_times_out_raises_timeout_error
207
+ io_with_nil_wait = fake(wait_readable: nil, wait_writable: nil)
208
+ socket = fake(
209
+ to_io: io_with_nil_wait,
210
+ closed?: false,
211
+ write_nonblock: :wait_readable
212
+ )
213
+ @timeout.instance_variable_set(:@socket, socket)
214
+
215
+ err = assert_raises(HTTP::TimeoutError) do
216
+ @timeout.write("data")
217
+ end
218
+ assert_match(/Write timed out/, err.message)
219
+ end
220
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class HTTPURINormalizerTest < Minitest::Test
6
+ def test_scheme_lower_cases_scheme
7
+ assert_equal "http", HTTP::URI::NORMALIZER.call("HttP://example.com").scheme
8
+ end
9
+
10
+ def test_hostname_lower_cases_hostname
11
+ assert_equal "example.com", HTTP::URI::NORMALIZER.call("http://EXAMPLE.com").host
12
+ end
13
+
14
+ def test_hostname_decodes_percent_encoded_hostname
15
+ assert_equal "example.com", HTTP::URI::NORMALIZER.call("http://ex%61mple.com").host
16
+ end
17
+
18
+ def test_hostname_removes_trailing_period_in_hostname
19
+ assert_equal "example.com", HTTP::URI::NORMALIZER.call("http://example.com.").host
20
+ end
21
+
22
+ def test_hostname_idn_encodes_non_ascii_hostname
23
+ assert_equal "xn--exmple-cua.com", HTTP::URI::NORMALIZER.call("http://ex\u00E4mple.com").host
24
+ end
25
+
26
+ def test_path_ensures_path_is_not_empty
27
+ assert_equal "/", HTTP::URI::NORMALIZER.call("http://example.com").path
28
+ end
29
+
30
+ def test_path_preserves_double_slashes_in_path
31
+ assert_equal "//a///b", HTTP::URI::NORMALIZER.call("http://example.com//a///b").path
32
+ end
33
+
34
+ def test_path_resolves_single_dot_segments_in_path
35
+ assert_equal "/a/b", HTTP::URI::NORMALIZER.call("http://example.com/a/./b").path
36
+ end
37
+
38
+ def test_path_resolves_double_dot_segments_in_path
39
+ assert_equal "/a/c", HTTP::URI::NORMALIZER.call("http://example.com/a/b/../c").path
40
+ end
41
+
42
+ def test_path_resolves_leading_double_dot_segments_in_path
43
+ assert_equal "/a/b", HTTP::URI::NORMALIZER.call("http://example.com/../a/b").path
44
+ end
45
+
46
+ def test_path_percent_encodes_control_characters_in_path
47
+ assert_equal "/%00%7F%0A", HTTP::URI::NORMALIZER.call("http://example.com/\x00\x7F\n").path
48
+ end
49
+
50
+ def test_path_percent_encodes_space_in_path
51
+ assert_equal "/a%20b", HTTP::URI::NORMALIZER.call("http://example.com/a b").path
52
+ end
53
+
54
+ def test_path_percent_encodes_non_ascii_characters_in_path
55
+ assert_equal "/%E3%82%AD%E3%83%A7", HTTP::URI::NORMALIZER.call("http://example.com/\u30AD\u30E7").path
56
+ end
57
+
58
+ def test_path_does_not_percent_encode_non_special_characters_in_path
59
+ assert_equal "/~.-_!$&()*,;=:@{}", HTTP::URI::NORMALIZER.call("http://example.com/~.-_!$&()*,;=:@{}").path
60
+ end
61
+
62
+ def test_path_preserves_escape_sequences_in_path
63
+ assert_equal "/%41", HTTP::URI::NORMALIZER.call("http://example.com/%41").path
64
+ end
65
+
66
+ def test_query_allows_no_query
67
+ assert_nil HTTP::URI::NORMALIZER.call("http://example.com").query
68
+ end
69
+
70
+ def test_query_percent_encodes_control_characters_in_query
71
+ assert_equal "%00%7F%0A", HTTP::URI::NORMALIZER.call("http://example.com/?\x00\x7F\n").query
72
+ end
73
+
74
+ def test_query_percent_encodes_space_in_query
75
+ assert_equal "a%20b", HTTP::URI::NORMALIZER.call("http://example.com/?a b").query
76
+ end
77
+
78
+ def test_query_percent_encodes_non_ascii_characters_in_query
79
+ assert_equal "%E3%82%AD%E3%83%A7", HTTP::URI::NORMALIZER.call("http://example.com?\u30AD\u30E7").query
80
+ end
81
+
82
+ def test_query_does_not_percent_encode_non_special_characters_in_query
83
+ assert_equal "~.-_!$&()*,;=:@{}?", HTTP::URI::NORMALIZER.call("http://example.com/?~.-_!$&()*,;=:@{}?").query
84
+ end
85
+
86
+ def test_query_preserves_escape_sequences_in_query
87
+ assert_equal "%41", HTTP::URI::NORMALIZER.call("http://example.com/?%41").query
88
+ end
89
+ end