extern crate std;
use super::*;
use alloc::string::String;
use core::time::Duration;
use tokio::{
io::{AsyncBufReadExt, AsyncWriteExt},
net::TcpListener,
time::timeout,
};
const DEADLINE: Duration = Duration::from_secs(15);
async fn receive(listener: &TcpListener, response: &str) -> String {
timeout(DEADLINE, async {
let (stream, _) = listener.accept().await.unwrap();
let mut stream = tokio::io::BufReader::new(stream);
let mut request = String::new();
while !request.ends_with("\r\n\r\n") {
assert!(stream.read_line(&mut request).await.unwrap() > 0);
assert!(request.len() < 8192);
}
stream
.get_mut()
.write_all(response.as_bytes())
.await
.unwrap();
request
})
.await
.expect("balance receiver timed out")
}
fn header<'a>(request: &'a str, name: &str) -> Option<&'a str> {
request.lines().find_map(|line| {
let (key, value) = line.split_once(':')?;
key.eq_ignore_ascii_case(name).then_some(value.trim())
})
}
#[tokio::test]
async fn balances_use_shared_proxy_policy() {
const CHILD: &str = "ASIMOV_CLOUD_HTTP_TEST";
if let Ok(case) = std::env::var(CHILD) {
let account: Account = "alice".parse().unwrap();
let result = timeout(DEADLINE, async {
match case.as_str() {
"balance-endpoint" => account.balance().await,
"has-credits-endpoint" => account.has_credits().await.map(|has_credits| {
if has_credits {
Credits::ONE
} else {
Credits::ZERO
}
}),
_ => Account::fetch_balance(&std::env::var("ASIMOV_CLOUD_TEST_URL").unwrap()).await,
}
})
.await
.expect("balance request timed out");
match case.as_str() {
"redirect" | "cross-origin-redirect" => assert!(matches!(
result,
Err(AccountBalanceError::UnexpectedResponse(status)) if status == 307
)),
"rejected-proxy" => assert!(matches!(
result,
Err(AccountBalanceError::UnexpectedResponse(status)) if status == 407
)),
"non-ok-success" => assert!(matches!(
result,
Err(AccountBalanceError::UnexpectedResponse(status)) if status == 204
)),
"balance-endpoint" | "has-credits-endpoint" => assert!(matches!(
result,
Err(AccountBalanceError::FailedRequest(error)) if error.is_connect()
)),
"invalid-balance" => assert!(matches!(
result,
Err(AccountBalanceError::InvalidResponseFormat(_))
)),
_ => assert_eq!(result.unwrap(), Credits::from_nanos(12_500_000_000)),
}
return;
}
for (case, bypass, status) in [
("proxy", "", 200),
("host-bypass", "127.0.0.1", 200),
("wildcard-bypass", "*", 200),
("redirect", "", 307),
("cross-origin-redirect", "", 307),
("rejected-proxy", "", 407),
("balance-endpoint", "", 407),
("has-credits-endpoint", "", 407),
("non-ok-success", "", 204),
("invalid-balance", "", 200),
] {
let origin = TcpListener::bind("127.0.0.1:0").await.unwrap();
let proxy = TcpListener::bind("127.0.0.1:0").await.unwrap();
let url = format!("http://{}/alice/balance.txt", origin.local_addr().unwrap());
let proxied = bypass.is_empty();
let public_endpoint = matches!(case, "balance-endpoint" | "has-credits-endpoint");
let mut command = std::process::Command::new(std::env::current_exe().unwrap());
command.args([
"--exact",
"account::tests::balances_use_shared_proxy_policy",
"--nocapture",
]);
for name in [
"HTTP_PROXY",
"http_proxy",
"HTTPS_PROXY",
"https_proxy",
"ALL_PROXY",
"all_proxy",
] {
command.env(name, "http://127.0.0.1:1");
}
command
.env(CHILD, case)
.env("ASIMOV_CLOUD_TEST_URL", &url)
.env_remove("REQUEST_METHOD")
.env("no_proxy", "*")
.env("NO_PROXY", bypass)
.env(
if public_endpoint {
"HTTPS_PROXY"
} else {
"HTTP_PROXY"
},
format!("http://user:pass@{}", proxy.local_addr().unwrap()),
);
let task = tokio::task::spawn_blocking(move || command.output().unwrap());
let location = if case == "cross-origin-redirect" {
"http://redirect.invalid/balance.txt"
} else {
"/redirect.txt"
};
let body = match case {
"invalid-balance" => "not a balance",
"non-ok-success" => "",
_ => "12.500000000",
};
let response = format!(
"HTTP/1.1 {status} Test\r\nlocation: {location}\r\n\
content-length: {}\r\nconnection: close\r\n\r\n{body}",
body.len()
);
let request = receive(if proxied { &proxy } else { &origin }, &response).await;
if public_endpoint {
assert!(request.starts_with("CONNECT asimov.credit:443 HTTP/1.1\r\n"));
} else {
let target = if proxied { &url } else { "/alice/balance.txt" };
assert!(request.starts_with(&format!("GET {target} HTTP/1.1\r\n")));
assert_eq!(header(&request, "user-agent"), Some("ASIMOV.rs"));
}
assert_eq!(
header(&request, "proxy-authorization"),
proxied.then_some("Basic dXNlcjpwYXNz"),
"{case}"
);
let output = timeout(DEADLINE, task).await.unwrap().unwrap();
assert!(
output.status.success(),
"{case}: {}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
for listener in [&origin, &proxy] {
assert!(
timeout(Duration::from_millis(50), listener.accept())
.await
.is_err(),
"unexpected connection: {case}"
);
}
}
}
#[tokio::test]
async fn invalid_native_targets_return_proxy_errors() {
for url in ["ftp://account.invalid/balance.txt", "not a URL"] {
assert!(matches!(
Account::fetch_balance(url).await,
Err(AccountBalanceError::Proxy(
asimov_http::proxy::Error::InvalidOrigin(_)
))
));
}
}