azeventhubs/
event_hubs_connection_string_properties.rs

1//! The set of properties that comprise a Event Hubs connection string.
2
3use azure_core::Url;
4
5/// Error with parsing the connection string.
6#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
7pub enum FormatError {
8    /// Connection string cannot be empty
9    #[error("Connection string cannot be empty")]
10    ConnectionStringIsEmpty,
11
12    /// Connection string is malformed
13    #[error("Connection string is malformed")]
14    InvalidConnectionString,
15}
16
17impl From<FormatError> for azure_core::Error {
18    fn from(err: FormatError) -> Self {
19        use azure_core::error::ErrorKind;
20
21        azure_core::Error::new(ErrorKind::Other, err)
22    }
23}
24
25/// Error with outputting the connection string.
26#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
27pub enum ToConnectionStringError {
28    /// Missing connection information
29    #[error("Missing connection information")]
30    MissingConnectionInformation,
31
32    /// Invalid endpoint address
33    #[error("Invalid endpoint address")]
34    InvalidEndpointAddress,
35
36    /// Only one of the shared access authorization tokens may be used
37    #[error("Only one shared access authorization can be used")]
38    OnlyOneSharedAccessAuthorizationMayBeUsed,
39}
40
41impl From<ToConnectionStringError> for azure_core::Error {
42    fn from(err: ToConnectionStringError) -> Self {
43        use azure_core::error::ErrorKind;
44
45        azure_core::Error::new(ErrorKind::Other, err)
46    }
47}
48
49/// The set of properties that comprise a Event Hubs connection string.
50#[derive(Debug, PartialEq, Eq, Hash)]
51pub struct EventHubsConnectionStringProperties<'a> {
52    pub(crate) endpoint: Option<url::Url>,
53    pub(crate) event_hub_name: Option<&'a str>,
54    pub(crate) shared_access_key_name: Option<&'a str>,
55    pub(crate) shared_access_key: Option<&'a str>,
56    pub(crate) shared_access_signature: Option<&'a str>,
57}
58
59impl<'a> EventHubsConnectionStringProperties<'a> {
60    /// The character used to separate a token and its value in the connection string.
61    const TOKEN_VALUE_SEPARATOR: char = '=';
62
63    /// The character used to mark the beginning of a new token/value pair in the connection string.
64    const TOKEN_VALUE_PAIR_DELIMITER: char = ';';
65
66    /// The name of the protocol used by an Event Hubs endpoint.
67    const SERVICE_BUS_ENDPOINT_SCHEME_NAME: &'static str = "sb";
68
69    /// The token that identifies the endpoint address for the Event Hubs namespace.
70    const ENDPOINT_TOKEN: &'static str = "Endpoint";
71
72    /// The token that identifies the name of a specific Event Hubs entity under the namespace.
73    const EVENT_HUB_NAME_TOKEN: &'static str = "EntityPath";
74
75    /// The token that identifies the name of a shared access key.
76    const SHARED_ACCESS_KEY_NAME_TOKEN: &'static str = "SharedAccessKeyName";
77
78    /// The token that identifies the value of a shared access key.
79    const SHARED_ACCESS_KEY_VALUE_TOKEN: &'static str = "SharedAccessKey";
80
81    /// The token that identifies the value of a shared access signature.
82    const SHARED_ACCESS_SIGNATURE_TOKEN: &'static str = "SharedAccessSignature";
83
84    /// The fully qualified Event Hubs namespace that the consumer is associated with.  This is
85    /// likely to be similar to `"{yournamespace}.servicebus.windows.net"`.
86    pub fn fully_qualified_namespace(&self) -> Option<&str> {
87        self.endpoint.as_ref().and_then(|url| url.host_str())
88    }
89
90    /// The endpoint to be used for connecting to the Event Hubs namespace.
91    pub fn endpoint(&self) -> Option<&Url> {
92        self.endpoint.as_ref()
93    }
94
95    /// The name of the specific Event Hubs entity instance under the associated Event Hubs
96    /// namespace.
97    pub fn event_hub_name(&self) -> Option<&str> {
98        self.event_hub_name
99    }
100
101    /// The name of the shared access key, either for the Event Hubs namespace or the Event Hubs
102    /// entity.
103    pub fn shared_access_key_name(&self) -> Option<&str> {
104        self.shared_access_key_name
105    }
106
107    /// The value of the shared access key, either for the Event Hubs namespace or the Event Hubs
108    /// entity.
109    pub fn shared_access_key(&self) -> Option<&str> {
110        self.shared_access_key
111    }
112
113    /// The value of the fully-formed shared access signature, either for the Event Hubs namespace
114    /// or the Event Hubs entity.
115    pub fn shared_access_signature(&self) -> Option<&str> {
116        self.shared_access_signature
117    }
118
119    /// Creates an Event Hubs connection string based on this set of
120    /// [`EventHubsConnectionStringProperties`].
121    pub fn to_connection_string(&self) -> Result<String, ToConnectionStringError> {
122        let mut s = String::new();
123
124        if let Some(endpoint) = self.endpoint() {
125            if endpoint.scheme() != Self::SERVICE_BUS_ENDPOINT_SCHEME_NAME {
126                // TODO: checking host name is unnecessary? `url::Url` cannot be built with invalid host name?.
127                return Err(ToConnectionStringError::InvalidEndpointAddress);
128            }
129
130            s.push_str(Self::ENDPOINT_TOKEN);
131            s.push(Self::TOKEN_VALUE_SEPARATOR);
132            s.push_str(endpoint.as_str());
133            s.push(Self::TOKEN_VALUE_PAIR_DELIMITER);
134        } else {
135            return Err(ToConnectionStringError::MissingConnectionInformation);
136        }
137
138        if let Some(event_hub_name) = self.event_hub_name.and_then(|s| match !s.is_empty() {
139            true => Some(s),
140            false => None,
141        }) {
142            s.push_str(Self::EVENT_HUB_NAME_TOKEN);
143            s.push(Self::TOKEN_VALUE_SEPARATOR);
144            s.push_str(event_hub_name);
145            s.push(Self::TOKEN_VALUE_PAIR_DELIMITER);
146        }
147
148        // The connection string may contain a precomputed shared access signature OR a shared key name and value,
149        // but not both.
150        match (
151            self.shared_access_signature,
152            self.shared_access_key_name,
153            self.shared_access_key,
154        ) {
155            (Some(signature), None, None) => {
156                if !signature.is_empty() {
157                    s.push_str(Self::SHARED_ACCESS_SIGNATURE_TOKEN);
158                    s.push(Self::TOKEN_VALUE_SEPARATOR);
159                    s.push_str(signature);
160                    s.push(Self::TOKEN_VALUE_PAIR_DELIMITER);
161                }
162            }
163            (None, Some(key_name), Some(key)) => {
164                if (!key_name.is_empty()) && (!key.is_empty()) {
165                    s.push_str(Self::SHARED_ACCESS_KEY_NAME_TOKEN);
166                    s.push(Self::TOKEN_VALUE_SEPARATOR);
167                    s.push_str(key_name);
168                    s.push(Self::TOKEN_VALUE_PAIR_DELIMITER);
169
170                    s.push_str(Self::SHARED_ACCESS_KEY_VALUE_TOKEN);
171                    s.push(Self::TOKEN_VALUE_SEPARATOR);
172                    s.push_str(key);
173                    s.push(Self::TOKEN_VALUE_PAIR_DELIMITER);
174                }
175            }
176            _ => {
177                return Err(ToConnectionStringError::OnlyOneSharedAccessAuthorizationMayBeUsed);
178            }
179        }
180
181        Ok(s)
182    }
183
184    /// Parses the specified Event Hubs connection string into its component properties.
185    pub fn parse(connection_string: &'a str) -> Result<Self, FormatError> {
186        if connection_string.is_empty() {
187            return Err(FormatError::ConnectionStringIsEmpty);
188        }
189
190        let mut endpoint: Option<Url> = None;
191        let mut event_hub_name: Option<&'a str> = None;
192        let mut shared_access_key_name: Option<&'a str> = None;
193        let mut shared_access_key: Option<&'a str> = None;
194        let mut shared_access_signature: Option<&'a str> = None;
195
196        let token_value_pairs = connection_string.split(Self::TOKEN_VALUE_PAIR_DELIMITER);
197
198        for token_value_pair in token_value_pairs {
199            // Do not remove the separator if it is part of the value.
200            let mut split = token_value_pair.split_inclusive(Self::TOKEN_VALUE_SEPARATOR);
201            let token = match split
202                .next()
203                .and_then(|s| s.split(Self::TOKEN_VALUE_SEPARATOR).next())
204                .and_then(|s| match s.trim() {
205                    "" => None,
206                    s => Some(s),
207                }) {
208                Some(token) => token,
209                None => continue,
210            };
211
212            let value = split
213                .next()
214                .and_then(|s| match s.trim() {
215                    "" => None,
216                    s => Some(s),
217                })
218                // If there was no value for a key, then consider the connection string to
219                // be malformed.
220                .ok_or(FormatError::InvalidConnectionString)?;
221
222            // Compare the token against the known connection string properties and capture the
223            // pair if they are a known attribute.
224            match token {
225                Self::ENDPOINT_TOKEN => {
226                    // TODO: What about the port?
227                    let mut url =
228                        Url::parse(value).map_err(|_| FormatError::InvalidConnectionString)?;
229                    url.set_scheme(Self::SERVICE_BUS_ENDPOINT_SCHEME_NAME)
230                        .map_err(|_| FormatError::InvalidConnectionString)?;
231                    endpoint = Some(url);
232                }
233                Self::EVENT_HUB_NAME_TOKEN => event_hub_name = Some(value),
234                Self::SHARED_ACCESS_KEY_NAME_TOKEN => shared_access_key_name = Some(value),
235                Self::SHARED_ACCESS_KEY_VALUE_TOKEN => shared_access_key = Some(value),
236                Self::SHARED_ACCESS_SIGNATURE_TOKEN => shared_access_signature = Some(value),
237                _ => {}
238            }
239        }
240
241        Ok(Self {
242            endpoint,
243            event_hub_name,
244            shared_access_key_name,
245            shared_access_key,
246            shared_access_signature,
247        })
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::{EventHubsConnectionStringProperties, FormatError};
254
255    const ENDPOINT: &str = "test.endpoint.com";
256    const EVENT_HUB: &str = "some-path";
257    const SAS_KEY_NAME: &str = "sasName";
258    const SAS_KEY: &str = "sasKey";
259    const SAS: &str = "fullsas";
260
261    ///
262    struct Expected {
263        endpoint: Option<&'static str>,
264        event_hub: Option<&'static str>,
265        sas_key_name: Option<&'static str>,
266        sas_key: Option<&'static str>,
267        sas: Option<&'static str>,
268    }
269
270    macro_rules! assert_parsed_and_expected {
271        ($connection_string:ident, $expected:ident) => {
272            let parsed = EventHubsConnectionStringProperties::parse(&$connection_string).unwrap();
273
274            assert_eq!(
275                parsed.endpoint().and_then(|url| url.host_str()),
276                $expected.endpoint
277            );
278            assert_eq!(parsed.shared_access_key_name(), $expected.sas_key_name);
279            assert_eq!(parsed.shared_access_key(), $expected.sas_key);
280            assert_eq!(parsed.shared_access_signature(), $expected.sas);
281            assert_eq!(parsed.event_hub_name(), $expected.event_hub);
282        };
283    }
284
285    fn random_ordering_connection_string_cases() -> Vec<(String, Expected)> {
286        vec![
287            (
288                format!(
289                    "Endpoint=sb://{};SharedAccessKeyName={};SharedAccessKey={};EntityPath={}",
290                    ENDPOINT, SAS_KEY_NAME, SAS_KEY, EVENT_HUB
291                ),
292                Expected {
293                    endpoint: Some(ENDPOINT),
294                    event_hub: Some(EVENT_HUB),
295                    sas_key_name: Some(SAS_KEY_NAME),
296                    sas_key: Some(SAS_KEY),
297                    sas: None,
298                },
299            ),
300            (
301                format!(
302                    "Endpoint=sb://{};SharedAccessKey={};EntityPath={};SharedAccessKeyName={}",
303                    ENDPOINT, SAS_KEY, EVENT_HUB, SAS_KEY_NAME,
304                ),
305                Expected {
306                    endpoint: Some(ENDPOINT),
307                    event_hub: Some(EVENT_HUB),
308                    sas_key_name: Some(SAS_KEY_NAME),
309                    sas_key: Some(SAS_KEY),
310                    sas: None,
311                },
312            ),
313            (
314                format!(
315                    "Endpoint=sb://{};EntityPath={};SharedAccessKeyName={};SharedAccessKey={}",
316                    ENDPOINT, EVENT_HUB, SAS_KEY_NAME, SAS_KEY
317                ),
318                Expected {
319                    endpoint: Some(ENDPOINT),
320                    event_hub: Some(EVENT_HUB),
321                    sas_key_name: Some(SAS_KEY_NAME),
322                    sas_key: Some(SAS_KEY),
323                    sas: None,
324                },
325            ),
326            (
327                format!(
328                    "SharedAccessKeyName={};SharedAccessKey={};Endpoint=sb://{};EntityPath={}",
329                    SAS_KEY_NAME, SAS_KEY, ENDPOINT, EVENT_HUB
330                ),
331                Expected {
332                    endpoint: Some(ENDPOINT),
333                    event_hub: Some(EVENT_HUB),
334                    sas_key_name: Some(SAS_KEY_NAME),
335                    sas_key: Some(SAS_KEY),
336                    sas: None,
337                },
338            ),
339            (
340                format!(
341                    "EntityPath={};SharedAccessKey={};SharedAccessKeyName={};Endpoint=sb://{}",
342                    EVENT_HUB, SAS_KEY, SAS_KEY_NAME, ENDPOINT
343                ),
344                Expected {
345                    endpoint: Some(ENDPOINT),
346                    event_hub: Some(EVENT_HUB),
347                    sas_key_name: Some(SAS_KEY_NAME),
348                    sas_key: Some(SAS_KEY),
349                    sas: None,
350                },
351            ),
352            (
353                format!(
354                    "EntityPath={};SharedAccessSignature={};Endpoint=sb://{}",
355                    EVENT_HUB, SAS, ENDPOINT,
356                ),
357                Expected {
358                    endpoint: Some(ENDPOINT),
359                    event_hub: Some(EVENT_HUB),
360                    sas_key_name: None,
361                    sas_key: None,
362                    sas: Some(SAS),
363                },
364            ),
365            (
366                format!(
367                    "SharedAccessKeyName={};SharedAccessKey={};Endpoint=sb://{};EntityPath={};SharedAccessSignature={}",
368                    SAS_KEY_NAME, SAS_KEY, ENDPOINT, EVENT_HUB, SAS
369                ),
370                Expected {
371                    endpoint: Some(ENDPOINT),
372                    event_hub: Some(EVENT_HUB),
373                    sas_key_name: Some(SAS_KEY_NAME),
374                    sas_key: Some(SAS_KEY),
375                    sas: Some(SAS),
376                },
377            ),
378        ]
379    }
380
381    fn partial_connection_string_cases() -> Vec<(String, Expected)> {
382        vec![
383            (
384                format!("Endpoint=sb://{}", ENDPOINT),
385                Expected {
386                    endpoint: Some(ENDPOINT),
387                    event_hub: None,
388                    sas_key_name: None,
389                    sas_key: None,
390                    sas: None,
391                },
392            ),
393            (
394                format!("SharedAccessKey={}", SAS_KEY),
395                Expected {
396                    endpoint: None,
397                    event_hub: None,
398                    sas_key_name: None,
399                    sas_key: Some(SAS_KEY),
400                    sas: None,
401                },
402            ),
403            (
404                format!(
405                    "EntityPath={};SharedAccessKeyName={}",
406                    EVENT_HUB, SAS_KEY_NAME
407                ),
408                Expected {
409                    endpoint: None,
410                    event_hub: Some(EVENT_HUB),
411                    sas_key_name: Some(SAS_KEY_NAME),
412                    sas_key: None,
413                    sas: None,
414                },
415            ),
416            (
417                format!(
418                    "SharedAccessKeyName={};SharedAccessKey={}",
419                    SAS_KEY_NAME, SAS_KEY
420                ),
421                Expected {
422                    endpoint: None,
423                    event_hub: None,
424                    sas_key_name: Some(SAS_KEY_NAME),
425                    sas_key: Some(SAS_KEY),
426                    sas: None,
427                },
428            ),
429            (
430                format!(
431                    "EntityPath={};SharedAccessKey={};SharedAccessKeyName={}",
432                    EVENT_HUB, SAS_KEY, SAS_KEY_NAME
433                ),
434                Expected {
435                    endpoint: None,
436                    event_hub: Some(EVENT_HUB),
437                    sas_key_name: Some(SAS_KEY_NAME),
438                    sas_key: Some(SAS_KEY),
439                    sas: None,
440                },
441            ),
442            (
443                format!(
444                    "SharedAccessKeyName={};SharedAccessSignature={}",
445                    SAS_KEY_NAME, SAS
446                ),
447                Expected {
448                    endpoint: None,
449                    event_hub: None,
450                    sas_key_name: Some(SAS_KEY_NAME),
451                    sas_key: None,
452                    sas: Some(SAS),
453                },
454            ),
455            (
456                format!("EntityPath={};SharedAccessSignature={}", EVENT_HUB, SAS),
457                Expected {
458                    endpoint: None,
459                    event_hub: Some(EVENT_HUB),
460                    sas_key_name: None,
461                    sas_key: None,
462                    sas: Some(SAS),
463                },
464            ),
465            (
466                format!(
467                "EntityPath={};SharedAccessKey={};SharedAccessKeyName={};SharedAccessSignature={}",
468                EVENT_HUB, SAS_KEY, SAS_KEY_NAME, SAS
469            ),
470                Expected {
471                    endpoint: None,
472                    event_hub: Some(EVENT_HUB),
473                    sas_key_name: Some(SAS_KEY_NAME),
474                    sas_key: Some(SAS_KEY),
475                    sas: Some(SAS),
476                },
477            ),
478        ]
479    }
480
481    fn to_connection_string_validates_properties_cases(
482    ) -> Vec<EventHubsConnectionStringProperties<'static>> {
483        let mut cases = Vec::new();
484        // "missing endpoint"
485        let case = EventHubsConnectionStringProperties {
486            endpoint: None,
487            event_hub_name: Some("fake"),
488            shared_access_signature: Some("fake"),
489            shared_access_key_name: None,
490            shared_access_key: None,
491        };
492        cases.push(case);
493
494        // "missing authorization"
495        let case = EventHubsConnectionStringProperties {
496            endpoint: Some(url::Url::parse("sb://someplace.hosname.ext").unwrap()),
497            event_hub_name: Some("fake"),
498            shared_access_signature: None,
499            shared_access_key_name: None,
500            shared_access_key: None,
501        };
502        cases.push(case);
503
504        // "SAS and key specified"
505        let case = EventHubsConnectionStringProperties {
506            endpoint: Some(url::Url::parse("sb://someplace.hosname.ext").unwrap()),
507            event_hub_name: Some("fake"),
508            shared_access_signature: Some("fake"),
509            shared_access_key: Some("fake"),
510            shared_access_key_name: None,
511        };
512        cases.push(case);
513
514        // "SAS and shared key name specified"
515        let case = EventHubsConnectionStringProperties {
516            endpoint: Some(url::Url::parse("sb://someplace.hosname.ext").unwrap()),
517            event_hub_name: Some("fake"),
518            shared_access_signature: Some("fake"),
519            shared_access_key_name: Some("fake"),
520            shared_access_key: None,
521        };
522        cases.push(case);
523
524        // "only shared key name specified"
525        let case = EventHubsConnectionStringProperties {
526            endpoint: Some(url::Url::parse("sb://someplace.hosname.ext").unwrap()),
527            event_hub_name: Some("fake"),
528            shared_access_signature: None,
529            shared_access_key_name: Some("fake"),
530            shared_access_key: None,
531        };
532        cases.push(case);
533
534        // "only shared key specified"
535        let case = EventHubsConnectionStringProperties {
536            endpoint: Some(url::Url::parse("sb://someplace.hosname.ext").unwrap()),
537            event_hub_name: Some("fake"),
538            shared_access_signature: None,
539            shared_access_key_name: None,
540            shared_access_key: Some("fake"),
541        };
542        cases.push(case);
543
544        cases
545    }
546
547    #[test]
548    fn parse_correctly_parses_a_namespace_connection_string() {
549        let endpoint = "test.endpoint.com";
550        let sas_key = "sasKey=";
551        let sas_key_name = "sasName";
552        let shared_access_signature = "fakeSAS";
553        let connection_string = format!("Endpoint=sb://{endpoint};SharedAccessKeyName={sas_key_name};SharedAccessKey={sas_key};SharedAccessSignature={shared_access_signature}");
554        let parsed = EventHubsConnectionStringProperties::parse(&connection_string).unwrap();
555
556        assert_eq!(
557            parsed.endpoint().and_then(|url| url.host_str()),
558            Some(endpoint)
559        );
560        assert_eq!(parsed.shared_access_key_name(), Some(sas_key_name));
561        assert_eq!(parsed.shared_access_key(), Some(sas_key));
562        assert_eq!(
563            parsed.shared_access_signature(),
564            Some(shared_access_signature)
565        );
566        assert_eq!(parsed.event_hub_name(), None);
567    }
568
569    #[test]
570    fn parse_correctly_parses_an_entity_connection_string() {
571        let endpoint = "test.endpoint.com";
572        let event_hub = "some-path";
573        let sas_key = "sasKey";
574        let sas_key_name = "sasName";
575        let shared_access_signature = "fakeSAS";
576        let connection_string = format!("Endpoint=sb://{endpoint};SharedAccessKeyName={sas_key_name};SharedAccessKey={sas_key};EntityPath={event_hub};SharedAccessSignature={shared_access_signature}");
577        let parsed = EventHubsConnectionStringProperties::parse(&connection_string).unwrap();
578
579        assert_eq!(
580            parsed.endpoint().and_then(|url| url.host_str()),
581            Some(endpoint)
582        );
583        assert_eq!(parsed.shared_access_key_name(), Some(sas_key_name));
584        assert_eq!(parsed.shared_access_key(), Some(sas_key));
585        assert_eq!(
586            parsed.shared_access_signature(),
587            Some(shared_access_signature)
588        );
589        assert_eq!(parsed.event_hub_name(), Some(event_hub));
590    }
591
592    #[test]
593    fn parse_correctly_parses_partial_connection_strings() {
594        let cases = partial_connection_string_cases();
595
596        for (connection_string, expected) in cases {
597            assert_parsed_and_expected!(connection_string, expected);
598        }
599    }
600
601    #[test]
602    fn parse_tolerates_leading_delimiters() {
603        let endpoint = "test.endpoint.com";
604        let event_hub = "some-path";
605        let sas_key = "sasKey";
606        let sas_key_name = "sasName";
607        let connection_string = format!(";Endpoint=sb://{endpoint};SharedAccessKeyName={sas_key_name};SharedAccessKey={sas_key};EntityPath={event_hub}");
608        let parsed = EventHubsConnectionStringProperties::parse(&connection_string).unwrap();
609
610        assert_eq!(
611            parsed.endpoint().and_then(|url| url.host_str()),
612            Some(endpoint)
613        );
614        assert_eq!(parsed.shared_access_key_name(), Some(sas_key_name));
615        assert_eq!(parsed.shared_access_key(), Some(sas_key));
616        assert_eq!(parsed.event_hub_name(), Some(event_hub));
617    }
618
619    #[test]
620    fn parse_tolerates_spaces_between_pairs() {
621        let endpoint = "test.endpoint.com";
622        let event_hub = "some-path";
623        let sas_key = "sasKey";
624        let sas_key_name = "sasName";
625        let connection_string = format!("Endpoint=sb://{endpoint}; SharedAccessKeyName={sas_key_name}; SharedAccessKey={sas_key}; EntityPath={event_hub}");
626        let parsed = EventHubsConnectionStringProperties::parse(&connection_string).unwrap();
627
628        assert_eq!(
629            parsed.endpoint().and_then(|url| url.host_str()),
630            Some(endpoint)
631        );
632        assert_eq!(parsed.shared_access_key_name(), Some(sas_key_name));
633        assert_eq!(parsed.shared_access_key(), Some(sas_key));
634        assert_eq!(parsed.event_hub_name(), Some(event_hub));
635    }
636
637    #[test]
638    fn parse_tolerates_spaces_between_values() {
639        let endpoint = "test.endpoint.com";
640        let event_hub = "some-path";
641        let sas_key = "sasKey";
642        let sas_key_name = "sasName";
643        let connection_string = format!("Endpoint = sb://{endpoint};SharedAccessKeyName ={sas_key_name};SharedAccessKey= {sas_key}; EntityPath  =  {event_hub}");
644        let parsed = EventHubsConnectionStringProperties::parse(&connection_string).unwrap();
645
646        assert_eq!(
647            parsed.endpoint().and_then(|url| url.host_str()),
648            Some(endpoint)
649        );
650        assert_eq!(parsed.shared_access_key_name(), Some(sas_key_name));
651        assert_eq!(parsed.shared_access_key(), Some(sas_key));
652        assert_eq!(parsed.event_hub_name(), Some(event_hub));
653    }
654
655    #[test]
656    fn parse_does_not_force_token_ordering() {
657        let cases = random_ordering_connection_string_cases();
658
659        for (connection_string, expected) in cases {
660            assert_parsed_and_expected!(connection_string, expected);
661        }
662    }
663
664    #[test]
665    fn parse_ignores_unknown_tokens() {
666        let endpoint = "test.endpoint.com";
667        let event_hub = "some-path";
668        let sas_key = "sasKey";
669        let sas_key_name = "sasName";
670        let connection_string = format!("Endpoint=sb://{endpoint};SharedAccessKeyName={sas_key_name};Unknown=INVALID;SharedAccessKey={sas_key};EntityPath={event_hub};Trailing=WHOAREYOU");
671        let parsed = EventHubsConnectionStringProperties::parse(&connection_string).unwrap();
672
673        assert_eq!(
674            parsed.endpoint().and_then(|url| url.host_str()),
675            Some(endpoint)
676        );
677        assert_eq!(parsed.shared_access_key_name(), Some(sas_key_name));
678        assert_eq!(parsed.shared_access_key(), Some(sas_key));
679        assert_eq!(parsed.event_hub_name(), Some(event_hub));
680    }
681
682    #[test]
683    fn parse_does_accept_host_names_and_urls_for_the_endpoint() {
684        let endpoint_values = &[
685            // "test.endpoint.com", // TODO: this is not a valid url and cannot be parsed by `url::Url`
686            "sb://test.endpoint.com",
687            "sb://test.endpoint.com:80",
688            "amqp://test.endpoint.com",
689            // "http://test.endpoint.com", // TODO: `url::Url` doesn't allow changing from http to other schemes
690            // "https://test.endpoint.com:8443",
691        ];
692
693        for endpoint_value in endpoint_values {
694            let connection_string = format!("Endpoint={};EntityPath=dummy", endpoint_value);
695            let parsed = EventHubsConnectionStringProperties::parse(&connection_string).unwrap();
696
697            assert_eq!(
698                parsed.endpoint().and_then(|url| url.host_str()),
699                Some("test.endpoint.com")
700            );
701        }
702    }
703
704    #[test]
705    fn parse_does_not_allow_an_invalid_endpoint_format() {
706        let endpoint = "test.endpoint.com";
707        let connection_string = format!("Endpoint={}", endpoint);
708        let result = EventHubsConnectionStringProperties::parse(&connection_string);
709        assert!(result.is_err());
710    }
711
712    #[test]
713    fn parse_considers_missing_values_as_malformed() {
714        let test_cases = &[
715            "Endpoint;SharedAccessKeyName=[value];SharedAccessKey=[value];EntityPath=[value]",
716            "Endpoint=value.com;SharedAccessKeyName=;SharedAccessKey=[value];EntityPath=[value]",
717            "Endpoint=value.com;SharedAccessKeyName=[value];SharedAccessKey;EntityPath=[value]",
718            "Endpoint=value.com;SharedAccessKeyName=[value];SharedAccessKey=[value];EntityPath",
719            "Endpoint;SharedAccessKeyName=;SharedAccessKey;EntityPath=",
720            "Endpoint=;SharedAccessKeyName;SharedAccessKey;EntityPath=",
721        ];
722
723        for test_case in test_cases {
724            let result = EventHubsConnectionStringProperties::parse(test_case);
725            assert_eq!(result, Err(FormatError::InvalidConnectionString));
726        }
727    }
728
729    #[test]
730    fn to_string_validates_properties() {
731        let cases = to_connection_string_validates_properties_cases();
732
733        for case in cases {
734            let result = case.to_connection_string();
735            assert!(result.is_err());
736        }
737    }
738
739    #[test]
740    fn to_connection_string_produces_the_connection_string_for_shared_access_signatures() {
741        let properties = EventHubsConnectionStringProperties {
742            endpoint: Some("sb://place.endpoint.ext".parse().unwrap()),
743            event_hub_name: Some("HubName"),
744            shared_access_signature: Some("FaKe#$1324@@"),
745            shared_access_key_name: None,
746            shared_access_key: None,
747        };
748
749        let connection_string = properties.to_connection_string();
750        assert!(connection_string.is_ok());
751        let connection_string = connection_string.unwrap();
752        assert!(!connection_string.is_empty());
753
754        let parsed = EventHubsConnectionStringProperties::parse(&connection_string);
755        assert!(parsed.is_ok());
756        assert_eq!(properties, parsed.unwrap());
757    }
758
759    #[test]
760    fn to_connection_string_produces_the_connection_string_for_shared_keys() {
761        let properties = EventHubsConnectionStringProperties {
762            endpoint: Some("sb://place.endpoint.ext".parse().unwrap()),
763            event_hub_name: Some("HubName"),
764            shared_access_signature: None,
765            shared_access_key_name: Some("RootSharedAccessManagementKey"),
766            shared_access_key: Some("FaKe#$1324@@"),
767        };
768
769        let connection_string = properties.to_connection_string();
770        assert!(connection_string.is_ok());
771        let connection_string = connection_string.unwrap();
772        assert!(!connection_string.is_empty());
773
774        let parsed = EventHubsConnectionStringProperties::parse(&connection_string);
775        assert!(parsed.is_ok());
776        assert_eq!(properties, parsed.unwrap());
777    }
778
779    #[test]
780    fn to_connection_string_returns_err_with_non_servicebus_endpoint_scheme() {
781        let schemes = vec![
782            "amqps://", "amqp://",
783            "http://", // TODO: `url::Url` does not allow changing the scheme away from `http` or `https`
784            "https://", "fake://",
785        ];
786
787        for scheme in schemes {
788            let endpoint = format!("{}myhub.servicebus.windows.net", scheme);
789            let properties = EventHubsConnectionStringProperties {
790                endpoint: Some(url::Url::parse(&endpoint).unwrap()),
791                event_hub_name: Some("HubName"),
792                shared_access_signature: None,
793                shared_access_key_name: Some("RootSharedAccessManagementKey"),
794                shared_access_key: Some("FaKe#$1324@@"),
795            };
796
797            let connection_string = properties.to_connection_string();
798            assert!(connection_string.is_err());
799        }
800    }
801
802    #[test]
803    fn to_connection_string_returns_ok_with_servicebus_endpoint_scheme() {
804        let endpoint = "sb://myhub.servicebus.windows.net";
805        let properties = EventHubsConnectionStringProperties {
806            endpoint: Some(url::Url::parse(endpoint).unwrap()),
807            event_hub_name: Some("HubName"),
808            shared_access_signature: None,
809            shared_access_key_name: Some("RootSharedAccessManagementKey"),
810            shared_access_key: Some("FaKe#$1324@@"),
811        };
812
813        let connection_string = properties.to_connection_string();
814        assert!(connection_string.is_ok());
815        let connection_string = connection_string.unwrap();
816
817        let parsed = EventHubsConnectionStringProperties::parse(&connection_string);
818        assert!(parsed.is_ok());
819        assert_eq!(properties, parsed.unwrap());
820    }
821
822    #[test]
823    fn to_connection_string_allows_shared_access_key_authorization() {
824        let fake_connection = "Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessKeyName=DummyKey;SharedAccessKey=[not_real]";
825        let properties = EventHubsConnectionStringProperties::parse(fake_connection).unwrap();
826
827        assert!(properties.to_connection_string().is_ok());
828    }
829
830    #[test]
831    fn to_connection_string_allows_shared_access_signature_authorization() {
832        let fake_connection =
833            "Endpoint=sb://not-real.servicebus.windows.net/;SharedAccessSignature=[not_real]";
834        let properties = EventHubsConnectionStringProperties::parse(fake_connection).unwrap();
835
836        assert!(properties.to_connection_string().is_ok());
837    }
838}