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,391 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class HTTPRequestWriterTest < Minitest::Test
6
+ cover "HTTP::Request::Writer*"
7
+
8
+ def build_writer(io: StringIO.new, body: HTTP::Request::Body.new(""), headers: HTTP::Headers.new,
9
+ headerstart: "GET /test HTTP/1.1")
10
+ HTTP::Request::Writer.new(io, body, headers, headerstart)
11
+ end
12
+
13
+ # #stream
14
+
15
+ def test_stream_with_multiple_headers_separates_with_crlf
16
+ io = StringIO.new
17
+ headers = HTTP::Headers.coerce "Host" => "example.org"
18
+ headerstart = "GET /test HTTP/1.1"
19
+ writer = build_writer(io: io, headers: headers, headerstart: headerstart)
20
+ writer.stream
21
+
22
+ assert_equal [
23
+ "#{headerstart}\r\n",
24
+ "Host: example.org\r\nContent-Length: 0\r\n\r\n"
25
+ ].join, io.string
26
+ end
27
+
28
+ def test_stream_with_mixed_case_headers_writes_with_same_casing
29
+ io = StringIO.new
30
+ headers = HTTP::Headers.coerce "content-Type" => "text", "X_MAX" => "200"
31
+ headerstart = "GET /test HTTP/1.1"
32
+ writer = build_writer(io: io, headers: headers, headerstart: headerstart)
33
+ writer.stream
34
+
35
+ assert_equal [
36
+ "#{headerstart}\r\n",
37
+ "content-Type: text\r\nX_MAX: 200\r\nContent-Length: 0\r\n\r\n"
38
+ ].join, io.string
39
+ end
40
+
41
+ def test_stream_with_nonempty_body_writes_body_and_sets_content_length
42
+ io = StringIO.new
43
+ body = HTTP::Request::Body.new("content")
44
+ headerstart = "GET /test HTTP/1.1"
45
+ writer = build_writer(io: io, body: body, headerstart: headerstart)
46
+ writer.stream
47
+
48
+ assert_equal [
49
+ "#{headerstart}\r\n",
50
+ "Content-Length: 7\r\n\r\n",
51
+ "content"
52
+ ].join, io.string
53
+ end
54
+
55
+ def test_stream_when_body_is_not_set_does_not_write_body_or_content_length
56
+ io = StringIO.new
57
+ body = HTTP::Request::Body.new(nil)
58
+ headerstart = "GET /test HTTP/1.1"
59
+ writer = build_writer(io: io, body: body, headerstart: headerstart)
60
+ writer.stream
61
+
62
+ assert_equal "#{headerstart}\r\n\r\n", io.string
63
+ end
64
+
65
+ def test_stream_when_body_is_empty_sets_content_length_zero
66
+ io = StringIO.new
67
+ body = HTTP::Request::Body.new("")
68
+ headerstart = "GET /test HTTP/1.1"
69
+ writer = build_writer(io: io, body: body, headerstart: headerstart)
70
+ writer.stream
71
+
72
+ assert_equal [
73
+ "#{headerstart}\r\n",
74
+ "Content-Length: 0\r\n\r\n"
75
+ ].join, io.string
76
+ end
77
+
78
+ def test_stream_when_content_length_header_is_set_keeps_given_value
79
+ io = StringIO.new
80
+ headers = HTTP::Headers.coerce "Content-Length" => "12"
81
+ body = HTTP::Request::Body.new("content")
82
+ headerstart = "GET /test HTTP/1.1"
83
+ writer = build_writer(io: io, body: body, headers: headers, headerstart: headerstart)
84
+ writer.stream
85
+
86
+ assert_equal [
87
+ "#{headerstart}\r\n",
88
+ "Content-Length: 12\r\n\r\n",
89
+ "content"
90
+ ].join, io.string
91
+ end
92
+
93
+ def test_stream_when_transfer_encoding_is_chunked_writes_encoded_content
94
+ io = StringIO.new
95
+ headers = HTTP::Headers.coerce "Transfer-Encoding" => "chunked"
96
+ body = HTTP::Request::Body.new(%w[request body])
97
+ headerstart = "GET /test HTTP/1.1"
98
+ writer = build_writer(io: io, body: body, headers: headers, headerstart: headerstart)
99
+ writer.stream
100
+
101
+ assert_equal [
102
+ "#{headerstart}\r\n",
103
+ "Transfer-Encoding: chunked\r\n\r\n",
104
+ "7\r\nrequest\r\n4\r\nbody\r\n0\r\n\r\n"
105
+ ].join, io.string
106
+ end
107
+
108
+ def test_stream_when_transfer_encoding_chunked_with_large_body_encodes_hex
109
+ io = StringIO.new
110
+ headers = HTTP::Headers.coerce "Transfer-Encoding" => "chunked"
111
+ body = HTTP::Request::Body.new(["a" * 255])
112
+ headerstart = "GET /test HTTP/1.1"
113
+ writer = build_writer(io: io, body: body, headers: headers, headerstart: headerstart)
114
+ writer.stream
115
+
116
+ assert_includes io.string, "ff\r\n#{'a' * 255}\r\n"
117
+ end
118
+
119
+ def test_stream_when_transfer_encoding_is_not_chunked_does_not_treat_as_chunked
120
+ io = StringIO.new
121
+ headers = HTTP::Headers.coerce "Transfer-Encoding" => "gzip"
122
+ body = HTTP::Request::Body.new("content")
123
+ headerstart = "GET /test HTTP/1.1"
124
+ writer = build_writer(io: io, body: body, headers: headers, headerstart: headerstart)
125
+ writer.stream
126
+
127
+ refute_includes io.string, "0\r\n\r\n"
128
+ assert_includes io.string, "content"
129
+ end
130
+
131
+ def test_stream_when_transfer_encoding_is_not_chunked_returns_false_from_chunked
132
+ headers = HTTP::Headers.coerce "Transfer-Encoding" => "gzip"
133
+ body = HTTP::Request::Body.new("content")
134
+ writer = build_writer(body: body, headers: headers)
135
+
136
+ refute_predicate writer, :chunked?
137
+ end
138
+
139
+ def test_stream_when_server_wont_accept_data_aborts_silently
140
+ mock_io = Object.new
141
+ mock_io.define_singleton_method(:write) { |*| raise Errno::EPIPE }
142
+ body = HTTP::Request::Body.new("")
143
+ headers = HTTP::Headers.new
144
+ w = HTTP::Request::Writer.new(mock_io, body, headers, "GET /test HTTP/1.1")
145
+ w.stream
146
+ end
147
+
148
+ def test_stream_when_body_is_nil_on_post_request_sets_content_length_to_zero
149
+ io = StringIO.new
150
+ body = HTTP::Request::Body.new(nil)
151
+ writer = build_writer(io: io, body: body, headerstart: "POST /test HTTP/1.1")
152
+ writer.stream
153
+
154
+ assert_equal "POST /test HTTP/1.1\r\nContent-Length: 0\r\n\r\n", io.string
155
+ end
156
+
157
+ def test_stream_when_body_is_nil_on_head_request_omits_content_length
158
+ io = StringIO.new
159
+ headers = HTTP::Headers.coerce "Host" => "example.org"
160
+ body = HTTP::Request::Body.new(nil)
161
+ writer = build_writer(io: io, body: body, headers: headers, headerstart: "HEAD /test HTTP/1.1")
162
+ writer.stream
163
+
164
+ refute_includes io.string, "Content-Length"
165
+ end
166
+
167
+ def test_stream_when_body_is_nil_on_delete_request_omits_content_length
168
+ io = StringIO.new
169
+ headers = HTTP::Headers.coerce "Host" => "example.org"
170
+ body = HTTP::Request::Body.new(nil)
171
+ writer = build_writer(io: io, body: body, headers: headers, headerstart: "DELETE /test HTTP/1.1")
172
+ writer.stream
173
+
174
+ refute_includes io.string, "Content-Length"
175
+ end
176
+
177
+ def test_stream_when_body_is_nil_on_connect_request_omits_content_length
178
+ io = StringIO.new
179
+ headers = HTTP::Headers.coerce "Host" => "example.com:443"
180
+ body = HTTP::Request::Body.new(nil)
181
+ writer = build_writer(io: io, body: body, headers: headers, headerstart: "CONNECT example.com:443 HTTP/1.1")
182
+ writer.stream
183
+
184
+ refute_includes io.string, "Content-Length"
185
+ end
186
+
187
+ def test_stream_when_socket_raises_exception_raises_connection_error
188
+ mock_io = Object.new
189
+ mock_io.define_singleton_method(:write) { |*| raise Errno::ECONNRESET }
190
+ body = HTTP::Request::Body.new("")
191
+ headers = HTTP::Headers.new
192
+ w = HTTP::Request::Writer.new(mock_io, body, headers, "GET /test HTTP/1.1")
193
+
194
+ assert_raises(HTTP::ConnectionError) { w.stream }
195
+ end
196
+
197
+ def test_stream_when_socket_raises_exception_includes_original_error_message
198
+ mock_io = Object.new
199
+ mock_io.define_singleton_method(:write) { |*| raise Errno::ECONNRESET }
200
+ body = HTTP::Request::Body.new("")
201
+ headers = HTTP::Headers.new
202
+ w = HTTP::Request::Writer.new(mock_io, body, headers, "GET /test HTTP/1.1")
203
+ err = assert_raises(HTTP::ConnectionError) { w.stream }
204
+
205
+ assert_includes err.message, "error writing to socket:"
206
+ assert_includes err.message, "Connection reset by peer"
207
+ end
208
+
209
+ def test_stream_when_socket_raises_exception_preserves_original_backtrace
210
+ mock_io = Object.new
211
+ mock_io.define_singleton_method(:write) { |*| raise Errno::ECONNRESET }
212
+ body = HTTP::Request::Body.new("")
213
+ headers = HTTP::Headers.new
214
+ w = HTTP::Request::Writer.new(mock_io, body, headers, "GET /test HTTP/1.1")
215
+ err = assert_raises(HTTP::ConnectionError) { w.stream }
216
+
217
+ assert_includes err.backtrace.first, "writer_test.rb"
218
+ end
219
+
220
+ def test_stream_when_socket_performs_partial_writes_writes_remaining_data
221
+ written = []
222
+ call_count = 0
223
+ mock_io = Object.new
224
+ mock_io.define_singleton_method(:write) do |data|
225
+ call_count += 1
226
+ bytes = call_count == 1 ? [5, data.bytesize].min : data.bytesize
227
+ written << data.byteslice(0, bytes)
228
+ bytes
229
+ end
230
+
231
+ body = HTTP::Request::Body.new("HelloWorld")
232
+ w = HTTP::Request::Writer.new(mock_io, body, HTTP::Headers.new, "GET /test HTTP/1.1")
233
+ w.stream
234
+
235
+ full_output = written.join
236
+
237
+ assert_includes full_output, "HelloWorld"
238
+ end
239
+
240
+ # #connect_through_proxy
241
+
242
+ def test_connect_through_proxy_writes_headers_without_body
243
+ io = StringIO.new
244
+ writer = build_writer(io: io)
245
+ writer.connect_through_proxy
246
+
247
+ assert_equal "GET /test HTTP/1.1\r\n\r\n", io.string
248
+ end
249
+
250
+ def test_connect_through_proxy_with_headers_includes_headers
251
+ io = StringIO.new
252
+ headers = HTTP::Headers.coerce "Host" => "example.org"
253
+ writer = build_writer(io: io, headers: headers)
254
+ writer.connect_through_proxy
255
+
256
+ assert_equal "GET /test HTTP/1.1\r\nHost: example.org\r\n\r\n", io.string
257
+ end
258
+
259
+ def test_connect_through_proxy_when_socket_raises_epipe_propagates_error
260
+ mock_io = Object.new
261
+ mock_io.define_singleton_method(:write) { |*| raise Errno::EPIPE }
262
+ body = HTTP::Request::Body.new("")
263
+ headers = HTTP::Headers.new
264
+ w = HTTP::Request::Writer.new(mock_io, body, headers, "GET /test HTTP/1.1")
265
+
266
+ assert_raises(Errno::EPIPE) { w.connect_through_proxy }
267
+ end
268
+
269
+ # #each_chunk
270
+
271
+ def test_each_chunk_when_body_has_content_yields_headers_combined_with_first_chunk
272
+ body = HTTP::Request::Body.new("content")
273
+ writer = build_writer(body: body)
274
+ writer.add_headers
275
+ writer.add_body_type_headers
276
+ chunks = []
277
+ writer.each_chunk { |chunk| chunks << chunk.dup }
278
+
279
+ assert_equal 1, chunks.length
280
+ assert_includes chunks.first, "content"
281
+ end
282
+
283
+ def test_each_chunk_when_body_is_empty_yields_headers_only_once
284
+ body = HTTP::Request::Body.new("")
285
+ headerstart = "GET /test HTTP/1.1"
286
+ writer = build_writer(body: body, headerstart: headerstart)
287
+ writer.add_headers
288
+ writer.add_body_type_headers
289
+ chunks = []
290
+ writer.each_chunk { |chunk| chunks << chunk.dup }
291
+
292
+ assert_equal 1, chunks.length
293
+ assert_includes chunks.first, headerstart
294
+ end
295
+
296
+ # #add_body_type_headers
297
+
298
+ def test_add_body_type_headers_when_body_is_nil_on_put_sets_content_length_zero
299
+ io = StringIO.new
300
+ body = HTTP::Request::Body.new(nil)
301
+ writer = build_writer(io: io, body: body, headerstart: "PUT /test HTTP/1.1")
302
+ writer.stream
303
+
304
+ assert_includes io.string, "Content-Length: 0"
305
+ end
306
+
307
+ def test_add_body_type_headers_when_body_is_nil_on_patch_sets_content_length_zero
308
+ io = StringIO.new
309
+ body = HTTP::Request::Body.new(nil)
310
+ writer = build_writer(io: io, body: body, headerstart: "PATCH /test HTTP/1.1")
311
+ writer.stream
312
+
313
+ assert_includes io.string, "Content-Length: 0"
314
+ end
315
+
316
+ def test_add_body_type_headers_when_body_is_nil_on_options_sets_content_length_zero
317
+ io = StringIO.new
318
+ body = HTTP::Request::Body.new(nil)
319
+ writer = build_writer(io: io, body: body, headerstart: "OPTIONS /test HTTP/1.1")
320
+ writer.stream
321
+
322
+ assert_includes io.string, "Content-Length: 0"
323
+ end
324
+
325
+ # #write (private) partial write handling
326
+
327
+ def test_write_partial_writes_exact_correct_bytes_no_duplication
328
+ written_data = +""
329
+ write_calls = 0
330
+ mock_io = Object.new
331
+ mock_io.define_singleton_method(:write) do |data|
332
+ write_calls += 1
333
+ bytes = [2, data.bytesize].min
334
+ written_data << data.byteslice(0, bytes)
335
+ bytes
336
+ end
337
+
338
+ body = HTTP::Request::Body.new("ABCDEF")
339
+ headerstart = "GET /test HTTP/1.1"
340
+ w = HTTP::Request::Writer.new(mock_io, body, HTTP::Headers.new, headerstart)
341
+ w.stream
342
+
343
+ assert_includes written_data, "ABCDEF"
344
+ body_start = written_data.index("ABCDEF")
345
+
346
+ refute_nil body_start
347
+ assert_nil written_data.index("ABCDEF", body_start + 1)
348
+ assert_operator write_calls, :>, 1
349
+ end
350
+
351
+ def test_write_when_socket_writes_all_bytes_at_once_calls_write_once
352
+ write_calls = 0
353
+ mock_io = Object.new
354
+ mock_io.define_singleton_method(:write) do |data|
355
+ write_calls += 1
356
+ data.bytesize
357
+ end
358
+
359
+ body = HTTP::Request::Body.new("Hello")
360
+ w = HTTP::Request::Writer.new(mock_io, body, HTTP::Headers.new, "GET /test HTTP/1.1")
361
+ w.stream
362
+
363
+ assert_equal 1, write_calls
364
+ end
365
+
366
+ def test_write_when_data_is_split_across_two_writes_correctly_slices_remaining
367
+ written_chunks = []
368
+ call_count = 0
369
+ mock_io = Object.new
370
+ mock_io.define_singleton_method(:write) do |data|
371
+ call_count += 1
372
+ written_chunks << data.dup
373
+ if call_count == 1
374
+ [5, data.bytesize].min
375
+ else
376
+ data.bytesize
377
+ end
378
+ end
379
+
380
+ body = HTTP::Request::Body.new("TESTDATA123")
381
+ headerstart = "GET /test HTTP/1.1"
382
+ w = HTTP::Request::Writer.new(mock_io, body, HTTP::Headers.new, headerstart)
383
+ w.stream
384
+
385
+ full_output = written_chunks.map { |c| c.byteslice(0, [5, c.bytesize].min) }.first +
386
+ written_chunks[1..].join
387
+
388
+ assert_includes full_output, "TESTDATA123"
389
+ assert_operator written_chunks[1].bytesize, :<, written_chunks[0].bytesize
390
+ end
391
+ end