#![allow(dead_code)]
use std::{
collections::HashMap,
future::Future,
net::TcpStream,
pin::Pin,
sync::{Arc, Mutex, OnceLock},
time::{Duration, Instant},
};
use cnctd_server::{
router::{
error::{ErrorCode, ErrorResponse},
response::{BinaryResponse, SuccessCode, SuccessResponse},
HttpMethod, RestRouterFunction, SocketRouterFunction,
},
Bytes,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
#[derive(Debug, Clone)]
pub struct Call {
pub stage: &'static str,
pub method: String,
pub path: String,
pub data: Value,
pub raw_body: Option<Vec<u8>>,
pub headers: Option<HashMap<String, String>>,
pub auth_token: Option<String>,
pub connection_id: Option<String>,
pub ip_address: Option<String>,
}
#[derive(Clone, Default)]
pub struct RecordingRouter {
pub calls: Arc<Mutex<Vec<Call>>>,
}
impl RecordingRouter {
pub fn record(&self, call: Call) {
self.calls.lock().unwrap().push(call);
}
pub fn calls_for(&self, needle: &str) -> Vec<Call> {
self.calls
.lock()
.unwrap()
.iter()
.filter(|c| c.path.contains(needle))
.cloned()
.collect()
}
}
fn boxed<T: Send + 'static>(v: T) -> Pin<Box<dyn Future<Output = T> + Send>> {
Box::pin(async move { v })
}
impl RestRouterFunction for RecordingRouter {
fn route(
&self,
method: HttpMethod,
path: String,
data: Value,
auth_token: Option<String>,
connection_id: Option<String>,
ip_address: Option<String>,
) -> Pin<Box<dyn Future<Output = Result<SuccessResponse, ErrorResponse>> + Send>> {
let method_s = format!("{:?}", method);
self.record(Call {
stage: "route",
method: method_s.clone(),
path: path.clone(),
data: data.clone(),
raw_body: None,
headers: None,
auth_token: auth_token.clone(),
connection_id: connection_id.clone(),
ip_address: ip_address.clone(),
});
let out = if path.starts_with("/api/echo/") || path.starts_with("/api/order/json") {
Ok(SuccessResponse::new(
None,
None,
Some(json!({
"method": method_s,
"path": path,
"data": data,
"auth_token": auth_token,
"connection_id": connection_id,
"ip_address": ip_address,
})),
))
} else if path == "/api/status/created" {
Ok(SuccessResponse::new(
Some(SuccessCode::Created),
Some("made".into()),
Some(json!({"id": 7})),
))
} else if path == "/api/status/msg" {
Ok(SuccessResponse::new(None, Some("just a message".into()), None))
} else if path == "/api/fail/bad-request" {
Err(ErrorResponse::new(Some(ErrorCode::BadRequest), Some("missing thing".into())))
} else if path == "/api/fail/unauthorized" {
Err(ErrorResponse::new(Some(ErrorCode::Unauthorized), Some("a token is required".into())))
} else if path == "/api/fail/internal" {
Err(ErrorResponse::new(Some(ErrorCode::InternalServerError), Some("internal failure".into())))
} else if path == "/api/fail/default" {
Err(ErrorResponse::new(None, Some("nothing here".into())))
} else {
Err(ErrorResponse::new(Some(ErrorCode::NotFound), Some(format!("unrouted: {}", path))))
};
boxed(out)
}
fn route_redirect(
&self,
path: String,
data: Value,
auth_token: Option<String>,
connection_id: Option<String>,
) -> Pin<Box<dyn Future<Output = String> + Send>> {
self.record(Call {
stage: "redirect",
method: "GET".into(),
path: path.clone(),
data,
raw_body: None,
headers: None,
auth_token,
connection_id,
ip_address: None,
});
let url = if path.starts_with("/redirect/ok/") {
format!("https://example.com/landed?from={}", path.trim_start_matches("/redirect/ok/"))
} else {
"this is not a url".to_string()
};
boxed(url)
}
fn route_binary(
&self,
method: HttpMethod,
path: String,
data: Value,
auth_token: Option<String>,
connection_id: Option<String>,
ip_address: Option<String>,
) -> Pin<Box<dyn Future<Output = Result<Option<BinaryResponse>, ErrorResponse>> + Send>> {
let method_s = format!("{:?}", method);
self.record(Call {
stage: "binary",
method: method_s.clone(),
path: path.clone(),
data,
raw_body: None,
headers: None,
auth_token,
connection_id,
ip_address,
});
let out = if path.starts_with("/api/bin/") || path == "/api/order/skip-raw" {
Ok(Some(BinaryResponse::octet_stream(format!("BIN:{}", method_s).into_bytes())))
} else if path.starts_with("/api/bin-status/") {
Ok(Some(BinaryResponse::new(b"accepted".to_vec(), "text/plain").with_status(202)))
} else if path.starts_with("/api/bin-fail/") {
Err(ErrorResponse::new(Some(ErrorCode::Forbidden), Some("no binary for you".into())))
} else {
Ok(None)
};
boxed(out)
}
fn route_with_raw_body(
&self,
method: HttpMethod,
path: String,
body_bytes: Bytes,
data: Value,
headers: HashMap<String, String>,
auth_token: Option<String>,
connection_id: Option<String>,
ip_address: Option<String>,
) -> Pin<Box<dyn Future<Output = Result<Option<BinaryResponse>, ErrorResponse>> + Send>> {
let method_s = format!("{:?}", method);
self.record(Call {
stage: "raw",
method: method_s,
path: path.clone(),
data: data.clone(),
raw_body: Some(body_bytes.to_vec()),
headers: Some(headers.clone()),
auth_token,
connection_id,
ip_address,
});
let out = if path.starts_with("/api/raw/") || path == "/api/order/all" {
let body = json!({
"raw": String::from_utf8_lossy(&body_bytes),
"data": data,
"headers": headers,
});
Ok(Some(BinaryResponse::new(serde_json::to_vec(&body).unwrap(), "application/json")))
} else if path.starts_with("/api/raw-fail/") {
Err(ErrorResponse::new(Some(ErrorCode::Unauthorized), Some("bad signature".into())))
} else {
Ok(None)
};
boxed(out)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestMsg {
pub channel: String,
pub data: Option<Value>,
}
#[derive(Clone, Default)]
pub struct RecordingSocketRouter {
pub calls: Arc<Mutex<Vec<(TestMsg, String)>>>,
}
impl SocketRouterFunction<TestMsg, Value> for RecordingSocketRouter {
fn route(&self, msg: TestMsg, connection_id: String) -> Pin<Box<dyn Future<Output = Option<Value>> + Send>> {
self.calls.lock().unwrap().push((msg.clone(), connection_id.clone()));
let out = if msg.channel == "echo" {
Some(json!({"echo": msg, "connection_id": connection_id}))
} else {
None
};
boxed(out)
}
}
pub fn spawn_server<F, Fut>(port: u16, start: F)
where
F: FnOnce() -> Fut + Send + 'static,
Fut: Future<Output = anyhow::Result<()>>,
{
std::thread::Builder::new()
.name(format!("cnctd-server-{}", port))
.spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("server runtime");
if let Err(e) = rt.block_on(start()) {
eprintln!("server on {} exited with error: {:?}", port, e);
}
})
.expect("server thread");
let deadline = Instant::now() + Duration::from_secs(90);
loop {
if TcpStream::connect(("127.0.0.1", port)).is_ok() {
return;
}
assert!(Instant::now() < deadline, "server on port {} never came up", port);
std::thread::sleep(Duration::from_millis(50));
}
}
pub fn http() -> reqwest::Client {
reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.no_proxy()
.build()
.unwrap()
}
pub fn base(port: u16) -> String {
format!("http://127.0.0.1:{}", port)
}
pub async fn eventually<T, F, Fut>(timeout: Duration, mut check: F) -> Option<T>
where
F: FnMut() -> Fut,
Fut: Future<Output = Option<T>>,
{
let deadline = Instant::now() + timeout;
loop {
if let Some(v) = check().await {
return Some(v);
}
if Instant::now() >= deadline {
return None;
}
tokio::time::sleep(Duration::from_millis(25)).await;
}
}
pub static ONCE: OnceLock<()> = OnceLock::new();