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