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