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