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,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TimeoutTests
4
+ # Including class must provide:
5
+ # - server: a DummyServer instance
6
+ # - build_client(**options): builds an HTTP::Client with given options
7
+
8
+ def test_timeout_without_timeouts_works
9
+ client = build_client(timeout_class: HTTP::Timeout::Null, timeout_options: {})
10
+
11
+ assert_equal "<!doctype html>", client.get(server.endpoint).body.to_s
12
+ end
13
+
14
+ def test_timeout_per_operation_works
15
+ client = build_client(
16
+ timeout_class: HTTP::Timeout::PerOperation,
17
+ timeout_options: {
18
+ connect_timeout: 0.5,
19
+ read_timeout: 0.1,
20
+ write_timeout: 0.5
21
+ }
22
+ )
23
+
24
+ assert_equal "<!doctype html>", client.get(server.endpoint).body.to_s
25
+ end
26
+
27
+ def test_timeout_per_operation_connection_of_half_second_does_not_time_out
28
+ client = build_client(
29
+ timeout_class: HTTP::Timeout::PerOperation,
30
+ timeout_options: {
31
+ connect_timeout: 0.5,
32
+ read_timeout: 0.1,
33
+ write_timeout: 0.5
34
+ }
35
+ )
36
+
37
+ client.get(server.endpoint).body.to_s
38
+ end
39
+
40
+ def test_timeout_per_operation_read_of_zero_times_out
41
+ client = build_client(
42
+ timeout_class: HTTP::Timeout::PerOperation,
43
+ timeout_options: {
44
+ connect_timeout: 0.5,
45
+ read_timeout: 0,
46
+ write_timeout: 0.5
47
+ }
48
+ )
49
+
50
+ err = assert_raises(HTTP::TimeoutError) do
51
+ client.get("#{server.endpoint}/sleep").body.to_s
52
+ end
53
+ assert_match(/Read/i, err.message)
54
+ end
55
+
56
+ def test_timeout_per_operation_read_of_tenth_does_not_time_out
57
+ client = build_client(
58
+ timeout_class: HTTP::Timeout::PerOperation,
59
+ timeout_options: {
60
+ connect_timeout: 0.5,
61
+ read_timeout: 0.1,
62
+ write_timeout: 0.5
63
+ }
64
+ )
65
+
66
+ client.get("#{server.endpoint}/sleep").body.to_s
67
+ end
68
+
69
+ def test_timeout_global_errors_if_connecting_takes_too_long
70
+ client = build_client(
71
+ timeout_class: HTTP::Timeout::Global,
72
+ timeout_options: { global_timeout: 0.01 }
73
+ )
74
+
75
+ TCPSocket.stub(:open, ->(*) { sleep 0.025 }) do
76
+ err = assert_raises(HTTP::ConnectTimeoutError) { client.get(server.endpoint).body.to_s }
77
+ assert_match(/execution/, err.message)
78
+ end
79
+ end
80
+
81
+ def test_timeout_global_errors_if_reading_takes_too_long
82
+ client = build_client(
83
+ timeout_class: HTTP::Timeout::Global,
84
+ timeout_options: { global_timeout: 0.01 }
85
+ )
86
+
87
+ err = assert_raises(HTTP::TimeoutError) do
88
+ client.get("#{server.endpoint}/sleep").body.to_s
89
+ end
90
+ assert_match(/Timed out|execution expired/, err.message)
91
+ end
92
+
93
+ def test_timeout_global_resets_state_when_reusing_connections
94
+ client = build_client(
95
+ timeout_class: HTTP::Timeout::Global,
96
+ timeout_options: { global_timeout: 0.5 },
97
+ persistent: server.endpoint
98
+ )
99
+
100
+ client.get("#{server.endpoint}/sleep").body.to_s
101
+ client.get("#{server.endpoint}/sleep").body.to_s
102
+ end
103
+
104
+ def test_timeout_combined_global_and_per_operation_works
105
+ client = build_client(
106
+ timeout_class: HTTP::Timeout::Global,
107
+ timeout_options: {
108
+ global_timeout: 0.5,
109
+ connect_timeout: 0.25,
110
+ read_timeout: 0.25,
111
+ write_timeout: 0.25
112
+ }
113
+ )
114
+
115
+ assert_equal "<!doctype html>", client.get(server.endpoint).body.to_s
116
+ end
117
+
118
+ def test_timeout_combined_per_op_read_of_zero_times_out
119
+ client = build_client(
120
+ timeout_class: HTTP::Timeout::Global,
121
+ timeout_options: {
122
+ global_timeout: 0.5,
123
+ connect_timeout: 0.25,
124
+ read_timeout: 0,
125
+ write_timeout: 0.25
126
+ }
127
+ )
128
+
129
+ err = assert_raises(HTTP::TimeoutError) do
130
+ client.get("#{server.endpoint}/sleep").body.to_s
131
+ end
132
+ assert_match(/Read timed out/, err.message)
133
+ end
134
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "support/http_handling_shared/timeout_tests"
4
+ require "support/http_handling_shared/connection_reuse_tests"
5
+
6
+ module HTTPHandlingTests
7
+ def self.included(base)
8
+ base.include TimeoutTests
9
+ base.include ConnectionReuseTests
10
+ end
11
+ end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "uri"
5
+
6
+ require "support/servers/runner"
7
+
8
+ class ProxyServer
9
+ Target = Struct.new(:host, :port, :path, :query)
10
+
11
+ def initialize
12
+ @tcp_server = TCPServer.new("127.0.0.1", 0)
13
+ @port = @tcp_server.addr[1]
14
+ @ready = Queue.new
15
+ @shutdown_read, @shutdown_write = IO.pipe
16
+ end
17
+
18
+ def addr
19
+ "127.0.0.1"
20
+ end
21
+
22
+ attr_reader :port
23
+
24
+ def wait_ready
25
+ @ready.pop
26
+ end
27
+
28
+ def start
29
+ @ready << true
30
+
31
+ loop do
32
+ readable, = IO.select([@tcp_server, @shutdown_read])
33
+ break unless readable
34
+ break if readable.include?(@shutdown_read)
35
+
36
+ client = @tcp_server.accept
37
+ Thread.new(client) do |c|
38
+ Thread.current.report_on_exception = false
39
+ handle_request(c)
40
+ end
41
+ end
42
+ rescue IOError, Errno::EBADF
43
+ # Server socket closed during shutdown
44
+ end
45
+
46
+ def shutdown
47
+ @shutdown_write.close
48
+ @tcp_server.close rescue nil
49
+ @shutdown_read.close rescue nil
50
+ rescue
51
+ nil
52
+ end
53
+
54
+ private
55
+
56
+ def handle_request(client)
57
+ method, target, version, headers, body = read_proxy_request(client)
58
+ return unless method && target
59
+
60
+ if (response = authenticate(headers))
61
+ client.write(response)
62
+ return
63
+ end
64
+
65
+ if method == "CONNECT"
66
+ tunnel_connection(client, target)
67
+ else
68
+ forward_and_respond(client, method, target, body, version)
69
+ end
70
+ rescue IOError, Errno::ECONNRESET, Errno::EPIPE, Errno::EBADF, URI::InvalidURIError
71
+ # Connection closed
72
+ ensure
73
+ client.close rescue nil
74
+ end
75
+
76
+ def read_proxy_request(client)
77
+ line = client.gets
78
+ return unless line
79
+
80
+ method, target, version = line.strip.split(" ", 3)
81
+ headers = read_headers(client)
82
+ body = headers["Content-Length"] ? client.read(headers["Content-Length"].to_i) : nil
83
+
84
+ [method, parse_target(method, target), version, headers, body]
85
+ end
86
+
87
+ def parse_target(method, target)
88
+ return parse_connect_target(target) if method == "CONNECT"
89
+
90
+ uri = URI.parse(target)
91
+ Target.new(host: uri.host, port: uri.port, path: uri.path, query: uri.query)
92
+ end
93
+
94
+ def parse_connect_target(target)
95
+ host, port = target.split(":", 2)
96
+ return unless host && port
97
+
98
+ Target.new(host: host, port: Integer(port))
99
+ rescue ArgumentError
100
+ nil
101
+ end
102
+
103
+ def read_headers(client)
104
+ headers = {}
105
+ while (header_line = client.gets)
106
+ break if header_line == "\r\n"
107
+
108
+ key, value = header_line.split(": ", 2)
109
+ headers[key] = value.strip
110
+ end
111
+ headers
112
+ end
113
+
114
+ def authenticate(_headers)
115
+ nil
116
+ end
117
+
118
+ def forward_and_respond(client, method, target, body, version)
119
+ target_socket = send_to_target(method, target, body, version)
120
+ relay_response(client, target_socket)
121
+ ensure
122
+ target_socket&.close rescue nil
123
+ end
124
+
125
+ def send_to_target(method, target, body, version)
126
+ socket = TCPSocket.new(target.host, target.port)
127
+ path = target.path.to_s.empty? ? "/" : target.path
128
+ path = "#{path}?#{target.query}" if target.query
129
+
130
+ socket.write("#{method} #{path} #{version}\r\n")
131
+ socket.write("Host: #{target.host}:#{target.port}\r\n")
132
+ socket.write("Content-Length: #{body.bytesize}\r\n") if body
133
+ socket.write("\r\n")
134
+ socket.write(body) if body
135
+ socket
136
+ end
137
+
138
+ def relay_response(client, target)
139
+ response_line = target.gets
140
+ return unless response_line
141
+
142
+ headers, content_length = read_response_headers(target)
143
+ body = content_length&.positive? ? target.read(content_length) : ""
144
+
145
+ client.write("#{response_line}X-PROXIED: true\r\n#{headers}\r\n#{body}")
146
+ rescue IOError, Errno::ECONNRESET
147
+ # Target connection error
148
+ end
149
+
150
+ def tunnel_connection(client, target)
151
+ target_socket = TCPSocket.new(target.host, target.port)
152
+
153
+ client.write("HTTP/1.1 200 Connection established\r\n\r\n")
154
+ relay_tunnel(client, target_socket)
155
+ ensure
156
+ target_socket&.close rescue nil
157
+ end
158
+
159
+ def relay_tunnel(client, target)
160
+ threads = [
161
+ Thread.new { copy_stream(client, target) },
162
+ Thread.new { copy_stream(target, client) }
163
+ ]
164
+ threads.each { |t| t.join(5) }
165
+ ensure
166
+ threads&.each { |t| t.kill if t.alive? }
167
+ end
168
+
169
+ def copy_stream(source, destination)
170
+ loop do
171
+ destination.write(source.readpartial(1024))
172
+ end
173
+ rescue IOError, Errno::ECONNRESET, Errno::EPIPE
174
+ nil
175
+ end
176
+
177
+ def read_response_headers(target)
178
+ headers = +""
179
+ content_length = nil
180
+ while (hl = target.gets)
181
+ break if hl == "\r\n"
182
+
183
+ content_length = ::Regexp.last_match(1).to_i if hl =~ /^Content-Length:\s*(\d+)/i
184
+ headers << hl
185
+ end
186
+ [headers, content_length]
187
+ end
188
+ end
189
+
190
+ class AuthProxyServer < ProxyServer
191
+ private
192
+
193
+ def authenticate(headers)
194
+ auth = headers["Proxy-Authorization"]
195
+
196
+ if auth
197
+ encoded = auth.sub(/^Basic\s+/, "")
198
+ user, pass = encoded.unpack1("m0").split(":", 2)
199
+ return if user == "username" && pass == "password"
200
+ end
201
+
202
+ "HTTP/1.1 407 Proxy Authentication Required\r\n" \
203
+ "Proxy-Authenticate: Basic realm=\"proxy\"\r\n" \
204
+ "Content-Length: 0\r\n" \
205
+ "\r\n"
206
+ end
207
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ServerRunner
4
+ @all_servers = []
5
+
6
+ def self.all_servers
7
+ @all_servers
8
+ end
9
+
10
+ def run_server(name)
11
+ defining_class = self
12
+
13
+ define_method(name) do
14
+ cache = defining_class.instance_variable_get(:@_server_cache) ||
15
+ defining_class.instance_variable_set(:@_server_cache, {})
16
+
17
+ cache[name] ||= begin
18
+ server = yield
19
+ Thread.new { server.start }
20
+ server.wait_ready
21
+ ServerRunner.all_servers << server
22
+ server
23
+ end
24
+ end
25
+
26
+ _run_servers << name
27
+ end
28
+
29
+ def _run_servers
30
+ @_run_servers ||= []
31
+ end
32
+ end
33
+
34
+ module ServerLifecycle
35
+ def setup
36
+ super
37
+ _all_run_servers.each do |s|
38
+ server = send(s)
39
+ server.reset if server.respond_to?(:reset)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def _all_run_servers
46
+ klass = self.class
47
+ servers = []
48
+
49
+ while klass.respond_to?(:_run_servers)
50
+ servers.concat(klass._run_servers)
51
+ klass = klass.superclass
52
+ end
53
+
54
+ servers.uniq
55
+ end
56
+ end
57
+
58
+ Minitest::Test.extend(ServerRunner)
59
+ Minitest::Test.include(ServerLifecycle)
60
+
61
+ Minitest.after_run do
62
+ ServerRunner.all_servers.each do |server|
63
+ server.shutdown
64
+ rescue
65
+ nil
66
+ end
67
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ return if ENV["MUTANT"] || ENV["NOCOV"]
4
+
3
5
  require "simplecov"
4
6
 
5
7
  if ENV["CI"]
@@ -14,6 +16,13 @@ if ENV["CI"]
14
16
  end
15
17
 
16
18
  SimpleCov.start do
17
- add_filter "/spec/"
18
- minimum_coverage 80
19
+ add_filter "/test/"
20
+ add_filter "/minitest-memory/"
21
+
22
+ if RUBY_ENGINE == "ruby"
23
+ enable_coverage :branch
24
+ minimum_coverage line: 100, branch: 100
25
+ else
26
+ minimum_coverage line: 99
27
+ end
19
28
  end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "pathname"
5
+
6
+ module SSLHelper
7
+ CERTS_PATH = Pathname.new File.expand_path("../../tmp/certs", __dir__)
8
+
9
+ class << self
10
+ def server_context
11
+ context = OpenSSL::SSL::SSLContext.new
12
+
13
+ context.verify_mode = OpenSSL::SSL::VERIFY_NONE
14
+ context.key = server_cert_key
15
+ context.cert = server_cert_cert
16
+ context.ca_file = ca_file
17
+
18
+ context
19
+ end
20
+
21
+ def client_context
22
+ # Ensure server cert is generated (triggers CA generation too)
23
+ server_cert_cert
24
+ context = OpenSSL::SSL::SSLContext.new
25
+
26
+ context.options = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options]
27
+ context.verify_mode = OpenSSL::SSL::VERIFY_PEER
28
+ context.verify_hostname = true if context.respond_to?(:verify_hostname=)
29
+ context.ca_file = ca_file
30
+
31
+ context
32
+ end
33
+
34
+ def client_params
35
+ server_cert_cert
36
+ {
37
+ ca_file: ca_file
38
+ }
39
+ end
40
+
41
+ private
42
+
43
+ def ca_key
44
+ @ca_key ||= OpenSSL::PKey::RSA.new(2048)
45
+ end
46
+
47
+ def ca_cert
48
+ @ca_cert ||= begin
49
+ cert = OpenSSL::X509::Certificate.new
50
+ cert.version = 2
51
+ cert.serial = 1
52
+ cert.subject = OpenSSL::X509::Name.parse("/CN=honestachmed.com")
53
+ cert.issuer = cert.subject
54
+ cert.public_key = ca_key.public_key
55
+ cert.not_before = Time.now - 60
56
+ cert.not_after = Time.now + (365 * 24 * 60 * 60)
57
+
58
+ ef = OpenSSL::X509::ExtensionFactory.new
59
+ ef.subject_certificate = cert
60
+ ef.issuer_certificate = cert
61
+
62
+ cert.add_extension(ef.create_extension("basicConstraints", "CA:TRUE", true))
63
+ cert.add_extension(ef.create_extension("keyUsage", "keyCertSign,cRLSign", true))
64
+
65
+ cert.sign(ca_key, OpenSSL::Digest.new("SHA256"))
66
+ cert
67
+ end
68
+ end
69
+
70
+ def ca_file
71
+ return @ca_file if defined?(@ca_file)
72
+
73
+ CERTS_PATH.mkpath
74
+ cert_file = CERTS_PATH.join("ca.crt")
75
+ cert_file.open("w") { |io| io << ca_cert.to_pem }
76
+ @ca_file = cert_file.to_s
77
+ end
78
+
79
+ def server_cert_key
80
+ @server_cert_key ||= OpenSSL::PKey::RSA.new(2048)
81
+ end
82
+
83
+ def server_cert_cert
84
+ @server_cert_cert ||= begin
85
+ cert = OpenSSL::X509::Certificate.new
86
+ cert.version = 2
87
+ cert.serial = 2
88
+ cert.subject = OpenSSL::X509::Name.parse("/CN=127.0.0.1")
89
+ cert.issuer = ca_cert.subject
90
+ cert.public_key = server_cert_key.public_key
91
+ cert.not_before = Time.now - 60
92
+ cert.not_after = Time.now + (365 * 24 * 60 * 60)
93
+
94
+ ef = OpenSSL::X509::ExtensionFactory.new
95
+ ef.subject_certificate = cert
96
+ ef.issuer_certificate = ca_cert
97
+
98
+ cert.add_extension(ef.create_extension("basicConstraints", "CA:FALSE"))
99
+ cert.add_extension(ef.create_extension("keyUsage", "digitalSignature,keyEncipherment", true))
100
+ cert.add_extension(ef.create_extension("extendedKeyUsage", "serverAuth"))
101
+ cert.add_extension(ef.create_extension("subjectAltName", "IP:127.0.0.1"))
102
+
103
+ cert.sign(ca_key, OpenSSL::Digest.new("SHA256"))
104
+ cert
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "support/simplecov"
4
+
5
+ require "minitest/autorun"
6
+ require "minitest/mock"
7
+ require "minitest/memory" if RUBY_ENGINE == "ruby"
8
+ require "minitest/strict"
9
+
10
+ require "http"
11
+
12
+ # No-op for mutant cover declarations when mutant is not loaded
13
+ Minitest::Test.extend(Module.new { def cover(*); end }) unless Minitest::Test.respond_to?(:cover)
14
+
15
+ require "support/capture_warning"
16
+ require "support/fakeio"
17
+
18
+ # Helper for creating fake objects with predefined method responses
19
+ module FakeHelper
20
+ def fake(**methods)
21
+ obj = Object.new
22
+ methods.each do |name, value|
23
+ if value.is_a?(Proc)
24
+ obj.define_singleton_method(name) { |*args, **kwargs, &blk| value.call(*args, **kwargs, &blk) }
25
+ else
26
+ obj.define_singleton_method(name) { |*| value }
27
+ end
28
+ end
29
+ obj
30
+ end
31
+ end
32
+
33
+ module Minitest
34
+ class Test
35
+ include FakeHelper
36
+ include Minitest::Memory if RUBY_ENGINE == "ruby"
37
+ end
38
+ end