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
58wire_enum! {
59 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
61 pub enum CallState {
62 Down => "DOWN",
63 Dialing => "DIALING",
64 Ringing => "RINGING",
65 Early => "EARLY",
66 Active => "ACTIVE",
67 Held => "HELD",
68 RingWait => "RING_WAIT",
69 Hangup => "HANGUP",
70 Unheld => "UNHELD",
71 }
72 error ParseCallStateError("call state");
73 tests: call_state_wire_tests;
74}
75
76wire_enum! {
77 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
79 pub enum AnswerState {
80 Hangup => "hangup",
81 Answered => "answered",
82 Early => "early",
83 Ringing => "ringing",
84 }
85 error ParseAnswerStateError("answer state");
86 tests: answer_state_wire_tests;
87}
88
89wire_enum! {
90 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92 pub enum CallDirection {
93 Inbound => "inbound",
94 Outbound => "outbound",
95 }
96 error ParseCallDirectionError("call direction");
97 tests: call_direction_wire_tests;
98}
99
100wire_enum! {
101 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
107 #[repr(u16)]
108 pub enum HangupCause {
109 None = 0 => "NONE",
110 UnallocatedNumber = 1 => "UNALLOCATED_NUMBER",
111 NoRouteTransitNet = 2 => "NO_ROUTE_TRANSIT_NET",
112 NoRouteDestination = 3 => "NO_ROUTE_DESTINATION",
113 ChannelUnacceptable = 6 => "CHANNEL_UNACCEPTABLE",
114 CallAwardedDelivered = 7 => "CALL_AWARDED_DELIVERED",
115 NormalClearing = 16 => "NORMAL_CLEARING",
116 UserBusy = 17 => "USER_BUSY",
117 NoUserResponse = 18 => "NO_USER_RESPONSE",
118 NoAnswer = 19 => "NO_ANSWER",
119 SubscriberAbsent = 20 => "SUBSCRIBER_ABSENT",
120 CallRejected = 21 => "CALL_REJECTED",
121 NumberChanged = 22 => "NUMBER_CHANGED",
122 RedirectionToNewDestination = 23 => "REDIRECTION_TO_NEW_DESTINATION",
123 ExchangeRoutingError = 25 => "EXCHANGE_ROUTING_ERROR",
124 DestinationOutOfOrder = 27 => "DESTINATION_OUT_OF_ORDER",
125 InvalidNumberFormat = 28 => "INVALID_NUMBER_FORMAT",
126 FacilityRejected = 29 => "FACILITY_REJECTED",
127 ResponseToStatusEnquiry = 30 => "RESPONSE_TO_STATUS_ENQUIRY",
128 NormalUnspecified = 31 => "NORMAL_UNSPECIFIED",
129 NormalCircuitCongestion = 34 => "NORMAL_CIRCUIT_CONGESTION",
130 NetworkOutOfOrder = 38 => "NETWORK_OUT_OF_ORDER",
131 NormalTemporaryFailure = 41 => "NORMAL_TEMPORARY_FAILURE",
132 SwitchCongestion = 42 => "SWITCH_CONGESTION",
133 AccessInfoDiscarded = 43 => "ACCESS_INFO_DISCARDED",
134 RequestedChanUnavail = 44 => "REQUESTED_CHAN_UNAVAIL",
135 PreEmpted = 45 => "PRE_EMPTED",
136 FacilityNotSubscribed = 50 => "FACILITY_NOT_SUBSCRIBED",
137 OutgoingCallBarred = 52 => "OUTGOING_CALL_BARRED",
138 IncomingCallBarred = 54 => "INCOMING_CALL_BARRED",
139 BearercapabilityNotauth = 57 => "BEARERCAPABILITY_NOTAUTH",
140 BearercapabilityNotavail = 58 => "BEARERCAPABILITY_NOTAVAIL",
141 ServiceUnavailable = 63 => "SERVICE_UNAVAILABLE",
142 BearercapabilityNotimpl = 65 => "BEARERCAPABILITY_NOTIMPL",
143 ChanNotImplemented = 66 => "CHAN_NOT_IMPLEMENTED",
144 FacilityNotImplemented = 69 => "FACILITY_NOT_IMPLEMENTED",
145 ServiceNotImplemented = 79 => "SERVICE_NOT_IMPLEMENTED",
146 InvalidCallReference = 81 => "INVALID_CALL_REFERENCE",
147 IncompatibleDestination = 88 => "INCOMPATIBLE_DESTINATION",
148 InvalidMsgUnspecified = 95 => "INVALID_MSG_UNSPECIFIED",
149 MandatoryIeMissing = 96 => "MANDATORY_IE_MISSING",
150 MessageTypeNonexist = 97 => "MESSAGE_TYPE_NONEXIST",
151 WrongMessage = 98 => "WRONG_MESSAGE",
152 IeNonexist = 99 => "IE_NONEXIST",
153 InvalidIeContents = 100 => "INVALID_IE_CONTENTS",
154 WrongCallState = 101 => "WRONG_CALL_STATE",
155 RecoveryOnTimerExpire = 102 => "RECOVERY_ON_TIMER_EXPIRE",
156 MandatoryIeLengthError = 103 => "MANDATORY_IE_LENGTH_ERROR",
157 ProtocolError = 111 => "PROTOCOL_ERROR",
158 Interworking = 127 => "INTERWORKING",
159 Success = 142 => "SUCCESS",
160 OriginatorCancel = 487 => "ORIGINATOR_CANCEL",
161 Crash = 700 => "CRASH",
162 SystemShutdown = 701 => "SYSTEM_SHUTDOWN",
163 LoseRace = 702 => "LOSE_RACE",
164 ManagerRequest = 703 => "MANAGER_REQUEST",
165 BlindTransfer = 800 => "BLIND_TRANSFER",
166 AttendedTransfer = 801 => "ATTENDED_TRANSFER",
167 AllottedTimeout = 802 => "ALLOTTED_TIMEOUT",
168 UserChallenge = 803 => "USER_CHALLENGE",
169 MediaTimeout = 804 => "MEDIA_TIMEOUT",
170 PickedOff = 805 => "PICKED_OFF",
171 UserNotRegistered = 806 => "USER_NOT_REGISTERED",
172 ProgressTimeout = 807 => "PROGRESS_TIMEOUT",
173 InvalidGateway = 808 => "INVALID_GATEWAY",
174 GatewayDown = 809 => "GATEWAY_DOWN",
175 InvalidUrl = 810 => "INVALID_URL",
176 InvalidProfile = 811 => "INVALID_PROFILE",
177 NoPickup = 812 => "NO_PICKUP",
178 SrtpReadError = 813 => "SRTP_READ_ERROR",
179 Bowout = 814 => "BOWOUT",
180 BusyEverywhere = 815 => "BUSY_EVERYWHERE",
181 Decline = 816 => "DECLINE",
182 DoesNotExistAnywhere = 817 => "DOES_NOT_EXIST_ANYWHERE",
183 NotAcceptable = 818 => "NOT_ACCEPTABLE",
184 Unwanted = 819 => "UNWANTED",
185 NoIdentity = 820 => "NO_IDENTITY",
186 BadIdentityInfo = 821 => "BAD_IDENTITY_INFO",
187 UnsupportedCertificate = 822 => "UNSUPPORTED_CERTIFICATE",
188 InvalidIdentity = 823 => "INVALID_IDENTITY",
189 StaleDate = 824 => "STALE_DATE",
191 RejectAll = 825 => "REJECT_ALL",
193 }
194 error ParseHangupCauseError("hangup cause");
195 numeric: from_number(u16);
196 tests: hangup_cause_wire_tests;
197}
198
199impl HangupCause {
200 pub fn from_sip_response(code: u16) -> Option<Self> {
205 match code {
206 200 => Some(Self::NormalClearing),
207 401 | 402 | 403 | 407 | 603 | 608 => Some(Self::CallRejected),
208 607 => Some(Self::Unwanted),
209 404 => Some(Self::UnallocatedNumber),
210 485 | 604 => Some(Self::NoRouteDestination),
211 408 | 504 => Some(Self::RecoveryOnTimerExpire),
212 410 => Some(Self::NumberChanged),
213 413 | 414 | 416 | 420 | 421 | 423 | 505 | 513 => Some(Self::Interworking),
214 480 => Some(Self::NoUserResponse),
215 400 | 481 | 500 | 503 => Some(Self::NormalTemporaryFailure),
216 486 | 600 => Some(Self::UserBusy),
217 484 => Some(Self::InvalidNumberFormat),
218 488 | 606 => Some(Self::IncompatibleDestination),
219 502 => Some(Self::NetworkOutOfOrder),
220 405 => Some(Self::ServiceUnavailable),
221 406 | 415 | 501 => Some(Self::ServiceNotImplemented),
222 482 | 483 => Some(Self::ExchangeRoutingError),
223 487 => Some(Self::OriginatorCancel),
224 428 => Some(Self::NoIdentity),
225 429 => Some(Self::BadIdentityInfo),
226 437 => Some(Self::UnsupportedCertificate),
227 438 => Some(Self::InvalidIdentity),
228 _ => None,
229 }
230 }
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
254#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
255#[non_exhaustive]
256pub struct ChannelTimetable {
257 pub profile_created: Option<i64>,
259 pub created: Option<i64>,
261 pub answered: Option<i64>,
263 pub progress: Option<i64>,
265 pub progress_media: Option<i64>,
267 pub hungup: Option<i64>,
269 pub transferred: Option<i64>,
271 pub resurrected: Option<i64>,
273 pub bridged: Option<i64>,
275 pub last_hold: Option<i64>,
277 pub hold_accum: Option<i64>,
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
287#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
288#[non_exhaustive]
289pub enum TimetablePrefix {
290 Caller,
292 OtherLeg,
294 Channel,
296 Hunt,
298 Originator,
300 Originatee,
302 PostOriginator,
304 PostOriginatee,
306}
307
308impl TimetablePrefix {
309 pub fn as_str(&self) -> &'static str {
311 match self {
312 Self::Caller => "Caller",
313 Self::OtherLeg => "Other-Leg",
314 Self::Channel => "Channel",
315 Self::Hunt => "Hunt",
316 Self::Originator => "ORIGINATOR",
317 Self::Originatee => "ORIGINATEE",
318 Self::PostOriginator => "POST-ORIGINATOR",
319 Self::PostOriginatee => "POST-ORIGINATEE",
320 }
321 }
322}
323
324impl fmt::Display for TimetablePrefix {
325 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326 f.write_str(self.as_str())
327 }
328}
329
330impl AsRef<str> for TimetablePrefix {
331 fn as_ref(&self) -> &str {
332 self.as_str()
333 }
334}
335
336#[derive(Debug, Clone, PartialEq, Eq)]
338#[non_exhaustive]
339pub struct ParseTimetableError {
340 pub header: String,
342 pub value: String,
344}
345
346impl fmt::Display for ParseTimetableError {
347 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
348 write!(
349 f,
350 "invalid timetable value for {}: {:?}",
351 self.header, self.value
352 )
353 }
354}
355
356impl std::error::Error for ParseTimetableError {}
357
358impl ParseTimetableError {
359 pub fn new(header: impl Into<String>, value: impl Into<String>) -> Self {
361 Self {
362 header: header.into(),
363 value: value.into(),
364 }
365 }
366}
367
368macro_rules! channel_timetable_fields {
372 ($($field:ident => $suffix:literal),+ $(,)?) => {
373 pub const SUFFIXES: &'static [&'static str] = &[$($suffix),+];
389
390 pub fn from_lookup<'a>(
415 prefix: impl AsRef<str>,
416 lookup: impl Fn(&str) -> Option<&'a str>,
417 ) -> Result<Option<Self>, ParseTimetableError> {
418 let prefix = prefix.as_ref();
419 let mut tt = Self::default();
420 let mut found = false;
421 $(
422 {
423 let header = format!("{}-{}", prefix, $suffix);
424 if let Some(raw) = lookup(&header) {
425 let v: i64 = raw.parse().map_err(|_| ParseTimetableError {
426 header: header.clone(),
427 value: raw.to_string(),
428 })?;
429 tt.$field = Some(v);
430 found = true;
431 }
432 }
433 )+
434 if found {
435 Ok(Some(tt))
436 } else {
437 Ok(None)
438 }
439 }
440 };
441}
442
443impl ChannelTimetable {
444 channel_timetable_fields! {
445 profile_created => "Profile-Created-Time",
446 created => "Channel-Created-Time",
447 answered => "Channel-Answered-Time",
448 progress => "Channel-Progress-Time",
449 progress_media => "Channel-Progress-Media-Time",
450 hungup => "Channel-Hangup-Time",
451 transferred => "Channel-Transfer-Time",
452 resurrected => "Channel-Resurrect-Time",
453 bridged => "Channel-Bridged-Time",
454 last_hold => "Channel-Last-Hold",
455 hold_accum => "Channel-Hold-Accum",
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462 use crate::event::EslEvent;
463 use crate::lookup::HeaderLookup;
464
465 #[test]
470 fn test_channel_state_from_number() {
471 assert_eq!(ChannelState::from_number(0), Some(ChannelState::CsNew));
472 assert_eq!(ChannelState::from_number(4), Some(ChannelState::CsExecute));
473 assert_eq!(ChannelState::from_number(10), Some(ChannelState::CsHangup));
474 assert_eq!(ChannelState::from_number(13), Some(ChannelState::CsNone));
475 assert_eq!(ChannelState::from_number(14), None);
476 assert_eq!(ChannelState::from_number(255), None);
477 }
478
479 #[test]
480 fn test_channel_state_as_number() {
481 assert_eq!(ChannelState::CsNew.as_number(), 0);
482 assert_eq!(ChannelState::CsExecute.as_number(), 4);
483 assert_eq!(ChannelState::CsHangup.as_number(), 10);
484 assert_eq!(ChannelState::CsNone.as_number(), 13);
485 }
486
487 #[test]
488 fn channel_state_ordering_follows_lifecycle() {
489 assert!(ChannelState::CsNew < ChannelState::CsInit);
490 assert!(ChannelState::CsInit < ChannelState::CsRouting);
491 assert!(ChannelState::CsRouting < ChannelState::CsExecute);
492 assert!(ChannelState::CsExecute < ChannelState::CsHangup);
493 assert!(ChannelState::CsHangup < ChannelState::CsReporting);
494 assert!(ChannelState::CsReporting < ChannelState::CsDestroy);
495 }
496
497 #[allow(clippy::nonminimal_bool)]
499 #[test]
500 fn channel_state_teardown_check() {
501 assert!(ChannelState::CsHangup >= ChannelState::CsHangup);
502 assert!(ChannelState::CsReporting >= ChannelState::CsHangup);
503 assert!(ChannelState::CsDestroy >= ChannelState::CsHangup);
504 assert!(!(ChannelState::CsExecute >= ChannelState::CsHangup));
505 assert!(!(ChannelState::CsPark >= ChannelState::CsHangup));
506 }
507
508 #[test]
515 fn call_state_ordering_matches_c_enum() {
516 assert!(CallState::Down < CallState::Dialing);
517 assert!(CallState::Dialing < CallState::Ringing);
518 assert!(CallState::Early < CallState::Active);
519 assert!(CallState::Active < CallState::Hangup);
520 }
521
522 #[test]
529 fn hangup_cause_display() {
530 assert_eq!(HangupCause::NormalClearing.to_string(), "NORMAL_CLEARING");
531 assert_eq!(HangupCause::UserBusy.to_string(), "USER_BUSY");
532 assert_eq!(
533 HangupCause::OriginatorCancel.to_string(),
534 "ORIGINATOR_CANCEL"
535 );
536 assert_eq!(HangupCause::None.to_string(), "NONE");
537 }
538
539 #[test]
540 fn hangup_cause_from_str() {
541 assert_eq!(
542 "NORMAL_CLEARING"
543 .parse::<HangupCause>()
544 .unwrap(),
545 HangupCause::NormalClearing
546 );
547 assert_eq!(
548 "USER_BUSY"
549 .parse::<HangupCause>()
550 .unwrap(),
551 HangupCause::UserBusy
552 );
553 }
554
555 #[test]
556 fn hangup_cause_from_str_rejects_wrong_case() {
557 assert!("normal_clearing"
558 .parse::<HangupCause>()
559 .is_err());
560 assert!("User_Busy"
561 .parse::<HangupCause>()
562 .is_err());
563 }
564
565 #[test]
566 fn hangup_cause_from_str_unknown() {
567 assert!("BOGUS_CAUSE"
568 .parse::<HangupCause>()
569 .is_err());
570 }
571
572 #[test]
573 fn hangup_cause_display_round_trip() {
574 let causes = [
575 HangupCause::None,
576 HangupCause::NormalClearing,
577 HangupCause::UserBusy,
578 HangupCause::NoAnswer,
579 HangupCause::OriginatorCancel,
580 HangupCause::BlindTransfer,
581 HangupCause::InvalidIdentity,
582 ];
583 for cause in causes {
584 let s = cause.to_string();
585 let parsed: HangupCause = s
586 .parse()
587 .unwrap();
588 assert_eq!(parsed, cause);
589 }
590 }
591
592 #[test]
593 fn hangup_cause_as_number_q850() {
594 assert_eq!(HangupCause::None.as_number(), 0);
595 assert_eq!(HangupCause::UnallocatedNumber.as_number(), 1);
596 assert_eq!(HangupCause::NormalClearing.as_number(), 16);
597 assert_eq!(HangupCause::UserBusy.as_number(), 17);
598 assert_eq!(HangupCause::NoAnswer.as_number(), 19);
599 assert_eq!(HangupCause::CallRejected.as_number(), 21);
600 assert_eq!(HangupCause::NormalUnspecified.as_number(), 31);
601 assert_eq!(HangupCause::Interworking.as_number(), 127);
602 }
603
604 #[test]
605 fn hangup_cause_as_number_freeswitch_extensions() {
606 assert_eq!(HangupCause::Success.as_number(), 142);
607 assert_eq!(HangupCause::OriginatorCancel.as_number(), 487);
608 assert_eq!(HangupCause::Crash.as_number(), 700);
609 assert_eq!(HangupCause::BlindTransfer.as_number(), 800);
610 assert_eq!(HangupCause::InvalidIdentity.as_number(), 823);
611 }
612
613 #[test]
614 fn hangup_cause_from_number_round_trip() {
615 let codes: &[u16] = &[0, 1, 16, 17, 19, 21, 31, 127, 142, 487, 700, 800, 823];
616 for &code in codes {
617 let cause = HangupCause::from_number(code).unwrap();
618 assert_eq!(cause.as_number(), code);
619 }
620 }
621
622 #[test]
623 fn hangup_cause_from_number_unknown() {
624 assert!(HangupCause::from_number(999).is_none());
625 assert!(HangupCause::from_number(4).is_none());
626 }
627
628 #[test]
631 fn from_sip_response_success() {
632 assert_eq!(
633 HangupCause::from_sip_response(200),
634 Some(HangupCause::NormalClearing)
635 );
636 }
637
638 #[test]
639 fn from_sip_response_4xx_auth_rejection() {
640 for code in [401, 402, 403, 407] {
641 assert_eq!(
642 HangupCause::from_sip_response(code),
643 Some(HangupCause::CallRejected),
644 "SIP {code}"
645 );
646 }
647 }
648
649 #[test]
650 fn from_sip_response_4xx_routing() {
651 assert_eq!(
652 HangupCause::from_sip_response(404),
653 Some(HangupCause::UnallocatedNumber)
654 );
655 assert_eq!(
656 HangupCause::from_sip_response(485),
657 Some(HangupCause::NoRouteDestination)
658 );
659 assert_eq!(
660 HangupCause::from_sip_response(484),
661 Some(HangupCause::InvalidNumberFormat)
662 );
663 assert_eq!(
664 HangupCause::from_sip_response(410),
665 Some(HangupCause::NumberChanged)
666 );
667 }
668
669 #[test]
670 fn from_sip_response_4xx_service() {
671 assert_eq!(
672 HangupCause::from_sip_response(405),
673 Some(HangupCause::ServiceUnavailable)
674 );
675 for code in [406, 415, 501] {
676 assert_eq!(
677 HangupCause::from_sip_response(code),
678 Some(HangupCause::ServiceNotImplemented),
679 "SIP {code}"
680 );
681 }
682 }
683
684 #[test]
685 fn from_sip_response_4xx_interworking() {
686 for code in [413, 414, 416, 420, 421, 423, 505, 513] {
687 assert_eq!(
688 HangupCause::from_sip_response(code),
689 Some(HangupCause::Interworking),
690 "SIP {code}"
691 );
692 }
693 }
694
695 #[test]
696 fn from_sip_response_4xx_timeout_and_busy() {
697 assert_eq!(
698 HangupCause::from_sip_response(408),
699 Some(HangupCause::RecoveryOnTimerExpire)
700 );
701 assert_eq!(
702 HangupCause::from_sip_response(504),
703 Some(HangupCause::RecoveryOnTimerExpire)
704 );
705 assert_eq!(
706 HangupCause::from_sip_response(480),
707 Some(HangupCause::NoUserResponse)
708 );
709 assert_eq!(
710 HangupCause::from_sip_response(486),
711 Some(HangupCause::UserBusy)
712 );
713 assert_eq!(
714 HangupCause::from_sip_response(487),
715 Some(HangupCause::OriginatorCancel)
716 );
717 }
718
719 #[test]
720 fn from_sip_response_4xx_temporary_failure() {
721 for code in [400, 481, 500, 503] {
722 assert_eq!(
723 HangupCause::from_sip_response(code),
724 Some(HangupCause::NormalTemporaryFailure),
725 "SIP {code}"
726 );
727 }
728 }
729
730 #[test]
731 fn from_sip_response_4xx_exchange_routing() {
732 for code in [482, 483] {
733 assert_eq!(
734 HangupCause::from_sip_response(code),
735 Some(HangupCause::ExchangeRoutingError),
736 "SIP {code}"
737 );
738 }
739 }
740
741 #[test]
742 fn from_sip_response_4xx_media() {
743 assert_eq!(
744 HangupCause::from_sip_response(488),
745 Some(HangupCause::IncompatibleDestination)
746 );
747 assert_eq!(
748 HangupCause::from_sip_response(606),
749 Some(HangupCause::IncompatibleDestination)
750 );
751 }
752
753 #[test]
754 fn from_sip_response_5xx() {
755 assert_eq!(
756 HangupCause::from_sip_response(502),
757 Some(HangupCause::NetworkOutOfOrder)
758 );
759 }
760
761 #[test]
762 fn from_sip_response_6xx() {
763 assert_eq!(
764 HangupCause::from_sip_response(600),
765 Some(HangupCause::UserBusy)
766 );
767 assert_eq!(
768 HangupCause::from_sip_response(603),
769 Some(HangupCause::CallRejected)
770 );
771 assert_eq!(
772 HangupCause::from_sip_response(604),
773 Some(HangupCause::NoRouteDestination)
774 );
775 assert_eq!(
776 HangupCause::from_sip_response(607),
777 Some(HangupCause::Unwanted)
778 );
779 assert_eq!(
780 HangupCause::from_sip_response(608),
781 Some(HangupCause::CallRejected)
782 );
783 }
784
785 #[test]
786 fn from_sip_response_stir_shaken() {
787 assert_eq!(
788 HangupCause::from_sip_response(428),
789 Some(HangupCause::NoIdentity)
790 );
791 assert_eq!(
792 HangupCause::from_sip_response(429),
793 Some(HangupCause::BadIdentityInfo)
794 );
795 assert_eq!(
796 HangupCause::from_sip_response(437),
797 Some(HangupCause::UnsupportedCertificate)
798 );
799 assert_eq!(
800 HangupCause::from_sip_response(438),
801 Some(HangupCause::InvalidIdentity)
802 );
803 }
804
805 #[test]
806 fn from_sip_response_unmapped_returns_none() {
807 for code in [100, 180, 183, 301, 302] {
809 assert_eq!(
810 HangupCause::from_sip_response(code),
811 None,
812 "SIP {code} should be None"
813 );
814 }
815 for code in [409, 411, 412, 422, 489, 491, 493, 506, 580] {
817 assert_eq!(
818 HangupCause::from_sip_response(code),
819 None,
820 "SIP {code} should be None"
821 );
822 }
823 }
824
825 #[test]
828 fn caller_timetable_all_fields() {
829 let mut event = EslEvent::new();
830 event.set_header("Caller-Profile-Created-Time", "1700000000000000");
831 event.set_header("Caller-Channel-Created-Time", "1700000001000000");
832 event.set_header("Caller-Channel-Answered-Time", "1700000005000000");
833 event.set_header("Caller-Channel-Progress-Time", "1700000002000000");
834 event.set_header("Caller-Channel-Progress-Media-Time", "1700000003000000");
835 event.set_header("Caller-Channel-Hangup-Time", "0");
836 event.set_header("Caller-Channel-Transfer-Time", "0");
837 event.set_header("Caller-Channel-Resurrect-Time", "0");
838 event.set_header("Caller-Channel-Bridged-Time", "1700000006000000");
839 event.set_header("Caller-Channel-Last-Hold", "0");
840 event.set_header("Caller-Channel-Hold-Accum", "0");
841
842 let tt = event
843 .caller_timetable()
844 .unwrap()
845 .expect("should have timetable");
846 assert_eq!(tt.profile_created, Some(1700000000000000));
847 assert_eq!(tt.created, Some(1700000001000000));
848 assert_eq!(tt.answered, Some(1700000005000000));
849 assert_eq!(tt.progress, Some(1700000002000000));
850 assert_eq!(tt.progress_media, Some(1700000003000000));
851 assert_eq!(tt.hungup, Some(0));
852 assert_eq!(tt.transferred, Some(0));
853 assert_eq!(tt.resurrected, Some(0));
854 assert_eq!(tt.bridged, Some(1700000006000000));
855 assert_eq!(tt.last_hold, Some(0));
856 assert_eq!(tt.hold_accum, Some(0));
857 }
858
859 #[test]
860 fn other_leg_timetable() {
861 let mut event = EslEvent::new();
862 event.set_header("Other-Leg-Profile-Created-Time", "1700000000000000");
863 event.set_header("Other-Leg-Channel-Created-Time", "1700000001000000");
864 event.set_header("Other-Leg-Channel-Answered-Time", "1700000005000000");
865 event.set_header("Other-Leg-Channel-Progress-Time", "0");
866 event.set_header("Other-Leg-Channel-Progress-Media-Time", "0");
867 event.set_header("Other-Leg-Channel-Hangup-Time", "0");
868 event.set_header("Other-Leg-Channel-Transfer-Time", "0");
869 event.set_header("Other-Leg-Channel-Resurrect-Time", "0");
870 event.set_header("Other-Leg-Channel-Bridged-Time", "1700000006000000");
871 event.set_header("Other-Leg-Channel-Last-Hold", "0");
872 event.set_header("Other-Leg-Channel-Hold-Accum", "0");
873
874 let tt = event
875 .other_leg_timetable()
876 .unwrap()
877 .expect("should have timetable");
878 assert_eq!(tt.created, Some(1700000001000000));
879 assert_eq!(tt.bridged, Some(1700000006000000));
880 }
881
882 #[test]
883 fn timetable_no_headers() {
884 let event = EslEvent::new();
885 assert_eq!(
886 event
887 .caller_timetable()
888 .unwrap(),
889 None
890 );
891 assert_eq!(
892 event
893 .other_leg_timetable()
894 .unwrap(),
895 None
896 );
897 }
898
899 #[test]
900 fn timetable_partial_headers() {
901 let mut event = EslEvent::new();
902 event.set_header("Caller-Channel-Created-Time", "1700000001000000");
903
904 let tt = event
905 .caller_timetable()
906 .unwrap()
907 .expect("at least one field parsed");
908 assert_eq!(tt.created, Some(1700000001000000));
909 assert_eq!(tt.answered, None);
910 assert_eq!(tt.profile_created, None);
911 }
912
913 #[test]
914 fn timetable_invalid_value_is_error() {
915 let mut event = EslEvent::new();
916 event.set_header("Caller-Channel-Created-Time", "not_a_number");
917
918 let err = event
919 .caller_timetable()
920 .unwrap_err();
921 assert_eq!(err.header, "Caller-Channel-Created-Time");
922 assert_eq!(err.value, "not_a_number");
923 }
924
925 #[test]
926 fn timetable_valid_then_invalid_is_error() {
927 let mut event = EslEvent::new();
928 event.set_header("Caller-Profile-Created-Time", "1700000000000000");
929 event.set_header("Caller-Channel-Created-Time", "garbage");
930
931 let err = event
932 .caller_timetable()
933 .unwrap_err();
934 assert_eq!(err.header, "Caller-Channel-Created-Time");
935 assert_eq!(err.value, "garbage");
936 }
937
938 #[test]
939 fn timetable_zero_preserved() {
940 let mut event = EslEvent::new();
941 event.set_header("Caller-Channel-Hangup-Time", "0");
942
943 let tt = event
944 .caller_timetable()
945 .unwrap()
946 .expect("should have timetable");
947 assert_eq!(tt.hungup, Some(0));
948 }
949
950 #[test]
951 fn timetable_custom_prefix() {
952 let mut event = EslEvent::new();
953 event.set_header("Channel-Channel-Created-Time", "1700000001000000");
954
955 let tt = event
956 .timetable("Channel")
957 .unwrap()
958 .expect("custom prefix should work");
959 assert_eq!(tt.created, Some(1700000001000000));
960 }
961}