#![cfg(feature = "emulation")]
use std::thread;
use async_net::TcpListener;
use futures_lite::future::block_on;
use futures_lite::io::{AsyncReadExt, AsyncWriteExt};
use ugi::{Client, Emulation, EmulationProfile};
fn run<T>(value: T) -> T::Output
where
T: std::future::IntoFuture,
{
block_on(async move { value.await })
}
#[test]
fn preset_emulation_applies_default_headers_end_to_end() {
let base = block_on(spawn_http_assert_server(|request| {
let request = request.to_ascii_lowercase();
assert!(request.contains("get /profile http/1.1\r\n"));
assert!(request.contains("\r\nuser-agent: mozilla/5.0 (macintosh; intel mac os x 10_15_7) applewebkit/605.1.15 (khtml, like gecko) version/18.4 safari/605.1.15\r\n"));
assert!(request.contains("\r\naccept-language: en-us,en;q=0.9\r\n"));
}))
.unwrap();
let client = Client::builder()
.emulation(Emulation::Safari18_4)
.http1_only()
.build()
.unwrap();
let response = run(client.get(format!("{base}/profile"))).unwrap();
assert_eq!(block_on(response.text()).unwrap(), "ok");
}
#[test]
fn request_level_emulation_replaces_client_profile_without_leaking_defaults() {
let base = block_on(spawn_http_assert_server(|request| {
let request = request.to_ascii_lowercase();
assert!(request.contains("get /override http/1.1\r\n"));
assert!(request.contains("\r\nuser-agent: customagent/1.0\r\n"));
assert!(request.contains("\r\nx-emulation: request\r\n"));
assert!(!request.contains("chrome/136.0.0.0"));
assert!(!request.contains("\r\naccept-language:"));
}))
.unwrap();
let profile = EmulationProfile::builder()
.default_header("user-agent", "CustomAgent/1.0")
.unwrap()
.default_header("x-emulation", "request")
.unwrap()
.build();
let client = Client::builder()
.base_url(&base)
.unwrap()
.emulation(Emulation::Chrome136)
.http1_only()
.build()
.unwrap();
let response = run(client.get("/override").emulation(profile)).unwrap();
assert_eq!(block_on(response.text()).unwrap(), "ok");
}
async fn spawn_http_assert_server<F>(assert_request: F) -> ugi::Result<String>
where
F: FnOnce(String) + Send + 'static,
{
let listener = TcpListener::bind(("127.0.0.1", 0)).await.map_err(|err| {
ugi::Error::with_source(ugi::ErrorKind::Transport, "failed to bind test server", err)
})?;
let addr = listener.local_addr().map_err(|err| {
ugi::Error::with_source(
ugi::ErrorKind::Transport,
"failed to inspect test server",
err,
)
})?;
thread::spawn(move || {
block_on(async move {
let (mut stream, _) = listener.accept().await.unwrap();
let mut request = Vec::new();
loop {
let mut chunk = [0_u8; 1024];
let read = stream.read(&mut chunk).await.unwrap();
if read == 0 {
break;
}
request.extend_from_slice(&chunk[..read]);
if request.windows(4).any(|window| window == b"\r\n\r\n") {
break;
}
}
assert_request(String::from_utf8_lossy(&request).to_string());
stream
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok")
.await
.unwrap();
stream.flush().await.unwrap();
});
});
Ok(format!("http://{}", addr))
}