#![cfg(feature = "http-client")]
use anyhow::{Context, Result};
pub fn get(url: &str) -> Result<String> {
let response = reqwest::blocking::get(url)
.with_context(|| format!("Failed to send GET request to {url}"))?;
let status = response.status();
if !status.is_success() {
anyhow::bail!("GET request failed with status {status}: {url}");
}
response
.text()
.with_context(|| format!("Failed to read response body from {url}"))
}
pub fn post(url: &str, body: &str) -> Result<String> {
let client = reqwest::blocking::Client::new();
let response = client
.post(url)
.header("content-type", "application/json")
.body(body.to_string())
.send()
.with_context(|| format!("Failed to send POST request to {url}"))?;
let status = response.status();
if !status.is_success() {
anyhow::bail!("POST request failed with status {status}: {url}");
}
response
.text()
.with_context(|| format!("Failed to read response body from {url}"))
}
pub fn put(url: &str, body: &str) -> Result<String> {
let client = reqwest::blocking::Client::new();
let response = client
.put(url)
.header("content-type", "application/json")
.body(body.to_string())
.send()
.with_context(|| format!("Failed to send PUT request to {url}"))?;
let status = response.status();
if !status.is_success() {
anyhow::bail!("PUT request failed with status {status}: {url}");
}
response
.text()
.with_context(|| format!("Failed to read response body from {url}"))
}
pub fn delete(url: &str) -> Result<String> {
let client = reqwest::blocking::Client::new();
let response = client
.delete(url)
.send()
.with_context(|| format!("Failed to send DELETE request to {url}"))?;
let status = response.status();
if !status.is_success() {
anyhow::bail!("DELETE request failed with status {status}: {url}");
}
response
.text()
.with_context(|| format!("Failed to read response body from {url}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_invalid_url_error() {
let result = get("not-a-url");
assert!(result.is_err(), "Invalid URL should return error");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Failed to send GET request"),
"Error should contain context"
);
}
#[test]
fn test_connection_refused_error() {
let result = get("http://localhost:1");
assert!(result.is_err(), "Connection refused should return error");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("localhost:1"),
"Error should contain failed URL"
);
}
#[test]
fn test_post_invalid_url() {
let result = post("not-a-url", "{}");
assert!(result.is_err(), "POST with invalid URL should fail");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Failed to send POST request"),
"Error should contain POST context"
);
}
#[test]
fn test_put_invalid_url() {
let result = put("not-a-url", "{}");
assert!(result.is_err(), "PUT with invalid URL should fail");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Failed to send PUT request"),
"Error should contain PUT context"
);
}
#[test]
fn test_delete_invalid_url() {
let result = delete("not-a-url");
assert!(result.is_err(), "DELETE with invalid URL should fail");
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Failed to send DELETE request"),
"Error should contain DELETE context"
);
}
#[test]
fn test_post_connection_refused() {
let result = post("http://localhost:1", "{}");
assert!(result.is_err(), "POST to closed port should fail");
}
#[test]
fn test_put_connection_refused() {
let result = put("http://localhost:1", "{}");
assert!(result.is_err(), "PUT to closed port should fail");
}
#[test]
fn test_delete_connection_refused() {
let result = delete("http://localhost:1");
assert!(result.is_err(), "DELETE to closed port should fail");
}
#[test]
fn test_get_success_with_httpbin() {
let result = get("https://httpbin.org/get");
if let Ok(response) = result {
assert!(!response.is_empty(), "Response should not be empty");
assert!(
response.contains("httpbin"),
"Response should be from httpbin"
);
}
}
#[test]
fn test_get_404_error() {
let result = get("https://httpbin.org/status/404");
assert!(result.is_err(), "404 status should return error");
if let Err(e) = result {
let err_msg = e.to_string();
assert!(
err_msg.contains("404") || err_msg.contains("failed"),
"Error should indicate HTTP failure"
);
}
}
#[test]
fn test_post_success_with_httpbin() {
let body = r#"{"test": "data"}"#;
let result = post("https://httpbin.org/post", body);
if let Ok(response) = result {
assert!(!response.is_empty(), "Response should not be empty");
assert!(response.contains("test"), "Response should echo data");
}
}
#[test]
fn test_put_success_with_httpbin() {
let body = r#"{"test": "update"}"#;
let result = put("https://httpbin.org/put", body);
if let Ok(response) = result {
assert!(!response.is_empty(), "Response should not be empty");
assert!(response.contains("test"), "Response should echo data");
}
}
#[test]
fn test_delete_success_with_httpbin() {
let result = delete("https://httpbin.org/delete");
if let Ok(response) = result {
assert!(!response.is_empty(), "Response should not be empty");
}
}
#[test]
fn test_empty_url_fails() {
assert!(get("").is_err(), "Empty URL should fail");
assert!(post("", "{}").is_err(), "Empty URL should fail");
assert!(put("", "{}").is_err(), "Empty URL should fail");
assert!(delete("").is_err(), "Empty URL should fail");
}
#[test]
fn test_url_without_scheme_fails() {
assert!(
get("example.com").is_err(),
"URL without scheme should fail"
);
assert!(
post("example.com", "{}").is_err(),
"URL without scheme should fail"
);
}
#[test]
fn test_empty_body_post() {
let result = post("http://localhost:1", "");
assert!(
result.is_err(),
"POST with empty body should fail on connection"
);
}
#[test]
fn test_large_body_post() {
let large_body = "x".repeat(10_000);
let result = post("http://localhost:1", &large_body);
assert!(
result.is_err(),
"POST with large body should fail on connection"
);
}
#[test]
fn test_special_characters_in_url() {
let result = get("http://localhost:1/test?query=value&foo=bar");
assert!(
result.is_err(),
"URL with query params should fail on connection"
);
}
}
#[cfg(test)]
mod property_tests {
use super::*;
#[test]
fn prop_get_never_panics_on_invalid_urls() {
let invalid_urls = vec![
"",
"not-a-url",
"://missing-scheme",
"http://",
"ftp://wrong-scheme.com",
"http:// spaces .com",
];
for url in invalid_urls {
let result = get(url);
assert!(result.is_err(), "get('{url}') should return Err, not panic");
}
}
#[test]
fn prop_post_never_panics_on_invalid_input() {
let test_cases = vec![
("", ""),
("not-a-url", "{}"),
("http://localhost:1", "invalid json {"),
];
for (url, body) in test_cases {
let result = post(url, body);
assert!(result.is_err(), "post('{url}', '{body}') should return Err");
}
}
#[test]
fn prop_all_methods_fail_on_unreachable_host() {
let unreachable = "http://localhost:1";
assert!(get(unreachable).is_err(), "GET should fail");
assert!(post(unreachable, "{}").is_err(), "POST should fail");
assert!(put(unreachable, "{}").is_err(), "PUT should fail");
assert!(delete(unreachable).is_err(), "DELETE should fail");
}
}