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,1533 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class HTTPConnectionTest < Minitest::Test
6
+ cover "HTTP::Connection*"
7
+
8
+ def build_req(uri: "http://example.com/", verb: :get, headers: {}, **)
9
+ HTTP::Request.new(verb: verb, uri: uri, headers: headers, **)
10
+ end
11
+
12
+ def build_connection(socket: nil, **)
13
+ socket ||= fake(connect: nil, close: nil)
14
+ timeout_class = fake(new: socket)
15
+ req = build_req
16
+ opts_obj = HTTP::Options.new(timeout_class: timeout_class, **)
17
+ HTTP::Connection.new(req, opts_obj)
18
+ end
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # #initialize
22
+ # ---------------------------------------------------------------------------
23
+ def test_initialize_initializes_state_from_options
24
+ connection = build_connection
25
+
26
+ refute_predicate connection, :failed_proxy_connect?
27
+ assert_predicate connection, :finished_request?
28
+ assert_predicate connection, :expired?
29
+ end
30
+
31
+ def test_initialize_raises_connection_error_on_io_error_during_connect
32
+ req = build_req
33
+ err_socket = fake(
34
+ connect: ->(*) { raise IOError, "connection refused" }
35
+ )
36
+ err_timeout_class = fake(new: err_socket)
37
+ err_opts = HTTP::Options.new(timeout_class: err_timeout_class)
38
+
39
+ err = assert_raises(HTTP::ConnectionError) do
40
+ HTTP::Connection.new(req, err_opts)
41
+ end
42
+ assert_includes err.message, "failed to connect"
43
+ assert_includes err.message, "connection refused"
44
+ refute_nil err.backtrace
45
+ end
46
+
47
+ def test_initialize_raises_connection_error_on_socket_error_during_connect
48
+ req = build_req
49
+ err_socket = fake(
50
+ connect: ->(*) { raise SocketError, "dns failure" }
51
+ )
52
+ err_timeout_class = fake(new: err_socket)
53
+ err_opts = HTTP::Options.new(timeout_class: err_timeout_class)
54
+
55
+ err = assert_raises(HTTP::ConnectionError) do
56
+ HTTP::Connection.new(req, err_opts)
57
+ end
58
+ assert_includes err.message, "failed to connect"
59
+ assert_includes err.message, "dns failure"
60
+ end
61
+
62
+ def test_initialize_raises_connection_error_on_system_call_error_during_connect
63
+ req = build_req
64
+ err_socket = fake(
65
+ connect: ->(*) { raise Errno::ECONNREFUSED, "refused" }
66
+ )
67
+ err_timeout_class = fake(new: err_socket)
68
+ err_opts = HTTP::Options.new(timeout_class: err_timeout_class)
69
+
70
+ err = assert_raises(HTTP::ConnectionError) do
71
+ HTTP::Connection.new(req, err_opts)
72
+ end
73
+ assert_includes err.message, "failed to connect"
74
+ end
75
+
76
+ def test_initialize_timeout_error_closes_socket_and_re_raises
77
+ https_req = build_req(uri: "https://example.com/")
78
+ closed = false
79
+ tls_socket = fake(
80
+ connect: nil,
81
+ close: -> { closed = true },
82
+ start_tls: ->(*) { raise HTTP::TimeoutError },
83
+ closed?: false
84
+ )
85
+ tls_timeout_class = fake(new: tls_socket)
86
+ tls_opts = HTTP::Options.new(timeout_class: tls_timeout_class)
87
+
88
+ assert_raises(HTTP::TimeoutError) do
89
+ HTTP::Connection.new(https_req, tls_opts)
90
+ end
91
+ assert closed, "socket should have been closed"
92
+ end
93
+
94
+ def test_initialize_io_timeout_error_converts_to_connect_timeout_error
95
+ req = build_req
96
+ io_timeout_socket = fake(
97
+ connect: lambda { |*|
98
+ raise IO::TimeoutError, "Connect timed out!"
99
+ },
100
+ close: nil,
101
+ closed?: false
102
+ )
103
+ io_timeout_class = fake(new: io_timeout_socket)
104
+ io_opts = HTTP::Options.new(timeout_class: io_timeout_class)
105
+
106
+ err = assert_raises(HTTP::ConnectTimeoutError) do
107
+ HTTP::Connection.new(req, io_opts)
108
+ end
109
+ assert_equal "Connect timed out!", err.message
110
+ end
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # #send_request
114
+ # ---------------------------------------------------------------------------
115
+ def test_send_request_streams_request_and_sets_pending_state
116
+ req = build_req
117
+ sr_req = build_req(uri: "http://example.com/path")
118
+
119
+ write_socket = fake(connect: nil, close: nil, write: proc(&:bytesize))
120
+ write_timeout_class = fake(new: write_socket)
121
+ write_opts = HTTP::Options.new(timeout_class: write_timeout_class)
122
+ conn = HTTP::Connection.new(req, write_opts)
123
+
124
+ assert_predicate conn, :finished_request?
125
+
126
+ conn.send_request(sr_req)
127
+
128
+ refute_predicate conn, :finished_request?
129
+ assert conn.instance_variable_get(:@pending_response)
130
+ refute conn.instance_variable_get(:@pending_request)
131
+ end
132
+
133
+ def test_send_request_raises_state_error_when_request_already_pending
134
+ connection = build_connection
135
+ connection.instance_variable_set(:@pending_request, true)
136
+ sr_req = build_req
137
+ err = assert_raises(HTTP::StateError) { connection.send_request(sr_req) }
138
+ assert_includes err.message, "response is pending"
139
+ end
140
+
141
+ def test_send_request_sets_pending_request_true_before_streaming_then_false_after
142
+ req = build_req
143
+ pending_during_stream = nil
144
+ conn = nil
145
+ stream_socket = fake(
146
+ connect: nil,
147
+ close: nil,
148
+ write: proc { |data|
149
+ pending_during_stream = conn.instance_variable_get(:@pending_request)
150
+ data.bytesize
151
+ }
152
+ )
153
+ stream_timeout_class = fake(new: stream_socket)
154
+ stream_opts = HTTP::Options.new(timeout_class: stream_timeout_class)
155
+ conn = HTTP::Connection.new(req, stream_opts)
156
+
157
+ sr_req = build_req
158
+ conn.send_request(sr_req)
159
+
160
+ assert pending_during_stream, "pending_request should be true during streaming"
161
+ assert conn.instance_variable_get(:@pending_response)
162
+ refute conn.instance_variable_get(:@pending_request)
163
+ end
164
+
165
+ def test_send_request_calls_req_stream_with_socket
166
+ req = build_req
167
+ stream_args = nil
168
+ sr_req = Minitest::Mock.new
169
+ sr_req.expect(:stream, nil) do |s|
170
+ stream_args = s
171
+ true
172
+ end
173
+
174
+ write_socket = fake(connect: nil, close: nil, write: lambda(&:bytesize))
175
+ write_timeout_class = fake(new: write_socket)
176
+ write_opts = HTTP::Options.new(timeout_class: write_timeout_class)
177
+ conn = HTTP::Connection.new(req, write_opts)
178
+
179
+ conn.send_request(sr_req)
180
+
181
+ assert_same conn.instance_variable_get(:@socket), stream_args
182
+ sr_req.verify
183
+ end
184
+
185
+ # ---------------------------------------------------------------------------
186
+ # #readpartial
187
+ # ---------------------------------------------------------------------------
188
+ def test_readpartial_raises_eof_error_when_no_response_pending
189
+ connection = build_connection
190
+
191
+ assert_raises(EOFError) { connection.readpartial }
192
+ end
193
+
194
+ def test_readpartial_reads_data_from_socket_and_returns_chunks
195
+ req = build_req
196
+ call_count = 0
197
+ responses = [
198
+ "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello",
199
+ :eof
200
+ ]
201
+ rp_socket = fake(
202
+ connect: nil,
203
+ close: nil,
204
+ readpartial: proc {
205
+ idx = [call_count, responses.length - 1].min
206
+ responses[idx].tap { call_count += 1 }
207
+ },
208
+ closed?: proc { call_count >= responses.length }
209
+ )
210
+ rp_timeout_class = fake(new: rp_socket)
211
+ rp_opts = HTTP::Options.new(timeout_class: rp_timeout_class)
212
+ conn = HTTP::Connection.new(req, rp_opts)
213
+ conn.instance_variable_set(:@pending_response, true)
214
+
215
+ conn.read_headers!
216
+ chunk = conn.readpartial
217
+
218
+ assert_equal "hello", chunk
219
+ end
220
+
221
+ def test_readpartial_reads_data_in_parts_and_finishes
222
+ req = build_req
223
+ call_count = 0
224
+ responses = [
225
+ "HTTP/1.1 200 OK\r\nContent-Type: text\r\n\r\n",
226
+ "1", "23", "456", "78", "9", "0", :eof
227
+ ]
228
+ rp_socket = fake(
229
+ connect: nil,
230
+ close: nil,
231
+ readpartial: proc {
232
+ idx = [call_count, responses.length - 1].min
233
+ responses[idx].tap { call_count += 1 }
234
+ },
235
+ closed?: proc { call_count >= responses.length }
236
+ )
237
+ rp_timeout_class = fake(new: rp_socket)
238
+ rp_opts = HTTP::Options.new(timeout_class: rp_timeout_class)
239
+ conn = HTTP::Connection.new(req, rp_opts)
240
+ conn.instance_variable_set(:@pending_response, true)
241
+
242
+ conn.read_headers!
243
+ buffer = +""
244
+ begin
245
+ loop do
246
+ s = conn.readpartial(3)
247
+ refute_predicate conn, :finished_request? if s != ""
248
+ buffer << s
249
+ end
250
+ rescue EOFError
251
+ # Expected
252
+ end
253
+
254
+ assert_equal "1234567890", buffer
255
+ assert_predicate conn, :finished_request?
256
+ end
257
+
258
+ def test_readpartial_fills_outbuf_when_provided
259
+ req = build_req
260
+ call_count = 0
261
+ responses = [
262
+ "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello",
263
+ :eof
264
+ ]
265
+ ob_socket = fake(
266
+ connect: nil,
267
+ close: nil,
268
+ readpartial: proc {
269
+ idx = [call_count, responses.length - 1].min
270
+ responses[idx].tap { call_count += 1 }
271
+ },
272
+ closed?: proc { call_count >= responses.length }
273
+ )
274
+ ob_timeout_class = fake(new: ob_socket)
275
+ ob_opts = HTTP::Options.new(timeout_class: ob_timeout_class)
276
+ conn = HTTP::Connection.new(req, ob_opts)
277
+ conn.instance_variable_set(:@pending_response, true)
278
+
279
+ conn.read_headers!
280
+ outbuf = +""
281
+ result = conn.readpartial(16_384, outbuf)
282
+
283
+ assert_equal "hello", outbuf
284
+ assert_same outbuf, result
285
+ end
286
+
287
+ def test_readpartial_uses_size_parameter_when_reading_from_socket
288
+ req = build_req
289
+ call_count = 0
290
+ read_sizes = []
291
+ responses = [
292
+ "HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\n",
293
+ "helloworld",
294
+ :eof
295
+ ]
296
+ sz_socket = fake(
297
+ connect: nil,
298
+ close: nil,
299
+ readpartial: proc { |size, *|
300
+ read_sizes << size
301
+ idx = [call_count, responses.length - 1].min
302
+ responses[idx].tap { call_count += 1 }
303
+ },
304
+ closed?: proc { call_count >= responses.length }
305
+ )
306
+ sz_timeout_class = fake(new: sz_socket)
307
+ sz_opts = HTTP::Options.new(timeout_class: sz_timeout_class)
308
+ conn = HTTP::Connection.new(req, sz_opts)
309
+ conn.instance_variable_set(:@pending_response, true)
310
+
311
+ conn.read_headers!
312
+ conn.readpartial(42)
313
+
314
+ assert_includes read_sizes, 42
315
+ end
316
+
317
+ def test_readpartial_detects_premature_eof_on_framed_content_length_response
318
+ req = build_req
319
+ call_count = 0
320
+ responses = [
321
+ "HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\nhello",
322
+ :eof
323
+ ]
324
+ eof_socket = fake(
325
+ connect: nil,
326
+ close: nil,
327
+ readpartial: proc {
328
+ idx = [call_count, responses.length - 1].min
329
+ responses[idx].tap { call_count += 1 }
330
+ },
331
+ closed?: false
332
+ )
333
+ eof_timeout_class = fake(new: eof_socket)
334
+ eof_opts = HTTP::Options.new(timeout_class: eof_timeout_class)
335
+ conn = HTTP::Connection.new(req, eof_opts)
336
+ conn.instance_variable_set(:@pending_response, true)
337
+
338
+ conn.read_headers!
339
+ chunk = conn.readpartial
340
+
341
+ assert_equal "hello", chunk
342
+ err = assert_raises(HTTP::ConnectionError) { conn.readpartial }
343
+ assert_includes err.message, "response body ended prematurely"
344
+ end
345
+
346
+ def test_readpartial_detects_premature_eof_on_chunked_response
347
+ req = build_req
348
+ call_count = 0
349
+ responses = [
350
+ "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n",
351
+ :eof
352
+ ]
353
+ eof_socket = fake(
354
+ connect: nil,
355
+ close: nil,
356
+ readpartial: proc {
357
+ idx = [call_count, responses.length - 1].min
358
+ responses[idx].tap { call_count += 1 }
359
+ },
360
+ closed?: false
361
+ )
362
+ eof_timeout_class = fake(new: eof_socket)
363
+ eof_opts = HTTP::Options.new(timeout_class: eof_timeout_class)
364
+ conn = HTTP::Connection.new(req, eof_opts)
365
+ conn.instance_variable_set(:@pending_response, true)
366
+
367
+ conn.read_headers!
368
+ chunk = conn.readpartial
369
+
370
+ assert_equal "hello", chunk
371
+ err = assert_raises(HTTP::ConnectionError) { conn.readpartial }
372
+ assert_includes err.message, "response body ended prematurely"
373
+ end
374
+
375
+ def test_readpartial_finishes_cleanly_when_not_framed
376
+ req = build_req
377
+ call_count = 0
378
+ responses = [
379
+ "HTTP/1.1 200 OK\r\n\r\nhello",
380
+ :eof
381
+ ]
382
+ unframed_socket = fake(
383
+ connect: nil,
384
+ close: nil,
385
+ readpartial: proc {
386
+ idx = [call_count, responses.length - 1].min
387
+ responses[idx].tap { call_count += 1 }
388
+ },
389
+ closed?: false
390
+ )
391
+ unframed_timeout_class = fake(new: unframed_socket)
392
+ unframed_opts = HTTP::Options.new(timeout_class: unframed_timeout_class)
393
+ conn = HTTP::Connection.new(req, unframed_opts)
394
+ conn.instance_variable_set(:@pending_response, true)
395
+
396
+ conn.read_headers!
397
+ chunk = conn.readpartial
398
+
399
+ assert_equal "hello", chunk
400
+ # Should not raise premature EOF because body is not framed
401
+ chunk2 = conn.readpartial
402
+
403
+ assert_equal "", chunk2
404
+ end
405
+
406
+ def test_readpartial_finishes_response_when_parser_says_finished
407
+ req = build_req
408
+ call_count = 0
409
+ responses = [
410
+ "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n",
411
+ "hello",
412
+ # After reading "hello", parser is finished (Content-Length satisfied)
413
+ # even if we haven't seen :eof yet
414
+ "more data that shouldn't matter",
415
+ :eof
416
+ ]
417
+ pf_socket = fake(
418
+ connect: nil,
419
+ close: nil,
420
+ readpartial: proc {
421
+ idx = [call_count, responses.length - 1].min
422
+ responses[idx].tap { call_count += 1 }
423
+ },
424
+ closed?: false
425
+ )
426
+ pf_timeout_class = fake(new: pf_socket)
427
+ pf_opts = HTTP::Options.new(timeout_class: pf_timeout_class)
428
+ conn = HTTP::Connection.new(req, pf_opts)
429
+ conn.instance_variable_set(:@pending_response, true)
430
+
431
+ conn.read_headers!
432
+ chunk = conn.readpartial
433
+
434
+ assert_equal "hello", chunk
435
+ # After reading exactly Content-Length bytes, parser.finished? should be true
436
+ # and finish_response should be called
437
+ assert_predicate conn, :finished_request?
438
+ end
439
+
440
+ def test_readpartial_returns_binary_empty_string_when_parser_has_no_data
441
+ req = build_req
442
+ call_count = 0
443
+ # Headers with no body data, then immediately EOF
444
+ responses = [
445
+ "HTTP/1.1 200 OK\r\n\r\n",
446
+ :eof
447
+ ]
448
+ empty_socket = fake(
449
+ connect: nil,
450
+ close: nil,
451
+ readpartial: proc {
452
+ idx = [call_count, responses.length - 1].min
453
+ responses[idx].tap { call_count += 1 }
454
+ },
455
+ closed?: false
456
+ )
457
+ empty_timeout_class = fake(new: empty_socket)
458
+ empty_opts = HTTP::Options.new(timeout_class: empty_timeout_class)
459
+ conn = HTTP::Connection.new(req, empty_opts)
460
+ conn.instance_variable_set(:@pending_response, true)
461
+
462
+ conn.read_headers!
463
+ chunk = conn.readpartial
464
+ # Should return binary empty string "".b
465
+ assert_equal Encoding::ASCII_8BIT, chunk.encoding
466
+ end
467
+
468
+ # ---------------------------------------------------------------------------
469
+ # #read_headers!
470
+ # ---------------------------------------------------------------------------
471
+ def test_read_headers_populates_headers_preserving_casing
472
+ req = build_req
473
+ raw_response = "HTTP/1.1 200 OK\r\nContent-Type: text\r\nfoo_bar: 123\r\n\r\n"
474
+ read_socket = fake(connect: nil, close: nil, readpartial: raw_response)
475
+ read_timeout_class = fake(new: read_socket)
476
+ read_opts = HTTP::Options.new(timeout_class: read_timeout_class)
477
+ conn = HTTP::Connection.new(req, read_opts)
478
+ conn.instance_variable_set(:@pending_response, true)
479
+
480
+ conn.read_headers!
481
+
482
+ assert_equal "text", conn.headers["Content-Type"]
483
+ assert_equal "123", conn.headers["Foo-Bar"]
484
+ assert_equal "123", conn.headers["foo_bar"]
485
+ end
486
+
487
+ def test_read_headers_raises_response_header_error_on_eof_before_headers_complete
488
+ req = build_req
489
+ eof_socket = fake(connect: nil, close: nil, readpartial: :eof)
490
+ eof_timeout_class = fake(new: eof_socket)
491
+ eof_opts = HTTP::Options.new(timeout_class: eof_timeout_class)
492
+ conn = HTTP::Connection.new(req, eof_opts)
493
+ conn.instance_variable_set(:@pending_response, true)
494
+
495
+ err = assert_raises(HTTP::ResponseHeaderError) { conn.read_headers! }
496
+ assert_includes err.message, "couldn't read response headers"
497
+ end
498
+
499
+ def test_read_headers_calls_set_keep_alive_after_reading
500
+ req = build_req
501
+ raw_response = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"
502
+ read_socket = fake(connect: nil, close: nil, readpartial: raw_response, closed?: false)
503
+ read_timeout_class = fake(new: read_socket)
504
+ read_opts = HTTP::Options.new(timeout_class: read_timeout_class)
505
+ conn = HTTP::Connection.new(req, read_opts)
506
+ conn.instance_variable_set(:@pending_response, true)
507
+ conn.instance_variable_set(:@persistent, true)
508
+
509
+ conn.read_headers!
510
+
511
+ assert_predicate conn, :keep_alive?
512
+ end
513
+
514
+ def test_read_headers_passes_buffer_size_to_read_more
515
+ req = build_req
516
+ read_sizes = []
517
+ call_count = 0
518
+ responses = [
519
+ "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
520
+ :eof
521
+ ]
522
+ bs_socket = fake(
523
+ connect: nil,
524
+ close: nil,
525
+ readpartial: proc { |size, *|
526
+ read_sizes << size
527
+ idx = [call_count, responses.length - 1].min
528
+ responses[idx].tap { call_count += 1 }
529
+ }
530
+ )
531
+ bs_timeout_class = fake(new: bs_socket)
532
+ bs_opts = HTTP::Options.new(timeout_class: bs_timeout_class)
533
+ conn = HTTP::Connection.new(req, bs_opts)
534
+ conn.instance_variable_set(:@pending_response, true)
535
+
536
+ conn.read_headers!
537
+
538
+ assert_includes read_sizes, HTTP::Connection::BUFFER_SIZE
539
+ end
540
+
541
+ # ---------------------------------------------------------------------------
542
+ # #read_headers! with 1xx informational response
543
+ # ---------------------------------------------------------------------------
544
+ def test_read_headers_skips_100_continue_and_returns_final_response
545
+ req = build_req
546
+ call_count = 0
547
+ responses = [
548
+ "HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello",
549
+ :eof
550
+ ]
551
+ info_socket = fake(
552
+ connect: nil,
553
+ close: nil,
554
+ readpartial: proc {
555
+ idx = [call_count, responses.length - 1].min
556
+ responses[idx].tap { call_count += 1 }
557
+ }
558
+ )
559
+ info_timeout_class = fake(new: info_socket)
560
+ info_opts = HTTP::Options.new(timeout_class: info_timeout_class)
561
+ conn = HTTP::Connection.new(req, info_opts)
562
+ conn.instance_variable_set(:@pending_response, true)
563
+
564
+ conn.read_headers!
565
+
566
+ assert_equal 200, conn.status_code
567
+ assert_equal "5", conn.headers["Content-Length"]
568
+ end
569
+
570
+ def test_read_headers_skips_100_continue_in_small_chunks
571
+ req = build_req
572
+ raw = "HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"
573
+ chunks = raw.chars + [:eof]
574
+ call_count = 0
575
+ chunked_socket = fake(
576
+ connect: nil,
577
+ close: nil,
578
+ readpartial: proc {
579
+ idx = [call_count, chunks.length - 1].min
580
+ chunks[idx].tap { call_count += 1 }
581
+ }
582
+ )
583
+ chunked_timeout_class = fake(new: chunked_socket)
584
+ chunked_opts = HTTP::Options.new(timeout_class: chunked_timeout_class)
585
+ conn = HTTP::Connection.new(req, chunked_opts)
586
+ conn.instance_variable_set(:@pending_response, true)
587
+
588
+ conn.read_headers!
589
+
590
+ assert_equal 200, conn.status_code
591
+ assert_equal "5", conn.headers["Content-Length"]
592
+ end
593
+
594
+ # ---------------------------------------------------------------------------
595
+ # #send_request when response already pending
596
+ # ---------------------------------------------------------------------------
597
+ def test_send_request_with_pending_response_boolean_closes_and_proceeds
598
+ req = build_req
599
+ socket = fake(connect: nil, close: nil, closed?: false, write: lambda(&:bytesize))
600
+ timeout_class = fake(new: socket)
601
+ opts = HTTP::Options.new(timeout_class: timeout_class)
602
+ connection = HTTP::Connection.new(req, opts)
603
+ connection.instance_variable_set(:@pending_response, true)
604
+ new_req = build_req
605
+ connection.send_request(new_req)
606
+
607
+ assert connection.instance_variable_get(:@pending_response)
608
+ end
609
+
610
+ def test_send_request_with_large_content_length_pending_closes_instead_of_flushing
611
+ req = build_req
612
+ socket = fake(connect: nil, close: nil, closed?: false, write: lambda(&:bytesize))
613
+ timeout_class = fake(new: socket)
614
+ opts = HTTP::Options.new(timeout_class: timeout_class)
615
+ connection = HTTP::Connection.new(req, opts)
616
+ response = fake(content_length: HTTP::Connection::MAX_FLUSH_SIZE + 1, flush: nil)
617
+ connection.instance_variable_set(:@pending_response, response)
618
+ new_req = build_req
619
+ connection.send_request(new_req)
620
+
621
+ assert connection.instance_variable_get(:@pending_response)
622
+ end
623
+
624
+ def test_send_request_when_flushing_raises_closes_and_proceeds
625
+ req = build_req
626
+ socket = fake(connect: nil, close: nil, closed?: false, write: lambda(&:bytesize))
627
+ timeout_class = fake(new: socket)
628
+ opts = HTTP::Options.new(timeout_class: timeout_class)
629
+ connection = HTTP::Connection.new(req, opts)
630
+ response = fake(content_length: nil, flush: -> { raise "boom" })
631
+ connection.instance_variable_set(:@pending_response, response)
632
+ new_req = build_req
633
+ connection.send_request(new_req)
634
+
635
+ assert connection.instance_variable_get(:@pending_response)
636
+ end
637
+
638
+ def test_send_request_with_small_body_pending_flushes_response_body
639
+ req = build_req
640
+ socket = fake(connect: nil, close: nil, closed?: false, write: lambda(&:bytesize))
641
+ timeout_class = fake(new: socket)
642
+ opts = HTTP::Options.new(timeout_class: timeout_class)
643
+ connection = HTTP::Connection.new(req, opts)
644
+ flushed = false
645
+ response = fake(content_length: 100, flush: -> { flushed = true })
646
+ connection.instance_variable_set(:@pending_response, response)
647
+ new_req = build_req
648
+ connection.send_request(new_req)
649
+
650
+ assert flushed
651
+ end
652
+
653
+ # ---------------------------------------------------------------------------
654
+ # #finish_response
655
+ # ---------------------------------------------------------------------------
656
+ def test_finish_response_closes_socket_when_not_keeping_alive
657
+ req = build_req
658
+ call_count = 0
659
+ responses = [
660
+ "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello",
661
+ :eof
662
+ ]
663
+ closed = false
664
+ fr_socket = fake(
665
+ connect: nil,
666
+ close: -> { closed = true },
667
+ readpartial: proc {
668
+ idx = [call_count, responses.length - 1].min
669
+ responses[idx].tap { call_count += 1 }
670
+ },
671
+ closed?: proc { closed }
672
+ )
673
+ fr_timeout_class = fake(new: fr_socket)
674
+ fr_opts = HTTP::Options.new(timeout_class: fr_timeout_class)
675
+ conn = HTTP::Connection.new(req, fr_opts)
676
+ conn.instance_variable_set(:@pending_response, true)
677
+
678
+ conn.read_headers!
679
+ conn.finish_response
680
+
681
+ assert closed, "socket should be closed when not keep-alive"
682
+ assert_predicate conn, :finished_request?
683
+ end
684
+
685
+ def test_finish_response_does_not_close_socket_when_keeping_alive
686
+ req = build_req
687
+ call_count = 0
688
+ responses = [
689
+ "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello",
690
+ :eof
691
+ ]
692
+ closed = false
693
+ ka_socket = fake(
694
+ connect: nil,
695
+ close: -> { closed = true },
696
+ readpartial: proc {
697
+ idx = [call_count, responses.length - 1].min
698
+ responses[idx].tap { call_count += 1 }
699
+ },
700
+ closed?: proc { closed }
701
+ )
702
+ ka_timeout_class = fake(new: ka_socket)
703
+ ka_opts = HTTP::Options.new(timeout_class: ka_timeout_class, keep_alive_timeout: 10)
704
+ conn = HTTP::Connection.new(req, ka_opts)
705
+ conn.instance_variable_set(:@pending_response, true)
706
+ conn.instance_variable_set(:@persistent, true)
707
+
708
+ conn.read_headers!
709
+
710
+ assert_predicate conn, :keep_alive?
711
+ conn.finish_response
712
+
713
+ refute closed, "socket should NOT be closed when keep-alive"
714
+ assert_predicate conn, :finished_request?
715
+ end
716
+
717
+ def test_finish_response_resets_parser_for_reuse
718
+ req = build_req
719
+ call_count = 0
720
+ responses = [
721
+ "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
722
+ :eof
723
+ ]
724
+ pr_socket = fake(
725
+ connect: nil,
726
+ close: nil,
727
+ readpartial: proc {
728
+ idx = [call_count, responses.length - 1].min
729
+ responses[idx].tap { call_count += 1 }
730
+ },
731
+ closed?: false
732
+ )
733
+ pr_timeout_class = fake(new: pr_socket)
734
+ pr_opts = HTTP::Options.new(timeout_class: pr_timeout_class)
735
+ conn = HTTP::Connection.new(req, pr_opts)
736
+ conn.instance_variable_set(:@pending_response, true)
737
+ conn.instance_variable_set(:@persistent, true)
738
+
739
+ conn.read_headers!
740
+
741
+ assert_equal 200, conn.status_code
742
+
743
+ conn.finish_response
744
+
745
+ # Parser should be reset -- feeding new response should work
746
+ parser = conn.instance_variable_get(:@parser)
747
+ parser << "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n"
748
+
749
+ assert_equal 404, conn.status_code
750
+ end
751
+
752
+ def test_finish_response_calls_reset_counter_when_socket_responds_to_it
753
+ req = build_req
754
+ counter_reset = false
755
+ call_count = 0
756
+ responses = [
757
+ "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
758
+ :eof
759
+ ]
760
+ rc_socket = fake(
761
+ connect: nil,
762
+ close: nil,
763
+ readpartial: proc {
764
+ idx = [call_count, responses.length - 1].min
765
+ responses[idx].tap { call_count += 1 }
766
+ },
767
+ closed?: false,
768
+ reset_counter: -> { counter_reset = true }
769
+ )
770
+ rc_timeout_class = fake(new: rc_socket)
771
+ rc_opts = HTTP::Options.new(timeout_class: rc_timeout_class)
772
+ conn = HTTP::Connection.new(req, rc_opts)
773
+ conn.instance_variable_set(:@pending_response, true)
774
+ conn.instance_variable_set(:@persistent, true)
775
+
776
+ conn.read_headers!
777
+ conn.finish_response
778
+
779
+ assert counter_reset, "reset_counter should have been called"
780
+ end
781
+
782
+ def test_finish_response_does_not_call_reset_counter_when_socket_does_not_respond
783
+ req = build_req
784
+ call_count = 0
785
+ responses = [
786
+ "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
787
+ :eof
788
+ ]
789
+ # Socket without reset_counter method
790
+ no_rc_socket = fake(
791
+ connect: nil,
792
+ close: nil,
793
+ readpartial: proc {
794
+ idx = [call_count, responses.length - 1].min
795
+ responses[idx].tap { call_count += 1 }
796
+ },
797
+ closed?: false
798
+ )
799
+ no_rc_timeout_class = fake(new: no_rc_socket)
800
+ no_rc_opts = HTTP::Options.new(timeout_class: no_rc_timeout_class)
801
+ conn = HTTP::Connection.new(req, no_rc_opts)
802
+ conn.instance_variable_set(:@pending_response, true)
803
+ conn.instance_variable_set(:@persistent, true)
804
+
805
+ conn.read_headers!
806
+ # Should not raise even though socket lacks reset_counter
807
+ conn.finish_response
808
+
809
+ assert_predicate conn, :finished_request?
810
+ end
811
+
812
+ def test_finish_response_resets_timer_for_persistent_connections
813
+ req = build_req
814
+ call_count = 0
815
+ responses = [
816
+ "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
817
+ :eof
818
+ ]
819
+ rt_socket = fake(
820
+ connect: nil,
821
+ close: nil,
822
+ readpartial: proc {
823
+ idx = [call_count, responses.length - 1].min
824
+ responses[idx].tap { call_count += 1 }
825
+ },
826
+ closed?: false
827
+ )
828
+ rt_timeout_class = fake(new: rt_socket)
829
+ rt_opts = HTTP::Options.new(timeout_class: rt_timeout_class, keep_alive_timeout: 30)
830
+ conn = HTTP::Connection.new(req, rt_opts)
831
+ conn.instance_variable_set(:@pending_response, true)
832
+ conn.instance_variable_set(:@persistent, true)
833
+
834
+ conn.read_headers!
835
+
836
+ before = Time.now
837
+ conn.finish_response
838
+ after = Time.now
839
+
840
+ expires = conn.instance_variable_get(:@conn_expires_at)
841
+
842
+ assert_operator expires, :>=, before + 30
843
+ assert_operator expires, :<=, after + 30
844
+ end
845
+
846
+ # ---------------------------------------------------------------------------
847
+ # #close
848
+ # ---------------------------------------------------------------------------
849
+ def test_close_when_socket_is_nil_raises_no_method_error
850
+ conn = HTTP::Connection.allocate
851
+ conn.instance_variable_set(:@socket, nil)
852
+ conn.instance_variable_set(:@pending_response, false)
853
+ conn.instance_variable_set(:@pending_request, false)
854
+ assert_raises(NoMethodError) { conn.close }
855
+ end
856
+
857
+ def test_close_closes_socket_and_clears_pending_state
858
+ req = build_req
859
+ closed = false
860
+ close_socket = fake(
861
+ connect: nil,
862
+ close: -> { closed = true },
863
+ closed?: proc { closed }
864
+ )
865
+ close_timeout_class = fake(new: close_socket)
866
+ close_opts = HTTP::Options.new(timeout_class: close_timeout_class)
867
+ conn = HTTP::Connection.new(req, close_opts)
868
+ conn.instance_variable_set(:@pending_response, true)
869
+ conn.instance_variable_set(:@pending_request, true)
870
+
871
+ conn.close
872
+
873
+ assert closed, "socket should be closed"
874
+ refute conn.instance_variable_get(:@pending_response)
875
+ refute conn.instance_variable_get(:@pending_request)
876
+ end
877
+
878
+ def test_close_does_not_close_already_closed_socket
879
+ req = build_req
880
+ close_count = 0
881
+ already_closed_socket = fake(
882
+ connect: nil,
883
+ close: -> { close_count += 1 },
884
+ closed?: true
885
+ )
886
+ ac_timeout_class = fake(new: already_closed_socket)
887
+ ac_opts = HTTP::Options.new(timeout_class: ac_timeout_class)
888
+ conn = HTTP::Connection.new(req, ac_opts)
889
+
890
+ conn.close
891
+
892
+ assert_equal 0, close_count
893
+ end
894
+
895
+ # ---------------------------------------------------------------------------
896
+ # #finished_request?
897
+ # ---------------------------------------------------------------------------
898
+ def test_finished_request_returns_true_when_neither_pending
899
+ connection = build_connection
900
+
901
+ assert_predicate connection, :finished_request?
902
+ end
903
+
904
+ def test_finished_request_returns_false_when_pending_response
905
+ connection = build_connection
906
+ connection.instance_variable_set(:@pending_response, true)
907
+
908
+ refute_predicate connection, :finished_request?
909
+ end
910
+
911
+ def test_finished_request_returns_false_when_pending_request
912
+ connection = build_connection
913
+ connection.instance_variable_set(:@pending_request, true)
914
+
915
+ refute_predicate connection, :finished_request?
916
+ end
917
+
918
+ def test_finished_request_returns_false_when_both_pending
919
+ connection = build_connection
920
+ connection.instance_variable_set(:@pending_request, true)
921
+ connection.instance_variable_set(:@pending_response, true)
922
+
923
+ refute_predicate connection, :finished_request?
924
+ end
925
+
926
+ # ---------------------------------------------------------------------------
927
+ # #keep_alive?
928
+ # ---------------------------------------------------------------------------
929
+ def test_keep_alive_returns_false_when_keep_alive_is_false
930
+ connection = build_connection
931
+ connection.instance_variable_set(:@keep_alive, false)
932
+
933
+ refute_predicate connection, :keep_alive?
934
+ end
935
+
936
+ def test_keep_alive_returns_false_when_socket_is_closed
937
+ req = build_req
938
+ closed_socket = fake(connect: nil, close: nil, closed?: true)
939
+ closed_timeout_class = fake(new: closed_socket)
940
+ closed_opts = HTTP::Options.new(timeout_class: closed_timeout_class)
941
+ conn = HTTP::Connection.new(req, closed_opts)
942
+ conn.instance_variable_set(:@keep_alive, true)
943
+
944
+ refute_predicate conn, :keep_alive?
945
+ end
946
+
947
+ def test_keep_alive_returns_true_when_keep_alive_and_socket_open
948
+ req = build_req
949
+ open_socket = fake(connect: nil, close: nil, closed?: false)
950
+ open_timeout_class = fake(new: open_socket)
951
+ open_opts = HTTP::Options.new(timeout_class: open_timeout_class)
952
+ conn = HTTP::Connection.new(req, open_opts)
953
+ conn.instance_variable_set(:@keep_alive, true)
954
+
955
+ assert_predicate conn, :keep_alive?
956
+ end
957
+
958
+ # ---------------------------------------------------------------------------
959
+ # #expired?
960
+ # ---------------------------------------------------------------------------
961
+ def test_expired_returns_true_when_conn_expires_at_is_nil
962
+ connection = build_connection
963
+
964
+ assert_predicate connection, :expired?
965
+ end
966
+
967
+ def test_expired_returns_true_when_connection_has_expired
968
+ connection = build_connection
969
+ connection.instance_variable_set(:@conn_expires_at, Time.now - 1)
970
+
971
+ assert_predicate connection, :expired?
972
+ end
973
+
974
+ def test_expired_returns_false_when_connection_has_not_expired
975
+ connection = build_connection
976
+ connection.instance_variable_set(:@conn_expires_at, Time.now + 60)
977
+
978
+ refute_predicate connection, :expired?
979
+ end
980
+
981
+ # ---------------------------------------------------------------------------
982
+ # keep_alive behavior (set_keep_alive)
983
+ # ---------------------------------------------------------------------------
984
+ def test_keep_alive_with_http10_and_keep_alive_header
985
+ req = build_req
986
+ response = "HTTP/1.0 200 OK\r\nConnection: Keep-Alive\r\nContent-Length: 2\r\n\r\nOK"
987
+ ka_socket = fake(connect: nil, close: nil, readpartial: response, closed?: false)
988
+ ka_timeout_class = fake(new: ka_socket)
989
+ ka_opts = HTTP::Options.new(timeout_class: ka_timeout_class)
990
+ conn = HTTP::Connection.new(req, ka_opts)
991
+ conn.instance_variable_set(:@pending_response, true)
992
+ conn.instance_variable_set(:@persistent, true)
993
+
994
+ conn.read_headers!
995
+
996
+ assert_predicate conn, :keep_alive?
997
+ end
998
+
999
+ def test_keep_alive_with_http10_without_keep_alive_header
1000
+ req = build_req
1001
+ response = "HTTP/1.0 200 OK\r\nContent-Length: 2\r\n\r\nOK"
1002
+ ka_socket = fake(connect: nil, close: nil, readpartial: response, closed?: false)
1003
+ ka_timeout_class = fake(new: ka_socket)
1004
+ ka_opts = HTTP::Options.new(timeout_class: ka_timeout_class)
1005
+ conn = HTTP::Connection.new(req, ka_opts)
1006
+ conn.instance_variable_set(:@pending_response, true)
1007
+ conn.instance_variable_set(:@persistent, true)
1008
+
1009
+ conn.read_headers!
1010
+
1011
+ refute_predicate conn, :keep_alive?
1012
+ end
1013
+
1014
+ def test_keep_alive_with_http11_and_no_connection_header
1015
+ req = build_req
1016
+ response = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK"
1017
+ ka_socket = fake(connect: nil, close: nil, readpartial: response, closed?: false)
1018
+ ka_timeout_class = fake(new: ka_socket)
1019
+ ka_opts = HTTP::Options.new(timeout_class: ka_timeout_class)
1020
+ conn = HTTP::Connection.new(req, ka_opts)
1021
+ conn.instance_variable_set(:@pending_response, true)
1022
+ conn.instance_variable_set(:@persistent, true)
1023
+
1024
+ conn.read_headers!
1025
+
1026
+ assert_predicate conn, :keep_alive?
1027
+ end
1028
+
1029
+ def test_keep_alive_with_http11_and_connection_close
1030
+ req = build_req
1031
+ response = "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 2\r\n\r\nOK"
1032
+ ka_socket = fake(connect: nil, close: nil, readpartial: response, closed?: false)
1033
+ ka_timeout_class = fake(new: ka_socket)
1034
+ ka_opts = HTTP::Options.new(timeout_class: ka_timeout_class)
1035
+ conn = HTTP::Connection.new(req, ka_opts)
1036
+ conn.instance_variable_set(:@pending_response, true)
1037
+ conn.instance_variable_set(:@persistent, true)
1038
+
1039
+ conn.read_headers!
1040
+
1041
+ refute_predicate conn, :keep_alive?
1042
+ end
1043
+
1044
+ def test_keep_alive_with_unknown_http_version
1045
+ req = build_req
1046
+ response = "HTTP/2.0 200 OK\r\nContent-Length: 2\r\n\r\nOK"
1047
+ ka_socket = fake(connect: nil, close: nil, readpartial: response, closed?: false)
1048
+ ka_timeout_class = fake(new: ka_socket)
1049
+ ka_opts = HTTP::Options.new(timeout_class: ka_timeout_class)
1050
+ conn = HTTP::Connection.new(req, ka_opts)
1051
+ conn.instance_variable_set(:@pending_response, true)
1052
+ conn.instance_variable_set(:@persistent, true)
1053
+
1054
+ conn.read_headers!
1055
+
1056
+ refute_predicate conn, :keep_alive?
1057
+ end
1058
+
1059
+ def test_keep_alive_when_not_persistent_sets_keep_alive_false
1060
+ req = build_req
1061
+ response = "HTTP/1.1 200 OK\r\nConnection: Keep-Alive\r\nContent-Length: 2\r\n\r\nOK"
1062
+ ka_socket = fake(connect: nil, close: nil, readpartial: response, closed?: false)
1063
+ ka_timeout_class = fake(new: ka_socket)
1064
+ ka_opts = HTTP::Options.new(timeout_class: ka_timeout_class)
1065
+ conn = HTTP::Connection.new(req, ka_opts)
1066
+ conn.instance_variable_set(:@pending_response, true)
1067
+
1068
+ conn.read_headers!
1069
+
1070
+ refute_predicate conn, :keep_alive?
1071
+ end
1072
+
1073
+ # ---------------------------------------------------------------------------
1074
+ # proxy connect
1075
+ # ---------------------------------------------------------------------------
1076
+ def test_proxy_connect_non_200_marks_failed_and_stores_headers
1077
+ proxy_req = build_req(
1078
+ uri: "https://example.com/",
1079
+ proxy: { proxy_address: "proxy.example.com", proxy_port: 8080 }
1080
+ )
1081
+ proxy_response = "HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic\r\n\r\n"
1082
+ call_count = 0
1083
+ proxy_socket = fake(
1084
+ connect: nil,
1085
+ close: nil,
1086
+ write: lambda(&:bytesize),
1087
+ readpartial: proc {
1088
+ call_count += 1
1089
+ call_count == 1 ? proxy_response : :eof
1090
+ },
1091
+ start_tls: ->(*) {}
1092
+ )
1093
+ proxy_timeout_class = fake(new: proxy_socket)
1094
+ proxy_opts = HTTP::Options.new(timeout_class: proxy_timeout_class)
1095
+ conn = HTTP::Connection.new(proxy_req, proxy_opts)
1096
+
1097
+ assert_predicate conn, :failed_proxy_connect?
1098
+ assert_instance_of HTTP::Headers, conn.proxy_response_headers
1099
+ assert_equal "Basic", conn.proxy_response_headers["Proxy-Authenticate"]
1100
+ # pending_response should still be true (not reset on failure)
1101
+ assert conn.instance_variable_get(:@pending_response)
1102
+ end
1103
+
1104
+ def test_proxy_connect_200_completes_successfully_and_resets_parser
1105
+ proxy_req = build_req(
1106
+ uri: "https://example.com/",
1107
+ proxy: { proxy_address: "proxy.example.com", proxy_port: 8080 }
1108
+ )
1109
+ proxy_response = "HTTP/1.1 200 Connection established\r\n\r\n"
1110
+ proxy_socket = fake(
1111
+ connect: nil,
1112
+ close: nil,
1113
+ write: lambda(&:bytesize),
1114
+ readpartial: proxy_response,
1115
+ start_tls: ->(*) {},
1116
+ "hostname=": ->(*) {},
1117
+ "sync_close=": ->(*) {},
1118
+ post_connection_check: ->(*) {}
1119
+ )
1120
+ proxy_timeout_class = fake(new: proxy_socket)
1121
+ proxy_opts = HTTP::Options.new(timeout_class: proxy_timeout_class)
1122
+ conn = HTTP::Connection.new(proxy_req, proxy_opts)
1123
+
1124
+ refute_predicate conn, :failed_proxy_connect?
1125
+ assert_instance_of HTTP::Headers, conn.proxy_response_headers
1126
+ # Parser should have been reset, pending_response should be false
1127
+ assert_predicate conn, :finished_request?
1128
+ end
1129
+
1130
+ def test_proxy_connect_skips_for_http_request
1131
+ http_proxy_req = build_req(
1132
+ uri: "http://example.com/",
1133
+ proxy: { proxy_address: "proxy.example.com", proxy_port: 8080 }
1134
+ )
1135
+ connect_called = false
1136
+ plain_socket = fake(
1137
+ connect: nil,
1138
+ close: nil,
1139
+ connect_using_proxy: ->(*) { connect_called = true }
1140
+ )
1141
+ plain_timeout_class = fake(new: plain_socket)
1142
+ plain_opts = HTTP::Options.new(timeout_class: plain_timeout_class)
1143
+ conn = HTTP::Connection.new(http_proxy_req, plain_opts)
1144
+
1145
+ refute connect_called
1146
+ refute_predicate conn, :failed_proxy_connect?
1147
+ end
1148
+
1149
+ # ---------------------------------------------------------------------------
1150
+ # start_tls
1151
+ # ---------------------------------------------------------------------------
1152
+ def test_start_tls_uses_provided_ssl_context
1153
+ ssl_ctx = OpenSSL::SSL::SSLContext.new
1154
+ https_req = build_req(uri: "https://example.com/")
1155
+
1156
+ start_tls_args = nil
1157
+ tls_socket = fake(
1158
+ connect: nil,
1159
+ close: nil,
1160
+ start_tls: ->(*args) { start_tls_args = args }
1161
+ )
1162
+ tls_timeout_class = fake(new: tls_socket)
1163
+ tls_opts = HTTP::Options.new(timeout_class: tls_timeout_class, ssl_context: ssl_ctx)
1164
+
1165
+ HTTP::Connection.new(https_req, tls_opts)
1166
+
1167
+ assert_equal "example.com", start_tls_args[0]
1168
+ assert_equal ssl_ctx, start_tls_args[2]
1169
+ end
1170
+
1171
+ def test_start_tls_creates_ssl_context_when_not_provided
1172
+ https_req = build_req(uri: "https://example.com/")
1173
+
1174
+ start_tls_args = nil
1175
+ tls_socket = fake(
1176
+ connect: nil,
1177
+ close: nil,
1178
+ start_tls: ->(*args) { start_tls_args = args }
1179
+ )
1180
+ tls_timeout_class = fake(new: tls_socket)
1181
+ tls_opts = HTTP::Options.new(timeout_class: tls_timeout_class)
1182
+
1183
+ HTTP::Connection.new(https_req, tls_opts)
1184
+
1185
+ assert_equal "example.com", start_tls_args[0]
1186
+ assert_instance_of OpenSSL::SSL::SSLContext, start_tls_args[2]
1187
+ end
1188
+
1189
+ def test_start_tls_passes_ssl_socket_class
1190
+ https_req = build_req(uri: "https://example.com/")
1191
+
1192
+ start_tls_args = nil
1193
+ tls_socket = fake(
1194
+ connect: nil,
1195
+ close: nil,
1196
+ start_tls: ->(*args) { start_tls_args = args }
1197
+ )
1198
+ tls_timeout_class = fake(new: tls_socket)
1199
+ custom_ssl_class = Class.new
1200
+ tls_opts = HTTP::Options.new(timeout_class: tls_timeout_class, ssl_socket_class: custom_ssl_class)
1201
+
1202
+ HTTP::Connection.new(https_req, tls_opts)
1203
+
1204
+ assert_equal custom_ssl_class, start_tls_args[1]
1205
+ end
1206
+
1207
+ def test_start_tls_passes_host_from_req_uri_host
1208
+ https_req = build_req(uri: "https://subdomain.example.com/")
1209
+
1210
+ start_tls_args = nil
1211
+ tls_socket = fake(
1212
+ connect: nil,
1213
+ close: nil,
1214
+ start_tls: ->(*args) { start_tls_args = args }
1215
+ )
1216
+ tls_timeout_class = fake(new: tls_socket)
1217
+ tls_opts = HTTP::Options.new(timeout_class: tls_timeout_class)
1218
+
1219
+ HTTP::Connection.new(https_req, tls_opts)
1220
+
1221
+ assert_equal "subdomain.example.com", start_tls_args[0]
1222
+ end
1223
+
1224
+ def test_start_tls_skips_tls_for_http_requests
1225
+ http_req = build_req(uri: "http://example.com/")
1226
+ start_tls_called = false
1227
+ no_tls_socket = fake(
1228
+ connect: nil,
1229
+ close: nil,
1230
+ start_tls: ->(*) { start_tls_called = true }
1231
+ )
1232
+ no_tls_timeout_class = fake(new: no_tls_socket)
1233
+ no_tls_opts = HTTP::Options.new(timeout_class: no_tls_timeout_class)
1234
+
1235
+ HTTP::Connection.new(http_req, no_tls_opts)
1236
+
1237
+ refute start_tls_called
1238
+ end
1239
+
1240
+ def test_start_tls_skips_tls_when_proxy_connect_failed
1241
+ proxy_req = build_req(
1242
+ uri: "https://example.com/",
1243
+ proxy: { proxy_address: "proxy.example.com", proxy_port: 8080 }
1244
+ )
1245
+ proxy_response = "HTTP/1.1 407 Auth Required\r\nContent-Length: 0\r\n\r\n"
1246
+ call_count = 0
1247
+ start_tls_called = false
1248
+ proxy_socket = fake(
1249
+ connect: nil,
1250
+ close: nil,
1251
+ write: lambda(&:bytesize),
1252
+ readpartial: proc {
1253
+ call_count += 1
1254
+ call_count == 1 ? proxy_response : :eof
1255
+ },
1256
+ start_tls: ->(*) { start_tls_called = true }
1257
+ )
1258
+ proxy_timeout_class = fake(new: proxy_socket)
1259
+ proxy_opts = HTTP::Options.new(timeout_class: proxy_timeout_class)
1260
+
1261
+ conn = HTTP::Connection.new(proxy_req, proxy_opts)
1262
+
1263
+ assert_predicate conn, :failed_proxy_connect?
1264
+ refute start_tls_called
1265
+ end
1266
+
1267
+ def test_start_tls_applies_ssl_options_via_set_params_when_no_ssl_context
1268
+ https_req = build_req(uri: "https://example.com/")
1269
+
1270
+ start_tls_args = nil
1271
+ tls_socket = fake(
1272
+ connect: nil,
1273
+ close: nil,
1274
+ start_tls: ->(*args) { start_tls_args = args }
1275
+ )
1276
+ tls_timeout_class = fake(new: tls_socket)
1277
+ ssl_options = { verify_mode: OpenSSL::SSL::VERIFY_NONE }
1278
+ tls_opts = HTTP::Options.new(timeout_class: tls_timeout_class, ssl: ssl_options)
1279
+
1280
+ HTTP::Connection.new(https_req, tls_opts)
1281
+
1282
+ ctx = start_tls_args[2]
1283
+
1284
+ assert_instance_of OpenSSL::SSL::SSLContext, ctx
1285
+ assert_equal OpenSSL::SSL::VERIFY_NONE, ctx.verify_mode
1286
+ end
1287
+
1288
+ # ---------------------------------------------------------------------------
1289
+ # read_more and error handling
1290
+ # ---------------------------------------------------------------------------
1291
+ def test_read_more_handles_nil_from_readpartial
1292
+ req = build_req
1293
+ call_count = 0
1294
+ responses = ["HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n", nil, :eof]
1295
+ rm_socket = fake(
1296
+ connect: nil,
1297
+ close: nil,
1298
+ readpartial: proc { responses[[call_count, responses.length - 1].min].tap { call_count += 1 } }
1299
+ )
1300
+ rm_timeout_class = fake(new: rm_socket)
1301
+ rm_opts = HTTP::Options.new(timeout_class: rm_timeout_class)
1302
+ conn = HTTP::Connection.new(req, rm_opts)
1303
+ conn.instance_variable_set(:@pending_response, true)
1304
+
1305
+ conn.read_headers!
1306
+ result = conn.readpartial
1307
+
1308
+ assert_equal "", result
1309
+ end
1310
+
1311
+ def test_read_more_raises_socket_read_error_on_io_errors
1312
+ req = build_req
1313
+ call_count = 0
1314
+ rm_socket = fake(
1315
+ connect: nil,
1316
+ close: nil,
1317
+ readpartial: proc {
1318
+ call_count += 1
1319
+ raise IOError, "broken" unless call_count == 1
1320
+
1321
+ "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n"
1322
+ }
1323
+ )
1324
+ rm_timeout_class = fake(new: rm_socket)
1325
+ rm_opts = HTTP::Options.new(timeout_class: rm_timeout_class)
1326
+ conn = HTTP::Connection.new(req, rm_opts)
1327
+ conn.instance_variable_set(:@pending_response, true)
1328
+
1329
+ conn.read_headers!
1330
+ err = assert_raises(HTTP::ConnectionError) { conn.readpartial }
1331
+ assert_includes err.message, "error reading from socket"
1332
+ assert_includes err.message, "broken"
1333
+ end
1334
+
1335
+ def test_read_more_raises_socket_read_error_on_socket_error
1336
+ req = build_req
1337
+ call_count = 0
1338
+ rm_socket = fake(
1339
+ connect: nil,
1340
+ close: nil,
1341
+ readpartial: proc {
1342
+ call_count += 1
1343
+ raise SocketError, "socket error" unless call_count == 1
1344
+
1345
+ "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n"
1346
+ }
1347
+ )
1348
+ rm_timeout_class = fake(new: rm_socket)
1349
+ rm_opts = HTTP::Options.new(timeout_class: rm_timeout_class)
1350
+ conn = HTTP::Connection.new(req, rm_opts)
1351
+ conn.instance_variable_set(:@pending_response, true)
1352
+
1353
+ conn.read_headers!
1354
+ err = assert_raises(HTTP::ConnectionError) { conn.readpartial }
1355
+ assert_includes err.message, "error reading from socket"
1356
+ assert_includes err.message, "socket error"
1357
+ end
1358
+
1359
+ def test_read_more_passes_buffer_to_socket_readpartial
1360
+ req = build_req
1361
+ read_args = []
1362
+ call_count = 0
1363
+ responses = [
1364
+ "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n",
1365
+ "hello",
1366
+ :eof
1367
+ ]
1368
+ buf_socket = fake(
1369
+ connect: nil,
1370
+ close: nil,
1371
+ readpartial: proc { |*args|
1372
+ read_args << args
1373
+ idx = [call_count, responses.length - 1].min
1374
+ responses[idx].tap { call_count += 1 }
1375
+ },
1376
+ closed?: false
1377
+ )
1378
+ buf_timeout_class = fake(new: buf_socket)
1379
+ buf_opts = HTTP::Options.new(timeout_class: buf_timeout_class)
1380
+ conn = HTTP::Connection.new(req, buf_opts)
1381
+ conn.instance_variable_set(:@pending_response, true)
1382
+
1383
+ conn.read_headers!
1384
+ conn.readpartial
1385
+
1386
+ # Verify that readpartial was called with 2 arguments (size + buffer)
1387
+ assert(read_args.all? { |a| a.length == 2 }, "readpartial should receive size and buffer")
1388
+ # Verify the buffer is a string (not nil)
1389
+ assert(read_args.all? { |a| a[1].is_a?(String) }, "buffer should be a String")
1390
+ end
1391
+
1392
+ # ---------------------------------------------------------------------------
1393
+ # connect_socket
1394
+ # ---------------------------------------------------------------------------
1395
+ def test_connect_socket_passes_correct_arguments
1396
+ req = build_req
1397
+ connect_args = nil
1398
+ connect_kwargs = nil
1399
+ cs_socket = fake(
1400
+ connect: lambda { |*args, **kwargs|
1401
+ connect_args = args
1402
+ connect_kwargs = kwargs
1403
+ },
1404
+ close: nil
1405
+ )
1406
+ cs_timeout_class = fake(new: cs_socket)
1407
+ cs_opts = HTTP::Options.new(timeout_class: cs_timeout_class)
1408
+
1409
+ HTTP::Connection.new(req, cs_opts)
1410
+
1411
+ assert_equal HTTP::Options.default_socket_class, connect_args[0]
1412
+ assert_equal "example.com", connect_args[1]
1413
+ assert_equal 80, connect_args[2]
1414
+ refute connect_kwargs.fetch(:nodelay)
1415
+ end
1416
+
1417
+ def test_connect_socket_passes_timeout_options_to_timeout_class_new
1418
+ req = build_req
1419
+ new_kwargs = nil
1420
+ cs_socket = fake(connect: nil, close: nil)
1421
+ cs_timeout_class = fake(
1422
+ new: lambda { |**kwargs|
1423
+ new_kwargs = kwargs
1424
+ cs_socket
1425
+ }
1426
+ )
1427
+ cs_opts = HTTP::Options.new(timeout_class: cs_timeout_class)
1428
+
1429
+ HTTP::Connection.new(req, cs_opts)
1430
+
1431
+ assert_instance_of Hash, new_kwargs
1432
+ end
1433
+
1434
+ def test_connect_socket_calls_reset_timer_during_initialization
1435
+ req = build_req
1436
+ persist_socket = fake(connect: nil, close: nil)
1437
+ persist_timeout_class = fake(new: persist_socket)
1438
+ persist_opts = HTTP::Options.new(timeout_class: persist_timeout_class, keep_alive_timeout: 5)
1439
+ conn = HTTP::Connection.new(req, persist_opts)
1440
+ conn.instance_variable_set(:@persistent, true)
1441
+
1442
+ # For persistent connections, reset_timer should set @conn_expires_at
1443
+ before = Time.now
1444
+ conn.send(:reset_timer)
1445
+ after = Time.now
1446
+
1447
+ expires = conn.instance_variable_get(:@conn_expires_at)
1448
+
1449
+ assert_operator expires, :>=, before + 5
1450
+ assert_operator expires, :<=, after + 5
1451
+ end
1452
+
1453
+ # ---------------------------------------------------------------------------
1454
+ # reset_timer
1455
+ # ---------------------------------------------------------------------------
1456
+ def test_reset_timer_sets_conn_expires_at_for_persistent_connections
1457
+ req = build_req
1458
+ persist_socket = fake(connect: nil, close: nil)
1459
+ persist_timeout_class = fake(new: persist_socket)
1460
+ persist_opts = HTTP::Options.new(timeout_class: persist_timeout_class, keep_alive_timeout: 5)
1461
+ conn = HTTP::Connection.new(req, persist_opts)
1462
+ conn.instance_variable_set(:@persistent, true)
1463
+
1464
+ before = Time.now
1465
+ conn.send(:reset_timer)
1466
+ after = Time.now
1467
+
1468
+ expires = conn.instance_variable_get(:@conn_expires_at)
1469
+
1470
+ assert_operator expires, :>=, before + 5
1471
+ assert_operator expires, :<=, after + 5
1472
+ end
1473
+
1474
+ def test_reset_timer_does_not_set_conn_expires_at_for_non_persistent
1475
+ connection = build_connection
1476
+ connection.instance_variable_set(:@conn_expires_at, nil)
1477
+ connection.send(:reset_timer)
1478
+
1479
+ assert_nil connection.instance_variable_get(:@conn_expires_at)
1480
+ end
1481
+
1482
+ def test_reset_timer_uses_keep_alive_timeout_from_options
1483
+ req = build_req
1484
+ persist_socket = fake(connect: nil, close: nil)
1485
+ persist_timeout_class = fake(new: persist_socket)
1486
+ persist_opts = HTTP::Options.new(timeout_class: persist_timeout_class, keep_alive_timeout: 42)
1487
+ conn = HTTP::Connection.new(req, persist_opts)
1488
+ conn.instance_variable_set(:@persistent, true)
1489
+
1490
+ before = Time.now
1491
+ conn.send(:reset_timer)
1492
+
1493
+ expires = conn.instance_variable_get(:@conn_expires_at)
1494
+ # Should be approximately now + 42
1495
+ assert_operator expires, :>=, before + 42
1496
+ assert_operator expires, :<, before + 43
1497
+ end
1498
+
1499
+ # ---------------------------------------------------------------------------
1500
+ # init_state
1501
+ # ---------------------------------------------------------------------------
1502
+ def test_init_state_stores_persistent_flag_from_options
1503
+ req = build_req
1504
+ persist_socket = fake(connect: nil, close: nil)
1505
+ persist_timeout_class = fake(new: persist_socket)
1506
+ persist_opts = HTTP::Options.new(timeout_class: persist_timeout_class, persistent: "http://example.com")
1507
+ conn = HTTP::Connection.new(req, persist_opts)
1508
+
1509
+ assert conn.instance_variable_get(:@persistent)
1510
+ end
1511
+
1512
+ def test_init_state_stores_keep_alive_timeout_as_float
1513
+ connection = build_connection
1514
+ kat = connection.instance_variable_get(:@keep_alive_timeout)
1515
+
1516
+ assert_instance_of Float, kat
1517
+ end
1518
+
1519
+ def test_init_state_initializes_buffer_as_binary_empty_string
1520
+ connection = build_connection
1521
+ buf = connection.instance_variable_get(:@buffer)
1522
+
1523
+ assert_equal "".b, buf
1524
+ assert_equal Encoding::ASCII_8BIT, buf.encoding
1525
+ end
1526
+
1527
+ def test_init_state_initializes_parser
1528
+ connection = build_connection
1529
+ parser = connection.instance_variable_get(:@parser)
1530
+
1531
+ assert_instance_of HTTP::Response::Parser, parser
1532
+ end
1533
+ end