use super::test_helpers;
use super::test_ua::{TestUa, TestUaConfig, TestUaEvent};
use crate::call::user::SipUser;
use crate::config::ProxyConfig;
use crate::proxy::{
locator::{Locator, MemoryLocator},
server::SipServerBuilder,
user::MemoryUserBackend,
};
use anyhow::Result;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::time::sleep;
use tokio_util::sync::CancellationToken;
use tracing::{info, warn};
fn create_test_proxy_config(port: u16) -> ProxyConfig {
test_helpers::test_proxy_config(port)
}
fn create_test_users() -> Vec<SipUser> {
let mut users = vec![
SipUser {
id: 1,
username: "alice".to_string(),
password: Some("password123".to_string()),
enabled: true,
realm: Some("127.0.0.1".to_string()),
..Default::default()
},
SipUser {
id: 2,
username: "bob".to_string(),
password: Some("password456".to_string()),
enabled: true,
realm: Some("127.0.0.1".to_string()),
..Default::default()
},
SipUser {
id: 3,
username: "charlie".to_string(),
password: Some("wrongpassword".to_string()),
enabled: false,
realm: Some("127.0.0.1".to_string()),
..Default::default()
},
];
users.push(SipUser {
id: 4,
username: "david".to_string(),
password: Some("password789".to_string()),
enabled: true,
realm: Some("other.com".to_string()),
..Default::default()
});
users.push(SipUser {
id: 5,
username: "testuser".to_string(),
password: Some("testpassword".to_string()),
enabled: true,
realm: Some("127.0.0.1".to_string()),
..Default::default()
});
users
}
pub struct TestProxyServer {
cancel_token: CancellationToken,
port: u16,
server: Arc<crate::proxy::server::SipServer>,
}
impl TestProxyServer {
pub async fn start() -> Result<Self> {
let port = portpicker::pick_unused_port().unwrap_or(15060);
let config = Arc::new(create_test_proxy_config(port));
let user_backend = MemoryUserBackend::new(None);
for user in create_test_users() {
user_backend.create_user(user).await?;
}
let locator = MemoryLocator::new();
let cancel_token = CancellationToken::new();
let builder = test_helpers::register_standard_modules(
SipServerBuilder::new(config)
.with_user_backend(Box::new(user_backend))
.with_locator(Box::new(locator))
.with_cancel_token(cancel_token.clone()),
);
let server = Arc::new(builder.build().await?);
let server_clone = server.clone();
crate::utils::spawn(async move {
if let Err(e) = server_clone.serve().await {
warn!("Proxy server error: {:?}", e);
}
});
sleep(Duration::from_millis(100)).await;
info!("Test proxy server started on port {}", port);
Ok(Self {
cancel_token,
port,
server,
})
}
pub fn get_locator(&self) -> Arc<Box<dyn Locator>> {
self.server.inner.locator.clone()
}
pub fn get_addr(&self) -> SocketAddr {
format!("127.0.0.1:{}", self.port).parse().unwrap()
}
pub fn stop(&self) {
self.cancel_token.cancel();
}
}
impl Drop for TestProxyServer {
fn drop(&mut self) {
self.stop();
}
}
async fn create_test_ua(
username: &str,
password: &str,
proxy_addr: SocketAddr,
port: u16,
) -> Result<TestUa> {
let config = TestUaConfig {
username: username.to_string(),
password: password.to_string(),
realm: "127.0.0.1".to_string(),
local_port: port,
proxy_addr,
};
let mut ua = TestUa::new(config);
ua.start().await?;
Ok(ua)
}
#[tokio::test]
async fn test_successful_registration() {
let proxy = TestProxyServer::start().await.unwrap();
let proxy_addr = proxy.get_addr();
let alice_port = portpicker::pick_unused_port().unwrap_or(25000);
let alice = create_test_ua("alice", "password123", proxy_addr, alice_port)
.await
.unwrap();
assert!(
alice.register().await.is_ok(),
"Alice registration should succeed"
);
alice.stop();
proxy.stop();
}
#[tokio::test]
async fn test_failed_registration_wrong_password() {
let proxy = TestProxyServer::start().await.unwrap();
let proxy_addr = proxy.get_addr();
let alice_port = portpicker::pick_unused_port().unwrap_or(25001);
let alice = create_test_ua("alice", "wrongpassword", proxy_addr, alice_port)
.await
.unwrap();
assert!(
alice.register().await.is_err(),
"Alice registration should fail with wrong password"
);
alice.stop();
proxy.stop();
}
#[tokio::test]
async fn test_failed_registration_disabled_user() {
let proxy = TestProxyServer::start().await.unwrap();
let proxy_addr = proxy.get_addr();
let charlie_port = portpicker::pick_unused_port().unwrap_or(25002);
let charlie = create_test_ua("charlie", "wrongpassword", proxy_addr, charlie_port)
.await
.unwrap();
assert!(
charlie.register().await.is_err(),
"Charlie registration should fail (disabled user)"
);
charlie.stop();
proxy.stop();
}
#[tokio::test]
async fn test_multiple_user_registration() {
let proxy = TestProxyServer::start().await.unwrap();
let proxy_addr = proxy.get_addr();
let alice_port = portpicker::pick_unused_port().unwrap_or(25010);
let bob_port = portpicker::pick_unused_port().unwrap_or(25011);
let alice = create_test_ua("alice", "password123", proxy_addr, alice_port)
.await
.unwrap();
let bob = create_test_ua("bob", "password456", proxy_addr, bob_port)
.await
.unwrap();
assert!(
alice.register().await.is_ok(),
"Alice registration should succeed"
);
assert!(
bob.register().await.is_ok(),
"Bob registration should succeed"
);
alice.stop();
bob.stop();
proxy.stop();
}
#[tokio::test]
async fn test_call_success() {
let _ = tracing_subscriber::fmt::try_init();
let proxy = TestProxyServer::start().await.unwrap();
let proxy_addr = proxy.get_addr();
let alice_port = portpicker::pick_unused_port().unwrap_or(25020);
let bob_port = portpicker::pick_unused_port().unwrap_or(25021);
let alice = create_test_ua("alice", "password123", proxy_addr, alice_port)
.await
.unwrap();
let bob = create_test_ua("bob", "password456", proxy_addr, bob_port)
.await
.unwrap();
alice.register().await.unwrap();
bob.register().await.unwrap();
sleep(Duration::from_millis(500)).await;
let dummy_sdp = "v=0\r\no=- 123456 123456 IN IP4 127.0.0.1\r\ns=-\r\nc=IN IP4 127.0.0.1\r\nt=0 0\r\nm=audio 1234 RTP/AVP 0 101\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:101 telephone-event/8000\r\na=fmtp:101 0-16\r\na=sendrecv\r\n".to_string();
let alice_sdp = dummy_sdp.clone();
let call_task =
crate::utils::spawn(async move { alice.make_call("bob", Some(alice_sdp)).await });
let bob_sdp = dummy_sdp.clone();
let answer_task = crate::utils::spawn(async move {
for _ in 0..50 {
let events = bob.process_dialog_events().await.unwrap_or_default();
for event in events {
if let TestUaEvent::IncomingCall(dialog_id, _) = event {
info!("Bob received incoming call: {}", dialog_id);
bob.answer_call(&dialog_id, Some(bob_sdp.clone()))
.await
.unwrap();
return Ok::<_, anyhow::Error>(dialog_id);
}
}
sleep(Duration::from_millis(100)).await;
}
Err(anyhow::anyhow!("No incoming call received"))
});
let (call_result, answer_result) = tokio::join!(call_task, answer_task);
assert!(call_result.is_ok(), "Call should be initiated successfully");
assert!(
answer_result.is_ok(),
"Call should be answered successfully"
);
proxy.stop();
}
#[tokio::test]
async fn test_call_to_nonexistent_user() {
let proxy = TestProxyServer::start().await.unwrap();
let proxy_addr = proxy.get_addr();
let alice_port = portpicker::pick_unused_port().unwrap_or(25030);
let alice = create_test_ua("alice", "password123", proxy_addr, alice_port)
.await
.unwrap();
alice.register().await.unwrap();
sleep(Duration::from_millis(200)).await;
let result = alice.make_call("nonexistent", None).await;
assert!(result.is_err(), "Call to nonexistent user should fail");
alice.stop();
proxy.stop();
}
#[tokio::test]
async fn test_call_to_different_realm() {
let proxy = TestProxyServer::start().await.unwrap();
let proxy_addr = proxy.get_addr();
let alice_port = portpicker::pick_unused_port().unwrap_or(25040);
let alice = create_test_ua("alice", "password123", proxy_addr, alice_port)
.await
.unwrap();
alice.register().await.unwrap();
sleep(Duration::from_millis(200)).await;
let result = alice.make_call("david", None).await;
assert!(
result.is_err(),
"Call to user in different realm should fail"
);
alice.stop();
proxy.stop();
}
#[tokio::test]
async fn test_call_rejection() {
let _ = tracing_subscriber::fmt::try_init();
let proxy = TestProxyServer::start().await.unwrap();
let proxy_addr = proxy.get_addr();
let alice_port = portpicker::pick_unused_port().unwrap_or(25050);
let bob_port = portpicker::pick_unused_port().unwrap_or(25051);
let alice = create_test_ua("alice", "password123", proxy_addr, alice_port)
.await
.unwrap();
let bob = create_test_ua("bob", "password456", proxy_addr, bob_port)
.await
.unwrap();
alice.register().await.unwrap();
bob.register().await.unwrap();
sleep(Duration::from_millis(500)).await;
let alice_sdp = "v=0\r\no=- 123456 123456 IN IP4 127.0.0.1\r\ns=-\r\nc=IN IP4 127.0.0.1\r\nt=0 0\r\nm=audio 1234 RTP/AVP 0 101\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:101 telephone-event/8000\r\na=fmtp:101 0-16\r\na=sendrecv\r\n".to_string();
let call_task =
crate::utils::spawn(async move { alice.make_call("bob", Some(alice_sdp)).await });
let reject_task = crate::utils::spawn(async move {
for _ in 0..50 {
let events = bob.process_dialog_events().await.unwrap_or_default();
for event in events {
if let TestUaEvent::IncomingCall(dialog_id, _) = event {
info!("Bob received incoming call: {}", dialog_id);
bob.reject_call(&dialog_id).await.unwrap();
return Ok::<_, anyhow::Error>(());
}
}
sleep(Duration::from_millis(100)).await;
}
Err(anyhow::anyhow!("No incoming call received"))
});
let (call_result, reject_result) = tokio::join!(call_task, reject_task);
assert!(call_result.is_ok(), "Call task should complete");
if call_result.unwrap().is_ok() {
panic!("Call should be rejected, but it succeeded");
}
assert!(reject_result.is_ok(), "Bob should reject the call");
proxy.stop();
}
#[tokio::test]
async fn test_call_hangup_flow() {
let _ = tracing_subscriber::fmt::try_init();
let proxy = TestProxyServer::start().await.unwrap();
let proxy_addr = proxy.get_addr();
let alice_port = portpicker::pick_unused_port().unwrap_or(25060);
let bob_port = portpicker::pick_unused_port().unwrap_or(25061);
let alice = create_test_ua("alice", "password123", proxy_addr, alice_port)
.await
.unwrap();
let bob = create_test_ua("bob", "password456", proxy_addr, bob_port)
.await
.unwrap();
alice.register().await.unwrap();
bob.register().await.unwrap();
sleep(Duration::from_millis(500)).await;
let dummy_sdp = "v=0\r\no=- 123456 123456 IN IP4 127.0.0.1\r\ns=-\r\nc=IN IP4 127.0.0.1\r\nt=0 0\r\nm=audio 1234 RTP/AVP 0 101\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:101 telephone-event/8000\r\na=fmtp:101 0-16\r\na=sendrecv\r\n".to_string();
let alice_sdp = dummy_sdp.clone();
let call_task =
crate::utils::spawn(async move { alice.make_call("bob", Some(alice_sdp)).await });
let bob_sdp = dummy_sdp.clone();
let answer_task = crate::utils::spawn(async move {
for _ in 0..50 {
let events = bob.process_dialog_events().await.unwrap_or_default();
for event in events {
if let TestUaEvent::IncomingCall(dialog_id, _) = event {
info!("Bob received incoming call: {}", dialog_id);
bob.answer_call(&dialog_id, Some(bob_sdp.clone()))
.await
.unwrap();
sleep(Duration::from_millis(500)).await;
bob.hangup(&dialog_id).await.unwrap();
return Ok::<_, anyhow::Error>(dialog_id);
}
}
sleep(Duration::from_millis(100)).await;
}
Err(anyhow::anyhow!("No incoming call received"))
});
let (call_result, answer_result) = tokio::join!(call_task, answer_task);
assert!(call_result.is_ok(), "Call should be established");
assert!(answer_result.is_ok(), "Call should be answered and hung up");
proxy.stop();
}
#[tokio::test]
async fn test_locator_lookup() {
let proxy = TestProxyServer::start().await.unwrap();
let proxy_addr = proxy.get_addr();
let locator = proxy.get_locator();
let testuser_port = portpicker::pick_unused_port().unwrap_or(25070);
let testuser = create_test_ua("testuser", "testpassword", proxy_addr, testuser_port)
.await
.unwrap();
assert!(
testuser.register().await.is_ok(),
"Testuser registration should succeed"
);
sleep(Duration::from_millis(200)).await;
let lookup_uri: rsipstack::sip::Uri = format!("sip:testuser@127.0.0.1:{}", proxy.port)
.try_into()
.unwrap();
let locations = locator.lookup(&lookup_uri).await;
assert!(locations.is_ok(), "Locator lookup should succeed");
let locations = locations.unwrap();
assert!(
!locations.is_empty(),
"Locator should return at least one location for registered user"
);
let location = &locations[0];
assert_eq!(
location.aor.user().unwrap_or_default(),
"testuser",
"Location AoR should have username 'testuser'"
);
info!(
"Locator lookup test passed: found {} location(s)",
locations.len()
);
info!("Location details: {:?}", location);
testuser.stop();
proxy.stop();
}