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 {} ({} bytes)",
351 self.header,
352 self.value
353 .len()
354 )
355 }
356}
357
358impl std::error::Error for ParseTimetableError {}
359
360impl ParseTimetableError {
361 pub fn new(header: impl Into<String>, value: impl Into<String>) -> Self {
363 Self {
364 header: header.into(),
365 value: value.into(),
366 }
367 }
368}
369
370macro_rules! channel_timetable_fields {
374 ($($field:ident => $suffix:literal),+ $(,)?) => {
375 pub const SUFFIXES: &'static [&'static str] = &[$($suffix),+];
391
392 pub fn from_lookup<'a>(
417 prefix: impl AsRef<str>,
418 lookup: impl Fn(&str) -> Option<&'a str>,
419 ) -> Result<Option<Self>, ParseTimetableError> {
420 let prefix = prefix.as_ref();
421 let mut tt = Self::default();
422 let mut found = false;
423 $(
424 {
425 let header = format!("{}-{}", prefix, $suffix);
426 if let Some(raw) = lookup(&header) {
427 let v: i64 = raw.parse().map_err(|_| ParseTimetableError {
428 header: header.clone(),
429 value: raw.to_string(),
430 })?;
431 tt.$field = Some(v);
432 found = true;
433 }
434 }
435 )+
436 if found {
437 Ok(Some(tt))
438 } else {
439 Ok(None)
440 }
441 }
442 };
443}
444
445impl ChannelTimetable {
446 channel_timetable_fields! {
447 profile_created => "Profile-Created-Time",
448 created => "Channel-Created-Time",
449 answered => "Channel-Answered-Time",
450 progress => "Channel-Progress-Time",
451 progress_media => "Channel-Progress-Media-Time",
452 hungup => "Channel-Hangup-Time",
453 transferred => "Channel-Transfer-Time",
454 resurrected => "Channel-Resurrect-Time",
455 bridged => "Channel-Bridged-Time",
456 last_hold => "Channel-Last-Hold",
457 hold_accum => "Channel-Hold-Accum",
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use crate::event::EslEvent;
465 use crate::lookup::HeaderLookup;
466
467 #[test]
472 fn timetable_error_display_omits_the_value() {
473 let err = ParseTimetableError::new("Caller-Channel-Created-Time", "5551234567");
474 let msg = err.to_string();
475 assert!(!msg.contains("5551234567"), "error quoted its input: {msg}");
476 assert!(msg.contains("Caller-Channel-Created-Time"), "{msg}");
477 assert_eq!(err.value, "5551234567");
478 }
479
480 #[test]
481 fn test_channel_state_from_number() {
482 assert_eq!(ChannelState::from_number(0), Some(ChannelState::CsNew));
483 assert_eq!(ChannelState::from_number(4), Some(ChannelState::CsExecute));
484 assert_eq!(ChannelState::from_number(10), Some(ChannelState::CsHangup));
485 assert_eq!(ChannelState::from_number(13), Some(ChannelState::CsNone));
486 assert_eq!(ChannelState::from_number(14), None);
487 assert_eq!(ChannelState::from_number(255), None);
488 }
489
490 #[test]
491 fn test_channel_state_as_number() {
492 assert_eq!(ChannelState::CsNew.as_number(), 0);
493 assert_eq!(ChannelState::CsExecute.as_number(), 4);
494 assert_eq!(ChannelState::CsHangup.as_number(), 10);
495 assert_eq!(ChannelState::CsNone.as_number(), 13);
496 }
497
498 #[test]
499 fn channel_state_ordering_follows_lifecycle() {
500 assert!(ChannelState::CsNew < ChannelState::CsInit);
501 assert!(ChannelState::CsInit < ChannelState::CsRouting);
502 assert!(ChannelState::CsRouting < ChannelState::CsExecute);
503 assert!(ChannelState::CsExecute < ChannelState::CsHangup);
504 assert!(ChannelState::CsHangup < ChannelState::CsReporting);
505 assert!(ChannelState::CsReporting < ChannelState::CsDestroy);
506 }
507
508 #[allow(clippy::nonminimal_bool)]
510 #[test]
511 fn channel_state_teardown_check() {
512 assert!(ChannelState::CsHangup >= ChannelState::CsHangup);
513 assert!(ChannelState::CsReporting >= ChannelState::CsHangup);
514 assert!(ChannelState::CsDestroy >= ChannelState::CsHangup);
515 assert!(!(ChannelState::CsExecute >= ChannelState::CsHangup));
516 assert!(!(ChannelState::CsPark >= ChannelState::CsHangup));
517 }
518
519 #[test]
526 fn call_state_ordering_matches_c_enum() {
527 assert!(CallState::Down < CallState::Dialing);
528 assert!(CallState::Dialing < CallState::Ringing);
529 assert!(CallState::Early < CallState::Active);
530 assert!(CallState::Active < CallState::Hangup);
531 }
532
533 #[test]
540 fn hangup_cause_display() {
541 assert_eq!(HangupCause::NormalClearing.to_string(), "NORMAL_CLEARING");
542 assert_eq!(HangupCause::UserBusy.to_string(), "USER_BUSY");
543 assert_eq!(
544 HangupCause::OriginatorCancel.to_string(),
545 "ORIGINATOR_CANCEL"
546 );
547 assert_eq!(HangupCause::None.to_string(), "NONE");
548 }
549
550 #[test]
551 fn hangup_cause_from_str() {
552 assert_eq!(
553 "NORMAL_CLEARING"
554 .parse::<HangupCause>()
555 .unwrap(),
556 HangupCause::NormalClearing
557 );
558 assert_eq!(
559 "USER_BUSY"
560 .parse::<HangupCause>()
561 .unwrap(),
562 HangupCause::UserBusy
563 );
564 }
565
566 #[test]
567 fn hangup_cause_from_str_rejects_wrong_case() {
568 assert!("normal_clearing"
569 .parse::<HangupCause>()
570 .is_err());
571 assert!("User_Busy"
572 .parse::<HangupCause>()
573 .is_err());
574 }
575
576 #[test]
577 fn hangup_cause_from_str_unknown() {
578 assert!("BOGUS_CAUSE"
579 .parse::<HangupCause>()
580 .is_err());
581 }
582
583 #[test]
584 fn hangup_cause_display_round_trip() {
585 let causes = [
586 HangupCause::None,
587 HangupCause::NormalClearing,
588 HangupCause::UserBusy,
589 HangupCause::NoAnswer,
590 HangupCause::OriginatorCancel,
591 HangupCause::BlindTransfer,
592 HangupCause::InvalidIdentity,
593 ];
594 for cause in causes {
595 let s = cause.to_string();
596 let parsed: HangupCause = s
597 .parse()
598 .unwrap();
599 assert_eq!(parsed, cause);
600 }
601 }
602
603 #[test]
604 fn hangup_cause_as_number_q850() {
605 assert_eq!(HangupCause::None.as_number(), 0);
606 assert_eq!(HangupCause::UnallocatedNumber.as_number(), 1);
607 assert_eq!(HangupCause::NormalClearing.as_number(), 16);
608 assert_eq!(HangupCause::UserBusy.as_number(), 17);
609 assert_eq!(HangupCause::NoAnswer.as_number(), 19);
610 assert_eq!(HangupCause::CallRejected.as_number(), 21);
611 assert_eq!(HangupCause::NormalUnspecified.as_number(), 31);
612 assert_eq!(HangupCause::Interworking.as_number(), 127);
613 }
614
615 #[test]
616 fn hangup_cause_as_number_freeswitch_extensions() {
617 assert_eq!(HangupCause::Success.as_number(), 142);
618 assert_eq!(HangupCause::OriginatorCancel.as_number(), 487);
619 assert_eq!(HangupCause::Crash.as_number(), 700);
620 assert_eq!(HangupCause::BlindTransfer.as_number(), 800);
621 assert_eq!(HangupCause::InvalidIdentity.as_number(), 823);
622 }
623
624 #[test]
625 fn hangup_cause_from_number_round_trip() {
626 let codes: &[u16] = &[0, 1, 16, 17, 19, 21, 31, 127, 142, 487, 700, 800, 823];
627 for &code in codes {
628 let cause = HangupCause::from_number(code).unwrap();
629 assert_eq!(cause.as_number(), code);
630 }
631 }
632
633 #[test]
634 fn hangup_cause_from_number_unknown() {
635 assert!(HangupCause::from_number(999).is_none());
636 assert!(HangupCause::from_number(4).is_none());
637 }
638
639 #[test]
642 fn from_sip_response_success() {
643 assert_eq!(
644 HangupCause::from_sip_response(200),
645 Some(HangupCause::NormalClearing)
646 );
647 }
648
649 #[test]
650 fn from_sip_response_4xx_auth_rejection() {
651 for code in [401, 402, 403, 407] {
652 assert_eq!(
653 HangupCause::from_sip_response(code),
654 Some(HangupCause::CallRejected),
655 "SIP {code}"
656 );
657 }
658 }
659
660 #[test]
661 fn from_sip_response_4xx_routing() {
662 assert_eq!(
663 HangupCause::from_sip_response(404),
664 Some(HangupCause::UnallocatedNumber)
665 );
666 assert_eq!(
667 HangupCause::from_sip_response(485),
668 Some(HangupCause::NoRouteDestination)
669 );
670 assert_eq!(
671 HangupCause::from_sip_response(484),
672 Some(HangupCause::InvalidNumberFormat)
673 );
674 assert_eq!(
675 HangupCause::from_sip_response(410),
676 Some(HangupCause::NumberChanged)
677 );
678 }
679
680 #[test]
681 fn from_sip_response_4xx_service() {
682 assert_eq!(
683 HangupCause::from_sip_response(405),
684 Some(HangupCause::ServiceUnavailable)
685 );
686 for code in [406, 415, 501] {
687 assert_eq!(
688 HangupCause::from_sip_response(code),
689 Some(HangupCause::ServiceNotImplemented),
690 "SIP {code}"
691 );
692 }
693 }
694
695 #[test]
696 fn from_sip_response_4xx_interworking() {
697 for code in [413, 414, 416, 420, 421, 423, 505, 513] {
698 assert_eq!(
699 HangupCause::from_sip_response(code),
700 Some(HangupCause::Interworking),
701 "SIP {code}"
702 );
703 }
704 }
705
706 #[test]
707 fn from_sip_response_4xx_timeout_and_busy() {
708 assert_eq!(
709 HangupCause::from_sip_response(408),
710 Some(HangupCause::RecoveryOnTimerExpire)
711 );
712 assert_eq!(
713 HangupCause::from_sip_response(504),
714 Some(HangupCause::RecoveryOnTimerExpire)
715 );
716 assert_eq!(
717 HangupCause::from_sip_response(480),
718 Some(HangupCause::NoUserResponse)
719 );
720 assert_eq!(
721 HangupCause::from_sip_response(486),
722 Some(HangupCause::UserBusy)
723 );
724 assert_eq!(
725 HangupCause::from_sip_response(487),
726 Some(HangupCause::OriginatorCancel)
727 );
728 }
729
730 #[test]
731 fn from_sip_response_4xx_temporary_failure() {
732 for code in [400, 481, 500, 503] {
733 assert_eq!(
734 HangupCause::from_sip_response(code),
735 Some(HangupCause::NormalTemporaryFailure),
736 "SIP {code}"
737 );
738 }
739 }
740
741 #[test]
742 fn from_sip_response_4xx_exchange_routing() {
743 for code in [482, 483] {
744 assert_eq!(
745 HangupCause::from_sip_response(code),
746 Some(HangupCause::ExchangeRoutingError),
747 "SIP {code}"
748 );
749 }
750 }
751
752 #[test]
753 fn from_sip_response_4xx_media() {
754 assert_eq!(
755 HangupCause::from_sip_response(488),
756 Some(HangupCause::IncompatibleDestination)
757 );
758 assert_eq!(
759 HangupCause::from_sip_response(606),
760 Some(HangupCause::IncompatibleDestination)
761 );
762 }
763
764 #[test]
765 fn from_sip_response_5xx() {
766 assert_eq!(
767 HangupCause::from_sip_response(502),
768 Some(HangupCause::NetworkOutOfOrder)
769 );
770 }
771
772 #[test]
773 fn from_sip_response_6xx() {
774 assert_eq!(
775 HangupCause::from_sip_response(600),
776 Some(HangupCause::UserBusy)
777 );
778 assert_eq!(
779 HangupCause::from_sip_response(603),
780 Some(HangupCause::CallRejected)
781 );
782 assert_eq!(
783 HangupCause::from_sip_response(604),
784 Some(HangupCause::NoRouteDestination)
785 );
786 assert_eq!(
787 HangupCause::from_sip_response(607),
788 Some(HangupCause::Unwanted)
789 );
790 assert_eq!(
791 HangupCause::from_sip_response(608),
792 Some(HangupCause::CallRejected)
793 );
794 }
795
796 #[test]
797 fn from_sip_response_stir_shaken() {
798 assert_eq!(
799 HangupCause::from_sip_response(428),
800 Some(HangupCause::NoIdentity)
801 );
802 assert_eq!(
803 HangupCause::from_sip_response(429),
804 Some(HangupCause::BadIdentityInfo)
805 );
806 assert_eq!(
807 HangupCause::from_sip_response(437),
808 Some(HangupCause::UnsupportedCertificate)
809 );
810 assert_eq!(
811 HangupCause::from_sip_response(438),
812 Some(HangupCause::InvalidIdentity)
813 );
814 }
815
816 #[test]
817 fn from_sip_response_unmapped_returns_none() {
818 for code in [100, 180, 183, 301, 302] {
820 assert_eq!(
821 HangupCause::from_sip_response(code),
822 None,
823 "SIP {code} should be None"
824 );
825 }
826 for code in [409, 411, 412, 422, 489, 491, 493, 506, 580] {
828 assert_eq!(
829 HangupCause::from_sip_response(code),
830 None,
831 "SIP {code} should be None"
832 );
833 }
834 }
835
836 #[test]
839 fn caller_timetable_all_fields() {
840 let mut event = EslEvent::new();
841 event.set_header("Caller-Profile-Created-Time", "1700000000000000");
842 event.set_header("Caller-Channel-Created-Time", "1700000001000000");
843 event.set_header("Caller-Channel-Answered-Time", "1700000005000000");
844 event.set_header("Caller-Channel-Progress-Time", "1700000002000000");
845 event.set_header("Caller-Channel-Progress-Media-Time", "1700000003000000");
846 event.set_header("Caller-Channel-Hangup-Time", "0");
847 event.set_header("Caller-Channel-Transfer-Time", "0");
848 event.set_header("Caller-Channel-Resurrect-Time", "0");
849 event.set_header("Caller-Channel-Bridged-Time", "1700000006000000");
850 event.set_header("Caller-Channel-Last-Hold", "0");
851 event.set_header("Caller-Channel-Hold-Accum", "0");
852
853 let tt = event
854 .caller_timetable()
855 .unwrap()
856 .expect("should have timetable");
857 assert_eq!(tt.profile_created, Some(1700000000000000));
858 assert_eq!(tt.created, Some(1700000001000000));
859 assert_eq!(tt.answered, Some(1700000005000000));
860 assert_eq!(tt.progress, Some(1700000002000000));
861 assert_eq!(tt.progress_media, Some(1700000003000000));
862 assert_eq!(tt.hungup, Some(0));
863 assert_eq!(tt.transferred, Some(0));
864 assert_eq!(tt.resurrected, Some(0));
865 assert_eq!(tt.bridged, Some(1700000006000000));
866 assert_eq!(tt.last_hold, Some(0));
867 assert_eq!(tt.hold_accum, Some(0));
868 }
869
870 #[test]
871 fn other_leg_timetable() {
872 let mut event = EslEvent::new();
873 event.set_header("Other-Leg-Profile-Created-Time", "1700000000000000");
874 event.set_header("Other-Leg-Channel-Created-Time", "1700000001000000");
875 event.set_header("Other-Leg-Channel-Answered-Time", "1700000005000000");
876 event.set_header("Other-Leg-Channel-Progress-Time", "0");
877 event.set_header("Other-Leg-Channel-Progress-Media-Time", "0");
878 event.set_header("Other-Leg-Channel-Hangup-Time", "0");
879 event.set_header("Other-Leg-Channel-Transfer-Time", "0");
880 event.set_header("Other-Leg-Channel-Resurrect-Time", "0");
881 event.set_header("Other-Leg-Channel-Bridged-Time", "1700000006000000");
882 event.set_header("Other-Leg-Channel-Last-Hold", "0");
883 event.set_header("Other-Leg-Channel-Hold-Accum", "0");
884
885 let tt = event
886 .other_leg_timetable()
887 .unwrap()
888 .expect("should have timetable");
889 assert_eq!(tt.created, Some(1700000001000000));
890 assert_eq!(tt.bridged, Some(1700000006000000));
891 }
892
893 #[test]
894 fn timetable_no_headers() {
895 let event = EslEvent::new();
896 assert_eq!(
897 event
898 .caller_timetable()
899 .unwrap(),
900 None
901 );
902 assert_eq!(
903 event
904 .other_leg_timetable()
905 .unwrap(),
906 None
907 );
908 }
909
910 #[test]
911 fn timetable_partial_headers() {
912 let mut event = EslEvent::new();
913 event.set_header("Caller-Channel-Created-Time", "1700000001000000");
914
915 let tt = event
916 .caller_timetable()
917 .unwrap()
918 .expect("at least one field parsed");
919 assert_eq!(tt.created, Some(1700000001000000));
920 assert_eq!(tt.answered, None);
921 assert_eq!(tt.profile_created, None);
922 }
923
924 #[test]
925 fn timetable_invalid_value_is_error() {
926 let mut event = EslEvent::new();
927 event.set_header("Caller-Channel-Created-Time", "not_a_number");
928
929 let err = event
930 .caller_timetable()
931 .unwrap_err();
932 assert_eq!(err.header, "Caller-Channel-Created-Time");
933 assert_eq!(err.value, "not_a_number");
934 }
935
936 #[test]
937 fn timetable_valid_then_invalid_is_error() {
938 let mut event = EslEvent::new();
939 event.set_header("Caller-Profile-Created-Time", "1700000000000000");
940 event.set_header("Caller-Channel-Created-Time", "garbage");
941
942 let err = event
943 .caller_timetable()
944 .unwrap_err();
945 assert_eq!(err.header, "Caller-Channel-Created-Time");
946 assert_eq!(err.value, "garbage");
947 }
948
949 #[test]
950 fn timetable_zero_preserved() {
951 let mut event = EslEvent::new();
952 event.set_header("Caller-Channel-Hangup-Time", "0");
953
954 let tt = event
955 .caller_timetable()
956 .unwrap()
957 .expect("should have timetable");
958 assert_eq!(tt.hungup, Some(0));
959 }
960
961 #[test]
962 fn timetable_custom_prefix() {
963 let mut event = EslEvent::new();
964 event.set_header("Channel-Channel-Created-Time", "1700000001000000");
965
966 let tt = event
967 .timetable("Channel")
968 .unwrap()
969 .expect("custom prefix should work");
970 assert_eq!(tt.created, Some(1700000001000000));
971 }
972}