Skip to main content

freeswitch_types/variables/
sip_passthrough.rs

1//! FreeSWITCH SIP header passthrough variables (`sip_h_*`, `sip_i_*`, etc.).
2//!
3//! FreeSWITCH exposes SIP headers as channel variables through six prefixes,
4//! each controlling a different direction or SIP method:
5//!
6//! | Prefix | Wire example | Purpose |
7//! |---|---|---|
8//! | `sip_i_` | `sip_i_call_info` | Read incoming INVITE headers |
9//! | `sip_h_` | `sip_h_Call-Info` | Inject header on outgoing request |
10//! | `sip_rh_` | `sip_rh_Call-Info` | Inject header on outgoing response |
11//! | `sip_ph_` | `sip_ph_Call-Info` | Inject header on provisional response |
12//! | `sip_bye_h_` | `sip_bye_h_Call-Info` | Inject header on BYE |
13//! | `sip_nobye_h_` | `sip_nobye_h_Call-Info` | Suppress header on BYE |
14//!
15//! The `Invite` prefix uses a different wire format from the others:
16//! lowercase with hyphens replaced by underscores (`sip_i_call_info`),
17//! while all other prefixes preserve the canonical SIP header casing
18//! (`sip_h_Call-Info`).
19
20use sip_header::SipHeader;
21
22/// Error returned when a raw header name contains invalid characters.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct InvalidHeaderName(String);
25
26impl std::fmt::Display for InvalidHeaderName {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        write!(
29            f,
30            "invalid SIP header name {:?}: contains \\n or \\r",
31            self.0
32        )
33    }
34}
35
36impl std::error::Error for InvalidHeaderName {}
37
38/// FreeSWITCH SIP header passthrough variable prefix.
39///
40/// Each prefix controls which SIP message direction the header variable
41/// applies to.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43#[non_exhaustive]
44pub enum SipHeaderPrefix {
45    /// `sip_i_` — raw INVITE header (read-only, set by `parse-all-invite-headers`).
46    Invite,
47    /// `sip_h_` — inject header on outgoing request (INVITE, REFER, etc.).
48    Request,
49    /// `sip_rh_` — inject header on outgoing response (200 OK, etc.).
50    Response,
51    /// `sip_ph_` — inject header on provisional response (180, 183).
52    Provisional,
53    /// `sip_bye_h_` — inject header on outgoing BYE.
54    Bye,
55    /// `sip_nobye_h_` — suppress a specific header on outgoing BYE.
56    NoBye,
57}
58
59impl SipHeaderPrefix {
60    /// Wire prefix string including trailing separator.
61    pub fn as_str(&self) -> &'static str {
62        match self {
63            Self::Invite => "sip_i_",
64            Self::Request => "sip_h_",
65            Self::Response => "sip_rh_",
66            Self::Provisional => "sip_ph_",
67            Self::Bye => "sip_bye_h_",
68            Self::NoBye => "sip_nobye_h_",
69        }
70    }
71}
72
73impl std::fmt::Display for SipHeaderPrefix {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        f.write_str(self.as_str())
76    }
77}
78
79/// Error returned when parsing an unrecognized passthrough header variable name.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct ParseSipPassthroughError(pub String);
82
83impl std::fmt::Display for ParseSipPassthroughError {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        write!(f, "not a SIP passthrough variable: {}", self.0)
86    }
87}
88
89impl std::error::Error for ParseSipPassthroughError {}
90
91/// A FreeSWITCH SIP passthrough header variable name.
92///
93/// Combines a [`SipHeaderPrefix`] (direction/method) with a SIP header name
94/// to produce the channel variable name used on the wire. Use with
95/// [`HeaderLookup::variable()`](crate::HeaderLookup::variable) for lookups,
96/// or insert into [`Variables`](crate::Variables) for originate commands.
97///
98/// # Typed constructors (known SIP headers)
99///
100/// ```
101/// use freeswitch_types::variables::{SipPassthroughHeader, SipHeaderPrefix};
102/// use sip_header::SipHeader;
103///
104/// let h = SipPassthroughHeader::request(SipHeader::CallInfo);
105/// assert_eq!(h.as_str(), "sip_h_Call-Info");
106///
107/// let h = SipPassthroughHeader::invite(SipHeader::CallInfo);
108/// assert_eq!(h.as_str(), "sip_i_call_info");
109/// ```
110///
111/// # Raw constructors (custom headers)
112///
113/// ```
114/// use freeswitch_types::variables::SipPassthroughHeader;
115///
116/// let h = SipPassthroughHeader::request_raw("X-Tenant").unwrap();
117/// assert_eq!(h.as_str(), "sip_h_X-Tenant");
118/// ```
119#[derive(Debug, Clone, PartialEq, Eq, Hash)]
120pub struct SipPassthroughHeader {
121    prefix: SipHeaderPrefix,
122    canonical_name: String,
123    wire: String,
124}
125
126/// Prefix patterns ordered longest-first for unambiguous `FromStr` matching.
127const PREFIX_PATTERNS: &[(SipHeaderPrefix, &str)] = &[
128    (SipHeaderPrefix::NoBye, "sip_nobye_h_"),
129    (SipHeaderPrefix::Bye, "sip_bye_h_"),
130    (SipHeaderPrefix::Provisional, "sip_ph_"),
131    (SipHeaderPrefix::Response, "sip_rh_"),
132    (SipHeaderPrefix::Invite, "sip_i_"),
133    (SipHeaderPrefix::Request, "sip_h_"),
134];
135
136fn validate_header_name(name: &str) -> Result<(), InvalidHeaderName> {
137    if name.is_empty() || name.contains('\n') || name.contains('\r') {
138        return Err(InvalidHeaderName(name.to_string()));
139    }
140    Ok(())
141}
142
143fn build_wire(prefix: SipHeaderPrefix, canonical: &str) -> String {
144    match prefix {
145        SipHeaderPrefix::Invite => {
146            let mut wire = String::with_capacity(6 + canonical.len());
147            wire.push_str("sip_i_");
148            for ch in canonical.chars() {
149                if ch == '-' {
150                    wire.push('_');
151                } else {
152                    wire.push(ch.to_ascii_lowercase());
153                }
154            }
155            wire
156        }
157        _ => {
158            let pfx = prefix.as_str();
159            let mut wire = String::with_capacity(pfx.len() + canonical.len());
160            wire.push_str(pfx);
161            wire.push_str(canonical);
162            wire
163        }
164    }
165}
166
167impl SipPassthroughHeader {
168    /// Create from a prefix and a known [`SipHeader`].
169    pub fn new(prefix: SipHeaderPrefix, header: SipHeader) -> Self {
170        let canonical = header
171            .as_str()
172            .to_string();
173        let wire = build_wire(prefix, &canonical);
174        Self {
175            prefix,
176            canonical_name: canonical,
177            wire,
178        }
179    }
180
181    /// Create from a prefix and an arbitrary header name.
182    ///
183    /// Returns `Err` if the name contains `\n` or `\r` (wire injection risk).
184    pub fn new_raw(
185        prefix: SipHeaderPrefix,
186        name: impl Into<String>,
187    ) -> Result<Self, InvalidHeaderName> {
188        let canonical = name.into();
189        validate_header_name(&canonical)?;
190        let wire = build_wire(prefix, &canonical);
191        Ok(Self {
192            prefix,
193            canonical_name: canonical,
194            wire,
195        })
196    }
197
198    /// Incoming INVITE header (`sip_i_*`).
199    pub fn invite(header: SipHeader) -> Self {
200        Self::new(SipHeaderPrefix::Invite, header)
201    }
202
203    /// Incoming INVITE header from raw name.
204    pub fn invite_raw(name: impl Into<String>) -> Result<Self, InvalidHeaderName> {
205        Self::new_raw(SipHeaderPrefix::Invite, name)
206    }
207
208    /// Outgoing request header (`sip_h_*`).
209    pub fn request(header: SipHeader) -> Self {
210        Self::new(SipHeaderPrefix::Request, header)
211    }
212
213    /// Outgoing request header from raw name.
214    pub fn request_raw(name: impl Into<String>) -> Result<Self, InvalidHeaderName> {
215        Self::new_raw(SipHeaderPrefix::Request, name)
216    }
217
218    /// Outgoing response header (`sip_rh_*`).
219    pub fn response(header: SipHeader) -> Self {
220        Self::new(SipHeaderPrefix::Response, header)
221    }
222
223    /// Outgoing response header from raw name.
224    pub fn response_raw(name: impl Into<String>) -> Result<Self, InvalidHeaderName> {
225        Self::new_raw(SipHeaderPrefix::Response, name)
226    }
227
228    /// Provisional response header (`sip_ph_*`).
229    pub fn provisional(header: SipHeader) -> Self {
230        Self::new(SipHeaderPrefix::Provisional, header)
231    }
232
233    /// Provisional response header from raw name.
234    pub fn provisional_raw(name: impl Into<String>) -> Result<Self, InvalidHeaderName> {
235        Self::new_raw(SipHeaderPrefix::Provisional, name)
236    }
237
238    /// BYE request header (`sip_bye_h_*`).
239    pub fn bye(header: SipHeader) -> Self {
240        Self::new(SipHeaderPrefix::Bye, header)
241    }
242
243    /// BYE request header from raw name.
244    pub fn bye_raw(name: impl Into<String>) -> Result<Self, InvalidHeaderName> {
245        Self::new_raw(SipHeaderPrefix::Bye, name)
246    }
247
248    /// Suppress header on BYE (`sip_nobye_h_*`).
249    pub fn no_bye(header: SipHeader) -> Self {
250        Self::new(SipHeaderPrefix::NoBye, header)
251    }
252
253    /// Suppress header on BYE from raw name.
254    pub fn no_bye_raw(name: impl Into<String>) -> Result<Self, InvalidHeaderName> {
255        Self::new_raw(SipHeaderPrefix::NoBye, name)
256    }
257
258    /// The prefix (direction/method) of this variable.
259    pub fn prefix(&self) -> SipHeaderPrefix {
260        self.prefix
261    }
262
263    /// The canonical SIP header name (e.g. `"Call-Info"`, `"X-Tenant"`).
264    pub fn canonical_name(&self) -> &str {
265        &self.canonical_name
266    }
267
268    /// The pre-computed wire variable name (e.g. `"sip_h_Call-Info"`).
269    pub fn as_str(&self) -> &str {
270        &self.wire
271    }
272
273    /// Extract this header's values from a raw SIP message.
274    ///
275    /// Returns each occurrence as a separate entry (RFC 3261 §7.3.1).
276    /// Delegates to [`sip_header::extract_header()`] using the canonical name.
277    pub fn extract_from(&self, message: &str) -> Vec<String> {
278        sip_header::extract_header(message, &self.canonical_name)
279    }
280
281    /// Whether this header may contain ARRAY-encoded values when read from FreeSWITCH.
282    ///
283    /// Only meaningful for the `Invite` prefix — FreeSWITCH stores multi-valued
284    /// incoming SIP headers in `ARRAY::val1|:val2` format. For other prefixes
285    /// (which are write-only), this always returns `false`.
286    pub fn is_array_header(&self) -> bool {
287        if self.prefix != SipHeaderPrefix::Invite {
288            return false;
289        }
290        self.canonical_name
291            .parse::<SipHeader>()
292            .map(|h| h.is_multi_valued())
293            .unwrap_or(false)
294    }
295}
296
297impl super::VariableName for SipPassthroughHeader {
298    fn as_str(&self) -> &str {
299        &self.wire
300    }
301}
302
303impl std::fmt::Display for SipPassthroughHeader {
304    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305        f.write_str(&self.wire)
306    }
307}
308
309impl AsRef<str> for SipPassthroughHeader {
310    fn as_ref(&self) -> &str {
311        &self.wire
312    }
313}
314
315impl From<SipPassthroughHeader> for String {
316    fn from(h: SipPassthroughHeader) -> Self {
317        h.wire
318    }
319}
320
321impl std::str::FromStr for SipPassthroughHeader {
322    type Err = ParseSipPassthroughError;
323
324    fn from_str(s: &str) -> Result<Self, Self::Err> {
325        for &(prefix, pat) in PREFIX_PATTERNS {
326            if let Some(suffix) = s.strip_prefix(pat) {
327                if suffix.is_empty() {
328                    return Err(ParseSipPassthroughError(s.to_string()));
329                }
330
331                let canonical = match prefix {
332                    SipHeaderPrefix::Invite => {
333                        // Reverse the lowercase+underscore transformation:
334                        // "call_info" → "call-info" → try SipHeader::from_str
335                        let with_hyphens = suffix.replace('_', "-");
336                        match with_hyphens.parse::<SipHeader>() {
337                            Ok(h) => h
338                                .as_str()
339                                .to_string(),
340                            Err(_) => with_hyphens,
341                        }
342                    }
343                    _ => match suffix.parse::<SipHeader>() {
344                        Ok(h) => h
345                            .as_str()
346                            .to_string(),
347                        Err(_) => suffix.to_string(),
348                    },
349                };
350
351                return Ok(Self {
352                    prefix,
353                    canonical_name: canonical,
354                    wire: s.to_string(),
355                });
356            }
357        }
358
359        // Case-insensitive prefix matching for sip_i_ (FreeSWITCH may
360        // uppercase in some contexts)
361        let lower = s.to_ascii_lowercase();
362        if lower != s {
363            for &(prefix, pat) in PREFIX_PATTERNS {
364                if let Some(suffix) = lower.strip_prefix(pat) {
365                    if suffix.is_empty() {
366                        return Err(ParseSipPassthroughError(s.to_string()));
367                    }
368                    let canonical = match prefix {
369                        SipHeaderPrefix::Invite => {
370                            let with_hyphens = suffix.replace('_', "-");
371                            match with_hyphens.parse::<SipHeader>() {
372                                Ok(h) => h
373                                    .as_str()
374                                    .to_string(),
375                                Err(_) => with_hyphens,
376                            }
377                        }
378                        _ => match suffix.parse::<SipHeader>() {
379                            Ok(h) => h
380                                .as_str()
381                                .to_string(),
382                            Err(_) => suffix.to_string(),
383                        },
384                    };
385                    let wire = build_wire(prefix, &canonical);
386                    return Ok(Self {
387                        prefix,
388                        canonical_name: canonical,
389                        wire,
390                    });
391                }
392            }
393        }
394
395        Err(ParseSipPassthroughError(s.to_string()))
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402
403    // --- Display / wire format ---
404
405    #[test]
406    fn invite_typed_wire_format() {
407        assert_eq!(
408            SipPassthroughHeader::invite(SipHeader::CallInfo).as_str(),
409            "sip_i_call_info"
410        );
411        assert_eq!(
412            SipPassthroughHeader::invite(SipHeader::PAssertedIdentity).as_str(),
413            "sip_i_p_asserted_identity"
414        );
415        assert_eq!(
416            SipPassthroughHeader::invite(SipHeader::Via).as_str(),
417            "sip_i_via"
418        );
419        assert_eq!(
420            SipPassthroughHeader::invite(SipHeader::CallId).as_str(),
421            "sip_i_call_id"
422        );
423    }
424
425    #[test]
426    fn request_typed_wire_format() {
427        assert_eq!(
428            SipPassthroughHeader::request(SipHeader::CallInfo).as_str(),
429            "sip_h_Call-Info"
430        );
431        assert_eq!(
432            SipPassthroughHeader::request(SipHeader::PAssertedIdentity).as_str(),
433            "sip_h_P-Asserted-Identity"
434        );
435    }
436
437    #[test]
438    fn response_typed_wire_format() {
439        assert_eq!(
440            SipPassthroughHeader::response(SipHeader::CallInfo).as_str(),
441            "sip_rh_Call-Info"
442        );
443    }
444
445    #[test]
446    fn provisional_typed_wire_format() {
447        assert_eq!(
448            SipPassthroughHeader::provisional(SipHeader::AlertInfo).as_str(),
449            "sip_ph_Alert-Info"
450        );
451    }
452
453    #[test]
454    fn bye_typed_wire_format() {
455        assert_eq!(
456            SipPassthroughHeader::bye(SipHeader::Reason).as_str(),
457            "sip_bye_h_Reason"
458        );
459    }
460
461    #[test]
462    fn no_bye_typed_wire_format() {
463        assert_eq!(
464            SipPassthroughHeader::no_bye(SipHeader::Reason).as_str(),
465            "sip_nobye_h_Reason"
466        );
467    }
468
469    #[test]
470    fn raw_custom_header() {
471        assert_eq!(
472            SipPassthroughHeader::request_raw("X-Tenant")
473                .unwrap()
474                .as_str(),
475            "sip_h_X-Tenant"
476        );
477        assert_eq!(
478            SipPassthroughHeader::invite_raw("X-Custom")
479                .unwrap()
480                .as_str(),
481            "sip_i_x_custom"
482        );
483    }
484
485    #[test]
486    fn raw_rejects_newlines() {
487        assert!(SipPassthroughHeader::request_raw("X-Bad\nHeader").is_err());
488        assert!(SipPassthroughHeader::request_raw("X-Bad\rHeader").is_err());
489        assert!(SipPassthroughHeader::request_raw("").is_err());
490    }
491
492    // --- Display trait ---
493
494    #[test]
495    fn display_matches_as_str() {
496        let h = SipPassthroughHeader::request(SipHeader::CallInfo);
497        assert_eq!(h.to_string(), h.as_str());
498    }
499
500    // --- FromStr round-trips ---
501
502    #[test]
503    fn from_str_request() {
504        let parsed: SipPassthroughHeader = "sip_h_Call-Info"
505            .parse()
506            .unwrap();
507        assert_eq!(parsed.prefix(), SipHeaderPrefix::Request);
508        assert_eq!(parsed.canonical_name(), "Call-Info");
509        assert_eq!(parsed.as_str(), "sip_h_Call-Info");
510    }
511
512    #[test]
513    fn from_str_invite() {
514        let parsed: SipPassthroughHeader = "sip_i_call_info"
515            .parse()
516            .unwrap();
517        assert_eq!(parsed.prefix(), SipHeaderPrefix::Invite);
518        assert_eq!(parsed.canonical_name(), "Call-Info");
519        assert_eq!(parsed.as_str(), "sip_i_call_info");
520    }
521
522    #[test]
523    fn from_str_invite_p_asserted_identity() {
524        let parsed: SipPassthroughHeader = "sip_i_p_asserted_identity"
525            .parse()
526            .unwrap();
527        assert_eq!(parsed.prefix(), SipHeaderPrefix::Invite);
528        assert_eq!(parsed.canonical_name(), "P-Asserted-Identity");
529    }
530
531    #[test]
532    fn from_str_response() {
533        let parsed: SipPassthroughHeader = "sip_rh_Call-Info"
534            .parse()
535            .unwrap();
536        assert_eq!(parsed.prefix(), SipHeaderPrefix::Response);
537        assert_eq!(parsed.canonical_name(), "Call-Info");
538    }
539
540    #[test]
541    fn from_str_bye() {
542        let parsed: SipPassthroughHeader = "sip_bye_h_Reason"
543            .parse()
544            .unwrap();
545        assert_eq!(parsed.prefix(), SipHeaderPrefix::Bye);
546        assert_eq!(parsed.canonical_name(), "Reason");
547    }
548
549    #[test]
550    fn from_str_no_bye() {
551        let parsed: SipPassthroughHeader = "sip_nobye_h_Reason"
552            .parse()
553            .unwrap();
554        assert_eq!(parsed.prefix(), SipHeaderPrefix::NoBye);
555        assert_eq!(parsed.canonical_name(), "Reason");
556    }
557
558    #[test]
559    fn from_str_unknown_custom_header() {
560        let parsed: SipPassthroughHeader = "sip_h_X-Tenant"
561            .parse()
562            .unwrap();
563        assert_eq!(parsed.prefix(), SipHeaderPrefix::Request);
564        assert_eq!(parsed.canonical_name(), "X-Tenant");
565    }
566
567    #[test]
568    fn from_str_invite_unknown_custom() {
569        let parsed: SipPassthroughHeader = "sip_i_x_custom"
570            .parse()
571            .unwrap();
572        assert_eq!(parsed.prefix(), SipHeaderPrefix::Invite);
573        // Unknown header: canonical is the hyphenated form
574        assert_eq!(parsed.canonical_name(), "x-custom");
575    }
576
577    #[test]
578    fn from_str_case_insensitive_invite() {
579        let parsed: SipPassthroughHeader = "SIP_I_CALL_INFO"
580            .parse()
581            .unwrap();
582        assert_eq!(parsed.prefix(), SipHeaderPrefix::Invite);
583        assert_eq!(parsed.canonical_name(), "Call-Info");
584    }
585
586    #[test]
587    fn from_str_rejects_no_prefix() {
588        assert!("call_info"
589            .parse::<SipPassthroughHeader>()
590            .is_err());
591        assert!("sip_call_info"
592            .parse::<SipPassthroughHeader>()
593            .is_err());
594    }
595
596    #[test]
597    fn from_str_rejects_empty_suffix() {
598        assert!("sip_h_"
599            .parse::<SipPassthroughHeader>()
600            .is_err());
601        assert!("sip_i_"
602            .parse::<SipPassthroughHeader>()
603            .is_err());
604    }
605
606    #[test]
607    fn from_str_round_trip_all_prefixes() {
608        let headers = [
609            SipPassthroughHeader::invite(SipHeader::CallInfo),
610            SipPassthroughHeader::request(SipHeader::CallInfo),
611            SipPassthroughHeader::response(SipHeader::CallInfo),
612            SipPassthroughHeader::provisional(SipHeader::AlertInfo),
613            SipPassthroughHeader::bye(SipHeader::Reason),
614            SipPassthroughHeader::no_bye(SipHeader::Reason),
615        ];
616        for h in &headers {
617            let parsed: SipPassthroughHeader = h
618                .as_str()
619                .parse()
620                .unwrap();
621            assert_eq!(&parsed, h, "round-trip failed for {}", h.as_str());
622        }
623    }
624
625    // --- Accessors ---
626
627    #[test]
628    fn prefix_accessor() {
629        assert_eq!(
630            SipPassthroughHeader::invite(SipHeader::Via).prefix(),
631            SipHeaderPrefix::Invite
632        );
633        assert_eq!(
634            SipPassthroughHeader::request(SipHeader::Via).prefix(),
635            SipHeaderPrefix::Request
636        );
637    }
638
639    #[test]
640    fn canonical_name_accessor() {
641        assert_eq!(
642            SipPassthroughHeader::invite(SipHeader::CallInfo).canonical_name(),
643            "Call-Info"
644        );
645        assert_eq!(
646            SipPassthroughHeader::request_raw("X-Tenant")
647                .unwrap()
648                .canonical_name(),
649            "X-Tenant"
650        );
651    }
652
653    // --- is_array_header ---
654
655    #[test]
656    fn is_array_header_invite_multi_valued() {
657        assert!(SipPassthroughHeader::invite(SipHeader::Via).is_array_header());
658        assert!(SipPassthroughHeader::invite(SipHeader::CallInfo).is_array_header());
659        assert!(SipPassthroughHeader::invite(SipHeader::PAssertedIdentity).is_array_header());
660        assert!(SipPassthroughHeader::invite(SipHeader::RecordRoute).is_array_header());
661    }
662
663    #[test]
664    fn is_array_header_invite_single_valued() {
665        assert!(!SipPassthroughHeader::invite(SipHeader::From).is_array_header());
666        assert!(!SipPassthroughHeader::invite(SipHeader::CallId).is_array_header());
667        assert!(!SipPassthroughHeader::invite(SipHeader::ContentType).is_array_header());
668    }
669
670    #[test]
671    fn is_array_header_non_invite_always_false() {
672        assert!(!SipPassthroughHeader::request(SipHeader::Via).is_array_header());
673        assert!(!SipPassthroughHeader::response(SipHeader::CallInfo).is_array_header());
674    }
675
676    #[test]
677    fn is_array_header_raw_unknown() {
678        assert!(!SipPassthroughHeader::invite_raw("X-Custom")
679            .unwrap()
680            .is_array_header());
681    }
682
683    // --- extract_from ---
684
685    #[test]
686    fn extract_from_sip_message() {
687        let msg = "INVITE sip:bob@example.com SIP/2.0\r\n\
688                   Call-Info: <sip:example.com>;answer-after=0\r\n\
689                   \r\n";
690        let h = SipPassthroughHeader::invite(SipHeader::CallInfo);
691        assert_eq!(
692            h.extract_from(msg),
693            vec!["<sip:example.com>;answer-after=0"]
694        );
695    }
696
697    #[test]
698    fn extract_from_missing() {
699        let msg = "INVITE sip:bob@example.com SIP/2.0\r\n\
700                   From: Alice <sip:alice@example.com>\r\n\
701                   \r\n";
702        let h = SipPassthroughHeader::invite(SipHeader::CallInfo);
703        assert!(h
704            .extract_from(msg)
705            .is_empty());
706    }
707
708    // --- VariableName trait ---
709
710    #[test]
711    fn variable_name_trait() {
712        use crate::variables::VariableName;
713        let h = SipPassthroughHeader::request(SipHeader::CallInfo);
714        let name: &str = VariableName::as_str(&h);
715        assert_eq!(name, "sip_h_Call-Info");
716    }
717}