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 numeric: from_number(u8);
28 tests: channel_state_wire_tests;
29}
30
31impl ChannelState {
32 pub fn is_terminal(&self) -> bool {
44 matches!(self, Self::CsDestroy)
45 }
46}
47
48pub fn channel_driver(name: &str) -> Option<&str> {
56 let (driver, _) = name.split_once('/')?;
57 if driver.is_empty() {
58 return None;
59 }
60 Some(driver)
61}
62
63wire_enum! {
64 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
66 pub enum CallState {
67 Down => "DOWN",
68 Dialing => "DIALING",
69 Ringing => "RINGING",
70 Early => "EARLY",
71 Active => "ACTIVE",
72 Held => "HELD",
73 RingWait => "RING_WAIT",
74 Hangup => "HANGUP",
75 Unheld => "UNHELD",
76 }
77 error ParseCallStateError("call state");
78 tests: call_state_wire_tests;
79}
80
81wire_enum! {
82 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
84 pub enum AnswerState {
85 Hangup => "hangup",
86 Answered => "answered",
87 Early => "early",
88 Ringing => "ringing",
89 }
90 error ParseAnswerStateError("answer state");
91 tests: answer_state_wire_tests;
92}
93
94wire_enum! {
95 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
97 pub enum CallDirection {
98 Inbound => "inbound",
99 Outbound => "outbound",
100 }
101 error ParseCallDirectionError("call direction");
102 tests: call_direction_wire_tests;
103}
104
105wire_enum! {
106 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
112 #[repr(u16)]
113 pub enum HangupCause {
114 None = 0 => "NONE",
115 UnallocatedNumber = 1 => "UNALLOCATED_NUMBER",
116 NoRouteTransitNet = 2 => "NO_ROUTE_TRANSIT_NET",
117 NoRouteDestination = 3 => "NO_ROUTE_DESTINATION",
118 ChannelUnacceptable = 6 => "CHANNEL_UNACCEPTABLE",
119 CallAwardedDelivered = 7 => "CALL_AWARDED_DELIVERED",
120 NormalClearing = 16 => "NORMAL_CLEARING",
121 UserBusy = 17 => "USER_BUSY",
122 NoUserResponse = 18 => "NO_USER_RESPONSE",
123 NoAnswer = 19 => "NO_ANSWER",
124 SubscriberAbsent = 20 => "SUBSCRIBER_ABSENT",
125 CallRejected = 21 => "CALL_REJECTED",
126 NumberChanged = 22 => "NUMBER_CHANGED",
127 RedirectionToNewDestination = 23 => "REDIRECTION_TO_NEW_DESTINATION",
128 ExchangeRoutingError = 25 => "EXCHANGE_ROUTING_ERROR",
129 DestinationOutOfOrder = 27 => "DESTINATION_OUT_OF_ORDER",
130 InvalidNumberFormat = 28 => "INVALID_NUMBER_FORMAT",
131 FacilityRejected = 29 => "FACILITY_REJECTED",
132 ResponseToStatusEnquiry = 30 => "RESPONSE_TO_STATUS_ENQUIRY",
133 NormalUnspecified = 31 => "NORMAL_UNSPECIFIED",
134 NormalCircuitCongestion = 34 => "NORMAL_CIRCUIT_CONGESTION",
135 NetworkOutOfOrder = 38 => "NETWORK_OUT_OF_ORDER",
136 NormalTemporaryFailure = 41 => "NORMAL_TEMPORARY_FAILURE",
137 SwitchCongestion = 42 => "SWITCH_CONGESTION",
138 AccessInfoDiscarded = 43 => "ACCESS_INFO_DISCARDED",
139 RequestedChanUnavail = 44 => "REQUESTED_CHAN_UNAVAIL",
140 PreEmpted = 45 => "PRE_EMPTED",
141 FacilityNotSubscribed = 50 => "FACILITY_NOT_SUBSCRIBED",
142 OutgoingCallBarred = 52 => "OUTGOING_CALL_BARRED",
143 IncomingCallBarred = 54 => "INCOMING_CALL_BARRED",
144 BearercapabilityNotauth = 57 => "BEARERCAPABILITY_NOTAUTH",
145 BearercapabilityNotavail = 58 => "BEARERCAPABILITY_NOTAVAIL",
146 ServiceUnavailable = 63 => "SERVICE_UNAVAILABLE",
147 BearercapabilityNotimpl = 65 => "BEARERCAPABILITY_NOTIMPL",
148 ChanNotImplemented = 66 => "CHAN_NOT_IMPLEMENTED",
149 FacilityNotImplemented = 69 => "FACILITY_NOT_IMPLEMENTED",
150 ServiceNotImplemented = 79 => "SERVICE_NOT_IMPLEMENTED",
151 InvalidCallReference = 81 => "INVALID_CALL_REFERENCE",
152 IncompatibleDestination = 88 => "INCOMPATIBLE_DESTINATION",
153 InvalidMsgUnspecified = 95 => "INVALID_MSG_UNSPECIFIED",
154 MandatoryIeMissing = 96 => "MANDATORY_IE_MISSING",
155 MessageTypeNonexist = 97 => "MESSAGE_TYPE_NONEXIST",
156 WrongMessage = 98 => "WRONG_MESSAGE",
157 IeNonexist = 99 => "IE_NONEXIST",
158 InvalidIeContents = 100 => "INVALID_IE_CONTENTS",
159 WrongCallState = 101 => "WRONG_CALL_STATE",
160 RecoveryOnTimerExpire = 102 => "RECOVERY_ON_TIMER_EXPIRE",
161 MandatoryIeLengthError = 103 => "MANDATORY_IE_LENGTH_ERROR",
162 ProtocolError = 111 => "PROTOCOL_ERROR",
163 Interworking = 127 => "INTERWORKING",
164 Success = 142 => "SUCCESS",
165 OriginatorCancel = 487 => "ORIGINATOR_CANCEL",
166 Crash = 700 => "CRASH",
167 SystemShutdown = 701 => "SYSTEM_SHUTDOWN",
168 LoseRace = 702 => "LOSE_RACE",
169 ManagerRequest = 703 => "MANAGER_REQUEST",
170 BlindTransfer = 800 => "BLIND_TRANSFER",
171 AttendedTransfer = 801 => "ATTENDED_TRANSFER",
172 AllottedTimeout = 802 => "ALLOTTED_TIMEOUT",
173 UserChallenge = 803 => "USER_CHALLENGE",
174 MediaTimeout = 804 => "MEDIA_TIMEOUT",
175 PickedOff = 805 => "PICKED_OFF",
176 UserNotRegistered = 806 => "USER_NOT_REGISTERED",
177 ProgressTimeout = 807 => "PROGRESS_TIMEOUT",
178 InvalidGateway = 808 => "INVALID_GATEWAY",
179 GatewayDown = 809 => "GATEWAY_DOWN",
180 InvalidUrl = 810 => "INVALID_URL",
181 InvalidProfile = 811 => "INVALID_PROFILE",
182 NoPickup = 812 => "NO_PICKUP",
183 SrtpReadError = 813 => "SRTP_READ_ERROR",
184 Bowout = 814 => "BOWOUT",
185 BusyEverywhere = 815 => "BUSY_EVERYWHERE",
186 Decline = 816 => "DECLINE",
187 DoesNotExistAnywhere = 817 => "DOES_NOT_EXIST_ANYWHERE",
188 NotAcceptable = 818 => "NOT_ACCEPTABLE",
189 Unwanted = 819 => "UNWANTED",
190 NoIdentity = 820 => "NO_IDENTITY",
191 BadIdentityInfo = 821 => "BAD_IDENTITY_INFO",
192 UnsupportedCertificate = 822 => "UNSUPPORTED_CERTIFICATE",
193 InvalidIdentity = 823 => "INVALID_IDENTITY",
194 StaleDate = 824 => "STALE_DATE",
196 RejectAll = 825 => "REJECT_ALL",
198 }
199 error ParseHangupCauseError("hangup cause");
200 numeric: from_number(u16);
201 tests: hangup_cause_wire_tests;
202}
203
204impl HangupCause {
205 pub fn from_sip_response(code: u16) -> Option<Self> {
210 match code {
211 200 => Some(Self::NormalClearing),
212 401 | 402 | 403 | 407 | 603 | 608 => Some(Self::CallRejected),
213 607 => Some(Self::Unwanted),
214 404 => Some(Self::UnallocatedNumber),
215 485 | 604 => Some(Self::NoRouteDestination),
216 408 | 504 => Some(Self::RecoveryOnTimerExpire),
217 410 => Some(Self::NumberChanged),
218 413 | 414 | 416 | 420 | 421 | 423 | 505 | 513 => Some(Self::Interworking),
219 480 => Some(Self::NoUserResponse),
220 400 | 481 | 500 | 503 => Some(Self::NormalTemporaryFailure),
221 486 | 600 => Some(Self::UserBusy),
222 484 => Some(Self::InvalidNumberFormat),
223 488 | 606 => Some(Self::IncompatibleDestination),
224 502 => Some(Self::NetworkOutOfOrder),
225 405 => Some(Self::ServiceUnavailable),
226 406 | 415 | 501 => Some(Self::ServiceNotImplemented),
227 482 | 483 => Some(Self::ExchangeRoutingError),
228 487 => Some(Self::OriginatorCancel),
229 428 => Some(Self::NoIdentity),
230 429 => Some(Self::BadIdentityInfo),
231 437 => Some(Self::UnsupportedCertificate),
232 438 => Some(Self::InvalidIdentity),
233 _ => None,
234 }
235 }
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
259#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
260#[non_exhaustive]
261pub struct ChannelTimetable {
262 pub profile_created: Option<i64>,
264 pub created: Option<i64>,
266 pub answered: Option<i64>,
268 pub progress: Option<i64>,
270 pub progress_media: Option<i64>,
272 pub hungup: Option<i64>,
274 pub transferred: Option<i64>,
276 pub resurrected: Option<i64>,
278 pub bridged: Option<i64>,
280 pub last_hold: Option<i64>,
282 pub hold_accum: Option<i64>,
284}
285
286#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
292#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
293#[non_exhaustive]
294pub enum TimetablePrefix {
295 Caller,
297 OtherLeg,
299 Channel,
301 Hunt,
303 Originator,
305 Originatee,
307 PostOriginator,
309 PostOriginatee,
311}
312
313impl TimetablePrefix {
314 pub fn as_str(&self) -> &'static str {
316 match self {
317 Self::Caller => "Caller",
318 Self::OtherLeg => "Other-Leg",
319 Self::Channel => "Channel",
320 Self::Hunt => "Hunt",
321 Self::Originator => "ORIGINATOR",
322 Self::Originatee => "ORIGINATEE",
323 Self::PostOriginator => "POST-ORIGINATOR",
324 Self::PostOriginatee => "POST-ORIGINATEE",
325 }
326 }
327}
328
329impl fmt::Display for TimetablePrefix {
330 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331 f.write_str(self.as_str())
332 }
333}
334
335impl AsRef<str> for TimetablePrefix {
336 fn as_ref(&self) -> &str {
337 self.as_str()
338 }
339}
340
341#[derive(Debug, Clone, PartialEq, Eq)]
343#[non_exhaustive]
344pub struct ParseTimetableError {
345 pub header: String,
347 pub value: String,
349}
350
351impl fmt::Display for ParseTimetableError {
352 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353 write!(
354 f,
355 "invalid timetable value for {} ({} bytes)",
356 self.header,
357 self.value
358 .len()
359 )
360 }
361}
362
363impl std::error::Error for ParseTimetableError {}
364
365impl ParseTimetableError {
366 pub fn new(header: impl Into<String>, value: impl Into<String>) -> Self {
368 Self {
369 header: header.into(),
370 value: value.into(),
371 }
372 }
373}
374
375macro_rules! channel_timetable_fields {
381 ($($field:ident => $Variant:ident => $suffix:literal),+ $(,)?) => {
382 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
390 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
391 #[non_exhaustive]
392 pub enum TimetableField {
393 $(
394 #[doc = concat!("`", $suffix, "`, into [`ChannelTimetable::", stringify!($field), "`].")]
395 $Variant,
396 )+
397 }
398
399 impl TimetableField {
400 pub fn as_str(&self) -> &'static str {
402 match self {
403 $(Self::$Variant => $suffix,)+
404 }
405 }
406
407 pub fn header_name(&self, prefix: impl AsRef<str>) -> String {
423 format!("{}-{}", prefix.as_ref(), self.as_str())
424 }
425 }
426
427 impl ChannelTimetable {
428 pub const SUFFIXES: &'static [&'static str] = &[$($suffix),+];
444
445 pub fn from_lookup<'a>(
470 prefix: impl AsRef<str>,
471 lookup: impl Fn(&str) -> Option<&'a str>,
472 ) -> Result<Option<Self>, ParseTimetableError> {
473 let prefix = prefix.as_ref();
474 let mut tt = Self::default();
475 let mut found = false;
476 $(
477 {
478 let header = TimetableField::$Variant.header_name(prefix);
479 if let Some(raw) = lookup(&header) {
480 let v: i64 = raw.parse().map_err(|_| ParseTimetableError {
481 header: header.clone(),
482 value: raw.to_string(),
483 })?;
484 tt.$field = Some(v);
485 found = true;
486 }
487 }
488 )+
489 if found {
490 Ok(Some(tt))
491 } else {
492 Ok(None)
493 }
494 }
495 }
496 };
497}
498
499channel_timetable_fields! {
500 profile_created => ProfileCreated => "Profile-Created-Time",
501 created => Created => "Channel-Created-Time",
502 answered => Answered => "Channel-Answered-Time",
503 progress => Progress => "Channel-Progress-Time",
504 progress_media => ProgressMedia => "Channel-Progress-Media-Time",
505 hungup => Hungup => "Channel-Hangup-Time",
506 transferred => Transferred => "Channel-Transfer-Time",
507 resurrected => Resurrected => "Channel-Resurrect-Time",
508 bridged => Bridged => "Channel-Bridged-Time",
509 last_hold => LastHold => "Channel-Last-Hold",
510 hold_accum => HoldAccum => "Channel-Hold-Accum",
511}
512
513#[cfg(test)]
514mod tests {
515 use super::*;
516
517 #[test]
522 fn channel_driver_is_the_segment_before_the_first_slash() {
523 assert_eq!(
524 channel_driver("sofia/internal/1000@example.com"),
525 Some("sofia")
526 );
527 assert_eq!(channel_driver("loopback/9199-a"), Some("loopback"));
528 assert_eq!(channel_driver("null/9199"), Some("null"));
529 }
530
531 #[test]
532 fn channel_driver_none_without_a_segment() {
533 for name in ["", "sofia", "/internal/1000"] {
534 assert_eq!(channel_driver(name), None, "{name:?}");
535 }
536 }
537
538 #[test]
539 fn timetable_error_display_omits_the_value() {
540 let err = ParseTimetableError::new("Caller-Channel-Created-Time", "5551234567");
541 let msg = err.to_string();
542 assert!(!msg.contains("5551234567"), "error quoted its input: {msg}");
543 assert!(msg.contains("Caller-Channel-Created-Time"), "{msg}");
544 assert_eq!(err.value, "5551234567");
545 }
546
547 #[test]
548 fn test_channel_state_from_number() {
549 assert_eq!(ChannelState::from_number(0), Some(ChannelState::CsNew));
550 assert_eq!(ChannelState::from_number(4), Some(ChannelState::CsExecute));
551 assert_eq!(ChannelState::from_number(10), Some(ChannelState::CsHangup));
552 assert_eq!(ChannelState::from_number(13), Some(ChannelState::CsNone));
553 assert_eq!(ChannelState::from_number(14), None);
554 assert_eq!(ChannelState::from_number(255), None);
555 }
556
557 #[test]
558 fn test_channel_state_as_number() {
559 assert_eq!(ChannelState::CsNew.as_number(), 0);
560 assert_eq!(ChannelState::CsExecute.as_number(), 4);
561 assert_eq!(ChannelState::CsHangup.as_number(), 10);
562 assert_eq!(ChannelState::CsNone.as_number(), 13);
563 }
564
565 #[test]
566 fn channel_state_ordering_follows_lifecycle() {
567 assert!(ChannelState::CsNew < ChannelState::CsInit);
568 assert!(ChannelState::CsInit < ChannelState::CsRouting);
569 assert!(ChannelState::CsRouting < ChannelState::CsExecute);
570 assert!(ChannelState::CsExecute < ChannelState::CsHangup);
571 assert!(ChannelState::CsHangup < ChannelState::CsReporting);
572 assert!(ChannelState::CsReporting < ChannelState::CsDestroy);
573 }
574
575 #[allow(clippy::nonminimal_bool)]
577 #[test]
578 fn channel_state_teardown_check() {
579 assert!(ChannelState::CsHangup >= ChannelState::CsHangup);
580 assert!(ChannelState::CsReporting >= ChannelState::CsHangup);
581 assert!(ChannelState::CsDestroy >= ChannelState::CsHangup);
582 assert!(!(ChannelState::CsExecute >= ChannelState::CsHangup));
583 assert!(!(ChannelState::CsPark >= ChannelState::CsHangup));
584 }
585
586 #[test]
587 fn only_destroy_is_terminal() {
588 assert!(ChannelState::CsDestroy.is_terminal());
589 for state in ChannelState::ALL {
590 if *state == ChannelState::CsDestroy {
591 continue;
592 }
593 assert!(!state.is_terminal(), "{state} is not an end of life");
594 }
595 }
596
597 #[test]
601 fn cs_none_outranks_destroy_without_being_terminal() {
602 assert!(ChannelState::CsNone > ChannelState::CsDestroy);
603 assert!(ChannelState::CsNone >= ChannelState::CsHangup);
604 assert!(!ChannelState::CsNone.is_terminal());
605 }
606
607 #[test]
614 fn call_state_ordering_matches_c_enum() {
615 assert!(CallState::Down < CallState::Dialing);
616 assert!(CallState::Dialing < CallState::Ringing);
617 assert!(CallState::Early < CallState::Active);
618 assert!(CallState::Active < CallState::Hangup);
619 }
620
621 #[test]
628 fn hangup_cause_as_number_q850() {
629 assert_eq!(HangupCause::None.as_number(), 0);
630 assert_eq!(HangupCause::UnallocatedNumber.as_number(), 1);
631 assert_eq!(HangupCause::NormalClearing.as_number(), 16);
632 assert_eq!(HangupCause::UserBusy.as_number(), 17);
633 assert_eq!(HangupCause::NoAnswer.as_number(), 19);
634 assert_eq!(HangupCause::CallRejected.as_number(), 21);
635 assert_eq!(HangupCause::NormalUnspecified.as_number(), 31);
636 assert_eq!(HangupCause::Interworking.as_number(), 127);
637 }
638
639 #[test]
640 fn hangup_cause_as_number_freeswitch_extensions() {
641 assert_eq!(HangupCause::Success.as_number(), 142);
642 assert_eq!(HangupCause::OriginatorCancel.as_number(), 487);
643 assert_eq!(HangupCause::Crash.as_number(), 700);
644 assert_eq!(HangupCause::BlindTransfer.as_number(), 800);
645 assert_eq!(HangupCause::InvalidIdentity.as_number(), 823);
646 }
647
648 #[test]
649 fn hangup_cause_from_number_round_trip() {
650 let codes: &[u16] = &[0, 1, 16, 17, 19, 21, 31, 127, 142, 487, 700, 800, 823];
651 for &code in codes {
652 let cause = HangupCause::from_number(code).unwrap();
653 assert_eq!(cause.as_number(), code);
654 }
655 }
656
657 #[test]
658 fn hangup_cause_from_number_unknown() {
659 assert!(HangupCause::from_number(999).is_none());
660 assert!(HangupCause::from_number(4).is_none());
661 }
662
663 #[test]
666 fn from_sip_response_success() {
667 assert_eq!(
668 HangupCause::from_sip_response(200),
669 Some(HangupCause::NormalClearing)
670 );
671 }
672
673 #[test]
674 fn from_sip_response_4xx_auth_rejection() {
675 for code in [401, 402, 403, 407] {
676 assert_eq!(
677 HangupCause::from_sip_response(code),
678 Some(HangupCause::CallRejected),
679 "SIP {code}"
680 );
681 }
682 }
683
684 #[test]
685 fn from_sip_response_4xx_routing() {
686 assert_eq!(
687 HangupCause::from_sip_response(404),
688 Some(HangupCause::UnallocatedNumber)
689 );
690 assert_eq!(
691 HangupCause::from_sip_response(485),
692 Some(HangupCause::NoRouteDestination)
693 );
694 assert_eq!(
695 HangupCause::from_sip_response(484),
696 Some(HangupCause::InvalidNumberFormat)
697 );
698 assert_eq!(
699 HangupCause::from_sip_response(410),
700 Some(HangupCause::NumberChanged)
701 );
702 }
703
704 #[test]
705 fn from_sip_response_4xx_service() {
706 assert_eq!(
707 HangupCause::from_sip_response(405),
708 Some(HangupCause::ServiceUnavailable)
709 );
710 for code in [406, 415, 501] {
711 assert_eq!(
712 HangupCause::from_sip_response(code),
713 Some(HangupCause::ServiceNotImplemented),
714 "SIP {code}"
715 );
716 }
717 }
718
719 #[test]
720 fn from_sip_response_4xx_interworking() {
721 for code in [413, 414, 416, 420, 421, 423, 505, 513] {
722 assert_eq!(
723 HangupCause::from_sip_response(code),
724 Some(HangupCause::Interworking),
725 "SIP {code}"
726 );
727 }
728 }
729
730 #[test]
731 fn from_sip_response_4xx_timeout_and_busy() {
732 assert_eq!(
733 HangupCause::from_sip_response(408),
734 Some(HangupCause::RecoveryOnTimerExpire)
735 );
736 assert_eq!(
737 HangupCause::from_sip_response(504),
738 Some(HangupCause::RecoveryOnTimerExpire)
739 );
740 assert_eq!(
741 HangupCause::from_sip_response(480),
742 Some(HangupCause::NoUserResponse)
743 );
744 assert_eq!(
745 HangupCause::from_sip_response(486),
746 Some(HangupCause::UserBusy)
747 );
748 assert_eq!(
749 HangupCause::from_sip_response(487),
750 Some(HangupCause::OriginatorCancel)
751 );
752 }
753
754 #[test]
755 fn from_sip_response_4xx_temporary_failure() {
756 for code in [400, 481, 500, 503] {
757 assert_eq!(
758 HangupCause::from_sip_response(code),
759 Some(HangupCause::NormalTemporaryFailure),
760 "SIP {code}"
761 );
762 }
763 }
764
765 #[test]
766 fn from_sip_response_4xx_exchange_routing() {
767 for code in [482, 483] {
768 assert_eq!(
769 HangupCause::from_sip_response(code),
770 Some(HangupCause::ExchangeRoutingError),
771 "SIP {code}"
772 );
773 }
774 }
775
776 #[test]
777 fn from_sip_response_4xx_media() {
778 assert_eq!(
779 HangupCause::from_sip_response(488),
780 Some(HangupCause::IncompatibleDestination)
781 );
782 assert_eq!(
783 HangupCause::from_sip_response(606),
784 Some(HangupCause::IncompatibleDestination)
785 );
786 }
787
788 #[test]
789 fn from_sip_response_5xx() {
790 assert_eq!(
791 HangupCause::from_sip_response(502),
792 Some(HangupCause::NetworkOutOfOrder)
793 );
794 }
795
796 #[test]
797 fn from_sip_response_6xx() {
798 assert_eq!(
799 HangupCause::from_sip_response(600),
800 Some(HangupCause::UserBusy)
801 );
802 assert_eq!(
803 HangupCause::from_sip_response(603),
804 Some(HangupCause::CallRejected)
805 );
806 assert_eq!(
807 HangupCause::from_sip_response(604),
808 Some(HangupCause::NoRouteDestination)
809 );
810 assert_eq!(
811 HangupCause::from_sip_response(607),
812 Some(HangupCause::Unwanted)
813 );
814 assert_eq!(
815 HangupCause::from_sip_response(608),
816 Some(HangupCause::CallRejected)
817 );
818 }
819
820 #[test]
821 fn from_sip_response_stir_shaken() {
822 assert_eq!(
823 HangupCause::from_sip_response(428),
824 Some(HangupCause::NoIdentity)
825 );
826 assert_eq!(
827 HangupCause::from_sip_response(429),
828 Some(HangupCause::BadIdentityInfo)
829 );
830 assert_eq!(
831 HangupCause::from_sip_response(437),
832 Some(HangupCause::UnsupportedCertificate)
833 );
834 assert_eq!(
835 HangupCause::from_sip_response(438),
836 Some(HangupCause::InvalidIdentity)
837 );
838 }
839
840 #[test]
841 fn from_sip_response_unmapped_returns_none() {
842 for code in [100, 180, 183, 301, 302] {
844 assert_eq!(
845 HangupCause::from_sip_response(code),
846 None,
847 "SIP {code} should be None"
848 );
849 }
850 for code in [409, 411, 412, 422, 489, 491, 493, 506, 580] {
852 assert_eq!(
853 HangupCause::from_sip_response(code),
854 None,
855 "SIP {code} should be None"
856 );
857 }
858 }
859
860 #[test]
861 fn timetable_field_names_one_header() {
862 assert_eq!(
863 TimetableField::ProfileCreated.as_str(),
864 "Profile-Created-Time"
865 );
866 assert_eq!(
867 TimetableField::Created.header_name(TimetablePrefix::Caller),
868 "Caller-Channel-Created-Time"
869 );
870 assert_eq!(
872 TimetableField::Hungup.header_name("Call-1"),
873 "Call-1-Channel-Hangup-Time"
874 );
875 }
876
877 #[test]
878 fn timetable_fields_and_suffixes_come_from_one_table() {
879 let fields = [
880 TimetableField::ProfileCreated,
881 TimetableField::Created,
882 TimetableField::Answered,
883 TimetableField::Progress,
884 TimetableField::ProgressMedia,
885 TimetableField::Hungup,
886 TimetableField::Transferred,
887 TimetableField::Resurrected,
888 TimetableField::Bridged,
889 TimetableField::LastHold,
890 TimetableField::HoldAccum,
891 ];
892 assert_eq!(fields.len(), ChannelTimetable::SUFFIXES.len());
893 for (field, suffix) in fields
894 .iter()
895 .zip(ChannelTimetable::SUFFIXES)
896 {
897 assert_eq!(field.as_str(), *suffix);
898 }
899 }
900
901 #[cfg(feature = "esl")]
902 mod from_events {
903 use super::*;
904 use crate::event::EslEvent;
905 use crate::lookup::HeaderLookup;
906
907 #[test]
908 fn caller_timetable_all_fields() {
909 let mut event = EslEvent::new();
910 event.set_header("Caller-Profile-Created-Time", "1700000000000000");
911 event.set_header("Caller-Channel-Created-Time", "1700000001000000");
912 event.set_header("Caller-Channel-Answered-Time", "1700000005000000");
913 event.set_header("Caller-Channel-Progress-Time", "1700000002000000");
914 event.set_header("Caller-Channel-Progress-Media-Time", "1700000003000000");
915 event.set_header("Caller-Channel-Hangup-Time", "0");
916 event.set_header("Caller-Channel-Transfer-Time", "0");
917 event.set_header("Caller-Channel-Resurrect-Time", "0");
918 event.set_header("Caller-Channel-Bridged-Time", "1700000006000000");
919 event.set_header("Caller-Channel-Last-Hold", "0");
920 event.set_header("Caller-Channel-Hold-Accum", "0");
921
922 let tt = event
923 .caller_timetable()
924 .unwrap()
925 .expect("should have timetable");
926 assert_eq!(tt.profile_created, Some(1700000000000000));
927 assert_eq!(tt.created, Some(1700000001000000));
928 assert_eq!(tt.answered, Some(1700000005000000));
929 assert_eq!(tt.progress, Some(1700000002000000));
930 assert_eq!(tt.progress_media, Some(1700000003000000));
931 assert_eq!(tt.hungup, Some(0));
932 assert_eq!(tt.transferred, Some(0));
933 assert_eq!(tt.resurrected, Some(0));
934 assert_eq!(tt.bridged, Some(1700000006000000));
935 assert_eq!(tt.last_hold, Some(0));
936 assert_eq!(tt.hold_accum, Some(0));
937 }
938
939 #[test]
940 fn other_leg_timetable() {
941 let mut event = EslEvent::new();
942 event.set_header("Other-Leg-Profile-Created-Time", "1700000000000000");
943 event.set_header("Other-Leg-Channel-Created-Time", "1700000001000000");
944 event.set_header("Other-Leg-Channel-Answered-Time", "1700000005000000");
945 event.set_header("Other-Leg-Channel-Progress-Time", "0");
946 event.set_header("Other-Leg-Channel-Progress-Media-Time", "0");
947 event.set_header("Other-Leg-Channel-Hangup-Time", "0");
948 event.set_header("Other-Leg-Channel-Transfer-Time", "0");
949 event.set_header("Other-Leg-Channel-Resurrect-Time", "0");
950 event.set_header("Other-Leg-Channel-Bridged-Time", "1700000006000000");
951 event.set_header("Other-Leg-Channel-Last-Hold", "0");
952 event.set_header("Other-Leg-Channel-Hold-Accum", "0");
953
954 let tt = event
955 .other_leg_timetable()
956 .unwrap()
957 .expect("should have timetable");
958 assert_eq!(tt.created, Some(1700000001000000));
959 assert_eq!(tt.bridged, Some(1700000006000000));
960 }
961
962 #[test]
963 fn timetable_no_headers() {
964 let event = EslEvent::new();
965 assert_eq!(
966 event
967 .caller_timetable()
968 .unwrap(),
969 None
970 );
971 assert_eq!(
972 event
973 .other_leg_timetable()
974 .unwrap(),
975 None
976 );
977 }
978
979 #[test]
980 fn timetable_partial_headers() {
981 let mut event = EslEvent::new();
982 event.set_header("Caller-Channel-Created-Time", "1700000001000000");
983
984 let tt = event
985 .caller_timetable()
986 .unwrap()
987 .expect("at least one field parsed");
988 assert_eq!(tt.created, Some(1700000001000000));
989 assert_eq!(tt.answered, None);
990 assert_eq!(tt.profile_created, None);
991 }
992
993 #[test]
994 fn timetable_invalid_value_is_error() {
995 let mut event = EslEvent::new();
996 event.set_header("Caller-Channel-Created-Time", "not_a_number");
997
998 let err = event
999 .caller_timetable()
1000 .unwrap_err();
1001 assert_eq!(err.header, "Caller-Channel-Created-Time");
1002 assert_eq!(err.value, "not_a_number");
1003 }
1004
1005 #[test]
1006 fn timetable_valid_then_invalid_is_error() {
1007 let mut event = EslEvent::new();
1008 event.set_header("Caller-Profile-Created-Time", "1700000000000000");
1009 event.set_header("Caller-Channel-Created-Time", "garbage");
1010
1011 let err = event
1012 .caller_timetable()
1013 .unwrap_err();
1014 assert_eq!(err.header, "Caller-Channel-Created-Time");
1015 assert_eq!(err.value, "garbage");
1016 }
1017
1018 #[test]
1019 fn timetable_zero_preserved() {
1020 let mut event = EslEvent::new();
1021 event.set_header("Caller-Channel-Hangup-Time", "0");
1022
1023 let tt = event
1024 .caller_timetable()
1025 .unwrap()
1026 .expect("should have timetable");
1027 assert_eq!(tt.hungup, Some(0));
1028 }
1029
1030 #[test]
1031 fn timetable_field_header_name_is_what_from_lookup_reads() {
1032 let mut event = EslEvent::new();
1033 event.set_header(
1034 TimetableField::Bridged.header_name(TimetablePrefix::OtherLeg),
1035 "1700000006000000",
1036 );
1037
1038 let tt = event
1039 .other_leg_timetable()
1040 .unwrap()
1041 .expect("should have timetable");
1042 assert_eq!(tt.bridged, Some(1700000006000000));
1043 }
1044
1045 #[test]
1046 fn timetable_custom_prefix() {
1047 let mut event = EslEvent::new();
1048 event.set_header("Channel-Channel-Created-Time", "1700000001000000");
1049
1050 let tt = event
1051 .timetable("Channel")
1052 .unwrap()
1053 .expect("custom prefix should work");
1054 assert_eq!(tt.created, Some(1700000001000000));
1055 }
1056 }
1057}