wreq 1.2.5 → 1.2.7
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.
- checksums.yaml +4 -4
- data/.cargo/config.toml +4 -0
- data/Cargo.lock +135 -119
- data/Cargo.toml +11 -3
- data/Rakefile +6 -0
- data/crates/wreq-util/README.md +1 -1
- data/crates/wreq-util/examples/emulate.rs +3 -3
- data/crates/wreq-util/src/emulate/macros.rs +13 -11
- data/crates/wreq-util/src/emulate/profile/chrome/header.rs +18 -3
- data/crates/wreq-util/src/emulate/profile/firefox/header.rs +16 -0
- data/crates/wreq-util/src/emulate/profile/opera/header.rs +6 -7
- data/crates/wreq-util/tests/client.rs +103 -1
- data/docs/windows-gnu-tokio-crash.md +49 -3
- data/extconf.rb +4 -0
- data/lib/wreq.rb +70 -45
- data/lib/wreq_ruby/body.rb +29 -11
- data/lib/wreq_ruby/client.rb +88 -55
- data/lib/wreq_ruby/cookie.rb +79 -19
- data/lib/wreq_ruby/emulate.rb +74 -6
- data/lib/wreq_ruby/error.rb +5 -7
- data/lib/wreq_ruby/http.rb +74 -0
- data/lib/wreq_ruby/response.rb +3 -0
- data/script/rust_env.rb +85 -0
- data/src/arch.rs +22 -0
- data/src/client/body/form.rs +2 -0
- data/src/client/body/json.rs +47 -14
- data/src/client/body/stream.rs +147 -43
- data/src/client/body.rs +11 -15
- data/src/client/param.rs +7 -7
- data/src/client/req.rs +87 -47
- data/src/client/resp.rs +23 -24
- data/src/client.rs +310 -230
- data/src/cookie.rs +280 -87
- data/src/emulate.rs +85 -50
- data/src/error.rs +198 -41
- data/src/extractor.rs +16 -76
- data/src/header.rs +146 -23
- data/src/http.rs +62 -33
- data/src/lib.rs +26 -20
- data/src/macros.rs +71 -46
- data/src/options.rs +284 -0
- data/src/rt.rs +22 -11
- data/src/serde/de/array_deserializer.rs +48 -0
- data/src/serde/de/array_enumerator.rs +59 -0
- data/src/serde/de/deserializer.rs +307 -0
- data/src/serde/de/enum_deserializer.rs +47 -0
- data/src/serde/de/hash_deserializer.rs +89 -0
- data/src/serde/de/number_deserializer.rs +51 -0
- data/src/serde/de/variant_deserializer.rs +101 -0
- data/src/serde/de.rs +33 -0
- data/src/serde/error.rs +146 -0
- data/src/serde/ser/enums.rs +14 -0
- data/src/serde/ser/map_serializer.rs +56 -0
- data/src/serde/ser/seq_serializer.rs +71 -0
- data/src/serde/ser/serializer.rs +194 -0
- data/src/serde/ser/struct_serializer.rs +106 -0
- data/src/serde/ser/struct_variant_serializer.rs +44 -0
- data/src/serde/ser/tuple_variant_serializer.rs +41 -0
- data/src/serde/ser.rs +18 -0
- data/src/serde/tests.rs +237 -0
- data/src/serde.rs +202 -0
- data/test/body_sender_test.rb +246 -0
- data/test/cookie_test.rb +211 -10
- data/test/json_precision_test.rb +209 -0
- data/test/option_validation_test.rb +311 -0
- data/test/value_semantics_test.rb +238 -0
- data/wreq.gemspec +1 -1
- metadata +28 -4
- data/script/build_windows_gnu.ps1 +0 -264
- data/src/header/helper.rs +0 -112
data/test/cookie_test.rb
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "test_helper"
|
|
4
|
+
require "bigdecimal"
|
|
5
|
+
require "open3"
|
|
6
|
+
require "rbconfig"
|
|
4
7
|
|
|
5
8
|
class CookieTest < Minitest::Test
|
|
6
9
|
def setup
|
|
@@ -41,6 +44,14 @@ class CookieTest < Minitest::Test
|
|
|
41
44
|
assert_equal true, (c.secure || c.secure?)
|
|
42
45
|
end
|
|
43
46
|
|
|
47
|
+
def test_add_rejects_invalid_cookie_type
|
|
48
|
+
error = assert_raises(TypeError) do
|
|
49
|
+
@jar.add(Object.new, @base_url)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
assert_equal "cookie must be a Wreq::Cookie or String", error.message
|
|
53
|
+
end
|
|
54
|
+
|
|
44
55
|
def test_add_multiple_and_remove
|
|
45
56
|
@jar.add("a=1; Path=/", @base_url)
|
|
46
57
|
@jar.add("b=2; Path=/", @base_url)
|
|
@@ -84,7 +95,9 @@ class CookieTest < Minitest::Test
|
|
|
84
95
|
@jar.add("exp=1; Expires=#{t.gmtime.strftime("%a, %d %b %Y %H:%M:%S GMT")}; Path=/", @base_url)
|
|
85
96
|
c2 = @jar.get_all.find { |c| c.name == "exp" }
|
|
86
97
|
assert c2
|
|
87
|
-
#
|
|
98
|
+
# expires_at returns Time. expires remains available for numeric callers.
|
|
99
|
+
assert_kind_of Time, c2.expires_at
|
|
100
|
+
assert_predicate c2.expires_at, :utc?
|
|
88
101
|
if (e = c2.expires)
|
|
89
102
|
assert_kind_of Float, e
|
|
90
103
|
assert_operator e, :>, Time.now.to_f - 1_000_000 # sanity bound
|
|
@@ -104,15 +117,18 @@ class CookieTest < Minitest::Test
|
|
|
104
117
|
assert_nil c.domain
|
|
105
118
|
assert_nil c.max_age
|
|
106
119
|
assert_nil c.expires
|
|
120
|
+
assert_nil c.expires_at
|
|
107
121
|
|
|
108
122
|
assert_equal false, (c.http_only || c.http_only?)
|
|
109
123
|
assert_equal false, (c.secure || c.secure?)
|
|
110
124
|
assert_equal false, c.same_site_lax?
|
|
111
125
|
assert_equal false, c.same_site_strict?
|
|
126
|
+
assert_nil c.same_site
|
|
127
|
+
assert_equal "sid=abc", c.to_s
|
|
112
128
|
end
|
|
113
129
|
|
|
114
130
|
def test_cookie_new_full_attributes
|
|
115
|
-
exp = Time.
|
|
131
|
+
exp = Time.utc(2030, 1, 1, 0, 0, Rational(123_456_789, 1_000_000_000))
|
|
116
132
|
c = Wreq::Cookie.new("sess", "v",
|
|
117
133
|
domain: "example.com",
|
|
118
134
|
path: "/",
|
|
@@ -130,16 +146,199 @@ class CookieTest < Minitest::Test
|
|
|
130
146
|
# Max-Age returns seconds as Integer
|
|
131
147
|
assert_equal 3600, c.max_age
|
|
132
148
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
assert_in_delta exp, c.expires, 2.0
|
|
149
|
+
assert_equal exp, c.expires_at
|
|
150
|
+
assert_predicate c.expires_at, :utc?
|
|
151
|
+
assert_in_delta exp.to_f, c.expires, 1e-6
|
|
137
152
|
|
|
138
153
|
assert_equal true, (c.http_only || c.http_only?)
|
|
139
154
|
assert_equal true, (c.secure || c.secure?)
|
|
140
|
-
# constructor currently sets SameSite to none
|
|
141
155
|
assert_equal true, c.same_site_lax?
|
|
142
156
|
assert_equal false, c.same_site_strict?
|
|
157
|
+
assert_equal Wreq::SameSite::Lax, c.same_site
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def test_same_site_reader_distinguishes_none_from_unset
|
|
161
|
+
cookie = Wreq::Cookie.new("same-site", "value", same_site: Wreq::SameSite::None)
|
|
162
|
+
|
|
163
|
+
assert_equal Wreq::SameSite::None, cookie.same_site
|
|
164
|
+
refute cookie.same_site_lax?
|
|
165
|
+
refute cookie.same_site_strict?
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def test_cookie_new_uses_shared_keyword_validation
|
|
169
|
+
cookie = Wreq::Cookie.new("sid", "abc", **{"path" => "/"})
|
|
170
|
+
assert_equal "/", cookie.path
|
|
171
|
+
|
|
172
|
+
secret = "must-not-appear"
|
|
173
|
+
unknown_error = assert_raises(ArgumentError) do
|
|
174
|
+
Wreq::Cookie.new("sid", "abc", domian: secret)
|
|
175
|
+
end
|
|
176
|
+
assert_includes unknown_error.message, ":domian"
|
|
177
|
+
refute_includes unknown_error.message, secret
|
|
178
|
+
|
|
179
|
+
duplicate_options = {path: "/one"}
|
|
180
|
+
duplicate_options["path"] = "/two"
|
|
181
|
+
duplicate_error = assert_raises(ArgumentError) do
|
|
182
|
+
Wreq::Cookie.new("sid", "abc", **duplicate_options)
|
|
183
|
+
end
|
|
184
|
+
assert_includes duplicate_error.message, "duplicate option: :path"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def test_expires_accepts_past_time
|
|
188
|
+
expiration = Time.at(Rational(-5, 4)).utc
|
|
189
|
+
cookie = Wreq::Cookie.new("past", "value", expires: expiration)
|
|
190
|
+
|
|
191
|
+
assert_equal expiration, cookie.expires_at
|
|
192
|
+
assert_in_delta(-1.25, cookie.expires, 1e-9)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def test_expires_accepts_integer_and_fractional_timestamps
|
|
196
|
+
[
|
|
197
|
+
1_893_456_000,
|
|
198
|
+
1_893_456_000.125,
|
|
199
|
+
Rational(-5, 4),
|
|
200
|
+
BigDecimal("1893456000.125")
|
|
201
|
+
].each do |timestamp|
|
|
202
|
+
cookie = Wreq::Cookie.new("timestamp", "value", expires: timestamp)
|
|
203
|
+
|
|
204
|
+
assert_kind_of Time, cookie.expires_at
|
|
205
|
+
assert_predicate cookie.expires_at, :utc?
|
|
206
|
+
assert_in_delta timestamp.to_f, cookie.expires_at.to_f, 1e-6
|
|
207
|
+
assert_in_delta timestamp.to_f, cookie.expires, 1e-6
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def test_expires_normalizes_time_to_utc_without_losing_precision
|
|
212
|
+
expiration = Time.new(
|
|
213
|
+
2030,
|
|
214
|
+
1,
|
|
215
|
+
1,
|
|
216
|
+
12,
|
|
217
|
+
34,
|
|
218
|
+
Rational(123_456_789, 1_000_000_000),
|
|
219
|
+
"+09:00"
|
|
220
|
+
)
|
|
221
|
+
actual = Wreq::Cookie.new("offset", "value", expires: expiration).expires_at
|
|
222
|
+
|
|
223
|
+
assert_predicate actual, :utc?
|
|
224
|
+
assert_equal expiration.to_r, actual.to_r
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def test_expires_rejects_non_finite_timestamps
|
|
228
|
+
[Float::NAN, Float::INFINITY, -Float::INFINITY].each do |timestamp|
|
|
229
|
+
error = assert_raises(ArgumentError) do
|
|
230
|
+
Wreq::Cookie.new("invalid", "value", expires: timestamp)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
assert_match(/expires.*finite/, error.message)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def test_expires_rejects_unrepresentable_times
|
|
238
|
+
expirations = [
|
|
239
|
+
-(2**63),
|
|
240
|
+
2**63 - 1,
|
|
241
|
+
-Float::MAX,
|
|
242
|
+
Float::MAX,
|
|
243
|
+
Time.utc(10_000, 1, 1)
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
expirations.each do |expiration|
|
|
247
|
+
error = assert_raises(RangeError) do
|
|
248
|
+
Wreq::Cookie.new("invalid", "value", expires: expiration)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
assert_match(/expires.*supported range/, error.message)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def test_max_age_accepts_signed_boundaries_without_wrapping
|
|
256
|
+
[-(2**63), -1, 0, 2**63 - 1].each do |max_age|
|
|
257
|
+
cookie = Wreq::Cookie.new("max-age", "value", max_age: max_age)
|
|
258
|
+
|
|
259
|
+
assert_equal max_age, cookie.max_age
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
[-(2**63) - 1, 2**63].each do |max_age|
|
|
263
|
+
assert_raises(RangeError) do
|
|
264
|
+
Wreq::Cookie.new("max-age", "value", max_age: max_age)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def test_non_positive_max_age_removes_cookie_from_jar
|
|
270
|
+
[-1, 0].each do |max_age|
|
|
271
|
+
jar = Wreq::Jar.new
|
|
272
|
+
jar.add("session=old; Path=/", @base_url)
|
|
273
|
+
deletion = Wreq::Cookie.new("session", "gone", path: "/", max_age: max_age)
|
|
274
|
+
|
|
275
|
+
jar.add(deletion, @base_url)
|
|
276
|
+
|
|
277
|
+
assert_equal max_age, deletion.max_age
|
|
278
|
+
assert_empty jar.get_all
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def test_past_expiration_removes_cookie_from_jar
|
|
283
|
+
@jar.add("session=old; Path=/", @base_url)
|
|
284
|
+
deletion = Wreq::Cookie.new(
|
|
285
|
+
"session",
|
|
286
|
+
"gone",
|
|
287
|
+
path: "/",
|
|
288
|
+
expires: Time.at(-1).utc
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
@jar.add(deletion, @base_url)
|
|
292
|
+
|
|
293
|
+
assert_empty @jar.get_all
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def test_expiration_regressions_exit_subprocess_normally
|
|
297
|
+
lib_dir = File.expand_path("../lib", __dir__)
|
|
298
|
+
script = <<~RUBY
|
|
299
|
+
require "wreq"
|
|
300
|
+
|
|
301
|
+
past = Wreq::Cookie.new("past", "value", expires: -1.0)
|
|
302
|
+
abort "past timestamp was not retained" unless past.expires_at == Time.at(-1).utc
|
|
303
|
+
|
|
304
|
+
[Float::NAN, Float::INFINITY, -Float::INFINITY].each do |timestamp|
|
|
305
|
+
begin
|
|
306
|
+
Wreq::Cookie.new("invalid", "value", expires: timestamp)
|
|
307
|
+
rescue ArgumentError
|
|
308
|
+
next
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
abort "non-finite timestamp did not raise ArgumentError"
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
[Float::MAX, -Float::MAX].each do |timestamp|
|
|
315
|
+
begin
|
|
316
|
+
Wreq::Cookie.new("invalid", "value", expires: timestamp)
|
|
317
|
+
rescue RangeError
|
|
318
|
+
next
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
abort "unrepresentable finite timestamp did not raise RangeError"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
[-(2**63) - 1, 2**63].each do |max_age|
|
|
325
|
+
begin
|
|
326
|
+
Wreq::Cookie.new("invalid", "value", max_age: max_age)
|
|
327
|
+
rescue RangeError
|
|
328
|
+
next
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
abort "out-of-range Max-Age did not raise RangeError"
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
puts "ok"
|
|
335
|
+
RUBY
|
|
336
|
+
|
|
337
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, "-I", lib_dir, "-e", script)
|
|
338
|
+
|
|
339
|
+
assert status.success?, "subprocess failed with #{status.inspect}: #{stderr}"
|
|
340
|
+
assert_equal "ok\n", stdout
|
|
341
|
+
refute_match(/panicked|fatal|access violation|cannot convert float seconds to Duration/i, stderr)
|
|
143
342
|
end
|
|
144
343
|
|
|
145
344
|
def test_same_site_flags_from_parsed_header
|
|
@@ -148,10 +347,12 @@ class CookieTest < Minitest::Test
|
|
|
148
347
|
@jar.add("s2=1; Path=/; SameSite=Lax", @base_url)
|
|
149
348
|
|
|
150
349
|
cookies = @jar.get_all
|
|
151
|
-
h = cookies.to_h
|
|
350
|
+
h = cookies.to_h do |cookie|
|
|
351
|
+
[cookie.name, [cookie.same_site, cookie.same_site_strict?, cookie.same_site_lax?]]
|
|
352
|
+
end
|
|
152
353
|
|
|
153
|
-
assert_equal [true, false], h["s1"]
|
|
154
|
-
assert_equal [false, true], h["s2"]
|
|
354
|
+
assert_equal [Wreq::SameSite::Strict, true, false], h["s1"]
|
|
355
|
+
assert_equal [Wreq::SameSite::Lax, false, true], h["s2"]
|
|
155
356
|
end
|
|
156
357
|
|
|
157
358
|
def test_request_uncompressed_cookies
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
require "json"
|
|
3
|
+
require "socket"
|
|
4
|
+
|
|
5
|
+
class JsonPrecisionTest < Minitest::Test
|
|
6
|
+
INTEGER_BOUNDARIES = [
|
|
7
|
+
(2**53) - 1,
|
|
8
|
+
2**53,
|
|
9
|
+
(2**63) - 1,
|
|
10
|
+
2**63,
|
|
11
|
+
(2**64) - 1,
|
|
12
|
+
-((2**53) - 1),
|
|
13
|
+
-(2**53),
|
|
14
|
+
-((2**63) - 1),
|
|
15
|
+
-(2**63),
|
|
16
|
+
-(2**63) - 1,
|
|
17
|
+
-((2**64) - 1),
|
|
18
|
+
-(2**64),
|
|
19
|
+
2**100,
|
|
20
|
+
-(2**100),
|
|
21
|
+
2**256,
|
|
22
|
+
-(2**256)
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
def test_response_json_matches_json_parse_for_large_integers
|
|
26
|
+
payload = precision_payload
|
|
27
|
+
|
|
28
|
+
with_json_server(response_body: JSON.generate(payload)) do |url, _requests|
|
|
29
|
+
response = Wreq.get(url)
|
|
30
|
+
expected = JSON.parse(response.bytes)
|
|
31
|
+
actual = response.json
|
|
32
|
+
|
|
33
|
+
assert_equal payload, expected
|
|
34
|
+
assert_equal expected, actual
|
|
35
|
+
assert actual.fetch("integers").all? { |value| value.instance_of?(Integer) }
|
|
36
|
+
assert_instance_of Float, actual.fetch("fraction")
|
|
37
|
+
assert_instance_of Float, actual.fetch("exponent")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_request_json_preserves_large_integers_and_nested_values
|
|
42
|
+
payload = precision_payload
|
|
43
|
+
|
|
44
|
+
with_json_server do |url, requests|
|
|
45
|
+
response = Wreq.post(url, json: payload)
|
|
46
|
+
request = requests.pop
|
|
47
|
+
|
|
48
|
+
assert_equal "application/json", request.fetch(:headers).fetch("content-type")
|
|
49
|
+
assert_equal JSON.generate(payload), request.fetch(:body)
|
|
50
|
+
assert_equal payload, response.json
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_request_json_nil_is_serialized_as_null
|
|
55
|
+
with_json_server do |url, requests|
|
|
56
|
+
response = Wreq.post(url, json: nil)
|
|
57
|
+
request = requests.pop
|
|
58
|
+
|
|
59
|
+
assert_equal "null", request.fetch(:body)
|
|
60
|
+
assert_nil response.json
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_request_json_accepts_symbols_and_preserves_object_order
|
|
65
|
+
payload = {second: 2**100, first: :value}
|
|
66
|
+
|
|
67
|
+
with_json_server do |url, requests|
|
|
68
|
+
response = Wreq.post(url, json: payload)
|
|
69
|
+
request = requests.pop
|
|
70
|
+
actual = response.json
|
|
71
|
+
|
|
72
|
+
assert_equal JSON.generate(payload), request.fetch(:body)
|
|
73
|
+
assert_equal ["second", "first"], actual.keys
|
|
74
|
+
assert_equal({"second" => 2**100, "first" => "value"}, actual)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_request_json_enforces_documented_nesting_limit
|
|
79
|
+
accepted = 0
|
|
80
|
+
100.times { accepted = [accepted] }
|
|
81
|
+
|
|
82
|
+
with_json_server do |url, requests|
|
|
83
|
+
Wreq.post(url, json: accepted)
|
|
84
|
+
|
|
85
|
+
assert_equal JSON.generate(accepted), requests.pop.fetch(:body)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
error = assert_raises(Wreq::BuilderError) do
|
|
89
|
+
Wreq.post("http://127.0.0.1:1/", json: [accepted])
|
|
90
|
+
end
|
|
91
|
+
assert_match(/nesting exceeds 100 levels/, error.message)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def test_unsupported_request_json_raises_before_socket_io
|
|
95
|
+
server = TCPServer.new("127.0.0.1", 0)
|
|
96
|
+
port = server.addr[1]
|
|
97
|
+
|
|
98
|
+
error = assert_raises(Wreq::BuilderError) do
|
|
99
|
+
Wreq.post("http://127.0.0.1:#{port}/", json: {"value" => Float::NAN})
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
cyclic = []
|
|
103
|
+
cyclic << cyclic
|
|
104
|
+
nesting_error = assert_raises(Wreq::BuilderError) do
|
|
105
|
+
Wreq.post("http://127.0.0.1:#{port}/", json: cyclic)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
cyclic_hash = {}
|
|
109
|
+
cyclic_hash["self"] = cyclic_hash
|
|
110
|
+
hash_nesting_error = assert_raises(Wreq::BuilderError) do
|
|
111
|
+
Wreq.post("http://127.0.0.1:#{port}/", json: cyclic_hash)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
key_error = assert_raises(Wreq::BuilderError) do
|
|
115
|
+
Wreq.post("http://127.0.0.1:#{port}/", json: {1 => "value"})
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
assert_match(/non-finite/, error.message)
|
|
119
|
+
assert_match(/nesting/, nesting_error.message)
|
|
120
|
+
assert_match(/nesting/, hash_nesting_error.message)
|
|
121
|
+
assert_match(/keys/, key_error.message)
|
|
122
|
+
assert_equal :wait_readable, server.accept_nonblock(exception: false)
|
|
123
|
+
ensure
|
|
124
|
+
server&.close unless server&.closed?
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def test_invalid_response_json_raises_decoding_error
|
|
128
|
+
with_json_server(response_body: '{"id":') do |url, _requests|
|
|
129
|
+
response = Wreq.get(url)
|
|
130
|
+
|
|
131
|
+
assert_raises(Wreq::DecodingError) { response.json }
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def test_fractional_and_large_exponent_numbers_match_json_parse
|
|
136
|
+
source = '{"fraction":0.12345678901234567890123456789,"exponent":1e400}'
|
|
137
|
+
|
|
138
|
+
with_json_server(response_body: source) do |url, _requests|
|
|
139
|
+
response = Wreq.get(url)
|
|
140
|
+
expected = JSON.parse(response.bytes)
|
|
141
|
+
actual = response.json
|
|
142
|
+
|
|
143
|
+
assert_equal expected, actual
|
|
144
|
+
assert_instance_of Float, actual.fetch("fraction")
|
|
145
|
+
assert_equal Float::INFINITY, actual.fetch("exponent")
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def precision_payload
|
|
152
|
+
{
|
|
153
|
+
"integers" => INTEGER_BOUNDARIES,
|
|
154
|
+
"nested" => {
|
|
155
|
+
"array" => [2**100, {"negative" => -(2**100)}],
|
|
156
|
+
"object" => {"unsigned_64_max" => (2**64) - 1}
|
|
157
|
+
},
|
|
158
|
+
"shapes" => ["text", true, false, nil],
|
|
159
|
+
"fraction" => 1.25,
|
|
160
|
+
"exponent" => 1.0e40
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def with_json_server(response_body: nil)
|
|
165
|
+
server = TCPServer.new("127.0.0.1", 0)
|
|
166
|
+
port = server.addr[1]
|
|
167
|
+
requests = Queue.new
|
|
168
|
+
thread = Thread.new do
|
|
169
|
+
socket = server.accept
|
|
170
|
+
begin
|
|
171
|
+
request_line = socket.gets
|
|
172
|
+
headers = read_headers(socket)
|
|
173
|
+
content_length = headers.fetch("content-length", "0").to_i
|
|
174
|
+
body = content_length.zero? ? "" : socket.read(content_length)
|
|
175
|
+
requests << {request_line: request_line, headers: headers, body: body}
|
|
176
|
+
|
|
177
|
+
response = response_body.nil? ? body : response_body
|
|
178
|
+
socket.write "HTTP/1.1 200 OK\r\n"
|
|
179
|
+
socket.write "Content-Type: application/json\r\n"
|
|
180
|
+
socket.write "Content-Length: #{response.bytesize}\r\n"
|
|
181
|
+
socket.write "Connection: close\r\n\r\n"
|
|
182
|
+
socket.write response
|
|
183
|
+
ensure
|
|
184
|
+
socket.close unless socket.closed?
|
|
185
|
+
end
|
|
186
|
+
rescue IOError, SystemCallError
|
|
187
|
+
nil
|
|
188
|
+
ensure
|
|
189
|
+
server.close unless server.closed?
|
|
190
|
+
end
|
|
191
|
+
thread.report_on_exception = false
|
|
192
|
+
|
|
193
|
+
yield "http://127.0.0.1:#{port}/", requests
|
|
194
|
+
ensure
|
|
195
|
+
server&.close unless server&.closed?
|
|
196
|
+
thread&.join(5)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def read_headers(socket)
|
|
200
|
+
headers = {}
|
|
201
|
+
while (line = socket.gets)
|
|
202
|
+
break if line == "\r\n"
|
|
203
|
+
|
|
204
|
+
name, value = line.split(":", 2)
|
|
205
|
+
headers[name.downcase] = value.strip
|
|
206
|
+
end
|
|
207
|
+
headers
|
|
208
|
+
end
|
|
209
|
+
end
|