1use crate::error::ConnectionError;
7use std::collections::HashMap;
8use std::fmt;
9use std::str::FromStr;
10use std::time::Duration;
11
12#[derive(Clone)]
14pub struct ConnectionParams {
15 pub host: String,
17
18 pub port: u16,
20
21 pub username: String,
23
24 password: String,
26
27 pub schema: Option<String>,
29
30 pub connection_timeout: Duration,
32
33 pub query_timeout: Duration,
35
36 pub idle_timeout: Duration,
38
39 pub use_tls: bool,
41
42 pub validate_server_certificate: bool,
44
45 pub certificate_fingerprint: Option<String>,
47
48 pub client_name: String,
50
51 pub client_version: String,
53
54 pub attributes: HashMap<String, String>,
56}
57
58impl ConnectionParams {
59 pub(crate) fn password(&self) -> &str {
61 &self.password
62 }
63
64 pub fn builder() -> ConnectionBuilder {
66 ConnectionBuilder::new()
67 }
68}
69
70impl FromStr for ConnectionParams {
71 type Err = ConnectionError;
72
73 fn from_str(s: &str) -> Result<Self, Self::Err> {
79 let url = s.trim();
81
82 if !url.starts_with("exasol://") {
84 return Err(ConnectionError::ParseError(
85 "Connection string must start with 'exasol://'".to_string(),
86 ));
87 }
88
89 let url = &url[9..]; let (main_part, query_string) = match url.split_once('?') {
93 Some((main, query)) => (main, Some(query)),
94 None => (url, None),
95 };
96
97 let mut params = parse_query_params(query_string)?;
99
100 let (auth_part, host_part) = match main_part.rfind('@') {
102 Some(pos) => {
103 let auth = &main_part[..pos];
104 let host = &main_part[pos + 1..];
105 (Some(auth), host)
106 }
107 None => (None, main_part),
108 };
109
110 let (username, password) = if let Some(auth) = auth_part {
112 parse_auth(auth)?
113 } else {
114 let username = params
116 .remove("user")
117 .or_else(|| params.remove("username"))
118 .ok_or_else(|| ConnectionError::ParseError("Username is required".to_string()))?;
119 let password = params
120 .remove("password")
121 .or_else(|| params.remove("pass"))
122 .unwrap_or_default();
123 (username, password)
124 };
125
126 let (host_port, schema) = match host_part.split_once('/') {
128 Some((host, schema)) => {
129 let schema = if schema.is_empty() {
130 None
131 } else {
132 Some(schema.to_string())
133 };
134 (host, schema)
135 }
136 None => (host_part, None),
137 };
138
139 let (host, port) = parse_host_port(host_port)?;
141
142 let mut builder = ConnectionBuilder::new()
144 .host(&host)
145 .port(port)
146 .username(&username)
147 .password(&password);
148
149 if let Some(schema) = schema {
150 builder = builder.schema(&schema);
151 }
152
153 builder = apply_query_params(builder, params)?;
155
156 builder.build()
157 }
158}
159
160impl fmt::Debug for ConnectionParams {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 f.debug_struct("ConnectionParams")
164 .field("host", &self.host)
165 .field("port", &self.port)
166 .field("username", &self.username)
167 .field("password", &"<redacted>")
168 .field("schema", &self.schema)
169 .field("connection_timeout", &self.connection_timeout)
170 .field("query_timeout", &self.query_timeout)
171 .field("idle_timeout", &self.idle_timeout)
172 .field("use_tls", &self.use_tls)
173 .field(
174 "validate_server_certificate",
175 &self.validate_server_certificate,
176 )
177 .field("certificate_fingerprint", &self.certificate_fingerprint)
178 .field("client_name", &self.client_name)
179 .field("client_version", &self.client_version)
180 .field("attributes", &self.attributes)
181 .finish()
182 }
183}
184
185impl fmt::Display for ConnectionParams {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 write!(
188 f,
189 "ConnectionParams {{ host: {}, port: {}, username: {}, schema: {:?}, use_tls: {} }}",
190 self.host, self.port, self.username, self.schema, self.use_tls
191 )
192 }
193}
194
195#[derive(Debug, Clone)]
197pub struct ConnectionBuilder {
198 host: Option<String>,
199 port: Option<u16>,
200 username: Option<String>,
201 password: Option<String>,
202 schema: Option<String>,
203 connection_timeout: Option<Duration>,
204 query_timeout: Option<Duration>,
205 idle_timeout: Option<Duration>,
206 use_tls: Option<bool>,
207 validate_server_certificate: Option<bool>,
208 certificate_fingerprint: Option<String>,
209 client_name: Option<String>,
210 client_version: Option<String>,
211 attributes: HashMap<String, String>,
212}
213
214impl ConnectionBuilder {
215 pub fn new() -> Self {
217 Self {
218 host: None,
219 port: None,
220 username: None,
221 password: None,
222 schema: None,
223 connection_timeout: None,
224 query_timeout: None,
225 idle_timeout: None,
226 use_tls: None,
227 validate_server_certificate: None,
228 certificate_fingerprint: None,
229 client_name: None,
230 client_version: None,
231 attributes: HashMap::new(),
232 }
233 }
234
235 pub fn host(mut self, host: &str) -> Self {
237 self.host = Some(host.to_string());
238 self
239 }
240
241 pub fn port(mut self, port: u16) -> Self {
243 self.port = Some(port);
244 self
245 }
246
247 pub fn username(mut self, username: &str) -> Self {
249 self.username = Some(username.to_string());
250 self
251 }
252
253 pub fn password(mut self, password: &str) -> Self {
255 self.password = Some(password.to_string());
256 self
257 }
258
259 pub fn schema(mut self, schema: &str) -> Self {
261 self.schema = Some(schema.to_string());
262 self
263 }
264
265 pub fn connection_timeout(mut self, timeout: Duration) -> Self {
267 self.connection_timeout = Some(timeout);
268 self
269 }
270
271 pub fn query_timeout(mut self, timeout: Duration) -> Self {
273 self.query_timeout = Some(timeout);
274 self
275 }
276
277 pub fn idle_timeout(mut self, timeout: Duration) -> Self {
279 self.idle_timeout = Some(timeout);
280 self
281 }
282
283 pub fn use_tls(mut self, use_tls: bool) -> Self {
285 self.use_tls = Some(use_tls);
286 self
287 }
288
289 pub fn validate_server_certificate(mut self, validate: bool) -> Self {
291 self.validate_server_certificate = Some(validate);
292 self
293 }
294
295 pub fn certificate_fingerprint(mut self, fingerprint: &str) -> Self {
297 self.certificate_fingerprint = Some(fingerprint.to_string());
298 self
299 }
300
301 pub fn client_name(mut self, name: &str) -> Self {
303 self.client_name = Some(name.to_string());
304 self
305 }
306
307 pub fn client_version(mut self, version: &str) -> Self {
309 self.client_version = Some(version.to_string());
310 self
311 }
312
313 pub fn attribute(mut self, key: &str, value: &str) -> Self {
315 self.attributes.insert(key.to_string(), value.to_string());
316 self
317 }
318
319 pub fn build(self) -> Result<ConnectionParams, ConnectionError> {
321 let host = self.host.ok_or_else(|| ConnectionError::InvalidParameter {
323 parameter: "host".to_string(),
324 message: "Host is required".to_string(),
325 })?;
326
327 let username = self
328 .username
329 .ok_or_else(|| ConnectionError::InvalidParameter {
330 parameter: "username".to_string(),
331 message: "Username is required".to_string(),
332 })?;
333
334 if host.is_empty() {
336 return Err(ConnectionError::InvalidParameter {
337 parameter: "host".to_string(),
338 message: "Host cannot be empty".to_string(),
339 });
340 }
341
342 if username.is_empty() {
344 return Err(ConnectionError::InvalidParameter {
345 parameter: "username".to_string(),
346 message: "Username cannot be empty".to_string(),
347 });
348 }
349
350 let port = self.port.unwrap_or(8563);
351
352 if port == 0 {
354 return Err(ConnectionError::InvalidParameter {
355 parameter: "port".to_string(),
356 message: "Port must be greater than 0".to_string(),
357 });
358 }
359
360 let connection_timeout = self.connection_timeout.unwrap_or(Duration::from_secs(30));
362 let query_timeout = self.query_timeout.unwrap_or(Duration::from_secs(300));
363 let idle_timeout = self.idle_timeout.unwrap_or(Duration::from_secs(600));
364
365 if connection_timeout.as_secs() > 300 {
366 return Err(ConnectionError::InvalidParameter {
367 parameter: "connection_timeout".to_string(),
368 message: "Connection timeout cannot exceed 300 seconds".to_string(),
369 });
370 }
371
372 Ok(ConnectionParams {
373 host,
374 port,
375 username,
376 password: self.password.unwrap_or_default(),
377 schema: self.schema,
378 connection_timeout,
379 query_timeout,
380 idle_timeout,
381 use_tls: self.use_tls.unwrap_or(false),
382 validate_server_certificate: self.validate_server_certificate.unwrap_or(true),
383 certificate_fingerprint: self.certificate_fingerprint,
384 client_name: self.client_name.unwrap_or_else(|| "exarrow-rs".to_string()),
385 client_version: self
386 .client_version
387 .unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()),
388 attributes: self.attributes,
389 })
390 }
391}
392
393impl Default for ConnectionBuilder {
394 fn default() -> Self {
395 Self::new()
396 }
397}
398
399fn parse_query_params(query: Option<&str>) -> Result<HashMap<String, String>, ConnectionError> {
401 let mut params = HashMap::new();
402
403 if let Some(query) = query {
404 for pair in query.split('&') {
405 if pair.is_empty() {
406 continue;
407 }
408
409 let (key, value) = match pair.split_once('=') {
410 Some((k, v)) => (k, v),
411 None => {
412 return Err(ConnectionError::ParseError(format!(
413 "Invalid query parameter format: {}",
414 pair
415 )));
416 }
417 };
418
419 let key = urlencoding::decode(key)
421 .map_err(|e| ConnectionError::ParseError(format!("Failed to decode key: {}", e)))?
422 .into_owned();
423 let value = urlencoding::decode(value)
424 .map_err(|e| ConnectionError::ParseError(format!("Failed to decode value: {}", e)))?
425 .into_owned();
426
427 params.insert(key, value);
428 }
429 }
430
431 Ok(params)
432}
433
434fn parse_auth(auth: &str) -> Result<(String, String), ConnectionError> {
436 match auth.split_once(':') {
437 Some((user, pass)) => {
438 let user = urlencoding::decode(user)
439 .map_err(|e| {
440 ConnectionError::ParseError(format!("Failed to decode username: {}", e))
441 })?
442 .into_owned();
443 let pass = urlencoding::decode(pass)
444 .map_err(|e| {
445 ConnectionError::ParseError(format!("Failed to decode password: {}", e))
446 })?
447 .into_owned();
448 Ok((user, pass))
449 }
450 None => {
451 let user = urlencoding::decode(auth)
452 .map_err(|e| {
453 ConnectionError::ParseError(format!("Failed to decode username: {}", e))
454 })?
455 .into_owned();
456 Ok((user, String::new()))
457 }
458 }
459}
460
461fn parse_host_port(host_port: &str) -> Result<(String, u16), ConnectionError> {
463 if host_port.starts_with('[') {
465 if let Some(close_bracket) = host_port.find(']') {
466 let host = host_port[1..close_bracket].to_string();
467 let port_part = &host_port[close_bracket + 1..];
468
469 let port = if let Some(stripped) = port_part.strip_prefix(':') {
470 stripped.parse().map_err(|_| {
471 ConnectionError::ParseError(format!("Invalid port: {}", port_part))
472 })?
473 } else {
474 8563
475 };
476
477 return Ok((host, port));
478 }
479 }
480
481 match host_port.rsplit_once(':') {
483 Some((host, port_str)) => {
484 let port = port_str
485 .parse()
486 .map_err(|_| ConnectionError::ParseError(format!("Invalid port: {}", port_str)))?;
487 Ok((host.to_string(), port))
488 }
489 None => Ok((host_port.to_string(), 8563)),
490 }
491}
492
493fn apply_query_params(
495 mut builder: ConnectionBuilder,
496 params: HashMap<String, String>,
497) -> Result<ConnectionBuilder, ConnectionError> {
498 for (key, value) in params {
499 match key.as_str() {
500 "timeout" | "connection_timeout" => {
501 let secs: u64 = value
502 .parse()
503 .map_err(|_| ConnectionError::InvalidParameter {
504 parameter: key.clone(),
505 message: format!("Invalid timeout value: {}", value),
506 })?;
507 builder = builder.connection_timeout(Duration::from_secs(secs));
508 }
509 "query_timeout" => {
510 let secs: u64 = value
511 .parse()
512 .map_err(|_| ConnectionError::InvalidParameter {
513 parameter: key.clone(),
514 message: format!("Invalid timeout value: {}", value),
515 })?;
516 builder = builder.query_timeout(Duration::from_secs(secs));
517 }
518 "idle_timeout" => {
519 let secs: u64 = value
520 .parse()
521 .map_err(|_| ConnectionError::InvalidParameter {
522 parameter: key.clone(),
523 message: format!("Invalid timeout value: {}", value),
524 })?;
525 builder = builder.idle_timeout(Duration::from_secs(secs));
526 }
527 "tls" | "use_tls" | "ssl" => {
528 let use_tls = parse_bool(&value)?;
529 builder = builder.use_tls(use_tls);
530 }
531 "validate_certificate" | "verify_certificate" | "validateservercertificate" => {
532 let validate = parse_bool(&value)?;
533 builder = builder.validate_server_certificate(validate);
534 }
535 "client_name" => {
536 builder = builder.client_name(&value);
537 }
538 "client_version" => {
539 builder = builder.client_version(&value);
540 }
541 "certificate_fingerprint" | "certificatefingerprint" => {
542 builder = builder.certificate_fingerprint(&value);
543 }
544 _ => {
545 builder = builder.attribute(&key, &value);
547 }
548 }
549 }
550
551 Ok(builder)
552}
553
554fn parse_bool(s: &str) -> Result<bool, ConnectionError> {
556 match s.to_lowercase().as_str() {
557 "true" | "1" | "yes" | "on" => Ok(true),
558 "false" | "0" | "no" | "off" => Ok(false),
559 _ => Err(ConnectionError::InvalidParameter {
560 parameter: "boolean".to_string(),
561 message: format!("Invalid boolean value: {}", s),
562 }),
563 }
564}
565
566#[cfg(test)]
567mod tests {
568 use super::*;
569
570 #[test]
571 fn test_builder_minimal() {
572 let params = ConnectionBuilder::new()
573 .host("localhost")
574 .username("test")
575 .build()
576 .unwrap();
577
578 assert_eq!(params.host, "localhost");
579 assert_eq!(params.port, 8563);
580 assert_eq!(params.username, "test");
581 assert_eq!(params.password(), "");
582 }
583
584 #[test]
585 fn test_builder_full() {
586 let params = ConnectionBuilder::new()
587 .host("db.example.com")
588 .port(9000)
589 .username("admin")
590 .password("secret")
591 .schema("MY_SCHEMA")
592 .connection_timeout(Duration::from_secs(20))
593 .query_timeout(Duration::from_secs(60))
594 .use_tls(true)
595 .client_name("test-client")
596 .attribute("custom", "value")
597 .build()
598 .unwrap();
599
600 assert_eq!(params.host, "db.example.com");
601 assert_eq!(params.port, 9000);
602 assert_eq!(params.username, "admin");
603 assert_eq!(params.password(), "secret");
604 assert_eq!(params.schema, Some("MY_SCHEMA".to_string()));
605 assert_eq!(params.connection_timeout, Duration::from_secs(20));
606 assert_eq!(params.query_timeout, Duration::from_secs(60));
607 assert!(params.use_tls);
608 assert_eq!(params.client_name, "test-client");
609 assert_eq!(params.attributes.get("custom"), Some(&"value".to_string()));
610 }
611
612 #[test]
613 fn test_builder_validation_missing_host() {
614 let result = ConnectionBuilder::new().username("test").build();
615
616 assert!(result.is_err());
617 assert!(matches!(
618 result.unwrap_err(),
619 ConnectionError::InvalidParameter { parameter, .. } if parameter == "host"
620 ));
621 }
622
623 #[test]
624 fn test_builder_validation_empty_host() {
625 let result = ConnectionBuilder::new().host("").username("test").build();
626
627 assert!(result.is_err());
628 }
629
630 #[test]
631 fn test_builder_validation_timeout() {
632 let result = ConnectionBuilder::new()
633 .host("localhost")
634 .username("test")
635 .connection_timeout(Duration::from_secs(400))
636 .build();
637
638 assert!(result.is_err());
639 }
640
641 #[test]
642 fn test_parse_basic() {
643 let params = ConnectionParams::from_str("exasol://user@localhost").unwrap();
644
645 assert_eq!(params.host, "localhost");
646 assert_eq!(params.port, 8563);
647 assert_eq!(params.username, "user");
648 }
649
650 #[test]
651 fn test_parse_with_port() {
652 let params = ConnectionParams::from_str("exasol://user@localhost:9000").unwrap();
653
654 assert_eq!(params.host, "localhost");
655 assert_eq!(params.port, 9000);
656 }
657
658 #[test]
659 fn test_parse_with_password() {
660 let params = ConnectionParams::from_str("exasol://user:pass@localhost").unwrap();
661
662 assert_eq!(params.username, "user");
663 assert_eq!(params.password(), "pass");
664 }
665
666 #[test]
667 fn test_parse_with_schema() {
668 let params = ConnectionParams::from_str("exasol://user@localhost/MY_SCHEMA").unwrap();
669
670 assert_eq!(params.schema, Some("MY_SCHEMA".to_string()));
671 }
672
673 #[test]
674 fn test_parse_with_query_params() {
675 let params = ConnectionParams::from_str(
676 "exasol://user@localhost?timeout=20&tls=true&client_name=test",
677 )
678 .unwrap();
679
680 assert_eq!(params.connection_timeout, Duration::from_secs(20));
681 assert!(params.use_tls);
682 assert_eq!(params.client_name, "test");
683 }
684
685 #[test]
686 fn test_parse_full_url() {
687 let params = ConnectionParams::from_str(
688 "exasol://admin:secret@db.example.com:9000/PROD?timeout=30&tls=true",
689 )
690 .unwrap();
691
692 assert_eq!(params.host, "db.example.com");
693 assert_eq!(params.port, 9000);
694 assert_eq!(params.username, "admin");
695 assert_eq!(params.password(), "secret");
696 assert_eq!(params.schema, Some("PROD".to_string()));
697 assert_eq!(params.connection_timeout, Duration::from_secs(30));
698 assert!(params.use_tls);
699 }
700
701 #[test]
702 fn test_parse_url_encoded() {
703 let params = ConnectionParams::from_str("exasol://user%40test:p%40ss@localhost").unwrap();
704
705 assert_eq!(params.username, "user@test");
706 assert_eq!(params.password(), "p@ss");
707 }
708
709 #[test]
710 fn test_parse_ipv6() {
711 let params = ConnectionParams::from_str("exasol://user@[::1]:8563").unwrap();
712
713 assert_eq!(params.host, "::1");
714 assert_eq!(params.port, 8563);
715 }
716
717 #[test]
718 fn test_parse_invalid_scheme() {
719 let result = ConnectionParams::from_str("postgres://user@localhost");
720 assert!(result.is_err());
721 }
722
723 #[test]
724 fn test_parse_missing_username() {
725 let result = ConnectionParams::from_str("exasol://localhost");
726 assert!(result.is_err());
727 }
728
729 #[test]
730 fn test_display_no_password_leak() {
731 let params = ConnectionBuilder::new()
732 .host("localhost")
733 .username("admin")
734 .password("super_secret")
735 .build()
736 .unwrap();
737
738 let display = format!("{}", params);
739 assert!(!display.contains("super_secret"));
740 assert!(display.contains("localhost"));
741 assert!(display.contains("admin"));
742 }
743
744 #[test]
745 fn test_debug_no_password_leak() {
746 let params = ConnectionBuilder::new()
747 .host("localhost")
748 .username("admin")
749 .password("super_secret")
750 .build()
751 .unwrap();
752
753 let debug = format!("{:?}", params);
754 assert!(!debug.contains("super_secret"));
756 }
757
758 #[test]
763 fn test_builder_validation_missing_username() {
764 let result = ConnectionBuilder::new().host("localhost").build();
765
766 assert!(result.is_err());
767 assert!(matches!(
768 result.unwrap_err(),
769 ConnectionError::InvalidParameter { parameter, .. } if parameter == "username"
770 ));
771 }
772
773 #[test]
774 fn test_builder_validation_empty_username() {
775 let result = ConnectionBuilder::new()
776 .host("localhost")
777 .username("")
778 .build();
779
780 assert!(result.is_err());
781 assert!(matches!(
782 result.unwrap_err(),
783 ConnectionError::InvalidParameter { parameter, message }
784 if parameter == "username" && message.contains("empty")
785 ));
786 }
787
788 #[test]
789 fn test_builder_validation_port_zero() {
790 let result = ConnectionBuilder::new()
791 .host("localhost")
792 .username("test")
793 .port(0)
794 .build();
795
796 assert!(result.is_err());
797 assert!(matches!(
798 result.unwrap_err(),
799 ConnectionError::InvalidParameter { parameter, message }
800 if parameter == "port" && message.contains("greater than 0")
801 ));
802 }
803
804 #[test]
805 fn test_builder_default() {
806 let builder = ConnectionBuilder::default();
807 let result = builder.host("localhost").username("user").build().unwrap();
808 assert_eq!(result.host, "localhost");
809 }
810
811 #[test]
812 fn test_connection_params_builder_method() {
813 let builder = ConnectionParams::builder();
814 let params = builder.host("localhost").username("user").build().unwrap();
815 assert_eq!(params.host, "localhost");
816 }
817
818 #[test]
819 fn test_builder_idle_timeout() {
820 let params = ConnectionBuilder::new()
821 .host("localhost")
822 .username("test")
823 .idle_timeout(Duration::from_secs(120))
824 .build()
825 .unwrap();
826
827 assert_eq!(params.idle_timeout, Duration::from_secs(120));
828 }
829
830 #[test]
831 fn test_builder_validate_server_certificate() {
832 let params = ConnectionBuilder::new()
833 .host("localhost")
834 .username("test")
835 .validate_server_certificate(false)
836 .build()
837 .unwrap();
838
839 assert!(!params.validate_server_certificate);
840 }
841
842 #[test]
843 fn test_builder_client_version() {
844 let params = ConnectionBuilder::new()
845 .host("localhost")
846 .username("test")
847 .client_version("1.2.3")
848 .build()
849 .unwrap();
850
851 assert_eq!(params.client_version, "1.2.3");
852 }
853
854 #[test]
855 fn test_builder_default_values() {
856 let params = ConnectionBuilder::new()
857 .host("localhost")
858 .username("test")
859 .build()
860 .unwrap();
861
862 assert_eq!(params.connection_timeout, Duration::from_secs(30));
863 assert_eq!(params.query_timeout, Duration::from_secs(300));
864 assert_eq!(params.idle_timeout, Duration::from_secs(600));
865 assert!(!params.use_tls);
866 assert!(params.validate_server_certificate);
867 assert_eq!(params.client_name, "exarrow-rs");
868 }
869
870 #[test]
875 fn test_parse_query_param_without_equals() {
876 let result = ConnectionParams::from_str("exasol://user@localhost?invalid_param");
877
878 assert!(result.is_err());
879 assert!(matches!(
880 result.unwrap_err(),
881 ConnectionError::ParseError(msg) if msg.contains("Invalid query parameter format")
882 ));
883 }
884
885 #[test]
886 fn test_parse_query_param_empty_pairs() {
887 let params =
889 ConnectionParams::from_str("exasol://user@localhost?timeout=10&&tls=true").unwrap();
890
891 assert_eq!(params.connection_timeout, Duration::from_secs(10));
892 assert!(params.use_tls);
893 }
894
895 #[test]
896 fn test_parse_query_timeout() {
897 let params =
898 ConnectionParams::from_str("exasol://user@localhost?query_timeout=60").unwrap();
899
900 assert_eq!(params.query_timeout, Duration::from_secs(60));
901 }
902
903 #[test]
904 fn test_parse_idle_timeout() {
905 let params =
906 ConnectionParams::from_str("exasol://user@localhost?idle_timeout=120").unwrap();
907
908 assert_eq!(params.idle_timeout, Duration::from_secs(120));
909 }
910
911 #[test]
912 fn test_parse_connection_timeout_param() {
913 let params =
914 ConnectionParams::from_str("exasol://user@localhost?connection_timeout=15").unwrap();
915
916 assert_eq!(params.connection_timeout, Duration::from_secs(15));
917 }
918
919 #[test]
920 fn test_parse_invalid_timeout_value() {
921 let result = ConnectionParams::from_str("exasol://user@localhost?timeout=not_a_number");
922
923 assert!(result.is_err());
924 assert!(matches!(
925 result.unwrap_err(),
926 ConnectionError::InvalidParameter { parameter, message }
927 if parameter == "timeout" && message.contains("Invalid timeout value")
928 ));
929 }
930
931 #[test]
932 fn test_parse_invalid_query_timeout_value() {
933 let result =
934 ConnectionParams::from_str("exasol://user@localhost?query_timeout=not_a_number");
935
936 assert!(result.is_err());
937 assert!(matches!(
938 result.unwrap_err(),
939 ConnectionError::InvalidParameter { parameter, .. } if parameter == "query_timeout"
940 ));
941 }
942
943 #[test]
944 fn test_parse_invalid_idle_timeout_value() {
945 let result =
946 ConnectionParams::from_str("exasol://user@localhost?idle_timeout=not_a_number");
947
948 assert!(result.is_err());
949 assert!(matches!(
950 result.unwrap_err(),
951 ConnectionError::InvalidParameter { parameter, .. } if parameter == "idle_timeout"
952 ));
953 }
954
955 #[test]
960 fn test_parse_ssl_param() {
961 let params = ConnectionParams::from_str("exasol://user@localhost?ssl=true").unwrap();
962
963 assert!(params.use_tls);
964 }
965
966 #[test]
967 fn test_parse_use_tls_param() {
968 let params = ConnectionParams::from_str("exasol://user@localhost?use_tls=1").unwrap();
969
970 assert!(params.use_tls);
971 }
972
973 #[test]
974 fn test_parse_validate_certificate_param() {
975 let params =
976 ConnectionParams::from_str("exasol://user@localhost?validate_certificate=false")
977 .unwrap();
978
979 assert!(!params.validate_server_certificate);
980 }
981
982 #[test]
983 fn test_parse_verify_certificate_param() {
984 let params =
985 ConnectionParams::from_str("exasol://user@localhost?verify_certificate=0").unwrap();
986
987 assert!(!params.validate_server_certificate);
988 }
989
990 #[test]
991 fn test_parse_validateservercertificate_param() {
992 let params =
993 ConnectionParams::from_str("exasol://user@localhost?validateservercertificate=no")
994 .unwrap();
995
996 assert!(!params.validate_server_certificate);
997 }
998
999 #[test]
1004 fn test_parse_bool_yes() {
1005 let params = ConnectionParams::from_str("exasol://user@localhost?tls=yes").unwrap();
1006 assert!(params.use_tls);
1007 }
1008
1009 #[test]
1010 fn test_parse_bool_no() {
1011 let params = ConnectionParams::from_str("exasol://user@localhost?tls=no").unwrap();
1012 assert!(!params.use_tls);
1013 }
1014
1015 #[test]
1016 fn test_parse_bool_on() {
1017 let params = ConnectionParams::from_str("exasol://user@localhost?tls=on").unwrap();
1018 assert!(params.use_tls);
1019 }
1020
1021 #[test]
1022 fn test_parse_bool_off() {
1023 let params = ConnectionParams::from_str("exasol://user@localhost?tls=off").unwrap();
1024 assert!(!params.use_tls);
1025 }
1026
1027 #[test]
1028 fn test_parse_bool_one() {
1029 let params = ConnectionParams::from_str("exasol://user@localhost?tls=1").unwrap();
1030 assert!(params.use_tls);
1031 }
1032
1033 #[test]
1034 fn test_parse_bool_zero() {
1035 let params = ConnectionParams::from_str("exasol://user@localhost?tls=0").unwrap();
1036 assert!(!params.use_tls);
1037 }
1038
1039 #[test]
1040 fn test_parse_bool_case_insensitive() {
1041 let params = ConnectionParams::from_str("exasol://user@localhost?tls=TRUE").unwrap();
1042 assert!(params.use_tls);
1043
1044 let params = ConnectionParams::from_str("exasol://user@localhost?tls=FALSE").unwrap();
1045 assert!(!params.use_tls);
1046 }
1047
1048 #[test]
1049 fn test_parse_bool_invalid() {
1050 let result = ConnectionParams::from_str("exasol://user@localhost?tls=maybe");
1051
1052 assert!(result.is_err());
1053 assert!(matches!(
1054 result.unwrap_err(),
1055 ConnectionError::InvalidParameter { parameter, message }
1056 if parameter == "boolean" && message.contains("Invalid boolean value")
1057 ));
1058 }
1059
1060 #[test]
1065 fn test_parse_client_version_param() {
1066 let params =
1067 ConnectionParams::from_str("exasol://user@localhost?client_version=2.0.0").unwrap();
1068
1069 assert_eq!(params.client_version, "2.0.0");
1070 }
1071
1072 #[test]
1073 fn test_parse_custom_attribute_param() {
1074 let params =
1075 ConnectionParams::from_str("exasol://user@localhost?custom_key=custom_value").unwrap();
1076
1077 assert_eq!(
1078 params.attributes.get("custom_key"),
1079 Some(&"custom_value".to_string())
1080 );
1081 }
1082
1083 #[test]
1088 fn test_parse_username_from_query_user() {
1089 let params = ConnectionParams::from_str("exasol://localhost?user=testuser").unwrap();
1090
1091 assert_eq!(params.username, "testuser");
1092 }
1093
1094 #[test]
1095 fn test_parse_username_from_query_username() {
1096 let params = ConnectionParams::from_str("exasol://localhost?username=testuser").unwrap();
1097
1098 assert_eq!(params.username, "testuser");
1099 }
1100
1101 #[test]
1102 fn test_parse_password_from_query_password() {
1103 let params =
1104 ConnectionParams::from_str("exasol://localhost?user=testuser&password=secret").unwrap();
1105
1106 assert_eq!(params.password(), "secret");
1107 }
1108
1109 #[test]
1110 fn test_parse_password_from_query_pass() {
1111 let params =
1112 ConnectionParams::from_str("exasol://localhost?user=testuser&pass=secret").unwrap();
1113
1114 assert_eq!(params.password(), "secret");
1115 }
1116
1117 #[test]
1118 fn test_parse_auth_from_query_no_password() {
1119 let params = ConnectionParams::from_str("exasol://localhost?user=testuser").unwrap();
1120
1121 assert_eq!(params.username, "testuser");
1122 assert_eq!(params.password(), "");
1123 }
1124
1125 #[test]
1130 fn test_parse_ipv6_without_port() {
1131 let params = ConnectionParams::from_str("exasol://user@[::1]").unwrap();
1132
1133 assert_eq!(params.host, "::1");
1134 assert_eq!(params.port, 8563);
1135 }
1136
1137 #[test]
1138 fn test_parse_ipv6_full_address() {
1139 let params = ConnectionParams::from_str("exasol://user@[2001:db8::1]:9000/schema").unwrap();
1140
1141 assert_eq!(params.host, "2001:db8::1");
1142 assert_eq!(params.port, 9000);
1143 assert_eq!(params.schema, Some("schema".to_string()));
1144 }
1145
1146 #[test]
1151 fn test_parse_empty_schema_path() {
1152 let params = ConnectionParams::from_str("exasol://user@localhost/").unwrap();
1153
1154 assert_eq!(params.schema, None);
1155 }
1156
1157 #[test]
1158 fn test_parse_schema_with_query_params() {
1159 let params =
1160 ConnectionParams::from_str("exasol://user@localhost/MY_SCHEMA?tls=true").unwrap();
1161
1162 assert_eq!(params.schema, Some("MY_SCHEMA".to_string()));
1163 assert!(params.use_tls);
1164 }
1165
1166 #[test]
1171 fn test_parse_invalid_port() {
1172 let result = ConnectionParams::from_str("exasol://user@localhost:not_a_port");
1173
1174 assert!(result.is_err());
1175 assert!(matches!(
1176 result.unwrap_err(),
1177 ConnectionError::ParseError(msg) if msg.contains("Invalid port")
1178 ));
1179 }
1180
1181 #[test]
1182 fn test_parse_ipv6_invalid_port() {
1183 let result = ConnectionParams::from_str("exasol://user@[::1]:invalid");
1184
1185 assert!(result.is_err());
1186 assert!(matches!(
1187 result.unwrap_err(),
1188 ConnectionError::ParseError(msg) if msg.contains("Invalid port")
1189 ));
1190 }
1191
1192 #[test]
1197 fn test_parse_url_encoded_query_params() {
1198 let params =
1199 ConnectionParams::from_str("exasol://user@localhost?client_name=my%20client").unwrap();
1200
1201 assert_eq!(params.client_name, "my client");
1202 }
1203
1204 #[test]
1205 fn test_parse_auth_without_password() {
1206 let params = ConnectionParams::from_str("exasol://testuser@localhost").unwrap();
1207
1208 assert_eq!(params.username, "testuser");
1209 assert_eq!(params.password(), "");
1210 }
1211
1212 #[test]
1217 fn test_parse_url_with_whitespace_trim() {
1218 let params = ConnectionParams::from_str(" exasol://user@localhost ").unwrap();
1219
1220 assert_eq!(params.host, "localhost");
1221 assert_eq!(params.username, "user");
1222 }
1223
1224 #[test]
1225 fn test_parse_certificate_fingerprint_param() {
1226 let params = "exasol://user:pass@localhost?tls=true&certificate_fingerprint=aabbcc"
1227 .parse::<ConnectionParams>()
1228 .unwrap();
1229 assert_eq!(params.certificate_fingerprint.as_deref(), Some("aabbcc"));
1230 }
1231
1232 #[test]
1233 fn test_parse_certificatefingerprint_alias() {
1234 let params = "exasol://user:pass@localhost?tls=true&certificatefingerprint=ddeeff"
1235 .parse::<ConnectionParams>()
1236 .unwrap();
1237 assert_eq!(params.certificate_fingerprint.as_deref(), Some("ddeeff"));
1238 }
1239}