use std::{env, net::SocketAddr, path::Path, time::Duration};
use apimock::{App, EnvArgs};
use tokio::net::TcpStream;
use super::{
constant::{CONFIG_FILE_NAME, CONFIG_TESTS_ROOT_DIR_PATH},
tls::{generate_tls_credentials, tls_credentials_are_ready},
};
const READINESS_TIMEOUT: Duration = Duration::from_secs(5);
const READINESS_POLL_INTERVAL: Duration = Duration::from_millis(10);
#[derive(Clone)]
pub struct TestSetup {
pub root_config_file_path: Option<String>,
pub port: Option<u16>,
pub fallback_respond_dir_path: Option<String>,
pub current_dir_path: Option<String>,
}
impl TestSetup {
pub fn default_with_root_config_dir(root_config_dir_path: &str) -> Self {
Self {
root_config_file_path: Some(
Path::new(env!("CARGO_MANIFEST_DIR"))
.join(CONFIG_TESTS_ROOT_DIR_PATH)
.join(root_config_dir_path)
.join(CONFIG_FILE_NAME)
.to_str()
.expect("failed to generate root config file path")
.to_string(),
),
..Default::default()
}
}
pub async fn launch(&self) -> u16 {
if !tls_credentials_are_ready() {
generate_tls_credentials();
}
let requested_port = self.port.unwrap_or(0);
self.launch_impl(requested_port).await
}
async fn launch_impl(&self, port: u16) -> u16 {
if let Some(current_dir_path) = self.current_dir_path.as_ref() {
let current_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
.join(CONFIG_TESTS_ROOT_DIR_PATH)
.join(current_dir_path.as_str());
match env::set_current_dir(current_dir.clone()) {
Ok(_) => (),
Err(err) => {
panic!(
"failed to set current dir: {} ({})",
current_dir.to_string_lossy(),
err
);
}
};
}
let mut app_env_args = env_args(port);
if let Some(root_config_file_path) = self.root_config_file_path.as_ref() {
app_env_args.config_file_path = Some(root_config_file_path.to_owned());
}
if let Some(fallback_respond_dir_path) = self.fallback_respond_dir_path.as_ref() {
app_env_args.fallback_respond_dir_path = Some(fallback_respond_dir_path.to_owned());
}
let app = App::new(&app_env_args, None, true)
.await
.expect("App::new failed in test setup");
if let Some(listener) = app
.server
.bind_http()
.await
.expect("failed to bind HTTP listener in test setup")
{
let bound_addr = listener
.local_addr()
.expect("bound HTTP listener has no local_addr");
tokio::spawn(async move {
app.server.serve_http(listener).await;
});
wait_until_accepting(bound_addr).await;
return bound_addr.port();
}
if let Some((listener, acceptor)) = app
.server
.bind_https()
.await
.expect("failed to bind HTTPS listener in test setup")
{
let bound_addr = listener
.local_addr()
.expect("bound HTTPS listener has no local_addr");
tokio::spawn(async move {
app.server.serve_https(listener, acceptor).await;
});
wait_until_accepting(bound_addr).await;
return bound_addr.port();
}
panic!(
"test config at {:?} configures neither an HTTP nor an HTTPS listener",
self.root_config_file_path
);
}
}
impl Default for TestSetup {
fn default() -> Self {
Self {
root_config_file_path: Some(
Path::new(env!("CARGO_MANIFEST_DIR"))
.join(CONFIG_TESTS_ROOT_DIR_PATH)
.join(CONFIG_FILE_NAME)
.to_str()
.expect("failed to generate root config file path")
.to_string(),
),
port: None,
fallback_respond_dir_path: None,
current_dir_path: None,
}
}
}
async fn wait_until_accepting(addr: SocketAddr) {
let connect_addr = if addr.ip().is_unspecified() {
let loopback = if addr.is_ipv6() {
std::net::Ipv6Addr::LOCALHOST.into()
} else {
std::net::Ipv4Addr::LOCALHOST.into()
};
SocketAddr::new(loopback, addr.port())
} else {
addr
};
let deadline = tokio::time::Instant::now() + READINESS_TIMEOUT;
loop {
if TcpStream::connect(connect_addr).await.is_ok() {
return;
}
if tokio::time::Instant::now() >= deadline {
panic!(
"server did not start accepting connections on {} within {:?}",
connect_addr, READINESS_TIMEOUT
);
}
tokio::time::sleep(READINESS_POLL_INTERVAL).await;
}
}
fn env_args(port: u16) -> EnvArgs {
let mut ret = EnvArgs::default()
.expect("failed to parse env args")
.expect("no env args returned (unexpected --init short-circuit in tests)");
ret.port = Some(port);
ret.validate().expect("env args validation failed");
ret
}