use crate::dialog::{
client_dialog::ClientInviteDialog,
dialog::{DialogInner, DialogState, TerminatedReason},
DialogId,
};
use crate::rsip;
use crate::transaction::{endpoint::EndpointBuilder, key::TransactionRole};
use crate::transport::TransportLayer;
use rsip::{headers::*, Request, Response, StatusCode, Uri};
use std::sync::Arc;
use tokio::sync::mpsc::unbounded_channel;
use tokio_util::sync::CancellationToken;
async fn create_test_endpoint() -> crate::Result<crate::transaction::endpoint::Endpoint> {
create_test_endpoint_with_follow_record_route(true).await
}
async fn create_test_endpoint_with_follow_record_route(
follow_record_route: bool,
) -> crate::Result<crate::transaction::endpoint::Endpoint> {
let token = CancellationToken::new();
let tl = TransportLayer::new(token.child_token());
let mut builder = EndpointBuilder::new();
builder
.with_user_agent("rsipstack-test")
.with_transport_layer(tl);
if !follow_record_route {
builder.follow_record_route(false);
}
Ok(builder.build())
}
fn create_invite_request(from_tag: &str, to_tag: &str, call_id: &str) -> Request {
Request {
method: rsip::Method::Invite,
uri: Uri::try_from("sip:bob@example.com:5060").unwrap(),
headers: vec![
Via::new("SIP/2.0/UDP alice.example.com:5060;branch=z9hG4bKnashds").into(),
CSeq::new("1 INVITE").into(),
From::new(&format!("Alice <sip:alice@example.com>;tag={}", from_tag)).into(),
To::new(&format!("Bob <sip:bob@example.com>;tag={}", to_tag)).into(),
CallId::new(call_id).into(),
Contact::new("<sip:alice@alice.example.com:5060>").into(),
MaxForwards::new("70").into(),
]
.into(),
version: rsip::Version::V2,
body: b"v=0\r\no=alice 2890844526 2890844527 IN IP4 host.atlanta.com\r\n".to_vec(),
}
}
fn create_invite_request_with_record_route(
from_tag: &str,
to_tag: &str,
call_id: &str,
record_routes: &[&str],
) -> Request {
let mut request = create_invite_request(from_tag, to_tag, call_id);
for rr in record_routes {
request.headers.push(RecordRoute::new(*rr).into());
}
request
}
#[tokio::test]
async fn test_client_dialog_creation() -> crate::Result<()> {
let endpoint = create_test_endpoint().await?;
let (state_sender, _) = unbounded_channel();
let dialog_id = DialogId {
call_id: "test-call-id".to_string(),
from_tag: "alice-tag".to_string(),
to_tag: "bob-tag".to_string(),
};
let invite_req = create_invite_request("alice-tag", "", "test-call-id");
let (tu_sender, _tu_receiver) = unbounded_channel();
let dialog_inner = DialogInner::new(
TransactionRole::Client,
dialog_id.clone(),
invite_req,
endpoint.inner.clone(),
state_sender,
None,
Some(Uri::try_from("sip:alice@alice.example.com:5060").unwrap()),
tu_sender,
)?;
let client_dialog = ClientInviteDialog {
inner: Arc::new(dialog_inner),
};
assert_eq!(client_dialog.id(), dialog_id);
assert!(!client_dialog.inner.is_confirmed());
Ok(())
}
#[tokio::test]
async fn test_client_dialog_sequence_handling() -> crate::Result<()> {
let endpoint = create_test_endpoint().await?;
let (state_sender, _) = unbounded_channel();
let dialog_id = DialogId {
call_id: "test-call-seq".to_string(),
from_tag: "alice-tag".to_string(),
to_tag: "bob-tag".to_string(),
};
let invite_req = create_invite_request("alice-tag", "bob-tag", "test-call-seq");
let (tu_sender, _tu_receiver) = unbounded_channel();
let dialog_inner = DialogInner::new(
TransactionRole::Client,
dialog_id.clone(),
invite_req,
endpoint.inner.clone(),
state_sender,
None,
Some(Uri::try_from("sip:alice@alice.example.com:5060").unwrap()),
tu_sender,
)?;
let client_dialog = ClientInviteDialog {
inner: Arc::new(dialog_inner),
};
let initial_seq = client_dialog.inner.get_local_seq();
assert_eq!(initial_seq, 1);
let next_seq = client_dialog.inner.increment_local_seq();
assert_eq!(next_seq, 2);
Ok(())
}
#[tokio::test]
async fn test_client_dialog_state_transitions() -> crate::Result<()> {
let endpoint = create_test_endpoint().await?;
let (state_sender, _) = unbounded_channel();
let dialog_id = DialogId {
call_id: "test-call-flow".to_string(),
from_tag: "alice-tag".to_string(),
to_tag: "".to_string(),
};
let invite_req = create_invite_request("alice-tag", "", "test-call-flow");
let (tu_sender, _tu_receiver) = unbounded_channel();
let dialog_inner = DialogInner::new(
TransactionRole::Client,
dialog_id.clone(),
invite_req,
endpoint.inner.clone(),
state_sender,
None,
Some(Uri::try_from("sip:alice@alice.example.com:5060").unwrap()),
tu_sender,
)?;
let client_dialog = ClientInviteDialog {
inner: Arc::new(dialog_inner),
};
let state = client_dialog.inner.state.lock().unwrap().clone();
assert!(matches!(state, DialogState::Calling(_)));
client_dialog
.inner
.transition(DialogState::Trying(dialog_id.clone()))?;
let state = client_dialog.inner.state.lock().unwrap().clone();
assert!(matches!(state, DialogState::Trying(_)));
let ringing_resp = Response {
status_code: StatusCode::Ringing,
version: rsip::Version::V2,
headers: vec![
Via::new("SIP/2.0/UDP alice.example.com:5060;branch=z9hG4bKnashds").into(),
CSeq::new("1 INVITE").into(),
From::new("Alice <sip:alice@example.com>;tag=alice-tag").into(),
To::new("Bob <sip:bob@example.com>;tag=bob-tag").into(),
CallId::new("test-call-flow").into(),
Contact::new("<sip:bob@bob.example.com:5060>").into(),
]
.into(),
body: vec![],
};
client_dialog
.inner
.transition(DialogState::Early(dialog_id.clone(), ringing_resp.clone()))?;
let state = client_dialog.inner.state.lock().unwrap().clone();
assert!(matches!(state, DialogState::Early(_, _)));
let mut final_resp = ringing_resp.clone();
final_resp.status_code = StatusCode::OK;
client_dialog
.inner
.transition(DialogState::Confirmed(dialog_id.clone(), final_resp))?;
let state = client_dialog.inner.state.lock().unwrap().clone();
assert!(matches!(state, DialogState::Confirmed(_, _)));
assert!(client_dialog.inner.is_confirmed());
Ok(())
}
#[tokio::test]
async fn test_client_dialog_route_set_respects_follow_record_route() -> crate::Result<()> {
let endpoint_follow = create_test_endpoint_with_follow_record_route(true).await?;
let (state_sender, _) = unbounded_channel();
let dialog_id = DialogId {
call_id: "route-test-follow".to_string(),
from_tag: "alice-tag".to_string(),
to_tag: "".to_string(),
};
let invite_req = create_invite_request_with_record_route(
"alice-tag",
"",
"route-test-follow",
&["<sip:proxy1.example.com;lr>", "<sip:proxy2.example.com;lr>"],
);
let (tu_sender, _tu_receiver) = unbounded_channel();
let dialog_follow = DialogInner::new(
TransactionRole::Client,
dialog_id.clone(),
invite_req,
endpoint_follow.inner.clone(),
state_sender,
None,
Some(Uri::try_from("sip:alice@alice.example.com:5060").unwrap()),
tu_sender,
)?;
let stored_routes = dialog_follow.route_set.lock().unwrap().clone();
assert_eq!(stored_routes.len(), 2);
let endpoint_skip = create_test_endpoint_with_follow_record_route(false).await?;
let (state_sender_skip, _) = unbounded_channel();
let invite_req_skip = create_invite_request_with_record_route(
"alice-tag",
"",
"route-test-skip",
&["<sip:proxy.example.com;lr>"],
);
let (tu_sender_skip, _tu_receiver_skip) = unbounded_channel();
let dialog_skip = DialogInner::new(
TransactionRole::Client,
DialogId {
call_id: "route-test-skip".to_string(),
from_tag: "alice-tag".to_string(),
to_tag: "".to_string(),
},
invite_req_skip,
endpoint_skip.inner.clone(),
state_sender_skip,
None,
Some(Uri::try_from("sip:alice@alice.example.com:5060").unwrap()),
tu_sender_skip,
)?;
let stored_routes_skip = dialog_skip.route_set.lock().unwrap().clone();
assert!(stored_routes_skip.is_empty());
Ok(())
}
#[tokio::test]
async fn test_client_dialog_termination_scenarios() -> crate::Result<()> {
let endpoint = create_test_endpoint().await?;
let (state_sender, _) = unbounded_channel();
let dialog_id_1 = DialogId {
call_id: "test-call-term-early".to_string(),
from_tag: "alice-tag".to_string(),
to_tag: "".to_string(),
};
let invite_req_1 = create_invite_request("alice-tag", "", "test-call-term-early");
let (tu_sender, _tu_receiver) = unbounded_channel();
let dialog_inner_1 = DialogInner::new(
TransactionRole::Client,
dialog_id_1.clone(),
invite_req_1,
endpoint.inner.clone(),
state_sender.clone(),
None,
Some(Uri::try_from("sip:alice@alice.example.com:5060").unwrap()),
tu_sender,
)?;
let client_dialog_1 = ClientInviteDialog {
inner: Arc::new(dialog_inner_1),
};
client_dialog_1.inner.transition(DialogState::Terminated(
dialog_id_1.clone(),
TerminatedReason::UasBusy,
))?;
let state = client_dialog_1.inner.state.lock().unwrap().clone();
assert!(matches!(
state,
DialogState::Terminated(_, TerminatedReason::UasBusy)
));
let dialog_id_2 = DialogId {
call_id: "test-call-term-normal".to_string(),
from_tag: "alice-tag".to_string(),
to_tag: "bob-tag".to_string(),
};
let invite_req_2 = create_invite_request("alice-tag", "bob-tag", "test-call-term-normal");
let (tu_sender, _tu_receiver) = unbounded_channel();
let dialog_inner_2 = DialogInner::new(
TransactionRole::Client,
dialog_id_2.clone(),
invite_req_2,
endpoint.inner.clone(),
state_sender,
None,
Some(Uri::try_from("sip:alice@alice.example.com:5060").unwrap()),
tu_sender,
)?;
let client_dialog_2 = ClientInviteDialog {
inner: Arc::new(dialog_inner_2),
};
client_dialog_2.inner.transition(DialogState::Confirmed(
dialog_id_2.clone(),
Response::default(),
))?;
assert!(client_dialog_2.inner.is_confirmed());
client_dialog_2.inner.transition(DialogState::Terminated(
dialog_id_2.clone(),
TerminatedReason::UacBye,
))?;
let state = client_dialog_2.inner.state.lock().unwrap().clone();
assert!(matches!(
state,
DialogState::Terminated(_, TerminatedReason::UacBye)
));
Ok(())
}