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};
#[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: "127.0.0.1".to_string(),
udp_port: sip_port,
log_level: Some("debug".to_string()),
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(40000 + (sip_port % 1000) * 20),
rtp_end_port: Some(40020 + (sip_port % 1000) * 20),
media_cache_path: "./target/tmp_media_test".to_string(),
..Default::default()
}
}
async fn start_webhook_server(port: u16, tx: mpsc::Sender<String>) {
use warp::Filter;
let route = warp::post()
.and(warp::path("mock-handler"))
.and(warp::body::json())
.map(move |payload: WebhookPayload| {
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)));
}
async fn has_sipbot() -> bool {
tokio::process::Command::new("sipbot")
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.await
.map(|s| s.success())
.unwrap_or(false)
}
#[tokio::test]
async fn test_call_lifecycle_play_and_hangup() {
if !has_sipbot().await {
info!("sipbot not found, skipping test");
return;
}
tracing_subscriber::fmt()
.with_max_level(Level::INFO)
.with_test_writer()
.try_init()
.ok();
let sip_port = 35070;
let http_port = 9070;
let webhook_port = 9970;
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();
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();
tokio::spawn(async move {
axum::serve(listener, router).await.unwrap();
});
tokio::time::sleep(Duration::from_millis(500)).await;
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",
"--hangup",
"30",
])
.spawn()
.expect("Failed to spawn sipbot");
let dialog_id = tokio::time::timeout(Duration::from_secs(5), rx.recv())
.await
.expect("Timed out waiting for invite")
.expect("Channel closed");
let ws_url = format!("ws://127.0.0.1:{}/call/sip?id={}", http_port, dialog_id);
let (ws_stream, _) = connect_async(ws_url).await.expect("WS connect failed");
let (mut ws_write, mut ws_read) = ws_stream.split();
ws_write
.send(Message::Text(
serde_json::json!({
"command": "accept",
"option": {}
})
.to_string()
.into(),
))
.await
.unwrap();
let mut answered = false;
let mut track_started = false;
let timeout = tokio::time::sleep(Duration::from_secs(5));
tokio::pin!(timeout);
loop {
tokio::select! {
msg = ws_read.next() => {
match msg {
Some(Ok(Message::Text(text))) => {
info!("WS Event Received: {}", text);
if text.contains("\"event\":\"answer\"") {
answered = true;
}
if text.contains("\"event\":\"trackStart\"") {
track_started = true;
}
if answered && track_started {
break;
}
}
Some(Ok(_)) => {}
Some(Err(e)) => panic!("WS read error: {}", e),
None => break,
}
}
_ = &mut timeout => {
break;
}
}
}
assert!(answered, "Failed to receive answer event");
assert!(track_started, "Failed to receive trackStart event");
let test_file = "fixtures/sample.wav";
ws_write
.send(Message::Text(
serde_json::json!({
"command": "play",
"url": test_file,
"play_id": "test_play_1"
})
.to_string()
.into(),
))
.await
.unwrap();
let mut track_ended = false;
let timeout = tokio::time::sleep(Duration::from_secs(15)); tokio::pin!(timeout);
loop {
tokio::select! {
msg = ws_read.next() => {
match msg {
Some(Ok(Message::Text(text))) => {
info!("WS Event Received: {}", text);
if text.contains("\"event\":\"trackEnd\"") {
track_ended = true;
break;
}
}
Some(Ok(_)) => {
}
_ => {
break;
}
}
}
_ = &mut timeout => {
break;
}
}
}
assert!(track_ended, "Failed to receive trackEnd event for play");
ws_write
.send(Message::Text(
serde_json::json!({"command": "hangup"}).to_string().into(),
))
.await
.unwrap();
let _ = child.wait().await;
}
#[tokio::test]
async fn test_app_invite_sipbot_wait() {
if !has_sipbot().await {
info!("sipbot not found, skipping test");
return;
}
tracing_subscriber::fmt()
.with_max_level(Level::INFO)
.with_test_writer()
.try_init()
.ok();
let sip_port = 35071;
let http_port = 9071;
let webhook_port = 9971;
let bot_sip_port = 35080;
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");
tokio::spawn(app.clone().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();
tokio::spawn(async move {
axum::serve(listener, router).await.unwrap();
});
tokio::time::sleep(Duration::from_millis(500)).await;
let mut child = tokio::process::Command::new("sipbot")
.args(&[
"wait",
"--addr",
&format!("127.0.0.1:{}", bot_sip_port),
"--answer",
"fixtures/sample.wav",
"--hangup",
"5",
])
.spawn()
.expect("Failed to spawn sipbot wait");
tokio::time::sleep(Duration::from_millis(500)).await;
let session_id = "test_invite_session";
let ws_url = format!("ws://127.0.0.1:{}/call/sip?id={}", http_port, session_id);
let (ws_stream, _) = connect_async(ws_url).await.expect("WS connect failed");
let (mut ws_write, mut ws_read) = ws_stream.split();
ws_write
.send(Message::Text(
serde_json::json!({
"command": "invite",
"option": {
"callee": format!("sip:bot@127.0.0.1:{}", bot_sip_port)
}
})
.to_string()
.into(),
))
.await
.unwrap();
let mut answered = false;
let timeout = tokio::time::sleep(Duration::from_secs(10));
tokio::pin!(timeout);
loop {
tokio::select! {
msg = ws_read.next() => {
if let Some(Ok(Message::Text(text))) = msg {
info!("WS Event: {}", text);
if text.contains("\"event\":\"answer\"") {
answered = true;
break;
}
} else if msg.is_none() {
break;
}
}
_ = &mut timeout => { break; }
}
}
assert!(answered, "App failed to receive answer from sipbot");
ws_write
.send(Message::Text(
serde_json::json!({"command": "hangup"}).to_string().into(),
))
.await
.unwrap();
let _ = child.kill().await;
}