wreq 1.2.4-x86_64-linux → 1.2.6-x86_64-linux
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/crates/wreq-util/.github/FUNDING.yml +15 -0
- data/crates/wreq-util/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- data/crates/wreq-util/.github/ISSUE_TEMPLATE/custom.md +10 -0
- data/crates/wreq-util/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/crates/wreq-util/.github/dependabot.yml +22 -0
- data/crates/wreq-util/.github/workflows/ci.yml +82 -0
- data/crates/wreq-util/.github/workflows/release-plz.yml +52 -0
- data/crates/wreq-util/.gitignore +24 -0
- data/crates/wreq-util/CHANGELOG.md +74 -0
- data/crates/wreq-util/LICENSE +201 -0
- data/crates/wreq-util/README.md +62 -0
- data/crates/wreq-util/release-plz.toml +2 -0
- data/crates/wreq-util/rustfmt.toml +5 -0
- data/examples/emulate_request.rb +1 -1
- data/lib/wreq.rb +72 -45
- data/lib/wreq_ruby/3.3/wreq_ruby.so +0 -0
- data/lib/wreq_ruby/3.4/wreq_ruby.so +0 -0
- data/lib/wreq_ruby/4.0/wreq_ruby.so +0 -0
- data/lib/wreq_ruby/body.rb +30 -9
- data/lib/wreq_ruby/client.rb +88 -55
- data/lib/wreq_ruby/cookie.rb +81 -21
- data/lib/wreq_ruby/emulate.rb +77 -7
- data/lib/wreq_ruby/error.rb +5 -7
- data/lib/wreq_ruby/header.rb +205 -114
- data/lib/wreq_ruby/http.rb +74 -0
- data/lib/wreq_ruby/response.rb +31 -3
- data/script/build_windows_gnu.ps1 +6 -0
- data/test/body_sender_test.rb +246 -0
- data/test/cookie_test.rb +211 -10
- data/test/header_test.rb +228 -192
- data/test/json_precision_test.rb +209 -0
- data/test/option_validation_test.rb +311 -0
- data/test/stream_test.rb +36 -0
- data/test/value_semantics_test.rb +238 -0
- metadata +19 -2
data/lib/wreq_ruby/response.rb
CHANGED
|
@@ -64,6 +64,19 @@ unless defined?(Wreq)
|
|
|
64
64
|
def content_length
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
# Get the response headers.
|
|
68
|
+
#
|
|
69
|
+
# Header names are case-insensitive. Use {Wreq::Headers#get_all} to get
|
|
70
|
+
# every value when a header appears more than once. Each call returns a
|
|
71
|
+
# fresh, mutable snapshot. Changing that snapshot does not change the
|
|
72
|
+
# response or a later snapshot, and object identity is not guaranteed.
|
|
73
|
+
#
|
|
74
|
+
# @return [Wreq::Headers] Response headers
|
|
75
|
+
# @example
|
|
76
|
+
# response.headers.get("content-type") # => "application/json"
|
|
77
|
+
def headers
|
|
78
|
+
end
|
|
79
|
+
|
|
67
80
|
# Get the local socket address.
|
|
68
81
|
#
|
|
69
82
|
# @return [String, nil] Local address (e.g., "127.0.0.1:54321"), or nil
|
|
@@ -80,6 +93,18 @@ unless defined?(Wreq)
|
|
|
80
93
|
def remote_addr
|
|
81
94
|
end
|
|
82
95
|
|
|
96
|
+
# Get cookies parsed from the response's `Set-Cookie` headers.
|
|
97
|
+
#
|
|
98
|
+
# Invalid `Set-Cookie` values are skipped.
|
|
99
|
+
#
|
|
100
|
+
# @return [Array<Wreq::Cookie>] Parsed response cookies
|
|
101
|
+
# @example
|
|
102
|
+
# response.cookies.each do |cookie|
|
|
103
|
+
# puts "#{cookie.name}=#{cookie.value}"
|
|
104
|
+
# end
|
|
105
|
+
def cookies
|
|
106
|
+
end
|
|
107
|
+
|
|
83
108
|
# Get the response bytes as a binary string.
|
|
84
109
|
# @return [String] Response body as binary data
|
|
85
110
|
# @example
|
|
@@ -91,17 +116,20 @@ unless defined?(Wreq)
|
|
|
91
116
|
# Get the response body as text with a specific charset.
|
|
92
117
|
# This method allows you to specify a default encoding
|
|
93
118
|
# to use when decoding the response body.
|
|
94
|
-
#
|
|
95
|
-
#
|
|
119
|
+
# @param default_encoding [String] Default encoding to use (e.g., "UTF-8")
|
|
120
|
+
# @return [String] Response body decoded as text using the specified encoding
|
|
96
121
|
# @example
|
|
97
122
|
# html = response.text("ISO-8859-1")
|
|
98
123
|
# puts html
|
|
99
124
|
# @raise [Wreq::DecodingError] if body cannot be decoded with the specified encoding
|
|
100
|
-
def text(default_encoding
|
|
125
|
+
def text(default_encoding = "UTF-8")
|
|
101
126
|
end
|
|
102
127
|
|
|
103
128
|
# Parse the response body as JSON.
|
|
104
129
|
#
|
|
130
|
+
# Integral JSON numbers are returned as arbitrary-precision Ruby Integer
|
|
131
|
+
# values. Fractional and exponent-form numbers are returned as Float values.
|
|
132
|
+
#
|
|
105
133
|
# @return [Object] Parsed JSON (Hash, Array, String, Integer, Float, Boolean, nil)
|
|
106
134
|
# @raise [Wreq::DecodingError] if body is not valid JSON
|
|
107
135
|
# @example
|
|
@@ -32,7 +32,12 @@ function Invoke-Step {
|
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
Write-Host "==> $Name"
|
|
35
|
+
# Native command failures do not honor ErrorActionPreference in Windows PowerShell.
|
|
36
|
+
$global:LASTEXITCODE = 0
|
|
35
37
|
& $Command
|
|
38
|
+
if ($LASTEXITCODE -ne 0) {
|
|
39
|
+
throw "$Name failed with exit code $LASTEXITCODE."
|
|
40
|
+
}
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
function Get-MissingUcrtTools {
|
|
@@ -122,6 +127,7 @@ Invoke-Step "Check MSYS2 UCRT build tools" {
|
|
|
122
127
|
$stillMissing = @(Get-MissingUcrtTools)
|
|
123
128
|
if ($stillMissing.Count -eq 0) {
|
|
124
129
|
Write-Warning "pacman returned exit code $LASTEXITCODE, but all required tools are present; continuing."
|
|
130
|
+
$global:LASTEXITCODE = 0
|
|
125
131
|
return
|
|
126
132
|
}
|
|
127
133
|
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
require "open3"
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
class BodySenderTest < Minitest::Test
|
|
7
|
+
def test_valid_capacity_creates_open_sender
|
|
8
|
+
sender = Wreq::BodySender.new(2)
|
|
9
|
+
|
|
10
|
+
refute_predicate sender, :closed?
|
|
11
|
+
ensure
|
|
12
|
+
sender&.close
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_omitted_capacity_defaults_to_eight
|
|
16
|
+
sender = Wreq::BodySender.new
|
|
17
|
+
progress = Queue.new
|
|
18
|
+
producer = Thread.new do
|
|
19
|
+
9.times do |index|
|
|
20
|
+
sender.push(index.to_s)
|
|
21
|
+
progress << index
|
|
22
|
+
end
|
|
23
|
+
sender.close
|
|
24
|
+
end
|
|
25
|
+
producer.report_on_exception = false
|
|
26
|
+
|
|
27
|
+
assert wait_until { progress.size >= 8 }, "the default channel should buffer eight chunks"
|
|
28
|
+
assert_equal 8, progress.size
|
|
29
|
+
assert_predicate producer, :alive?, "the ninth push should wait for channel capacity"
|
|
30
|
+
|
|
31
|
+
with_request_body_server do |url, request_thread|
|
|
32
|
+
response = Wreq.post(url, body: sender)
|
|
33
|
+
|
|
34
|
+
assert_equal 200, response.code
|
|
35
|
+
assert producer.join(5), "the ninth push should finish once the request drains"
|
|
36
|
+
assert_equal "012345678", request_thread.value
|
|
37
|
+
end
|
|
38
|
+
ensure
|
|
39
|
+
producer&.kill if producer&.alive?
|
|
40
|
+
producer&.join(5)
|
|
41
|
+
sender&.close
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_non_positive_and_excessive_capacities_raise_argument_error
|
|
45
|
+
[0, -1, 2**256].each do |capacity|
|
|
46
|
+
error = assert_raises(ArgumentError) { Wreq::BodySender.new(capacity) }
|
|
47
|
+
assert_match(/capacity/, error.message)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_non_integer_capacities_raise_type_error
|
|
52
|
+
[1.0, "not an integer"].each do |capacity|
|
|
53
|
+
error = assert_raises(TypeError) { Wreq::BodySender.new(capacity) }
|
|
54
|
+
assert_match(/capacity/, error.message)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_too_many_constructor_arguments_raise_argument_error
|
|
59
|
+
assert_raises(ArgumentError) { Wreq::BodySender.new(1, 2) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_close_is_idempotent_and_updates_closed_state
|
|
63
|
+
sender = Wreq::BodySender.new
|
|
64
|
+
|
|
65
|
+
refute_predicate sender, :closed?
|
|
66
|
+
assert_nil sender.close
|
|
67
|
+
assert_predicate sender, :closed?
|
|
68
|
+
assert_nil sender.close
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_push_after_close_raises_io_error
|
|
72
|
+
sender = Wreq::BodySender.new
|
|
73
|
+
sender.close
|
|
74
|
+
|
|
75
|
+
error = assert_raises(IOError) { sender.push("data") }
|
|
76
|
+
assert_equal "closed body sender", error.message
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def test_queued_chunks_survive_close_before_request_attachment
|
|
80
|
+
sender = Wreq::BodySender.new(2)
|
|
81
|
+
sender.push("queued-")
|
|
82
|
+
sender.push("body")
|
|
83
|
+
sender.close
|
|
84
|
+
|
|
85
|
+
with_request_body_server do |url, request_thread|
|
|
86
|
+
response = Wreq.post(url, body: sender)
|
|
87
|
+
|
|
88
|
+
assert_equal 200, response.code
|
|
89
|
+
assert_equal "queued-body", request_thread.value
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def test_valid_capacity_keeps_backpressure_until_request_drains
|
|
94
|
+
sender = Wreq::BodySender.new(1)
|
|
95
|
+
sender.push("first-")
|
|
96
|
+
producer = Thread.new do
|
|
97
|
+
sender.push("second")
|
|
98
|
+
sender.close
|
|
99
|
+
end
|
|
100
|
+
producer.report_on_exception = false
|
|
101
|
+
|
|
102
|
+
sleep 0.05
|
|
103
|
+
assert_predicate producer, :alive?, "the second push should wait for channel capacity"
|
|
104
|
+
|
|
105
|
+
with_request_body_server do |url, request_thread|
|
|
106
|
+
response = Wreq.post(url, body: sender)
|
|
107
|
+
|
|
108
|
+
assert_equal 200, response.code
|
|
109
|
+
assert producer.join(5), "the blocked producer should finish once the request drains"
|
|
110
|
+
assert_equal "first-second", request_thread.value
|
|
111
|
+
end
|
|
112
|
+
ensure
|
|
113
|
+
producer&.kill if producer&.alive?
|
|
114
|
+
producer&.join(5)
|
|
115
|
+
sender&.close
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def test_receiver_termination_closes_sender
|
|
119
|
+
sender = Wreq::BodySender.new(1)
|
|
120
|
+
sender.push("data")
|
|
121
|
+
|
|
122
|
+
with_reset_server do |url|
|
|
123
|
+
assert_raises(StandardError) { Wreq.post(url, body: sender, timeout: 2) }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
assert wait_until { sender.closed? }, "sender should close after the request drops its receiver"
|
|
127
|
+
assert_raises(IOError) { sender.push("more") }
|
|
128
|
+
ensure
|
|
129
|
+
sender&.close
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def test_zero_capacity_regression_exits_subprocess_normally
|
|
133
|
+
lib_dir = File.expand_path("../lib", __dir__)
|
|
134
|
+
script = <<~RUBY
|
|
135
|
+
require "wreq"
|
|
136
|
+
|
|
137
|
+
begin
|
|
138
|
+
Wreq::BodySender.new(0)
|
|
139
|
+
rescue ArgumentError => error
|
|
140
|
+
warn "\#{error.class}: \#{error.message}"
|
|
141
|
+
exit 0
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
warn "zero capacity did not raise"
|
|
145
|
+
exit 2
|
|
146
|
+
RUBY
|
|
147
|
+
|
|
148
|
+
_stdout, stderr, status = Open3.capture3(RbConfig.ruby, "-I", lib_dir, "-e", script)
|
|
149
|
+
|
|
150
|
+
assert status.success?, "subprocess failed with #{status.inspect}: #{stderr}"
|
|
151
|
+
assert_match(/ArgumentError:.*capacity/, stderr)
|
|
152
|
+
refute_match(/panicked|mpsc bounded channel/i, stderr)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
private
|
|
156
|
+
|
|
157
|
+
def wait_until(timeout: 2)
|
|
158
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
159
|
+
loop do
|
|
160
|
+
return true if yield
|
|
161
|
+
return false if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
162
|
+
|
|
163
|
+
sleep 0.01
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def with_request_body_server
|
|
168
|
+
server = TCPServer.new("127.0.0.1", 0)
|
|
169
|
+
port = server.addr[1]
|
|
170
|
+
request_thread = Thread.new do
|
|
171
|
+
socket = server.accept
|
|
172
|
+
begin
|
|
173
|
+
socket.gets
|
|
174
|
+
headers = read_headers(socket)
|
|
175
|
+
body = read_request_body(socket, headers)
|
|
176
|
+
socket.write "HTTP/1.1 200 OK\r\n"
|
|
177
|
+
socket.write "Content-Length: 0\r\n"
|
|
178
|
+
socket.write "Connection: close\r\n\r\n"
|
|
179
|
+
body
|
|
180
|
+
ensure
|
|
181
|
+
socket.close unless socket.closed?
|
|
182
|
+
end
|
|
183
|
+
ensure
|
|
184
|
+
server.close unless server.closed?
|
|
185
|
+
end
|
|
186
|
+
request_thread.report_on_exception = false
|
|
187
|
+
|
|
188
|
+
yield "http://127.0.0.1:#{port}/", request_thread
|
|
189
|
+
ensure
|
|
190
|
+
server&.close unless server&.closed?
|
|
191
|
+
request_thread&.join(5)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def with_reset_server
|
|
195
|
+
server = TCPServer.new("127.0.0.1", 0)
|
|
196
|
+
port = server.addr[1]
|
|
197
|
+
thread = Thread.new do
|
|
198
|
+
socket = server.accept
|
|
199
|
+
socket.close
|
|
200
|
+
ensure
|
|
201
|
+
server.close unless server.closed?
|
|
202
|
+
end
|
|
203
|
+
thread.report_on_exception = false
|
|
204
|
+
|
|
205
|
+
yield "http://127.0.0.1:#{port}/"
|
|
206
|
+
ensure
|
|
207
|
+
server&.close unless server&.closed?
|
|
208
|
+
thread&.join(5)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def read_headers(socket)
|
|
212
|
+
headers = {}
|
|
213
|
+
while (line = socket.gets)
|
|
214
|
+
break if line == "\r\n"
|
|
215
|
+
|
|
216
|
+
name, value = line.split(":", 2)
|
|
217
|
+
headers[name.downcase] = value.strip
|
|
218
|
+
end
|
|
219
|
+
headers
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def read_request_body(socket, headers)
|
|
223
|
+
if headers.fetch("transfer-encoding", "").downcase.include?("chunked")
|
|
224
|
+
read_chunked_body(socket)
|
|
225
|
+
else
|
|
226
|
+
socket.read(headers.fetch("content-length", "0").to_i)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def read_chunked_body(socket)
|
|
231
|
+
body = "".b
|
|
232
|
+
loop do
|
|
233
|
+
size_line = socket.gets or raise EOFError, "missing chunk size"
|
|
234
|
+
size = Integer(size_line.split(";", 2).first, 16)
|
|
235
|
+
break if size.zero?
|
|
236
|
+
|
|
237
|
+
body << socket.read(size)
|
|
238
|
+
raise IOError, "missing chunk terminator" unless socket.read(2) == "\r\n"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
while (line = socket.gets)
|
|
242
|
+
break if line == "\r\n"
|
|
243
|
+
end
|
|
244
|
+
body
|
|
245
|
+
end
|
|
246
|
+
end
|
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
|