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 value from a raw SIP message.
274    ///
275    /// Delegates to [`sip_header::extract_header()`] using the canonical name.
276    pub fn extract_from(&self, message: &str) -> Option<String> {
277        sip_header::extract_header(message, &self.canonical_name)
278    }
279
280    /// Whether this header may contain ARRAY-encoded values when read from FreeSWITCH.
281    ///
282    /// Only meaningful for the `Invite` prefix — FreeSWITCH stores multi-valued
283    /// incoming SIP headers in `ARRAY::val1|:val2` format. For other prefixes
284    /// (which are write-only), this always returns `false`.
285    pub fn is_array_header(&self) -> bool {
286        if self.prefix != SipHeaderPrefix::Invite {
287            return false;
288        }
289        self.canonical_name
290            .parse::<SipHeader>()
291            .map(|h| h.is_multi_valued())
292            .unwrap_or(false)
293    }
294}
295
296impl super::VariableName for SipPassthroughHeader {
297    fn as_str(&self) -> &str {
298        &self.wire
299    }
300}
301
302impl std::fmt::Display for SipPassthroughHeader {
303    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304        f.write_str(&self.wire)
305    }
306}
307
308impl AsRef<str> for SipPassthroughHeader {
309    fn as_ref(&self) -> &str {
310        &self.wire
311    }
312}
313
314impl From<SipPassthroughHeader> for String {
315    fn from(h: SipPassthroughHeader) -> Self {
316        h.wire
317    }
318}
319
320impl std::str::FromStr for SipPassthroughHeader {
321    type Err = ParseSipPassthroughError;
322
323    fn from_str(s: &str) -> Result<Self, Self::Err> {
324        for &(prefix, pat) in PREFIX_PATTERNS {
325            if let Some(suffix) = s.strip_prefix(pat) {
326                if suffix.is_empty() {
327                    return Err(ParseSipPassthroughError(s.to_string()));
328                }
329
330                let canonical = match prefix {
331                    SipHeaderPrefix::Invite => {
332                        // Reverse the lowercase+underscore transformation:
333                        // "call_info" → "call-info" → try SipHeader::from_str
334                        let with_hyphens = suffix.replace('_', "-");
335                        match with_hyphens.parse::<SipHeader>() {
336                            Ok(h) => h
337                                .as_str()
338                                .to_string(),
339                            Err(_) => with_hyphens,
340                        }
341                    }
342                    _ => match suffix.parse::<SipHeader>() {
343                        Ok(h) => h
344                            .as_str()
345                            .to_string(),
346                        Err(_) => suffix.to_string(),
347                    },
348                };
349
350                return Ok(Self {
351                    prefix,
352                    canonical_name: canonical,
353                    wire: s.to_string(),
354                });
355            }
356        }
357
358        // Case-insensitive prefix matching for sip_i_ (FreeSWITCH may
359        // uppercase in some contexts)
360        let lower = s.to_ascii_lowercase();
361        if lower != s {
362            for &(prefix, pat) in PREFIX_PATTERNS {
363                if let Some(suffix) = lower.strip_prefix(pat) {
364                    if suffix.is_empty() {
365                        return Err(ParseSipPassthroughError(s.to_string()));
366                    }
367                    let canonical = match prefix {
368                        SipHeaderPrefix::Invite => {
369                            let with_hyphens = suffix.replace('_', "-");
370                            match with_hyphens.parse::<SipHeader>() {
371                                Ok(h) => h
372                                    .as_str()
373                                    .to_string(),
374                                Err(_) => with_hyphens,
375                            }
376                        }
377                        _ => match suffix.parse::<SipHeader>() {
378                            Ok(h) => h
379                                .as_str()
380                                .to_string(),
381                            Err(_) => suffix.to_string(),
382                        },
383                    };
384                    let wire = build_wire(prefix, &canonical);
385                    return Ok(Self {
386                        prefix,
387                        canonical_name: canonical,
388                        wire,
389                    });
390                }
391            }
392        }
393
394        Err(ParseSipPassthroughError(s.to_string()))
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401
402    // --- Display / wire format ---
403
404    #[test]
405    fn invite_typed_wire_format() {
406        assert_eq!(
407            SipPassthroughHeader::invite(SipHeader::CallInfo).as_str(),
408            "sip_i_call_info"
409        );
410        assert_eq!(
411            SipPassthroughHeader::invite(SipHeader::PAssertedIdentity).as_str(),
412            "sip_i_p_asserted_identity"
413        );
414        assert_eq!(
415            SipPassthroughHeader::invite(SipHeader::Via).as_str(),
416            "sip_i_via"
417        );
418        assert_eq!(
419            SipPassthroughHeader::invite(SipHeader::CallId).as_str(),
420            "sip_i_call_id"
421        );
422    }
423
424    #[test]
425    fn request_typed_wire_format() {
426        assert_eq!(
427            SipPassthroughHeader::request(SipHeader::CallInfo).as_str(),
428            "sip_h_Call-Info"
429        );
430        assert_eq!(
431            SipPassthroughHeader::request(SipHeader::PAssertedIdentity).as_str(),
432            "sip_h_P-Asserted-Identity"
433        );
434    }
435
436    #[test]
437    fn response_typed_wire_format() {
438        assert_eq!(
439            SipPassthroughHeader::response(SipHeader::CallInfo).as_str(),
440            "sip_rh_Call-Info"
441        );
442    }
443
444    #[test]
445    fn provisional_typed_wire_format() {
446        assert_eq!(
447            SipPassthroughHeader::provisional(SipHeader::AlertInfo).as_str(),
448            "sip_ph_Alert-Info"
449        );
450    }
451
452    #[test]
453    fn bye_typed_wire_format() {
454        assert_eq!(
455            SipPassthroughHeader::bye(SipHeader::Reason).as_str(),
456            "sip_bye_h_Reason"
457        );
458    }
459
460    #[test]
461    fn no_bye_typed_wire_format() {
462        assert_eq!(
463            SipPassthroughHeader::no_bye(SipHeader::Reason).as_str(),
464            "sip_nobye_h_Reason"
465        );
466    }
467
468    #[test]
469    fn raw_custom_header() {
470        assert_eq!(
471            SipPassthroughHeader::request_raw("X-Tenant")
472                .unwrap()
473                .as_str(),
474            "sip_h_X-Tenant"
475        );
476        assert_eq!(
477            SipPassthroughHeader::invite_raw("X-Custom")
478                .unwrap()
479                .as_str(),
480            "sip_i_x_custom"
481        );
482    }
483
484    #[test]
485    fn raw_rejects_newlines() {
486        assert!(SipPassthroughHeader::request_raw("X-Bad\nHeader").is_err());
487        assert!(SipPassthroughHeader::request_raw("X-Bad\rHeader").is_err());
488        assert!(SipPassthroughHeader::request_raw("").is_err());
489    }
490
491    // --- Display trait ---
492
493    #[test]
494    fn display_matches_as_str() {
495        let h = SipPassthroughHeader::request(SipHeader::CallInfo);
496        assert_eq!(h.to_string(), h.as_str());
497    }
498
499    // --- FromStr round-trips ---
500
501    #[test]
502    fn from_str_request() {
503        let parsed: SipPassthroughHeader = "sip_h_Call-Info"
504            .parse()
505            .unwrap();
506        assert_eq!(parsed.prefix(), SipHeaderPrefix::Request);
507        assert_eq!(parsed.canonical_name(), "Call-Info");
508        assert_eq!(parsed.as_str(), "sip_h_Call-Info");
509    }
510
511    #[test]
512    fn from_str_invite() {
513        let parsed: SipPassthroughHeader = "sip_i_call_info"
514            .parse()
515            .unwrap();
516        assert_eq!(parsed.prefix(), SipHeaderPrefix::Invite);
517        assert_eq!(parsed.canonical_name(), "Call-Info");
518        assert_eq!(parsed.as_str(), "sip_i_call_info");
519    }
520
521    #[test]
522    fn from_str_invite_p_asserted_identity() {
523        let parsed: SipPassthroughHeader = "sip_i_p_asserted_identity"
524            .parse()
525            .unwrap();
526        assert_eq!(parsed.prefix(), SipHeaderPrefix::Invite);
527        assert_eq!(parsed.canonical_name(), "P-Asserted-Identity");
528    }
529
530    #[test]
531    fn from_str_response() {
532        let parsed: SipPassthroughHeader = "sip_rh_Call-Info"
533            .parse()
534            .unwrap();
535        assert_eq!(parsed.prefix(), SipHeaderPrefix::Response);
536        assert_eq!(parsed.canonical_name(), "Call-Info");
537    }
538
539    #[test]
540    fn from_str_bye() {
541        let parsed: SipPassthroughHeader = "sip_bye_h_Reason"
542            .parse()
543            .unwrap();
544        assert_eq!(parsed.prefix(), SipHeaderPrefix::Bye);
545        assert_eq!(parsed.canonical_name(), "Reason");
546    }
547
548    #[test]
549    fn from_str_no_bye() {
550        let parsed: SipPassthroughHeader = "sip_nobye_h_Reason"
551            .parse()
552            .unwrap();
553        assert_eq!(parsed.prefix(), SipHeaderPrefix::NoBye);
554        assert_eq!(parsed.canonical_name(), "Reason");
555    }
556
557    #[test]
558    fn from_str_unknown_custom_header() {
559        let parsed: SipPassthroughHeader = "sip_h_X-Tenant"
560            .parse()
561            .unwrap();
562        assert_eq!(parsed.prefix(), SipHeaderPrefix::Request);
563        assert_eq!(parsed.canonical_name(), "X-Tenant");
564    }
565
566    #[test]
567    fn from_str_invite_unknown_custom() {
568        let parsed: SipPassthroughHeader = "sip_i_x_custom"
569            .parse()
570            .unwrap();
571        assert_eq!(parsed.prefix(), SipHeaderPrefix::Invite);
572        // Unknown header: canonical is the hyphenated form
573        assert_eq!(parsed.canonical_name(), "x-custom");
574    }
575
576    #[test]
577    fn from_str_case_insensitive_invite() {
578        let parsed: SipPassthroughHeader = "SIP_I_CALL_INFO"
579            .parse()
580            .unwrap();
581        assert_eq!(parsed.prefix(), SipHeaderPrefix::Invite);
582        assert_eq!(parsed.canonical_name(), "Call-Info");
583    }
584
585    #[test]
586    fn from_str_rejects_no_prefix() {
587        assert!("call_info"
588            .parse::<SipPassthroughHeader>()
589            .is_err());
590        assert!("sip_call_info"
591            .parse::<SipPassthroughHeader>()
592            .is_err());
593    }
594
595    #[test]
596    fn from_str_rejects_empty_suffix() {
597        assert!("sip_h_"
598            .parse::<SipPassthroughHeader>()
599            .is_err());
600        assert!("sip_i_"
601            .parse::<SipPassthroughHeader>()
602            .is_err());
603    }
604
605    #[test]
606    fn from_str_round_trip_all_prefixes() {
607        let headers = [
608            SipPassthroughHeader::invite(SipHeader::CallInfo),
609            SipPassthroughHeader::request(SipHeader::CallInfo),
610            SipPassthroughHeader::response(SipHeader::CallInfo),
611            SipPassthroughHeader::provisional(SipHeader::AlertInfo),
612            SipPassthroughHeader::bye(SipHeader::Reason),
613            SipPassthroughHeader::no_bye(SipHeader::Reason),
614        ];
615        for h in &headers {
616            let parsed: SipPassthroughHeader = h
617                .as_str()
618                .parse()
619                .unwrap();
620            assert_eq!(&parsed, h, "round-trip failed for {}", h.as_str());
621        }
622    }
623
624    // --- Accessors ---
625
626    #[test]
627    fn prefix_accessor() {
628        assert_eq!(
629            SipPassthroughHeader::invite(SipHeader::Via).prefix(),
630            SipHeaderPrefix::Invite
631        );
632        assert_eq!(
633            SipPassthroughHeader::request(SipHeader::Via).prefix(),
634            SipHeaderPrefix::Request
635        );
636    }
637
638    #[test]
639    fn canonical_name_accessor() {
640        assert_eq!(
641            SipPassthroughHeader::invite(SipHeader::CallInfo).canonical_name(),
642            "Call-Info"
643        );
644        assert_eq!(
645            SipPassthroughHeader::request_raw("X-Tenant")
646                .unwrap()
647                .canonical_name(),
648            "X-Tenant"
649        );
650    }
651
652    // --- is_array_header ---
653
654    #[test]
655    fn is_array_header_invite_multi_valued() {
656        assert!(SipPassthroughHeader::invite(SipHeader::Via).is_array_header());
657        assert!(SipPassthroughHeader::invite(SipHeader::CallInfo).is_array_header());
658        assert!(SipPassthroughHeader::invite(SipHeader::PAssertedIdentity).is_array_header());
659        assert!(SipPassthroughHeader::invite(SipHeader::RecordRoute).is_array_header());
660    }
661
662    #[test]
663    fn is_array_header_invite_single_valued() {
664        assert!(!SipPassthroughHeader::invite(SipHeader::From).is_array_header());
665        assert!(!SipPassthroughHeader::invite(SipHeader::CallId).is_array_header());
666        assert!(!SipPassthroughHeader::invite(SipHeader::ContentType).is_array_header());
667    }
668
669    #[test]
670    fn is_array_header_non_invite_always_false() {
671        assert!(!SipPassthroughHeader::request(SipHeader::Via).is_array_header());
672        assert!(!SipPassthroughHeader::response(SipHeader::CallInfo).is_array_header());
673    }
674
675    #[test]
676    fn is_array_header_raw_unknown() {
677        assert!(!SipPassthroughHeader::invite_raw("X-Custom")
678            .unwrap()
679            .is_array_header());
680    }
681
682    // --- extract_from ---
683
684    #[test]
685    fn extract_from_sip_message() {
686        let msg = "INVITE sip:bob@example.com SIP/2.0\r\n\
687                   Call-Info: <sip:example.com>;answer-after=0\r\n\
688                   \r\n";
689        let h = SipPassthroughHeader::invite(SipHeader::CallInfo);
690        assert_eq!(
691            h.extract_from(msg),
692            Some("<sip:example.com>;answer-after=0".into())
693        );
694    }
695
696    #[test]
697    fn extract_from_missing() {
698        let msg = "INVITE sip:bob@example.com SIP/2.0\r\n\
699                   From: Alice <sip:alice@example.com>\r\n\
700                   \r\n";
701        let h = SipPassthroughHeader::invite(SipHeader::CallInfo);
702        assert_eq!(h.extract_from(msg), None);
703    }
704
705    // --- VariableName trait ---
706
707    #[test]
708    fn variable_name_trait() {
709        use crate::variables::VariableName;
710        let h = SipPassthroughHeader::request(SipHeader::CallInfo);
711        let name: &str = VariableName::as_str(&h);
712        assert_eq!(name, "sip_h_Call-Info");
713    }
714}