azservicebus/primitives/
service_bus_connection_string_properties.rs

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