wreq 1.0.0-aarch64-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 +7 -0
- data/Gemfile +17 -0
- data/LICENSE +201 -0
- data/README.md +150 -0
- data/Rakefile +90 -0
- data/examples/body.rb +42 -0
- data/examples/client.rb +33 -0
- data/examples/emulation_request.rb +37 -0
- data/examples/headers.rb +27 -0
- data/examples/proxy.rb +113 -0
- data/examples/send_stream.rb +85 -0
- data/examples/stream.rb +14 -0
- data/examples/thread_interrupt.rb +83 -0
- data/extconf.rb +7 -0
- data/lib/wreq.rb +313 -0
- 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 +36 -0
- data/lib/wreq_ruby/client.rb +516 -0
- data/lib/wreq_ruby/cookie.rb +144 -0
- data/lib/wreq_ruby/emulation.rb +186 -0
- data/lib/wreq_ruby/error.rb +159 -0
- data/lib/wreq_ruby/header.rb +197 -0
- data/lib/wreq_ruby/http.rb +132 -0
- data/lib/wreq_ruby/response.rb +208 -0
- data/script/build_platform_gem.rb +34 -0
- data/test/client_cookie_test.rb +46 -0
- data/test/client_test.rb +136 -0
- data/test/cookie_test.rb +166 -0
- data/test/emulation_test.rb +21 -0
- data/test/error_handling_test.rb +89 -0
- data/test/header_test.rb +290 -0
- data/test/module_methods_test.rb +75 -0
- data/test/request_parameters_test.rb +175 -0
- data/test/request_test.rb +234 -0
- data/test/response_test.rb +69 -0
- data/test/stream_test.rb +81 -0
- data/test/test_helper.rb +9 -0
- data/wreq.gemspec +68 -0
- metadata +98 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
unless defined?(Wreq)
|
|
4
|
+
module Wreq
|
|
5
|
+
# HTTP response object containing status, headers, and body.
|
|
6
|
+
#
|
|
7
|
+
# This class wraps a native Rust implementation providing efficient
|
|
8
|
+
# access to HTTP response data including status codes, headers, body
|
|
9
|
+
# content, and streaming capabilities.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic response handling
|
|
12
|
+
# response = client.get("https://api.example.com")
|
|
13
|
+
# puts response.status.as_int # => 200
|
|
14
|
+
# puts response.text
|
|
15
|
+
#
|
|
16
|
+
# @example JSON response
|
|
17
|
+
# response = client.get("https://api.example.com/data")
|
|
18
|
+
# data = response.json
|
|
19
|
+
#
|
|
20
|
+
# @example Streaming response
|
|
21
|
+
# response = client.get("https://example.com/large-file")
|
|
22
|
+
# response.stream.each do |chunk|
|
|
23
|
+
# # Process chunk
|
|
24
|
+
# end
|
|
25
|
+
class Response
|
|
26
|
+
# Get the HTTP status code as an integer.
|
|
27
|
+
#
|
|
28
|
+
# @return [Integer] Status code (e.g., 200, 404, 500)
|
|
29
|
+
# @example
|
|
30
|
+
# response.code # => 200
|
|
31
|
+
def code
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Get the HTTP status code object.
|
|
35
|
+
#
|
|
36
|
+
# @return [Wreq::StatusCode] Status code wrapper with helper methods
|
|
37
|
+
# @example
|
|
38
|
+
# status = response.status
|
|
39
|
+
# status.success? # => true
|
|
40
|
+
def status
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Get the HTTP protocol version used.
|
|
44
|
+
#
|
|
45
|
+
# @return [Wreq::Version] HTTP version (HTTP/1.1, HTTP/2, etc.)
|
|
46
|
+
# @example
|
|
47
|
+
# response.version # => Wreq::Version::HTTP_11
|
|
48
|
+
def version
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Get the final URL after redirects.
|
|
52
|
+
#
|
|
53
|
+
# @return [String] The final URL
|
|
54
|
+
# @example
|
|
55
|
+
# response.url # => "https://example.com/final-page"
|
|
56
|
+
def url
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Get the content length if known.
|
|
60
|
+
#
|
|
61
|
+
# @return [Integer, nil] Content length in bytes, or nil if unknown
|
|
62
|
+
# @example
|
|
63
|
+
# response.content_length # => 1024
|
|
64
|
+
def content_length
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get the local socket address.
|
|
68
|
+
#
|
|
69
|
+
# @return [String, nil] Local address (e.g., "127.0.0.1:54321"), or nil
|
|
70
|
+
# @example
|
|
71
|
+
# response.local_addr # => "192.168.1.100:54321"
|
|
72
|
+
def local_addr
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Get the remote socket address.
|
|
76
|
+
#
|
|
77
|
+
# @return [String, nil] Remote address (e.g., "93.184.216.34:443"), or nil
|
|
78
|
+
# @example
|
|
79
|
+
# response.remote_addr # => "93.184.216.34:443"
|
|
80
|
+
def remote_addr
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Get the response bytes as a binary string.
|
|
84
|
+
# @return [String] Response body as binary data
|
|
85
|
+
# @example
|
|
86
|
+
# binary_data = response.bytes
|
|
87
|
+
# puts binary_data.size # => 1024
|
|
88
|
+
def bytes
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Get the response body as text.
|
|
92
|
+
#
|
|
93
|
+
# @return [String] Response body decoded as UTF-8 text
|
|
94
|
+
# @example
|
|
95
|
+
# html = response.text
|
|
96
|
+
# puts html
|
|
97
|
+
# @raise [Wreq::DecodingError] if body cannot be decoded as binary
|
|
98
|
+
def text
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Get the response body as text with a specific charset.
|
|
102
|
+
# This method allows you to specify a default encoding
|
|
103
|
+
# to use when decoding the response body.
|
|
104
|
+
# # @param default_encoding [String] Default encoding to use (e.g., "UTF-8")
|
|
105
|
+
# # @return [String] Response body decoded as text using the specified encoding
|
|
106
|
+
# @example
|
|
107
|
+
# html = response.text_with_charset("ISO-8859-1")
|
|
108
|
+
# puts html
|
|
109
|
+
# @raise [Wreq::DecodingError] if body cannot be decoded with the specified encoding
|
|
110
|
+
def text_with_charset(default_encoding)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Parse the response body as JSON.
|
|
114
|
+
#
|
|
115
|
+
# @return [Object] Parsed JSON (Hash, Array, String, Integer, Float, Boolean, nil)
|
|
116
|
+
# @raise [Wreq::DecodingError] if body is not valid JSON
|
|
117
|
+
# @example
|
|
118
|
+
# data = response.json
|
|
119
|
+
# puts data["key"]
|
|
120
|
+
def json
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Get a streaming iterator for the response body, yielding each chunk.
|
|
124
|
+
#
|
|
125
|
+
# This method allows you to process large HTTP responses efficiently,
|
|
126
|
+
# by yielding each chunk of the body as it arrives, without loading
|
|
127
|
+
# the entire response into memory.
|
|
128
|
+
#
|
|
129
|
+
# @return An iterator over response body chunks (binary String)
|
|
130
|
+
# @yield [chunk] Each chunk of the response body as a binary String
|
|
131
|
+
# @example Save response to file
|
|
132
|
+
# File.open("output.bin", "wb") do |f|
|
|
133
|
+
# response.chunks { |chunk| f.write(chunk) }
|
|
134
|
+
# end
|
|
135
|
+
# @example Count total bytes streamed
|
|
136
|
+
# total = 0
|
|
137
|
+
# response.chunks { |chunk| total += chunk.bytesize }
|
|
138
|
+
# puts "Downloaded #{total} bytes"
|
|
139
|
+
#
|
|
140
|
+
# Note: The returned Receiver is only for reading response bodies, not for uploads.
|
|
141
|
+
def chunks
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Close the response and free associated resources.
|
|
145
|
+
#
|
|
146
|
+
# @return [void]
|
|
147
|
+
# @example
|
|
148
|
+
# response.close
|
|
149
|
+
def close
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# ======================== Ruby API Extensions ========================
|
|
156
|
+
|
|
157
|
+
module Wreq
|
|
158
|
+
class Response
|
|
159
|
+
# Returns a compact string representation of the response.
|
|
160
|
+
#
|
|
161
|
+
# Format: #<Wreq::Response STATUS content-type="..." body=SIZE>
|
|
162
|
+
#
|
|
163
|
+
# @return [String] Compact formatted response information
|
|
164
|
+
# @example
|
|
165
|
+
# puts response.to_s
|
|
166
|
+
# # => #<Wreq::Response 200 content-type="application/json" body=456B>
|
|
167
|
+
def to_s
|
|
168
|
+
parts = ["#<Wreq::Response"]
|
|
169
|
+
|
|
170
|
+
# Status code
|
|
171
|
+
parts << code.to_s
|
|
172
|
+
|
|
173
|
+
# Content-Type header if present
|
|
174
|
+
if headers.respond_to?(:get)
|
|
175
|
+
content_type = headers.get("content-type")
|
|
176
|
+
parts << "content-type=#{content_type.inspect}" if content_type
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Body size
|
|
180
|
+
if content_length
|
|
181
|
+
parts << "body=#{format_bytes(content_length)}"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
parts.join(" ") + ">"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
def format_bytes(bytes)
|
|
190
|
+
return "0B" if bytes.zero?
|
|
191
|
+
|
|
192
|
+
units = ["B", "KB", "MB", "GB"]
|
|
193
|
+
size = bytes.to_f
|
|
194
|
+
unit_index = 0
|
|
195
|
+
|
|
196
|
+
while size >= 1024 && unit_index < units.length - 1
|
|
197
|
+
size /= 1024.0
|
|
198
|
+
unit_index += 1
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
if unit_index == 0
|
|
202
|
+
"#{size.to_i}#{units[unit_index]}"
|
|
203
|
+
else
|
|
204
|
+
"#{size.round(1)}#{units[unit_index]}"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Build a platform-specific gem with pre-compiled native extensions.
|
|
5
|
+
#
|
|
6
|
+
# Usage: ruby script/build_platform_gem.rb PLATFORM
|
|
7
|
+
# Example: ruby script/build_platform_gem.rb arm64-darwin
|
|
8
|
+
#
|
|
9
|
+
# Expects compiled .bundle/.so files in version-specific directories:
|
|
10
|
+
# lib/wreq_ruby/3.3/wreq_ruby.bundle
|
|
11
|
+
# lib/wreq_ruby/3.4/wreq_ruby.bundle
|
|
12
|
+
# lib/wreq_ruby/4.0/wreq_ruby.bundle
|
|
13
|
+
|
|
14
|
+
require "rubygems/package"
|
|
15
|
+
require "fileutils"
|
|
16
|
+
|
|
17
|
+
platform = ARGV.fetch(0) { abort "Usage: #{$0} PLATFORM" }
|
|
18
|
+
|
|
19
|
+
spec = Gem::Specification.load("wreq.gemspec")
|
|
20
|
+
spec.platform = Gem::Platform.new(platform)
|
|
21
|
+
spec.extensions = []
|
|
22
|
+
# Keep in sync with Rakefile cross_compiling block
|
|
23
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.3", "< 4.1.dev")
|
|
24
|
+
|
|
25
|
+
# Add version-specific compiled extensions
|
|
26
|
+
binaries = Dir.glob("lib/wreq_ruby/[0-9]*/*.{bundle,so}")
|
|
27
|
+
abort "No compiled binaries found in lib/wreq_ruby/*/. Did compilation succeed?" if binaries.empty?
|
|
28
|
+
spec.files += binaries
|
|
29
|
+
|
|
30
|
+
FileUtils.mkdir_p("pkg")
|
|
31
|
+
gem_file = Gem::Package.build(spec)
|
|
32
|
+
FileUtils.mv(gem_file, "pkg/")
|
|
33
|
+
|
|
34
|
+
puts "Built: pkg/#{File.basename(gem_file)}"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class ClientCookieProviderTest < Minitest::Test
|
|
6
|
+
HOST = "http://localhost:8080"
|
|
7
|
+
|
|
8
|
+
def setup
|
|
9
|
+
@jar = Wreq::Jar.new
|
|
10
|
+
@client = Wreq::Client.new(
|
|
11
|
+
cookie_store: true,
|
|
12
|
+
cookie_provider: @jar,
|
|
13
|
+
allow_redirects: true
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_custom_jar_captures_set_cookie_and_sends_back
|
|
18
|
+
# starts empty
|
|
19
|
+
assert_kind_of Array, @jar.get_all
|
|
20
|
+
assert_equal 0, @jar.get_all.length
|
|
21
|
+
|
|
22
|
+
# server sets cookie; client follows redirect; jar should store it
|
|
23
|
+
res1 = @client.get("#{HOST}/cookies/set?foo=bar")
|
|
24
|
+
assert_equal 200, res1.code
|
|
25
|
+
|
|
26
|
+
names = @jar.get_all.map(&:name)
|
|
27
|
+
assert_includes names, "foo"
|
|
28
|
+
|
|
29
|
+
# subsequent request should send the stored cookie automatically
|
|
30
|
+
res2 = @client.get("#{HOST}/cookies")
|
|
31
|
+
assert_equal 200, res2.code
|
|
32
|
+
body = res2.json
|
|
33
|
+
assert_kind_of Hash, body
|
|
34
|
+
assert_equal "bar", body.dig("cookies", "foo")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_prepopulated_jar_is_used_by_client
|
|
38
|
+
# pre-populate jar
|
|
39
|
+
@jar.add_cookie_str("pref=1; Path=/", "#{HOST}/")
|
|
40
|
+
|
|
41
|
+
res = @client.get("#{HOST}/cookies")
|
|
42
|
+
assert_equal 200, res.code
|
|
43
|
+
cookies = res.json["cookies"]
|
|
44
|
+
assert_equal "1", cookies["pref"]
|
|
45
|
+
end
|
|
46
|
+
end
|
data/test/client_test.rb
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
class ClientTest < Minitest::Test
|
|
4
|
+
def setup
|
|
5
|
+
@client = Wreq::Client.new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def test_client_creation
|
|
9
|
+
refute_nil @client
|
|
10
|
+
assert_instance_of Wreq::Client, @client
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_client_with_configuration
|
|
14
|
+
client = Wreq::Client.new(
|
|
15
|
+
timeout: 30,
|
|
16
|
+
headers: {"User-Agent" => "wreq-ruby/test"}
|
|
17
|
+
)
|
|
18
|
+
refute_nil client
|
|
19
|
+
assert_instance_of Wreq::Client, client
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_get_request
|
|
23
|
+
response = @client.get("http://localhost:8080/get")
|
|
24
|
+
refute_nil response
|
|
25
|
+
assert_equal 200, response.code
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_post_request
|
|
29
|
+
response = @client.post("http://localhost:8080/post",
|
|
30
|
+
json: {test: "wreq-ruby"})
|
|
31
|
+
refute_nil response
|
|
32
|
+
assert_equal 200, response.code
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_put_request
|
|
36
|
+
response = @client.put("http://localhost:8080/put",
|
|
37
|
+
json: {data: "test"})
|
|
38
|
+
refute_nil response
|
|
39
|
+
assert_equal 200, response.code
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def test_delete_request
|
|
43
|
+
response = @client.delete("http://localhost:8080/delete")
|
|
44
|
+
refute_nil response
|
|
45
|
+
assert_equal 200, response.code
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_patch_request
|
|
49
|
+
response = @client.patch("http://localhost:8080/patch",
|
|
50
|
+
json: {update: "field"})
|
|
51
|
+
refute_nil response
|
|
52
|
+
assert_equal 200, response.code
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_request_with_query_params
|
|
56
|
+
response = @client.get("http://localhost:8080/get",
|
|
57
|
+
query: {"param" => "value"})
|
|
58
|
+
refute_nil response
|
|
59
|
+
assert_equal 200, response.code
|
|
60
|
+
assert_includes response.text, "param"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_request_with_form_data
|
|
64
|
+
response = @client.post("http://localhost:8080/post",
|
|
65
|
+
form: {"field" => "value"})
|
|
66
|
+
refute_nil response
|
|
67
|
+
assert_equal 200, response.code
|
|
68
|
+
assert_includes response.text, "field"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_request_with_raw_body
|
|
72
|
+
response = @client.post("http://localhost:8080/post",
|
|
73
|
+
body: "raw content",
|
|
74
|
+
headers: {"Content-Type" => "text/plain"})
|
|
75
|
+
refute_nil response
|
|
76
|
+
assert_equal 200, response.code
|
|
77
|
+
assert_includes response.text, "raw content"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def test_basic_authentication
|
|
81
|
+
response = @client.get("http://localhost:8080/basic-auth/user/pass",
|
|
82
|
+
basic_auth: ["user", "pass"])
|
|
83
|
+
refute_nil response
|
|
84
|
+
assert_equal 200, response.code
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def test_bearer_authentication
|
|
88
|
+
response = @client.get("http://localhost:8080/bearer",
|
|
89
|
+
bearer_auth: "test-token")
|
|
90
|
+
refute_nil response
|
|
91
|
+
assert_equal 200, response.code
|
|
92
|
+
assert_includes response.text, "test-token"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def test_redirect_following
|
|
96
|
+
response = @client.get("http://localhost:8080/redirect/1",
|
|
97
|
+
allow_redirects: true)
|
|
98
|
+
refute_nil response
|
|
99
|
+
assert_equal 200, response.code
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def test_redirect_blocking
|
|
103
|
+
response = @client.get("http://localhost:8080/redirect/1",
|
|
104
|
+
allow_redirects: false)
|
|
105
|
+
refute_nil response
|
|
106
|
+
assert_equal 302, response.code
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def test_gzip_compression
|
|
110
|
+
response = @client.get("http://localhost:8080/gzip", gzip: true)
|
|
111
|
+
refute_nil response
|
|
112
|
+
assert_equal 200, response.code
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def test_timeout_handling
|
|
116
|
+
# Test that short timeouts properly raise exceptions
|
|
117
|
+
assert_raises(Wreq::TimeoutError) do
|
|
118
|
+
@client.get("http://localhost:8080/delay/10", timeout: 1)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Test that reasonable timeouts work normally
|
|
122
|
+
start_time = Time.now
|
|
123
|
+
response = @client.get("http://localhost:8080/delay/1", timeout: 5)
|
|
124
|
+
elapsed = Time.now - start_time
|
|
125
|
+
|
|
126
|
+
refute_nil response
|
|
127
|
+
assert_equal 200, response.code
|
|
128
|
+
assert elapsed < 5, "Request should complete within timeout"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def test_status_codes
|
|
132
|
+
response = @client.get("http://localhost:8080/status/404")
|
|
133
|
+
refute_nil response
|
|
134
|
+
assert_equal 404, response.code
|
|
135
|
+
end
|
|
136
|
+
end
|
data/test/cookie_test.rb
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class CookieTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
@jar = Wreq::Jar.new
|
|
8
|
+
@base_url = "https://example.com"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_jar_initially_empty
|
|
12
|
+
assert_instance_of Wreq::Jar, @jar
|
|
13
|
+
cookies = begin
|
|
14
|
+
Wreq::Jar.get_all(@jar)
|
|
15
|
+
rescue
|
|
16
|
+
@jar.get_all
|
|
17
|
+
end # support either binding style
|
|
18
|
+
assert_kind_of Array, cookies
|
|
19
|
+
assert_equal 0, cookies.length
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_add_cookie_str_and_get_all
|
|
23
|
+
set_cookie = "sid=abc123; Path=/; Domain=example.com; HttpOnly; Secure"
|
|
24
|
+
@jar.add_cookie_str(set_cookie, @base_url)
|
|
25
|
+
|
|
26
|
+
cookies = @jar.get_all
|
|
27
|
+
assert_kind_of Array, cookies
|
|
28
|
+
assert_equal 1, cookies.length
|
|
29
|
+
|
|
30
|
+
c = cookies.first
|
|
31
|
+
assert_instance_of Wreq::Cookie, c
|
|
32
|
+
assert_equal "sid", c.name
|
|
33
|
+
assert_equal "abc123", c.value
|
|
34
|
+
|
|
35
|
+
# attributes parsed from Set-Cookie
|
|
36
|
+
assert_equal "/", c.path
|
|
37
|
+
assert_equal "example.com", c.domain
|
|
38
|
+
|
|
39
|
+
# predicate-ish flags
|
|
40
|
+
assert_equal true, (c.http_only || c.http_only?)
|
|
41
|
+
assert_equal true, (c.secure || c.secure?)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_add_multiple_and_remove
|
|
45
|
+
@jar.add_cookie_str("a=1; Path=/", @base_url)
|
|
46
|
+
@jar.add_cookie_str("b=2; Path=/", @base_url)
|
|
47
|
+
@jar.add_cookie_str("c=3; Path=/", @base_url)
|
|
48
|
+
|
|
49
|
+
cookies = @jar.get_all
|
|
50
|
+
assert_equal 3, cookies.length
|
|
51
|
+
|
|
52
|
+
# remove one by name
|
|
53
|
+
@jar.remove("b", @base_url)
|
|
54
|
+
names = @jar.get_all.map(&:name)
|
|
55
|
+
refute_includes names, "b"
|
|
56
|
+
assert_includes names, "a"
|
|
57
|
+
assert_includes names, "c"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_clear
|
|
61
|
+
@jar.add_cookie_str("x=1; Path=/", @base_url)
|
|
62
|
+
@jar.add_cookie_str("y=2; Path=/", @base_url)
|
|
63
|
+
refute_empty @jar.get_all
|
|
64
|
+
|
|
65
|
+
@jar.clear
|
|
66
|
+
assert_empty @jar.get_all
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_max_age_and_expires_optional
|
|
70
|
+
# Max-Age only
|
|
71
|
+
@jar.clear
|
|
72
|
+
@jar.add_cookie_str("ma=1; Max-Age=3600; Path=/", @base_url)
|
|
73
|
+
c1 = @jar.get_all.find { |c| c.name == "ma" }
|
|
74
|
+
assert c1
|
|
75
|
+
# can be nil or Integer; just ensure responds and is truthy integer
|
|
76
|
+
if (v = c1.max_age)
|
|
77
|
+
assert_kind_of Integer, v
|
|
78
|
+
assert_operator v, :>=, 0
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Expires only
|
|
82
|
+
@jar.clear
|
|
83
|
+
t = Time.now + 3600
|
|
84
|
+
@jar.add_cookie_str("exp=1; Expires=#{t.gmtime.strftime("%a, %d %b %Y %H:%M:%S GMT")}; Path=/", @base_url)
|
|
85
|
+
c2 = @jar.get_all.find { |c| c.name == "exp" }
|
|
86
|
+
assert c2
|
|
87
|
+
# expires returns Float (unix seconds) or nil
|
|
88
|
+
if (e = c2.expires)
|
|
89
|
+
assert_kind_of Float, e
|
|
90
|
+
assert_operator e, :>, Time.now.to_f - 1_000_000 # sanity bound
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# -------- Wreq::Cookie unit tests --------
|
|
95
|
+
|
|
96
|
+
def test_cookie_new_minimal
|
|
97
|
+
c = Wreq::Cookie.new("sid", "abc")
|
|
98
|
+
|
|
99
|
+
assert_instance_of Wreq::Cookie, c
|
|
100
|
+
assert_equal "sid", c.name
|
|
101
|
+
assert_equal "abc", c.value
|
|
102
|
+
|
|
103
|
+
assert_nil c.path
|
|
104
|
+
assert_nil c.domain
|
|
105
|
+
assert_nil c.max_age
|
|
106
|
+
assert_nil c.expires
|
|
107
|
+
|
|
108
|
+
assert_equal false, (c.http_only || c.http_only?)
|
|
109
|
+
assert_equal false, (c.secure || c.secure?)
|
|
110
|
+
assert_equal false, c.same_site_lax?
|
|
111
|
+
assert_equal false, c.same_site_strict?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def test_cookie_new_full_attributes
|
|
115
|
+
exp = Time.now.to_f + 7200.0
|
|
116
|
+
c = Wreq::Cookie.new("sess", "v",
|
|
117
|
+
domain: "example.com",
|
|
118
|
+
path: "/",
|
|
119
|
+
max_age: 3600,
|
|
120
|
+
expires: exp,
|
|
121
|
+
http_only: true,
|
|
122
|
+
secure: true,
|
|
123
|
+
same_site: Wreq::SameSite::Lax)
|
|
124
|
+
|
|
125
|
+
assert_equal "sess", c.name
|
|
126
|
+
assert_equal "v", c.value
|
|
127
|
+
assert_equal "example.com", c.domain
|
|
128
|
+
assert_equal "/", c.path
|
|
129
|
+
|
|
130
|
+
# Max-Age returns seconds as Integer
|
|
131
|
+
assert_equal 3600, c.max_age
|
|
132
|
+
|
|
133
|
+
# Expires returns Float seconds-since-epoch (with small tolerance)
|
|
134
|
+
assert c.expires
|
|
135
|
+
assert_kind_of Float, c.expires
|
|
136
|
+
assert_in_delta exp, c.expires, 2.0
|
|
137
|
+
|
|
138
|
+
assert_equal true, (c.http_only || c.http_only?)
|
|
139
|
+
assert_equal true, (c.secure || c.secure?)
|
|
140
|
+
# constructor currently sets SameSite to none
|
|
141
|
+
assert_equal true, c.same_site_lax?
|
|
142
|
+
assert_equal false, c.same_site_strict?
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def test_same_site_flags_from_parsed_header
|
|
146
|
+
@jar.clear
|
|
147
|
+
@jar.add_cookie_str("s1=1; Path=/; SameSite=Strict", @base_url)
|
|
148
|
+
@jar.add_cookie_str("s2=1; Path=/; SameSite=Lax", @base_url)
|
|
149
|
+
|
|
150
|
+
cookies = @jar.get_all
|
|
151
|
+
h = cookies.to_h { |ck| [ck.name, [ck.same_site_strict?, ck.same_site_lax?]] }
|
|
152
|
+
|
|
153
|
+
assert_equal [true, false], h["s1"]
|
|
154
|
+
assert_equal [false, true], h["s2"]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def test_request_cookie_value_percent_encoding
|
|
158
|
+
raw_value = "hello world?"
|
|
159
|
+
client = Wreq::Client.new
|
|
160
|
+
resp = client.get(
|
|
161
|
+
"http://localhost:8080/cookies",
|
|
162
|
+
cookies: {"mykey" => raw_value}
|
|
163
|
+
)
|
|
164
|
+
assert_includes resp.text, "hello%20world%3F"
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class EmulationTest < Minitest::Test
|
|
6
|
+
def test_all_emulation_device_constants_are_non_nil
|
|
7
|
+
Wreq::EmulationDevice.constants.each do |name|
|
|
8
|
+
const = Wreq::EmulationDevice.const_get(name)
|
|
9
|
+
assert_instance_of Wreq::EmulationDevice, const,
|
|
10
|
+
"#{name} should be EmulationDevice, got #{const.inspect}"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_all_emulation_os_constants_are_non_nil
|
|
15
|
+
Wreq::EmulationOS.constants.each do |name|
|
|
16
|
+
const = Wreq::EmulationOS.const_get(name)
|
|
17
|
+
assert_instance_of Wreq::EmulationOS, const,
|
|
18
|
+
"#{name} should be EmulationOS, got #{const.inspect}"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
class ErrorHandlingTest < Minitest::Test
|
|
4
|
+
def test_network_error_handling
|
|
5
|
+
# Try to connect to a non-existent domain
|
|
6
|
+
response = Wreq.get("https://definitely-not-a-real-domain-12345.com")
|
|
7
|
+
flunk "Expected network error but got response: #{response.code}"
|
|
8
|
+
rescue => e
|
|
9
|
+
assert_instance_of Wreq::ConnectionError, e
|
|
10
|
+
# Network errors should be caught and wrapped appropriately
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_invalid_url_handling
|
|
14
|
+
# Invalid URL format
|
|
15
|
+
response = Wreq.get("not-a-valid-url")
|
|
16
|
+
flunk "Expected URL error but got response: #{response.code}"
|
|
17
|
+
rescue => e
|
|
18
|
+
assert_instance_of Wreq::BuilderError, e
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_http_error_status_codes
|
|
22
|
+
# These should not raise errors, just return responses with error codes
|
|
23
|
+
[400, 401, 403, 404, 500, 502, 503].each do |status_code|
|
|
24
|
+
response = Wreq.get("http://localhost:8080/status/#{status_code}")
|
|
25
|
+
assert_equal status_code, response.code
|
|
26
|
+
# Should not raise an exception for HTTP error codes
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_timeout_handling
|
|
31
|
+
# Test timeout with a delay that should definitely cause timeout
|
|
32
|
+
|
|
33
|
+
# Request with a very short timeout that should fail
|
|
34
|
+
response = Wreq.get("http://localhost:8080/delay/10", timeout: 1)
|
|
35
|
+
# If we get here, the request didn't timeout (unexpected)
|
|
36
|
+
flunk "Expected timeout error but got response: #{response.code}"
|
|
37
|
+
rescue => e
|
|
38
|
+
# Timeout error is expected
|
|
39
|
+
assert_instance_of Wreq::TimeoutError, e
|
|
40
|
+
# Could also check error message contains timeout-related keywords
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_invalid_json_response
|
|
44
|
+
# Get a non-JSON response and try to parse as JSON
|
|
45
|
+
response = Wreq.get("http://localhost:8080/html")
|
|
46
|
+
assert_equal 200, response.code
|
|
47
|
+
|
|
48
|
+
# Should raise an error when trying to parse HTML as JSON
|
|
49
|
+
begin
|
|
50
|
+
response.json
|
|
51
|
+
rescue => e
|
|
52
|
+
assert_instance_of Wreq::DecodingError, e
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_empty_response_json
|
|
57
|
+
response = Wreq.get("http://localhost:8080/status/204")
|
|
58
|
+
assert_equal 204, response.code
|
|
59
|
+
assert_equal "", response.text
|
|
60
|
+
|
|
61
|
+
# Empty body should raise error when parsing as JSON
|
|
62
|
+
begin
|
|
63
|
+
response.json
|
|
64
|
+
rescue => e
|
|
65
|
+
assert_instance_of Wreq::DecodingError, e
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_proxy_error_handling
|
|
70
|
+
invalid_proxies = [
|
|
71
|
+
"http://invalid.proxy:8080",
|
|
72
|
+
"https://invalid.proxy:8080",
|
|
73
|
+
"socks4://invalid.proxy:8080",
|
|
74
|
+
"socks4a://invalid.proxy:8080",
|
|
75
|
+
"socks5://invalid.proxy:8080",
|
|
76
|
+
"socks5h://invalid.proxy:8080"
|
|
77
|
+
]
|
|
78
|
+
target_urls = ["https://example.com", "http://example.com"]
|
|
79
|
+
|
|
80
|
+
invalid_proxies.each do |proxy|
|
|
81
|
+
target_urls.each do |url|
|
|
82
|
+
Wreq.get(url, proxy: proxy, timeout: 5)
|
|
83
|
+
flunk "Expected proxy connection error but got response"
|
|
84
|
+
rescue => e
|
|
85
|
+
assert_instance_of Wreq::ProxyConnectionError, e
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|