1use std::borrow::Cow;
2use std::ops::Range;
3
4use super::{after_prefix, check_expression_field, check_field, undeliverable, EndpointFieldFault};
5use crate::commands::originate::OriginateError;
6use crate::commands::variables::Variables;
7use crate::switch_passes::originate_legs::splits_into_threads;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12#[cfg_attr(feature = "serde", serde(try_from = "config::SofiaEndpoint"))]
13#[non_exhaustive]
14pub struct SofiaEndpoint {
15 pub profile: String,
17 pub destination: String,
19 #[cfg_attr(
21 feature = "serde",
22 serde(default, skip_serializing_if = "Option::is_none")
23 )]
24 pub variables: Option<Variables>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31#[cfg_attr(feature = "serde", serde(try_from = "config::SofiaGateway"))]
32#[non_exhaustive]
33pub struct SofiaGateway {
34 pub gateway: String,
36 pub destination: String,
38 #[cfg_attr(
40 feature = "serde",
41 serde(default, skip_serializing_if = "Option::is_none")
42 )]
43 pub profile: Option<String>,
44 #[cfg_attr(
46 feature = "serde",
47 serde(default, skip_serializing_if = "Option::is_none")
48 )]
49 pub variables: Option<Variables>,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
65#[cfg_attr(feature = "serde", serde(try_from = "config::SofiaContact"))]
66#[non_exhaustive]
67pub struct SofiaContact {
68 pub user: String,
70 pub domain: String,
72 #[cfg_attr(
74 feature = "serde",
75 serde(default, skip_serializing_if = "Option::is_none")
76 )]
77 pub profile: Option<String>,
78 #[cfg_attr(
80 feature = "serde",
81 serde(default, skip_serializing_if = "Option::is_none")
82 )]
83 pub variables: Option<Variables>,
84}
85
86pub(super) const URL_UNSAFE: &[u8] = b"\r\n #%&+:;<=>?@[\\]^`{|}\"";
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98#[non_exhaustive]
99pub enum DestinationEncoding {
100 Encoded,
102 Suppressed,
104}
105
106impl SofiaEndpoint {
107 pub fn new(profile: impl Into<String>, destination: impl Into<String>) -> Self {
109 Self {
110 profile: profile.into(),
111 destination: destination.into(),
112 variables: None,
113 }
114 }
115
116 pub fn user(&self, encoding: DestinationEncoding) -> Option<Cow<'_, str>> {
125 dialed_user(&self.destination, encoding, DialPath::Profile)
126 }
127}
128
129impl SofiaGateway {
130 pub fn new(gateway: impl Into<String>, destination: impl Into<String>) -> Self {
132 Self {
133 gateway: gateway.into(),
134 destination: destination.into(),
135 profile: None,
136 variables: None,
137 }
138 }
139
140 pub fn with_profile(mut self, profile: impl Into<String>) -> Self {
142 self.profile = Some(profile.into());
143 self
144 }
145
146 pub fn user(&self, encoding: DestinationEncoding) -> Option<Cow<'_, str>> {
156 dialed_user(&self.destination, encoding, DialPath::Gateway)
157 }
158}
159
160#[derive(Clone, Copy)]
161enum DialPath {
162 Profile,
163 Gateway,
164}
165
166fn dialed_user(
169 destination: &str,
170 encoding: DestinationEncoding,
171 path: DialPath,
172) -> Option<Cow<'_, str>> {
173 let (text, encoded) = match encoding {
174 DestinationEncoding::Encoded => protect_dest_uri(destination),
175 DestinationEncoding::Suppressed => (Cow::Borrowed(destination), None),
176 };
177 let find = |byte: u8, from: usize, end: usize| {
178 (from..end).find(|&at| {
179 text.as_bytes()[at] == byte
180 && !encoded
181 .as_ref()
182 .is_some_and(|user| user.contains(&at))
183 })
184 };
185 let end = find(b'^', 0, text.len()).unwrap_or(text.len());
186 let user = match path {
187 DialPath::Profile => {
188 let scheme = match &encoded {
189 Some(user) if user.start == 0 => 0,
190 _ => scheme_len(&text[..end]),
191 };
192 match (scheme, find(b'@', 0, end)) {
193 (0, Some(at)) => 0..at,
194 (0, None) => 0..find(b'%', 0, end).unwrap_or(end),
195 (len, at) => len..at?,
196 }
197 }
198 DialPath::Gateway => {
199 let end = find(b';', 0, end).unwrap_or(end);
200 0..find(b'@', 0, end).unwrap_or(end)
201 }
202 };
203 if user.is_empty() {
204 return None;
205 }
206 Some(match text {
207 Cow::Borrowed(text) => Cow::Borrowed(&text[user]),
208 Cow::Owned(text) => Cow::Owned(text[user].to_owned()),
209 })
210}
211
212fn scheme_len(text: &str) -> usize {
214 ["sip:", "sips:"]
215 .into_iter()
216 .find(|scheme| {
217 text.get(..scheme.len())
218 .is_some_and(|head| head.eq_ignore_ascii_case(scheme))
219 })
220 .map_or(0, str::len)
221}
222
223fn protect_dest_uri(destination: &str) -> (Cow<'_, str>, Option<Range<usize>>) {
226 let unchanged = (Cow::Borrowed(destination), None);
227 if !destination.contains('@') {
228 return unchanged;
229 }
230 let segment_start = destination
231 .rfind('/')
232 .map_or(0, |slash| slash + 1);
233 let (prefix, segment) = destination.split_at(segment_start);
234 let ahead = segment
235 .split_once('@')
236 .map_or(segment, |(ahead, _)| ahead);
237 if !ahead
238 .bytes()
239 .any(|byte| URL_UNSAFE.contains(&byte))
240 {
241 return unchanged;
242 }
243 let scheme = scheme_len(segment);
244 let rest = &segment[scheme..];
245 let Some(at) = rest.find('@') else {
246 return (
247 Cow::Borrowed(&destination[..segment_start.saturating_sub(1)]),
248 None,
249 );
250 };
251 let user = prefix.len()..prefix.len() + at;
252 let text = match (scheme, prefix.is_empty()) {
253 (0, _) => Cow::Borrowed(destination),
254 (_, true) => Cow::Borrowed(rest),
255 (_, false) => Cow::Owned(format!("{prefix}{rest}")),
256 };
257 (text, Some(user))
258}
259
260impl SofiaContact {
261 pub fn new(user: impl Into<String>, domain: impl Into<String>) -> Self {
263 Self {
264 user: user.into(),
265 domain: domain.into(),
266 profile: None,
267 variables: None,
268 }
269 }
270
271 pub fn with_profile(mut self, profile: impl Into<String>) -> Self {
276 self.profile = Some(profile.into());
277 self
278 }
279}
280
281impl_dial_string_with_variables!(SofiaEndpoint, write_module_text, |this| format!(
282 "sofia/{}/{}",
283 this.profile, this.destination
284));
285
286impl_dial_string_with_variables!(
287 SofiaGateway,
288 write_module_text,
289 |this| match &this.profile {
290 Some(p) => format!("sofia/gateway/{p}::{}/{}", this.gateway, this.destination),
291 None => format!("sofia/gateway/{}/{}", this.gateway, this.destination),
292 }
293);
294
295impl_dial_string_with_variables!(SofiaContact, write_expression, |this| match &this.profile {
296 Some(p) => format!("${{sofia_contact({p}/{}@{})}}", this.user, this.domain),
297 None => format!("${{sofia_contact({}@{})}}", this.user, this.domain),
298});
299
300impl SofiaEndpoint {
301 pub(crate) fn check_deliverable(&self) -> Result<(), OriginateError> {
304 check_field("sofia", "profile", &self.profile, &["/", "^", "@"])?;
305 if self
306 .profile
307 .eq_ignore_ascii_case("gateway")
308 {
309 return Err(undeliverable(
310 "sofia",
311 "profile",
312 EndpointFieldFault::ReadsAsGateway,
313 ));
314 }
315 check_field("sofia", "destination", &self.destination, &[])
316 }
317
318 pub(crate) fn parse_bare(text: &str) -> Result<Self, OriginateError> {
319 let rest = after_prefix(text, "sofia/", "sofia")?;
320 let (profile, destination) = rest
321 .split_once('/')
322 .ok_or_else(|| {
323 OriginateError::ParseError("sofia endpoint needs profile/destination".into())
324 })?;
325 let ep = Self::new(profile, destination);
326 ep.check_deliverable()?;
327 Ok(ep)
328 }
329}
330
331impl SofiaGateway {
332 pub(crate) fn check_deliverable(&self) -> Result<(), OriginateError> {
335 const KIND: &str = "sofia gateway";
336 let gateway_splits: &[&str] = match self.profile {
337 Some(_) => &["/", "^", "@"],
338 None => &["/", "^", "@", "::"],
339 };
340 check_field(KIND, "gateway", &self.gateway, gateway_splits)?;
341 if let Some(profile) = &self.profile {
342 check_field(KIND, "profile", profile, &["/", "^", "@", "::"])?;
343 if profile.ends_with(':') {
344 return Err(undeliverable(
345 KIND,
346 "profile",
347 EndpointFieldFault::ModuleSeparator("::"),
348 ));
349 }
350 if splits_into_threads(&format!("{profile}::{}", self.gateway)) {
351 let field = if profile.ends_with(":_") {
352 "profile"
353 } else {
354 "gateway"
355 };
356 return Err(undeliverable(
357 KIND,
358 field,
359 EndpointFieldFault::EnterpriseSeparator,
360 ));
361 }
362 }
363 check_field(KIND, "destination", &self.destination, &[])
364 }
365
366 pub(crate) fn parse_bare(text: &str) -> Result<Self, OriginateError> {
367 let rest = after_prefix(text, "sofia/gateway/", "sofia gateway")?;
368 let (gateway_part, destination) = rest
369 .split_once('/')
370 .ok_or_else(|| {
371 OriginateError::ParseError("sofia gateway needs gateway/destination".into())
372 })?;
373 let ep = match gateway_part.split_once("::") {
374 Some((profile, gateway)) => Self::new(gateway, destination).with_profile(profile),
375 None => Self::new(gateway_part, destination),
376 };
377 ep.check_deliverable()?;
378 Ok(ep)
379 }
380}
381
382impl SofiaContact {
383 pub(crate) fn check_deliverable(&self) -> Result<(), OriginateError> {
386 const KIND: &str = "sofia_contact";
387 let user_splits: &[&str] = match self.profile {
388 Some(_) => &["~", "@"],
389 None => &["~", "@", "/"],
390 };
391 check_expression_field(KIND, "user", &self.user, user_splits)?;
392 for (field, value) in [
393 ("domain", Some(&self.domain)),
394 (
395 "profile",
396 self.profile
397 .as_ref(),
398 ),
399 ] {
400 let Some(value) = value else {
401 continue;
402 };
403 check_expression_field(KIND, field, value, &["~", "/"])?;
404 if value.is_empty() {
405 return Err(undeliverable(
406 KIND,
407 field,
408 EndpointFieldFault::EmptyReadsAsDefault,
409 ));
410 }
411 }
412 Ok(())
413 }
414
415 pub(crate) fn parse_bare(text: &str) -> Result<Self, OriginateError> {
416 let inner = text
417 .strip_prefix("${sofia_contact(")
418 .and_then(|r| r.strip_suffix(")}"))
419 .ok_or_else(|| OriginateError::ParseError("not a sofia_contact expression".into()))?;
420 let (profile, user_at_domain) = if let Some((p, rest)) = inner.split_once('/') {
421 (Some(p.to_string()), rest)
422 } else {
423 (None, inner)
424 };
425 let (user, domain) = user_at_domain
426 .split_once('@')
427 .ok_or_else(|| OriginateError::ParseError("sofia_contact needs user@domain".into()))?;
428 let ep = Self {
429 user: user.into(),
430 domain: domain.into(),
431 profile,
432 variables: None,
433 };
434 ep.check_deliverable()?;
435 Ok(ep)
436 }
437}
438
439impl_endpoint_parse!(from_str: SofiaEndpoint, SofiaGateway);
440impl_endpoint_parse!(expression_from_str: SofiaContact => "sofia_contact");
441
442impl_endpoint_parse!(config:
443 SofiaEndpoint {
444 profile: String,
445 destination: String,
446 ;
447 variables: Option<Variables>,
448 }
449 SofiaContact {
450 user: String,
451 domain: String,
452 ;
453 profile: Option<String>,
454 variables: Option<Variables>,
455 }
456 SofiaGateway {
457 gateway: String,
458 destination: String,
459 ;
460 profile: Option<String>,
461 variables: Option<Variables>,
462 }
463);
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468 use crate::commands::variables::VariablesType;
469
470 #[test]
473 fn sofia_endpoint_display() {
474 let ep = SofiaEndpoint {
475 profile: "internal".into(),
476 destination: "1000@example.com".into(),
477 variables: None,
478 };
479 assert_eq!(ep.to_string(), "sofia/internal/1000@example.com");
480 }
481
482 #[test]
483 fn sofia_endpoint_display_with_variables() {
484 let mut vars = Variables::new(VariablesType::Default);
485 vars.insert("originate_timeout", "30");
486 let ep = SofiaEndpoint {
487 profile: "internal".into(),
488 destination: "1000@example.com".into(),
489 variables: Some(vars),
490 };
491 assert_eq!(
492 ep.to_string(),
493 "{originate_timeout=30}sofia/internal/1000@example.com"
494 );
495 }
496
497 #[test]
498 fn sofia_endpoint_from_str() {
499 let ep: SofiaEndpoint = "sofia/internal/1000@example.com"
500 .parse()
501 .unwrap();
502 assert_eq!(ep.profile, "internal");
503 assert_eq!(ep.destination, "1000@example.com");
504 assert!(ep
505 .variables
506 .is_none());
507 }
508
509 #[test]
510 fn sofia_endpoint_from_str_with_variables() {
511 let ep: SofiaEndpoint = "{originate_timeout=30}sofia/internal/1000@example.com"
512 .parse()
513 .unwrap();
514 assert_eq!(ep.profile, "internal");
515 assert_eq!(ep.destination, "1000@example.com");
516 assert_eq!(
517 ep.variables
518 .unwrap()
519 .get("originate_timeout"),
520 Some("30")
521 );
522 }
523
524 #[test]
525 fn sofia_endpoint_round_trip() {
526 let ep = SofiaEndpoint {
527 profile: "external".into(),
528 destination: "sip:user@host:5060".into(),
529 variables: None,
530 };
531 let s = ep.to_string();
532 let parsed: SofiaEndpoint = s
533 .parse()
534 .unwrap();
535 assert_eq!(parsed, ep);
536 }
537
538 #[test]
539 fn serde_sofia_endpoint() {
540 let ep = SofiaEndpoint {
541 profile: "internal".into(),
542 destination: "1000@example.com".into(),
543 variables: None,
544 };
545 let json = serde_json::to_string(&ep).unwrap();
546 let parsed: SofiaEndpoint = serde_json::from_str(&json).unwrap();
547 assert_eq!(parsed, ep);
548 }
549
550 #[test]
551 fn serde_sofia_endpoint_with_variables() {
552 let mut vars = Variables::new(VariablesType::Default);
553 vars.insert("originate_timeout", "30");
554 let ep = SofiaEndpoint {
555 profile: "internal".into(),
556 destination: "1000@example.com".into(),
557 variables: Some(vars),
558 };
559 let json = serde_json::to_string(&ep).unwrap();
560 let parsed: SofiaEndpoint = serde_json::from_str(&json).unwrap();
561 assert_eq!(parsed, ep);
562 }
563
564 #[test]
567 fn sofia_gateway_display() {
568 let ep = SofiaGateway {
569 gateway: "my_provider".into(),
570 destination: "18005551234".into(),
571 profile: None,
572 variables: None,
573 };
574 assert_eq!(ep.to_string(), "sofia/gateway/my_provider/18005551234");
575 }
576
577 #[test]
578 fn sofia_gateway_display_with_profile() {
579 let ep = SofiaGateway {
580 gateway: "my_provider".into(),
581 destination: "18005551234".into(),
582 profile: Some("external".into()),
583 variables: None,
584 };
585 assert_eq!(
586 ep.to_string(),
587 "sofia/gateway/external::my_provider/18005551234"
588 );
589 }
590
591 #[test]
592 fn sofia_gateway_display_with_variables() {
593 let mut vars = Variables::new(VariablesType::Default);
594 vars.insert("sip_h_X-Custom", "value");
595 let ep = SofiaGateway {
596 gateway: "gw1".into(),
597 destination: "1234".into(),
598 profile: None,
599 variables: Some(vars),
600 };
601 assert_eq!(
602 ep.to_string(),
603 "{sip_h_X-Custom=value}sofia/gateway/gw1/1234"
604 );
605 }
606
607 #[test]
608 fn sofia_gateway_from_str() {
609 let ep: SofiaGateway = "sofia/gateway/my_provider/18005551234"
610 .parse()
611 .unwrap();
612 assert_eq!(ep.gateway, "my_provider");
613 assert_eq!(ep.destination, "18005551234");
614 assert!(ep
615 .profile
616 .is_none());
617 }
618
619 #[test]
620 fn sofia_gateway_from_str_with_profile() {
621 let ep: SofiaGateway = "sofia/gateway/external::my_provider/18005551234"
622 .parse()
623 .unwrap();
624 assert_eq!(ep.gateway, "my_provider");
625 assert_eq!(ep.destination, "18005551234");
626 assert_eq!(
627 ep.profile
628 .as_deref(),
629 Some("external")
630 );
631 }
632
633 #[test]
634 fn sofia_gateway_round_trip() {
635 let ep = SofiaGateway {
636 gateway: "carrier".into(),
637 destination: "15551234567".into(),
638 profile: Some("external".into()),
639 variables: None,
640 };
641 let s = ep.to_string();
642 let parsed: SofiaGateway = s
643 .parse()
644 .unwrap();
645 assert_eq!(parsed, ep);
646 }
647
648 #[test]
649 fn serde_sofia_gateway() {
650 let ep = SofiaGateway {
651 gateway: "my_provider".into(),
652 destination: "18005551234".into(),
653 profile: None,
654 variables: None,
655 };
656 let json = serde_json::to_string(&ep).unwrap();
657 let parsed: SofiaGateway = serde_json::from_str(&json).unwrap();
658 assert_eq!(parsed, ep);
659 }
660
661 #[test]
662 fn serde_sofia_gateway_with_profile() {
663 let ep = SofiaGateway {
664 gateway: "my_provider".into(),
665 destination: "18005551234".into(),
666 profile: Some("external".into()),
667 variables: None,
668 };
669 let json = serde_json::to_string(&ep).unwrap();
670 let parsed: SofiaGateway = serde_json::from_str(&json).unwrap();
671 assert_eq!(parsed, ep);
672 }
673
674 #[test]
677 fn sofia_contact_display() {
678 let ep = SofiaContact {
679 user: "1000".into(),
680 domain: "example.com".into(),
681 profile: None,
682 variables: None,
683 };
684 assert_eq!(ep.to_string(), "${sofia_contact(1000@example.com)}");
685 }
686
687 #[test]
688 fn sofia_contact_display_with_profile() {
689 let ep = SofiaContact {
690 user: "1000".into(),
691 domain: "example.com".into(),
692 profile: Some("internal".into()),
693 variables: None,
694 };
695 assert_eq!(
696 ep.to_string(),
697 "${sofia_contact(internal/1000@example.com)}"
698 );
699 }
700
701 #[test]
702 fn sofia_contact_display_all_profiles() {
703 let ep = SofiaContact {
704 user: "1000".into(),
705 domain: "example.com".into(),
706 profile: Some("*".into()),
707 variables: None,
708 };
709 assert_eq!(ep.to_string(), "${sofia_contact(*/1000@example.com)}");
710 }
711
712 #[test]
713 fn sofia_contact_display_with_variables() {
714 let mut vars = Variables::new(VariablesType::Default);
715 vars.insert("presence_id", "1000@example.com");
716 let ep = SofiaContact {
717 user: "1000".into(),
718 domain: "example.com".into(),
719 profile: None,
720 variables: Some(vars),
721 };
722 assert_eq!(
723 ep.to_string(),
724 "{presence_id=1000@example.com}${sofia_contact(1000@example.com)}"
725 );
726 }
727
728 #[test]
729 fn sofia_contact_from_str() {
730 let ep = SofiaContact::parse_bare("${sofia_contact(1000@example.com)}").unwrap();
731 assert_eq!(ep.user, "1000");
732 assert_eq!(ep.domain, "example.com");
733 assert!(ep
734 .profile
735 .is_none());
736 }
737
738 #[test]
739 fn sofia_contact_from_str_with_profile() {
740 let ep = SofiaContact::parse_bare("${sofia_contact(internal/1000@example.com)}").unwrap();
741 assert_eq!(ep.user, "1000");
742 assert_eq!(ep.domain, "example.com");
743 assert_eq!(
744 ep.profile
745 .as_deref(),
746 Some("internal")
747 );
748 }
749
750 #[test]
751 fn sofia_contact_round_trip() {
752 let ep = SofiaContact {
753 user: "bob".into(),
754 domain: "example.com".into(),
755 profile: Some("*".into()),
756 variables: None,
757 };
758 let s = ep.to_string();
759 let parsed = SofiaContact::parse_bare(&s).unwrap();
760 assert_eq!(parsed, ep);
761 }
762
763 #[test]
764 fn sofia_contact_profile_with_at_sign() {
765 let ep = SofiaContact::new("1000", "example.com").with_profile("user@realm");
766 let s = ep.to_string();
767 assert_eq!(s, "${sofia_contact(user@realm/1000@example.com)}");
768 let parsed = SofiaContact::parse_bare(&s).unwrap();
771 assert_eq!(
772 parsed
773 .profile
774 .as_deref(),
775 Some("user@realm")
776 );
777 assert_eq!(parsed.user, "1000");
778 assert_eq!(parsed.domain, "example.com");
779 }
780
781 #[test]
782 fn serde_sofia_contact() {
783 let ep = SofiaContact {
784 user: "1000".into(),
785 domain: "example.com".into(),
786 profile: Some("*".into()),
787 variables: None,
788 };
789 let json = serde_json::to_string(&ep).unwrap();
790 let parsed: SofiaContact = serde_json::from_str(&json).unwrap();
791 assert_eq!(parsed, ep);
792 }
793
794 type UserCase = (&'static str, Option<&'static str>, Option<&'static str>);
797
798 fn assert_users(
799 cases: &[UserCase],
800 user: impl Fn(&str, DestinationEncoding) -> Option<String>,
801 ) {
802 for &(destination, encoded, suppressed) in cases {
803 assert_eq!(
804 user(destination, DestinationEncoding::Encoded).as_deref(),
805 encoded,
806 "{destination:?} encoded"
807 );
808 assert_eq!(
809 user(destination, DestinationEncoding::Suppressed).as_deref(),
810 suppressed,
811 "{destination:?} suppressed"
812 );
813 }
814 }
815
816 #[test]
817 fn sofia_endpoint_user() {
818 assert_users(
819 &[
820 (
821 "5550001234@pbx.example.com",
822 Some("5550001234"),
823 Some("5550001234"),
824 ),
825 ("sip:1000@pbx.example.com:5060", Some("1000"), Some("1000")),
826 ("SIPS:1000@pbx.example.com", Some("1000"), Some("1000")),
827 ("1000^2000@pbx.example.com", Some("1000^2000"), Some("1000")),
828 ("1000@pbx.example.com^2000", Some("1000"), Some("1000")),
829 ("1000%pbx.example.com", Some("1000"), Some("1000")),
830 ("1000", Some("1000"), Some("1000")),
831 ("sip:pbx.example.com", None, None),
832 ("a@b@pbx.example.com", Some("a"), Some("a")),
833 ("x@pbx.example.com/z w", Some("x"), Some("x")),
834 ("a/sip:b@pbx.example.com", Some("a/b"), Some("a/sip:b")),
835 ("@pbx.example.com", None, None),
836 (
837 "+15550001234;npdi=yes@pbx.example.com",
838 Some("+15550001234;npdi=yes"),
839 Some("+15550001234;npdi=yes"),
840 ),
841 ],
842 |destination, encoding| {
843 SofiaEndpoint::new("internal", destination)
844 .user(encoding)
845 .map(Cow::into_owned)
846 },
847 );
848 }
849
850 #[test]
851 fn sofia_gateway_user() {
852 assert_users(
853 &[
854 ("5550009999", Some("5550009999"), Some("5550009999")),
855 (
856 "+15550001234@agent.example.com",
857 Some("+15550001234"),
858 Some("+15550001234"),
859 ),
860 ("1000;transport=tcp", Some("1000"), Some("1000")),
861 ("1000;x=a@b.example.com", Some("1000;x=a"), Some("1000")),
862 ("sip:1000@example.com", Some("1000"), Some("sip:1000")),
863 ("1000^2000@example.com", Some("1000^2000"), Some("1000")),
864 ("", None, None),
865 ],
866 |destination, encoding| {
867 SofiaGateway::new("montreal", destination)
868 .with_profile("voipms")
869 .user(encoding)
870 .map(Cow::into_owned)
871 },
872 );
873 }
874}