1use std::fmt;
4
5wire_enum! {
6 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
9 #[repr(u8)]
10 pub enum ChannelState {
11 CsNew = 0 => "CS_NEW",
12 CsInit = 1 => "CS_INIT",
13 CsRouting = 2 => "CS_ROUTING",
14 CsSoftExecute = 3 => "CS_SOFT_EXECUTE",
15 CsExecute = 4 => "CS_EXECUTE",
16 CsExchangeMedia = 5 => "CS_EXCHANGE_MEDIA",
17 CsPark = 6 => "CS_PARK",
18 CsConsumeMedia = 7 => "CS_CONSUME_MEDIA",
19 CsHibernate = 8 => "CS_HIBERNATE",
20 CsReset = 9 => "CS_RESET",
21 CsHangup = 10 => "CS_HANGUP",
22 CsReporting = 11 => "CS_REPORTING",
23 CsDestroy = 12 => "CS_DESTROY",
24 CsNone = 13 => "CS_NONE",
25 }
26 error ParseChannelStateError("channel state");
27 tests: channel_state_wire_tests;
28}
29
30impl ChannelState {
31 pub fn from_number(n: u8) -> Option<Self> {
33 match n {
34 0 => Some(Self::CsNew),
35 1 => Some(Self::CsInit),
36 2 => Some(Self::CsRouting),
37 3 => Some(Self::CsSoftExecute),
38 4 => Some(Self::CsExecute),
39 5 => Some(Self::CsExchangeMedia),
40 6 => Some(Self::CsPark),
41 7 => Some(Self::CsConsumeMedia),
42 8 => Some(Self::CsHibernate),
43 9 => Some(Self::CsReset),
44 10 => Some(Self::CsHangup),
45 11 => Some(Self::CsReporting),
46 12 => Some(Self::CsDestroy),
47 13 => Some(Self::CsNone),
48 _ => None,
49 }
50 }
51
52 pub fn as_number(&self) -> u8 {
54 *self as u8
55 }
56
57 pub fn is_terminal(&self) -> bool {
69 matches!(self, Self::CsDestroy)
70 }
71}
72
73pub fn channel_driver(name: &str) -> Option<&str> {
81 let (driver, _) = name.split_once('/')?;
82 if driver.is_empty() {
83 return None;
84 }
85 Some(driver)
86}
87
88wire_enum! {
89 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
91 pub enum CallState {
92 Down => "DOWN",
93 Dialing => "DIALING",
94 Ringing => "RINGING",
95 Early => "EARLY",
96 Active => "ACTIVE",
97 Held => "HELD",
98 RingWait => "RING_WAIT",
99 Hangup => "HANGUP",
100 Unheld => "UNHELD",
101 }
102 error ParseCallStateError("call state");
103 tests: call_state_wire_tests;
104}
105
106wire_enum! {
107 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
109 pub enum AnswerState {
110 Hangup => "hangup",
111 Answered => "answered",
112 Early => "early",
113 Ringing => "ringing",
114 }
115 error ParseAnswerStateError("answer state");
116 tests: answer_state_wire_tests;
117}
118
119wire_enum! {
120 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
122 pub enum CallDirection {
123 Inbound => "inbound",
124 Outbound => "outbound",
125 }
126 error ParseCallDirectionError("call direction");
127 tests: call_direction_wire_tests;
128}
129
130wire_enum! {
131 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
137 #[repr(u16)]
138 pub enum HangupCause {
139 None = 0 => "NONE",
140 UnallocatedNumber = 1 => "UNALLOCATED_NUMBER",
141 NoRouteTransitNet = 2 => "NO_ROUTE_TRANSIT_NET",
142 NoRouteDestination = 3 => "NO_ROUTE_DESTINATION",
143 ChannelUnacceptable = 6 => "CHANNEL_UNACCEPTABLE",
144 CallAwardedDelivered = 7 => "CALL_AWARDED_DELIVERED",
145 NormalClearing = 16 => "NORMAL_CLEARING",
146 UserBusy = 17 => "USER_BUSY",
147 NoUserResponse = 18 => "NO_USER_RESPONSE",
148 NoAnswer = 19 => "NO_ANSWER",
149 SubscriberAbsent = 20 => "SUBSCRIBER_ABSENT",
150 CallRejected = 21 => "CALL_REJECTED",
151 NumberChanged = 22 => "NUMBER_CHANGED",
152 RedirectionToNewDestination = 23 => "REDIRECTION_TO_NEW_DESTINATION",
153 ExchangeRoutingError = 25 => "EXCHANGE_ROUTING_ERROR",
154 DestinationOutOfOrder = 27 => "DESTINATION_OUT_OF_ORDER",
155 InvalidNumberFormat = 28 => "INVALID_NUMBER_FORMAT",
156 FacilityRejected = 29 => "FACILITY_REJECTED",
157 ResponseToStatusEnquiry = 30 => "RESPONSE_TO_STATUS_ENQUIRY",
158 NormalUnspecified = 31 => "NORMAL_UNSPECIFIED",
159 NormalCircuitCongestion = 34 => "NORMAL_CIRCUIT_CONGESTION",
160 NetworkOutOfOrder = 38 => "NETWORK_OUT_OF_ORDER",
161 NormalTemporaryFailure = 41 => "NORMAL_TEMPORARY_FAILURE",
162 SwitchCongestion = 42 => "SWITCH_CONGESTION",
163 AccessInfoDiscarded = 43 => "ACCESS_INFO_DISCARDED",
164 RequestedChanUnavail = 44 => "REQUESTED_CHAN_UNAVAIL",
165 PreEmpted = 45 => "PRE_EMPTED",
166 FacilityNotSubscribed = 50 => "FACILITY_NOT_SUBSCRIBED",
167 OutgoingCallBarred = 52 => "OUTGOING_CALL_BARRED",
168 IncomingCallBarred = 54 => "INCOMING_CALL_BARRED",
169 BearercapabilityNotauth = 57 => "BEARERCAPABILITY_NOTAUTH",
170 BearercapabilityNotavail = 58 => "BEARERCAPABILITY_NOTAVAIL",
171 ServiceUnavailable = 63 => "SERVICE_UNAVAILABLE",
172 BearercapabilityNotimpl = 65 => "BEARERCAPABILITY_NOTIMPL",
173 ChanNotImplemented = 66 => "CHAN_NOT_IMPLEMENTED",
174 FacilityNotImplemented = 69 => "FACILITY_NOT_IMPLEMENTED",
175 ServiceNotImplemented = 79 => "SERVICE_NOT_IMPLEMENTED",
176 InvalidCallReference = 81 => "INVALID_CALL_REFERENCE",
177 IncompatibleDestination = 88 => "INCOMPATIBLE_DESTINATION",
178 InvalidMsgUnspecified = 95 => "INVALID_MSG_UNSPECIFIED",
179 MandatoryIeMissing = 96 => "MANDATORY_IE_MISSING",
180 MessageTypeNonexist = 97 => "MESSAGE_TYPE_NONEXIST",
181 WrongMessage = 98 => "WRONG_MESSAGE",
182 IeNonexist = 99 => "IE_NONEXIST",
183 InvalidIeContents = 100 => "INVALID_IE_CONTENTS",
184 WrongCallState = 101 => "WRONG_CALL_STATE",
185 RecoveryOnTimerExpire = 102 => "RECOVERY_ON_TIMER_EXPIRE",
186 MandatoryIeLengthError = 103 => "MANDATORY_IE_LENGTH_ERROR",
187 ProtocolError = 111 => "PROTOCOL_ERROR",
188 Interworking = 127 => "INTERWORKING",
189 Success = 142 => "SUCCESS",
190 OriginatorCancel = 487 => "ORIGINATOR_CANCEL",
191 Crash = 700 => "CRASH",
192 SystemShutdown = 701 => "SYSTEM_SHUTDOWN",
193 LoseRace = 702 => "LOSE_RACE",
194 ManagerRequest = 703 => "MANAGER_REQUEST",
195 BlindTransfer = 800 => "BLIND_TRANSFER",
196 AttendedTransfer = 801 => "ATTENDED_TRANSFER",
197 AllottedTimeout = 802 => "ALLOTTED_TIMEOUT",
198 UserChallenge = 803 => "USER_CHALLENGE",
199 MediaTimeout = 804 => "MEDIA_TIMEOUT",
200 PickedOff = 805 => "PICKED_OFF",
201 UserNotRegistered = 806 => "USER_NOT_REGISTERED",
202 ProgressTimeout = 807 => "PROGRESS_TIMEOUT",
203 InvalidGateway = 808 => "INVALID_GATEWAY",
204 GatewayDown = 809 => "GATEWAY_DOWN",
205 InvalidUrl = 810 => "INVALID_URL",
206 InvalidProfile = 811 => "INVALID_PROFILE",
207 NoPickup = 812 => "NO_PICKUP",
208 SrtpReadError = 813 => "SRTP_READ_ERROR",
209 Bowout = 814 => "BOWOUT",
210 BusyEverywhere = 815 => "BUSY_EVERYWHERE",
211 Decline = 816 => "DECLINE",
212 DoesNotExistAnywhere = 817 => "DOES_NOT_EXIST_ANYWHERE",
213 NotAcceptable = 818 => "NOT_ACCEPTABLE",
214 Unwanted = 819 => "UNWANTED",
215 NoIdentity = 820 => "NO_IDENTITY",
216 BadIdentityInfo = 821 => "BAD_IDENTITY_INFO",
217 UnsupportedCertificate = 822 => "UNSUPPORTED_CERTIFICATE",
218 InvalidIdentity = 823 => "INVALID_IDENTITY",
219 StaleDate = 824 => "STALE_DATE",
221 RejectAll = 825 => "REJECT_ALL",
223 }
224 error ParseHangupCauseError("hangup cause");
225 numeric: from_number(u16);
226 tests: hangup_cause_wire_tests;
227}
228
229impl HangupCause {
230 pub fn from_sip_response(code: u16) -> Option<Self> {
235 match code {
236 200 => Some(Self::NormalClearing),
237 401 | 402 | 403 | 407 | 603 | 608 => Some(Self::CallRejected),
238 607 => Some(Self::Unwanted),
239 404 => Some(Self::UnallocatedNumber),
240 485 | 604 => Some(Self::NoRouteDestination),
241 408 | 504 => Some(Self::RecoveryOnTimerExpire),
242 410 => Some(Self::NumberChanged),
243 413 | 414 | 416 | 420 | 421 | 423 | 505 | 513 => Some(Self::Interworking),
244 480 => Some(Self::NoUserResponse),
245 400 | 481 | 500 | 503 => Some(Self::NormalTemporaryFailure),
246 486 | 600 => Some(Self::UserBusy),
247 484 => Some(Self::InvalidNumberFormat),
248 488 | 606 => Some(Self::IncompatibleDestination),
249 502 => Some(Self::NetworkOutOfOrder),
250 405 => Some(Self::ServiceUnavailable),
251 406 | 415 | 501 => Some(Self::ServiceNotImplemented),
252 482 | 483 => Some(Self::ExchangeRoutingError),
253 487 => Some(Self::OriginatorCancel),
254 428 => Some(Self::NoIdentity),
255 429 => Some(Self::BadIdentityInfo),
256 437 => Some(Self::UnsupportedCertificate),
257 438 => Some(Self::InvalidIdentity),
258 _ => None,
259 }
260 }
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
284#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
285#[non_exhaustive]
286pub struct ChannelTimetable {
287 pub profile_created: Option<i64>,
289 pub created: Option<i64>,
291 pub answered: Option<i64>,
293 pub progress: Option<i64>,
295 pub progress_media: Option<i64>,
297 pub hungup: Option<i64>,
299 pub transferred: Option<i64>,
301 pub resurrected: Option<i64>,
303 pub bridged: Option<i64>,
305 pub last_hold: Option<i64>,
307 pub hold_accum: Option<i64>,
309}
310
311#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
317#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
318#[non_exhaustive]
319pub enum TimetablePrefix {
320 Caller,
322 OtherLeg,
324 Channel,
326 Hunt,
328 Originator,
330 Originatee,
332 PostOriginator,
334 PostOriginatee,
336}
337
338impl TimetablePrefix {
339 pub fn as_str(&self) -> &'static str {
341 match self {
342 Self::Caller => "Caller",
343 Self::OtherLeg => "Other-Leg",
344 Self::Channel => "Channel",
345 Self::Hunt => "Hunt",
346 Self::Originator => "ORIGINATOR",
347 Self::Originatee => "ORIGINATEE",
348 Self::PostOriginator => "POST-ORIGINATOR",
349 Self::PostOriginatee => "POST-ORIGINATEE",
350 }
351 }
352}
353
354impl fmt::Display for TimetablePrefix {
355 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
356 f.write_str(self.as_str())
357 }
358}
359
360impl AsRef<str> for TimetablePrefix {
361 fn as_ref(&self) -> &str {
362 self.as_str()
363 }
364}
365
366#[derive(Debug, Clone, PartialEq, Eq)]
368#[non_exhaustive]
369pub struct ParseTimetableError {
370 pub header: String,
372 pub value: String,
374}
375
376impl fmt::Display for ParseTimetableError {
377 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
378 write!(
379 f,
380 "invalid timetable value for {} ({} bytes)",
381 self.header,
382 self.value
383 .len()
384 )
385 }
386}
387
388impl std::error::Error for ParseTimetableError {}
389
390impl ParseTimetableError {
391 pub fn new(header: impl Into<String>, value: impl Into<String>) -> Self {
393 Self {
394 header: header.into(),
395 value: value.into(),
396 }
397 }
398}
399
400macro_rules! channel_timetable_fields {
406 ($($field:ident => $Variant:ident => $suffix:literal),+ $(,)?) => {
407 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
415 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
416 #[non_exhaustive]
417 pub enum TimetableField {
418 $(
419 #[doc = concat!("`", $suffix, "`, into [`ChannelTimetable::", stringify!($field), "`].")]
420 $Variant,
421 )+
422 }
423
424 impl TimetableField {
425 pub fn as_str(&self) -> &'static str {
427 match self {
428 $(Self::$Variant => $suffix,)+
429 }
430 }
431
432 pub fn header_name(&self, prefix: impl AsRef<str>) -> String {
448 format!("{}-{}", prefix.as_ref(), self.as_str())
449 }
450 }
451
452 impl ChannelTimetable {
453 pub const SUFFIXES: &'static [&'static str] = &[$($suffix),+];
469
470 pub fn from_lookup<'a>(
495 prefix: impl AsRef<str>,
496 lookup: impl Fn(&str) -> Option<&'a str>,
497 ) -> Result<Option<Self>, ParseTimetableError> {
498 let prefix = prefix.as_ref();
499 let mut tt = Self::default();
500 let mut found = false;
501 $(
502 {
503 let header = TimetableField::$Variant.header_name(prefix);
504 if let Some(raw) = lookup(&header) {
505 let v: i64 = raw.parse().map_err(|_| ParseTimetableError {
506 header: header.clone(),
507 value: raw.to_string(),
508 })?;
509 tt.$field = Some(v);
510 found = true;
511 }
512 }
513 )+
514 if found {
515 Ok(Some(tt))
516 } else {
517 Ok(None)
518 }
519 }
520 }
521 };
522}
523
524channel_timetable_fields! {
525 profile_created => ProfileCreated => "Profile-Created-Time",
526 created => Created => "Channel-Created-Time",
527 answered => Answered => "Channel-Answered-Time",
528 progress => Progress => "Channel-Progress-Time",
529 progress_media => ProgressMedia => "Channel-Progress-Media-Time",
530 hungup => Hungup => "Channel-Hangup-Time",
531 transferred => Transferred => "Channel-Transfer-Time",
532 resurrected => Resurrected => "Channel-Resurrect-Time",
533 bridged => Bridged => "Channel-Bridged-Time",
534 last_hold => LastHold => "Channel-Last-Hold",
535 hold_accum => HoldAccum => "Channel-Hold-Accum",
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541 use crate::event::EslEvent;
542 use crate::lookup::HeaderLookup;
543
544 #[test]
549 fn channel_driver_is_the_segment_before_the_first_slash() {
550 assert_eq!(
551 channel_driver("sofia/internal/1000@example.com"),
552 Some("sofia")
553 );
554 assert_eq!(channel_driver("loopback/9199-a"), Some("loopback"));
555 assert_eq!(channel_driver("null/9199"), Some("null"));
556 }
557
558 #[test]
559 fn channel_driver_none_without_a_segment() {
560 for name in ["", "sofia", "/internal/1000"] {
561 assert_eq!(channel_driver(name), None, "{name:?}");
562 }
563 }
564
565 #[test]
566 fn timetable_error_display_omits_the_value() {
567 let err = ParseTimetableError::new("Caller-Channel-Created-Time", "5551234567");
568 let msg = err.to_string();
569 assert!(!msg.contains("5551234567"), "error quoted its input: {msg}");
570 assert!(msg.contains("Caller-Channel-Created-Time"), "{msg}");
571 assert_eq!(err.value, "5551234567");
572 }
573
574 #[test]
575 fn test_channel_state_from_number() {
576 assert_eq!(ChannelState::from_number(0), Some(ChannelState::CsNew));
577 assert_eq!(ChannelState::from_number(4), Some(ChannelState::CsExecute));
578 assert_eq!(ChannelState::from_number(10), Some(ChannelState::CsHangup));
579 assert_eq!(ChannelState::from_number(13), Some(ChannelState::CsNone));
580 assert_eq!(ChannelState::from_number(14), None);
581 assert_eq!(ChannelState::from_number(255), None);
582 }
583
584 #[test]
585 fn test_channel_state_as_number() {
586 assert_eq!(ChannelState::CsNew.as_number(), 0);
587 assert_eq!(ChannelState::CsExecute.as_number(), 4);
588 assert_eq!(ChannelState::CsHangup.as_number(), 10);
589 assert_eq!(ChannelState::CsNone.as_number(), 13);
590 }
591
592 #[test]
593 fn channel_state_ordering_follows_lifecycle() {
594 assert!(ChannelState::CsNew < ChannelState::CsInit);
595 assert!(ChannelState::CsInit < ChannelState::CsRouting);
596 assert!(ChannelState::CsRouting < ChannelState::CsExecute);
597 assert!(ChannelState::CsExecute < ChannelState::CsHangup);
598 assert!(ChannelState::CsHangup < ChannelState::CsReporting);
599 assert!(ChannelState::CsReporting < ChannelState::CsDestroy);
600 }
601
602 #[allow(clippy::nonminimal_bool)]
604 #[test]
605 fn channel_state_teardown_check() {
606 assert!(ChannelState::CsHangup >= ChannelState::CsHangup);
607 assert!(ChannelState::CsReporting >= ChannelState::CsHangup);
608 assert!(ChannelState::CsDestroy >= ChannelState::CsHangup);
609 assert!(!(ChannelState::CsExecute >= ChannelState::CsHangup));
610 assert!(!(ChannelState::CsPark >= ChannelState::CsHangup));
611 }
612
613 #[test]
614 fn only_destroy_is_terminal() {
615 assert!(ChannelState::CsDestroy.is_terminal());
616 for state in [
617 ChannelState::CsNew,
618 ChannelState::CsInit,
619 ChannelState::CsRouting,
620 ChannelState::CsSoftExecute,
621 ChannelState::CsExecute,
622 ChannelState::CsExchangeMedia,
623 ChannelState::CsPark,
624 ChannelState::CsConsumeMedia,
625 ChannelState::CsHibernate,
626 ChannelState::CsReset,
627 ChannelState::CsHangup,
628 ChannelState::CsReporting,
629 ChannelState::CsNone,
630 ] {
631 assert!(!state.is_terminal(), "{state} is not an end of life");
632 }
633 }
634
635 #[test]
639 fn cs_none_outranks_destroy_without_being_terminal() {
640 assert!(ChannelState::CsNone > ChannelState::CsDestroy);
641 assert!(ChannelState::CsNone >= ChannelState::CsHangup);
642 assert!(!ChannelState::CsNone.is_terminal());
643 }
644
645 #[test]
652 fn call_state_ordering_matches_c_enum() {
653 assert!(CallState::Down < CallState::Dialing);
654 assert!(CallState::Dialing < CallState::Ringing);
655 assert!(CallState::Early < CallState::Active);
656 assert!(CallState::Active < CallState::Hangup);
657 }
658
659 #[test]
666 fn hangup_cause_display() {
667 assert_eq!(HangupCause::NormalClearing.to_string(), "NORMAL_CLEARING");
668 assert_eq!(HangupCause::UserBusy.to_string(), "USER_BUSY");
669 assert_eq!(
670 HangupCause::OriginatorCancel.to_string(),
671 "ORIGINATOR_CANCEL"
672 );
673 assert_eq!(HangupCause::None.to_string(), "NONE");
674 }
675
676 #[test]
677 fn hangup_cause_from_str() {
678 assert_eq!(
679 "NORMAL_CLEARING"
680 .parse::<HangupCause>()
681 .unwrap(),
682 HangupCause::NormalClearing
683 );
684 assert_eq!(
685 "USER_BUSY"
686 .parse::<HangupCause>()
687 .unwrap(),
688 HangupCause::UserBusy
689 );
690 }
691
692 #[test]
693 fn hangup_cause_from_str_rejects_wrong_case() {
694 assert!("normal_clearing"
695 .parse::<HangupCause>()
696 .is_err());
697 assert!("User_Busy"
698 .parse::<HangupCause>()
699 .is_err());
700 }
701
702 #[test]
703 fn hangup_cause_from_str_unknown() {
704 assert!("BOGUS_CAUSE"
705 .parse::<HangupCause>()
706 .is_err());
707 }
708
709 #[test]
710 fn hangup_cause_display_round_trip() {
711 let causes = [
712 HangupCause::None,
713 HangupCause::NormalClearing,
714 HangupCause::UserBusy,
715 HangupCause::NoAnswer,
716 HangupCause::OriginatorCancel,
717 HangupCause::BlindTransfer,
718 HangupCause::InvalidIdentity,
719 ];
720 for cause in causes {
721 let s = cause.to_string();
722 let parsed: HangupCause = s
723 .parse()
724 .unwrap();
725 assert_eq!(parsed, cause);
726 }
727 }
728
729 #[test]
730 fn hangup_cause_as_number_q850() {
731 assert_eq!(HangupCause::None.as_number(), 0);
732 assert_eq!(HangupCause::UnallocatedNumber.as_number(), 1);
733 assert_eq!(HangupCause::NormalClearing.as_number(), 16);
734 assert_eq!(HangupCause::UserBusy.as_number(), 17);
735 assert_eq!(HangupCause::NoAnswer.as_number(), 19);
736 assert_eq!(HangupCause::CallRejected.as_number(), 21);
737 assert_eq!(HangupCause::NormalUnspecified.as_number(), 31);
738 assert_eq!(HangupCause::Interworking.as_number(), 127);
739 }
740
741 #[test]
742 fn hangup_cause_as_number_freeswitch_extensions() {
743 assert_eq!(HangupCause::Success.as_number(), 142);
744 assert_eq!(HangupCause::OriginatorCancel.as_number(), 487);
745 assert_eq!(HangupCause::Crash.as_number(), 700);
746 assert_eq!(HangupCause::BlindTransfer.as_number(), 800);
747 assert_eq!(HangupCause::InvalidIdentity.as_number(), 823);
748 }
749
750 #[test]
751 fn hangup_cause_from_number_round_trip() {
752 let codes: &[u16] = &[0, 1, 16, 17, 19, 21, 31, 127, 142, 487, 700, 800, 823];
753 for &code in codes {
754 let cause = HangupCause::from_number(code).unwrap();
755 assert_eq!(cause.as_number(), code);
756 }
757 }
758
759 #[test]
760 fn hangup_cause_from_number_unknown() {
761 assert!(HangupCause::from_number(999).is_none());
762 assert!(HangupCause::from_number(4).is_none());
763 }
764
765 #[test]
768 fn from_sip_response_success() {
769 assert_eq!(
770 HangupCause::from_sip_response(200),
771 Some(HangupCause::NormalClearing)
772 );
773 }
774
775 #[test]
776 fn from_sip_response_4xx_auth_rejection() {
777 for code in [401, 402, 403, 407] {
778 assert_eq!(
779 HangupCause::from_sip_response(code),
780 Some(HangupCause::CallRejected),
781 "SIP {code}"
782 );
783 }
784 }
785
786 #[test]
787 fn from_sip_response_4xx_routing() {
788 assert_eq!(
789 HangupCause::from_sip_response(404),
790 Some(HangupCause::UnallocatedNumber)
791 );
792 assert_eq!(
793 HangupCause::from_sip_response(485),
794 Some(HangupCause::NoRouteDestination)
795 );
796 assert_eq!(
797 HangupCause::from_sip_response(484),
798 Some(HangupCause::InvalidNumberFormat)
799 );
800 assert_eq!(
801 HangupCause::from_sip_response(410),
802 Some(HangupCause::NumberChanged)
803 );
804 }
805
806 #[test]
807 fn from_sip_response_4xx_service() {
808 assert_eq!(
809 HangupCause::from_sip_response(405),
810 Some(HangupCause::ServiceUnavailable)
811 );
812 for code in [406, 415, 501] {
813 assert_eq!(
814 HangupCause::from_sip_response(code),
815 Some(HangupCause::ServiceNotImplemented),
816 "SIP {code}"
817 );
818 }
819 }
820
821 #[test]
822 fn from_sip_response_4xx_interworking() {
823 for code in [413, 414, 416, 420, 421, 423, 505, 513] {
824 assert_eq!(
825 HangupCause::from_sip_response(code),
826 Some(HangupCause::Interworking),
827 "SIP {code}"
828 );
829 }
830 }
831
832 #[test]
833 fn from_sip_response_4xx_timeout_and_busy() {
834 assert_eq!(
835 HangupCause::from_sip_response(408),
836 Some(HangupCause::RecoveryOnTimerExpire)
837 );
838 assert_eq!(
839 HangupCause::from_sip_response(504),
840 Some(HangupCause::RecoveryOnTimerExpire)
841 );
842 assert_eq!(
843 HangupCause::from_sip_response(480),
844 Some(HangupCause::NoUserResponse)
845 );
846 assert_eq!(
847 HangupCause::from_sip_response(486),
848 Some(HangupCause::UserBusy)
849 );
850 assert_eq!(
851 HangupCause::from_sip_response(487),
852 Some(HangupCause::OriginatorCancel)
853 );
854 }
855
856 #[test]
857 fn from_sip_response_4xx_temporary_failure() {
858 for code in [400, 481, 500, 503] {
859 assert_eq!(
860 HangupCause::from_sip_response(code),
861 Some(HangupCause::NormalTemporaryFailure),
862 "SIP {code}"
863 );
864 }
865 }
866
867 #[test]
868 fn from_sip_response_4xx_exchange_routing() {
869 for code in [482, 483] {
870 assert_eq!(
871 HangupCause::from_sip_response(code),
872 Some(HangupCause::ExchangeRoutingError),
873 "SIP {code}"
874 );
875 }
876 }
877
878 #[test]
879 fn from_sip_response_4xx_media() {
880 assert_eq!(
881 HangupCause::from_sip_response(488),
882 Some(HangupCause::IncompatibleDestination)
883 );
884 assert_eq!(
885 HangupCause::from_sip_response(606),
886 Some(HangupCause::IncompatibleDestination)
887 );
888 }
889
890 #[test]
891 fn from_sip_response_5xx() {
892 assert_eq!(
893 HangupCause::from_sip_response(502),
894 Some(HangupCause::NetworkOutOfOrder)
895 );
896 }
897
898 #[test]
899 fn from_sip_response_6xx() {
900 assert_eq!(
901 HangupCause::from_sip_response(600),
902 Some(HangupCause::UserBusy)
903 );
904 assert_eq!(
905 HangupCause::from_sip_response(603),
906 Some(HangupCause::CallRejected)
907 );
908 assert_eq!(
909 HangupCause::from_sip_response(604),
910 Some(HangupCause::NoRouteDestination)
911 );
912 assert_eq!(
913 HangupCause::from_sip_response(607),
914 Some(HangupCause::Unwanted)
915 );
916 assert_eq!(
917 HangupCause::from_sip_response(608),
918 Some(HangupCause::CallRejected)
919 );
920 }
921
922 #[test]
923 fn from_sip_response_stir_shaken() {
924 assert_eq!(
925 HangupCause::from_sip_response(428),
926 Some(HangupCause::NoIdentity)
927 );
928 assert_eq!(
929 HangupCause::from_sip_response(429),
930 Some(HangupCause::BadIdentityInfo)
931 );
932 assert_eq!(
933 HangupCause::from_sip_response(437),
934 Some(HangupCause::UnsupportedCertificate)
935 );
936 assert_eq!(
937 HangupCause::from_sip_response(438),
938 Some(HangupCause::InvalidIdentity)
939 );
940 }
941
942 #[test]
943 fn from_sip_response_unmapped_returns_none() {
944 for code in [100, 180, 183, 301, 302] {
946 assert_eq!(
947 HangupCause::from_sip_response(code),
948 None,
949 "SIP {code} should be None"
950 );
951 }
952 for code in [409, 411, 412, 422, 489, 491, 493, 506, 580] {
954 assert_eq!(
955 HangupCause::from_sip_response(code),
956 None,
957 "SIP {code} should be None"
958 );
959 }
960 }
961
962 #[test]
965 fn caller_timetable_all_fields() {
966 let mut event = EslEvent::new();
967 event.set_header("Caller-Profile-Created-Time", "1700000000000000");
968 event.set_header("Caller-Channel-Created-Time", "1700000001000000");
969 event.set_header("Caller-Channel-Answered-Time", "1700000005000000");
970 event.set_header("Caller-Channel-Progress-Time", "1700000002000000");
971 event.set_header("Caller-Channel-Progress-Media-Time", "1700000003000000");
972 event.set_header("Caller-Channel-Hangup-Time", "0");
973 event.set_header("Caller-Channel-Transfer-Time", "0");
974 event.set_header("Caller-Channel-Resurrect-Time", "0");
975 event.set_header("Caller-Channel-Bridged-Time", "1700000006000000");
976 event.set_header("Caller-Channel-Last-Hold", "0");
977 event.set_header("Caller-Channel-Hold-Accum", "0");
978
979 let tt = event
980 .caller_timetable()
981 .unwrap()
982 .expect("should have timetable");
983 assert_eq!(tt.profile_created, Some(1700000000000000));
984 assert_eq!(tt.created, Some(1700000001000000));
985 assert_eq!(tt.answered, Some(1700000005000000));
986 assert_eq!(tt.progress, Some(1700000002000000));
987 assert_eq!(tt.progress_media, Some(1700000003000000));
988 assert_eq!(tt.hungup, Some(0));
989 assert_eq!(tt.transferred, Some(0));
990 assert_eq!(tt.resurrected, Some(0));
991 assert_eq!(tt.bridged, Some(1700000006000000));
992 assert_eq!(tt.last_hold, Some(0));
993 assert_eq!(tt.hold_accum, Some(0));
994 }
995
996 #[test]
997 fn other_leg_timetable() {
998 let mut event = EslEvent::new();
999 event.set_header("Other-Leg-Profile-Created-Time", "1700000000000000");
1000 event.set_header("Other-Leg-Channel-Created-Time", "1700000001000000");
1001 event.set_header("Other-Leg-Channel-Answered-Time", "1700000005000000");
1002 event.set_header("Other-Leg-Channel-Progress-Time", "0");
1003 event.set_header("Other-Leg-Channel-Progress-Media-Time", "0");
1004 event.set_header("Other-Leg-Channel-Hangup-Time", "0");
1005 event.set_header("Other-Leg-Channel-Transfer-Time", "0");
1006 event.set_header("Other-Leg-Channel-Resurrect-Time", "0");
1007 event.set_header("Other-Leg-Channel-Bridged-Time", "1700000006000000");
1008 event.set_header("Other-Leg-Channel-Last-Hold", "0");
1009 event.set_header("Other-Leg-Channel-Hold-Accum", "0");
1010
1011 let tt = event
1012 .other_leg_timetable()
1013 .unwrap()
1014 .expect("should have timetable");
1015 assert_eq!(tt.created, Some(1700000001000000));
1016 assert_eq!(tt.bridged, Some(1700000006000000));
1017 }
1018
1019 #[test]
1020 fn timetable_no_headers() {
1021 let event = EslEvent::new();
1022 assert_eq!(
1023 event
1024 .caller_timetable()
1025 .unwrap(),
1026 None
1027 );
1028 assert_eq!(
1029 event
1030 .other_leg_timetable()
1031 .unwrap(),
1032 None
1033 );
1034 }
1035
1036 #[test]
1037 fn timetable_partial_headers() {
1038 let mut event = EslEvent::new();
1039 event.set_header("Caller-Channel-Created-Time", "1700000001000000");
1040
1041 let tt = event
1042 .caller_timetable()
1043 .unwrap()
1044 .expect("at least one field parsed");
1045 assert_eq!(tt.created, Some(1700000001000000));
1046 assert_eq!(tt.answered, None);
1047 assert_eq!(tt.profile_created, None);
1048 }
1049
1050 #[test]
1051 fn timetable_invalid_value_is_error() {
1052 let mut event = EslEvent::new();
1053 event.set_header("Caller-Channel-Created-Time", "not_a_number");
1054
1055 let err = event
1056 .caller_timetable()
1057 .unwrap_err();
1058 assert_eq!(err.header, "Caller-Channel-Created-Time");
1059 assert_eq!(err.value, "not_a_number");
1060 }
1061
1062 #[test]
1063 fn timetable_valid_then_invalid_is_error() {
1064 let mut event = EslEvent::new();
1065 event.set_header("Caller-Profile-Created-Time", "1700000000000000");
1066 event.set_header("Caller-Channel-Created-Time", "garbage");
1067
1068 let err = event
1069 .caller_timetable()
1070 .unwrap_err();
1071 assert_eq!(err.header, "Caller-Channel-Created-Time");
1072 assert_eq!(err.value, "garbage");
1073 }
1074
1075 #[test]
1076 fn timetable_zero_preserved() {
1077 let mut event = EslEvent::new();
1078 event.set_header("Caller-Channel-Hangup-Time", "0");
1079
1080 let tt = event
1081 .caller_timetable()
1082 .unwrap()
1083 .expect("should have timetable");
1084 assert_eq!(tt.hungup, Some(0));
1085 }
1086
1087 #[test]
1088 fn timetable_field_names_one_header() {
1089 assert_eq!(
1090 TimetableField::ProfileCreated.as_str(),
1091 "Profile-Created-Time"
1092 );
1093 assert_eq!(
1094 TimetableField::Created.header_name(TimetablePrefix::Caller),
1095 "Caller-Channel-Created-Time"
1096 );
1097 assert_eq!(
1099 TimetableField::Hungup.header_name("Call-1"),
1100 "Call-1-Channel-Hangup-Time"
1101 );
1102 }
1103
1104 #[test]
1105 fn timetable_fields_and_suffixes_come_from_one_table() {
1106 let fields = [
1107 TimetableField::ProfileCreated,
1108 TimetableField::Created,
1109 TimetableField::Answered,
1110 TimetableField::Progress,
1111 TimetableField::ProgressMedia,
1112 TimetableField::Hungup,
1113 TimetableField::Transferred,
1114 TimetableField::Resurrected,
1115 TimetableField::Bridged,
1116 TimetableField::LastHold,
1117 TimetableField::HoldAccum,
1118 ];
1119 assert_eq!(fields.len(), ChannelTimetable::SUFFIXES.len());
1120 for (field, suffix) in fields
1121 .iter()
1122 .zip(ChannelTimetable::SUFFIXES)
1123 {
1124 assert_eq!(field.as_str(), *suffix);
1125 }
1126 }
1127
1128 #[test]
1129 fn timetable_field_header_name_is_what_from_lookup_reads() {
1130 let mut event = EslEvent::new();
1131 event.set_header(
1132 TimetableField::Bridged.header_name(TimetablePrefix::OtherLeg),
1133 "1700000006000000",
1134 );
1135
1136 let tt = event
1137 .other_leg_timetable()
1138 .unwrap()
1139 .expect("should have timetable");
1140 assert_eq!(tt.bridged, Some(1700000006000000));
1141 }
1142
1143 #[test]
1144 fn timetable_custom_prefix() {
1145 let mut event = EslEvent::new();
1146 event.set_header("Channel-Channel-Created-Time", "1700000001000000");
1147
1148 let tt = event
1149 .timetable("Channel")
1150 .unwrap()
1151 .expect("custom prefix should work");
1152 assert_eq!(tt.created, Some(1700000001000000));
1153 }
1154}