use async_trait::async_trait;
use http_client::{Error, HttpClient, Request, Response};
use http_client_vcr::{CassetteFormat, DefaultMatcher, NoOpClient, VcrClient, VcrMode};
use http_types::{Method, Url};
use std::env;
use std::path::PathBuf;
#[derive(Debug, Clone)]
struct ReqwestAdapter {
client: reqwest::Client,
}
impl ReqwestAdapter {
fn new() -> Self {
Self {
client: reqwest::Client::new(),
}
}
}
#[async_trait]
impl HttpClient for ReqwestAdapter {
async fn send(&self, mut req: Request) -> Result<Response, Error> {
let method = match req.method() {
Method::Get => reqwest::Method::GET,
Method::Post => reqwest::Method::POST,
Method::Put => reqwest::Method::PUT,
Method::Delete => reqwest::Method::DELETE,
Method::Head => reqwest::Method::HEAD,
Method::Options => reqwest::Method::OPTIONS,
Method::Patch => reqwest::Method::PATCH,
_ => reqwest::Method::GET,
};
let mut reqwest_req = self.client.request(method, req.url().as_str());
for (name, values) in req.iter() {
for value in values.iter() {
reqwest_req = reqwest_req.header(name.as_str(), value.as_str());
}
}
let body = req
.body_string()
.await
.map_err(|e| Error::from_str(500, e))?;
if !body.is_empty() {
reqwest_req = reqwest_req.body(body);
}
let reqwest_resp = reqwest_req
.send()
.await
.map_err(|e| Error::from_str(500, e))?;
let mut response = Response::new(reqwest_resp.status().as_u16());
for (name, value) in reqwest_resp.headers() {
let _ = response.insert_header(name.as_str(), value.to_str().unwrap_or(""));
}
let body_bytes = reqwest_resp
.bytes()
.await
.map_err(|e| Error::from_str(500, e))?;
response.set_body(body_bytes.to_vec());
Ok(response)
}
}
struct VcrTestSetup {
cassette_path: PathBuf,
mode: VcrMode,
}
impl VcrTestSetup {
fn new(test_name: &str) -> Result<Self, Box<dyn std::error::Error>> {
let cassette_path = PathBuf::from(format!("tests/fixtures/{test_name}"));
let vcr_record = env::var("VCR_RECORD").unwrap_or_default();
let cassette_exists = cassette_path.exists();
let mode = match vcr_record.as_str() {
"1" | "true" | "on" => VcrMode::Record,
_ => {
if !cassette_exists {
return Err(format!(
"No cassette found at '{}' and VCR_RECORD is not set. Either set VCR_RECORD=1 to record new interactions or ensure the cassette directory exists.",
cassette_path.display()
).into());
}
VcrMode::Replay
}
};
Ok(Self {
cassette_path,
mode,
})
}
async fn create_vcr_client(&self) -> Result<VcrClient, Box<dyn std::error::Error>> {
if let Some(parent_dir) = self.cassette_path.parent() {
std::fs::create_dir_all(parent_dir)?;
}
let inner_client: Box<dyn HttpClient + Send + Sync> = match self.mode {
VcrMode::Record => Box::new(ReqwestAdapter::new()),
_ => Box::new(NoOpClient::new()),
};
let matcher = DefaultMatcher::new().with_headers(vec![]);
let vcr_client = VcrClient::builder(&self.cassette_path)
.inner_client(inner_client)
.mode(self.mode.clone())
.format(CassetteFormat::Directory) .matcher(Box::new(matcher))
.build()
.await?;
Ok(vcr_client)
}
}
#[tokio::test]
async fn test_multiple_requests_directory_format() -> Result<(), Box<dyn std::error::Error>> {
let setup = VcrTestSetup::new("multiple_requests_test")?;
let vcr_client = setup.create_vcr_client().await?;
let urls = [
"https://httpbin.org/get?test=1",
"https://httpbin.org/get?test=2",
"https://httpbin.org/post",
"https://httpbin.org/put",
];
let methods = [Method::Get, Method::Get, Method::Post, Method::Put];
for (url, method) in urls.iter().zip(methods.iter()) {
let mut request = http_types::Request::new(*method, Url::parse(url)?);
if *method != Method::Get {
request.set_body(format!("test body for {method} request"));
}
let response = vcr_client.send(request).await?;
println!("Request to {} returned status: {}", url, response.status());
assert!(response.status().is_success() || response.status().is_informational());
}
Ok(())
}
#[tokio::test]
async fn test_repeated_requests_directory_format() -> Result<(), Box<dyn std::error::Error>> {
let setup = VcrTestSetup::new("repeated_requests_test")?;
let vcr_client = setup.create_vcr_client().await?;
let url = "https://httpbin.org/json";
for i in 1..=3 {
let request = http_types::Request::new(Method::Get, Url::parse(url)?);
let response = vcr_client.send(request).await?;
println!(
"Request {} to {} returned status: {}",
i,
url,
response.status()
);
assert!(response.status().is_success());
if matches!(setup.mode, VcrMode::Replay) {
assert_eq!(response.status(), 200);
}
}
Ok(())
}
#[tokio::test]
async fn test_json_and_text_responses_directory_format() -> Result<(), Box<dyn std::error::Error>> {
let setup = VcrTestSetup::new("json_text_responses_test")?;
let vcr_client = setup.create_vcr_client().await?;
let test_cases = [
("https://httpbin.org/json", "JSON response"),
("https://httpbin.org/html", "HTML response"),
("https://httpbin.org/xml", "XML response"),
];
for (url, description) in test_cases {
let request = http_types::Request::new(Method::Get, Url::parse(url)?);
let response = vcr_client.send(request).await?;
println!("{}: {}", description, response.status());
assert!(response.status().is_success());
let mut response = response;
let body = response.body_string().await?;
assert!(
!body.is_empty(),
"Response body should not be empty for {url}"
);
}
Ok(())
}
#[tokio::test]
async fn test_sequential_identical_requests_directory_format(
) -> Result<(), Box<dyn std::error::Error>> {
let setup = VcrTestSetup::new("sequential_identical_requests_test")?;
let vcr_client = setup.create_vcr_client().await?;
let url = "https://httpbin.org/uuid";
let mut responses = Vec::new();
for i in 1..=3 {
let request = http_types::Request::new(Method::Get, Url::parse(url)?);
let response = vcr_client.send(request).await?;
println!(
"Request {} to {} returned status: {}",
i,
url,
response.status()
);
assert!(response.status().is_success());
let mut response = response;
let body = response.body_string().await?;
responses.push(body);
println!("Response {}: {}", i, responses[i - 1]);
}
if matches!(setup.mode, VcrMode::Record) {
assert_ne!(
responses[0], responses[1],
"First and second responses should be different"
);
assert_ne!(
responses[1], responses[2],
"Second and third responses should be different"
);
assert_ne!(
responses[0], responses[2],
"First and third responses should be different"
);
} else {
assert_ne!(
responses[0], responses[1],
"Replayed responses should maintain their differences"
);
assert_ne!(
responses[1], responses[2],
"Replayed responses should maintain their differences"
);
assert_ne!(
responses[0], responses[2],
"Replayed responses should maintain their differences"
);
}
Ok(())
}