1use crate::channel::{
9 channel_driver, AnswerState, CallDirection, CallState, ChannelState, ChannelTimetable,
10 HangupCause, ParseAnswerStateError, ParseCallDirectionError, ParseCallStateError,
11 ParseChannelStateError, ParseHangupCauseError, ParseTimetableError,
12};
13#[cfg(feature = "esl")]
14use crate::event::{EslEventPriority, ParsePriorityError};
15use crate::headers::EventHeader;
16use crate::sofia::{
17 GatewayPingStatus, GatewayRegState, ParseGatewayPingStatusError, ParseGatewayRegStateError,
18 ParseSipUserPingStatusError, ParseSofiaEventSubclassError, SipUserPingStatus,
19 SofiaEventSubclass,
20};
21use crate::variables::{
22 LoopbackLeg, LoopbackResignation, LoopbackVariable, ParseLoopbackLegError, VariableName,
23};
24use sip_header::SipHeaderLookup;
25use std::str::FromStr;
26
27pub fn variable_key(name: &str) -> String {
30 format!("{}{name}", crate::VARIABLE_PREFIX)
31}
32
33#[inline]
38fn parse_opt<T: FromStr>(raw: Option<&str>) -> Result<Option<T>, T::Err> {
39 raw.map(str::parse)
40 .transpose()
41}
42
43pub trait HeaderLookup: SipHeaderLookup {
109 fn header_str(&self, name: &str) -> Option<&str>;
111
112 fn variable_str(&self, name: &str) -> Option<&str>;
116
117 fn header(&self, name: EventHeader) -> Option<&str> {
119 self.header_str(name.as_str())
120 }
121
122 fn variable(&self, name: impl VariableName) -> Option<&str> {
124 self.variable_str(name.as_str())
125 }
126
127 fn first_header<const N: usize>(&self, names: [EventHeader; N]) -> Option<&str> {
133 names
134 .into_iter()
135 .find_map(|name| self.header(name))
136 }
137
138 fn unique_id(&self) -> Option<&str> {
140 self.first_header([EventHeader::UniqueId, EventHeader::CallerUniqueId])
141 }
142
143 fn job_uuid(&self) -> Option<&str> {
149 self.header(EventHeader::JobUuid)
150 }
151
152 fn channel_name(&self) -> Option<&str> {
154 self.header(EventHeader::ChannelName)
155 }
156
157 fn channel_driver(&self) -> Option<&str> {
165 self.channel_name()
166 .and_then(channel_driver)
167 }
168
169 fn loopback_leg(&self) -> Result<Option<LoopbackLeg>, ParseLoopbackLegError> {
176 parse_opt(self.variable(LoopbackVariable::LoopbackLeg))
177 }
178
179 fn caller_id_number(&self) -> Option<&str> {
181 self.header(EventHeader::CallerCallerIdNumber)
182 }
183
184 fn caller_id_name(&self) -> Option<&str> {
186 self.header(EventHeader::CallerCallerIdName)
187 }
188
189 fn destination_number(&self) -> Option<&str> {
191 self.header(EventHeader::CallerDestinationNumber)
192 }
193
194 fn callee_id_number(&self) -> Option<&str> {
196 self.header(EventHeader::CallerCalleeIdNumber)
197 }
198
199 fn callee_id_name(&self) -> Option<&str> {
201 self.header(EventHeader::CallerCalleeIdName)
202 }
203
204 fn channel_presence_id(&self) -> Option<&str> {
206 self.header(EventHeader::ChannelPresenceId)
207 }
208
209 fn presence_call_direction(&self) -> Result<Option<CallDirection>, ParseCallDirectionError> {
211 parse_opt(self.header(EventHeader::PresenceCallDirection))
212 }
213
214 fn event_date_timestamp(&self) -> Option<&str> {
216 self.header(EventHeader::EventDateTimestamp)
217 }
218
219 fn event_sequence(&self) -> Option<&str> {
221 self.header(EventHeader::EventSequence)
222 }
223
224 fn dtmf_duration(&self) -> Option<&str> {
226 self.header(EventHeader::DtmfDuration)
227 }
228
229 fn dtmf_source(&self) -> Option<&str> {
231 self.header(EventHeader::DtmfSource)
232 }
233
234 fn hangup_cause(&self) -> Result<Option<HangupCause>, ParseHangupCauseError> {
238 parse_opt(self.header(EventHeader::HangupCause))
239 }
240
241 fn loopback_resignation(&self) -> Option<LoopbackResignation<'_>> {
259 self.variable(LoopbackVariable::LoopbackHangupCause)
260 .map(|cause| {
261 LoopbackResignation::new(
262 cause,
263 self.variable(LoopbackVariable::LoopbackBowoutOtherUuid),
264 )
265 })
266 }
267
268 fn event_subclass(&self) -> Option<&str> {
270 self.header(EventHeader::EventSubclass)
271 }
272
273 fn sofia_event_subclass(
278 &self,
279 ) -> Result<Option<SofiaEventSubclass>, ParseSofiaEventSubclassError> {
280 parse_opt(self.event_subclass())
281 }
282
283 fn gateway(&self) -> Option<&str> {
285 self.header(EventHeader::Gateway)
286 }
287
288 fn profile_name(&self) -> Option<&str> {
299 self.first_header([EventHeader::ProfileName, EventHeader::ProfileNameSnake])
300 }
301
302 fn module_name(&self) -> Option<&str> {
304 self.header(EventHeader::ModuleName)
305 }
306
307 fn profile_uri(&self) -> Option<&str> {
310 self.header(EventHeader::ProfileUri)
311 }
312
313 fn phrase(&self) -> Option<&str> {
315 self.header(EventHeader::Phrase)
316 }
317
318 fn sip_status_code(&self) -> Result<Option<u16>, std::num::ParseIntError> {
321 parse_opt(self.header(EventHeader::Status))
322 }
323
324 fn gateway_reg_state(&self) -> Result<Option<GatewayRegState>, ParseGatewayRegStateError> {
328 parse_opt(self.header(EventHeader::State))
329 }
330
331 fn gateway_ping_status(
336 &self,
337 ) -> Result<Option<GatewayPingStatus>, ParseGatewayPingStatusError> {
338 parse_opt(self.header(EventHeader::PingStatus))
339 }
340
341 fn sip_user_ping_status(
346 &self,
347 ) -> Result<Option<SipUserPingStatus>, ParseSipUserPingStatusError> {
348 parse_opt(self.header(EventHeader::PingStatus))
349 }
350
351 fn pl_data(&self) -> Option<&str> {
357 self.header(EventHeader::PlData)
358 }
359
360 fn sip_event(&self) -> Option<&str> {
364 self.header(EventHeader::SipEvent)
365 }
366
367 fn gateway_name(&self) -> Option<&str> {
369 self.header(EventHeader::GatewayName)
370 }
371
372 fn channel_state(&self) -> Result<Option<ChannelState>, ParseChannelStateError> {
376 parse_opt(self.header(EventHeader::ChannelState))
377 }
378
379 fn channel_state_number(&self) -> Result<Option<ChannelState>, ParseChannelStateError> {
387 let Some(raw) = self.header(EventHeader::ChannelStateNumber) else {
388 return Ok(None);
389 };
390 let rejected = || ParseChannelStateError(raw.to_string());
391 parse_opt::<u8>(Some(raw))
392 .map_err(|_| rejected())?
393 .and_then(ChannelState::from_number)
394 .ok_or_else(rejected)
395 .map(Some)
396 }
397
398 fn is_terminal_channel_state(&self) -> Result<bool, ParseChannelStateError> {
409 Ok(self
410 .channel_state()?
411 .is_some_and(|state| state.is_terminal()))
412 }
413
414 fn call_state(&self) -> Result<Option<CallState>, ParseCallStateError> {
418 parse_opt(self.header(EventHeader::ChannelCallState))
419 }
420
421 fn answer_state(&self) -> Result<Option<AnswerState>, ParseAnswerStateError> {
425 parse_opt(self.header(EventHeader::AnswerState))
426 }
427
428 fn call_direction(&self) -> Result<Option<CallDirection>, ParseCallDirectionError> {
432 parse_opt(self.header(EventHeader::CallDirection))
433 }
434
435 #[cfg(feature = "esl")]
439 fn priority(&self) -> Result<Option<EslEventPriority>, ParsePriorityError> {
440 parse_opt(self.header(EventHeader::Priority))
441 }
442
443 fn timetable(&self, prefix: &str) -> Result<Option<ChannelTimetable>, ParseTimetableError> {
448 ChannelTimetable::from_lookup(prefix, |key| self.header_str(key))
449 }
450
451 fn caller_timetable(&self) -> Result<Option<ChannelTimetable>, ParseTimetableError> {
453 self.timetable("Caller")
454 }
455
456 fn other_leg_timetable(&self) -> Result<Option<ChannelTimetable>, ParseTimetableError> {
458 self.timetable("Other-Leg")
459 }
460}
461
462impl HeaderLookup for std::collections::HashMap<String, String> {
463 fn header_str(&self, name: &str) -> Option<&str> {
464 self.get(name)
465 .map(|s| s.as_str())
466 }
467
468 fn variable_str(&self, name: &str) -> Option<&str> {
469 self.get(&variable_key(name))
470 .map(|s| s.as_str())
471 }
472}
473
474#[derive(Debug, Clone, PartialEq, Eq)]
485#[non_exhaustive]
486pub enum ParseHeaderError {
487 ChannelState(ParseChannelStateError),
489 CallState(ParseCallStateError),
491 AnswerState(ParseAnswerStateError),
493 CallDirection(ParseCallDirectionError),
495 HangupCause(ParseHangupCauseError),
497 GatewayRegState(ParseGatewayRegStateError),
499 LoopbackLeg(ParseLoopbackLegError),
501 #[cfg(feature = "esl")]
503 Priority(ParsePriorityError),
504 Timetable(ParseTimetableError),
507 SipStatusCode(std::num::ParseIntError),
514}
515
516macro_rules! parse_header_error_impls {
522 (
523 owned { $($(#[$attr:meta])* $Variant:ident($Error:ty)),+ $(,)? }
524 borrowed { $($(#[$battr:meta])* $BVariant:ident),+ $(,)? }
525 ) => {
526 $(
527 $(#[$attr])*
528 impl From<$Error> for ParseHeaderError {
529 fn from(e: $Error) -> Self {
530 Self::$Variant(e)
531 }
532 }
533 )+
534
535 impl ::std::fmt::Display for ParseHeaderError {
536 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
537 match self {
538 $($(#[$attr])* Self::$Variant(e) => e.fmt(f),)+
539 $($(#[$battr])* Self::$BVariant(e) => e.fmt(f),)+
540 }
541 }
542 }
543
544 impl ::std::error::Error for ParseHeaderError {
545 fn source(&self) -> Option<&(dyn ::std::error::Error + 'static)> {
546 match self {
547 $($(#[$attr])* Self::$Variant(e) => Some(e),)+
548 $($(#[$battr])* Self::$BVariant(e) => Some(e),)+
549 }
550 }
551 }
552 };
553}
554
555parse_header_error_impls! {
556 owned {
557 ChannelState(ParseChannelStateError),
558 CallState(ParseCallStateError),
559 AnswerState(ParseAnswerStateError),
560 CallDirection(ParseCallDirectionError),
561 HangupCause(ParseHangupCauseError),
562 GatewayRegState(ParseGatewayRegStateError),
563 LoopbackLeg(ParseLoopbackLegError),
564 #[cfg(feature = "esl")]
565 Priority(ParsePriorityError),
566 Timetable(ParseTimetableError),
567 }
568 borrowed {
569 SipStatusCode,
570 }
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576 use crate::variables::{
577 ChannelVariable, LoopbackChannelName, LoopbackHangupCause, LoopbackLeg,
578 };
579 use std::collections::HashMap;
580 use std::error::Error;
581
582 #[test]
583 fn every_accessor_error_converts_into_the_union() {
584 let cases: Vec<(ParseHeaderError, &str)> = vec![
585 (
586 ParseChannelStateError("x".into()).into(),
587 "ChannelState(ParseChannelStateError",
588 ),
589 (
590 ParseCallStateError("x".into()).into(),
591 "CallState(ParseCallStateError",
592 ),
593 (
594 ParseAnswerStateError("x".into()).into(),
595 "AnswerState(ParseAnswerStateError",
596 ),
597 (
598 ParseCallDirectionError("x".into()).into(),
599 "CallDirection(ParseCallDirectionError",
600 ),
601 (
602 ParseHangupCauseError("x".into()).into(),
603 "HangupCause(ParseHangupCauseError",
604 ),
605 (
606 ParseGatewayRegStateError("x".into()).into(),
607 "GatewayRegState(ParseGatewayRegStateError",
608 ),
609 (
610 ParseLoopbackLegError("x".into()).into(),
611 "LoopbackLeg(ParseLoopbackLegError",
612 ),
613 (
614 ParseTimetableError::new("Caller-Channel-Created-Time", "x").into(),
615 "Timetable(ParseTimetableError",
616 ),
617 (
618 ParseHeaderError::SipStatusCode(
619 "x".parse::<u16>()
620 .expect_err("non-numeric must fail"),
621 ),
622 "SipStatusCode(ParseIntError",
623 ),
624 ];
625 for (err, expected) in cases {
626 assert!(
627 format!("{err:?}").starts_with(expected),
628 "{err:?} should be {expected}"
629 );
630 assert!(
631 err.source()
632 .is_some(),
633 "{err:?} must expose its inner error as source"
634 );
635 }
636 }
637
638 #[cfg(feature = "esl")]
639 #[test]
640 fn priority_error_converts_into_the_union() {
641 let err: ParseHeaderError = ParsePriorityError("x".into()).into();
642 assert!(matches!(err, ParseHeaderError::Priority(_)));
643 assert!(err
644 .source()
645 .is_some());
646 }
647
648 #[test]
651 fn display_delegates_to_the_inner_error() {
652 let inner = ParseChannelStateError("CS_NONSENSE".into());
653 let expected = inner.to_string();
654 let err: ParseHeaderError = inner.into();
655 assert_eq!(err.to_string(), expected);
656 }
657
658 struct TestStore(HashMap<String, String>);
659
660 impl SipHeaderLookup for TestStore {
661 fn sip_header_str(&self, name: &str) -> Option<&str> {
662 self.0
663 .get(name)
664 .map(|s| s.as_str())
665 }
666 }
667
668 impl HeaderLookup for TestStore {
669 fn header_str(&self, name: &str) -> Option<&str> {
670 self.0
671 .get(name)
672 .map(|s| s.as_str())
673 }
674 fn variable_str(&self, name: &str) -> Option<&str> {
675 self.0
676 .get(&variable_key(name))
677 .map(|s| s.as_str())
678 }
679 }
680
681 fn store_with(pairs: &[(&str, &str)]) -> TestStore {
682 let map: HashMap<String, String> = pairs
683 .iter()
684 .map(|(k, v)| (k.to_string(), v.to_string()))
685 .collect();
686 TestStore(map)
687 }
688
689 #[test]
690 fn header_str_direct() {
691 let s = store_with(&[("Unique-ID", "abc-123")]);
692 assert_eq!(s.header_str("Unique-ID"), Some("abc-123"));
693 assert_eq!(s.header_str("Missing"), None);
694 }
695
696 #[test]
697 fn header_by_enum() {
698 let s = store_with(&[("Unique-ID", "abc-123")]);
699 assert_eq!(s.header(EventHeader::UniqueId), Some("abc-123"));
700 }
701
702 #[test]
703 fn variable_str_direct() {
704 let s = store_with(&[("variable_read_codec", "PCMU")]);
705 assert_eq!(s.variable_str("read_codec"), Some("PCMU"));
706 assert_eq!(s.variable_str("missing"), None);
707 }
708
709 #[test]
710 fn variable_by_enum() {
711 let s = store_with(&[("variable_read_codec", "PCMU")]);
712 assert_eq!(s.variable(ChannelVariable::ReadCodec), Some("PCMU"));
713 }
714
715 #[test]
716 fn unique_id_primary() {
717 let s = store_with(&[("Unique-ID", "uuid-1")]);
718 assert_eq!(s.unique_id(), Some("uuid-1"));
719 }
720
721 #[test]
722 fn unique_id_fallback() {
723 let s = store_with(&[("Caller-Unique-ID", "uuid-2")]);
724 assert_eq!(s.unique_id(), Some("uuid-2"));
725 }
726
727 #[test]
728 fn unique_id_none() {
729 let s = store_with(&[]);
730 assert_eq!(s.unique_id(), None);
731 }
732
733 #[test]
734 fn job_uuid() {
735 let s = store_with(&[("Job-UUID", "job-1")]);
736 assert_eq!(s.job_uuid(), Some("job-1"));
737 }
738
739 #[test]
740 fn channel_name() {
741 let s = store_with(&[("Channel-Name", "sofia/internal/1000@example.com")]);
742 assert_eq!(s.channel_name(), Some("sofia/internal/1000@example.com"));
743 }
744
745 #[test]
746 fn caller_id_number_and_name() {
747 let s = store_with(&[
748 ("Caller-Caller-ID-Number", "1000"),
749 ("Caller-Caller-ID-Name", "Alice"),
750 ]);
751 assert_eq!(s.caller_id_number(), Some("1000"));
752 assert_eq!(s.caller_id_name(), Some("Alice"));
753 }
754
755 #[test]
756 fn hangup_cause_typed() {
757 let s = store_with(&[("Hangup-Cause", "NORMAL_CLEARING")]);
758 assert_eq!(
759 s.hangup_cause()
760 .unwrap(),
761 Some(crate::channel::HangupCause::NormalClearing)
762 );
763 }
764
765 #[test]
766 fn hangup_cause_invalid_is_error() {
767 let s = store_with(&[("Hangup-Cause", "BOGUS_CAUSE")]);
768 assert!(s
769 .hangup_cause()
770 .is_err());
771 }
772
773 #[test]
774 fn destination_number() {
775 let s = store_with(&[("Caller-Destination-Number", "1000")]);
776 assert_eq!(s.destination_number(), Some("1000"));
777 }
778
779 #[test]
780 fn callee_id() {
781 let s = store_with(&[
782 ("Caller-Callee-ID-Number", "2000"),
783 ("Caller-Callee-ID-Name", "Bob"),
784 ]);
785 assert_eq!(s.callee_id_number(), Some("2000"));
786 assert_eq!(s.callee_id_name(), Some("Bob"));
787 }
788
789 #[test]
790 fn event_subclass() {
791 let s = store_with(&[("Event-Subclass", "sofia::register")]);
792 assert_eq!(s.event_subclass(), Some("sofia::register"));
793 }
794
795 #[test]
796 fn sofia_event_subclass_typed() {
797 let s = store_with(&[("Event-Subclass", "sofia::gateway_state")]);
798 assert_eq!(
799 s.sofia_event_subclass()
800 .unwrap(),
801 Some(crate::sofia::SofiaEventSubclass::GatewayState)
802 );
803 }
804
805 #[test]
806 fn sofia_event_subclass_absent() {
807 let s = store_with(&[]);
808 assert_eq!(
809 s.sofia_event_subclass()
810 .unwrap(),
811 None
812 );
813 }
814
815 #[test]
816 fn sofia_event_subclass_non_sofia_is_error() {
817 let s = store_with(&[("Event-Subclass", "conference::maintenance")]);
818 assert!(s
819 .sofia_event_subclass()
820 .is_err());
821 }
822
823 #[test]
824 fn gateway_reg_state_typed() {
825 let s = store_with(&[("State", "REGED")]);
826 assert_eq!(
827 s.gateway_reg_state()
828 .unwrap(),
829 Some(crate::sofia::GatewayRegState::Reged)
830 );
831 }
832
833 #[test]
834 fn gateway_reg_state_invalid_is_error() {
835 let s = store_with(&[("State", "BOGUS")]);
836 assert!(s
837 .gateway_reg_state()
838 .is_err());
839 }
840
841 #[test]
842 fn gateway_ping_status_typed() {
843 let s = store_with(&[("Ping-Status", "UP")]);
844 assert_eq!(
845 s.gateway_ping_status()
846 .unwrap(),
847 Some(crate::sofia::GatewayPingStatus::Up)
848 );
849 }
850
851 #[test]
852 fn sip_user_ping_status_typed() {
853 let s = store_with(&[("Ping-Status", "REACHABLE")]);
854 assert_eq!(
855 s.sip_user_ping_status()
856 .unwrap(),
857 Some(crate::sofia::SipUserPingStatus::Reachable)
858 );
859 }
860
861 #[test]
862 fn gateway_accessor() {
863 let s = store_with(&[("Gateway", "my-gateway")]);
864 assert_eq!(s.gateway(), Some("my-gateway"));
865 }
866
867 #[test]
868 fn profile_name_accessor() {
869 let s = store_with(&[("profile-name", "internal")]);
870 assert_eq!(s.profile_name(), Some("internal"));
871 }
872
873 #[test]
874 fn profile_name_spellings_resolve_distinct_variants() {
875 let hyphen = store_with(&[("profile-name", "internal")]);
876 assert_eq!(hyphen.header(EventHeader::ProfileName), Some("internal"));
877 assert_eq!(hyphen.header(EventHeader::ProfileNameSnake), None);
878 assert_eq!(hyphen.profile_name(), Some("internal"));
879
880 let underscore = store_with(&[("profile_name", "external")]);
881 assert_eq!(underscore.header(EventHeader::ProfileName), None);
882 assert_eq!(
883 underscore.header(EventHeader::ProfileNameSnake),
884 Some("external")
885 );
886 assert_eq!(underscore.profile_name(), Some("external"));
887 }
888
889 #[test]
890 fn profile_name_probes_hyphen_before_underscore() {
891 let s = store_with(&[("profile-name", "internal"), ("profile_name", "external")]);
892 assert_eq!(s.profile_name(), Some("internal"));
893 }
894
895 #[test]
896 fn module_name_accessor() {
897 let s = store_with(&[("module_name", "mod_sofia")]);
898 assert_eq!(s.module_name(), Some("mod_sofia"));
899 }
900
901 #[test]
902 fn profile_uri_accessor() {
903 let s = store_with(&[("profile_uri", "sip:mod_sofia@192.0.2.1:5060")]);
904 assert_eq!(s.profile_uri(), Some("sip:mod_sofia@192.0.2.1:5060"));
905 }
906
907 #[test]
908 fn phrase_accessor() {
909 let s = store_with(&[("Phrase", "OK")]);
910 assert_eq!(s.phrase(), Some("OK"));
911 }
912
913 #[test]
914 fn channel_state_typed() {
915 let s = store_with(&[("Channel-State", "CS_EXECUTE")]);
916 assert_eq!(
917 s.channel_state()
918 .unwrap(),
919 Some(ChannelState::CsExecute)
920 );
921 }
922
923 #[test]
924 fn channel_state_number_typed() {
925 let s = store_with(&[("Channel-State-Number", "4")]);
926 assert_eq!(
927 s.channel_state_number()
928 .unwrap(),
929 Some(ChannelState::CsExecute)
930 );
931 }
932
933 #[test]
934 fn terminal_state_is_the_final_channel_state_event() {
935 let s = store_with(&[("Channel-State", "CS_DESTROY")]);
936 assert_eq!(s.is_terminal_channel_state(), Ok(true));
937 }
938
939 #[test]
940 fn channel_destroy_shape_is_not_terminal() {
941 let s = store_with(&[
942 ("Channel-State", "CS_REPORTING"),
943 ("Channel-State-Number", "12"),
944 ]);
945 assert_eq!(s.is_terminal_channel_state(), Ok(false));
946 }
947
948 #[test]
949 fn hangup_state_is_not_terminal() {
950 let s = store_with(&[("Channel-State", "CS_HANGUP")]);
951 assert_eq!(s.is_terminal_channel_state(), Ok(false));
952 }
953
954 #[test]
955 fn unparseable_channel_state_is_an_error() {
956 let s = store_with(&[("Channel-State", "CS_BOGUS")]);
957 assert!(s
958 .is_terminal_channel_state()
959 .is_err());
960 }
961
962 #[test]
963 fn missing_channel_state_is_not_terminal() {
964 let s = store_with(&[("Event-Name", "HEARTBEAT")]);
965 assert_eq!(s.is_terminal_channel_state(), Ok(false));
966 }
967
968 #[test]
969 fn call_state_typed() {
970 let s = store_with(&[("Channel-Call-State", "ACTIVE")]);
971 assert_eq!(
972 s.call_state()
973 .unwrap(),
974 Some(CallState::Active)
975 );
976 }
977
978 #[test]
979 fn answer_state_typed() {
980 let s = store_with(&[("Answer-State", "answered")]);
981 assert_eq!(
982 s.answer_state()
983 .unwrap(),
984 Some(AnswerState::Answered)
985 );
986 }
987
988 #[test]
989 fn call_direction_typed() {
990 let s = store_with(&[("Call-Direction", "inbound")]);
991 assert_eq!(
992 s.call_direction()
993 .unwrap(),
994 Some(CallDirection::Inbound)
995 );
996 }
997
998 #[cfg(feature = "esl")]
999 #[test]
1000 fn priority_typed() {
1001 let s = store_with(&[("priority", "HIGH")]);
1002 assert_eq!(
1003 s.priority()
1004 .unwrap(),
1005 Some(EslEventPriority::High)
1006 );
1007 assert_eq!(
1008 store_with(&[])
1009 .priority()
1010 .unwrap(),
1011 None
1012 );
1013 assert!(store_with(&[("priority", "BOGUS")])
1014 .priority()
1015 .is_err());
1016 }
1017
1018 #[test]
1019 fn timetable_extraction() {
1020 let s = store_with(&[
1021 ("Caller-Channel-Created-Time", "1700000001000000"),
1022 ("Caller-Channel-Answered-Time", "1700000005000000"),
1023 ]);
1024 let tt = s
1025 .caller_timetable()
1026 .unwrap()
1027 .expect("should have timetable");
1028 assert_eq!(tt.created, Some(1700000001000000));
1029 assert_eq!(tt.answered, Some(1700000005000000));
1030 assert_eq!(tt.hungup, None);
1031 }
1032
1033 #[test]
1034 fn timetable_other_leg() {
1035 let s = store_with(&[("Other-Leg-Channel-Created-Time", "1700000001000000")]);
1036 let tt = s
1037 .other_leg_timetable()
1038 .unwrap()
1039 .expect("should have timetable");
1040 assert_eq!(tt.created, Some(1700000001000000));
1041 }
1042
1043 #[test]
1044 fn timetable_none_when_absent() {
1045 let s = store_with(&[]);
1046 assert_eq!(
1047 s.caller_timetable()
1048 .unwrap(),
1049 None
1050 );
1051 }
1052
1053 #[test]
1054 fn timetable_invalid_is_error() {
1055 let s = store_with(&[("Caller-Channel-Created-Time", "not_a_number")]);
1056 let err = s
1057 .caller_timetable()
1058 .unwrap_err();
1059 assert_eq!(err.header, "Caller-Channel-Created-Time");
1060 }
1061
1062 #[test]
1065 fn loopback_resignation_absent_on_a_real_teardown() {
1066 let s = store_with(&[("variable_hangup_cause", "NORMAL_CLEARING")]);
1067 assert!(s
1068 .loopback_resignation()
1069 .is_none());
1070 }
1071
1072 #[test]
1073 fn loopback_resignation_on_execute_masquerade_path() {
1074 let s = store_with(&[
1075 ("variable_loopback_hangup_cause", "bowout"),
1076 ("variable_loopback_bowout_other_uuid", "survivor-uuid"),
1077 ]);
1078 let r = s
1079 .loopback_resignation()
1080 .expect("marker present");
1081 assert_eq!(r.cause(), Ok(LoopbackHangupCause::Bowout));
1082 assert_eq!(r.cause_raw(), "bowout");
1083 assert_eq!(r.other_uuid(), Some("survivor-uuid"));
1084 }
1085
1086 #[test]
1087 fn loopback_resignation_on_frame_count_path() {
1088 let s = store_with(&[
1089 ("variable_loopback_hangup_cause", "bridge"),
1090 ("variable_loopback_bowout_other_uuid", "survivor-uuid"),
1091 ]);
1092 let r = s
1093 .loopback_resignation()
1094 .expect("marker present");
1095 assert_eq!(r.cause(), Ok(LoopbackHangupCause::Bridge));
1096 assert_eq!(r.other_uuid(), Some("survivor-uuid"));
1097 }
1098
1099 #[test]
1102 fn loopback_resignation_survives_an_unknown_token() {
1103 let s = store_with(&[
1104 ("variable_loopback_hangup_cause", "some_future_path"),
1105 ("variable_loopback_bowout_other_uuid", "survivor-uuid"),
1106 ]);
1107 let r = s
1108 .loopback_resignation()
1109 .expect("presence is the signal, not the value");
1110 assert_eq!(r.other_uuid(), Some("survivor-uuid"));
1111 assert_eq!(r.cause_raw(), "some_future_path");
1112 assert!(r
1113 .cause()
1114 .is_err());
1115 }
1116
1117 #[test]
1118 fn loopback_resignation_without_a_surviving_uuid() {
1119 let s = store_with(&[("variable_loopback_hangup_cause", "bridge")]);
1120 let r = s
1121 .loopback_resignation()
1122 .expect("marker present");
1123 assert_eq!(r.other_uuid(), None);
1124 }
1125
1126 #[test]
1129 fn the_resigning_leg_names_itself() {
1130 let leg = store_with(&[
1131 ("Channel-Name", "loopback/9199-a"),
1132 ("variable_loopback_hangup_cause", "bowout"),
1133 ("variable_loopback_leg", "A"),
1134 ]);
1135 assert_eq!(leg.channel_driver(), Some("loopback"));
1136 let name = leg
1137 .channel_name()
1138 .and_then(LoopbackChannelName::parse)
1139 .expect("a loopback name");
1140 assert_eq!(name.extension(), "9199");
1141 assert_eq!(name.leg(), LoopbackLeg::A);
1142 assert_eq!(
1143 leg.loopback_leg()
1144 .unwrap(),
1145 Some(LoopbackLeg::A)
1146 );
1147 }
1148
1149 #[test]
1152 fn a_survivor_that_inherited_the_marker_is_not_a_loopback() {
1153 let survivor = store_with(&[
1154 ("Channel-Name", "sofia/internal/1000@example.com"),
1155 ("Caller-Channel-Name", "loopback/9199-a"),
1156 ("variable_loopback_hangup_cause", "bowout"),
1157 ("variable_is_loopback", "true"),
1158 ("variable_loopback_leg", "A"),
1159 ]);
1160 assert!(survivor
1161 .loopback_resignation()
1162 .is_some());
1163 assert_eq!(
1164 survivor
1165 .loopback_leg()
1166 .unwrap(),
1167 Some(LoopbackLeg::A)
1168 );
1169
1170 assert_eq!(survivor.channel_driver(), Some("sofia"));
1171 assert_eq!(
1172 survivor
1173 .channel_name()
1174 .and_then(LoopbackChannelName::parse),
1175 None
1176 );
1177 }
1178
1179 #[test]
1180 fn channel_kind_accessors_absent() {
1181 let s = store_with(&[]);
1182 assert_eq!(s.channel_driver(), None);
1183 assert_eq!(
1184 s.loopback_leg()
1185 .unwrap(),
1186 None
1187 );
1188 }
1189
1190 #[test]
1191 fn loopback_leg_invalid_is_error() {
1192 let s = store_with(&[("variable_loopback_leg", "a")]);
1193 assert!(s
1194 .loopback_leg()
1195 .is_err());
1196 }
1197
1198 type RawAccessor = (&'static str, fn(&TestStore) -> Option<&str>);
1200 type TypedAccessor = (&'static str, fn(&TestStore) -> bool, fn(&TestStore) -> bool);
1202
1203 const RAW_ACCESSORS: &[RawAccessor] = &[
1204 ("channel_name", |s| s.channel_name()),
1205 ("caller_id_number", |s| s.caller_id_number()),
1206 ("caller_id_name", |s| s.caller_id_name()),
1207 ("destination_number", |s| s.destination_number()),
1208 ("callee_id_number", |s| s.callee_id_number()),
1209 ("callee_id_name", |s| s.callee_id_name()),
1210 ("event_subclass", |s| s.event_subclass()),
1211 ("job_uuid", |s| s.job_uuid()),
1212 ("pl_data", |s| s.pl_data()),
1213 ("sip_event", |s| s.sip_event()),
1214 ("gateway_name", |s| s.gateway_name()),
1215 ("channel_presence_id", |s| s.channel_presence_id()),
1216 ("event_date_timestamp", |s| s.event_date_timestamp()),
1217 ("event_sequence", |s| s.event_sequence()),
1218 ("dtmf_duration", |s| s.dtmf_duration()),
1219 ("dtmf_source", |s| s.dtmf_source()),
1220 ];
1221
1222 const TYPED_ACCESSORS: &[TypedAccessor] = &[
1223 (
1224 "channel_state",
1225 |s| {
1226 s.channel_state()
1227 .unwrap()
1228 .is_none()
1229 },
1230 |s| {
1231 s.channel_state()
1232 .is_err()
1233 },
1234 ),
1235 (
1236 "channel_state_number",
1237 |s| {
1238 s.channel_state_number()
1239 .unwrap()
1240 .is_none()
1241 },
1242 |s| {
1243 s.channel_state_number()
1244 .is_err()
1245 },
1246 ),
1247 (
1248 "call_state",
1249 |s| {
1250 s.call_state()
1251 .unwrap()
1252 .is_none()
1253 },
1254 |s| {
1255 s.call_state()
1256 .is_err()
1257 },
1258 ),
1259 (
1260 "answer_state",
1261 |s| {
1262 s.answer_state()
1263 .unwrap()
1264 .is_none()
1265 },
1266 |s| {
1267 s.answer_state()
1268 .is_err()
1269 },
1270 ),
1271 (
1272 "call_direction",
1273 |s| {
1274 s.call_direction()
1275 .unwrap()
1276 .is_none()
1277 },
1278 |s| {
1279 s.call_direction()
1280 .is_err()
1281 },
1282 ),
1283 (
1284 "presence_call_direction",
1285 |s| {
1286 s.presence_call_direction()
1287 .unwrap()
1288 .is_none()
1289 },
1290 |s| {
1291 s.presence_call_direction()
1292 .is_err()
1293 },
1294 ),
1295 (
1296 "hangup_cause",
1297 |s| {
1298 s.hangup_cause()
1299 .unwrap()
1300 .is_none()
1301 },
1302 |s| {
1303 s.hangup_cause()
1304 .is_err()
1305 },
1306 ),
1307 ];
1308
1309 #[test]
1310 fn missing_headers_return_none() {
1311 let s = store_with(&[]);
1312 for (name, accessor) in RAW_ACCESSORS {
1313 assert_eq!(accessor(&s), None, "{name}");
1314 }
1315 for (name, absent_is_none, _) in TYPED_ACCESSORS {
1316 assert!(absent_is_none(&s), "{name}");
1317 }
1318 }
1319
1320 #[test]
1321 fn notify_in_headers() {
1322 let s = store_with(&[
1323 ("pl_data", r#"{"invite":"INVITE ..."}"#),
1324 ("event", "emergency-AbandonedCall"),
1325 ("gateway_name", "ng911-bcf"),
1326 ]);
1327 assert_eq!(s.pl_data(), Some(r#"{"invite":"INVITE ..."}"#));
1328 assert_eq!(s.sip_event(), Some("emergency-AbandonedCall"));
1329 assert_eq!(s.gateway_name(), Some("ng911-bcf"));
1330 }
1331
1332 #[test]
1333 fn channel_presence_id() {
1334 let s = store_with(&[("Channel-Presence-ID", "1000@example.com")]);
1335 assert_eq!(s.channel_presence_id(), Some("1000@example.com"));
1336 }
1337
1338 #[test]
1339 fn presence_call_direction_typed() {
1340 let s = store_with(&[("Presence-Call-Direction", "outbound")]);
1341 assert_eq!(
1342 s.presence_call_direction()
1343 .unwrap(),
1344 Some(CallDirection::Outbound)
1345 );
1346 }
1347
1348 #[test]
1349 fn event_date_timestamp() {
1350 let s = store_with(&[("Event-Date-Timestamp", "1700000001000000")]);
1351 assert_eq!(s.event_date_timestamp(), Some("1700000001000000"));
1352 }
1353
1354 #[test]
1355 fn event_sequence() {
1356 let s = store_with(&[("Event-Sequence", "12345")]);
1357 assert_eq!(s.event_sequence(), Some("12345"));
1358 }
1359
1360 #[test]
1361 fn dtmf_duration() {
1362 let s = store_with(&[("DTMF-Duration", "2000")]);
1363 assert_eq!(s.dtmf_duration(), Some("2000"));
1364 }
1365
1366 #[test]
1367 fn dtmf_source() {
1368 let s = store_with(&[("DTMF-Source", "rtp")]);
1369 assert_eq!(s.dtmf_source(), Some("rtp"));
1370 }
1371
1372 #[test]
1373 fn invalid_values_return_err() {
1374 let s = store_with(&[
1375 ("Channel-State", "BOGUS"),
1376 ("Channel-State-Number", "999"),
1377 ("Channel-Call-State", "BOGUS"),
1378 ("Answer-State", "bogus"),
1379 ("Call-Direction", "bogus"),
1380 ("Presence-Call-Direction", "bogus"),
1381 ("Hangup-Cause", "BOGUS"),
1382 ]);
1383 for (name, _, unparseable_is_err) in TYPED_ACCESSORS {
1384 assert!(unparseable_is_err(&s), "{name}");
1385 }
1386 }
1387}