use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::dialog::DialogId;
use crate::ice::Candidate;
use crate::sdp::ice_attrs;
use crate::sdp::negotiation::{
create_answer, create_media_attributes, process_answer, Codec, NegotiatedMedia,
};
use crate::sdp::parser::{Direction, SessionDescription};
use crate::session::call::{
Call, CallConfig, CallDirection, CallEndReason, CallEvent, CallId, CallState, Dialog,
PendingAnswer,
};
use crate::session::ice_session::IceLocalParams;
use crate::sip::headers::Refresher;
use crate::sip::{Method, SipRequest, SipResponse};
#[derive(Debug, Clone, Copy)]
pub struct IceAnswerInputs<'a> {
pub default_candidate: &'a Candidate,
pub local: &'a IceLocalParams,
}
impl<'a> IceAnswerInputs<'a> {
pub fn new(default_candidate: &'a Candidate, local: &'a IceLocalParams) -> Self {
Self {
default_candidate,
local,
}
}
}
#[derive(Debug, Clone)]
pub struct InviteOfferHeaders {
pub supported_tags: Vec<String>,
pub allow_methods: Vec<Method>,
pub session_expires: Option<(u32, Refresher)>,
pub min_se: Option<u32>,
}
#[derive(Debug, Clone)]
pub enum InboundSessionTimer {
Disabled,
Accept {
session_expires: Duration,
refresher: Refresher,
},
Reject422 {
min_se: u32,
},
}
#[derive(Debug, Clone)]
pub struct OutboundRequest {
pub call_id: CallId,
pub request: SipRequest,
pub kind: OutboundRequestKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutboundRequestKind {
Prack,
SessionTimerUpdate,
SessionTimerReInvite,
SessionTimerExpiryBye,
}
#[derive(Debug)]
pub enum ManagerEvent {
IncomingCall(CallId),
CallStateChanged(CallId, CallState),
CallEvent(CallId, CallEvent),
Error(String),
}
#[derive(Debug, Clone)]
pub struct ManagerConfig {
pub local_sip_addr: String,
pub local_rtp_addr: String,
pub rtp_port_range: (u16, u16),
pub call_config: CallConfig,
}
impl Default for ManagerConfig {
fn default() -> Self {
Self {
local_sip_addr: "127.0.0.1:5060".to_string(),
local_rtp_addr: "127.0.0.1".to_string(),
rtp_port_range: (10000, 20000),
call_config: CallConfig::default(),
}
}
}
pub struct CallManager {
config: Arc<ManagerConfig>,
call_config: Arc<CallConfig>,
calls: HashMap<CallId, Call>,
dialog_to_call: HashMap<DialogId, CallId>,
next_rtp_port: u16,
events: Vec<ManagerEvent>,
pending_outbound_requests: Vec<OutboundRequest>,
}
impl CallManager {
pub fn new(config: ManagerConfig) -> Self {
let next_rtp_port = config.rtp_port_range.0;
let call_config = Arc::new(config.call_config.clone());
Self {
config: Arc::new(config),
call_config,
calls: HashMap::new(),
dialog_to_call: HashMap::new(),
next_rtp_port,
events: Vec::new(),
pending_outbound_requests: Vec::new(),
}
}
pub fn call_count(&self) -> usize {
self.calls.len()
}
pub fn get_call(&self, id: &CallId) -> Option<&Call> {
self.calls.get(id)
}
pub fn get_call_mut(&mut self, id: &CallId) -> Option<&mut Call> {
self.calls.get_mut(id)
}
pub fn get_call_by_dialog(&self, dialog_id: &DialogId) -> Option<&Call> {
self.dialog_to_call
.get(dialog_id)
.and_then(|call_id| self.calls.get(call_id))
}
fn allocate_rtp_port(&mut self) -> u16 {
let port = self.next_rtp_port;
self.next_rtp_port += 2; if self.next_rtp_port > self.config.rtp_port_range.1 {
self.next_rtp_port = self.config.rtp_port_range.0;
}
port
}
pub fn create_call(&mut self, remote_uri: String) -> CallId {
let call = Call::new_outbound(self.call_config.clone(), remote_uri);
let call_id = call.id().clone();
self.calls.insert(call_id.clone(), call);
call_id
}
pub fn handle_incoming_invite(
&mut self,
dialog: Dialog,
offer_sdp: &SessionDescription,
) -> Option<(CallId, SessionDescription, u16)> {
let local_port = self.allocate_rtp_port();
let (answer_sdp, negotiated) =
create_answer(offer_sdp, &self.call_config.codecs, local_port)?;
let media = negotiated.into_iter().next().expect("negotiated media");
let call_id = self.create_inbound_call_internal(dialog, media, local_port)?;
Some((call_id, answer_sdp, local_port))
}
pub fn accept_inbound_invite(
&mut self,
dialog: Dialog,
offer_sdp: &SessionDescription,
) -> Option<CallId> {
let (_, mut negotiated) = create_answer(offer_sdp, &self.call_config.codecs, 0)?;
let media = negotiated.pop()?;
let pending = PendingAnswer {
offer: offer_sdp.clone(),
negotiated: media,
};
self.create_inbound_call_pending(dialog, pending)
}
pub fn reject_inbound_invite(&mut self, call_id: &CallId) -> Option<DialogId> {
let call = self.calls.get_mut(call_id)?;
if call.state() != CallState::Ringing || !call.has_pending_answer() {
return None;
}
let _ = call.take_pending_answer();
call.handle_ended(CallEndReason::Error);
let dialog_id = call.dialog_id().cloned();
self.events.push(ManagerEvent::CallEvent(
call_id.clone(),
CallEvent::Ended(CallEndReason::Error),
));
dialog_id
}
pub fn build_answer_for(
&mut self,
call_id: &CallId,
inputs: &IceAnswerInputs<'_>,
) -> Option<SessionDescription> {
let local_port = inputs.default_candidate.address.port();
let pending = {
let call = self.calls.get_mut(call_id)?;
call.take_pending_answer()?
};
let PendingAnswer {
mut offer,
negotiated,
} = pending;
let answer_direction = swap_direction(negotiated.direction);
let audio = offer
.media
.iter_mut()
.find(|m| m.media_type == crate::sdp::parser::MediaType::Audio)?;
audio.port = local_port;
audio.formats = vec![negotiated.codec.payload_type.to_string()];
audio.attributes = create_media_attributes(&negotiated.codec, answer_direction);
let audio_idx = offer
.media
.iter()
.position(|m| m.media_type == crate::sdp::parser::MediaType::Audio)
.expect("audio media present (just patched above)");
ice_attrs::apply_default_candidate(&mut offer, audio_idx, inputs.default_candidate);
let audio = offer.media.get_mut(audio_idx).expect("audio media present");
ice_attrs::write_ice_credentials(audio, &inputs.local.ufrag, &inputs.local.pwd);
ice_attrs::write_candidates(audio, &inputs.local.candidates);
ice_attrs::write_rtcp_mux(audio);
let call = self.calls.get_mut(call_id).expect("call exists");
if let Err(e) = call.set_negotiated_media(negotiated, local_port) {
tracing::warn!(error = %e, "build_answer_for: media session construction failed");
return None;
}
Some(offer)
}
fn create_inbound_call_pending(
&mut self,
dialog: Dialog,
pending: PendingAnswer,
) -> Option<CallId> {
let remote_uri = dialog.remote_uri().to_string();
let call = Call::new_inbound_pending(self.call_config.clone(), remote_uri, dialog, pending);
let call_id = call.id().clone();
self.calls.insert(call_id.clone(), call);
let call = self.calls.get(&call_id).expect("call inserted");
let dialog_id = call.dialog_id().expect("dialog id").clone();
self.dialog_to_call.insert(dialog_id, call_id.clone());
self.events
.push(ManagerEvent::IncomingCall(call_id.clone()));
Some(call_id)
}
fn create_inbound_call_internal(
&mut self,
dialog: Dialog,
media: NegotiatedMedia,
local_port: u16,
) -> Option<CallId> {
let remote_uri = dialog.remote_uri().to_string();
let call = Call::new_inbound(self.call_config.clone(), remote_uri, dialog);
let call_id = call.id().clone();
self.calls.insert(call_id.clone(), call);
let call = self.calls.get_mut(&call_id).expect("call inserted");
if let Err(e) = call.set_negotiated_media(media, local_port) {
tracing::warn!(error = %e, "rejecting INVITE: media session construction failed");
self.calls.remove(&call_id);
return None;
}
let dialog_id = call.dialog_id().expect("dialog id");
self.dialog_to_call
.insert(dialog_id.clone(), call_id.clone());
self.events
.push(ManagerEvent::IncomingCall(call_id.clone()));
Some(call_id)
}
pub fn handle_invite_success(
&mut self,
call_id: &CallId,
dialog: Dialog,
answer_sdp: &SessionDescription,
response: Option<&SipResponse>,
now: Instant,
) -> bool {
let negotiated = process_answer(answer_sdp);
let media = match negotiated.into_iter().next() {
Some(m) => m,
None => return false,
};
let local_port = self.allocate_rtp_port();
let call = match self.calls.get_mut(call_id) {
Some(c) => c,
None => return false,
};
let mut dialog = dialog;
if let Some(resp) = response {
let record_routes = resp.record_routes();
if !record_routes.is_empty() {
dialog.set_route_set_from_record_routes(&record_routes, true);
}
if let Some(contact) = resp.contact_uri() {
dialog.set_remote_target(contact.to_string());
}
}
call.set_dialog(dialog);
if let Err(e) = call.set_negotiated_media(media, local_port) {
tracing::warn!(error = %e, "200 OK media setup failed");
return false;
}
call.handle_answer();
if let Some(resp) = response {
Self::apply_invite_2xx_session_timer(call, resp, now);
}
let dialog_id = call.dialog_id().expect("dialog id");
self.dialog_to_call
.insert(dialog_id.clone(), call_id.clone());
self.events.push(ManagerEvent::CallStateChanged(
call_id.clone(),
CallState::Established,
));
true
}
fn apply_invite_2xx_session_timer(call: &mut Call, response: &SipResponse, now: Instant) {
let Some(se) = response.session_expires() else {
return;
};
let refresher = se.refresher.unwrap_or(Refresher::Uac);
let session_expires = Duration::from_secs(se.delta_seconds as u64);
call.session_expires = Some(session_expires);
call.refresher = Some(refresher);
match refresher {
Refresher::Uac => {
call.refresh_at = Some(now + session_expires / 2);
call.expiry_at = None;
}
Refresher::Uas => {
call.expiry_at = Some(now + session_expires);
call.refresh_at = None;
}
}
}
pub fn handle_provisional(
&mut self,
call_id: &CallId,
has_sdp: bool,
sdp: Option<&SessionDescription>,
) {
let local_port = self.allocate_rtp_port();
if let Some(call) = self.calls.get_mut(call_id) {
if has_sdp {
if let Some(answer_sdp) = sdp {
let negotiated = process_answer(answer_sdp);
if let Some(media) = negotiated.into_iter().next() {
if let Err(e) = call.set_negotiated_media(media, local_port) {
tracing::warn!(error = %e, "early-media setup failed");
}
}
}
}
call.handle_provisional(has_sdp);
self.events.push(ManagerEvent::CallStateChanged(
call_id.clone(),
call.state(),
));
}
}
pub fn handle_provisional_response(
&mut self,
call_id: &CallId,
response: &SipResponse,
sdp: Option<&SessionDescription>,
local_contact: &str,
) {
let has_sdp = sdp.is_some();
if response.to_tag().is_some() {
if let Some(call) = self.calls.get_mut(call_id) {
if let Some(dialog) = call.dialog_mut() {
dialog.populate_uac_from_response(response, local_contact.to_string());
}
}
}
self.handle_provisional(call_id, has_sdp, sdp);
}
pub fn populate_uas_dialog_routing(
&mut self,
call_id: &CallId,
invite: &SipRequest,
local_contact: String,
) {
if let Some(call) = self.calls.get_mut(call_id) {
if let Some(dialog) = call.dialog_mut() {
dialog.populate_uas_from_invite(invite, local_contact);
}
}
}
pub fn handle_invite_failure(&mut self, call_id: &CallId, status_code: u16) {
if let Some(call) = self.calls.get_mut(call_id) {
let reason = match status_code {
486 => CallEndReason::Busy,
480 | 408 => CallEndReason::NoAnswer,
603 => CallEndReason::Rejected,
_ => CallEndReason::Error,
};
call.handle_ended(reason);
self.events.push(ManagerEvent::CallEvent(
call_id.clone(),
CallEvent::Ended(reason),
));
}
}
pub fn invite_offer_headers(&self) -> InviteOfferHeaders {
let allow_methods = vec![
Method::Invite,
Method::Ack,
Method::Bye,
Method::Cancel,
Method::Options,
Method::Prack,
Method::Update,
];
if self.call_config.session_expires.is_zero() {
InviteOfferHeaders {
supported_tags: vec!["100rel".to_string()],
allow_methods,
session_expires: None,
min_se: None,
}
} else {
let se_secs = self.call_config.session_expires.as_secs() as u32;
let min_se_secs = self.call_config.min_se.as_secs() as u32;
InviteOfferHeaders {
supported_tags: vec!["timer".to_string(), "100rel".to_string()],
allow_methods,
session_expires: Some((se_secs, Refresher::Uac)),
min_se: Some(min_se_secs),
}
}
}
pub fn handle_provisional_reliable(
&mut self,
call_id: &CallId,
response: &SipResponse,
) -> Option<SipRequest> {
let _ = response.rseq()?;
let call = self.calls.get_mut(call_id)?;
let dialog = call.dialog_mut()?;
let mut invite_dialog = dialog.to_invite_dialog();
let prack = invite_dialog.build_prack(response);
let _ = dialog.next_cseq();
self.pending_outbound_requests.push(OutboundRequest {
call_id: call_id.clone(),
request: prack.clone(),
kind: OutboundRequestKind::Prack,
});
Some(prack)
}
pub fn evaluate_inbound_invite_session_timer(
&self,
request: &SipRequest,
) -> InboundSessionTimer {
let Some(se) = request.session_expires() else {
return InboundSessionTimer::Disabled;
};
let min_se_secs = self.call_config.min_se.as_secs() as u32;
if (se.delta_seconds as u64) < self.call_config.min_se.as_secs() {
return InboundSessionTimer::Reject422 {
min_se: min_se_secs,
};
}
let peer_supports_timer = request
.supported()
.map(|s| s.0.iter().any(|t| t.eq_ignore_ascii_case("timer")))
.unwrap_or(false);
let refresher = if peer_supports_timer {
Refresher::Uac
} else {
Refresher::Uas
};
InboundSessionTimer::Accept {
session_expires: Duration::from_secs(se.delta_seconds as u64),
refresher,
}
}
pub fn accept_session_timer(
&mut self,
call_id: &CallId,
session_expires: Duration,
refresher: Refresher,
now: Instant,
) {
if let Some(call) = self.calls.get_mut(call_id) {
call.session_expires = Some(session_expires);
call.refresher = Some(refresher);
match refresher {
Refresher::Uac => {
call.expiry_at = Some(now + session_expires);
call.refresh_at = None;
}
Refresher::Uas => {
call.refresh_at = Some(now + session_expires / 2);
call.expiry_at = None;
}
}
}
}
pub fn handle_inbound_update(
&mut self,
dialog_id: &DialogId,
request: &SipRequest,
now: Instant,
) -> Option<SipResponse> {
let call_id = self.dialog_to_call.get(dialog_id)?.clone();
let call = self.calls.get_mut(&call_id)?;
if let Some(se) = request.session_expires() {
let min_se_secs = self.call_config.min_se.as_secs();
if (se.delta_seconds as u64) < min_se_secs {
let resp = SipResponse::builder()
.status(422, "Session Interval Too Small")
.from_request(request)
.min_se(min_se_secs as u32)
.build()
.ok()?;
return Some(resp);
}
let new_se = Duration::from_secs(se.delta_seconds as u64);
call.session_expires = Some(new_se);
}
call.slide_deadlines(now);
let dialog = call.dialog()?;
let invite_dialog = dialog.to_invite_dialog();
Some(invite_dialog.handle_update(request))
}
pub fn slide_deadlines_for_dialog(&mut self, dialog_id: &DialogId, now: Instant) {
if let Some(call_id) = self.dialog_to_call.get(dialog_id).cloned() {
if let Some(call) = self.calls.get_mut(&call_id) {
call.slide_deadlines(now);
}
}
}
pub fn mark_in_dialog_2xx(&mut self, call_id: &CallId, method: Method, now: Instant) {
if !matches!(method, Method::Update | Method::Invite) {
return;
}
if let Some(call) = self.calls.get_mut(call_id) {
call.slide_deadlines(now);
}
}
pub fn set_uac_in_flight(&mut self, call_id: &CallId, in_flight: bool) {
if let Some(call) = self.calls.get_mut(call_id) {
call.uac_in_flight = in_flight;
}
}
pub fn note_update_unsupported(&mut self, call_id: &CallId) {
if let Some(call) = self.calls.get_mut(call_id) {
call.update_unsupported = true;
}
}
pub fn drain_outbound_requests(&mut self) -> Vec<OutboundRequest> {
std::mem::take(&mut self.pending_outbound_requests)
}
pub fn tick(&mut self, now: Instant) {
let ids: Vec<CallId> = self
.calls
.iter()
.filter(|(_, c)| c.state() == CallState::Established)
.map(|(id, _)| id.clone())
.collect();
for id in ids {
self.maybe_fire_refresh(&id, now);
self.maybe_fire_expiry_bye(&id, now);
}
}
pub fn next_deadline(&self) -> Option<Instant> {
self.calls
.values()
.filter(|c| c.state() == CallState::Established)
.filter_map(|c| match (c.refresh_at, c.expiry_at) {
(Some(r), Some(e)) => Some(r.min(e)),
(Some(r), None) => Some(r),
(None, Some(e)) => Some(e),
(None, None) => None,
})
.min()
}
fn maybe_fire_refresh(&mut self, call_id: &CallId, now: Instant) {
let (build_update, body) = {
let call = match self.calls.get(call_id) {
Some(c) => c,
None => return,
};
let Some(deadline) = call.refresh_at else {
return;
};
if deadline > now {
return;
}
if call.uac_in_flight {
return;
}
let Some(_) = call.dialog() else { return };
let use_update = !call.update_unsupported;
let body = if use_update {
None
} else {
Some(self.build_refresh_invite_sdp(call_id))
};
(use_update, body)
};
let request = if build_update {
self.build_update_request(call_id)
} else {
self.build_reinvite_refresh_request(call_id, body.flatten())
};
let Some(request) = request else { return };
let kind = if build_update {
OutboundRequestKind::SessionTimerUpdate
} else {
OutboundRequestKind::SessionTimerReInvite
};
if let Some(call) = self.calls.get_mut(call_id) {
if let Some(se) = call.session_expires {
call.refresh_at = Some(now + se / 2);
}
}
self.pending_outbound_requests.push(OutboundRequest {
call_id: call_id.clone(),
request,
kind,
});
}
fn maybe_fire_expiry_bye(&mut self, call_id: &CallId, now: Instant) {
let should_fire = {
let call = match self.calls.get(call_id) {
Some(c) => c,
None => return,
};
matches!(call.expiry_at, Some(deadline) if deadline <= now)
};
if !should_fire {
return;
}
let request = match self.build_expiry_bye_request(call_id) {
Some(r) => r,
None => return,
};
if let Some(call) = self.calls.get_mut(call_id) {
call.expiry_at = None;
call.set_state(CallState::Terminating);
}
self.pending_outbound_requests.push(OutboundRequest {
call_id: call_id.clone(),
request,
kind: OutboundRequestKind::SessionTimerExpiryBye,
});
self.events.push(ManagerEvent::CallStateChanged(
call_id.clone(),
CallState::Terminating,
));
}
fn build_update_request(&mut self, call_id: &CallId) -> Option<SipRequest> {
let call = self.calls.get_mut(call_id)?;
let se = call.session_expires?;
let dialog = call.dialog_mut()?;
let mut invite_dialog = dialog.to_invite_dialog();
let update = invite_dialog.build_update(Some(se.as_secs() as u32));
let _ = dialog.next_cseq();
Some(update)
}
fn build_reinvite_refresh_request(
&mut self,
call_id: &CallId,
body: Option<Vec<u8>>,
) -> Option<SipRequest> {
use crate::dialog::DialogState;
let call = self.calls.get_mut(call_id)?;
let se = call.session_expires?;
let dialog = call.dialog_mut()?;
let info = dialog.to_invite_dialog().info().clone();
let _ = (DialogState::Confirmed,); let next_cseq = dialog.next_cseq();
let request_uri = if info.remote_target.is_empty() {
info.remote_uri.clone()
} else {
info.remote_target.clone()
};
let routes = info.route_set.routes();
let branch = format!("z9hG4bK{}", uuid::Uuid::new_v4().simple());
let mut builder = SipRequest::builder()
.method(Method::Invite)
.uri(&request_uri)
.via(&self.config.local_rtp_addr, 5060, "UDP", &branch)
.from(&info.local_uri, &info.id.local_tag)
.to(&info.remote_uri)
.to_tag(&info.id.remote_tag)
.call_id(&info.id.call_id)
.cseq(next_cseq)
.max_forwards(70)
.route(routes)
.session_expires(se.as_secs() as u32, Some(Refresher::Uac))
.supported(&["timer"])
.allow(&[
Method::Invite,
Method::Ack,
Method::Bye,
Method::Cancel,
Method::Options,
Method::Prack,
Method::Update,
]);
if !info.local_contact.is_empty() {
builder = builder.contact(&info.local_contact);
}
if let Some(body_bytes) = body {
builder = builder.body(body_bytes, "application/sdp");
}
builder.build().ok()
}
fn build_refresh_invite_sdp(&self, call_id: &CallId) -> Option<Vec<u8>> {
use crate::sdp::builder::{MediaBuilder, SdpBuilder};
let call = self.calls.get(call_id)?;
let media = call.media()?;
let codec = call.codec()?;
let local_addr: std::net::IpAddr = self.config.local_rtp_addr.parse().ok()?;
let local_port = media.local_port();
let media_builder = match codec.encoding.to_uppercase().as_str() {
"PCMU" => MediaBuilder::audio(local_port).pcmu(),
"PCMA" => MediaBuilder::audio(local_port).pcma(),
"G722" => MediaBuilder::audio(local_port).g722(),
other => MediaBuilder::audio(local_port).codec(
codec.payload_type,
other,
codec.clock_rate,
Some(codec.channels),
),
};
let sdp = SdpBuilder::new(local_addr)
.session_name("rsiprtp refresh")
.add_media(media_builder)
.build();
Some(sdp.to_string().into_bytes())
}
fn build_expiry_bye_request(&mut self, call_id: &CallId) -> Option<SipRequest> {
let call = self.calls.get_mut(call_id)?;
let dialog = call.dialog_mut()?;
let mut invite_dialog = dialog.to_invite_dialog();
let bye =
invite_dialog.build_bye_with_reason(r#"SIP;cause=200;text="Session timer expired""#)?;
let _ = dialog.next_cseq();
Some(bye)
}
pub fn handle_bye(&mut self, dialog_id: &DialogId) {
if let Some(call_id) = self.dialog_to_call.get(dialog_id).cloned() {
if let Some(call) = self.calls.get_mut(&call_id) {
call.handle_ended(CallEndReason::NormalClearing);
self.events.push(ManagerEvent::CallEvent(
call_id,
CallEvent::Ended(CallEndReason::NormalClearing),
));
}
}
}
pub fn terminate_call(&mut self, call_id: &CallId) -> Option<DialogId> {
let call = self.calls.get_mut(call_id)?;
if call.state() != CallState::Established {
return None;
}
call.set_state(CallState::Terminating);
call.dialog_id().cloned()
}
pub fn remove_call(&mut self, call_id: &CallId) {
if let Some(call) = self.calls.remove(call_id) {
if let Some(dialog_id) = call.dialog_id() {
self.dialog_to_call.remove(dialog_id);
}
}
}
pub fn answer_call(&mut self, call_id: &CallId) -> bool {
if let Some(call) = self.calls.get_mut(call_id) {
if call.direction() == CallDirection::Inbound && call.state() == CallState::Ringing {
call.handle_answer();
self.events.push(ManagerEvent::CallStateChanged(
call_id.clone(),
CallState::Established,
));
return true;
}
}
false
}
pub fn reject_call(&mut self, call_id: &CallId) -> Option<DialogId> {
if let Some(call) = self.calls.get_mut(call_id) {
if call.direction() == CallDirection::Inbound && call.state() == CallState::Ringing {
call.handle_ended(CallEndReason::Rejected);
return call.dialog_id().cloned();
}
}
None
}
pub fn drain_events(&mut self) -> Vec<ManagerEvent> {
std::mem::take(&mut self.events)
}
pub fn active_calls(&self) -> Vec<CallId> {
self.calls
.iter()
.filter(|(_, call)| call.is_active())
.map(|(id, _)| id.clone())
.collect()
}
pub fn codecs(&self) -> &[Codec] {
&self.call_config.codecs
}
pub fn local_rtp_addr(&self) -> &str {
&self.config.local_rtp_addr
}
}
fn swap_direction(d: Direction) -> Direction {
match d {
Direction::SendRecv => Direction::SendRecv,
Direction::SendOnly => Direction::RecvOnly,
Direction::RecvOnly => Direction::SendOnly,
Direction::Inactive => Direction::Inactive,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_sdp() -> SessionDescription {
let sdp = r#"v=0
o=- 123 1 IN IP4 192.168.1.1
s=-
c=IN IP4 192.168.1.1
t=0 0
m=audio 5000 RTP/AVP 0 8
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=sendrecv
"#;
SessionDescription::parse(sdp).unwrap()
}
fn test_video_only_sdp() -> SessionDescription {
let sdp = r#"v=0
o=- 123 1 IN IP4 192.168.1.1
s=-
c=IN IP4 192.168.1.1
t=0 0
m=video 5000 RTP/AVP 96
a=rtpmap:96 H264/90000
"#;
SessionDescription::parse(sdp).unwrap()
}
#[test]
fn test_manager_event_debug() {
let event = ManagerEvent::IncomingCall(CallId::new());
let debug = format!("{:?}", event);
assert!(debug.contains("IncomingCall"));
let event = ManagerEvent::CallStateChanged(CallId::new(), CallState::Established);
let debug = format!("{:?}", event);
assert!(debug.contains("CallStateChanged"));
let event = ManagerEvent::CallEvent(
CallId::new(),
CallEvent::Ended(CallEndReason::NormalClearing),
);
let debug = format!("{:?}", event);
assert!(debug.contains("CallEvent"));
let event = ManagerEvent::Error("test error".to_string());
let debug = format!("{:?}", event);
assert!(debug.contains("Error"));
}
#[test]
fn test_manager_config_default() {
let config = ManagerConfig::default();
assert_eq!(config.local_sip_addr, "127.0.0.1:5060");
assert_eq!(config.local_rtp_addr, "127.0.0.1");
assert_eq!(config.rtp_port_range, (10000, 20000));
}
#[test]
fn test_manager_config_debug() {
let config = ManagerConfig::default();
let debug = format!("{:?}", config);
assert!(debug.contains("ManagerConfig"));
}
#[test]
fn test_manager_config_clone() {
let config = ManagerConfig::default();
let cloned = config.clone();
assert_eq!(cloned.local_sip_addr, "127.0.0.1:5060");
}
#[test]
fn test_create_outbound_call() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
assert_eq!(manager.call_count(), 1);
let call = manager.get_call(&call_id).unwrap();
assert_eq!(call.state(), CallState::Idle);
assert_eq!(call.direction(), CallDirection::Outbound);
}
#[test]
fn test_get_call_nonexistent() {
let manager = CallManager::new(ManagerConfig::default());
let fake_id = CallId::new();
assert!(manager.get_call(&fake_id).is_none());
}
#[test]
fn test_get_call_mut() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
let call = manager.get_call_mut(&call_id);
assert!(call.is_some());
}
#[test]
fn test_get_call_mut_nonexistent() {
let mut manager = CallManager::new(ManagerConfig::default());
let fake_id = CallId::new();
assert!(manager.get_call_mut(&fake_id).is_none());
}
#[test]
fn test_get_call_by_dialog() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
let dialog = Dialog::new_uac(
"call-123".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let answer_sdp = test_sdp();
manager.handle_invite_success(&call_id, dialog, &answer_sdp, None, Instant::now());
let dialog_id = manager
.get_call(&call_id)
.unwrap()
.dialog_id()
.unwrap()
.clone();
let call = manager.get_call_by_dialog(&dialog_id);
assert!(call.is_some());
}
#[test]
fn test_get_call_by_dialog_nonexistent() {
let manager = CallManager::new(ManagerConfig::default());
let fake_dialog_id =
DialogId::new("call-id".to_string(), "from".to_string(), "to".to_string());
assert!(manager.get_call_by_dialog(&fake_dialog_id).is_none());
}
#[test]
fn test_handle_incoming_invite() {
let mut manager = CallManager::new(ManagerConfig::default());
let dialog = Dialog::new_uas(
"call-123".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let offer_sdp = test_sdp();
let result = manager.handle_incoming_invite(dialog, &offer_sdp);
assert!(result.is_some());
let (call_id, answer_sdp, _port) = result.unwrap();
assert_eq!(manager.call_count(), 1);
let call = manager.get_call(&call_id).unwrap();
assert_eq!(call.direction(), CallDirection::Inbound);
assert_eq!(call.state(), CallState::Ringing);
let audio = answer_sdp.audio_media().unwrap();
assert!(audio.port >= 10000);
}
#[test]
fn test_handle_incoming_invite_no_compatible_media() {
let mut manager = CallManager::new(ManagerConfig::default());
let dialog = Dialog::new_uas(
"call-124".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let offer_sdp = test_video_only_sdp();
let result = manager.handle_incoming_invite(dialog, &offer_sdp);
assert!(result.is_none());
assert_eq!(manager.call_count(), 0);
}
#[test]
fn test_handle_invite_success() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
let dialog = Dialog::new_uac(
"call-123".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let answer_sdp = test_sdp();
let result =
manager.handle_invite_success(&call_id, dialog, &answer_sdp, None, Instant::now());
assert!(result);
let call = manager.get_call(&call_id).unwrap();
assert_eq!(call.state(), CallState::Established);
assert!(call.media().is_some());
}
#[test]
fn test_handle_invite_success_no_media() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
let dialog = Dialog::new_uac(
"call-123".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let answer_sdp = test_video_only_sdp();
let result =
manager.handle_invite_success(&call_id, dialog, &answer_sdp, None, Instant::now());
assert!(!result);
}
#[test]
fn test_handle_invite_success_nonexistent_call() {
let mut manager = CallManager::new(ManagerConfig::default());
let fake_id = CallId::new();
let dialog = Dialog::new_uac(
"call-123".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let answer_sdp = test_sdp();
let result =
manager.handle_invite_success(&fake_id, dialog, &answer_sdp, None, Instant::now());
assert!(!result);
}
#[test]
fn test_handle_provisional() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
manager.handle_provisional(&call_id, false, None);
let call = manager.get_call(&call_id).unwrap();
assert_eq!(call.state(), CallState::Ringing);
}
#[test]
fn test_handle_provisional_with_early_media() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
let early_sdp = test_sdp();
manager.handle_provisional(&call_id, true, Some(&early_sdp));
let call = manager.get_call(&call_id).unwrap();
assert_eq!(call.state(), CallState::EarlyMedia);
}
#[test]
fn test_handle_provisional_missing_sdp_body() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
manager.handle_provisional(&call_id, true, None);
let call = manager.get_call(&call_id).unwrap();
assert_eq!(call.state(), CallState::EarlyMedia);
assert!(call.media().is_none());
}
#[test]
fn test_handle_provisional_unmatched_media() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
let video_sdp = test_video_only_sdp();
manager.handle_provisional(&call_id, true, Some(&video_sdp));
let call = manager.get_call(&call_id).unwrap();
assert_eq!(call.state(), CallState::EarlyMedia);
assert!(call.media().is_none());
}
#[test]
fn test_handle_provisional_nonexistent() {
let mut manager = CallManager::new(ManagerConfig::default());
let fake_id = CallId::new();
manager.handle_provisional(&fake_id, false, None);
}
#[test]
fn test_handle_invite_failure_486() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
manager.handle_invite_failure(&call_id, 486);
let call = manager.get_call(&call_id).unwrap();
assert_eq!(call.state(), CallState::Terminated);
}
#[test]
fn test_handle_invite_failure_480() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
manager.handle_invite_failure(&call_id, 480);
let call = manager.get_call(&call_id).unwrap();
assert_eq!(call.state(), CallState::Terminated);
}
#[test]
fn test_handle_invite_failure_408() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
manager.handle_invite_failure(&call_id, 408);
let call = manager.get_call(&call_id).unwrap();
assert_eq!(call.state(), CallState::Terminated);
}
#[test]
fn test_handle_invite_failure_603() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
manager.handle_invite_failure(&call_id, 603);
let call = manager.get_call(&call_id).unwrap();
assert_eq!(call.state(), CallState::Terminated);
}
#[test]
fn test_handle_invite_failure_other() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
manager.handle_invite_failure(&call_id, 500);
let call = manager.get_call(&call_id).unwrap();
assert_eq!(call.state(), CallState::Terminated);
}
#[test]
fn test_handle_invite_failure_nonexistent() {
let mut manager = CallManager::new(ManagerConfig::default());
let fake_id = CallId::new();
manager.handle_invite_failure(&fake_id, 486);
}
#[test]
fn test_handle_bye() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
let dialog = Dialog::new_uac(
"call-123".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let answer_sdp = test_sdp();
manager.handle_invite_success(&call_id, dialog, &answer_sdp, None, Instant::now());
let dialog_id = manager
.get_call(&call_id)
.unwrap()
.dialog_id()
.cloned()
.unwrap();
manager.handle_bye(&dialog_id);
let call = manager.get_call(&call_id).unwrap();
assert_eq!(call.state(), CallState::Terminated);
}
#[test]
fn test_handle_bye_nonexistent_dialog() {
let mut manager = CallManager::new(ManagerConfig::default());
let fake_dialog_id =
DialogId::new("call-id".to_string(), "from".to_string(), "to".to_string());
manager.handle_bye(&fake_dialog_id);
}
#[test]
fn test_handle_bye_missing_call_entry() {
let mut manager = CallManager::new(ManagerConfig::default());
let dialog_id = DialogId::new("call-id".to_string(), "from".to_string(), "to".to_string());
let call_id = CallId::new();
manager.dialog_to_call.insert(dialog_id.clone(), call_id);
manager.handle_bye(&dialog_id);
assert!(manager.events.is_empty());
}
#[test]
fn test_terminate_call() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
let dialog = Dialog::new_uac(
"call-123".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let answer_sdp = test_sdp();
manager.handle_invite_success(&call_id, dialog, &answer_sdp, None, Instant::now());
let dialog_id = manager.terminate_call(&call_id);
assert!(dialog_id.is_some());
let call = manager.get_call(&call_id).unwrap();
assert_eq!(call.state(), CallState::Terminating);
}
#[test]
fn test_terminate_call_not_established() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
let result = manager.terminate_call(&call_id);
assert!(result.is_none());
}
#[test]
fn test_terminate_call_nonexistent() {
let mut manager = CallManager::new(ManagerConfig::default());
let fake_id = CallId::new();
let result = manager.terminate_call(&fake_id);
assert!(result.is_none());
}
#[test]
fn test_remove_call() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
assert_eq!(manager.call_count(), 1);
manager.remove_call(&call_id);
assert_eq!(manager.call_count(), 0);
}
#[test]
fn test_remove_call_with_dialog() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
let dialog = Dialog::new_uac(
"call-123".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let answer_sdp = test_sdp();
manager.handle_invite_success(&call_id, dialog, &answer_sdp, None, Instant::now());
let dialog_id = manager
.get_call(&call_id)
.unwrap()
.dialog_id()
.unwrap()
.clone();
manager.remove_call(&call_id);
assert!(manager.get_call_by_dialog(&dialog_id).is_none());
}
#[test]
fn test_remove_call_nonexistent() {
let mut manager = CallManager::new(ManagerConfig::default());
let fake_id = CallId::new();
manager.remove_call(&fake_id);
}
#[test]
fn test_answer_call() {
let mut manager = CallManager::new(ManagerConfig::default());
let dialog = Dialog::new_uas(
"call-123".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let offer_sdp = test_sdp();
let (call_id, _, _) = manager.handle_incoming_invite(dialog, &offer_sdp).unwrap();
let result = manager.answer_call(&call_id);
assert!(result);
let call = manager.get_call(&call_id).unwrap();
assert_eq!(call.state(), CallState::Established);
}
#[test]
fn test_answer_call_inbound_not_ringing() {
let mut manager = CallManager::new(ManagerConfig::default());
let dialog = Dialog::new_uas(
"call-124".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let offer_sdp = test_sdp();
let (call_id, _, _) = manager.handle_incoming_invite(dialog, &offer_sdp).unwrap();
let call = manager.get_call_mut(&call_id).unwrap();
call.set_state(CallState::Established);
let result = manager.answer_call(&call_id);
assert!(!result);
}
#[test]
fn test_answer_call_outbound() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
let result = manager.answer_call(&call_id);
assert!(!result);
}
#[test]
fn test_answer_call_nonexistent() {
let mut manager = CallManager::new(ManagerConfig::default());
let fake_id = CallId::new();
let result = manager.answer_call(&fake_id);
assert!(!result);
}
#[test]
fn test_reject_call() {
let mut manager = CallManager::new(ManagerConfig::default());
let dialog = Dialog::new_uas(
"call-123".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let offer_sdp = test_sdp();
let (call_id, _, _) = manager.handle_incoming_invite(dialog, &offer_sdp).unwrap();
let result = manager.reject_call(&call_id);
assert!(result.is_some());
let call = manager.get_call(&call_id).unwrap();
assert_eq!(call.state(), CallState::Terminated);
}
#[test]
fn test_reject_call_inbound_not_ringing() {
let mut manager = CallManager::new(ManagerConfig::default());
let dialog = Dialog::new_uas(
"call-125".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let offer_sdp = test_sdp();
let (call_id, _, _) = manager.handle_incoming_invite(dialog, &offer_sdp).unwrap();
let call = manager.get_call_mut(&call_id).unwrap();
call.set_state(CallState::Established);
let result = manager.reject_call(&call_id);
assert!(result.is_none());
}
#[test]
fn test_reject_call_outbound() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
let result = manager.reject_call(&call_id);
assert!(result.is_none());
}
#[test]
fn test_reject_call_nonexistent() {
let mut manager = CallManager::new(ManagerConfig::default());
let fake_id = CallId::new();
let result = manager.reject_call(&fake_id);
assert!(result.is_none());
}
#[test]
fn test_active_calls() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id1 = manager.create_call("sip:bob@example.com".to_string());
let call_id2 = manager.create_call("sip:carol@example.com".to_string());
let active = manager.active_calls();
assert_eq!(active.len(), 0);
let call = manager.get_call_mut(&call_id1).expect("call exists");
call.set_state(CallState::Established);
let active = manager.active_calls();
assert_eq!(active.len(), 1);
assert!(active.contains(&call_id1));
let call = manager.get_call_mut(&call_id2).expect("call exists");
call.set_state(CallState::Established);
let active = manager.active_calls();
assert_eq!(active.len(), 2);
}
#[test]
fn test_codecs() {
let manager = CallManager::new(ManagerConfig::default());
let codecs = manager.codecs();
assert!(!codecs.is_empty());
}
#[test]
fn test_local_rtp_addr() {
let manager = CallManager::new(ManagerConfig::default());
assert_eq!(manager.local_rtp_addr(), "127.0.0.1");
}
#[test]
fn test_port_allocation() {
let mut manager = CallManager::new(ManagerConfig::default());
let port1 = manager.allocate_rtp_port();
let port2 = manager.allocate_rtp_port();
assert_eq!(port1, 10000);
assert_eq!(port2, 10002);
}
#[test]
fn test_port_allocation_wrapping() {
let config = ManagerConfig {
rtp_port_range: (10000, 10004),
..Default::default()
};
let mut manager = CallManager::new(config);
let port1 = manager.allocate_rtp_port();
let port2 = manager.allocate_rtp_port();
let port3 = manager.allocate_rtp_port();
assert_eq!(port1, 10000);
assert_eq!(port2, 10002);
assert_eq!(port3, 10004);
let port4 = manager.allocate_rtp_port();
assert_eq!(port4, 10000);
}
#[test]
fn test_events() {
let mut manager = CallManager::new(ManagerConfig::default());
let dialog = Dialog::new_uas(
"call-123".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let offer_sdp = test_sdp();
manager.handle_incoming_invite(dialog, &offer_sdp);
let events = manager.drain_events();
assert!(!events.is_empty());
assert!(events
.iter()
.any(|e| matches!(e, ManagerEvent::IncomingCall(_))));
let events2 = manager.drain_events();
assert!(events2.is_empty());
}
#[test]
fn test_events_call_state_changed() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
let dialog = Dialog::new_uac(
"call-123".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let answer_sdp = test_sdp();
manager.handle_invite_success(&call_id, dialog, &answer_sdp, None, Instant::now());
let events = manager.drain_events();
assert!(events
.iter()
.any(|e| matches!(e, ManagerEvent::CallStateChanged(_, CallState::Established))));
}
use crate::ice::Candidate;
use crate::session::ice_session::IceLocalParams;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
fn host_candidate(ip: [u8; 4], port: u16) -> Candidate {
Candidate::host(
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3])), port),
1,
)
}
fn ice_local(host: &Candidate) -> IceLocalParams {
IceLocalParams {
ufrag: "abc1234".to_string(),
pwd: "0123456789abcdef01234567".to_string(),
candidates: vec![host.clone()],
}
}
#[test]
fn accept_inbound_invite_creates_call_without_answer() {
let mut manager = CallManager::new(ManagerConfig::default());
let dialog = Dialog::new_uas(
"call-300".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let offer_sdp = test_sdp();
let call_id = manager
.accept_inbound_invite(dialog, &offer_sdp)
.expect("accept_inbound_invite");
assert_eq!(manager.call_count(), 1);
let call = manager.get_call(&call_id).expect("call exists");
assert_eq!(call.direction(), CallDirection::Inbound);
assert_eq!(call.state(), CallState::Ringing);
assert!(
call.media().is_none(),
"media must not be attached until build_answer_for runs"
);
let dialog_id = call.dialog_id().expect("dialog id").clone();
assert!(manager.get_call_by_dialog(&dialog_id).is_some());
let events = manager.drain_events();
assert!(events
.iter()
.any(|e| matches!(e, ManagerEvent::IncomingCall(_))));
}
#[test]
fn accept_inbound_invite_no_compatible_media() {
let mut manager = CallManager::new(ManagerConfig::default());
let dialog = Dialog::new_uas(
"call-301".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let offer_sdp = test_video_only_sdp();
let result = manager.accept_inbound_invite(dialog, &offer_sdp);
assert!(result.is_none());
assert_eq!(manager.call_count(), 0);
}
#[test]
fn build_answer_for_emits_ice_attrs() {
let mut manager = CallManager::new(ManagerConfig::default());
let dialog = Dialog::new_uas(
"call-302".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let offer_sdp = test_sdp();
let call_id = manager
.accept_inbound_invite(dialog, &offer_sdp)
.expect("accept_inbound_invite");
let host = host_candidate([10, 0, 0, 5], 7100);
let local = ice_local(&host);
let inputs = IceAnswerInputs::new(&host, &local);
let answer = manager
.build_answer_for(&call_id, &inputs)
.expect("build_answer_for");
let conn = answer.connection.as_ref().expect("session-level c=");
assert_eq!(conn.address, "192.168.1.1");
let audio = answer.audio_media().expect("audio media");
assert_eq!(audio.port, 7100);
let mconn = audio.connection.as_ref().expect("media-level c=");
assert_eq!(mconn.address, "10.0.0.5");
assert_eq!(mconn.addr_type, "IP4");
let (ufrag, pwd) =
ice_attrs::read_ice_credentials(audio).expect("ICE credentials on answer");
assert_eq!(ufrag, "abc1234");
assert_eq!(pwd, "0123456789abcdef01234567");
let cands = ice_attrs::read_candidates(audio);
assert_eq!(cands, vec![host.clone()]);
assert!(ice_attrs::read_rtcp_mux(audio));
let call = manager.get_call(&call_id).expect("call exists");
assert!(call.media().is_some(), "media wired up after answer build");
assert_eq!(call.media().unwrap().local_port(), 7100);
}
#[test]
fn build_answer_for_unknown_call() {
let mut manager = CallManager::new(ManagerConfig::default());
let host = host_candidate([10, 0, 0, 5], 7100);
let local = ice_local(&host);
let inputs = IceAnswerInputs::new(&host, &local);
let fake = CallId::new();
let answer = manager.build_answer_for(&fake, &inputs);
assert!(answer.is_none());
}
#[test]
fn build_answer_for_called_twice() {
let mut manager = CallManager::new(ManagerConfig::default());
let dialog = Dialog::new_uas(
"call-303".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let offer_sdp = test_sdp();
let call_id = manager
.accept_inbound_invite(dialog, &offer_sdp)
.expect("accept_inbound_invite");
let host = host_candidate([10, 0, 0, 5], 7100);
let local = ice_local(&host);
let inputs = IceAnswerInputs::new(&host, &local);
assert!(manager.build_answer_for(&call_id, &inputs).is_some());
assert!(
manager.build_answer_for(&call_id, &inputs).is_none(),
"second call returns None — pending answer cleared"
);
}
#[test]
fn accept_then_reject_inbound_invite_terminates_cleanly() {
let mut manager = CallManager::new(ManagerConfig::default());
let dialog = Dialog::new_uas(
"call-304".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let offer_sdp = test_sdp();
let call_id = manager
.accept_inbound_invite(dialog, &offer_sdp)
.expect("accept_inbound_invite");
let _ = manager.drain_events();
assert!(
manager.terminate_call(&call_id).is_none(),
"terminate_call must not act on a pending inbound call"
);
let dialog_id = manager
.reject_inbound_invite(&call_id)
.expect("reject_inbound_invite returns dialog id");
{
let call = manager.get_call(&call_id).expect("call still present");
assert_eq!(call.state(), CallState::Terminated);
assert!(
!call.has_pending_answer(),
"pending answer must be cleared on rejection"
);
assert_eq!(call.dialog_id(), Some(&dialog_id));
}
let events = manager.drain_events();
assert!(events.iter().any(|e| matches!(
e,
ManagerEvent::CallEvent(_, CallEvent::Ended(CallEndReason::Error))
)));
assert!(manager.reject_inbound_invite(&call_id).is_none());
}
#[test]
fn reject_inbound_invite_after_build_is_noop() {
let mut manager = CallManager::new(ManagerConfig::default());
let dialog = Dialog::new_uas(
"call-305".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let offer_sdp = test_sdp();
let call_id = manager
.accept_inbound_invite(dialog, &offer_sdp)
.expect("accept_inbound_invite");
let host = host_candidate([10, 0, 0, 5], 7100);
let local = ice_local(&host);
let inputs = IceAnswerInputs::new(&host, &local);
manager
.build_answer_for(&call_id, &inputs)
.expect("build_answer_for");
assert!(
manager.reject_inbound_invite(&call_id).is_none(),
"reject must be a no-op once the answer is built"
);
}
#[test]
fn reject_inbound_invite_unknown_call() {
let mut manager = CallManager::new(ManagerConfig::default());
let fake = CallId::new();
assert!(manager.reject_inbound_invite(&fake).is_none());
}
#[test]
fn reject_inbound_invite_on_normal_ringing_call_is_noop() {
let mut manager = CallManager::new(ManagerConfig::default());
let dialog = Dialog::new_uas(
"call-306".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let offer_sdp = test_sdp();
let (call_id, _, _) = manager
.handle_incoming_invite(dialog, &offer_sdp)
.expect("handle_incoming_invite");
assert!(manager.reject_inbound_invite(&call_id).is_none());
}
#[test]
fn inbound_ice_full_flow_unit() {
let mut manager = CallManager::new(ManagerConfig::default());
let dialog = Dialog::new_uas(
"call-307".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let offer_sdp = test_sdp();
let call_id = manager
.accept_inbound_invite(dialog, &offer_sdp)
.expect("accept_inbound_invite");
let host = host_candidate([10, 0, 0, 5], 7100);
let local = ice_local(&host);
let inputs = IceAnswerInputs::new(&host, &local);
let answer = manager
.build_answer_for(&call_id, &inputs)
.expect("build_answer_for");
let answer_str = answer.to_string();
let parsed = SessionDescription::parse(&answer_str).expect("answer SDP parses");
let mconn = parsed
.audio_media()
.expect("audio media")
.connection
.as_ref()
.expect("media-level c=");
assert_eq!(mconn.address, "10.0.0.5");
assert_eq!(mconn.addr_type, "IP4");
let audio = parsed.audio_media().expect("audio media");
assert_eq!(audio.port, 7100);
let (ufrag, pwd) = ice_attrs::read_ice_credentials(audio).expect("ICE credentials parsed");
assert_eq!(ufrag, "abc1234");
assert_eq!(pwd, "0123456789abcdef01234567");
let cands = ice_attrs::read_candidates(audio);
assert_eq!(cands, vec![host.clone()]);
assert!(ice_attrs::read_rtcp_mux(audio));
let rtpmaps = audio.rtpmaps();
assert!(
rtpmaps.iter().any(|r| r.encoding == "PCMU"),
"answer must carry PCMU rtpmap"
);
let call = manager.get_call(&call_id).expect("call");
assert_eq!(call.state(), CallState::Ringing);
assert!(manager.answer_call(&call_id));
let call = manager.get_call(&call_id).expect("call");
assert_eq!(call.state(), CallState::Established);
assert!(call.media().is_some(), "media must be wired after build");
assert_eq!(call.media().unwrap().local_port(), 7100);
}
fn established_outbound_call(manager: &mut CallManager) -> CallId {
let call_id = manager.create_call("sip:bob@example.com".to_string());
let dialog = Dialog::new_uac(
"call-st-1".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let answer_sdp = test_sdp();
manager.handle_invite_success(&call_id, dialog, &answer_sdp, None, Instant::now());
let _ = manager.drain_events();
call_id
}
#[test]
fn invite_offer_headers_default_emits_timer_and_min_se() {
let manager = CallManager::new(ManagerConfig::default());
let h = manager.invite_offer_headers();
assert!(h.supported_tags.iter().any(|t| t == "timer"));
assert!(h.supported_tags.iter().any(|t| t == "100rel"));
assert!(h.allow_methods.contains(&Method::Prack));
assert!(h.allow_methods.contains(&Method::Update));
assert_eq!(h.session_expires.unwrap().0, 1800);
assert!(matches!(h.session_expires.unwrap().1, Refresher::Uac));
assert_eq!(h.min_se, Some(90));
}
#[test]
fn invite_offer_headers_zero_session_expires_suppresses_timer() {
let mut cfg = ManagerConfig::default();
cfg.call_config.session_expires = Duration::ZERO;
let manager = CallManager::new(cfg);
let h = manager.invite_offer_headers();
assert!(!h.supported_tags.iter().any(|t| t == "timer"));
assert!(h.supported_tags.iter().any(|t| t == "100rel"));
assert!(h.session_expires.is_none());
assert!(h.min_se.is_none());
assert!(h.allow_methods.contains(&Method::Prack));
}
fn establish_with_2xx(
manager: &mut CallManager,
response: &SipResponse,
now: Instant,
) -> CallId {
let call_id = manager.create_call("sip:bob@example.com".to_string());
let dialog = Dialog::new_uac(
"call-st-1".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let answer_sdp = test_sdp();
manager.handle_invite_success(&call_id, dialog, &answer_sdp, Some(response), now);
let _ = manager.drain_events();
call_id
}
#[test]
fn handle_invite_success_uac_refresher_sets_refresh_at() {
let mut manager = CallManager::new(ManagerConfig::default());
let response = SipResponse::builder()
.status(200, "OK")
.session_expires(60, Some(Refresher::Uac))
.build()
.expect("response");
let now = Instant::now();
let call_id = establish_with_2xx(&mut manager, &response, now);
let call = manager.get_call(&call_id).expect("call");
assert_eq!(call.session_expires, Some(Duration::from_secs(60)));
assert!(matches!(call.refresher, Some(Refresher::Uac)));
assert!(call.refresh_at.is_some());
assert!(call.expiry_at.is_none());
}
#[test]
fn handle_invite_success_uas_refresher_sets_expiry_at() {
let mut manager = CallManager::new(ManagerConfig::default());
let response = SipResponse::builder()
.status(200, "OK")
.session_expires(60, Some(Refresher::Uas))
.build()
.expect("response");
let now = Instant::now();
let call_id = establish_with_2xx(&mut manager, &response, now);
let call = manager.get_call(&call_id).expect("call");
assert_eq!(call.session_expires, Some(Duration::from_secs(60)));
assert!(call.refresh_at.is_none());
assert!(call.expiry_at.is_some());
}
#[test]
fn handle_invite_success_no_session_expires_leaves_timers_disabled() {
let mut manager = CallManager::new(ManagerConfig::default());
let response = SipResponse::builder()
.status(200, "OK")
.build()
.expect("response");
let call_id = establish_with_2xx(&mut manager, &response, Instant::now());
let call = manager.get_call(&call_id).expect("call");
assert!(call.session_expires.is_none());
assert!(call.refresh_at.is_none());
assert!(call.expiry_at.is_none());
}
#[test]
fn handle_invite_success_emits_state_change_in_single_call() {
let mut manager = CallManager::new(ManagerConfig::default());
let response = SipResponse::builder()
.status(200, "OK")
.session_expires(60, Some(Refresher::Uac))
.build()
.expect("response");
let call_id = manager.create_call("sip:bob@example.com".to_string());
let dialog = Dialog::new_uac(
"call-st-1".to_string(),
"from-tag".to_string(),
"to-tag".to_string(),
"sip:alice@example.com".to_string(),
"sip:bob@example.com".to_string(),
1,
);
let answer_sdp = test_sdp();
let now = Instant::now();
let ok = manager.handle_invite_success(&call_id, dialog, &answer_sdp, Some(&response), now);
assert!(ok);
let events = manager.drain_events();
assert!(events
.iter()
.any(|e| matches!(e, ManagerEvent::CallStateChanged(_, CallState::Established))));
let call = manager.get_call(&call_id).expect("call");
assert_eq!(call.session_expires, Some(Duration::from_secs(60)));
assert!(call.refresh_at.is_some());
}
#[test]
fn evaluate_inbound_invite_session_timer_disabled() {
let manager = CallManager::new(ManagerConfig::default());
let req = SipRequest::builder()
.method(Method::Invite)
.uri("sip:bob@example.com")
.via("192.168.1.1", 5060, "UDP", "z9hG4bKtest")
.from("sip:alice@example.com", "ftag")
.to("sip:bob@example.com")
.call_id("c@h")
.cseq(1)
.build()
.unwrap();
assert!(matches!(
manager.evaluate_inbound_invite_session_timer(&req),
InboundSessionTimer::Disabled
));
}
#[test]
fn evaluate_inbound_invite_session_timer_below_min_se_rejects_422() {
let manager = CallManager::new(ManagerConfig::default());
let req = SipRequest::builder()
.method(Method::Invite)
.uri("sip:bob@example.com")
.via("192.168.1.1", 5060, "UDP", "z9hG4bKtest")
.from("sip:alice@example.com", "ftag")
.to("sip:bob@example.com")
.call_id("c@h")
.cseq(1)
.session_expires(30, None)
.build()
.unwrap();
match manager.evaluate_inbound_invite_session_timer(&req) {
InboundSessionTimer::Reject422 { min_se } => assert_eq!(min_se, 90),
other => panic!("expected Reject422, got {:?}", other),
}
}
#[test]
fn evaluate_inbound_invite_session_timer_picks_uac_when_peer_supports() {
let manager = CallManager::new(ManagerConfig::default());
let req = SipRequest::builder()
.method(Method::Invite)
.uri("sip:bob@example.com")
.via("192.168.1.1", 5060, "UDP", "z9hG4bKtest")
.from("sip:alice@example.com", "ftag")
.to("sip:bob@example.com")
.call_id("c@h")
.cseq(1)
.session_expires(120, None)
.supported(&["timer", "100rel"])
.build()
.unwrap();
match manager.evaluate_inbound_invite_session_timer(&req) {
InboundSessionTimer::Accept {
session_expires,
refresher,
} => {
assert_eq!(session_expires, Duration::from_secs(120));
assert!(matches!(refresher, Refresher::Uac));
}
other => panic!("expected Accept, got {:?}", other),
}
}
#[test]
fn evaluate_inbound_invite_session_timer_picks_uas_when_peer_silent() {
let manager = CallManager::new(ManagerConfig::default());
let req = SipRequest::builder()
.method(Method::Invite)
.uri("sip:bob@example.com")
.via("192.168.1.1", 5060, "UDP", "z9hG4bKtest")
.from("sip:alice@example.com", "ftag")
.to("sip:bob@example.com")
.call_id("c@h")
.cseq(1)
.session_expires(120, None)
.build()
.unwrap();
match manager.evaluate_inbound_invite_session_timer(&req) {
InboundSessionTimer::Accept { refresher, .. } => {
assert!(matches!(refresher, Refresher::Uas));
}
other => panic!("expected Accept, got {:?}", other),
}
}
#[test]
fn tick_fires_refresh_when_we_are_refresher() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = established_outbound_call(&mut manager);
let now = Instant::now();
let past = now - Duration::from_secs(1);
{
let call = manager.get_call_mut(&call_id).unwrap();
call.session_expires = Some(Duration::from_secs(60));
call.refresher = Some(Refresher::Uac);
call.refresh_at = Some(past);
}
manager.tick(now);
let outbound = manager.drain_outbound_requests();
assert_eq!(outbound.len(), 1);
assert_eq!(outbound[0].kind, OutboundRequestKind::SessionTimerUpdate);
assert_eq!(outbound[0].request.method(), Method::Update);
manager.tick(now);
assert!(manager.drain_outbound_requests().is_empty());
}
#[test]
fn tick_uses_re_invite_when_update_unsupported() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = established_outbound_call(&mut manager);
let media = NegotiatedMedia {
codec: Codec::pcmu(),
remote_port: 6000,
remote_addr: Some("10.0.0.1".to_string()),
direction: Direction::SendRecv,
};
manager
.get_call_mut(&call_id)
.unwrap()
.set_negotiated_media(media, 5000)
.expect("media");
let now = Instant::now();
{
let call = manager.get_call_mut(&call_id).unwrap();
call.session_expires = Some(Duration::from_secs(60));
call.refresher = Some(Refresher::Uac);
call.refresh_at = Some(now - Duration::from_secs(1));
call.update_unsupported = true;
}
manager.tick(now);
let outbound = manager.drain_outbound_requests();
assert_eq!(outbound.len(), 1);
assert_eq!(outbound[0].kind, OutboundRequestKind::SessionTimerReInvite);
assert_eq!(outbound[0].request.method(), Method::Invite);
assert!(!outbound[0].request.body().is_empty());
}
#[test]
fn tick_skips_refresh_when_uac_in_flight() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = established_outbound_call(&mut manager);
let now = Instant::now();
{
let call = manager.get_call_mut(&call_id).unwrap();
call.session_expires = Some(Duration::from_secs(60));
call.refresher = Some(Refresher::Uac);
call.refresh_at = Some(now - Duration::from_secs(1));
call.uac_in_flight = true;
}
manager.tick(now);
assert!(manager.drain_outbound_requests().is_empty());
}
#[test]
fn tick_byes_call_when_peer_refresher_expired() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = established_outbound_call(&mut manager);
let now = Instant::now();
{
let call = manager.get_call_mut(&call_id).unwrap();
call.session_expires = Some(Duration::from_secs(60));
call.refresher = Some(Refresher::Uas);
call.expiry_at = Some(now - Duration::from_secs(1));
}
manager.tick(now);
let outbound = manager.drain_outbound_requests();
assert_eq!(outbound.len(), 1);
assert_eq!(outbound[0].kind, OutboundRequestKind::SessionTimerExpiryBye);
assert_eq!(outbound[0].request.method(), Method::Bye);
let call = manager.get_call(&call_id).unwrap();
assert_eq!(call.state(), CallState::Terminating);
manager.tick(now);
assert!(manager.drain_outbound_requests().is_empty());
}
#[test]
fn tick_skips_non_established_calls() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = manager.create_call("sip:bob@example.com".to_string());
let now = Instant::now();
{
let call = manager.get_call_mut(&call_id).unwrap();
call.session_expires = Some(Duration::from_secs(60));
call.refresher = Some(Refresher::Uac);
call.refresh_at = Some(now - Duration::from_secs(1));
}
manager.tick(now);
assert!(manager.drain_outbound_requests().is_empty());
}
#[test]
fn next_deadline_returns_soonest() {
let mut manager = CallManager::new(ManagerConfig::default());
let id1 = established_outbound_call(&mut manager);
let id2 = manager.create_call("sip:carol@example.com".to_string());
let dialog = Dialog::new_uac(
"call-st-2".to_string(),
"ftag2".to_string(),
"ttag2".to_string(),
"sip:alice@example.com".to_string(),
"sip:carol@example.com".to_string(),
1,
);
manager.handle_invite_success(&id2, dialog, &test_sdp(), None, Instant::now());
let _ = manager.drain_events();
let now = Instant::now();
manager.get_call_mut(&id1).unwrap().refresh_at = Some(now + Duration::from_secs(30));
manager.get_call_mut(&id2).unwrap().expiry_at = Some(now + Duration::from_secs(10));
let deadline = manager.next_deadline().expect("deadline");
assert_eq!(deadline, now + Duration::from_secs(10));
}
#[test]
fn next_deadline_none_when_no_timers() {
let manager = CallManager::new(ManagerConfig::default());
assert!(manager.next_deadline().is_none());
}
#[test]
fn slide_deadlines_for_dialog_refreshes_expiry() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = established_outbound_call(&mut manager);
let early = Instant::now();
{
let call = manager.get_call_mut(&call_id).unwrap();
call.session_expires = Some(Duration::from_secs(60));
call.refresher = Some(Refresher::Uas);
call.expiry_at = Some(early + Duration::from_secs(60));
}
let dialog_id = manager
.get_call(&call_id)
.unwrap()
.dialog_id()
.cloned()
.unwrap();
let later = early + Duration::from_secs(30);
manager.slide_deadlines_for_dialog(&dialog_id, later);
let call = manager.get_call(&call_id).unwrap();
assert_eq!(call.expiry_at, Some(later + Duration::from_secs(60)));
}
#[test]
fn handle_inbound_update_returns_200_and_slides() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = established_outbound_call(&mut manager);
let now = Instant::now();
{
let call = manager.get_call_mut(&call_id).unwrap();
call.session_expires = Some(Duration::from_secs(120));
call.refresher = Some(Refresher::Uas);
call.expiry_at = Some(now);
}
let dialog_id = manager
.get_call(&call_id)
.unwrap()
.dialog_id()
.cloned()
.unwrap();
let update = SipRequest::builder()
.method(Method::Update)
.uri("sip:alice@host")
.via("10.0.0.1", 5060, "UDP", "z9hG4bKupd")
.from("sip:bob@host", "ftag")
.to("sip:alice@host")
.to_tag("ttag")
.call_id("call-st-1")
.cseq(2)
.session_expires(120, Some(Refresher::Uac))
.build()
.expect("update");
let later = now + Duration::from_secs(5);
let response = manager
.handle_inbound_update(&dialog_id, &update, later)
.expect("200 OK built");
assert_eq!(response.status_code(), 200);
assert!(response.session_expires().is_some());
let call = manager.get_call(&call_id).unwrap();
assert_eq!(call.expiry_at, Some(later + Duration::from_secs(120)));
}
#[test]
fn handle_provisional_reliable_emits_prack() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = established_outbound_call(&mut manager);
let response = SipResponse::builder()
.status(180, "Ringing")
.from_request(
&SipRequest::builder()
.method(Method::Invite)
.uri("sip:bob@host")
.via("10.0.0.1", 5060, "UDP", "z9hG4bKabc")
.from("sip:alice@host", "ftag")
.to("sip:bob@host")
.call_id("call-st-1")
.cseq(1)
.build()
.unwrap(),
)
.require(&["100rel"])
.rseq(1)
.build()
.expect("180");
let prack = manager
.handle_provisional_reliable(&call_id, &response)
.expect("PRACK built");
assert_eq!(prack.method(), Method::Prack);
let rack = prack.rack().expect("RAck on PRACK");
assert_eq!(rack.rseq, 1);
assert_eq!(rack.cseq, 1);
assert_eq!(rack.method, Method::Invite);
let outbound = manager.drain_outbound_requests();
assert_eq!(outbound.len(), 1);
assert_eq!(outbound[0].kind, OutboundRequestKind::Prack);
}
#[test]
fn note_update_unsupported_sets_flag() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = established_outbound_call(&mut manager);
manager.note_update_unsupported(&call_id);
assert!(manager.get_call(&call_id).unwrap().update_unsupported);
}
#[test]
fn mark_in_dialog_2xx_slides_only_for_update_and_invite() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = established_outbound_call(&mut manager);
let now = Instant::now();
{
let call = manager.get_call_mut(&call_id).unwrap();
call.session_expires = Some(Duration::from_secs(60));
call.refresher = Some(Refresher::Uas);
call.expiry_at = Some(now + Duration::from_secs(60));
}
let later = now + Duration::from_secs(30);
manager.mark_in_dialog_2xx(&call_id, Method::Bye, later);
assert_eq!(
manager.get_call(&call_id).unwrap().expiry_at,
Some(now + Duration::from_secs(60)),
"BYE 2xx must not slide deadlines"
);
manager.mark_in_dialog_2xx(&call_id, Method::Update, later);
assert_eq!(
manager.get_call(&call_id).unwrap().expiry_at,
Some(later + Duration::from_secs(60)),
"UPDATE 2xx must slide expiry_at"
);
let even_later = later + Duration::from_secs(20);
manager.mark_in_dialog_2xx(&call_id, Method::Invite, even_later);
assert_eq!(
manager.get_call(&call_id).unwrap().expiry_at,
Some(even_later + Duration::from_secs(60)),
"INVITE 2xx must slide expiry_at"
);
}
#[test]
fn refresh_update_carries_route_and_contact_through_dialog_layer() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = established_outbound_call(&mut manager);
{
let call = manager.get_call_mut(&call_id).unwrap();
let dialog = call.dialog_mut().unwrap();
dialog.set_route_set_from_record_routes(
&["<sip:proxy.example.com;lr>".to_string()],
true,
);
dialog.set_remote_target("sip:bob@10.0.0.2:5060".to_string());
dialog.set_local_contact("sip:alice@10.0.0.1:5060".to_string());
call.session_expires = Some(Duration::from_secs(1800));
call.refresher = Some(Refresher::Uac);
call.refresh_at = Some(Instant::now());
}
manager.tick(Instant::now() + Duration::from_secs(1));
let outbound = manager.drain_outbound_requests();
assert_eq!(outbound.len(), 1, "tick must emit exactly one refresh");
assert_eq!(outbound[0].kind, OutboundRequestKind::SessionTimerUpdate);
let bytes = outbound[0].request.to_bytes();
let parsed = crate::sip::SipMessage::parse(&bytes).unwrap();
let parsed_req = parsed.as_request().unwrap();
let routes = parsed_req.route_headers();
assert_eq!(
routes.len(),
1,
"refresh UPDATE must carry the Route header (RFC 3261 §12.2.1.1)"
);
assert!(routes[0].contains("proxy.example.com"));
let contact = parsed_req
.contact_uri()
.expect("refresh UPDATE must carry Contact");
assert!(contact.to_string().contains("10.0.0.1"));
assert!(
parsed_req.uri().to_string().contains("10.0.0.2"),
"request URI must be the dialog's remote target"
);
}
#[test]
fn route_set_two_proxies_emit_in_uac_reversed_order() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = established_outbound_call(&mut manager);
{
let call = manager.get_call_mut(&call_id).unwrap();
let dialog = call.dialog_mut().unwrap();
dialog.set_route_set_from_record_routes(
&[
"<sip:proxy1.example.com;lr>".to_string(),
"<sip:proxy2.example.com;lr>".to_string(),
],
true, );
dialog.set_remote_target("sip:bob@10.0.0.2:5060".to_string());
dialog.set_local_contact("sip:alice@10.0.0.1:5060".to_string());
call.session_expires = Some(Duration::from_secs(1800));
call.refresher = Some(Refresher::Uac);
call.refresh_at = Some(Instant::now());
}
manager.tick(Instant::now() + Duration::from_secs(1));
let outbound = manager.drain_outbound_requests();
assert_eq!(outbound.len(), 1);
let bytes = outbound[0].request.to_bytes();
let parsed = crate::sip::SipMessage::parse(&bytes).unwrap();
let parsed_req = parsed.as_request().unwrap();
let routes = parsed_req.route_headers();
assert_eq!(routes.len(), 2);
assert!(
routes[0].contains("proxy2"),
"first Route must be proxy2 (UAC reverses Record-Route per RFC 3261 §12.1.2); got {:?}",
routes
);
assert!(
routes[1].contains("proxy1"),
"second Route must be proxy1 (UAC reverses Record-Route per RFC 3261 §12.1.2); got {:?}",
routes
);
}
#[test]
fn expiry_bye_carries_route_and_reason_through_dialog_layer() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = established_outbound_call(&mut manager);
{
let call = manager.get_call_mut(&call_id).unwrap();
let dialog = call.dialog_mut().unwrap();
dialog.set_route_set_from_record_routes(
&["<sip:proxy.example.com;lr>".to_string()],
true,
);
dialog.set_remote_target("sip:bob@10.0.0.2:5060".to_string());
dialog.set_local_contact("sip:alice@10.0.0.1:5060".to_string());
call.session_expires = Some(Duration::from_secs(60));
call.refresher = Some(Refresher::Uas);
call.expiry_at = Some(Instant::now());
}
manager.tick(Instant::now() + Duration::from_secs(1));
let outbound = manager.drain_outbound_requests();
assert_eq!(outbound.len(), 1);
assert_eq!(outbound[0].kind, OutboundRequestKind::SessionTimerExpiryBye);
let bytes = outbound[0].request.to_bytes();
let raw = String::from_utf8(bytes.to_vec()).expect("utf8");
assert!(
raw.contains(r#"Reason: SIP;cause=200;text="Session timer expired""#),
"expiry BYE must carry Reason header verbatim"
);
let parsed = crate::sip::SipMessage::parse(&bytes).unwrap();
let parsed_req = parsed.as_request().unwrap();
assert_eq!(parsed_req.method(), Method::Bye);
assert_eq!(parsed_req.route_headers().len(), 1);
assert!(parsed_req.contact_uri().is_some());
}
#[test]
fn prack_carries_route_and_contact_through_dialog_layer() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = established_outbound_call(&mut manager);
let raw_180 = b"SIP/2.0 180 Ringing\r\n\
Via: SIP/2.0/UDP 10.0.0.1:5060;branch=z9hG4bKabc\r\n\
Record-Route: <sip:proxy.example.com;lr>\r\n\
From: <sip:alice@host>;tag=ftag\r\n\
To: <sip:bob@host>;tag=ttag\r\n\
Contact: <sip:bob@10.0.0.2:5060>\r\n\
Call-ID: call-st-1\r\n\
CSeq: 1 INVITE\r\n\
Require: 100rel\r\n\
RSeq: 1\r\n\
Content-Length: 0\r\n\
\r\n";
let parsed = crate::sip::SipMessage::parse(raw_180).expect("parse 180");
let response = parsed.as_response().expect("response").clone();
manager.handle_provisional_response(&call_id, &response, None, "sip:alice@10.0.0.1:5060");
{
let call = manager.get_call(&call_id).unwrap();
let dialog = call.dialog().unwrap();
assert!(
!dialog.route_set().is_empty(),
"route_set must be populated from 18x"
);
assert!(
dialog.remote_target().contains("10.0.0.2"),
"remote_target must come from Contact in 18x"
);
assert!(
dialog.local_contact().contains("10.0.0.1"),
"local_contact must come from the supplied UAC contact"
);
}
let prack = manager
.handle_provisional_reliable(&call_id, &response)
.expect("PRACK built");
let bytes = prack.to_bytes();
let parsed = crate::sip::SipMessage::parse(&bytes).unwrap();
let parsed_req = parsed.as_request().unwrap();
let routes = parsed_req.route_headers();
assert_eq!(
routes.len(),
1,
"PRACK must carry the dialog's Route header"
);
assert!(routes[0].contains("proxy.example.com"));
assert!(
parsed_req.contact_uri().is_some(),
"PRACK must carry Contact"
);
}
#[test]
fn handle_inbound_update_200_carries_contact() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = established_outbound_call(&mut manager);
let dialog_id = {
let call = manager.get_call_mut(&call_id).unwrap();
let dialog = call.dialog_mut().unwrap();
dialog.set_local_contact("sip:alice@10.0.0.1:5060".to_string());
dialog.set_remote_target("sip:bob@10.0.0.2:5060".to_string());
call.session_expires = Some(Duration::from_secs(120));
call.refresher = Some(Refresher::Uas);
call.expiry_at = Some(Instant::now());
call.dialog_id().unwrap().clone()
};
let update = SipRequest::builder()
.method(Method::Update)
.uri("sip:alice@host")
.via("10.0.0.2", 5060, "UDP", "z9hG4bKupd")
.from("sip:bob@host", "ftag")
.to("sip:alice@host")
.to_tag("ttag")
.call_id("call-st-1")
.cseq(7)
.session_expires(120, Some(Refresher::Uac))
.build()
.expect("update");
let resp = manager
.handle_inbound_update(&dialog_id, &update, Instant::now())
.expect("200 OK built");
let bytes = resp.to_bytes();
let parsed = crate::sip::SipMessage::parse(&bytes).unwrap();
let parsed_resp = parsed.as_response().unwrap();
assert_eq!(parsed_resp.status_code(), 200);
let contact = parsed_resp
.contact_uri()
.expect("200 OK to UPDATE must carry Contact (RFC 3261 §12.2.1.1)");
assert!(contact.to_string().contains("10.0.0.1"));
}
#[test]
fn inbound_update_below_min_se_returns_422() {
let mut manager = CallManager::new(ManagerConfig::default());
let call_id = established_outbound_call(&mut manager);
let dialog_id;
let pre_session_expires;
let pre_expiry_at;
let baseline = Instant::now();
{
let call = manager.get_call_mut(&call_id).unwrap();
let dialog = call.dialog_mut().unwrap();
dialog.set_local_contact("sip:alice@10.0.0.1:5060".to_string());
dialog.set_remote_target("sip:bob@10.0.0.2:5060".to_string());
call.session_expires = Some(Duration::from_secs(1800));
call.refresher = Some(Refresher::Uac);
call.expiry_at = Some(baseline + Duration::from_secs(1800));
dialog_id = call.dialog_id().unwrap().clone();
pre_session_expires = call.session_expires;
pre_expiry_at = call.expiry_at;
}
let update = SipRequest::builder()
.method(Method::Update)
.uri("sip:alice@host")
.via("10.0.0.2", 5060, "UDP", "z9hG4bKupd")
.from("sip:bob@host", "ftag")
.to("sip:alice@host")
.to_tag("ttag")
.call_id("call-st-1")
.cseq(7)
.session_expires(30, Some(Refresher::Uac))
.build()
.expect("update");
let resp = manager
.handle_inbound_update(&dialog_id, &update, baseline + Duration::from_secs(60))
.expect("422 response built");
assert_eq!(
resp.status_code(),
422,
"RFC 4028 §10.3: SE < Min-SE must yield 422"
);
let bytes = resp.to_bytes();
let parsed = crate::sip::SipMessage::parse(&bytes).unwrap();
let parsed_resp = parsed.as_response().unwrap();
let min_se = parsed_resp
.min_se()
.expect("422 must carry Min-SE per RFC 4028 §10.3");
assert_eq!(min_se.0, 90);
let call = manager.get_call(&call_id).unwrap();
assert_eq!(
call.session_expires, pre_session_expires,
"rejected UPDATE must not mutate session_expires"
);
assert_eq!(
call.expiry_at, pre_expiry_at,
"rejected UPDATE must not slide expiry_at"
);
}
#[test]
fn uas_populated_dialog_emits_route_and_contact_on_bye() {
let mut manager = CallManager::new(ManagerConfig::default());
let dialog = Dialog::new_uas(
"call-uas-1".to_string(),
"ftag".to_string(),
"ttag".to_string(),
"sip:alice@host".to_string(),
"sip:bob@host".to_string(),
1,
);
let offer_sdp = test_sdp();
let (call_id, _, _) = manager
.handle_incoming_invite(dialog, &offer_sdp)
.expect("incoming");
let raw_invite = b"INVITE sip:alice@host SIP/2.0\r\n\
Via: SIP/2.0/UDP 10.0.0.2:5060;branch=z9hG4bKxxx\r\n\
Record-Route: <sip:proxy1.example.com;lr>\r\n\
Record-Route: <sip:proxy2.example.com;lr>\r\n\
From: <sip:bob@host>;tag=ftag\r\n\
To: <sip:alice@host>\r\n\
Contact: <sip:bob@10.0.0.2:5060>\r\n\
Call-ID: call-uas-1\r\n\
CSeq: 1 INVITE\r\n\
Content-Length: 0\r\n\
\r\n";
let parsed = crate::sip::SipMessage::parse(raw_invite).expect("parse INVITE");
let invite = parsed.as_request().expect("request").clone();
manager.populate_uas_dialog_routing(
&call_id,
&invite,
"sip:alice@10.0.0.1:5060".to_string(),
);
manager.answer_call(&call_id);
let call = manager.get_call(&call_id).unwrap();
let dialog = call.dialog().expect("dialog");
assert_eq!(
dialog.route_set().len(),
2,
"UAS dialog must carry both Record-Route entries"
);
assert!(
dialog.route_set().routes()[0].contains("proxy1"),
"UAS first route must be proxy1 (no reversal); got {:?}",
dialog.route_set().routes()
);
assert!(
dialog.route_set().routes()[1].contains("proxy2"),
"UAS second route must be proxy2 (no reversal); got {:?}",
dialog.route_set().routes()
);
assert!(
dialog.remote_target().contains("10.0.0.2"),
"remote_target must come from INVITE Contact"
);
assert!(
dialog.local_contact().contains("10.0.0.1"),
"local_contact must come from the supplied UAS local Contact"
);
}
}