use active_call::{
app::AppStateBuilder,
config::{Config, InviteHandlerConfig},
};
use futures::{SinkExt, StreamExt};
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
use tokio::time::Duration;
use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};
use tracing::{Level, info};
use warp::Filter;
#[derive(Debug, Deserialize, Serialize)]
struct WebhookPayload {
#[serde(rename = "dialogId")]
dialog_id: String,
event: String,
}
fn create_test_config(sip_port: u16, http_port: u16, webhook_port: u16) -> Config {
Config {
http_addr: format!("127.0.0.1:{}", http_port),
addr: "0.0.0.0".to_string(),
udp_port: sip_port,
log_level: Some("debug".to_string()),
http_access_skip_paths: vec![],
useragent: Some("ActiveCallTest".to_string()),
graceful_shutdown: Some(true),
handler: Some(InviteHandlerConfig::Webhook {
url: Some(format!("http://127.0.0.1:{}/mock-handler", webhook_port)),
method: Some("POST".to_string()),
headers: None,
urls: None,
}),
accept_timeout: Some("5s".to_string()),
rtp_start_port: Some(30000 + (sip_port % 1000) * 20),
rtp_end_port: Some(30020 + (sip_port % 1000) * 20),
media_cache_path: "./target/tmp_media".to_string(),
..Default::default()
}
}
async fn start_webhook_server(port: u16, tx: mpsc::Sender<String>) {
let route = warp::post()
.and(warp::path("mock-handler"))
.and(warp::body::json())
.map(move |payload: WebhookPayload| {
info!("Webhook received: {:?}", payload);
if payload.event == "invite" {
let _ = tx.try_send(payload.dialog_id);
}
warp::reply::json(&serde_json::json!({"status": "ok"}))
});
tokio::spawn(warp::serve(route).run(([127, 0, 0, 1], port)));
}
#[tokio::test]
async fn test_sip_options_ping() {
tracing_subscriber::fmt()
.with_max_level(Level::DEBUG)
.with_test_writer()
.try_init()
.ok();
let sip_port = 35060;
let http_port = 9090;
let webhook_port = 9999;
let config = create_test_config(sip_port, http_port, webhook_port);
let builder = AppStateBuilder::new().with_config(config);
let app = builder.build().await.expect("Failed to build app state");
let _app_handle = tokio::spawn(app.clone().serve());
tokio::time::sleep(Duration::from_millis(500)).await;
info!("Running sipbot options...");
let output = tokio::process::Command::new("sipbot")
.args(&["options", &format!("sip:100@127.0.0.1:{}", sip_port), "-v"])
.output()
.await
.expect("Failed to execute sipbot");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
info!("SIPBot stdout: {}", stdout);
info!("SIPBot stderr: {}", stderr);
}
#[tokio::test]
async fn test_sip_invite_call() {
let sip_port = 35061;
let http_port = 9091;
let webhook_port = 9991;
let config = create_test_config(sip_port, http_port, webhook_port);
let (tx, mut rx) = mpsc::channel(1);
start_webhook_server(webhook_port, tx).await;
let builder = AppStateBuilder::new().with_config(config);
let app = builder.build().await.expect("Failed to build app state");
let app_for_serve = app.clone();
let _sip_handle = tokio::spawn(app_for_serve.serve());
let router = active_call::handler::handler::call_router().with_state(app.clone());
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", http_port))
.await
.unwrap();
let _http_handle = tokio::spawn(async move {
axum::serve(listener, router).await.unwrap();
});
tokio::time::sleep(Duration::from_millis(500)).await;
info!("Running sipbot call...");
let mut child = tokio::process::Command::new("sipbot")
.args(&[
"call",
"--target",
&format!("sip:100@127.0.0.1:{}", sip_port),
"--external",
"127.0.0.1",
"-v",
])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("Failed to spawn sipbot");
let dialog_id = match tokio::time::timeout(Duration::from_secs(5), rx.recv()).await {
Ok(Some(id)) => id,
Ok(None) => panic!("Channel closed without receiving dialogId"),
Err(_) => panic!("Timed out waiting for webhook invite"),
};
info!("Received dialog ID: {}", dialog_id);
let ws_url = format!("ws://127.0.0.1:{}/call/sip?id={}", http_port, dialog_id);
info!("Connecting to WebSocket: {}", ws_url);
let (ws_stream, _) = connect_async(ws_url)
.await
.expect("Failed to connect WebSocket");
let (mut write, mut read) = ws_stream.split();
let accept_cmd = serde_json::json!({
"command": "accept",
"option": {}
});
write
.send(Message::Text(accept_cmd.to_string().into()))
.await
.expect("Failed to send accept command");
let ws_handle = tokio::spawn(async move {
while let Some(msg) = read.next().await {
match msg {
Ok(Message::Text(t)) => info!("WS Message: {}", t),
Ok(Message::Binary(_)) => {} Err(e) => info!("WS Error: {}", e),
_ => {}
}
}
});
tokio::time::sleep(Duration::from_secs(4)).await;
if let Ok(Some(status)) = child.try_wait() {
let output = child.wait_with_output().await.expect("Failed to wait");
info!("SIPBot stdout: {}", String::from_utf8_lossy(&output.stdout));
info!("SIPBot stderr: {}", String::from_utf8_lossy(&output.stderr));
panic!("sipbot exited prematurely with status: {}", status);
}
let _ = child.kill().await;
let output = child
.wait_with_output()
.await
.expect("Failed to wait on sipbot");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
info!("SIPBot stdout: {}", stdout);
info!("SIPBot stderr: {}", stderr);
if !stderr.contains("Call established")
&& !stdout.contains("Call established")
&& !stderr.contains("200 OK")
&& !stdout.contains("200 OK")
{
panic!(
"Did not see 'Call established' or '200 OK' in sipbot output. Call might not have been answered."
);
}
ws_handle.abort();
}