use crate::channel::{
channel_driver, AnswerState, CallDirection, CallState, ChannelState, ChannelTimetable,
HangupCause, ParseAnswerStateError, ParseCallDirectionError, ParseCallStateError,
ParseChannelStateError, ParseHangupCauseError, ParseTimetableError,
};
#[cfg(feature = "esl")]
use crate::event::{EslEventPriority, ParsePriorityError};
use crate::headers::EventHeader;
use crate::sofia::{
GatewayPingStatus, GatewayRegState, ParseGatewayPingStatusError, ParseGatewayRegStateError,
ParseSipUserPingStatusError, ParseSofiaEventSubclassError, SipUserPingStatus,
SofiaEventSubclass,
};
use crate::variables::{
LoopbackLeg, LoopbackResignation, LoopbackVariable, ParseLoopbackLegError, VariableName,
};
use sip_header::SipHeaderLookup;
use std::str::FromStr;
pub fn variable_key(name: &str) -> String {
format!("{}{name}", crate::VARIABLE_PREFIX)
}
#[inline]
fn parse_opt<T: FromStr>(raw: Option<&str>) -> Result<Option<T>, T::Err> {
raw.map(str::parse)
.transpose()
}
pub trait HeaderLookup: SipHeaderLookup {
fn header_str(&self, name: &str) -> Option<&str>;
fn variable_str(&self, name: &str) -> Option<&str>;
fn header(&self, name: EventHeader) -> Option<&str> {
self.header_str(name.as_str())
}
fn variable(&self, name: impl VariableName) -> Option<&str> {
self.variable_str(name.as_str())
}
fn first_header<const N: usize>(&self, names: [EventHeader; N]) -> Option<&str> {
names
.into_iter()
.find_map(|name| self.header(name))
}
fn unique_id(&self) -> Option<&str> {
self.first_header([EventHeader::UniqueId, EventHeader::CallerUniqueId])
}
fn job_uuid(&self) -> Option<&str> {
self.header(EventHeader::JobUuid)
}
fn channel_name(&self) -> Option<&str> {
self.header(EventHeader::ChannelName)
}
fn channel_driver(&self) -> Option<&str> {
self.channel_name()
.and_then(channel_driver)
}
fn loopback_leg(&self) -> Result<Option<LoopbackLeg>, ParseLoopbackLegError> {
parse_opt(self.variable(LoopbackVariable::LoopbackLeg))
}
fn caller_id_number(&self) -> Option<&str> {
self.header(EventHeader::CallerCallerIdNumber)
}
fn caller_id_name(&self) -> Option<&str> {
self.header(EventHeader::CallerCallerIdName)
}
fn destination_number(&self) -> Option<&str> {
self.header(EventHeader::CallerDestinationNumber)
}
fn callee_id_number(&self) -> Option<&str> {
self.header(EventHeader::CallerCalleeIdNumber)
}
fn callee_id_name(&self) -> Option<&str> {
self.header(EventHeader::CallerCalleeIdName)
}
fn channel_presence_id(&self) -> Option<&str> {
self.header(EventHeader::ChannelPresenceId)
}
fn presence_call_direction(&self) -> Result<Option<CallDirection>, ParseCallDirectionError> {
parse_opt(self.header(EventHeader::PresenceCallDirection))
}
fn event_date_timestamp(&self) -> Option<&str> {
self.header(EventHeader::EventDateTimestamp)
}
fn event_sequence(&self) -> Option<&str> {
self.header(EventHeader::EventSequence)
}
fn dtmf_duration(&self) -> Option<&str> {
self.header(EventHeader::DtmfDuration)
}
fn dtmf_source(&self) -> Option<&str> {
self.header(EventHeader::DtmfSource)
}
fn hangup_cause(&self) -> Result<Option<HangupCause>, ParseHangupCauseError> {
parse_opt(self.header(EventHeader::HangupCause))
}
fn loopback_resignation(&self) -> Option<LoopbackResignation<'_>> {
self.variable(LoopbackVariable::LoopbackHangupCause)
.map(|cause| {
LoopbackResignation::new(
cause,
self.variable(LoopbackVariable::LoopbackBowoutOtherUuid),
)
})
}
fn event_subclass(&self) -> Option<&str> {
self.header(EventHeader::EventSubclass)
}
fn sofia_event_subclass(
&self,
) -> Result<Option<SofiaEventSubclass>, ParseSofiaEventSubclassError> {
parse_opt(self.event_subclass())
}
fn gateway(&self) -> Option<&str> {
self.header(EventHeader::Gateway)
}
fn profile_name(&self) -> Option<&str> {
self.first_header([EventHeader::ProfileName, EventHeader::ProfileNameSnake])
}
fn module_name(&self) -> Option<&str> {
self.header(EventHeader::ModuleName)
}
fn profile_uri(&self) -> Option<&str> {
self.header(EventHeader::ProfileUri)
}
fn phrase(&self) -> Option<&str> {
self.header(EventHeader::Phrase)
}
fn sip_status_code(&self) -> Result<Option<u16>, std::num::ParseIntError> {
parse_opt(self.header(EventHeader::Status))
}
fn gateway_reg_state(&self) -> Result<Option<GatewayRegState>, ParseGatewayRegStateError> {
parse_opt(self.header(EventHeader::State))
}
fn gateway_ping_status(
&self,
) -> Result<Option<GatewayPingStatus>, ParseGatewayPingStatusError> {
parse_opt(self.header(EventHeader::PingStatus))
}
fn sip_user_ping_status(
&self,
) -> Result<Option<SipUserPingStatus>, ParseSipUserPingStatusError> {
parse_opt(self.header(EventHeader::PingStatus))
}
fn pl_data(&self) -> Option<&str> {
self.header(EventHeader::PlData)
}
fn sip_event(&self) -> Option<&str> {
self.header(EventHeader::SipEvent)
}
fn gateway_name(&self) -> Option<&str> {
self.header(EventHeader::GatewayName)
}
fn channel_state(&self) -> Result<Option<ChannelState>, ParseChannelStateError> {
parse_opt(self.header(EventHeader::ChannelState))
}
fn channel_state_number(&self) -> Result<Option<ChannelState>, ParseChannelStateError> {
let Some(raw) = self.header(EventHeader::ChannelStateNumber) else {
return Ok(None);
};
let rejected = || ParseChannelStateError(raw.to_string());
parse_opt::<u8>(Some(raw))
.map_err(|_| rejected())?
.and_then(ChannelState::from_number)
.ok_or_else(rejected)
.map(Some)
}
fn is_terminal_channel_state(&self) -> Result<bool, ParseChannelStateError> {
Ok(self
.channel_state()?
.is_some_and(|state| state.is_terminal()))
}
fn call_state(&self) -> Result<Option<CallState>, ParseCallStateError> {
parse_opt(self.header(EventHeader::ChannelCallState))
}
fn answer_state(&self) -> Result<Option<AnswerState>, ParseAnswerStateError> {
parse_opt(self.header(EventHeader::AnswerState))
}
fn call_direction(&self) -> Result<Option<CallDirection>, ParseCallDirectionError> {
parse_opt(self.header(EventHeader::CallDirection))
}
#[cfg(feature = "esl")]
fn priority(&self) -> Result<Option<EslEventPriority>, ParsePriorityError> {
parse_opt(self.header(EventHeader::Priority))
}
fn timetable(&self, prefix: &str) -> Result<Option<ChannelTimetable>, ParseTimetableError> {
ChannelTimetable::from_lookup(prefix, |key| self.header_str(key))
}
fn caller_timetable(&self) -> Result<Option<ChannelTimetable>, ParseTimetableError> {
self.timetable("Caller")
}
fn other_leg_timetable(&self) -> Result<Option<ChannelTimetable>, ParseTimetableError> {
self.timetable("Other-Leg")
}
}
impl HeaderLookup for std::collections::HashMap<String, String> {
fn header_str(&self, name: &str) -> Option<&str> {
self.get(name)
.map(|s| s.as_str())
}
fn variable_str(&self, name: &str) -> Option<&str> {
self.get(&variable_key(name))
.map(|s| s.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseHeaderError {
ChannelState(ParseChannelStateError),
CallState(ParseCallStateError),
AnswerState(ParseAnswerStateError),
CallDirection(ParseCallDirectionError),
HangupCause(ParseHangupCauseError),
GatewayRegState(ParseGatewayRegStateError),
LoopbackLeg(ParseLoopbackLegError),
#[cfg(feature = "esl")]
Priority(ParsePriorityError),
Timetable(ParseTimetableError),
SipStatusCode(std::num::ParseIntError),
}
macro_rules! parse_header_error_impls {
(
owned { $($(#[$attr:meta])* $Variant:ident($Error:ty)),+ $(,)? }
borrowed { $($(#[$battr:meta])* $BVariant:ident),+ $(,)? }
) => {
$(
$(#[$attr])*
impl From<$Error> for ParseHeaderError {
fn from(e: $Error) -> Self {
Self::$Variant(e)
}
}
)+
impl ::std::fmt::Display for ParseHeaderError {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
match self {
$($(#[$attr])* Self::$Variant(e) => e.fmt(f),)+
$($(#[$battr])* Self::$BVariant(e) => e.fmt(f),)+
}
}
}
impl ::std::error::Error for ParseHeaderError {
fn source(&self) -> Option<&(dyn ::std::error::Error + 'static)> {
match self {
$($(#[$attr])* Self::$Variant(e) => Some(e),)+
$($(#[$battr])* Self::$BVariant(e) => Some(e),)+
}
}
}
};
}
parse_header_error_impls! {
owned {
ChannelState(ParseChannelStateError),
CallState(ParseCallStateError),
AnswerState(ParseAnswerStateError),
CallDirection(ParseCallDirectionError),
HangupCause(ParseHangupCauseError),
GatewayRegState(ParseGatewayRegStateError),
LoopbackLeg(ParseLoopbackLegError),
#[cfg(feature = "esl")]
Priority(ParsePriorityError),
Timetable(ParseTimetableError),
}
borrowed {
SipStatusCode,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::variables::{
ChannelVariable, LoopbackChannelName, LoopbackHangupCause, LoopbackLeg,
};
use std::collections::HashMap;
use std::error::Error;
#[test]
fn every_accessor_error_converts_into_the_union() {
let cases: Vec<(ParseHeaderError, &str)> = vec![
(
ParseChannelStateError("x".into()).into(),
"ChannelState(ParseChannelStateError",
),
(
ParseCallStateError("x".into()).into(),
"CallState(ParseCallStateError",
),
(
ParseAnswerStateError("x".into()).into(),
"AnswerState(ParseAnswerStateError",
),
(
ParseCallDirectionError("x".into()).into(),
"CallDirection(ParseCallDirectionError",
),
(
ParseHangupCauseError("x".into()).into(),
"HangupCause(ParseHangupCauseError",
),
(
ParseGatewayRegStateError("x".into()).into(),
"GatewayRegState(ParseGatewayRegStateError",
),
(
ParseLoopbackLegError("x".into()).into(),
"LoopbackLeg(ParseLoopbackLegError",
),
(
ParseTimetableError::new("Caller-Channel-Created-Time", "x").into(),
"Timetable(ParseTimetableError",
),
(
ParseHeaderError::SipStatusCode(
"x".parse::<u16>()
.expect_err("non-numeric must fail"),
),
"SipStatusCode(ParseIntError",
),
];
for (err, expected) in cases {
assert!(
format!("{err:?}").starts_with(expected),
"{err:?} should be {expected}"
);
assert!(
err.source()
.is_some(),
"{err:?} must expose its inner error as source"
);
}
}
#[cfg(feature = "esl")]
#[test]
fn priority_error_converts_into_the_union() {
let err: ParseHeaderError = ParsePriorityError("x".into()).into();
assert!(matches!(err, ParseHeaderError::Priority(_)));
assert!(err
.source()
.is_some());
}
#[test]
fn display_delegates_to_the_inner_error() {
let inner = ParseChannelStateError("CS_NONSENSE".into());
let expected = inner.to_string();
let err: ParseHeaderError = inner.into();
assert_eq!(err.to_string(), expected);
}
struct TestStore(HashMap<String, String>);
impl SipHeaderLookup for TestStore {
fn sip_header_str(&self, name: &str) -> Option<&str> {
self.0
.get(name)
.map(|s| s.as_str())
}
}
impl HeaderLookup for TestStore {
fn header_str(&self, name: &str) -> Option<&str> {
self.0
.get(name)
.map(|s| s.as_str())
}
fn variable_str(&self, name: &str) -> Option<&str> {
self.0
.get(&variable_key(name))
.map(|s| s.as_str())
}
}
fn store_with(pairs: &[(&str, &str)]) -> TestStore {
let map: HashMap<String, String> = pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
TestStore(map)
}
#[test]
fn header_str_direct() {
let s = store_with(&[("Unique-ID", "abc-123")]);
assert_eq!(s.header_str("Unique-ID"), Some("abc-123"));
assert_eq!(s.header_str("Missing"), None);
}
#[test]
fn header_by_enum() {
let s = store_with(&[("Unique-ID", "abc-123")]);
assert_eq!(s.header(EventHeader::UniqueId), Some("abc-123"));
}
#[test]
fn variable_str_direct() {
let s = store_with(&[("variable_read_codec", "PCMU")]);
assert_eq!(s.variable_str("read_codec"), Some("PCMU"));
assert_eq!(s.variable_str("missing"), None);
}
#[test]
fn variable_by_enum() {
let s = store_with(&[("variable_read_codec", "PCMU")]);
assert_eq!(s.variable(ChannelVariable::ReadCodec), Some("PCMU"));
}
#[test]
fn unique_id_primary() {
let s = store_with(&[("Unique-ID", "uuid-1")]);
assert_eq!(s.unique_id(), Some("uuid-1"));
}
#[test]
fn unique_id_fallback() {
let s = store_with(&[("Caller-Unique-ID", "uuid-2")]);
assert_eq!(s.unique_id(), Some("uuid-2"));
}
#[test]
fn unique_id_none() {
let s = store_with(&[]);
assert_eq!(s.unique_id(), None);
}
#[test]
fn job_uuid() {
let s = store_with(&[("Job-UUID", "job-1")]);
assert_eq!(s.job_uuid(), Some("job-1"));
}
#[test]
fn channel_name() {
let s = store_with(&[("Channel-Name", "sofia/internal/1000@example.com")]);
assert_eq!(s.channel_name(), Some("sofia/internal/1000@example.com"));
}
#[test]
fn caller_id_number_and_name() {
let s = store_with(&[
("Caller-Caller-ID-Number", "1000"),
("Caller-Caller-ID-Name", "Alice"),
]);
assert_eq!(s.caller_id_number(), Some("1000"));
assert_eq!(s.caller_id_name(), Some("Alice"));
}
#[test]
fn hangup_cause_typed() {
let s = store_with(&[("Hangup-Cause", "NORMAL_CLEARING")]);
assert_eq!(
s.hangup_cause()
.unwrap(),
Some(crate::channel::HangupCause::NormalClearing)
);
}
#[test]
fn hangup_cause_invalid_is_error() {
let s = store_with(&[("Hangup-Cause", "BOGUS_CAUSE")]);
assert!(s
.hangup_cause()
.is_err());
}
#[test]
fn destination_number() {
let s = store_with(&[("Caller-Destination-Number", "1000")]);
assert_eq!(s.destination_number(), Some("1000"));
}
#[test]
fn callee_id() {
let s = store_with(&[
("Caller-Callee-ID-Number", "2000"),
("Caller-Callee-ID-Name", "Bob"),
]);
assert_eq!(s.callee_id_number(), Some("2000"));
assert_eq!(s.callee_id_name(), Some("Bob"));
}
#[test]
fn event_subclass() {
let s = store_with(&[("Event-Subclass", "sofia::register")]);
assert_eq!(s.event_subclass(), Some("sofia::register"));
}
#[test]
fn sofia_event_subclass_typed() {
let s = store_with(&[("Event-Subclass", "sofia::gateway_state")]);
assert_eq!(
s.sofia_event_subclass()
.unwrap(),
Some(crate::sofia::SofiaEventSubclass::GatewayState)
);
}
#[test]
fn sofia_event_subclass_absent() {
let s = store_with(&[]);
assert_eq!(
s.sofia_event_subclass()
.unwrap(),
None
);
}
#[test]
fn sofia_event_subclass_non_sofia_is_error() {
let s = store_with(&[("Event-Subclass", "conference::maintenance")]);
assert!(s
.sofia_event_subclass()
.is_err());
}
#[test]
fn gateway_reg_state_typed() {
let s = store_with(&[("State", "REGED")]);
assert_eq!(
s.gateway_reg_state()
.unwrap(),
Some(crate::sofia::GatewayRegState::Reged)
);
}
#[test]
fn gateway_reg_state_invalid_is_error() {
let s = store_with(&[("State", "BOGUS")]);
assert!(s
.gateway_reg_state()
.is_err());
}
#[test]
fn gateway_ping_status_typed() {
let s = store_with(&[("Ping-Status", "UP")]);
assert_eq!(
s.gateway_ping_status()
.unwrap(),
Some(crate::sofia::GatewayPingStatus::Up)
);
}
#[test]
fn sip_user_ping_status_typed() {
let s = store_with(&[("Ping-Status", "REACHABLE")]);
assert_eq!(
s.sip_user_ping_status()
.unwrap(),
Some(crate::sofia::SipUserPingStatus::Reachable)
);
}
#[test]
fn gateway_accessor() {
let s = store_with(&[("Gateway", "my-gateway")]);
assert_eq!(s.gateway(), Some("my-gateway"));
}
#[test]
fn profile_name_accessor() {
let s = store_with(&[("profile-name", "internal")]);
assert_eq!(s.profile_name(), Some("internal"));
}
#[test]
fn profile_name_spellings_resolve_distinct_variants() {
let hyphen = store_with(&[("profile-name", "internal")]);
assert_eq!(hyphen.header(EventHeader::ProfileName), Some("internal"));
assert_eq!(hyphen.header(EventHeader::ProfileNameSnake), None);
assert_eq!(hyphen.profile_name(), Some("internal"));
let underscore = store_with(&[("profile_name", "external")]);
assert_eq!(underscore.header(EventHeader::ProfileName), None);
assert_eq!(
underscore.header(EventHeader::ProfileNameSnake),
Some("external")
);
assert_eq!(underscore.profile_name(), Some("external"));
}
#[test]
fn profile_name_probes_hyphen_before_underscore() {
let s = store_with(&[("profile-name", "internal"), ("profile_name", "external")]);
assert_eq!(s.profile_name(), Some("internal"));
}
#[test]
fn module_name_accessor() {
let s = store_with(&[("module_name", "mod_sofia")]);
assert_eq!(s.module_name(), Some("mod_sofia"));
}
#[test]
fn profile_uri_accessor() {
let s = store_with(&[("profile_uri", "sip:mod_sofia@192.0.2.1:5060")]);
assert_eq!(s.profile_uri(), Some("sip:mod_sofia@192.0.2.1:5060"));
}
#[test]
fn phrase_accessor() {
let s = store_with(&[("Phrase", "OK")]);
assert_eq!(s.phrase(), Some("OK"));
}
#[test]
fn channel_state_typed() {
let s = store_with(&[("Channel-State", "CS_EXECUTE")]);
assert_eq!(
s.channel_state()
.unwrap(),
Some(ChannelState::CsExecute)
);
}
#[test]
fn channel_state_number_typed() {
let s = store_with(&[("Channel-State-Number", "4")]);
assert_eq!(
s.channel_state_number()
.unwrap(),
Some(ChannelState::CsExecute)
);
}
#[test]
fn terminal_state_is_the_final_channel_state_event() {
let s = store_with(&[("Channel-State", "CS_DESTROY")]);
assert_eq!(s.is_terminal_channel_state(), Ok(true));
}
#[test]
fn channel_destroy_shape_is_not_terminal() {
let s = store_with(&[
("Channel-State", "CS_REPORTING"),
("Channel-State-Number", "12"),
]);
assert_eq!(s.is_terminal_channel_state(), Ok(false));
}
#[test]
fn hangup_state_is_not_terminal() {
let s = store_with(&[("Channel-State", "CS_HANGUP")]);
assert_eq!(s.is_terminal_channel_state(), Ok(false));
}
#[test]
fn unparseable_channel_state_is_an_error() {
let s = store_with(&[("Channel-State", "CS_BOGUS")]);
assert!(s
.is_terminal_channel_state()
.is_err());
}
#[test]
fn missing_channel_state_is_not_terminal() {
let s = store_with(&[("Event-Name", "HEARTBEAT")]);
assert_eq!(s.is_terminal_channel_state(), Ok(false));
}
#[test]
fn call_state_typed() {
let s = store_with(&[("Channel-Call-State", "ACTIVE")]);
assert_eq!(
s.call_state()
.unwrap(),
Some(CallState::Active)
);
}
#[test]
fn answer_state_typed() {
let s = store_with(&[("Answer-State", "answered")]);
assert_eq!(
s.answer_state()
.unwrap(),
Some(AnswerState::Answered)
);
}
#[test]
fn call_direction_typed() {
let s = store_with(&[("Call-Direction", "inbound")]);
assert_eq!(
s.call_direction()
.unwrap(),
Some(CallDirection::Inbound)
);
}
#[cfg(feature = "esl")]
#[test]
fn priority_typed() {
let s = store_with(&[("priority", "HIGH")]);
assert_eq!(
s.priority()
.unwrap(),
Some(EslEventPriority::High)
);
assert_eq!(
store_with(&[])
.priority()
.unwrap(),
None
);
assert!(store_with(&[("priority", "BOGUS")])
.priority()
.is_err());
}
#[test]
fn timetable_extraction() {
let s = store_with(&[
("Caller-Channel-Created-Time", "1700000001000000"),
("Caller-Channel-Answered-Time", "1700000005000000"),
]);
let tt = s
.caller_timetable()
.unwrap()
.expect("should have timetable");
assert_eq!(tt.created, Some(1700000001000000));
assert_eq!(tt.answered, Some(1700000005000000));
assert_eq!(tt.hungup, None);
}
#[test]
fn timetable_other_leg() {
let s = store_with(&[("Other-Leg-Channel-Created-Time", "1700000001000000")]);
let tt = s
.other_leg_timetable()
.unwrap()
.expect("should have timetable");
assert_eq!(tt.created, Some(1700000001000000));
}
#[test]
fn timetable_none_when_absent() {
let s = store_with(&[]);
assert_eq!(
s.caller_timetable()
.unwrap(),
None
);
}
#[test]
fn timetable_invalid_is_error() {
let s = store_with(&[("Caller-Channel-Created-Time", "not_a_number")]);
let err = s
.caller_timetable()
.unwrap_err();
assert_eq!(err.header, "Caller-Channel-Created-Time");
}
#[test]
fn loopback_resignation_absent_on_a_real_teardown() {
let s = store_with(&[("variable_hangup_cause", "NORMAL_CLEARING")]);
assert!(s
.loopback_resignation()
.is_none());
}
#[test]
fn loopback_resignation_on_execute_masquerade_path() {
let s = store_with(&[
("variable_loopback_hangup_cause", "bowout"),
("variable_loopback_bowout_other_uuid", "survivor-uuid"),
]);
let r = s
.loopback_resignation()
.expect("marker present");
assert_eq!(r.cause(), Ok(LoopbackHangupCause::Bowout));
assert_eq!(r.cause_raw(), "bowout");
assert_eq!(r.other_uuid(), Some("survivor-uuid"));
}
#[test]
fn loopback_resignation_on_frame_count_path() {
let s = store_with(&[
("variable_loopback_hangup_cause", "bridge"),
("variable_loopback_bowout_other_uuid", "survivor-uuid"),
]);
let r = s
.loopback_resignation()
.expect("marker present");
assert_eq!(r.cause(), Ok(LoopbackHangupCause::Bridge));
assert_eq!(r.other_uuid(), Some("survivor-uuid"));
}
#[test]
fn loopback_resignation_survives_an_unknown_token() {
let s = store_with(&[
("variable_loopback_hangup_cause", "some_future_path"),
("variable_loopback_bowout_other_uuid", "survivor-uuid"),
]);
let r = s
.loopback_resignation()
.expect("presence is the signal, not the value");
assert_eq!(r.other_uuid(), Some("survivor-uuid"));
assert_eq!(r.cause_raw(), "some_future_path");
assert!(r
.cause()
.is_err());
}
#[test]
fn loopback_resignation_without_a_surviving_uuid() {
let s = store_with(&[("variable_loopback_hangup_cause", "bridge")]);
let r = s
.loopback_resignation()
.expect("marker present");
assert_eq!(r.other_uuid(), None);
}
#[test]
fn the_resigning_leg_names_itself() {
let leg = store_with(&[
("Channel-Name", "loopback/9199-a"),
("variable_loopback_hangup_cause", "bowout"),
("variable_loopback_leg", "A"),
]);
assert_eq!(leg.channel_driver(), Some("loopback"));
let name = leg
.channel_name()
.and_then(LoopbackChannelName::parse)
.expect("a loopback name");
assert_eq!(name.extension(), "9199");
assert_eq!(name.leg(), LoopbackLeg::A);
assert_eq!(
leg.loopback_leg()
.unwrap(),
Some(LoopbackLeg::A)
);
}
#[test]
fn a_survivor_that_inherited_the_marker_is_not_a_loopback() {
let survivor = store_with(&[
("Channel-Name", "sofia/internal/1000@example.com"),
("Caller-Channel-Name", "loopback/9199-a"),
("variable_loopback_hangup_cause", "bowout"),
("variable_is_loopback", "true"),
("variable_loopback_leg", "A"),
]);
assert!(survivor
.loopback_resignation()
.is_some());
assert_eq!(
survivor
.loopback_leg()
.unwrap(),
Some(LoopbackLeg::A)
);
assert_eq!(survivor.channel_driver(), Some("sofia"));
assert_eq!(
survivor
.channel_name()
.and_then(LoopbackChannelName::parse),
None
);
}
#[test]
fn channel_kind_accessors_absent() {
let s = store_with(&[]);
assert_eq!(s.channel_driver(), None);
assert_eq!(
s.loopback_leg()
.unwrap(),
None
);
}
#[test]
fn loopback_leg_invalid_is_error() {
let s = store_with(&[("variable_loopback_leg", "a")]);
assert!(s
.loopback_leg()
.is_err());
}
type RawAccessor = (&'static str, fn(&TestStore) -> Option<&str>);
type TypedAccessor = (&'static str, fn(&TestStore) -> bool, fn(&TestStore) -> bool);
const RAW_ACCESSORS: &[RawAccessor] = &[
("channel_name", |s| s.channel_name()),
("caller_id_number", |s| s.caller_id_number()),
("caller_id_name", |s| s.caller_id_name()),
("destination_number", |s| s.destination_number()),
("callee_id_number", |s| s.callee_id_number()),
("callee_id_name", |s| s.callee_id_name()),
("event_subclass", |s| s.event_subclass()),
("job_uuid", |s| s.job_uuid()),
("pl_data", |s| s.pl_data()),
("sip_event", |s| s.sip_event()),
("gateway_name", |s| s.gateway_name()),
("channel_presence_id", |s| s.channel_presence_id()),
("event_date_timestamp", |s| s.event_date_timestamp()),
("event_sequence", |s| s.event_sequence()),
("dtmf_duration", |s| s.dtmf_duration()),
("dtmf_source", |s| s.dtmf_source()),
];
const TYPED_ACCESSORS: &[TypedAccessor] = &[
(
"channel_state",
|s| {
s.channel_state()
.unwrap()
.is_none()
},
|s| {
s.channel_state()
.is_err()
},
),
(
"channel_state_number",
|s| {
s.channel_state_number()
.unwrap()
.is_none()
},
|s| {
s.channel_state_number()
.is_err()
},
),
(
"call_state",
|s| {
s.call_state()
.unwrap()
.is_none()
},
|s| {
s.call_state()
.is_err()
},
),
(
"answer_state",
|s| {
s.answer_state()
.unwrap()
.is_none()
},
|s| {
s.answer_state()
.is_err()
},
),
(
"call_direction",
|s| {
s.call_direction()
.unwrap()
.is_none()
},
|s| {
s.call_direction()
.is_err()
},
),
(
"presence_call_direction",
|s| {
s.presence_call_direction()
.unwrap()
.is_none()
},
|s| {
s.presence_call_direction()
.is_err()
},
),
(
"hangup_cause",
|s| {
s.hangup_cause()
.unwrap()
.is_none()
},
|s| {
s.hangup_cause()
.is_err()
},
),
];
#[test]
fn missing_headers_return_none() {
let s = store_with(&[]);
for (name, accessor) in RAW_ACCESSORS {
assert_eq!(accessor(&s), None, "{name}");
}
for (name, absent_is_none, _) in TYPED_ACCESSORS {
assert!(absent_is_none(&s), "{name}");
}
}
#[test]
fn notify_in_headers() {
let s = store_with(&[
("pl_data", r#"{"invite":"INVITE ..."}"#),
("event", "emergency-AbandonedCall"),
("gateway_name", "ng911-bcf"),
]);
assert_eq!(s.pl_data(), Some(r#"{"invite":"INVITE ..."}"#));
assert_eq!(s.sip_event(), Some("emergency-AbandonedCall"));
assert_eq!(s.gateway_name(), Some("ng911-bcf"));
}
#[test]
fn channel_presence_id() {
let s = store_with(&[("Channel-Presence-ID", "1000@example.com")]);
assert_eq!(s.channel_presence_id(), Some("1000@example.com"));
}
#[test]
fn presence_call_direction_typed() {
let s = store_with(&[("Presence-Call-Direction", "outbound")]);
assert_eq!(
s.presence_call_direction()
.unwrap(),
Some(CallDirection::Outbound)
);
}
#[test]
fn event_date_timestamp() {
let s = store_with(&[("Event-Date-Timestamp", "1700000001000000")]);
assert_eq!(s.event_date_timestamp(), Some("1700000001000000"));
}
#[test]
fn event_sequence() {
let s = store_with(&[("Event-Sequence", "12345")]);
assert_eq!(s.event_sequence(), Some("12345"));
}
#[test]
fn dtmf_duration() {
let s = store_with(&[("DTMF-Duration", "2000")]);
assert_eq!(s.dtmf_duration(), Some("2000"));
}
#[test]
fn dtmf_source() {
let s = store_with(&[("DTMF-Source", "rtp")]);
assert_eq!(s.dtmf_source(), Some("rtp"));
}
#[test]
fn invalid_values_return_err() {
let s = store_with(&[
("Channel-State", "BOGUS"),
("Channel-State-Number", "999"),
("Channel-Call-State", "BOGUS"),
("Answer-State", "bogus"),
("Call-Direction", "bogus"),
("Presence-Call-Direction", "bogus"),
("Hangup-Cause", "BOGUS"),
]);
for (name, _, unparseable_is_err) in TYPED_ACCESSORS {
assert!(unparseable_is_err(&s), "{name}");
}
}
}