use std::cell::RefCell;
use std::io::{BufRead, BufReader};
use std::net::TcpListener;
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant};
static TEST_COUNTER: AtomicUsize = AtomicUsize::new(0);
thread_local! {
static TEST_API_KEY: RefCell<Option<String>> = const { RefCell::new(None) };
}
fn current_test_api_key() -> Option<String> {
TEST_API_KEY.with(|k| k.borrow().clone())
}
pub struct TestServer {
pub url: String,
pub data_dir: String,
#[allow(dead_code)]
pub api_key: String,
child: Option<Child>,
}
impl TestServer {
pub fn spawn() -> Self {
let port = pick_port();
let unique = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
let pid = std::process::id();
let data_dir = format!("/tmp/microresolve_test_{}_{}_{}", pid, unique, port);
let _ = std::fs::remove_dir_all(&data_dir);
let bin = if std::path::Path::new("./target/release/microresolve-studio").exists() {
"./target/release/microresolve-studio"
} else {
"./target/debug/microresolve-studio"
};
let keys_file = format!("{}/keys.json", data_dir);
std::fs::create_dir_all(&data_dir).expect("create test data_dir");
let child = Command::new(bin)
.args([
"--port",
&port.to_string(),
"--no-browser",
"--data",
&data_dir,
"--keys-file",
&keys_file,
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect(
"failed to spawn server (build with `cargo build --release --features server`)",
);
let url = format!("http://localhost:{}", port);
wait_for_ready(&url, Duration::from_secs(10));
let admin_key_path = format!("{}/admin-key.txt", data_dir);
let api_key = std::fs::read_to_string(&admin_key_path)
.map(|s| s.trim().to_string())
.unwrap_or_else(|e| {
panic!(
"TestServer: bootstrap admin-key.txt not found at {}: {}",
admin_key_path, e
)
});
TEST_API_KEY.with(|k| *k.borrow_mut() = Some(api_key.clone()));
Self {
url,
data_dir,
api_key,
child: Some(child),
}
}
pub fn client(&self) -> reqwest::blocking::Client {
reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(10))
.build()
.unwrap()
}
}
impl Drop for TestServer {
fn drop(&mut self) {
if let Some(mut child) = self.child.take() {
let _ = child.kill();
let _ = child.wait();
}
let _ = std::fs::remove_dir_all(&self.data_dir);
}
}
fn pick_port() -> u16 {
let listener = TcpListener::bind("127.0.0.1:0").expect("failed to find free port");
let port = listener.local_addr().unwrap().port();
drop(listener);
port
}
fn wait_for_ready(url: &str, timeout: Duration) {
let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_millis(500))
.build()
.unwrap();
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
if let Ok(resp) = client.get(format!("{}/api/health", url)).send() {
if resp.status().is_success() {
return;
}
}
std::thread::sleep(Duration::from_millis(100));
}
panic!(
"server at {} did not become ready within {:?}",
url, timeout
);
}
fn caller_set_api_key(headers: &[(&str, &str)]) -> bool {
headers
.iter()
.any(|(k, _)| k.eq_ignore_ascii_case("X-Api-Key"))
}
pub fn post_json<T: serde::Serialize>(
client: &reqwest::blocking::Client,
url: &str,
headers: &[(&str, &str)],
body: &T,
) -> (u16, String) {
let mut req = client.post(url).json(body);
for (k, v) in headers {
req = req.header(*k, *v);
}
if !caller_set_api_key(headers) {
if let Some(k) = current_test_api_key() {
req = req.header("X-Api-Key", k);
}
}
let resp = req.send().expect("request failed");
(resp.status().as_u16(), resp.text().unwrap_or_default())
}
#[allow(dead_code)]
pub fn get(
client: &reqwest::blocking::Client,
url: &str,
headers: &[(&str, &str)],
) -> (u16, String) {
let mut req = client.get(url);
for (k, v) in headers {
req = req.header(*k, *v);
}
if !caller_set_api_key(headers) {
if let Some(k) = current_test_api_key() {
req = req.header("X-Api-Key", k);
}
}
let resp = req.send().expect("request failed");
(resp.status().as_u16(), resp.text().unwrap_or_default())
}
pub fn delete_json<T: serde::Serialize>(
client: &reqwest::blocking::Client,
url: &str,
headers: &[(&str, &str)],
body: &T,
) -> u16 {
let mut req = client.delete(url).json(body);
for (k, v) in headers {
req = req.header(*k, *v);
}
if !caller_set_api_key(headers) {
if let Some(k) = current_test_api_key() {
req = req.header("X-Api-Key", k);
}
}
let resp = req.send().expect("request failed");
resp.status().as_u16()
}
#[allow(dead_code)]
pub fn patch_json<T: serde::Serialize>(
client: &reqwest::blocking::Client,
url: &str,
headers: &[(&str, &str)],
body: &T,
) -> u16 {
let mut req = client.patch(url).json(body);
for (k, v) in headers {
req = req.header(*k, *v);
}
if !caller_set_api_key(headers) {
if let Some(k) = current_test_api_key() {
req = req.header("X-Api-Key", k);
}
}
let resp = req.send().expect("request failed");
resp.status().as_u16()
}
#[allow(dead_code)]
fn _unused() {
let _ = BufReader::new(std::io::empty()).lines();
}