wreq 1.2.3-x64-mingw-ucrt

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 (69) hide show
  1. checksums.yaml +7 -0
  2. data/Cargo.lock +1687 -0
  3. data/Cargo.toml +54 -0
  4. data/Gemfile +18 -0
  5. data/LICENSE +201 -0
  6. data/README.md +153 -0
  7. data/Rakefile +90 -0
  8. data/build.rs +16 -0
  9. data/docs/windows-gnu-tokio-crash.md +70 -0
  10. data/examples/body.rb +42 -0
  11. data/examples/client.rb +33 -0
  12. data/examples/cookie.rb +24 -0
  13. data/examples/emulate_request.rb +37 -0
  14. data/examples/headers.rb +27 -0
  15. data/examples/proxy.rb +113 -0
  16. data/examples/send_stream.rb +85 -0
  17. data/examples/stream.rb +14 -0
  18. data/examples/thread_interrupt.rb +83 -0
  19. data/extconf.rb +7 -0
  20. data/lib/wreq.rb +303 -0
  21. data/lib/wreq_ruby/3.3/wreq_ruby.so +0 -0
  22. data/lib/wreq_ruby/3.4/wreq_ruby.so +0 -0
  23. data/lib/wreq_ruby/4.0/wreq_ruby.so +0 -0
  24. data/lib/wreq_ruby/body.rb +36 -0
  25. data/lib/wreq_ruby/client.rb +526 -0
  26. data/lib/wreq_ruby/cookie.rb +156 -0
  27. data/lib/wreq_ruby/emulate.rb +232 -0
  28. data/lib/wreq_ruby/error.rb +157 -0
  29. data/lib/wreq_ruby/header.rb +205 -0
  30. data/lib/wreq_ruby/http.rb +160 -0
  31. data/lib/wreq_ruby/response.rb +209 -0
  32. data/script/build_platform_gem.rb +34 -0
  33. data/script/build_windows_gnu.ps1 +257 -0
  34. data/src/arch.rs +33 -0
  35. data/src/client/body/form.rs +2 -0
  36. data/src/client/body/json.rs +16 -0
  37. data/src/client/body/stream.rs +146 -0
  38. data/src/client/body.rs +57 -0
  39. data/src/client/param.rs +19 -0
  40. data/src/client/query.rs +2 -0
  41. data/src/client/req.rs +274 -0
  42. data/src/client/resp.rs +248 -0
  43. data/src/client.rs +413 -0
  44. data/src/cookie.rs +312 -0
  45. data/src/emulate.rs +376 -0
  46. data/src/error.rs +163 -0
  47. data/src/extractor.rs +117 -0
  48. data/src/gvl.rs +154 -0
  49. data/src/header.rs +245 -0
  50. data/src/http.rs +142 -0
  51. data/src/lib.rs +98 -0
  52. data/src/macros.rs +123 -0
  53. data/src/rt.rs +46 -0
  54. data/test/client_cookie_test.rb +46 -0
  55. data/test/client_test.rb +136 -0
  56. data/test/cookie_test.rb +182 -0
  57. data/test/emulation_test.rb +21 -0
  58. data/test/error_handling_test.rb +92 -0
  59. data/test/header_test.rb +290 -0
  60. data/test/inspect_test.rb +125 -0
  61. data/test/module_methods_test.rb +75 -0
  62. data/test/orig_header_test.rb +115 -0
  63. data/test/request_parameters_test.rb +175 -0
  64. data/test/request_test.rb +244 -0
  65. data/test/response_test.rb +69 -0
  66. data/test/stream_test.rb +397 -0
  67. data/test/test_helper.rb +52 -0
  68. data/wreq.gemspec +68 -0
  69. metadata +123 -0
data/src/http.rs ADDED
@@ -0,0 +1,142 @@
1
+ use magnus::{Error, Module, RModule, Ruby, TryConvert, Value, method, typed_data::Inspect};
2
+
3
+ define_ruby_enum!(
4
+ /// An HTTP version.
5
+ const,
6
+ Version,
7
+ "Wreq::Version",
8
+ wreq::Version,
9
+ HTTP_09,
10
+ HTTP_10,
11
+ HTTP_11,
12
+ HTTP_2,
13
+ HTTP_3,
14
+ );
15
+
16
+ define_ruby_enum!(
17
+ /// An HTTP method.
18
+ Method,
19
+ "Wreq::Method",
20
+ wreq::Method,
21
+ GET,
22
+ HEAD,
23
+ POST,
24
+ PUT,
25
+ DELETE,
26
+ OPTIONS,
27
+ TRACE,
28
+ PATCH,
29
+ );
30
+
31
+ /// HTTP status code.
32
+ #[derive(Clone, Copy, PartialEq, Eq, Hash)]
33
+ #[magnus::wrap(class = "Wreq::StatusCode", free_immediately, size)]
34
+ pub struct StatusCode(pub wreq::StatusCode);
35
+
36
+ // ===== impl Version =====
37
+
38
+ impl Version {
39
+ /// Convert version to string.
40
+ #[inline]
41
+ pub fn to_s(&self) -> String {
42
+ self.into_ffi().inspect()
43
+ }
44
+
45
+ /// Value-based equality for Ruby (`==`).
46
+ #[inline]
47
+ pub fn equals(&self, other: Value) -> bool {
48
+ <&Version>::try_convert(other)
49
+ .map(|other| *self == *other)
50
+ .unwrap_or(false)
51
+ }
52
+ }
53
+
54
+ impl TryConvert for Version {
55
+ fn try_convert(value: magnus::Value) -> Result<Self, magnus::Error> {
56
+ <&Version>::try_convert(value).cloned()
57
+ }
58
+ }
59
+
60
+ // ===== impl StatusCode =====
61
+
62
+ impl StatusCode {
63
+ /// Return the status code as an integer.
64
+ #[inline]
65
+ pub const fn as_int(&self) -> u16 {
66
+ self.0.as_u16()
67
+ }
68
+
69
+ /// Check if status is within 100-199.
70
+ #[inline]
71
+ pub fn is_informational(&self) -> bool {
72
+ self.0.is_informational()
73
+ }
74
+
75
+ /// Check if status is within 200-299.
76
+ #[inline]
77
+ pub fn is_success(&self) -> bool {
78
+ self.0.is_success()
79
+ }
80
+
81
+ /// Check if status is within 300-399.
82
+ #[inline]
83
+ pub fn is_redirection(&self) -> bool {
84
+ self.0.is_redirection()
85
+ }
86
+
87
+ /// Check if status is within 400-499.
88
+ #[inline]
89
+ pub fn is_client_error(&self) -> bool {
90
+ self.0.is_client_error()
91
+ }
92
+
93
+ /// Check if status is within 500-599.
94
+ #[inline]
95
+ pub fn is_server_error(&self) -> bool {
96
+ self.0.is_server_error()
97
+ }
98
+
99
+ /// Convert status code to string.
100
+ #[inline]
101
+ pub fn to_s(&self) -> String {
102
+ self.0.to_string()
103
+ }
104
+ }
105
+
106
+ impl From<wreq::StatusCode> for StatusCode {
107
+ fn from(status: wreq::StatusCode) -> Self {
108
+ Self(status)
109
+ }
110
+ }
111
+
112
+ pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), Error> {
113
+ let method_class = gem_module.define_class("Method", ruby.class_object())?;
114
+ method_class.const_set("GET", Method::GET)?;
115
+ method_class.const_set("POST", Method::POST)?;
116
+ method_class.const_set("PUT", Method::PUT)?;
117
+ method_class.const_set("DELETE", Method::DELETE)?;
118
+ method_class.const_set("PATCH", Method::PATCH)?;
119
+ method_class.const_set("HEAD", Method::HEAD)?;
120
+ method_class.const_set("TRACE", Method::TRACE)?;
121
+ method_class.const_set("OPTIONS", Method::OPTIONS)?;
122
+
123
+ let version_class = gem_module.define_class("Version", ruby.class_object())?;
124
+ version_class.const_set("HTTP_09", Version::HTTP_09)?;
125
+ version_class.const_set("HTTP_10", Version::HTTP_10)?;
126
+ version_class.const_set("HTTP_11", Version::HTTP_11)?;
127
+ version_class.const_set("HTTP_2", Version::HTTP_2)?;
128
+ version_class.const_set("HTTP_3", Version::HTTP_3)?;
129
+ version_class.define_method("to_s", method!(Version::to_s, 0))?;
130
+ version_class.define_method("==", method!(Version::equals, 1))?;
131
+
132
+ let status_code_class = gem_module.define_class("StatusCode", ruby.class_object())?;
133
+ status_code_class.define_method("as_int", method!(StatusCode::as_int, 0))?;
134
+ status_code_class.define_method("informational?", method!(StatusCode::is_informational, 0))?;
135
+ status_code_class.define_method("success?", method!(StatusCode::is_success, 0))?;
136
+ status_code_class.define_method("redirection?", method!(StatusCode::is_redirection, 0))?;
137
+ status_code_class.define_method("client_error?", method!(StatusCode::is_client_error, 0))?;
138
+ status_code_class.define_method("server_error?", method!(StatusCode::is_server_error, 0))?;
139
+ status_code_class.define_method("to_s", method!(StatusCode::to_s, 0))?;
140
+
141
+ Ok(())
142
+ }
data/src/lib.rs ADDED
@@ -0,0 +1,98 @@
1
+ #![allow(clippy::wrong_self_convention)]
2
+
3
+ #[macro_use]
4
+ mod macros;
5
+ mod arch;
6
+ mod client;
7
+ mod cookie;
8
+ mod emulate;
9
+ mod error;
10
+ mod extractor;
11
+ mod gvl;
12
+ mod header;
13
+ mod http;
14
+ mod rt;
15
+
16
+ use magnus::{Error, Module, Ruby, Value};
17
+
18
+ use crate::client::{Client, resp::Response};
19
+
20
+ const RUBY_MODULE_NAME: &str = "Wreq";
21
+ const VERSION: &str = env!("CARGO_PKG_VERSION");
22
+
23
+ /// Send a HTTP request.
24
+ #[inline]
25
+ pub fn request(args: &[Value]) -> Result<Response, magnus::Error> {
26
+ Client::request(&Client::default(), args)
27
+ }
28
+
29
+ /// Send a GET request.
30
+ #[inline]
31
+ pub fn get(args: &[Value]) -> Result<Response, magnus::Error> {
32
+ Client::get(&Client::default(), args)
33
+ }
34
+
35
+ /// Send a POST request.
36
+ #[inline]
37
+ pub fn post(args: &[Value]) -> Result<Response, magnus::Error> {
38
+ Client::post(&Client::default(), args)
39
+ }
40
+
41
+ /// Send a PUT request.
42
+ #[inline]
43
+ pub fn put(args: &[Value]) -> Result<Response, magnus::Error> {
44
+ Client::put(&Client::default(), args)
45
+ }
46
+
47
+ /// Send a DELETE request.
48
+ #[inline]
49
+ pub fn delete(args: &[Value]) -> Result<Response, magnus::Error> {
50
+ Client::delete(&Client::default(), args)
51
+ }
52
+
53
+ /// Send a HEAD request.
54
+ #[inline]
55
+ pub fn head(args: &[Value]) -> Result<Response, magnus::Error> {
56
+ Client::head(&Client::default(), args)
57
+ }
58
+
59
+ /// Send an OPTIONS request.
60
+ #[inline]
61
+ pub fn options(args: &[Value]) -> Result<Response, magnus::Error> {
62
+ Client::options(&Client::default(), args)
63
+ }
64
+
65
+ /// Send a TRACE request.
66
+ #[inline]
67
+ pub fn trace(args: &[Value]) -> Result<Response, magnus::Error> {
68
+ Client::trace(&Client::default(), args)
69
+ }
70
+
71
+ /// Send a PATCH request.
72
+ #[inline]
73
+ pub fn patch(args: &[Value]) -> Result<Response, magnus::Error> {
74
+ Client::patch(&Client::default(), args)
75
+ }
76
+
77
+ /// wreq ruby binding
78
+ #[magnus::init]
79
+ fn init(ruby: &Ruby) -> Result<(), Error> {
80
+ let gem_module = ruby.define_module(RUBY_MODULE_NAME)?;
81
+ gem_module.const_set("VERSION", VERSION)?;
82
+ gem_module.define_module_function("request", magnus::function!(request, -1))?;
83
+ gem_module.define_module_function("get", magnus::function!(get, -1))?;
84
+ gem_module.define_module_function("post", magnus::function!(post, -1))?;
85
+ gem_module.define_module_function("put", magnus::function!(put, -1))?;
86
+ gem_module.define_module_function("delete", magnus::function!(delete, -1))?;
87
+ gem_module.define_module_function("head", magnus::function!(head, -1))?;
88
+ gem_module.define_module_function("options", magnus::function!(options, -1))?;
89
+ gem_module.define_module_function("trace", magnus::function!(trace, -1))?;
90
+ gem_module.define_module_function("patch", magnus::function!(patch, -1))?;
91
+ http::include(ruby, &gem_module)?;
92
+ header::include(ruby, &gem_module)?;
93
+ cookie::include(ruby, &gem_module)?;
94
+ client::include(ruby, &gem_module)?;
95
+ emulate::include(ruby, &gem_module)?;
96
+ error::include(ruby);
97
+ Ok(())
98
+ }
data/src/macros.rs ADDED
@@ -0,0 +1,123 @@
1
+ macro_rules! apply_option {
2
+ (set_if_some, $builder:expr, $option:expr, $method:ident) => {
3
+ if let Some(value) = $option.take() {
4
+ $builder = $builder.$method(value);
5
+ }
6
+ };
7
+ (set_if_some_ref, $builder:expr, $option:expr, $method:ident) => {
8
+ if let Some(value) = $option.take() {
9
+ $builder = $builder.$method(&value);
10
+ }
11
+ };
12
+ (set_if_some_inner, $builder:expr, $option:expr, $method:ident) => {
13
+ if let Some(value) = $option.take() {
14
+ $builder = $builder.$method(value.0);
15
+ }
16
+ };
17
+ (set_if_some_into_inner, $builder:expr, $option:expr, $method:ident) => {
18
+ if let Some(value) = $option.take() {
19
+ $builder = $builder.$method(value.0.into_inner());
20
+ }
21
+ };
22
+ (set_if_some_map, $builder:expr, $option:expr, $method:ident, $transform:expr) => {
23
+ if let Some(value) = $option.take() {
24
+ $builder = $builder.$method($transform(value));
25
+ }
26
+ };
27
+ (set_if_some_map_ref, $builder:expr, $option:expr, $method:ident, $transform:expr) => {
28
+ if let Some(value) = $option.take() {
29
+ $builder = $builder.$method($transform(&value));
30
+ }
31
+ };
32
+ (set_if_true, $builder:expr, $option:expr, $method:ident, $default:expr) => {
33
+ if $option.unwrap_or($default) {
34
+ $builder = $builder.$method();
35
+ }
36
+ };
37
+ (set_if_true_with, $builder:expr, $option:expr, $method:ident, $default:expr, $value:expr) => {
38
+ if $option.unwrap_or($default) {
39
+ $builder = $builder.$method($value);
40
+ }
41
+ };
42
+ }
43
+
44
+ macro_rules! define_ruby_enum {
45
+ ($(#[$meta:meta])* $enum_type:ident, $ruby_class:expr, $ffi_type:ty, $($variant:ident),* $(,)?) => {
46
+ define_ruby_enum!($(#[$meta])* $enum_type, $ruby_class, $ffi_type, $( ($variant, $variant) ),*);
47
+ };
48
+
49
+ ($(#[$meta:meta])* const, $enum_type:ident, $ruby_class:expr, $ffi_type:ty, $($variant:ident),* $(,)?) => {
50
+ define_ruby_enum!($(#[$meta])* const, $enum_type, $ruby_class, $ffi_type, $( ($variant, $variant) ),*);
51
+ };
52
+
53
+ ($(#[$meta:meta])* $enum_type:ident, $ruby_class:expr, $ffi_type:ty, $(($rust_variant:ident, $ffi_variant:ident)),* $(,)?) => {
54
+ $(#[$meta])*
55
+ #[magnus::wrap(class = $ruby_class, free_immediately, size)]
56
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57
+ #[allow(non_camel_case_types)]
58
+ #[allow(clippy::upper_case_acronyms)]
59
+ pub enum $enum_type {
60
+ $($rust_variant),*
61
+ }
62
+
63
+ impl $enum_type {
64
+ pub fn into_ffi(self) -> $ffi_type {
65
+ match self {
66
+ $(<$enum_type>::$rust_variant => <$ffi_type>::$ffi_variant,)*
67
+ }
68
+ }
69
+
70
+ #[allow(dead_code)]
71
+ pub fn from_ffi(ffi: $ffi_type) -> Self {
72
+ #[allow(unreachable_patterns)]
73
+ match ffi {
74
+ $(<$ffi_type>::$ffi_variant => <$enum_type>::$rust_variant,)*
75
+ _ => unreachable!(),
76
+ }
77
+ }
78
+ }
79
+ };
80
+
81
+ ($(#[$meta:meta])* const, $enum_type:ident, $ruby_class:expr, $ffi_type:ty, $(($rust_variant:ident, $ffi_variant:ident)),* $(,)?) => {
82
+ $(#[$meta])*
83
+ #[magnus::wrap(class = $ruby_class, free_immediately, size)]
84
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
85
+ #[allow(non_camel_case_types)]
86
+ #[allow(clippy::upper_case_acronyms)]
87
+ pub enum $enum_type {
88
+ $($rust_variant),*
89
+ }
90
+
91
+ impl $enum_type {
92
+ pub const fn into_ffi(self) -> $ffi_type {
93
+ match self {
94
+ $(<$enum_type>::$rust_variant => <$ffi_type>::$ffi_variant,)*
95
+ }
96
+ }
97
+
98
+ #[allow(dead_code)]
99
+ pub const fn from_ffi(ffi: $ffi_type) -> Self {
100
+ #[allow(unreachable_patterns)]
101
+ match ffi {
102
+ $(<$ffi_type>::$ffi_variant => <$enum_type>::$rust_variant,)*
103
+ _ => unreachable!(),
104
+ }
105
+ }
106
+ }
107
+ };
108
+ }
109
+
110
+ macro_rules! ruby {
111
+ () => {
112
+ magnus::Ruby::get().expect("Failed to get Ruby VM instance")
113
+ };
114
+ }
115
+
116
+ macro_rules! extract_request {
117
+ ($args:expr, $required:ty) => {{
118
+ let args = magnus::scan_args::scan_args::<$required, (), (), (), magnus::RHash, ()>($args)?;
119
+ let required = args.required;
120
+ let request = crate::client::req::Request::new(&ruby!(), args.keywords)?;
121
+ (required, request)
122
+ }};
123
+ }
data/src/rt.rs ADDED
@@ -0,0 +1,46 @@
1
+ use std::sync::LazyLock;
2
+
3
+ use tokio::runtime::{Builder, Runtime};
4
+
5
+ use crate::{error::interrupt_error, gvl};
6
+
7
+ static RUNTIME: LazyLock<Runtime> = LazyLock::new(|| {
8
+ let mut builder = Builder::new_multi_thread();
9
+
10
+ builder
11
+ .enable_all()
12
+ .build()
13
+ .expect("Failed to initialize Tokio runtime")
14
+ });
15
+
16
+ enum BlockOnError<E> {
17
+ Interrupted,
18
+ Future(E),
19
+ }
20
+
21
+ /// Block on a future to completion on the global Tokio runtime.
22
+ ///
23
+ /// The future runs without Ruby's GVL, so it must not construct Ruby objects or
24
+ /// Ruby exceptions. Convert Rust errors back into Ruby errors after the GVL has
25
+ /// been reacquired.
26
+ pub fn try_block_on<F, T, E, M>(future: F, map_err: M) -> Result<T, magnus::Error>
27
+ where
28
+ F: Future<Output = Result<T, E>>,
29
+ M: FnOnce(E) -> magnus::Error,
30
+ {
31
+ let result = gvl::nogvl_cancellable(|flag| {
32
+ RUNTIME.block_on(async move {
33
+ tokio::select! {
34
+ biased;
35
+ _ = flag.cancelled() => Err(BlockOnError::Interrupted),
36
+ result = future => result.map_err(BlockOnError::Future),
37
+ }
38
+ })
39
+ });
40
+
41
+ match result {
42
+ Ok(value) => Ok(value),
43
+ Err(BlockOnError::Interrupted) => Err(interrupt_error()),
44
+ Err(BlockOnError::Future(err)) => Err(map_err(err)),
45
+ }
46
+ }
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class ClientCookieProviderTest < Minitest::Test
6
+ HOST = HTTPBIN_URL
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
+ cookies = httpbin_cookies(res2.json)
33
+ assert_kind_of Hash, cookies
34
+ assert_equal "bar", httpbin_fetch(cookies, "foo")
35
+ end
36
+
37
+ def test_prepopulated_jar_is_used_by_client
38
+ # pre-populate jar
39
+ @jar.add("pref=1; Path=/", "#{HOST}/")
40
+
41
+ res = @client.get("#{HOST}/cookies")
42
+ assert_equal 200, res.code
43
+ cookies = httpbin_cookies(res.json)
44
+ assert_equal "1", httpbin_fetch(cookies, "pref")
45
+ end
46
+ end
@@ -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("#{HTTPBIN_URL}/get")
24
+ refute_nil response
25
+ assert_equal 200, response.code
26
+ end
27
+
28
+ def test_post_request
29
+ response = @client.post("#{HTTPBIN_URL}/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("#{HTTPBIN_URL}/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("#{HTTPBIN_URL}/delete")
44
+ refute_nil response
45
+ assert_equal 200, response.code
46
+ end
47
+
48
+ def test_patch_request
49
+ response = @client.patch("#{HTTPBIN_URL}/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("#{HTTPBIN_URL}/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("#{HTTPBIN_URL}/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("#{HTTPBIN_URL}/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("#{HTTPBIN_URL}/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("#{HTTPBIN_URL}/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("#{HTTPBIN_URL}/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("#{HTTPBIN_URL}/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("#{HTTPBIN_URL}/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("#{HTTPBIN_URL}/delay/10", timeout: 1)
119
+ end
120
+
121
+ # Test that reasonable timeouts work normally
122
+ start_time = Time.now
123
+ response = @client.get("#{HTTPBIN_URL}/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("#{HTTPBIN_URL}/status/404")
133
+ refute_nil response
134
+ assert_equal 404, response.code
135
+ end
136
+ end