#[cfg(test)]
mod callsession_b2bua_tests {
use super::super::media_bridge::MediaBridge;
use super::super::session::NegotiationState;
use super::super::test_util::tests::MockMediaPeer;
use crate::call::{DialStrategy, DialplanFlow};
use crate::media::negotiate::{CodecInfo, MediaNegotiator};
use audio_codec::CodecType;
use rustrtc::RtpCodecParameters;
use std::sync::Arc;
fn mock_rtp_params(payload_type: u8, clock_rate: u32, channels: u8) -> RtpCodecParameters {
RtpCodecParameters {
payload_type,
clock_rate,
channels,
}
}
fn create_sdp_offer(codec: &str, payload_type: u8) -> String {
format!(
"v=0\r\n\
o=- 1234567890 1234567890 IN IP4 192.168.1.1\r\n\
s=-\r\n\
c=IN IP4 192.168.1.1\r\n\
t=0 0\r\n\
m=audio 10000 RTP/AVP {}\r\n\
a=rtpmap:{} {}\r\n",
payload_type, payload_type, codec
)
}
fn create_sdp_answer(codec: &str, payload_type: u8) -> String {
format!(
"v=0\r\n\
o=- 9876543210 9876543210 IN IP4 192.168.1.2\r\n\
s=-\r\n\
c=IN IP4 192.168.1.2\r\n\
t=0 0\r\n\
m=audio 20000 RTP/AVP {}\r\n\
a=rtpmap:{} {}\r\n",
payload_type, payload_type, codec
)
}
#[tokio::test]
async fn test_simple_forward_pcmu_to_pcmu() {
let leg_a = Arc::new(MockMediaPeer::new());
let leg_b = Arc::new(MockMediaPeer::new());
let params_a = mock_rtp_params(0, 8000, 1);
let params_b = mock_rtp_params(0, 8000, 1);
let bridge = MediaBridge::new(
leg_a.clone(),
leg_b.clone(),
params_a,
params_b,
vec![],
vec![],
CodecType::PCMU,
CodecType::PCMU,
None,
None,
None,
"test_simple_forward_pcmu_to_pcmu".to_string(),
None,
);
assert_eq!(bridge.codec_a, CodecType::PCMU);
assert_eq!(bridge.codec_b, CodecType::PCMU);
bridge
.start()
.await
.expect("Bridge should start successfully");
bridge.stop();
assert!(leg_a.stop_count() > 0);
assert!(leg_b.stop_count() > 0);
}
#[tokio::test]
async fn test_forward_opus_to_pcmu_transcoding() {
let leg_a = Arc::new(MockMediaPeer::new());
let leg_b = Arc::new(MockMediaPeer::new());
let params_a = mock_rtp_params(111, 48000, 2);
let params_b = mock_rtp_params(0, 8000, 1);
let bridge = MediaBridge::new(
leg_a.clone(),
leg_b.clone(),
params_a,
params_b,
vec![],
vec![],
CodecType::Opus,
CodecType::PCMU,
None,
None,
None,
"test_forward_opus_to_pcmu_transcoding".to_string(),
None,
);
assert_eq!(bridge.codec_a, CodecType::Opus);
assert_eq!(bridge.codec_b, CodecType::PCMU);
bridge
.start()
.await
.expect("Bridge should start successfully");
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
bridge.stop();
}
#[tokio::test]
async fn test_pcma_to_pcmu_transcoding() {
let leg_a = Arc::new(MockMediaPeer::new());
let leg_b = Arc::new(MockMediaPeer::new());
let params_a = mock_rtp_params(8, 8000, 1);
let params_b = mock_rtp_params(0, 8000, 1);
let bridge = MediaBridge::new(
leg_a.clone(),
leg_b.clone(),
params_a.clone(),
params_b.clone(),
vec![],
vec![],
CodecType::PCMA,
CodecType::PCMU,
None,
None,
None,
"test_pcma_to_pcmu_transcoding".to_string(),
None,
);
assert_eq!(bridge.codec_a, CodecType::PCMA);
assert_eq!(bridge.codec_b, CodecType::PCMU);
bridge
.start()
.await
.expect("Bridge should start successfully");
bridge.stop();
}
#[test]
fn test_parse_rtp_map_from_sdp_multi_codec() {
let sdp = "v=0\r\n\
o=- 8819118164752754436 2 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 53824 UDP/TLS/RTP/SAVPF 111 9 0 8\r\n\
a=rtpmap:111 opus/48000/2\r\n\
a=rtpmap:9 G722/8000\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:8 PCMA/8000\r\n\
a=rtpmap:126 telephone-event/8000\r\n";
let parsed_sdp = rustrtc::SessionDescription::parse(rustrtc::SdpType::Offer, sdp).unwrap();
let section = parsed_sdp
.media_sections
.iter()
.find(|m| m.kind == rustrtc::MediaKind::Audio)
.unwrap();
let rtp_map = MediaNegotiator::parse_rtp_map_from_section(section);
assert!(
rtp_map
.iter()
.any(|(pt, (codec, _, _))| *pt == 111 && *codec == CodecType::Opus)
);
assert!(
rtp_map
.iter()
.any(|(pt, (codec, _, _))| *pt == 9 && *codec == CodecType::G722)
);
assert!(
rtp_map
.iter()
.any(|(pt, (codec, _, _))| *pt == 0 && *codec == CodecType::PCMU)
);
assert!(
rtp_map
.iter()
.any(|(pt, (codec, _, _))| *pt == 8 && *codec == CodecType::PCMA)
);
}
#[test]
fn test_extract_codec_params_pcmu() {
let sdp = create_sdp_answer("PCMU/8000/1", 0);
let codecs = MediaNegotiator::extract_codec_params(&sdp);
let first = &codecs.audio[0];
let params = first.to_params();
assert_eq!(first.codec, CodecType::PCMU);
assert_eq!(params.payload_type, 0);
assert_eq!(params.clock_rate, 8000);
assert_eq!(params.channels, 1);
assert!(codecs.dtmf.is_empty());
}
#[test]
fn test_extract_codec_params_pcma() {
let sdp = create_sdp_answer("PCMA/8000/1", 8);
let codecs = MediaNegotiator::extract_codec_params(&sdp);
let first = &codecs.audio[0];
let params = first.to_params();
assert_eq!(first.codec, CodecType::PCMA);
assert_eq!(params.payload_type, 8);
assert_eq!(params.clock_rate, 8000);
assert_eq!(params.channels, 1);
}
#[test]
fn test_extract_codec_params_opus() {
let sdp = create_sdp_answer("opus/48000/2", 111);
let codecs = MediaNegotiator::extract_codec_params(&sdp);
let first = &codecs.audio[0];
let params = first.to_params();
assert_eq!(first.codec, CodecType::Opus);
assert_eq!(params.payload_type, 111);
assert_eq!(params.clock_rate, 48000);
assert_eq!(params.channels, 2);
}
#[test]
fn test_extract_codec_params_g722() {
let sdp = create_sdp_answer("G722/8000", 9);
let codecs = MediaNegotiator::extract_codec_params(&sdp);
let first = &codecs.audio[0];
let params = first.to_params();
assert_eq!(first.codec, CodecType::G722);
assert_eq!(params.payload_type, 9);
assert_eq!(params.clock_rate, 8000);
}
#[test]
fn test_dtmf_payload_extraction() {
let sdp = "v=0\r\n\
o=- 1234567890 1234567890 IN IP4 192.168.1.1\r\n\
s=-\r\n\
c=IN IP4 192.168.1.1\r\n\
t=0 0\r\n\
m=audio 10000 RTP/AVP 0 101\r\n\
a=rtpmap:0 PCMU/8000/1\r\n\
a=rtpmap:101 telephone-event/8000\r\n";
let codecs = MediaNegotiator::extract_codec_params(sdp);
let first = &codecs.audio[0];
let params = first.to_params();
assert_eq!(first.codec, CodecType::PCMU);
assert_eq!(params.payload_type, 0);
assert_eq!(
codecs
.dtmf
.iter()
.map(|codec| codec.payload_type)
.collect::<Vec<_>>(),
vec![101]
);
}
#[test]
fn test_codec_compatibility_exact_match() {
let alice_offer = create_sdp_offer("PCMU/8000/1", 0);
let bob_answer = create_sdp_answer("PCMU/8000/1", 0);
let alice_codecs = MediaNegotiator::extract_codec_params(&alice_offer).audio;
let bob_codecs = MediaNegotiator::extract_codec_params(&bob_answer).audio;
let bob_codec = bob_codecs[0].codec;
let compatible = alice_codecs.iter().any(|c| c.codec == bob_codec);
assert!(compatible, "Alice should support PCMU");
assert_eq!(bob_codec, CodecType::PCMU);
}
#[test]
fn test_codec_incompatibility_requires_transcoding() {
let alice_offer = create_sdp_offer("opus/48000/2", 111);
let bob_answer = create_sdp_answer("PCMU/8000/1", 0);
let alice_sdp =
rustrtc::SessionDescription::parse(rustrtc::SdpType::Offer, &alice_offer).unwrap();
let alice_section = alice_sdp
.media_sections
.iter()
.find(|m| m.kind == rustrtc::MediaKind::Audio)
.unwrap();
let alice_codecs = MediaNegotiator::parse_rtp_map_from_section(alice_section);
let bob_codecs = MediaNegotiator::extract_codec_params(&bob_answer).audio;
let bob_codec = bob_codecs[0].codec;
let compatible = alice_codecs
.iter()
.any(|(_, (codec, _, _))| *codec == bob_codec);
assert!(
!compatible,
"Alice should NOT support PCMU - transcoding required"
);
assert_eq!(bob_codec, CodecType::PCMU);
}
#[tokio::test]
async fn test_bridge_supports_suppress_and_resume() {
let leg_a = Arc::new(MockMediaPeer::new());
let leg_b = Arc::new(MockMediaPeer::new());
let bridge = MediaBridge::new(
leg_a.clone(),
leg_b.clone(),
mock_rtp_params(0, 8000, 1),
mock_rtp_params(0, 8000, 1),
vec![],
vec![],
CodecType::PCMU,
CodecType::PCMU,
None,
None,
None,
"test_bridge_supports_suppress_and_resume".to_string(),
None,
);
bridge.start().await.expect("Bridge should start");
bridge
.suppress_forwarding("test-track")
.await
.expect("Should suppress");
bridge
.resume_forwarding("test-track")
.await
.expect("Should resume");
bridge.stop();
}
#[tokio::test]
async fn test_bridge_multiple_start_is_idempotent() {
let leg_a = Arc::new(MockMediaPeer::new());
let leg_b = Arc::new(MockMediaPeer::new());
let bridge = MediaBridge::new(
leg_a.clone(),
leg_b.clone(),
mock_rtp_params(0, 8000, 1),
mock_rtp_params(0, 8000, 1),
vec![],
vec![],
CodecType::PCMU,
CodecType::PCMU,
None,
None,
None,
"test_bridge_multiple_start_is_idempotent".to_string(),
None,
);
bridge.start().await.expect("First start should succeed");
bridge.start().await.expect("Second start should succeed");
bridge.start().await.expect("Third start should succeed");
bridge.stop();
}
#[test]
fn test_negotiation_state_transitions() {
let idle = NegotiationState::Idle;
let stable = NegotiationState::Stable;
let local_offer = NegotiationState::LocalOfferSent;
let remote_offer = NegotiationState::RemoteOfferReceived;
assert_ne!(idle, stable);
assert_ne!(idle, local_offer);
assert_ne!(idle, remote_offer);
assert_ne!(stable, local_offer);
assert_ne!(stable, remote_offer);
assert_ne!(local_offer, remote_offer);
let copied = stable;
assert_eq!(copied, stable);
}
#[tokio::test]
async fn test_bridge_with_dtmf_payload_types() {
let leg_a = Arc::new(MockMediaPeer::new());
let leg_b = Arc::new(MockMediaPeer::new());
let bridge = MediaBridge::new(
leg_a.clone(),
leg_b.clone(),
mock_rtp_params(0, 8000, 1),
mock_rtp_params(0, 8000, 1),
vec![CodecInfo {
payload_type: 101,
codec: CodecType::TelephoneEvent,
clock_rate: 8000,
channels: 1,
}],
vec![CodecInfo {
payload_type: 101,
codec: CodecType::TelephoneEvent,
clock_rate: 8000,
channels: 1,
}],
CodecType::PCMU,
CodecType::PCMU,
None,
None,
None,
"test_bridge_with_dtmf_payload_types".to_string(),
None,
);
assert_eq!(bridge.dtmf_codecs_a.len(), 1);
assert_eq!(bridge.dtmf_codecs_b.len(), 1);
bridge.start().await.expect("Bridge should start");
bridge.stop();
}
#[test]
fn test_dialplan_flow_validation() {
let empty_strategy = DialStrategy::Sequential(vec![]);
let flow = DialplanFlow::Targets(empty_strategy);
assert!(matches!(flow, DialplanFlow::Targets(_)));
}
#[test]
fn test_rtp_params_clock_rates() {
let pcmu_params = mock_rtp_params(0, 8000, 1);
assert_eq!(pcmu_params.clock_rate, 8000);
assert_eq!(pcmu_params.channels, 1);
let opus_params = mock_rtp_params(111, 48000, 2);
assert_eq!(opus_params.clock_rate, 48000);
assert_eq!(opus_params.channels, 2);
let g722_params = mock_rtp_params(9, 8000, 1);
assert_eq!(g722_params.clock_rate, 8000);
}
#[tokio::test]
async fn test_bridge_without_recorder() {
let leg_a = Arc::new(MockMediaPeer::new());
let leg_b = Arc::new(MockMediaPeer::new());
let bridge = MediaBridge::new(
leg_a.clone(),
leg_b.clone(),
mock_rtp_params(0, 8000, 1),
mock_rtp_params(0, 8000, 1),
vec![],
vec![],
CodecType::PCMU,
CodecType::PCMU,
None,
None,
None,
"test_bridge_without_recorder".to_string(),
None,
);
bridge
.start()
.await
.expect("Bridge should start without recorder");
bridge.stop();
}
#[test]
fn test_queue_plan_creation() {
use crate::call::{DialStrategy, Location, QueuePlan};
use rsipstack::sip::Uri;
let target1 = Location {
aor: Uri::try_from("sip:agent1@example.com").unwrap(),
expires: 3600,
..Default::default()
};
let mut plan = QueuePlan::default();
plan.dial_strategy = Some(DialStrategy::Sequential(vec![target1]));
plan.accept_immediately = true;
assert!(plan.accept_immediately);
assert!(plan.dial_strategy.is_some());
}
#[test]
fn test_queue_plan_with_hold_config() {
use crate::call::{QueueHoldConfig, QueuePlan};
let mut plan = QueuePlan::default();
plan.hold = Some(QueueHoldConfig {
audio_file: Some("/audio/hold-music.wav".to_string()),
loop_playback: true,
});
assert!(plan.hold.is_some());
assert_eq!(
plan.hold.as_ref().unwrap().audio_file,
Some("/audio/hold-music.wav".to_string())
);
assert!(plan.hold.as_ref().unwrap().loop_playback);
}
#[test]
fn test_queue_plan_fallback_actions() {
use crate::call::{FailureAction, QueueFallbackAction, QueuePlan};
use rsipstack::sip::StatusCode;
let mut plan = QueuePlan::default();
plan.fallback = Some(QueueFallbackAction::Failure(
FailureAction::PlayThenHangup {
audio_file: "/audio/unavailable.wav".to_string(),
use_early_media: false,
status_code: StatusCode::TemporarilyUnavailable,
reason: Some("All agents busy".to_string()),
},
));
assert!(plan.fallback.is_some());
}
#[test]
fn test_dialplan_flow_queue_construction() {
use crate::call::{DialStrategy, DialplanFlow, Location, QueuePlan};
use rsipstack::sip::Uri;
let target = Location {
aor: Uri::try_from("sip:agent1@example.com").unwrap(),
expires: 3600,
..Default::default()
};
let mut plan = QueuePlan::default();
plan.dial_strategy = Some(DialStrategy::Sequential(vec![target.clone()]));
let fallback = DialplanFlow::Targets(DialStrategy::Sequential(vec![target]));
let queue_flow = DialplanFlow::Queue {
plan,
next: Box::new(fallback),
};
match queue_flow {
DialplanFlow::Queue { plan: _, next } => {
assert!(matches!(*next, DialplanFlow::Targets(_)));
}
_ => panic!("Expected Queue flow"),
}
}
#[test]
fn test_dialplan_flow_queue_with_multiple_fallback_levels() {
use crate::call::{DialStrategy, DialplanFlow, Location, QueuePlan};
use rsipstack::sip::Uri;
let target1 = Location {
aor: Uri::try_from("sip:agent1@example.com").unwrap(),
expires: 3600,
..Default::default()
};
let target2 = Location {
aor: Uri::try_from("sip:agent2@example.com").unwrap(),
expires: 3600,
..Default::default()
};
let mut primary_queue = QueuePlan::default();
primary_queue.dial_strategy = Some(DialStrategy::Sequential(vec![target1]));
primary_queue.label = Some("Primary Queue".to_string());
let mut fallback_queue = QueuePlan::default();
fallback_queue.dial_strategy = Some(DialStrategy::Sequential(vec![target2.clone()]));
fallback_queue.label = Some("Fallback Queue".to_string());
let final_fallback = DialplanFlow::Targets(DialStrategy::Sequential(vec![target2]));
let fallback_queue_flow = DialplanFlow::Queue {
plan: fallback_queue,
next: Box::new(final_fallback),
};
let primary_queue_flow = DialplanFlow::Queue {
plan: primary_queue,
next: Box::new(fallback_queue_flow),
};
match primary_queue_flow {
DialplanFlow::Queue { plan, next } => {
assert_eq!(plan.label, Some("Primary Queue".to_string()));
match *next {
DialplanFlow::Queue {
plan: fallback_plan,
next: final_next,
} => {
assert_eq!(fallback_plan.label, Some("Fallback Queue".to_string()));
assert!(matches!(*final_next, DialplanFlow::Targets(_)));
}
_ => panic!("Expected nested Queue flow"),
}
}
_ => panic!("Expected Queue flow"),
}
}
#[test]
fn test_queue_plan_without_dial_strategy() {
use crate::call::QueuePlan;
let plan = QueuePlan {
accept_immediately: false,
passthrough_ringback: false,
hold: None,
fallback: None,
dial_strategy: None, ring_timeout: None,
label: Some("Empty Queue".to_string()),
..Default::default()
};
assert_eq!(plan.label, Some("Empty Queue".to_string()));
assert!(plan.dial_strategy.is_none());
}
#[tokio::test]
async fn test_media_bridge_drop_calls_stop_on_legs() {
use super::super::test_util::tests::MockMediaPeer;
let leg_a = Arc::new(MockMediaPeer::new_with_stop_tracking());
let leg_b = Arc::new(MockMediaPeer::new_with_stop_tracking());
assert_eq!(leg_a.stop_count(), 0);
assert_eq!(leg_b.stop_count(), 0);
let bridge = MediaBridge::new(
leg_a.clone(),
leg_b.clone(),
mock_rtp_params(0, 8000, 1),
mock_rtp_params(0, 8000, 1),
vec![],
vec![],
CodecType::PCMU,
CodecType::PCMU,
None,
None,
None,
"test_media_bridge_drop_calls_stop_on_legs".to_string(),
None,
);
bridge.start().await.expect("Bridge should start");
drop(bridge);
assert_eq!(
leg_a.stop_count(),
1,
"leg_a.stop() should be called exactly once on Drop"
);
assert_eq!(
leg_b.stop_count(),
1,
"leg_b.stop() should be called exactly once on Drop"
);
}
#[tokio::test]
async fn test_media_bridge_drop_idempotent() {
use super::super::test_util::tests::MockMediaPeer;
let leg_a = Arc::new(MockMediaPeer::new_with_stop_tracking());
let leg_b = Arc::new(MockMediaPeer::new_with_stop_tracking());
let bridge = MediaBridge::new(
leg_a.clone(),
leg_b.clone(),
mock_rtp_params(0, 8000, 1),
mock_rtp_params(0, 8000, 1),
vec![],
vec![],
CodecType::PCMU,
CodecType::PCMU,
None,
None,
None,
"test_media_bridge_drop_idempotent".to_string(),
None,
);
bridge.start().await.expect("Bridge should start");
bridge.stop();
drop(bridge);
assert_eq!(
leg_a.stop_count(),
1,
"leg_a.stop() should be called exactly once even if stopped before drop"
);
assert_eq!(
leg_b.stop_count(),
1,
"leg_b.stop() should be called exactly once even if stopped before drop"
);
}
#[tokio::test]
async fn test_media_bridge_stop_can_be_called_multiple_times() {
let leg_a = Arc::new(MockMediaPeer::new());
let leg_b = Arc::new(MockMediaPeer::new());
let bridge = MediaBridge::new(
leg_a.clone(),
leg_b.clone(),
mock_rtp_params(0, 8000, 1),
mock_rtp_params(0, 8000, 1),
vec![],
vec![],
CodecType::PCMU,
CodecType::PCMU,
None,
None,
None,
"test_media_bridge_stop_multiple".to_string(),
None,
);
bridge.start().await.expect("Bridge should start");
bridge.stop();
bridge.stop();
bridge.stop();
assert!(leg_a.stop_count() > 0);
assert!(leg_b.stop_count() > 0);
}
#[test]
fn test_extract_codec_params_is_idempotent() {
let sdp = "v=0\r\n\
o=- 8819118164752754436 2 IN IP4 127.0.0.1\r\n\
s=-\r\n\
t=0 0\r\n\
m=audio 53824 UDP/TLS/RTP/SAVPF 111 9 0 8\r\n\
a=rtpmap:111 opus/48000/2\r\n\
a=rtpmap:9 G722/8000\r\n\
a=rtpmap:0 PCMU/8000\r\n\
a=rtpmap:8 PCMA/8000\r\n\
a=rtpmap:126 telephone-event/8000\r\n";
let result1 = MediaNegotiator::extract_codec_params(sdp);
let result2 = MediaNegotiator::extract_codec_params(sdp);
let result3 = MediaNegotiator::extract_codec_params(sdp);
assert_eq!(
result1.audio.len(),
result2.audio.len(),
"Audio codec count should be consistent"
);
assert_eq!(
result1.audio.len(),
result3.audio.len(),
"Audio codec count should be consistent"
);
for i in 0..result1.audio.len() {
assert_eq!(
result1.audio[i].codec, result2.audio[i].codec,
"Codec at position {} should be the same",
i
);
assert_eq!(
result1.audio[i].payload_type, result2.audio[i].payload_type,
"Payload type at position {} should be the same",
i
);
}
}
#[test]
fn test_extract_codec_params_twice_does_not_double_dtmf() {
let sdp = "v=0\r\n\
o=- 1234567890 1234567890 IN IP4 192.168.1.1\r\n\
s=-\r\n\
c=IN IP4 192.168.1.1\r\n\
t=0 0\r\n\
m=audio 10000 RTP/AVP 0 101\r\n\
a=rtpmap:0 PCMU/8000/1\r\n\
a=rtpmap:101 telephone-event/8000\r\n";
let result1 = MediaNegotiator::extract_codec_params(sdp);
let result2 = MediaNegotiator::extract_codec_params(sdp);
let dtmf_count_1 = result1.dtmf.len();
let dtmf_count_2 = result2.dtmf.len();
assert_eq!(
dtmf_count_1, dtmf_count_2,
"DTMF codec count should be consistent"
);
}
#[test]
fn test_negotiation_state_ordering() {
use super::super::session::NegotiationState;
let states = [
NegotiationState::Idle,
NegotiationState::LocalOfferSent,
NegotiationState::RemoteOfferReceived,
NegotiationState::Stable,
];
for (i, state1) in states.iter().enumerate() {
for (j, state2) in states.iter().enumerate() {
if i != j {
assert_ne!(
state1, state2,
"States at different positions should not be equal"
);
}
}
}
}
#[tokio::test]
async fn test_media_bridge_with_mock_tracks_get_tracks_called() {
use super::super::test_util::tests::MockMediaPeer;
let leg_a = Arc::new(MockMediaPeer::new_with_stop_tracking());
let leg_b = Arc::new(MockMediaPeer::new_with_stop_tracking());
let bridge = MediaBridge::new(
leg_a.clone(),
leg_b.clone(),
mock_rtp_params(0, 8000, 1),
mock_rtp_params(0, 8000, 1),
vec![],
vec![],
CodecType::PCMU,
CodecType::PCMU,
None,
None,
None,
"test_get_tracks_called".to_string(),
None,
);
bridge.start().await.expect("Bridge should start");
assert!(
leg_a.get_tracks_call_count() > 0 || leg_b.get_tracks_call_count() > 0,
"get_tracks should be called at least once during start"
);
bridge.stop();
}
#[tokio::test]
async fn test_bridge_start_idempotent_multiple_calls() {
let leg_a = Arc::new(MockMediaPeer::new());
let leg_b = Arc::new(MockMediaPeer::new());
let bridge = MediaBridge::new(
leg_a.clone(),
leg_b.clone(),
mock_rtp_params(0, 8000, 1),
mock_rtp_params(0, 8000, 1),
vec![],
vec![],
CodecType::PCMU,
CodecType::PCMU,
None,
None,
None,
"test_bridge_start_idempotent".to_string(),
None,
);
for _ in 0..5 {
bridge.start().await.expect("start() should be idempotent");
}
bridge.stop();
}
}