1use std::fmt;
5use std::str::FromStr;
6use std::time::Duration;
7
8use indexmap::IndexMap;
9use serde::de::Deserializer;
10use serde::ser::Serializer;
11use serde::{Deserialize, Serialize};
12
13use super::{originate_quote, originate_split, originate_unquote};
14
15const UNDEF: &str = "undef";
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24#[serde(rename_all = "lowercase")]
25#[non_exhaustive]
26pub enum DialplanType {
27 Inline,
29 Xml,
31}
32
33impl fmt::Display for DialplanType {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 match self {
36 Self::Inline => f.write_str("inline"),
37 Self::Xml => f.write_str("XML"),
38 }
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct ParseDialplanTypeError(pub String);
45
46impl fmt::Display for ParseDialplanTypeError {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 write!(f, "unknown dialplan type: {}", self.0)
49 }
50}
51
52impl std::error::Error for ParseDialplanTypeError {}
53
54impl FromStr for DialplanType {
55 type Err = ParseDialplanTypeError;
56
57 fn from_str(s: &str) -> Result<Self, Self::Err> {
58 if s.eq_ignore_ascii_case("inline") {
59 Ok(Self::Inline)
60 } else if s.eq_ignore_ascii_case("xml") {
61 Ok(Self::Xml)
62 } else {
63 Err(ParseDialplanTypeError(s.to_string()))
64 }
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(rename_all = "lowercase")]
75#[non_exhaustive]
76pub enum VariablesType {
77 Enterprise,
79 Default,
81 Channel,
83}
84
85impl VariablesType {
86 fn delimiters(self) -> (char, char) {
87 match self {
88 Self::Enterprise => ('<', '>'),
89 Self::Default => ('{', '}'),
90 Self::Channel => ('[', ']'),
91 }
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct Variables {
108 vars_type: VariablesType,
109 inner: IndexMap<String, String>,
110}
111
112fn escape_value(value: &str) -> String {
113 let escaped = value
114 .replace('\'', "\\'")
115 .replace(',', "\\,");
116 if escaped.contains(' ') {
117 format!("'{}'", escaped)
118 } else {
119 escaped
120 }
121}
122
123fn unescape_value(value: &str) -> String {
124 let s = value
125 .strip_prefix('\'')
126 .and_then(|s| s.strip_suffix('\''))
127 .unwrap_or(value);
128 s.replace("\\,", ",")
129 .replace("\\'", "'")
130}
131
132impl Variables {
133 pub fn new(vars_type: VariablesType) -> Self {
135 Self {
136 vars_type,
137 inner: IndexMap::new(),
138 }
139 }
140
141 pub fn with_vars(
143 vars_type: VariablesType,
144 vars: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
145 ) -> Self {
146 Self {
147 vars_type,
148 inner: vars
149 .into_iter()
150 .map(|(k, v)| (k.into(), v.into()))
151 .collect(),
152 }
153 }
154
155 pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) {
157 self.inner
158 .insert(key.into(), value.into());
159 }
160
161 pub fn remove(&mut self, key: &str) -> Option<String> {
163 self.inner
164 .shift_remove(key)
165 }
166
167 pub fn get(&self, key: &str) -> Option<&str> {
169 self.inner
170 .get(key)
171 .map(|s| s.as_str())
172 }
173
174 pub fn is_empty(&self) -> bool {
176 self.inner
177 .is_empty()
178 }
179
180 pub fn len(&self) -> usize {
182 self.inner
183 .len()
184 }
185
186 pub fn scope(&self) -> VariablesType {
188 self.vars_type
189 }
190
191 pub fn set_scope(&mut self, scope: VariablesType) {
193 self.vars_type = scope;
194 }
195
196 pub fn iter(&self) -> impl Iterator<Item = (&String, &String)> {
198 self.inner
199 .iter()
200 }
201
202 pub fn iter_mut(&mut self) -> impl Iterator<Item = (&String, &mut String)> {
204 self.inner
205 .iter_mut()
206 }
207
208 pub fn values_mut(&mut self) -> impl Iterator<Item = &mut String> {
210 self.inner
211 .values_mut()
212 }
213}
214
215impl Serialize for Variables {
216 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
217 if self.vars_type == VariablesType::Default {
218 self.inner
219 .serialize(serializer)
220 } else {
221 use serde::ser::SerializeStruct;
222 let mut s = serializer.serialize_struct("Variables", 2)?;
223 s.serialize_field("scope", &self.vars_type)?;
224 s.serialize_field("vars", &self.inner)?;
225 s.end()
226 }
227 }
228}
229
230impl<'de> Deserialize<'de> for Variables {
231 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
232 #[derive(Deserialize)]
233 #[serde(untagged)]
234 enum VariablesRepr {
235 Scoped {
236 scope: VariablesType,
237 vars: IndexMap<String, String>,
238 },
239 Flat(IndexMap<String, String>),
240 }
241
242 match VariablesRepr::deserialize(deserializer)? {
243 VariablesRepr::Scoped { scope, vars } => Ok(Self {
244 vars_type: scope,
245 inner: vars,
246 }),
247 VariablesRepr::Flat(map) => Ok(Self {
248 vars_type: VariablesType::Default,
249 inner: map,
250 }),
251 }
252 }
253}
254
255impl fmt::Display for Variables {
256 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257 let (open, close) = self
258 .vars_type
259 .delimiters();
260 f.write_fmt(format_args!("{}", open))?;
261 for (i, (key, value)) in self
262 .inner
263 .iter()
264 .enumerate()
265 {
266 if i > 0 {
267 f.write_str(",")?;
268 }
269 write!(f, "{}={}", key, escape_value(value))?;
270 }
271 f.write_fmt(format_args!("{}", close))
272 }
273}
274
275impl FromStr for Variables {
276 type Err = OriginateError;
277
278 fn from_str(s: &str) -> Result<Self, Self::Err> {
279 let s = s.trim();
280 if s.len() < 2 {
281 return Err(OriginateError::ParseError(
282 "variable block too short".into(),
283 ));
284 }
285
286 let (vars_type, inner_str) = match (s.as_bytes()[0], s.as_bytes()[s.len() - 1]) {
287 (b'{', b'}') => (VariablesType::Default, &s[1..s.len() - 1]),
288 (b'<', b'>') => (VariablesType::Enterprise, &s[1..s.len() - 1]),
289 (b'[', b']') => (VariablesType::Channel, &s[1..s.len() - 1]),
290 _ => {
291 return Err(OriginateError::ParseError(format!(
292 "unknown variable delimiters: {}",
293 s
294 )));
295 }
296 };
297
298 let mut inner = IndexMap::new();
299 if !inner_str.is_empty() {
300 for part in split_unescaped_commas(inner_str) {
302 let (key, value) = part
303 .split_once('=')
304 .ok_or_else(|| {
305 OriginateError::ParseError(format!("missing = in variable: {}", part))
306 })?;
307 inner.insert(key.to_string(), unescape_value(value));
308 }
309 }
310
311 Ok(Self { vars_type, inner })
312 }
313}
314
315fn split_unescaped_commas(s: &str) -> Vec<&str> {
321 let mut parts = Vec::new();
322 let mut start = 0;
323 let bytes = s.as_bytes();
324
325 for i in 0..bytes.len() {
326 if bytes[i] == b',' {
327 let mut backslashes = 0;
328 let mut j = i;
329 while j > 0 && bytes[j - 1] == b'\\' {
330 backslashes += 1;
331 j -= 1;
332 }
333 if backslashes % 2 == 0 {
334 parts.push(&s[start..i]);
335 start = i + 1;
336 }
337 }
338 }
339 parts.push(&s[start..]);
340 parts
341}
342
343pub use super::endpoint::Endpoint;
345
346#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
352#[non_exhaustive]
353pub struct Application {
354 pub name: String,
356 #[serde(default, skip_serializing_if = "Option::is_none")]
358 pub args: Option<String>,
359}
360
361impl Application {
362 pub fn new(name: impl Into<String>, args: Option<impl Into<String>>) -> Self {
364 Self {
365 name: name.into(),
366 args: args.map(|a| a.into()),
367 }
368 }
369
370 pub fn simple(name: impl Into<String>) -> Self {
372 Self {
373 name: name.into(),
374 args: None,
375 }
376 }
377
378 pub fn to_string_with_dialplan(&self, dialplan: &DialplanType) -> String {
380 match dialplan {
381 DialplanType::Inline => match &self.args {
382 Some(args) => format!("{}:{}", self.name, args),
383 None => self
384 .name
385 .clone(),
386 },
387 _ => {
389 let args = self
390 .args
391 .as_deref()
392 .unwrap_or("");
393 format!("&{}({})", self.name, args)
394 }
395 }
396 }
397}
398
399#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
406#[serde(rename_all = "snake_case")]
407#[non_exhaustive]
408pub enum OriginateTarget {
409 Extension(String),
411 Application(Application),
413 InlineApplications(Vec<Application>),
415}
416
417impl From<Application> for OriginateTarget {
418 fn from(app: Application) -> Self {
419 Self::Application(app)
420 }
421}
422
423impl From<Vec<Application>> for OriginateTarget {
424 fn from(apps: Vec<Application>) -> Self {
425 Self::InlineApplications(apps)
426 }
427}
428
429#[derive(Debug, Clone, PartialEq, Eq)]
449pub struct Originate {
450 endpoint: Endpoint,
451 target: OriginateTarget,
452 dialplan: Option<DialplanType>,
453 context: Option<String>,
454 cid_name: Option<String>,
455 cid_num: Option<String>,
456 timeout: Option<Duration>,
457}
458
459#[derive(Serialize, Deserialize)]
461struct OriginateRaw {
462 endpoint: Endpoint,
463 #[serde(flatten)]
464 target: OriginateTarget,
465 #[serde(default, skip_serializing_if = "Option::is_none")]
466 dialplan: Option<DialplanType>,
467 #[serde(default, skip_serializing_if = "Option::is_none")]
468 context: Option<String>,
469 #[serde(default, skip_serializing_if = "Option::is_none")]
470 cid_name: Option<String>,
471 #[serde(default, skip_serializing_if = "Option::is_none")]
472 cid_num: Option<String>,
473 #[serde(default, skip_serializing_if = "Option::is_none")]
474 timeout_secs: Option<u64>,
475}
476
477impl TryFrom<OriginateRaw> for Originate {
478 type Error = OriginateError;
479
480 fn try_from(raw: OriginateRaw) -> Result<Self, Self::Error> {
481 if matches!(raw.target, OriginateTarget::Extension(_))
482 && matches!(raw.dialplan, Some(DialplanType::Inline))
483 {
484 return Err(OriginateError::ExtensionWithInlineDialplan);
485 }
486 if let OriginateTarget::InlineApplications(ref apps) = raw.target {
487 if apps.is_empty() {
488 return Err(OriginateError::EmptyInlineApplications);
489 }
490 }
491 Ok(Self {
492 endpoint: raw.endpoint,
493 target: raw.target,
494 dialplan: raw.dialplan,
495 context: raw.context,
496 cid_name: raw.cid_name,
497 cid_num: raw.cid_num,
498 timeout: raw
499 .timeout_secs
500 .map(Duration::from_secs),
501 })
502 }
503}
504
505impl From<Originate> for OriginateRaw {
506 fn from(o: Originate) -> Self {
507 Self {
508 endpoint: o.endpoint,
509 target: o.target,
510 dialplan: o.dialplan,
511 context: o.context,
512 cid_name: o.cid_name,
513 cid_num: o.cid_num,
514 timeout_secs: o
515 .timeout
516 .map(|d| d.as_secs()),
517 }
518 }
519}
520
521impl Serialize for Originate {
522 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
523 OriginateRaw::from(self.clone()).serialize(serializer)
524 }
525}
526
527impl<'de> Deserialize<'de> for Originate {
528 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
529 let raw = OriginateRaw::deserialize(deserializer)?;
530 Originate::try_from(raw).map_err(serde::de::Error::custom)
531 }
532}
533
534impl Originate {
535 pub fn extension(endpoint: Endpoint, extension: impl Into<String>) -> Self {
537 Self {
538 endpoint,
539 target: OriginateTarget::Extension(extension.into()),
540 dialplan: None,
541 context: None,
542 cid_name: None,
543 cid_num: None,
544 timeout: None,
545 }
546 }
547
548 pub fn application(endpoint: Endpoint, app: Application) -> Self {
550 Self {
551 endpoint,
552 target: OriginateTarget::Application(app),
553 dialplan: None,
554 context: None,
555 cid_name: None,
556 cid_num: None,
557 timeout: None,
558 }
559 }
560
561 pub fn inline(
565 endpoint: Endpoint,
566 apps: impl IntoIterator<Item = Application>,
567 ) -> Result<Self, OriginateError> {
568 let apps: Vec<Application> = apps
569 .into_iter()
570 .collect();
571 if apps.is_empty() {
572 return Err(OriginateError::EmptyInlineApplications);
573 }
574 Ok(Self {
575 endpoint,
576 target: OriginateTarget::InlineApplications(apps),
577 dialplan: None,
578 context: None,
579 cid_name: None,
580 cid_num: None,
581 timeout: None,
582 })
583 }
584
585 pub fn dialplan(mut self, dp: DialplanType) -> Result<Self, OriginateError> {
589 if matches!(self.target, OriginateTarget::Extension(_)) && dp == DialplanType::Inline {
590 return Err(OriginateError::ExtensionWithInlineDialplan);
591 }
592 self.dialplan = Some(dp);
593 Ok(self)
594 }
595
596 pub fn context(mut self, ctx: impl Into<String>) -> Self {
598 self.context = Some(ctx.into());
599 self
600 }
601
602 pub fn cid_name(mut self, name: impl Into<String>) -> Self {
604 self.cid_name = Some(name.into());
605 self
606 }
607
608 pub fn cid_num(mut self, num: impl Into<String>) -> Self {
610 self.cid_num = Some(num.into());
611 self
612 }
613
614 pub fn timeout(mut self, duration: Duration) -> Self {
617 self.timeout = Some(duration);
618 self
619 }
620
621 pub fn endpoint(&self) -> &Endpoint {
623 &self.endpoint
624 }
625
626 pub fn endpoint_mut(&mut self) -> &mut Endpoint {
628 &mut self.endpoint
629 }
630
631 pub fn target(&self) -> &OriginateTarget {
633 &self.target
634 }
635
636 pub fn dialplan_type(&self) -> Option<&DialplanType> {
638 self.dialplan
639 .as_ref()
640 }
641
642 pub fn context_str(&self) -> Option<&str> {
644 self.context
645 .as_deref()
646 }
647
648 pub fn caller_id_name(&self) -> Option<&str> {
650 self.cid_name
651 .as_deref()
652 }
653
654 pub fn caller_id_number(&self) -> Option<&str> {
656 self.cid_num
657 .as_deref()
658 }
659
660 pub fn timeout_duration(&self) -> Option<Duration> {
662 self.timeout
663 }
664
665 pub fn timeout_seconds(&self) -> Option<u64> {
667 self.timeout
668 .map(|d| d.as_secs())
669 }
670}
671
672impl fmt::Display for Originate {
673 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
674 let target_str = match &self.target {
675 OriginateTarget::Extension(ext) => ext.clone(),
676 OriginateTarget::Application(app) => app.to_string_with_dialplan(&DialplanType::Xml),
677 OriginateTarget::InlineApplications(apps) => {
678 let parts: Vec<String> = apps
680 .iter()
681 .map(|a| a.to_string_with_dialplan(&DialplanType::Inline))
682 .collect();
683 parts.join(",")
684 }
685 };
686
687 write!(
688 f,
689 "originate {} {}",
690 self.endpoint,
691 originate_quote(&target_str)
692 )?;
693
694 let dialplan = match &self.target {
698 OriginateTarget::InlineApplications(_) => Some(
699 self.dialplan
700 .unwrap_or(DialplanType::Inline),
701 ),
702 _ => self.dialplan,
703 };
704 let has_ctx = self
705 .context
706 .is_some();
707 let has_name = self
708 .cid_name
709 .is_some();
710 let has_num = self
711 .cid_num
712 .is_some();
713 let has_timeout = self
714 .timeout
715 .is_some();
716
717 if dialplan.is_some() || has_ctx || has_name || has_num || has_timeout {
718 let dp = dialplan
719 .as_ref()
720 .cloned()
721 .unwrap_or(DialplanType::Xml);
722 write!(f, " {}", dp)?;
723 }
724 if has_ctx || has_name || has_num || has_timeout {
725 write!(
726 f,
727 " {}",
728 self.context
729 .as_deref()
730 .unwrap_or("default")
731 )?;
732 }
733 if has_name || has_num || has_timeout {
734 let name = self
735 .cid_name
736 .as_deref()
737 .unwrap_or(UNDEF);
738 write!(f, " {}", originate_quote(name))?;
739 }
740 if has_num || has_timeout {
741 let num = self
742 .cid_num
743 .as_deref()
744 .unwrap_or(UNDEF);
745 write!(f, " {}", originate_quote(num))?;
746 }
747 if let Some(ref timeout) = self.timeout {
748 write!(f, " {}", timeout.as_secs())?;
749 }
750 Ok(())
751 }
752}
753
754impl FromStr for Originate {
755 type Err = OriginateError;
756
757 fn from_str(s: &str) -> Result<Self, Self::Err> {
758 let s = s
759 .strip_prefix("originate")
760 .unwrap_or(s)
761 .trim();
762 let mut args = originate_split(s, ' ')?;
763
764 if args.is_empty() {
765 return Err(OriginateError::ParseError("empty originate".into()));
766 }
767
768 let endpoint_str = args.remove(0);
769 let endpoint: Endpoint = endpoint_str.parse()?;
770
771 if args.is_empty() {
772 return Err(OriginateError::ParseError(
773 "missing target in originate".into(),
774 ));
775 }
776
777 let target_str = originate_unquote(&args.remove(0));
778
779 let dialplan = args
780 .first()
781 .and_then(|s| {
782 s.parse::<DialplanType>()
783 .ok()
784 });
785 if dialplan.is_some() {
786 args.remove(0);
787 }
788
789 let target = super::parse_originate_target(&target_str, dialplan.as_ref())?;
790
791 let context = if !args.is_empty() {
792 Some(args.remove(0))
793 } else {
794 None
795 };
796 let cid_name = if !args.is_empty() {
797 let v = args.remove(0);
798 if v.eq_ignore_ascii_case(UNDEF) {
799 None
800 } else {
801 Some(v)
802 }
803 } else {
804 None
805 };
806 let cid_num = if !args.is_empty() {
807 let v = args.remove(0);
808 if v.eq_ignore_ascii_case(UNDEF) {
809 None
810 } else {
811 Some(v)
812 }
813 } else {
814 None
815 };
816 let timeout = if !args.is_empty() {
817 Some(Duration::from_secs(
818 args.remove(0)
819 .parse::<u64>()
820 .map_err(|e| OriginateError::ParseError(format!("invalid timeout: {}", e)))?,
821 ))
822 } else {
823 None
824 };
825
826 let mut orig = match target {
828 OriginateTarget::Extension(ref ext) => Self::extension(endpoint, ext.clone()),
829 OriginateTarget::Application(ref app) => Self::application(endpoint, app.clone()),
830 OriginateTarget::InlineApplications(ref apps) => Self::inline(endpoint, apps.clone())?,
831 };
832 orig.dialplan = dialplan;
833 orig.context = context;
834 orig.cid_name = cid_name;
835 orig.cid_num = cid_num;
836 orig.timeout = timeout;
837 Ok(orig)
838 }
839}
840
841#[derive(Debug, thiserror::Error)]
843#[non_exhaustive]
844pub enum OriginateError {
845 #[error("unclosed quote at: {0}")]
847 UnclosedQuote(String),
848 #[error("parse error: {0}")]
850 ParseError(String),
851 #[error("inline originate requires at least one application")]
853 EmptyInlineApplications,
854 #[error("extension target is incompatible with inline dialplan")]
856 ExtensionWithInlineDialplan,
857}
858
859#[cfg(test)]
860mod tests {
861 use super::*;
862 use crate::commands::endpoint::{LoopbackEndpoint, SofiaEndpoint, SofiaGateway};
863
864 #[test]
867 fn variables_standard_chars() {
868 let mut vars = Variables::new(VariablesType::Default);
869 vars.insert("test_key", "this_value");
870 let result = vars.to_string();
871 assert!(result.contains("test_key"));
872 assert!(result.contains("this_value"));
873 }
874
875 #[test]
876 fn variables_comma_escaped() {
877 let mut vars = Variables::new(VariablesType::Default);
878 vars.insert("test_key", "this,is,a,value");
879 let result = vars.to_string();
880 assert!(result.contains("\\,"));
881 }
882
883 #[test]
884 fn variables_spaces_quoted() {
885 let mut vars = Variables::new(VariablesType::Default);
886 vars.insert("test_key", "this is a value");
887 let result = vars.to_string();
888 assert_eq!(
889 result
890 .matches('\'')
891 .count(),
892 2
893 );
894 }
895
896 #[test]
897 fn variables_single_quote_escaped() {
898 let mut vars = Variables::new(VariablesType::Default);
899 vars.insert("test_key", "let's_this_be_a_value");
900 let result = vars.to_string();
901 assert!(result.contains("\\'"));
902 }
903
904 #[test]
905 fn variables_enterprise_delimiters() {
906 let mut vars = Variables::new(VariablesType::Enterprise);
907 vars.insert("k", "v");
908 let result = vars.to_string();
909 assert!(result.starts_with('<'));
910 assert!(result.ends_with('>'));
911 }
912
913 #[test]
914 fn variables_channel_delimiters() {
915 let mut vars = Variables::new(VariablesType::Channel);
916 vars.insert("k", "v");
917 let result = vars.to_string();
918 assert!(result.starts_with('['));
919 assert!(result.ends_with(']'));
920 }
921
922 #[test]
923 fn variables_default_delimiters() {
924 let mut vars = Variables::new(VariablesType::Default);
925 vars.insert("k", "v");
926 let result = vars.to_string();
927 assert!(result.starts_with('{'));
928 assert!(result.ends_with('}'));
929 }
930
931 #[test]
932 fn variables_parse_round_trip() {
933 let mut vars = Variables::new(VariablesType::Default);
934 vars.insert("origination_caller_id_number", "9005551212");
935 vars.insert("sip_h_Call-Info", "<url>;meta=123,<uri>");
936 let s = vars.to_string();
937 let parsed: Variables = s
938 .parse()
939 .unwrap();
940 assert_eq!(
941 parsed.get("origination_caller_id_number"),
942 Some("9005551212")
943 );
944 assert_eq!(parsed.get("sip_h_Call-Info"), Some("<url>;meta=123,<uri>"));
945 }
946
947 #[test]
948 fn split_unescaped_commas_basic() {
949 assert_eq!(split_unescaped_commas("a,b,c"), vec!["a", "b", "c"]);
950 }
951
952 #[test]
953 fn split_unescaped_commas_escaped() {
954 assert_eq!(split_unescaped_commas(r"a\,b,c"), vec![r"a\,b", "c"]);
955 }
956
957 #[test]
958 fn split_unescaped_commas_double_backslash() {
959 assert_eq!(split_unescaped_commas(r"a\\,b"), vec![r"a\\", "b"]);
961 }
962
963 #[test]
964 fn split_unescaped_commas_triple_backslash() {
965 assert_eq!(split_unescaped_commas(r"a\\\,b"), vec![r"a\\\,b"]);
967 }
968
969 #[test]
972 fn endpoint_uri_only() {
973 let ep = Endpoint::Sofia(SofiaEndpoint {
974 profile: "internal".into(),
975 destination: "123@example.com".into(),
976 variables: None,
977 });
978 assert_eq!(ep.to_string(), "sofia/internal/123@example.com");
979 }
980
981 #[test]
982 fn endpoint_uri_with_variable() {
983 let mut vars = Variables::new(VariablesType::Default);
984 vars.insert("one_variable", "1");
985 let ep = Endpoint::Sofia(SofiaEndpoint {
986 profile: "internal".into(),
987 destination: "123@example.com".into(),
988 variables: Some(vars),
989 });
990 assert_eq!(
991 ep.to_string(),
992 "{one_variable=1}sofia/internal/123@example.com"
993 );
994 }
995
996 #[test]
997 fn endpoint_variable_with_quote() {
998 let mut vars = Variables::new(VariablesType::Default);
999 vars.insert("one_variable", "one'quote");
1000 let ep = Endpoint::Sofia(SofiaEndpoint {
1001 profile: "internal".into(),
1002 destination: "123@example.com".into(),
1003 variables: Some(vars),
1004 });
1005 assert_eq!(
1006 ep.to_string(),
1007 "{one_variable=one\\'quote}sofia/internal/123@example.com"
1008 );
1009 }
1010
1011 #[test]
1012 fn loopback_endpoint_display() {
1013 let mut vars = Variables::new(VariablesType::Default);
1014 vars.insert("one_variable", "1");
1015 let ep = Endpoint::Loopback(
1016 LoopbackEndpoint::new("aUri")
1017 .with_context("aContext")
1018 .with_variables(vars),
1019 );
1020 assert_eq!(ep.to_string(), "{one_variable=1}loopback/aUri/aContext");
1021 }
1022
1023 #[test]
1024 fn sofia_gateway_endpoint_display() {
1025 let mut vars = Variables::new(VariablesType::Default);
1026 vars.insert("one_variable", "1");
1027 let ep = Endpoint::SofiaGateway(SofiaGateway {
1028 destination: "aUri".into(),
1029 profile: None,
1030 gateway: "internal".into(),
1031 variables: Some(vars),
1032 });
1033 assert_eq!(
1034 ep.to_string(),
1035 "{one_variable=1}sofia/gateway/internal/aUri"
1036 );
1037 }
1038
1039 #[test]
1042 fn application_xml_format() {
1043 let app = Application::new("testApp", Some("testArg"));
1044 assert_eq!(
1045 app.to_string_with_dialplan(&DialplanType::Xml),
1046 "&testApp(testArg)"
1047 );
1048 }
1049
1050 #[test]
1051 fn application_inline_format() {
1052 let app = Application::new("testApp", Some("testArg"));
1053 assert_eq!(
1054 app.to_string_with_dialplan(&DialplanType::Inline),
1055 "testApp:testArg"
1056 );
1057 }
1058
1059 #[test]
1060 fn application_inline_no_args() {
1061 let app = Application::simple("park");
1062 assert_eq!(app.to_string_with_dialplan(&DialplanType::Inline), "park");
1063 }
1064
1065 #[test]
1068 fn originate_xml_display() {
1069 let ep = Endpoint::Sofia(SofiaEndpoint {
1070 profile: "internal".into(),
1071 destination: "123@example.com".into(),
1072 variables: None,
1073 });
1074 let orig = Originate::application(ep, Application::new("conference", Some("1")))
1075 .dialplan(DialplanType::Xml)
1076 .unwrap();
1077 assert_eq!(
1078 orig.to_string(),
1079 "originate sofia/internal/123@example.com &conference(1) XML"
1080 );
1081 }
1082
1083 #[test]
1084 fn originate_inline_display() {
1085 let ep = Endpoint::Sofia(SofiaEndpoint {
1086 profile: "internal".into(),
1087 destination: "123@example.com".into(),
1088 variables: None,
1089 });
1090 let orig = Originate::inline(ep, vec![Application::new("conference", Some("1"))])
1091 .unwrap()
1092 .dialplan(DialplanType::Inline)
1093 .unwrap();
1094 assert_eq!(
1095 orig.to_string(),
1096 "originate sofia/internal/123@example.com conference:1 inline"
1097 );
1098 }
1099
1100 #[test]
1101 fn originate_extension_display() {
1102 let ep = Endpoint::Sofia(SofiaEndpoint {
1103 profile: "internal".into(),
1104 destination: "123@example.com".into(),
1105 variables: None,
1106 });
1107 let orig = Originate::extension(ep, "1000")
1108 .dialplan(DialplanType::Xml)
1109 .unwrap()
1110 .context("default");
1111 assert_eq!(
1112 orig.to_string(),
1113 "originate sofia/internal/123@example.com 1000 XML default"
1114 );
1115 }
1116
1117 #[test]
1118 fn originate_extension_round_trip() {
1119 let input = "originate sofia/internal/test@example.com 1000 XML default";
1120 let parsed: Originate = input
1121 .parse()
1122 .unwrap();
1123 assert_eq!(parsed.to_string(), input);
1124 assert!(matches!(parsed.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1125 }
1126
1127 #[test]
1128 fn originate_extension_no_dialplan() {
1129 let input = "originate sofia/internal/test@example.com 1000";
1130 let parsed: Originate = input
1131 .parse()
1132 .unwrap();
1133 assert!(matches!(parsed.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1134 assert_eq!(parsed.to_string(), input);
1135 }
1136
1137 #[test]
1138 fn originate_extension_with_inline_errors() {
1139 let ep = Endpoint::Sofia(SofiaEndpoint {
1140 profile: "internal".into(),
1141 destination: "123@example.com".into(),
1142 variables: None,
1143 });
1144 let result = Originate::extension(ep, "1000").dialplan(DialplanType::Inline);
1145 assert!(result.is_err());
1146 }
1147
1148 #[test]
1149 fn originate_empty_inline_errors() {
1150 let ep = Endpoint::Sofia(SofiaEndpoint {
1151 profile: "internal".into(),
1152 destination: "123@example.com".into(),
1153 variables: None,
1154 });
1155 let result = Originate::inline(ep, vec![]);
1156 assert!(result.is_err());
1157 }
1158
1159 #[test]
1160 fn originate_from_string_round_trip() {
1161 let input = "originate {test='variable with quote'}sofia/internal/test@example.com 123";
1162 let orig: Originate = input
1163 .parse()
1164 .unwrap();
1165 assert!(matches!(orig.target(), OriginateTarget::Extension(ref e) if e == "123"));
1166 assert_eq!(orig.to_string(), input);
1167 }
1168
1169 #[test]
1170 fn originate_socket_app_quoted() {
1171 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1172 let orig = Originate::application(
1173 ep,
1174 Application::new("socket", Some("127.0.0.1:8040 async full")),
1175 );
1176 assert_eq!(
1177 orig.to_string(),
1178 "originate loopback/9199/test '&socket(127.0.0.1:8040 async full)'"
1179 );
1180 }
1181
1182 #[test]
1183 fn originate_socket_round_trip() {
1184 let input = "originate loopback/9199/test '&socket(127.0.0.1:8040 async full)'";
1185 let parsed: Originate = input
1186 .parse()
1187 .unwrap();
1188 assert_eq!(parsed.to_string(), input);
1189 if let OriginateTarget::Application(ref app) = parsed.target() {
1190 assert_eq!(
1191 app.args
1192 .as_deref(),
1193 Some("127.0.0.1:8040 async full")
1194 );
1195 } else {
1196 panic!("expected Application target");
1197 }
1198 }
1199
1200 #[test]
1201 fn originate_display_round_trip() {
1202 let ep = Endpoint::Sofia(SofiaEndpoint {
1203 profile: "internal".into(),
1204 destination: "123@example.com".into(),
1205 variables: None,
1206 });
1207 let orig = Originate::application(ep, Application::new("conference", Some("1")))
1208 .dialplan(DialplanType::Xml)
1209 .unwrap();
1210 let s = orig.to_string();
1211 let parsed: Originate = s
1212 .parse()
1213 .unwrap();
1214 assert_eq!(parsed.to_string(), s);
1215 }
1216
1217 #[test]
1218 fn originate_inline_no_args_round_trip() {
1219 let input = "originate sofia/internal/123@example.com park inline";
1220 let parsed: Originate = input
1221 .parse()
1222 .unwrap();
1223 assert_eq!(parsed.to_string(), input);
1224 if let OriginateTarget::InlineApplications(ref apps) = parsed.target() {
1225 assert!(apps[0]
1226 .args
1227 .is_none());
1228 } else {
1229 panic!("expected InlineApplications target");
1230 }
1231 }
1232
1233 #[test]
1234 fn originate_inline_multi_app_round_trip() {
1235 let input =
1236 "originate sofia/internal/123@example.com playback:/tmp/test.wav,hangup:NORMAL_CLEARING inline";
1237 let parsed: Originate = input
1238 .parse()
1239 .unwrap();
1240 assert_eq!(parsed.to_string(), input);
1241 }
1242
1243 #[test]
1244 fn originate_inline_auto_dialplan() {
1245 let ep = Endpoint::Sofia(SofiaEndpoint {
1246 profile: "internal".into(),
1247 destination: "123@example.com".into(),
1248 variables: None,
1249 });
1250 let orig = Originate::inline(ep, vec![Application::simple("park")]).unwrap();
1251 assert!(orig
1252 .to_string()
1253 .contains("inline"));
1254 }
1255
1256 #[test]
1259 fn dialplan_type_display() {
1260 assert_eq!(DialplanType::Inline.to_string(), "inline");
1261 assert_eq!(DialplanType::Xml.to_string(), "XML");
1262 }
1263
1264 #[test]
1265 fn dialplan_type_from_str() {
1266 assert_eq!(
1267 "inline"
1268 .parse::<DialplanType>()
1269 .unwrap(),
1270 DialplanType::Inline
1271 );
1272 assert_eq!(
1273 "XML"
1274 .parse::<DialplanType>()
1275 .unwrap(),
1276 DialplanType::Xml
1277 );
1278 }
1279
1280 #[test]
1281 fn dialplan_type_from_str_case_insensitive() {
1282 assert_eq!(
1283 "xml"
1284 .parse::<DialplanType>()
1285 .unwrap(),
1286 DialplanType::Xml
1287 );
1288 assert_eq!(
1289 "Xml"
1290 .parse::<DialplanType>()
1291 .unwrap(),
1292 DialplanType::Xml
1293 );
1294 assert_eq!(
1295 "INLINE"
1296 .parse::<DialplanType>()
1297 .unwrap(),
1298 DialplanType::Inline
1299 );
1300 assert_eq!(
1301 "Inline"
1302 .parse::<DialplanType>()
1303 .unwrap(),
1304 DialplanType::Inline
1305 );
1306 }
1307
1308 #[test]
1311 fn serde_dialplan_type_xml() {
1312 let json = serde_json::to_string(&DialplanType::Xml).unwrap();
1313 assert_eq!(json, "\"xml\"");
1314 let parsed: DialplanType = serde_json::from_str(&json).unwrap();
1315 assert_eq!(parsed, DialplanType::Xml);
1316 }
1317
1318 #[test]
1319 fn serde_dialplan_type_inline() {
1320 let json = serde_json::to_string(&DialplanType::Inline).unwrap();
1321 assert_eq!(json, "\"inline\"");
1322 let parsed: DialplanType = serde_json::from_str(&json).unwrap();
1323 assert_eq!(parsed, DialplanType::Inline);
1324 }
1325
1326 #[test]
1327 fn serde_variables_type() {
1328 let json = serde_json::to_string(&VariablesType::Enterprise).unwrap();
1329 assert_eq!(json, "\"enterprise\"");
1330 let parsed: VariablesType = serde_json::from_str(&json).unwrap();
1331 assert_eq!(parsed, VariablesType::Enterprise);
1332 }
1333
1334 #[test]
1335 fn serde_variables_flat_default() {
1336 let mut vars = Variables::new(VariablesType::Default);
1337 vars.insert("key1", "val1");
1338 vars.insert("key2", "val2");
1339 let json = serde_json::to_string(&vars).unwrap();
1340 let parsed: Variables = serde_json::from_str(&json).unwrap();
1342 assert_eq!(parsed.scope(), VariablesType::Default);
1343 assert_eq!(parsed.get("key1"), Some("val1"));
1344 assert_eq!(parsed.get("key2"), Some("val2"));
1345 }
1346
1347 #[test]
1348 fn serde_variables_scoped_enterprise() {
1349 let mut vars = Variables::new(VariablesType::Enterprise);
1350 vars.insert("key1", "val1");
1351 let json = serde_json::to_string(&vars).unwrap();
1352 assert!(json.contains("\"enterprise\""));
1354 let parsed: Variables = serde_json::from_str(&json).unwrap();
1355 assert_eq!(parsed.scope(), VariablesType::Enterprise);
1356 assert_eq!(parsed.get("key1"), Some("val1"));
1357 }
1358
1359 #[test]
1360 fn serde_variables_flat_map_deserializes_as_default() {
1361 let json = r#"{"key1":"val1","key2":"val2"}"#;
1362 let vars: Variables = serde_json::from_str(json).unwrap();
1363 assert_eq!(vars.scope(), VariablesType::Default);
1364 assert_eq!(vars.get("key1"), Some("val1"));
1365 assert_eq!(vars.get("key2"), Some("val2"));
1366 }
1367
1368 #[test]
1369 fn serde_variables_scoped_deserializes() {
1370 let json = r#"{"scope":"channel","vars":{"k":"v"}}"#;
1371 let vars: Variables = serde_json::from_str(json).unwrap();
1372 assert_eq!(vars.scope(), VariablesType::Channel);
1373 assert_eq!(vars.get("k"), Some("v"));
1374 }
1375
1376 #[test]
1377 fn serde_application() {
1378 let app = Application::new("park", None::<&str>);
1379 let json = serde_json::to_string(&app).unwrap();
1380 let parsed: Application = serde_json::from_str(&json).unwrap();
1381 assert_eq!(parsed, app);
1382 }
1383
1384 #[test]
1385 fn serde_application_with_args() {
1386 let app = Application::new("conference", Some("1"));
1387 let json = serde_json::to_string(&app).unwrap();
1388 let parsed: Application = serde_json::from_str(&json).unwrap();
1389 assert_eq!(parsed, app);
1390 }
1391
1392 #[test]
1393 fn serde_application_skips_none_args() {
1394 let app = Application::new("park", None::<&str>);
1395 let json = serde_json::to_string(&app).unwrap();
1396 assert!(!json.contains("args"));
1397 }
1398
1399 #[test]
1400 fn serde_originate_application_round_trip() {
1401 let ep = Endpoint::Sofia(SofiaEndpoint {
1402 profile: "internal".into(),
1403 destination: "123@example.com".into(),
1404 variables: None,
1405 });
1406 let orig = Originate::application(ep, Application::new("park", None::<&str>))
1407 .dialplan(DialplanType::Xml)
1408 .unwrap()
1409 .context("default")
1410 .cid_name("Test")
1411 .cid_num("5551234")
1412 .timeout(Duration::from_secs(30));
1413 let json = serde_json::to_string(&orig).unwrap();
1414 assert!(json.contains("\"application\""));
1415 let parsed: Originate = serde_json::from_str(&json).unwrap();
1416 assert_eq!(parsed, orig);
1417 }
1418
1419 #[test]
1420 fn serde_originate_extension() {
1421 let json = r#"{
1422 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1423 "extension": "1000",
1424 "dialplan": "xml",
1425 "context": "default"
1426 }"#;
1427 let orig: Originate = serde_json::from_str(json).unwrap();
1428 assert!(matches!(orig.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1429 assert_eq!(
1430 orig.to_string(),
1431 "originate sofia/internal/123@example.com 1000 XML default"
1432 );
1433 }
1434
1435 #[test]
1436 fn serde_originate_extension_with_inline_rejected() {
1437 let json = r#"{
1438 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1439 "extension": "1000",
1440 "dialplan": "inline"
1441 }"#;
1442 let result = serde_json::from_str::<Originate>(json);
1443 assert!(result.is_err());
1444 }
1445
1446 #[test]
1447 fn serde_originate_empty_inline_rejected() {
1448 let json = r#"{
1449 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1450 "inline_applications": []
1451 }"#;
1452 let result = serde_json::from_str::<Originate>(json);
1453 assert!(result.is_err());
1454 }
1455
1456 #[test]
1457 fn serde_originate_inline_applications() {
1458 let json = r#"{
1459 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1460 "inline_applications": [
1461 {"name": "playback", "args": "/tmp/test.wav"},
1462 {"name": "hangup", "args": "NORMAL_CLEARING"}
1463 ]
1464 }"#;
1465 let orig: Originate = serde_json::from_str(json).unwrap();
1466 if let OriginateTarget::InlineApplications(ref apps) = orig.target() {
1467 assert_eq!(apps.len(), 2);
1468 } else {
1469 panic!("expected InlineApplications");
1470 }
1471 assert!(orig
1472 .to_string()
1473 .contains("inline"));
1474 }
1475
1476 #[test]
1477 fn serde_originate_skips_none_fields() {
1478 let ep = Endpoint::Sofia(SofiaEndpoint {
1479 profile: "internal".into(),
1480 destination: "123@example.com".into(),
1481 variables: None,
1482 });
1483 let orig = Originate::application(ep, Application::new("park", None::<&str>));
1484 let json = serde_json::to_string(&orig).unwrap();
1485 assert!(!json.contains("dialplan"));
1486 assert!(!json.contains("context"));
1487 assert!(!json.contains("cid_name"));
1488 assert!(!json.contains("cid_num"));
1489 assert!(!json.contains("timeout"));
1490 }
1491
1492 #[test]
1493 fn serde_originate_to_wire_format() {
1494 let json = r#"{
1495 "endpoint": {"sofia": {"profile": "internal", "destination": "123@example.com"}},
1496 "application": {"name": "park"},
1497 "dialplan": "xml",
1498 "context": "default"
1499 }"#;
1500 let orig: Originate = serde_json::from_str(json).unwrap();
1501 let wire = orig.to_string();
1502 assert!(wire.starts_with("originate"));
1503 assert!(wire.contains("sofia/internal/123@example.com"));
1504 assert!(wire.contains("&park()"));
1505 assert!(wire.contains("XML"));
1506 }
1507
1508 #[test]
1511 fn application_simple_no_args() {
1512 let app = Application::simple("park");
1513 assert_eq!(app.name, "park");
1514 assert!(app
1515 .args
1516 .is_none());
1517 }
1518
1519 #[test]
1520 fn application_simple_xml_format() {
1521 let app = Application::simple("park");
1522 assert_eq!(app.to_string_with_dialplan(&DialplanType::Xml), "&park()");
1523 }
1524
1525 #[test]
1528 fn originate_target_from_application() {
1529 let target: OriginateTarget = Application::simple("park").into();
1530 assert!(matches!(target, OriginateTarget::Application(_)));
1531 }
1532
1533 #[test]
1534 fn originate_target_from_vec() {
1535 let target: OriginateTarget = vec![
1536 Application::new("conference", Some("1")),
1537 Application::new("hangup", Some("NORMAL_CLEARING")),
1538 ]
1539 .into();
1540 if let OriginateTarget::InlineApplications(apps) = target {
1541 assert_eq!(apps.len(), 2);
1542 } else {
1543 panic!("expected InlineApplications");
1544 }
1545 }
1546
1547 #[test]
1548 fn originate_target_application_wire_format() {
1549 let ep = Endpoint::Sofia(SofiaEndpoint {
1550 profile: "internal".into(),
1551 destination: "123@example.com".into(),
1552 variables: None,
1553 });
1554 let orig = Originate::application(ep, Application::simple("park"));
1555 assert_eq!(
1556 orig.to_string(),
1557 "originate sofia/internal/123@example.com &park()"
1558 );
1559 }
1560
1561 #[test]
1562 fn originate_timeout_only_fills_positional_gaps() {
1563 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1564 let cmd = Originate::application(ep, Application::simple("park"))
1565 .timeout(Duration::from_secs(30));
1566 assert_eq!(
1569 cmd.to_string(),
1570 "originate loopback/9199/test &park() XML default undef undef 30"
1571 );
1572 }
1573
1574 #[test]
1575 fn originate_cid_num_only_fills_preceding_gaps() {
1576 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1577 let cmd = Originate::application(ep, Application::simple("park")).cid_num("5551234");
1578 assert_eq!(
1579 cmd.to_string(),
1580 "originate loopback/9199/test &park() XML default undef 5551234"
1581 );
1582 }
1583
1584 #[test]
1585 fn originate_context_only_fills_dialplan() {
1586 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1587 let cmd = Originate::extension(ep, "1000").context("myctx");
1588 assert_eq!(
1589 cmd.to_string(),
1590 "originate loopback/9199/test 1000 XML myctx"
1591 );
1592 }
1593
1594 #[test]
1600 fn originate_context_gap_filler_round_trip_asymmetry() {
1601 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("test"));
1602 let cmd = Originate::application(ep, Application::simple("park")).cid_name("Alice");
1603 let wire = cmd.to_string();
1604 assert!(wire.contains("default"), "gap-filler should emit 'default'");
1605
1606 let parsed: Originate = wire
1607 .parse()
1608 .unwrap();
1609 assert_eq!(parsed.context_str(), Some("default"));
1611
1612 assert_eq!(parsed.to_string(), wire);
1614 }
1615
1616 #[test]
1619 fn serde_originate_full_round_trip_with_variables() {
1620 let mut ep_vars = Variables::new(VariablesType::Default);
1621 ep_vars.insert("originate_timeout", "30");
1622 ep_vars.insert("sip_h_X-Custom", "value with spaces");
1623 let ep = Endpoint::SofiaGateway(SofiaGateway {
1624 gateway: "my_provider".into(),
1625 destination: "18005551234".into(),
1626 profile: Some("external".into()),
1627 variables: Some(ep_vars),
1628 });
1629 let orig = Originate::application(ep, Application::new("park", None::<&str>))
1630 .dialplan(DialplanType::Xml)
1631 .unwrap()
1632 .context("public")
1633 .cid_name("Test Caller")
1634 .cid_num("5551234")
1635 .timeout(Duration::from_secs(60));
1636 let json = serde_json::to_string(&orig).unwrap();
1637 let parsed: Originate = serde_json::from_str(&json).unwrap();
1638 assert_eq!(parsed, orig);
1639 assert_eq!(parsed.to_string(), orig.to_string());
1640 }
1641
1642 #[test]
1643 fn serde_originate_inline_round_trip_with_all_fields() {
1644 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("default"));
1645 let orig = Originate::inline(
1646 ep,
1647 vec![
1648 Application::new("playback", Some("/tmp/test.wav")),
1649 Application::new("hangup", Some("NORMAL_CLEARING")),
1650 ],
1651 )
1652 .unwrap()
1653 .dialplan(DialplanType::Inline)
1654 .unwrap()
1655 .context("default")
1656 .cid_name("IVR")
1657 .cid_num("0000")
1658 .timeout(Duration::from_secs(45));
1659 let json = serde_json::to_string(&orig).unwrap();
1660 let parsed: Originate = serde_json::from_str(&json).unwrap();
1661 assert_eq!(parsed, orig);
1662 assert_eq!(parsed.to_string(), orig.to_string());
1663 }
1664
1665 #[test]
1668 fn variables_from_str_empty_block() {
1669 let result = "{}".parse::<Variables>();
1670 assert!(
1671 result.is_ok(),
1672 "empty variable block should parse successfully"
1673 );
1674 let vars = result.unwrap();
1675 assert!(
1676 vars.is_empty(),
1677 "parsed empty block should have no variables"
1678 );
1679 }
1680
1681 #[test]
1682 fn variables_from_str_empty_channel_block() {
1683 let result = "[]".parse::<Variables>();
1684 assert!(result.is_ok());
1685 let vars = result.unwrap();
1686 assert!(vars.is_empty());
1687 assert_eq!(vars.scope(), VariablesType::Channel);
1688 }
1689
1690 #[test]
1691 fn variables_from_str_empty_enterprise_block() {
1692 let result = "<>".parse::<Variables>();
1693 assert!(result.is_ok());
1694 let vars = result.unwrap();
1695 assert!(vars.is_empty());
1696 assert_eq!(vars.scope(), VariablesType::Enterprise);
1697 }
1698
1699 #[test]
1702 fn originate_context_named_inline() {
1703 let ep = Endpoint::Sofia(SofiaEndpoint {
1704 profile: "internal".into(),
1705 destination: "123@example.com".into(),
1706 variables: None,
1707 });
1708 let orig = Originate::extension(ep, "1000")
1709 .dialplan(DialplanType::Xml)
1710 .unwrap()
1711 .context("inline");
1712 let wire = orig.to_string();
1713 assert!(wire.contains("XML inline"), "wire: {}", wire);
1714 let parsed: Originate = wire
1715 .parse()
1716 .unwrap();
1717 assert_eq!(parsed.to_string(), wire);
1720 }
1721
1722 #[test]
1723 fn originate_context_named_xml() {
1724 let ep = Endpoint::Sofia(SofiaEndpoint {
1725 profile: "internal".into(),
1726 destination: "123@example.com".into(),
1727 variables: None,
1728 });
1729 let orig = Originate::extension(ep, "1000")
1730 .dialplan(DialplanType::Xml)
1731 .unwrap()
1732 .context("XML");
1733 let wire = orig.to_string();
1734 assert!(wire.contains("XML XML"), "wire: {}", wire);
1736 let parsed: Originate = wire
1737 .parse()
1738 .unwrap();
1739 assert_eq!(parsed.to_string(), wire);
1740 }
1741
1742 #[test]
1743 fn originate_accessors() {
1744 let ep = Endpoint::Loopback(LoopbackEndpoint::new("9199").with_context("default"));
1745 let cmd = Originate::extension(ep, "1000")
1746 .dialplan(DialplanType::Xml)
1747 .unwrap()
1748 .context("default")
1749 .cid_name("Alice")
1750 .cid_num("5551234")
1751 .timeout(Duration::from_secs(30));
1752
1753 assert!(matches!(cmd.target(), OriginateTarget::Extension(ref e) if e == "1000"));
1754 assert_eq!(cmd.dialplan_type(), Some(&DialplanType::Xml));
1755 assert_eq!(cmd.context_str(), Some("default"));
1756 assert_eq!(cmd.caller_id_name(), Some("Alice"));
1757 assert_eq!(cmd.caller_id_number(), Some("5551234"));
1758 assert_eq!(cmd.timeout_seconds(), Some(30));
1759 }
1760}