#![cfg(feature = "integration")]
use std::time::Duration;
use xphone::config::Config;
use xphone::sip::client::{Client, ClientConfig};
use xphone::types::ExtensionState;
use xphone::Phone;
fn asterisk_host() -> String {
std::env::var("ASTERISK_HOST").unwrap_or_else(|_| "127.0.0.1".into())
}
fn asterisk_password() -> String {
std::env::var("ASTERISK_PASSWORD").unwrap_or_else(|_| "test".into())
}
fn asterisk_port() -> u16 {
std::env::var("ASTERISK_PORT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(5160)
}
fn local_ip_override() -> Option<String> {
std::env::var("LOCAL_IP").ok()
}
fn integration_client_config(ext: &str, password: &str) -> ClientConfig {
let host = asterisk_host();
let port = asterisk_port();
ClientConfig {
server_addr: format!("{}:{}", host, port).parse().unwrap(),
username: ext.into(),
password: password.into(),
domain: host,
..Default::default()
}
}
fn integration_phone_config(ext: &str, password: &str) -> Config {
Config {
username: ext.into(),
password: password.into(),
host: asterisk_host(),
port: asterisk_port(),
local_ip: local_ip_override().unwrap_or_default(),
register_expiry: Duration::from_secs(10),
register_retry: Duration::from_secs(1),
register_max_retry: 3,
media_timeout: Duration::from_secs(10),
rtp_port_min: 20000,
rtp_port_max: 20099,
..Config::default()
}
}
#[test]
fn register_1001_raw() {
let cfg = integration_client_config("1001", &asterisk_password());
let client = Client::new(cfg).unwrap();
let (code, reason) = client.send_register(Duration::from_secs(5)).unwrap();
assert_eq!(code, 200, "expected 200, got {} {}", code, reason);
client.close();
}
#[test]
fn register_1002_raw() {
let cfg = integration_client_config("1002", &asterisk_password());
let client = Client::new(cfg).unwrap();
let (code, reason) = client.send_register(Duration::from_secs(5)).unwrap();
assert_eq!(code, 200, "expected 200, got {} {}", code, reason);
client.close();
}
#[test]
fn register_wrong_password() {
let cfg = integration_client_config("1001", "wrong");
let client = Client::new(cfg).unwrap();
let result = client.send_register(Duration::from_secs(5));
if let Ok((code, _)) = result {
assert_ne!(code, 200, "should not get 200 with wrong password");
}
client.close();
}
#[test]
fn keepalive() {
let cfg = integration_client_config("1001", &asterisk_password());
let client = Client::new(cfg).unwrap();
let (code, _) = client.send_register(Duration::from_secs(5)).unwrap();
assert_eq!(code, 200);
client.send_keepalive().unwrap();
client.close();
}
#[test]
fn phone_connect_and_disconnect() {
let cfg = integration_phone_config("1001", &asterisk_password());
let phone = Phone::new(cfg);
phone.connect().unwrap();
assert_eq!(phone.state(), xphone::PhoneState::Registered);
phone.disconnect().unwrap();
assert_eq!(phone.state(), xphone::PhoneState::Disconnected);
}
#[test]
fn phone_connect_wrong_password() {
let cfg = integration_phone_config("1001", "wrong");
let phone = Phone::new(cfg);
let result = phone.connect();
assert!(result.is_err());
}
#[test]
fn dial_between_extensions() {
let cfg1 = integration_phone_config("1001", &asterisk_password());
let cfg2 = integration_phone_config("1002", &asterisk_password());
let p1 = Phone::new(cfg1);
let p2 = Phone::new(cfg2);
p1.connect().unwrap();
p2.connect().unwrap();
let (call_tx, call_rx) = crossbeam_channel::bounded(1);
p2.on_incoming(move |call| {
call.accept().unwrap();
let _ = call_tx.send(call);
});
let opts = xphone::config::DialOptions {
timeout: Duration::from_secs(10),
..Default::default()
};
let call1 = p1.dial("1002", opts).unwrap();
let call2 = call_rx.recv_timeout(Duration::from_secs(10)).unwrap();
assert_eq!(call1.state(), xphone::types::CallState::Active);
assert_eq!(call2.state(), xphone::types::CallState::Active);
call1.end().unwrap();
std::thread::sleep(Duration::from_millis(500));
assert_eq!(call2.state(), xphone::types::CallState::Ended);
p1.disconnect().unwrap();
p2.disconnect().unwrap();
}
#[test]
fn inbound_accept_and_remote_bye() {
let cfg1 = integration_phone_config("1001", &asterisk_password());
let cfg2 = integration_phone_config("1002", &asterisk_password());
let p1 = Phone::new(cfg1);
let p2 = Phone::new(cfg2);
p1.connect().unwrap();
p2.connect().unwrap();
let (ended_tx, ended_rx) = crossbeam_channel::bounded::<xphone::types::EndReason>(1);
let (call_tx, call_rx) = crossbeam_channel::bounded(1);
p2.on_incoming(move |call| {
let ended_tx = ended_tx.clone();
call.on_ended(move |reason| {
let _ = ended_tx.send(reason);
});
call.accept().unwrap();
let _ = call_tx.send(true);
});
let opts = xphone::config::DialOptions {
timeout: Duration::from_secs(10),
..Default::default()
};
let call1 = p1.dial("1002", opts).unwrap();
call_rx.recv_timeout(Duration::from_secs(10)).unwrap();
call1.end().unwrap();
let reason = ended_rx.recv_timeout(Duration::from_secs(5)).unwrap();
assert_eq!(reason, xphone::types::EndReason::Remote);
p1.disconnect().unwrap();
p2.disconnect().unwrap();
}
#[test]
fn hold_resume() {
let cfg1 = integration_phone_config("1001", &asterisk_password());
let cfg2 = integration_phone_config("1002", &asterisk_password());
let p1 = Phone::new(cfg1);
let p2 = Phone::new(cfg2);
p1.connect().unwrap();
p2.connect().unwrap();
let (call_tx, call_rx) = crossbeam_channel::bounded(1);
p2.on_incoming(move |call| {
call.accept().unwrap();
let _ = call_tx.send(call);
});
let opts = xphone::config::DialOptions {
timeout: Duration::from_secs(10),
..Default::default()
};
let call1 = p1.dial("1002", opts).unwrap();
let _call2 = call_rx.recv_timeout(Duration::from_secs(10)).unwrap();
assert_eq!(call1.state(), xphone::types::CallState::Active);
call1.hold().unwrap();
assert_eq!(call1.state(), xphone::types::CallState::OnHold);
std::thread::sleep(Duration::from_millis(500));
call1.resume().unwrap();
assert_eq!(call1.state(), xphone::types::CallState::Active);
std::thread::sleep(Duration::from_millis(500));
call1.end().unwrap();
p1.disconnect().unwrap();
p2.disconnect().unwrap();
}
#[test]
#[ignore]
fn dtmf_send_receive() {
let cfg1 = integration_phone_config("1001", &asterisk_password());
let cfg2 = integration_phone_config("1002", &asterisk_password());
let p1 = Phone::new(cfg1);
let p2 = Phone::new(cfg2);
p1.connect().unwrap();
p2.connect().unwrap();
let (call_tx, call_rx) = crossbeam_channel::bounded(1);
p2.on_incoming(move |call| {
call.accept().unwrap();
let _ = call_tx.send(call);
});
let opts = xphone::config::DialOptions {
timeout: Duration::from_secs(10),
..Default::default()
};
let call1 = p1.dial("1002", opts).unwrap();
let call2 = call_rx.recv_timeout(Duration::from_secs(10)).unwrap();
assert_eq!(call1.state(), xphone::types::CallState::Active);
assert_eq!(call2.state(), xphone::types::CallState::Active);
let (dtmf_tx, dtmf_rx) = crossbeam_channel::bounded(10);
call1.on_dtmf(move |digit| {
let _ = dtmf_tx.send(digit);
});
std::thread::sleep(Duration::from_secs(2));
let mut digit = None;
for attempt in 0..3 {
if attempt > 0 {
std::thread::sleep(Duration::from_secs(1));
}
call2.send_dtmf("5").unwrap();
if let Ok(d) = dtmf_rx.recv_timeout(Duration::from_secs(3)) {
digit = Some(d);
break;
}
}
assert_eq!(digit.expect("DTMF digit never received by p1"), "5");
call1.end().unwrap();
p1.disconnect().unwrap();
p2.disconnect().unwrap();
}
#[test]
fn echo_test() {
let cfg = integration_phone_config("1001", &asterisk_password());
let p = Phone::new(cfg);
p.connect().unwrap();
let opts = xphone::config::DialOptions {
timeout: Duration::from_secs(10),
..Default::default()
};
let call = p.dial("9999", opts).unwrap();
assert_eq!(call.state(), xphone::types::CallState::Active);
let rtp_writer = call.rtp_writer().expect("rtp_writer channel not available");
let rtp_reader = call.rtp_reader().expect("rtp_reader channel not available");
let silence = vec![0xFFu8; 160]; for i in 0..50 {
let pkt = xphone::types::RtpPacket {
header: xphone::types::RtpHeader {
version: 2,
payload_type: 0, sequence_number: i as u16,
timestamp: (i as u32) * 160,
ssrc: 0xDEADBEEF,
marker: false,
},
payload: silence.clone(),
};
let _ = rtp_writer.send(pkt);
std::thread::sleep(Duration::from_millis(20));
}
let pkt = rtp_reader
.recv_timeout(Duration::from_secs(5))
.expect("no echo response received");
assert!(!pkt.payload.is_empty());
call.end().unwrap();
p.disconnect().unwrap();
}
#[test]
fn sip_message_between_extensions() {
let cfg1 = integration_phone_config("1001", &asterisk_password());
let cfg2 = integration_phone_config("1002", &asterisk_password());
let p1 = Phone::new(cfg1);
let p2 = Phone::new(cfg2);
let (msg_tx, msg_rx) = crossbeam_channel::bounded(1);
p2.on_message(move |msg| {
let _ = msg_tx.send(msg);
});
p1.connect().unwrap();
p2.connect().unwrap();
p1.send_message("1002", "Hello from 1001").unwrap();
let msg = msg_rx
.recv_timeout(Duration::from_secs(5))
.expect("MESSAGE not received by p2");
assert_eq!(msg.body, "Hello from 1001");
assert_eq!(msg.content_type, "text/plain");
assert!(
msg.from.contains("1001"),
"from should contain sender: {}",
msg.from
);
p1.disconnect().unwrap();
p2.disconnect().unwrap();
}
#[test]
fn blf_watch_state_changes() {
let cfg1 = integration_phone_config("1001", &asterisk_password());
let cfg2 = integration_phone_config("1002", &asterisk_password());
let p1 = Phone::new(cfg1);
let p2 = Phone::new(cfg2);
p1.connect().unwrap();
p2.connect().unwrap();
let (blf_tx, blf_rx) = crossbeam_channel::bounded(10);
p1.watch("1002", move |status, _prev| {
let _ = blf_tx.send(status.state);
})
.unwrap();
let initial = blf_rx.recv_timeout(Duration::from_secs(5));
if let Ok(state) = initial {
assert_eq!(state, ExtensionState::Available);
}
let (call_tx, call_rx) = crossbeam_channel::bounded(1);
p1.on_incoming(move |call| {
call.accept().unwrap();
let _ = call_tx.send(call);
});
let opts = xphone::config::DialOptions {
timeout: Duration::from_secs(10),
..Default::default()
};
let call2 = p2.dial("1001", opts).unwrap();
let _call1 = call_rx.recv_timeout(Duration::from_secs(10)).unwrap();
let mut saw_busy = false;
for _ in 0..10 {
match blf_rx.recv_timeout(Duration::from_secs(2)) {
Ok(ExtensionState::OnThePhone) | Ok(ExtensionState::Ringing) => {
saw_busy = true;
break;
}
Ok(_) => continue,
Err(_) => break,
}
}
assert!(saw_busy, "expected BLF to show Ringing or OnThePhone");
call2.end().unwrap();
std::thread::sleep(Duration::from_millis(500));
let mut saw_available = false;
for _ in 0..10 {
match blf_rx.recv_timeout(Duration::from_secs(2)) {
Ok(ExtensionState::Available) => {
saw_available = true;
break;
}
Ok(_) => continue,
Err(_) => break,
}
}
assert!(
saw_available,
"expected BLF to return to Available after hangup"
);
p1.unwatch("1002").unwrap();
p1.disconnect().unwrap();
p2.disconnect().unwrap();
}