scratch-server 1.1.0

Simple HTTP Server
Documentation
use std::{
    net::TcpListener,
    path::PathBuf,
    process::{Child, Command, Stdio},
    thread,
    time::Duration,
};

use reqwest::blocking::{Client, ClientBuilder};

pub struct TestServer {
    pub child: Child,
    pub port: u16,
}

impl TestServer {
    pub fn base_url(&self) -> String {
        format!("http://127.0.0.1:{}", self.port)
    }

    pub fn https_base_url(&self) -> String {
        format!("https://127.0.0.1:{}", self.port)
    }
}

impl Drop for TestServer {
    fn drop(&mut self) {
        let _ = self.child.kill();
        let _ = self.child.wait();
    }
}

pub fn release_binary_path() -> PathBuf {
    let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    path.push("target");
    path.push("release");
    if cfg!(windows) {
        path.push("scratch-server.exe");
    } else {
        path.push("scratch-server");
    }
    assert!(
        path.exists(),
        "Release binary not found at {}. Run `cargo build --release --bin scratch-server` first.",
        path.display()
    );
    path
}

pub fn find_free_port() -> u16 {
    let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to ephemeral port");
    let port = listener
        .local_addr()
        .expect("Failed to resolve local addr")
        .port();
    drop(listener);
    port
}

pub fn spawn_server(extra_args: &[&str], capture_output: bool) -> TestServer {
    let port = find_free_port();
    let mut cmd = Command::new(release_binary_path());

    cmd.arg("--port").arg(port.to_string());
    cmd.args(extra_args);
    cmd.current_dir(env!("CARGO_MANIFEST_DIR"));

    if capture_output {
        cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
    } else {
        cmd.stdout(Stdio::null()).stderr(Stdio::null());
    }

    let mut child = cmd.spawn().expect("Failed to spawn release server process");

    thread::sleep(Duration::from_secs(2));

    if let Some(status) = child
        .try_wait()
        .expect("Failed to check server process status")
    {
        panic!("Server exited early with status: {status}");
    }

    TestServer { child, port }
}

pub fn http_client() -> Client {
    Client::builder()
        .timeout(Duration::from_secs(5))
        .http1_title_case_headers()
        .build()
        .expect("Failed to build HTTP client")
}

pub fn https_client() -> Client {
    ClientBuilder::new()
        .http1_title_case_headers()
        .danger_accept_invalid_certs(true)
        .timeout(Duration::from_secs(5))
        .build()
        .expect("Failed to build HTTPS client")
}

pub fn testdata_path(parts: &[&str]) -> PathBuf {
    let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    path.push("tests/data");
    for part in parts {
        path.push(part);
    }
    path
}