Skip to main content

exarrow_rs/connection/
params.rs

1//! Connection parameter parsing and validation.
2//!
3//! This module handles parsing connection strings and building connection
4//! parameters with validation.
5
6use crate::error::ConnectionError;
7use std::collections::HashMap;
8use std::fmt;
9use std::str::FromStr;
10use std::time::Duration;
11
12/// Connection parameters for establishing a database connection.
13#[derive(Clone)]
14pub struct ConnectionParams {
15    /// Database host address
16    pub host: String,
17
18    /// Database port (default: 8563)
19    pub port: u16,
20
21    /// Username for authentication
22    pub username: String,
23
24    /// Password for authentication (stored securely)
25    password: String,
26
27    /// Optional schema to use after connection
28    pub schema: Option<String>,
29
30    /// Connection timeout
31    pub connection_timeout: Duration,
32
33    /// Query execution timeout
34    pub query_timeout: Duration,
35
36    /// Idle connection timeout
37    pub idle_timeout: Duration,
38
39    /// Enable TLS/SSL encryption
40    pub use_tls: bool,
41
42    /// TLS certificate validation mode
43    pub validate_server_certificate: bool,
44
45    /// Expected SHA-256 hex fingerprint of the server's DER certificate
46    pub certificate_fingerprint: Option<String>,
47
48    /// Client name for session identification
49    pub client_name: String,
50
51    /// Client version
52    pub client_version: String,
53
54    /// Additional connection attributes
55    pub attributes: HashMap<String, String>,
56}
57
58impl ConnectionParams {
59    /// Get the password (for internal use only, never logged).
60    pub(crate) fn password(&self) -> &str {
61        &self.password
62    }
63
64    /// Create a new ConnectionBuilder.
65    pub fn builder() -> ConnectionBuilder {
66        ConnectionBuilder::new()
67    }
68}
69
70impl FromStr for ConnectionParams {
71    type Err = ConnectionError;
72
73    /// Parse a connection string in the format:
74    /// `exasol://username[:password]@host[:port][/schema][?param=value&...]`
75    ///
76    /// # Examples
77    ///
78    fn from_str(s: &str) -> Result<Self, Self::Err> {
79        // Parse the connection string
80        let url = s.trim();
81
82        // Check for exasol:// prefix
83        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..]; // Skip "exasol://"
90
91        // Split into main part and query string
92        let (main_part, query_string) = match url.split_once('?') {
93            Some((main, query)) => (main, Some(query)),
94            None => (url, None),
95        };
96
97        // Parse query parameters
98        let mut params = parse_query_params(query_string)?;
99
100        // Split main part into auth@host/schema
101        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        // Parse authentication
111        let (username, password) = if let Some(auth) = auth_part {
112            parse_auth(auth)?
113        } else {
114            // Check query params for username/password
115            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        // Parse host and schema
127        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        // Parse host and port
140        let (host, port) = parse_host_port(host_port)?;
141
142        // Build connection params
143        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        // Apply query parameters
154        builder = apply_query_params(builder, params)?;
155
156        builder.build()
157    }
158}
159
160// Prevent password from being displayed in debug or display output
161impl 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/// Builder for constructing ConnectionParams with validation.
196#[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    /// Create a new ConnectionBuilder with default values.
216    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    /// Set the database host.
236    pub fn host(mut self, host: &str) -> Self {
237        self.host = Some(host.to_string());
238        self
239    }
240
241    /// Set the database port.
242    pub fn port(mut self, port: u16) -> Self {
243        self.port = Some(port);
244        self
245    }
246
247    /// Set the username.
248    pub fn username(mut self, username: &str) -> Self {
249        self.username = Some(username.to_string());
250        self
251    }
252
253    /// Set the password.
254    pub fn password(mut self, password: &str) -> Self {
255        self.password = Some(password.to_string());
256        self
257    }
258
259    /// Set the default schema.
260    pub fn schema(mut self, schema: &str) -> Self {
261        self.schema = Some(schema.to_string());
262        self
263    }
264
265    /// Set the connection timeout.
266    pub fn connection_timeout(mut self, timeout: Duration) -> Self {
267        self.connection_timeout = Some(timeout);
268        self
269    }
270
271    /// Set the query execution timeout.
272    pub fn query_timeout(mut self, timeout: Duration) -> Self {
273        self.query_timeout = Some(timeout);
274        self
275    }
276
277    /// Set the idle connection timeout.
278    pub fn idle_timeout(mut self, timeout: Duration) -> Self {
279        self.idle_timeout = Some(timeout);
280        self
281    }
282
283    /// Enable or disable TLS/SSL.
284    pub fn use_tls(mut self, use_tls: bool) -> Self {
285        self.use_tls = Some(use_tls);
286        self
287    }
288
289    /// Enable or disable server certificate validation.
290    pub fn validate_server_certificate(mut self, validate: bool) -> Self {
291        self.validate_server_certificate = Some(validate);
292        self
293    }
294
295    /// Pin TLS connection to a specific certificate fingerprint (SHA-256 hex of DER cert).
296    pub fn certificate_fingerprint(mut self, fingerprint: &str) -> Self {
297        self.certificate_fingerprint = Some(fingerprint.to_string());
298        self
299    }
300
301    /// Set the client name.
302    pub fn client_name(mut self, name: &str) -> Self {
303        self.client_name = Some(name.to_string());
304        self
305    }
306
307    /// Set the client version.
308    pub fn client_version(mut self, version: &str) -> Self {
309        self.client_version = Some(version.to_string());
310        self
311    }
312
313    /// Add a custom connection attribute.
314    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    /// Build the ConnectionParams with validation.
320    pub fn build(self) -> Result<ConnectionParams, ConnectionError> {
321        // Validate required fields
322        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        // Validate host is not empty
335        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        // Validate username is not empty
343        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        // Validate port range
353        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        // Validate timeouts
361        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
399/// Parse query parameters from URL query string.
400fn 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            // URL decode the values
420            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
434/// Parse authentication part (username:password).
435fn 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
461/// Parse host and port.
462fn parse_host_port(host_port: &str) -> Result<(String, u16), ConnectionError> {
463    // Check for IPv6 address format [host]:port
464    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    // Regular host:port or just host
482    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
493/// Apply query parameters to builder.
494fn 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                // Store as custom attribute
546                builder = builder.attribute(&key, &value);
547            }
548        }
549    }
550
551    Ok(builder)
552}
553
554/// Parse boolean value from string.
555fn 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        // Debug output should not contain the password
755        assert!(!debug.contains("super_secret"));
756    }
757
758    // ============================================================
759    // Builder validation tests
760    // ============================================================
761
762    #[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    // ============================================================
871    // Query parameter parsing tests
872    // ============================================================
873
874    #[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        // Empty pairs between && should be skipped
888        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    // ============================================================
956    // TLS/SSL parameter tests
957    // ============================================================
958
959    #[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    // ============================================================
1000    // Boolean parsing tests
1001    // ============================================================
1002
1003    #[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    // ============================================================
1061    // Client info parameter tests
1062    // ============================================================
1063
1064    #[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    // ============================================================
1084    // Authentication from query params tests
1085    // ============================================================
1086
1087    #[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    // ============================================================
1126    // IPv6 tests
1127    // ============================================================
1128
1129    #[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    // ============================================================
1147    // Schema edge cases
1148    // ============================================================
1149
1150    #[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    // ============================================================
1167    // Port parsing edge cases
1168    // ============================================================
1169
1170    #[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    // ============================================================
1193    // URL encoding edge cases
1194    // ============================================================
1195
1196    #[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    // ============================================================
1213    // Whitespace handling tests
1214    // ============================================================
1215
1216    #[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}