3b7a4928c9
## TLS (#11) - Self-signed cert generated at startup via rcgen (covers localhost, 127.0.0.1, fut.ea.com, utas.mob.v4.fut.ea.com); activated with TLS_ENABLED=true - Custom accept loop: tokio-rustls acceptor → hyper-util auto Builder → axum Router (no axum-server dependency — uses hyper 1.x natively) ## Replay CLI (#12) - New binary: openfut-bridge-replay <file.json|dir> [bridge-url] - Replays single capture or entire directory against Bridge - Accepts self-signed certs automatically ## Capture quality (#13, #14) - DELETE /_bridge/captures — wipe all capture files from disk - Deduplication: same method+path within 1 s is skipped (configurable constant) ## Admin UI (#21, #22, #23) - GET /_bridge/admin — embedded HTML dashboard; auto-refresh every 10 s Shows: live stats, SSE log of incoming traffic, endpoint status table, recent captures list with delete button - GET /_bridge/status — per-endpoint mapped/known/unknown status - GET /_bridge/captures/stream — SSE stream; event: capture on each request Uses tokio::sync::broadcast channel (capacity 256) in ProxyState ## Tests (#24, #25) - 9 new tests: placeholder format, full HTTP integration (health, placeholder, captures list, delete captures, status), TLS cert generation + acceptor build - Total bridge tests: 13/13 passing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
114 lines
3.3 KiB
Rust
114 lines
3.3 KiB
Rust
//! openfut-bridge-replay — replay a captured request file against the bridge.
|
|
//!
|
|
//! Usage:
|
|
//! openfut-bridge-replay \<capture.json\> [bridge-url]
|
|
//! openfut-bridge-replay captures/ (replay all captures in a dir)
|
|
//!
|
|
//! The bridge URL defaults to `http://127.0.0.1:8443`.
|
|
//! Self-signed TLS certificates are accepted automatically.
|
|
|
|
use openfut_bridge::capture::CapturedRequest;
|
|
use std::process::ExitCode;
|
|
|
|
#[tokio::main]
|
|
async fn main() -> ExitCode {
|
|
let args: Vec<String> = std::env::args().collect();
|
|
if args.len() < 2 {
|
|
eprintln!("Usage: openfut-bridge-replay <capture.json|dir> [bridge-url]");
|
|
eprintln!(" bridge-url defaults to http://127.0.0.1:8443");
|
|
return ExitCode::FAILURE;
|
|
}
|
|
|
|
let path_arg = &args[1];
|
|
let bridge_url = args
|
|
.get(2)
|
|
.map(|s| s.as_str())
|
|
.unwrap_or("http://127.0.0.1:8443");
|
|
|
|
let client = match reqwest::Client::builder()
|
|
.danger_accept_invalid_certs(true)
|
|
.timeout(std::time::Duration::from_secs(10))
|
|
.build()
|
|
{
|
|
Ok(c) => c,
|
|
Err(e) => {
|
|
eprintln!("Failed to build HTTP client: {e}");
|
|
return ExitCode::FAILURE;
|
|
}
|
|
};
|
|
|
|
let captures = match collect_captures(path_arg) {
|
|
Ok(c) => c,
|
|
Err(e) => {
|
|
eprintln!("Error loading captures: {e}");
|
|
return ExitCode::FAILURE;
|
|
}
|
|
};
|
|
|
|
if captures.is_empty() {
|
|
eprintln!("No capture files found at {path_arg}");
|
|
return ExitCode::FAILURE;
|
|
}
|
|
|
|
println!(
|
|
"Replaying {} capture(s) against {bridge_url}",
|
|
captures.len()
|
|
);
|
|
|
|
let mut failures = 0u32;
|
|
for capture in &captures {
|
|
let result = replay_one(&client, bridge_url, capture).await;
|
|
match result {
|
|
Ok(status) => println!(" [{}] {} {} → {status}", capture.id, capture.method, capture.path),
|
|
Err(e) => {
|
|
eprintln!(" [{}] {} {} → ERROR: {e}", capture.id, capture.method, capture.path);
|
|
failures += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if failures > 0 {
|
|
eprintln!("{failures} replay(s) failed.");
|
|
ExitCode::FAILURE
|
|
} else {
|
|
println!("All replays succeeded.");
|
|
ExitCode::SUCCESS
|
|
}
|
|
}
|
|
|
|
fn collect_captures(path_arg: &str) -> anyhow::Result<Vec<CapturedRequest>> {
|
|
let path = std::path::Path::new(path_arg);
|
|
if path.is_dir() {
|
|
openfut_bridge::capture::load_all_captures(path_arg)
|
|
} else {
|
|
let content = std::fs::read_to_string(path)?;
|
|
let capture: CapturedRequest = serde_json::from_str(&content)?;
|
|
Ok(vec![capture])
|
|
}
|
|
}
|
|
|
|
async fn replay_one(
|
|
client: &reqwest::Client,
|
|
bridge_url: &str,
|
|
capture: &CapturedRequest,
|
|
) -> anyhow::Result<u16> {
|
|
let url = format!("{}{}", bridge_url, capture.path);
|
|
let builder = match capture.method.to_uppercase().as_str() {
|
|
"POST" => client.post(&url),
|
|
"PUT" => client.put(&url),
|
|
"DELETE" => client.delete(&url),
|
|
_ => client.get(&url),
|
|
};
|
|
|
|
let builder = if let Some(body) = &capture.body {
|
|
builder
|
|
.header("content-type", "application/json")
|
|
.body(body.clone())
|
|
} else {
|
|
builder
|
|
};
|
|
|
|
let resp = builder.send().await?;
|
|
Ok(resp.status().as_u16())
|
|
}
|