1mod types;
109pub use types::*;
110
111use std::time::Duration;
112
113use mssql_auth::Credentials;
114#[cfg(feature = "tls")]
115use mssql_tls::TlsConfig;
116use tds_protocol::version::TdsVersion;
117
118fn parse_conn_bool(key: &str, value: &str) -> Result<bool, crate::error::Error> {
124 match value.to_lowercase().as_str() {
125 "true" | "yes" | "1" => Ok(true),
126 "false" | "no" | "0" => Ok(false),
127 _ => Err(crate::error::Error::Config(format!(
128 "invalid boolean value for '{key}': '{value}' (expected true/false/yes/no/1/0)"
129 ))),
130 }
131}
132
133fn split_connection_string(conn_str: &str) -> Result<Vec<(String, String)>, crate::error::Error> {
142 let mut pairs = Vec::new();
143 let chars: Vec<char> = conn_str.chars().collect();
144 let len = chars.len();
145 let mut i = 0;
146
147 while i < len {
148 while i < len && (chars[i] == ';' || chars[i].is_whitespace()) {
150 i += 1;
151 }
152 if i >= len {
153 break;
154 }
155
156 let key_start = i;
158 while i < len && chars[i] != '=' {
159 i += 1;
160 }
161 if i >= len {
162 let remaining = chars[key_start..].iter().collect::<String>();
164 if remaining.trim().is_empty() {
165 break;
166 }
167 return Err(crate::error::Error::Config(format!(
168 "invalid key-value pair (missing '='): '{remaining}'"
169 )));
170 }
171 let key: String = chars[key_start..i].iter().collect();
172 i += 1; while i < len && chars[i].is_whitespace() {
177 i += 1;
178 }
179
180 let value = if i < len && (chars[i] == '"' || chars[i] == '\'') {
181 let quote_char = chars[i];
183 i += 1; let mut val = String::new();
185 loop {
186 if i >= len {
187 return Err(crate::error::Error::Config(format!(
188 "unterminated quoted value for key '{}'",
189 key.trim()
190 )));
191 }
192 if chars[i] == quote_char {
193 if i + 1 < len && chars[i + 1] == quote_char {
195 val.push(quote_char);
196 i += 2;
197 } else {
198 i += 1; break;
200 }
201 } else {
202 val.push(chars[i]);
203 i += 1;
204 }
205 }
206 while i < len && chars[i] != ';' {
208 i += 1;
209 }
210 val
211 } else {
212 let val_start = i;
214 while i < len && chars[i] != ';' {
215 i += 1;
216 }
217 chars[val_start..i].iter().collect::<String>()
218 };
219
220 let key_trimmed = key.trim().to_string();
221 if !key_trimmed.is_empty() {
222 pairs.push((key_trimmed, value));
223 }
224 }
225
226 Ok(pairs)
227}
228
229fn non_empty(value: &str) -> Option<String> {
234 if value.is_empty() {
235 None
236 } else {
237 Some(value.to_string())
238 }
239}
240
241#[derive(Debug, Clone)]
247#[non_exhaustive]
248pub struct Config {
249 pub host: String,
251
252 pub port: u16,
254
255 pub database: Option<String>,
257
258 pub credentials: Credentials,
260
261 #[cfg(feature = "tls")]
263 pub tls: TlsConfig,
264
265 pub application_name: String,
267
268 pub connect_timeout: Duration,
270
271 pub command_timeout: Duration,
273
274 pub max_response_size: usize,
284
285 pub packet_size: u16,
287
288 pub strict_mode: bool,
290
291 pub trust_server_certificate: bool,
293
294 pub instance: Option<String>,
296
297 pub mars: bool,
299
300 pub encrypt: bool,
304
305 pub no_tls: bool,
324
325 pub redirect: RedirectConfig,
327
328 pub retry: RetryPolicy,
330
331 pub timeouts: TimeoutConfig,
333
334 pub tds_version: TdsVersion,
347
348 pub application_intent: ApplicationIntent,
354
355 pub workstation_id: Option<String>,
362
363 pub language: Option<String>,
369
370 pub multi_subnet_failover: bool,
381
382 pub send_string_parameters_as_unicode: bool,
397
398 #[cfg(feature = "always-encrypted")]
410 pub column_encryption: Option<std::sync::Arc<crate::encryption::EncryptionConfig>>,
411}
412
413impl Default for Config {
414 fn default() -> Self {
415 let timeouts = TimeoutConfig::default();
416 Self {
417 host: "localhost".to_string(),
418 port: 1433,
419 database: None,
420 credentials: Credentials::sql_server("", ""),
421 #[cfg(feature = "tls")]
422 tls: TlsConfig::default(),
423 application_name: "mssql-client".to_string(),
424 connect_timeout: timeouts.connect_timeout,
425 command_timeout: timeouts.command_timeout,
426 max_response_size: 0,
427 packet_size: 4096,
428 strict_mode: false,
429 trust_server_certificate: false,
430 instance: None,
431 mars: false,
432 encrypt: true, no_tls: false, redirect: RedirectConfig::default(),
435 retry: RetryPolicy::default(),
436 timeouts,
437 tds_version: TdsVersion::V7_4, application_intent: ApplicationIntent::default(),
439 workstation_id: None,
440 language: None,
441 multi_subnet_failover: false,
442 send_string_parameters_as_unicode: true,
443 #[cfg(feature = "always-encrypted")]
444 column_encryption: None,
445 }
446 }
447}
448
449impl Config {
450 #[must_use]
452 pub fn new() -> Self {
453 Self::default()
454 }
455
456 pub fn from_connection_string(conn_str: &str) -> Result<Self, crate::error::Error> {
467 let mut config = Self::default();
468 let pairs = split_connection_string(conn_str)?;
469
470 let mut authentication: Option<String> = None;
474
475 for (key, value) in &pairs {
476 let key = key.trim().to_lowercase();
477 let value = value.trim();
478
479 match key.as_str() {
480 "server" | "data source" | "addr" | "address" | "network address" | "host" => {
482 let lower_value = value.to_lowercase();
486 let server_value = if lower_value.starts_with("tcp:") {
487 &value[4..]
488 } else if lower_value.starts_with("np:") {
489 return Err(crate::error::Error::Config(
490 "Named Pipes connections (np:) are not supported. Use TCP connections instead."
491 .into(),
492 ));
493 } else if lower_value.starts_with("lpc:") {
494 return Err(crate::error::Error::Config(
495 "Shared Memory connections (lpc:) are not supported. Use TCP connections instead."
496 .into(),
497 ));
498 } else {
499 value
500 };
501
502 if let Some((host, port_or_instance)) = server_value.split_once(',') {
504 config.host = host.to_string();
505 config.port = port_or_instance.trim().parse().map_err(|_| {
506 crate::error::Error::Config(format!("invalid port: {port_or_instance}"))
507 })?;
508 } else if let Some((host, instance)) = server_value.split_once('\\') {
509 config.host = host.to_string();
510 config.instance = non_empty(instance);
511 } else {
512 config.host = server_value.to_string();
513 }
514 }
515 "port" => {
516 config.port = value.parse().map_err(|_| {
517 crate::error::Error::Config(format!("invalid port: {value}"))
518 })?;
519 }
520 "database" | "initial catalog" => {
522 config.database = non_empty(value);
523 }
524 "user id" | "uid" | "user" => {
526 if let Credentials::SqlServer { password, .. } = &config.credentials {
527 config.credentials =
528 Credentials::sql_server(value.to_string(), password.clone());
529 }
530 }
531 "password" | "pwd" => {
532 if let Credentials::SqlServer { username, .. } = &config.credentials {
533 config.credentials =
534 Credentials::sql_server(username.clone(), value.to_string());
535 }
536 }
537 "authentication" => {
541 authentication = non_empty(value).map(|v| v.to_lowercase().replace(' ', ""));
542 }
543 "application name" | "app" => {
545 config.application_name = value.to_string();
546 }
547 "applicationintent" | "application intent" => {
548 config.application_intent = match value.to_lowercase().as_str() {
549 "readonly" => ApplicationIntent::ReadOnly,
550 "readwrite" => ApplicationIntent::ReadWrite,
551 _ => {
552 return Err(crate::error::Error::Config(format!(
553 "invalid ApplicationIntent: '{value}' (expected ReadOnly or ReadWrite)"
554 )));
555 }
556 };
557 }
558 "workstation id" | "wsid" => {
559 config.workstation_id = non_empty(value);
560 }
561 "current language" | "language" => {
562 config.language = non_empty(value);
563 }
564 "connect timeout" | "connection timeout" | "timeout" => {
566 let secs: u64 = value.parse().map_err(|_| {
567 crate::error::Error::Config(format!("invalid timeout: {value}"))
568 })?;
569 config.connect_timeout = Duration::from_secs(secs);
570 }
571 "command timeout" => {
572 let secs: u64 = value.parse().map_err(|_| {
573 crate::error::Error::Config(format!("invalid timeout: {value}"))
574 })?;
575 config.command_timeout = Duration::from_secs(secs);
576 }
577 "trustservercertificate" | "trust server certificate" => {
579 config.trust_server_certificate = parse_conn_bool(&key, value)?;
580 }
581 "encrypt" => {
582 if value.eq_ignore_ascii_case("strict") {
590 config.strict_mode = true;
591 config.encrypt = true;
592 config.no_tls = false;
593 } else if value.eq_ignore_ascii_case("mandatory") {
594 config.encrypt = true;
595 config.no_tls = false;
596 } else if value.eq_ignore_ascii_case("optional") {
597 config.encrypt = false;
598 config.no_tls = false;
599 } else if value.eq_ignore_ascii_case("no_tls") {
600 config.no_tls = true;
601 config.encrypt = false;
602 } else {
603 let enabled = parse_conn_bool(&key, value)?;
605 config.encrypt = enabled;
606 config.no_tls = false;
607 }
608 }
609 "integrated security" | "trusted_connection" => {
610 let enabled =
612 value.eq_ignore_ascii_case("sspi") || parse_conn_bool(&key, value)?;
613 if enabled {
614 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
615 {
616 config.credentials = Credentials::Integrated;
617 }
618 #[cfg(not(any(feature = "integrated-auth", feature = "sspi-auth")))]
619 {
620 return Err(crate::error::Error::Config(
621 "Integrated Security requires the 'integrated-auth' (Linux/macOS) \
622 or 'sspi-auth' (Windows) feature to be enabled"
623 .into(),
624 ));
625 }
626 }
627 }
628 "column encryption setting" | "columnencryptionsetting" => {
630 #[cfg(feature = "always-encrypted")]
631 if value.eq_ignore_ascii_case("enabled") {
632 config.column_encryption = Some(std::sync::Arc::new(
633 crate::encryption::EncryptionConfig::new(),
634 ));
635 }
636 #[cfg(not(feature = "always-encrypted"))]
637 if value.eq_ignore_ascii_case("enabled") {
638 return Err(crate::error::Error::Config(
639 "Column Encryption Setting=Enabled requires the 'always-encrypted' feature. \
640 Enable it in your Cargo.toml: mssql-client = { features = [\"always-encrypted\"] }"
641 .to_string(),
642 ));
643 }
644 }
645 "multipleactiveresultsets" | "mars" => {
647 config.mars = parse_conn_bool(&key, value)?;
648 }
649 "packet size" => {
650 config.packet_size = value.parse().map_err(|_| {
651 crate::error::Error::Config(format!("invalid packet size: {value}"))
652 })?;
653 }
654 "tdsversion" | "tds version" | "protocolversion" | "protocol version" => {
655 config.tds_version = TdsVersion::parse(value).ok_or_else(|| {
656 crate::error::Error::Config(format!(
657 "invalid TDS version: {value}. Supported values: 7.3, 7.3A, 7.3B, 7.4, 8.0"
658 ))
659 })?;
660 if config.tds_version.is_tds_8() {
661 config.strict_mode = true;
662 }
663 }
664 "connectretrycount" | "connect retry count" => {
666 config.retry.max_retries = value.parse().map_err(|_| {
667 crate::error::Error::Config(format!("invalid ConnectRetryCount: '{value}'"))
668 })?;
669 }
670 "connectretryinterval" | "connect retry interval" => {
671 let secs: u64 = value.parse().map_err(|_| {
672 crate::error::Error::Config(format!(
673 "invalid ConnectRetryInterval: '{value}'"
674 ))
675 })?;
676 config.retry.initial_backoff = Duration::from_secs(secs);
677 }
678 "max pool size"
680 | "min pool size"
681 | "pooling"
682 | "connection lifetime"
683 | "load balance timeout" => {
684 tracing::info!(
685 key = key.as_str(),
686 value = value,
687 "connection string keyword '{}' is recognized but pool settings \
688 must be configured via PoolConfig, not the connection string",
689 key,
690 );
691 }
692 "multisubnetfailover" | "multi subnet failover" => {
694 config.multi_subnet_failover = parse_conn_bool(&key, value)?;
695 }
696 "sendstringparametersasunicode" | "send string parameters as unicode" => {
698 config.send_string_parameters_as_unicode = parse_conn_bool(&key, value)?;
699 }
700 "failover partner"
702 | "persist security info"
703 | "persistsecurityinfo"
704 | "enlist"
705 | "replication"
706 | "transaction binding"
707 | "type system version"
708 | "user instance"
709 | "attachdbfilename"
710 | "extended properties"
711 | "initial file name"
712 | "context connection"
713 | "network library"
714 | "network"
715 | "net"
716 | "asynchronous processing"
717 | "async"
718 | "transparentnetworkipresolution"
719 | "poolblockingperiod"
720 | "hostnameincertificate"
721 | "servercertificate" => {
722 tracing::info!(
723 key = key.as_str(),
724 value = value,
725 "connection string keyword '{}' is recognized but not supported by this driver",
726 key,
727 );
728 }
729 _ => {
730 tracing::debug!(
731 key = key.as_str(),
732 value = value,
733 "ignoring unknown connection string option"
734 );
735 }
736 }
737 }
738
739 if let Some(method) = authentication {
740 config.apply_authentication_keyword(&method)?;
741 }
742
743 Ok(config)
744 }
745
746 fn apply_authentication_keyword(&mut self, method: &str) -> Result<(), crate::error::Error> {
752 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
755 if matches!(self.credentials, Credentials::Integrated) {
756 return Err(crate::error::Error::Config(
757 "the Authentication keyword cannot be combined with Integrated Security".into(),
758 ));
759 }
760
761 match method {
762 "sqlpassword" => {}
765 "activedirectorymanagedidentity" | "activedirectorymsi" => {
766 #[cfg(not(feature = "azure-identity"))]
767 return Err(crate::error::Error::Config(format!(
768 "Authentication={method} requires the 'azure-identity' feature. \
769 Enable it in your Cargo.toml: \
770 mssql-client = {{ features = [\"azure-identity\"] }}"
771 )));
772 #[cfg(feature = "azure-identity")]
773 {
774 let client_id = match &self.credentials {
776 Credentials::SqlServer { username, .. } if !username.is_empty() => {
777 Some(username.clone())
778 }
779 _ => None,
780 };
781 self.credentials = Credentials::AzureManagedIdentity { client_id };
782 }
783 }
784 "activedirectoryserviceprincipal" => {
785 #[cfg(not(feature = "azure-identity"))]
786 return Err(crate::error::Error::Config(format!(
787 "Authentication={method} requires the 'azure-identity' feature. \
788 Enable it in your Cargo.toml: \
789 mssql-client = {{ features = [\"azure-identity\"] }}"
790 )));
791 #[cfg(feature = "azure-identity")]
792 {
793 let Credentials::SqlServer { username, password } = &self.credentials else {
794 return Err(crate::error::Error::Config(
795 "Authentication=ActiveDirectoryServicePrincipal requires \
796 User Id and Password (client id and secret)"
797 .into(),
798 ));
799 };
800 let Some((client_id, tenant_id)) = username.split_once('@') else {
804 return Err(crate::error::Error::Config(
805 "Authentication=ActiveDirectoryServicePrincipal requires \
806 User Id=<client-id>@<tenant-id> (the tenant id is needed \
807 for client-side token acquisition)"
808 .into(),
809 ));
810 };
811 if client_id.is_empty() || tenant_id.is_empty() {
812 return Err(crate::error::Error::Config(
813 "Authentication=ActiveDirectoryServicePrincipal: client id \
814 and tenant id must both be non-empty in \
815 User Id=<client-id>@<tenant-id>"
816 .into(),
817 ));
818 }
819 if password.is_empty() {
820 return Err(crate::error::Error::Config(
821 "Authentication=ActiveDirectoryServicePrincipal requires \
822 Password=<client secret>"
823 .into(),
824 ));
825 }
826 let (client_id, tenant_id) = (client_id.to_string(), tenant_id.to_string());
827 let client_secret = password.clone();
828 self.credentials = Credentials::AzureServicePrincipal {
829 tenant_id: tenant_id.into(),
830 client_id: client_id.into(),
831 client_secret,
832 };
833 }
834 }
835 "activedirectorypassword"
836 | "activedirectoryintegrated"
837 | "activedirectoryinteractive"
838 | "activedirectorydefault"
839 | "activedirectorydevicecodeflow" => {
840 return Err(crate::error::Error::Config(format!(
841 "Authentication value '{method}' is not supported. Supported values: \
842 SqlPassword, ActiveDirectoryServicePrincipal, \
843 ActiveDirectoryManagedIdentity (alias ActiveDirectoryMSI). The remaining \
844 ADAL/MSAL workflows are tracked in \
845 https://github.com/praxiomlabs/rust-mssql-driver/issues/155"
846 )));
847 }
848 other => {
849 return Err(crate::error::Error::Config(format!(
850 "invalid Authentication value: '{other}'. Supported values: \
851 SqlPassword, ActiveDirectoryServicePrincipal, \
852 ActiveDirectoryManagedIdentity (alias ActiveDirectoryMSI)"
853 )));
854 }
855 }
856
857 Ok(())
858 }
859
860 #[must_use]
862 pub fn host(mut self, host: impl Into<String>) -> Self {
863 self.host = host.into();
864 self
865 }
866
867 #[must_use]
869 pub fn port(mut self, port: u16) -> Self {
870 self.port = port;
871 self
872 }
873
874 #[must_use]
876 pub fn database(mut self, database: impl Into<String>) -> Self {
877 self.database = Some(database.into());
878 self
879 }
880
881 #[must_use]
883 pub fn credentials(mut self, credentials: Credentials) -> Self {
884 self.credentials = credentials;
885 self
886 }
887
888 #[must_use]
890 pub fn application_name(mut self, name: impl Into<String>) -> Self {
891 self.application_name = name.into();
892 self
893 }
894
895 #[must_use]
897 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
898 self.connect_timeout = timeout;
899 self
900 }
901
902 #[must_use]
904 pub fn trust_server_certificate(mut self, trust: bool) -> Self {
905 self.trust_server_certificate = trust;
906 #[cfg(feature = "tls")]
907 {
908 self.tls = self.tls.trust_server_certificate(trust);
909 }
910 self
911 }
912
913 #[must_use]
915 pub fn strict_mode(mut self, enabled: bool) -> Self {
916 self.strict_mode = enabled;
917 #[cfg(feature = "tls")]
918 {
919 self.tls = self.tls.strict_mode(enabled);
920 }
921 if enabled {
922 self.tds_version = TdsVersion::V8_0;
923 }
924 self
925 }
926
927 #[must_use]
951 pub fn tds_version(mut self, version: TdsVersion) -> Self {
952 self.tds_version = version;
953 if version.is_tds_8() {
955 self.strict_mode = true;
956 #[cfg(feature = "tls")]
957 {
958 self.tls = self.tls.strict_mode(true);
959 }
960 }
961 self
962 }
963
964 #[must_use]
972 pub fn encrypt(mut self, enabled: bool) -> Self {
973 self.encrypt = enabled;
974 self
975 }
976
977 #[must_use]
1018 pub fn no_tls(mut self, enabled: bool) -> Self {
1019 self.no_tls = enabled;
1020 if enabled {
1021 self.encrypt = false;
1022 }
1023 self
1024 }
1025
1026 #[cfg(feature = "always-encrypted")]
1048 #[must_use]
1049 pub fn with_column_encryption(mut self, config: crate::encryption::EncryptionConfig) -> Self {
1050 self.column_encryption = Some(std::sync::Arc::new(config));
1051 self
1052 }
1053
1054 #[must_use]
1056 pub fn with_host(mut self, host: &str) -> Self {
1057 self.host = host.to_string();
1058 self
1059 }
1060
1061 #[must_use]
1063 pub fn with_port(mut self, port: u16) -> Self {
1064 self.port = port;
1065 self
1066 }
1067
1068 #[must_use]
1070 pub fn redirect(mut self, redirect: RedirectConfig) -> Self {
1071 self.redirect = redirect;
1072 self
1073 }
1074
1075 #[must_use]
1077 pub fn max_redirects(mut self, max: u8) -> Self {
1078 self.redirect.max_redirects = max;
1079 self
1080 }
1081
1082 #[must_use]
1084 pub fn retry(mut self, retry: RetryPolicy) -> Self {
1085 self.retry = retry;
1086 self
1087 }
1088
1089 #[must_use]
1091 pub fn max_retries(mut self, max: u32) -> Self {
1092 self.retry.max_retries = max;
1093 self
1094 }
1095
1096 #[must_use]
1098 pub fn timeouts(mut self, timeouts: TimeoutConfig) -> Self {
1099 self.connect_timeout = timeouts.connect_timeout;
1101 self.command_timeout = timeouts.command_timeout;
1102 self.timeouts = timeouts;
1103 self
1104 }
1105
1106 #[must_use]
1108 pub fn application_intent(mut self, intent: ApplicationIntent) -> Self {
1109 self.application_intent = intent;
1110 self
1111 }
1112
1113 #[must_use]
1118 pub fn workstation_id(mut self, id: impl Into<String>) -> Self {
1119 self.workstation_id = Some(id.into());
1120 self
1121 }
1122
1123 #[must_use]
1127 pub fn language(mut self, lang: impl Into<String>) -> Self {
1128 self.language = Some(lang.into());
1129 self
1130 }
1131
1132 #[must_use]
1137 pub fn multi_subnet_failover(mut self, enabled: bool) -> Self {
1138 self.multi_subnet_failover = enabled;
1139 self
1140 }
1141
1142 #[must_use]
1150 pub fn send_string_parameters_as_unicode(mut self, enabled: bool) -> Self {
1151 self.send_string_parameters_as_unicode = enabled;
1152 self
1153 }
1154}
1155
1156#[cfg(test)]
1157#[allow(clippy::unwrap_used)]
1158mod tests {
1159 use super::*;
1160
1161 #[test]
1162 fn test_connection_string_parsing() {
1163 let config = Config::from_connection_string(
1164 "Server=localhost;Database=test;User Id=sa;Password=secret;",
1165 )
1166 .unwrap();
1167
1168 assert_eq!(config.host, "localhost");
1169 assert_eq!(config.database, Some("test".to_string()));
1170 }
1171
1172 #[test]
1173 fn test_connection_string_with_port() {
1174 let config =
1175 Config::from_connection_string("Server=localhost,1434;Database=test;").unwrap();
1176
1177 assert_eq!(config.host, "localhost");
1178 assert_eq!(config.port, 1434);
1179 }
1180
1181 #[test]
1182 fn test_connection_string_with_instance() {
1183 let config =
1184 Config::from_connection_string("Server=localhost\\SQLEXPRESS;Database=test;").unwrap();
1185
1186 assert_eq!(config.host, "localhost");
1187 assert_eq!(config.instance, Some("SQLEXPRESS".to_string()));
1188 }
1189
1190 #[test]
1191 fn test_connection_string_dot_instance() {
1192 let config = Config::from_connection_string("Server=.\\SQLEXPRESS;Database=test;").unwrap();
1194
1195 assert_eq!(config.host, ".");
1196 assert_eq!(config.instance, Some("SQLEXPRESS".to_string()));
1197 }
1198
1199 #[test]
1200 fn test_connection_string_local_instance() {
1201 let config =
1203 Config::from_connection_string("Server=(local)\\SQLEXPRESS;Database=test;").unwrap();
1204
1205 assert_eq!(config.host, "(local)");
1206 assert_eq!(config.instance, Some("SQLEXPRESS".to_string()));
1207 }
1208
1209 #[test]
1210 fn test_redirect_config_defaults() {
1211 let config = RedirectConfig::default();
1212 assert_eq!(config.max_redirects, 2);
1213 assert!(config.follow_redirects);
1214 }
1215
1216 #[test]
1217 fn test_redirect_config_builder() {
1218 let config = RedirectConfig::new()
1219 .max_redirects(5)
1220 .follow_redirects(false);
1221 assert_eq!(config.max_redirects, 5);
1222 assert!(!config.follow_redirects);
1223 }
1224
1225 #[test]
1226 fn test_redirect_config_no_follow() {
1227 let config = RedirectConfig::no_follow();
1228 assert_eq!(config.max_redirects, 0);
1229 assert!(!config.follow_redirects);
1230 }
1231
1232 #[test]
1233 fn test_config_redirect_builder() {
1234 let config = Config::new().max_redirects(3);
1235 assert_eq!(config.redirect.max_redirects, 3);
1236
1237 let config2 = Config::new().redirect(RedirectConfig::no_follow());
1238 assert!(!config2.redirect.follow_redirects);
1239 }
1240
1241 #[test]
1242 fn test_retry_policy_defaults() {
1243 let policy = RetryPolicy::default();
1244 assert_eq!(policy.max_retries, 3);
1245 assert_eq!(policy.initial_backoff, Duration::from_millis(100));
1246 assert_eq!(policy.max_backoff, Duration::from_secs(30));
1247 assert!((policy.backoff_multiplier - 2.0).abs() < f64::EPSILON);
1248 assert!(policy.jitter);
1249 }
1250
1251 #[test]
1252 fn test_retry_policy_builder() {
1253 let policy = RetryPolicy::new()
1254 .max_retries(5)
1255 .initial_backoff(Duration::from_millis(200))
1256 .max_backoff(Duration::from_secs(60))
1257 .backoff_multiplier(3.0)
1258 .jitter(false);
1259
1260 assert_eq!(policy.max_retries, 5);
1261 assert_eq!(policy.initial_backoff, Duration::from_millis(200));
1262 assert_eq!(policy.max_backoff, Duration::from_secs(60));
1263 assert!((policy.backoff_multiplier - 3.0).abs() < f64::EPSILON);
1264 assert!(!policy.jitter);
1265 }
1266
1267 #[test]
1268 fn test_retry_policy_no_retry() {
1269 let policy = RetryPolicy::no_retry();
1270 assert_eq!(policy.max_retries, 0);
1271 assert!(!policy.should_retry(0));
1272 }
1273
1274 #[test]
1275 fn test_retry_policy_should_retry() {
1276 let policy = RetryPolicy::new().max_retries(3);
1277 assert!(policy.should_retry(0));
1278 assert!(policy.should_retry(1));
1279 assert!(policy.should_retry(2));
1280 assert!(!policy.should_retry(3));
1281 assert!(!policy.should_retry(4));
1282 }
1283
1284 #[test]
1285 fn test_retry_policy_backoff_calculation() {
1286 let policy = RetryPolicy::new()
1287 .initial_backoff(Duration::from_millis(100))
1288 .backoff_multiplier(2.0)
1289 .max_backoff(Duration::from_secs(10))
1290 .jitter(false);
1291
1292 assert_eq!(policy.backoff_for_attempt(0), Duration::ZERO);
1293 assert_eq!(policy.backoff_for_attempt(1), Duration::from_millis(100));
1294 assert_eq!(policy.backoff_for_attempt(2), Duration::from_millis(200));
1295 assert_eq!(policy.backoff_for_attempt(3), Duration::from_millis(400));
1296 }
1297
1298 #[test]
1299 fn test_retry_policy_backoff_capped() {
1300 let policy = RetryPolicy::new()
1301 .initial_backoff(Duration::from_secs(1))
1302 .backoff_multiplier(10.0)
1303 .max_backoff(Duration::from_secs(5))
1304 .jitter(false);
1305
1306 assert_eq!(policy.backoff_for_attempt(3), Duration::from_secs(5));
1308 }
1309
1310 #[test]
1311 fn test_config_retry_builder() {
1312 let config = Config::new().max_retries(5);
1313 assert_eq!(config.retry.max_retries, 5);
1314
1315 let config2 = Config::new().retry(RetryPolicy::no_retry());
1316 assert_eq!(config2.retry.max_retries, 0);
1317 }
1318
1319 #[test]
1320 fn test_timeout_config_defaults() {
1321 let config = TimeoutConfig::default();
1322 assert_eq!(config.connect_timeout, Duration::from_secs(15));
1323 assert_eq!(config.tls_timeout, Duration::from_secs(10));
1324 assert_eq!(config.login_timeout, Duration::from_secs(30));
1325 assert_eq!(config.command_timeout, Duration::from_secs(30));
1326 assert_eq!(config.idle_timeout, Duration::from_secs(300));
1327 assert_eq!(config.keepalive_interval, Some(Duration::from_secs(30)));
1328 }
1329
1330 #[test]
1331 fn test_timeout_config_builder() {
1332 let config = TimeoutConfig::new()
1333 .connect_timeout(Duration::from_secs(5))
1334 .tls_timeout(Duration::from_secs(3))
1335 .login_timeout(Duration::from_secs(10))
1336 .command_timeout(Duration::from_secs(60))
1337 .idle_timeout(Duration::from_secs(600))
1338 .keepalive_interval(Some(Duration::from_secs(60)));
1339
1340 assert_eq!(config.connect_timeout, Duration::from_secs(5));
1341 assert_eq!(config.tls_timeout, Duration::from_secs(3));
1342 assert_eq!(config.login_timeout, Duration::from_secs(10));
1343 assert_eq!(config.command_timeout, Duration::from_secs(60));
1344 assert_eq!(config.idle_timeout, Duration::from_secs(600));
1345 assert_eq!(config.keepalive_interval, Some(Duration::from_secs(60)));
1346 }
1347
1348 #[test]
1349 fn test_timeout_config_no_keepalive() {
1350 let config = TimeoutConfig::new().no_keepalive();
1351 assert_eq!(config.keepalive_interval, None);
1352 }
1353
1354 #[test]
1355 fn test_timeout_config_total_connect() {
1356 let config = TimeoutConfig::new()
1357 .connect_timeout(Duration::from_secs(5))
1358 .tls_timeout(Duration::from_secs(3))
1359 .login_timeout(Duration::from_secs(10));
1360
1361 assert_eq!(config.total_connect_timeout(), Duration::from_secs(18));
1363 }
1364
1365 #[test]
1366 fn test_config_timeouts_builder() {
1367 let timeouts = TimeoutConfig::new()
1368 .connect_timeout(Duration::from_secs(5))
1369 .command_timeout(Duration::from_secs(60));
1370
1371 let config = Config::new().timeouts(timeouts);
1372 assert_eq!(config.timeouts.connect_timeout, Duration::from_secs(5));
1373 assert_eq!(config.timeouts.command_timeout, Duration::from_secs(60));
1374 assert_eq!(config.connect_timeout, Duration::from_secs(5));
1376 assert_eq!(config.command_timeout, Duration::from_secs(60));
1377 }
1378
1379 #[test]
1380 fn test_tds_version_default() {
1381 let config = Config::default();
1382 assert_eq!(config.tds_version, TdsVersion::V7_4);
1383 assert!(!config.strict_mode);
1384 }
1385
1386 #[test]
1387 fn test_tds_version_builder() {
1388 let config = Config::new().tds_version(TdsVersion::V7_3A);
1389 assert_eq!(config.tds_version, TdsVersion::V7_3A);
1390 assert!(!config.strict_mode);
1391
1392 let config = Config::new().tds_version(TdsVersion::V7_3B);
1393 assert_eq!(config.tds_version, TdsVersion::V7_3B);
1394 assert!(!config.strict_mode);
1395
1396 let config = Config::new().tds_version(TdsVersion::V8_0);
1398 assert_eq!(config.tds_version, TdsVersion::V8_0);
1399 assert!(config.strict_mode);
1400 }
1401
1402 #[test]
1403 fn test_strict_mode_sets_tds_8() {
1404 let config = Config::new().strict_mode(true);
1405 assert!(config.strict_mode);
1406 assert_eq!(config.tds_version, TdsVersion::V8_0);
1407 }
1408
1409 #[test]
1410 fn test_connection_string_tds_version() {
1411 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3;").unwrap();
1413 assert_eq!(config.tds_version, TdsVersion::V7_3A);
1414
1415 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3A;").unwrap();
1417 assert_eq!(config.tds_version, TdsVersion::V7_3A);
1418
1419 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.3B;").unwrap();
1421 assert_eq!(config.tds_version, TdsVersion::V7_3B);
1422
1423 let config = Config::from_connection_string("Server=localhost;TDSVersion=7.4;").unwrap();
1425 assert_eq!(config.tds_version, TdsVersion::V7_4);
1426
1427 let config = Config::from_connection_string("Server=localhost;TDSVersion=8.0;").unwrap();
1429 assert_eq!(config.tds_version, TdsVersion::V8_0);
1430 assert!(config.strict_mode);
1431
1432 let config =
1434 Config::from_connection_string("Server=localhost;ProtocolVersion=7.3;").unwrap();
1435 assert_eq!(config.tds_version, TdsVersion::V7_3A);
1436 }
1437
1438 #[test]
1439 fn test_connection_string_invalid_tds_version() {
1440 let result = Config::from_connection_string("Server=localhost;TDSVersion=invalid;");
1441 assert!(result.is_err());
1442
1443 let result = Config::from_connection_string("Server=localhost;TDSVersion=9.0;");
1444 assert!(result.is_err());
1445 }
1446
1447 #[test]
1448 fn test_connection_string_no_tls() {
1449 let config = Config::from_connection_string("Server=legacy;Encrypt=no_tls;").unwrap();
1451 assert!(config.no_tls);
1452 assert!(!config.encrypt);
1453 assert!(!config.strict_mode);
1454
1455 let config = Config::from_connection_string("Server=legacy;Encrypt=no_tls;").unwrap();
1457 assert!(config.no_tls);
1458
1459 let config = Config::from_connection_string("Server=localhost;Encrypt=true;").unwrap();
1461 assert!(!config.no_tls);
1462 assert!(config.encrypt);
1463
1464 let config = Config::from_connection_string("Server=localhost;Encrypt=strict;").unwrap();
1466 assert!(!config.no_tls);
1467 assert!(config.encrypt);
1468 assert!(config.strict_mode);
1469
1470 let config = Config::from_connection_string("Server=localhost;Encrypt=mandatory;").unwrap();
1472 assert!(config.encrypt);
1473 assert!(!config.no_tls);
1474
1475 let config = Config::from_connection_string("Server=localhost;Encrypt=optional;").unwrap();
1477 assert!(!config.encrypt);
1478 assert!(!config.no_tls);
1479 }
1480
1481 #[test]
1482 fn test_no_tls_builder() {
1483 let config = Config::new().no_tls(true);
1485 assert!(config.no_tls);
1486 assert!(!config.encrypt);
1487
1488 let config = Config::new().no_tls(true).no_tls(false);
1490 assert!(!config.no_tls);
1491 }
1492
1493 #[test]
1494 #[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
1495 fn test_connection_string_integrated_security() {
1496 let config =
1498 Config::from_connection_string("Server=localhost;Integrated Security=true;").unwrap();
1499 assert_eq!(
1500 config.credentials.method_name(),
1501 "Integrated Authentication"
1502 );
1503
1504 let config =
1506 Config::from_connection_string("Server=localhost;Integrated Security=yes;").unwrap();
1507 assert_eq!(
1508 config.credentials.method_name(),
1509 "Integrated Authentication"
1510 );
1511
1512 let config =
1514 Config::from_connection_string("Server=localhost;Integrated Security=sspi;").unwrap();
1515 assert_eq!(
1516 config.credentials.method_name(),
1517 "Integrated Authentication"
1518 );
1519
1520 let config =
1522 Config::from_connection_string("Server=localhost;Integrated Security=1;").unwrap();
1523 assert_eq!(
1524 config.credentials.method_name(),
1525 "Integrated Authentication"
1526 );
1527
1528 let config =
1530 Config::from_connection_string("Server=localhost;Trusted_Connection=true;").unwrap();
1531 assert_eq!(
1532 config.credentials.method_name(),
1533 "Integrated Authentication"
1534 );
1535 }
1536
1537 #[test]
1538 #[cfg(not(any(feature = "integrated-auth", feature = "sspi-auth")))]
1539 fn test_connection_string_integrated_security_without_feature() {
1540 let result = Config::from_connection_string("Server=localhost;Integrated Security=true;");
1542 assert!(result.is_err());
1543 let err = result.unwrap_err().to_string();
1544 assert!(err.contains("integrated-auth"));
1545 }
1546
1547 #[test]
1552 fn test_parse_conn_bool_all_values() {
1553 assert!(parse_conn_bool("test", "true").unwrap());
1554 assert!(parse_conn_bool("test", "True").unwrap());
1555 assert!(parse_conn_bool("test", "TRUE").unwrap());
1556 assert!(parse_conn_bool("test", "yes").unwrap());
1557 assert!(parse_conn_bool("test", "Yes").unwrap());
1558 assert!(parse_conn_bool("test", "1").unwrap());
1559
1560 assert!(!parse_conn_bool("test", "false").unwrap());
1561 assert!(!parse_conn_bool("test", "False").unwrap());
1562 assert!(!parse_conn_bool("test", "FALSE").unwrap());
1563 assert!(!parse_conn_bool("test", "no").unwrap());
1564 assert!(!parse_conn_bool("test", "No").unwrap());
1565 assert!(!parse_conn_bool("test", "0").unwrap());
1566
1567 assert!(parse_conn_bool("test", "banana").is_err());
1569 assert!(parse_conn_bool("test", "tru").is_err());
1570 assert!(parse_conn_bool("test", "").is_err());
1571 }
1572
1573 #[test]
1574 fn test_boolean_validation_trust_server_certificate() {
1575 let config =
1577 Config::from_connection_string("Server=localhost;TrustServerCertificate=true;")
1578 .unwrap();
1579 assert!(config.trust_server_certificate);
1580
1581 let config =
1582 Config::from_connection_string("Server=localhost;TrustServerCertificate=no;").unwrap();
1583 assert!(!config.trust_server_certificate);
1584
1585 let result =
1587 Config::from_connection_string("Server=localhost;TrustServerCertificate=banana;");
1588 assert!(result.is_err());
1589 assert!(result.unwrap_err().to_string().contains("invalid boolean"));
1590 }
1591
1592 #[test]
1593 fn test_boolean_validation_mars() {
1594 let config = Config::from_connection_string("Server=localhost;MARS=true;").unwrap();
1595 assert!(config.mars);
1596
1597 let result = Config::from_connection_string("Server=localhost;MARS=tru;");
1599 assert!(result.is_err());
1600 }
1601
1602 #[test]
1603 fn test_quoted_value_semicolon() {
1604 let config = Config::from_connection_string(
1606 r#"Server=localhost;User Id=sa;Password="my;complex;pass";"#,
1607 )
1608 .unwrap();
1609 if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1610 assert_eq!(password.as_ref(), "my;complex;pass");
1611 } else {
1612 unreachable!("expected SqlServer credentials");
1613 }
1614 }
1615
1616 #[test]
1617 fn test_quoted_value_single_quotes() {
1618 let config =
1619 Config::from_connection_string("Server=localhost;User Id=sa;Password='my;pass';")
1620 .unwrap();
1621 if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1622 assert_eq!(password.as_ref(), "my;pass");
1623 } else {
1624 unreachable!("expected SqlServer credentials");
1625 }
1626 }
1627
1628 #[test]
1629 fn test_quoted_value_escaped_double_quotes() {
1630 let config = Config::from_connection_string(
1632 r#"Server=localhost;User Id=sa;Password="has ""quotes""";"#,
1633 )
1634 .unwrap();
1635 if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1636 assert_eq!(password.as_ref(), r#"has "quotes""#);
1637 } else {
1638 unreachable!("expected SqlServer credentials");
1639 }
1640 }
1641
1642 #[test]
1643 fn test_quoted_value_escaped_single_quotes() {
1644 let config =
1645 Config::from_connection_string("Server=localhost;User Id=sa;Password='it''s complex';")
1646 .unwrap();
1647 if let mssql_auth::Credentials::SqlServer { password, .. } = &config.credentials {
1648 assert_eq!(password.as_ref(), "it's complex");
1649 } else {
1650 unreachable!("expected SqlServer credentials");
1651 }
1652 }
1653
1654 #[test]
1655 fn test_quoted_value_unterminated() {
1656 let result = Config::from_connection_string(r#"Server=localhost;Password="unterminated;"#);
1657 assert!(result.is_err());
1658 assert!(result.unwrap_err().to_string().contains("unterminated"));
1659 }
1660
1661 #[test]
1662 fn test_tcp_prefix_stripped() {
1663 let config = Config::from_connection_string(
1665 "Server=tcp:myserver.database.windows.net,1433;Database=mydb;",
1666 )
1667 .unwrap();
1668 assert_eq!(config.host, "myserver.database.windows.net");
1669 assert_eq!(config.port, 1433);
1670 }
1671
1672 #[test]
1673 fn test_tcp_prefix_mixed_case() {
1674 let config = Config::from_connection_string("Server=Tcp:myhost,1433;").unwrap();
1676 assert_eq!(config.host, "myhost");
1677
1678 let config = Config::from_connection_string("Server=TCP:myhost,1433;").unwrap();
1679 assert_eq!(config.host, "myhost");
1680 }
1681
1682 #[test]
1683 fn test_tcp_prefix_with_instance() {
1684 let config =
1685 Config::from_connection_string("Server=tcp:myhost\\INST;Database=test;").unwrap();
1686 assert_eq!(config.host, "myhost");
1687 assert_eq!(config.instance, Some("INST".to_string()));
1688 }
1689
1690 #[test]
1691 fn test_np_prefix_rejected() {
1692 let result =
1693 Config::from_connection_string(r"Server=np:\\myhost\pipe\sql\query;Database=test;");
1694 assert!(result.is_err());
1695 assert!(result.unwrap_err().to_string().contains("Named Pipes"));
1696
1697 let result =
1699 Config::from_connection_string(r"Server=NP:\\myhost\pipe\sql\query;Database=test;");
1700 assert!(result.is_err());
1701 }
1702
1703 #[test]
1704 fn test_lpc_prefix_rejected() {
1705 let result = Config::from_connection_string("Server=lpc:myhost;Database=test;");
1706 assert!(result.is_err());
1707 assert!(result.unwrap_err().to_string().contains("Shared Memory"));
1708 }
1709
1710 #[test]
1711 fn test_server_alias_addr() {
1712 let config = Config::from_connection_string("Addr=myhost;").unwrap();
1713 assert_eq!(config.host, "myhost");
1714 }
1715
1716 #[test]
1717 fn test_server_alias_address() {
1718 let config = Config::from_connection_string("Address=myhost,1434;").unwrap();
1719 assert_eq!(config.host, "myhost");
1720 assert_eq!(config.port, 1434);
1721 }
1722
1723 #[test]
1724 fn test_server_alias_network_address() {
1725 let config = Config::from_connection_string("Network Address=myhost;").unwrap();
1726 assert_eq!(config.host, "myhost");
1727 }
1728
1729 #[test]
1730 fn test_timeout_alias() {
1731 let config = Config::from_connection_string("Server=localhost;Timeout=30;").unwrap();
1732 assert_eq!(config.connect_timeout, Duration::from_secs(30));
1733 }
1734
1735 #[test]
1736 fn test_application_intent_readonly() {
1737 let config =
1738 Config::from_connection_string("Server=localhost;ApplicationIntent=ReadOnly;").unwrap();
1739 assert_eq!(config.application_intent, ApplicationIntent::ReadOnly);
1740 }
1741
1742 #[test]
1743 fn test_application_intent_readwrite() {
1744 let config =
1745 Config::from_connection_string("Server=localhost;Application Intent=ReadWrite;")
1746 .unwrap();
1747 assert_eq!(config.application_intent, ApplicationIntent::ReadWrite);
1748 }
1749
1750 #[test]
1751 fn test_application_intent_invalid() {
1752 let result = Config::from_connection_string("Server=localhost;ApplicationIntent=banana;");
1753 assert!(result.is_err());
1754 assert!(
1755 result
1756 .unwrap_err()
1757 .to_string()
1758 .contains("ApplicationIntent")
1759 );
1760 }
1761
1762 #[test]
1763 fn test_workstation_id() {
1764 let config =
1765 Config::from_connection_string("Server=localhost;Workstation ID=MYPC;").unwrap();
1766 assert_eq!(config.workstation_id, Some("MYPC".to_string()));
1767 }
1768
1769 #[test]
1770 fn test_wsid_alias() {
1771 let config =
1772 Config::from_connection_string("Server=localhost;WSID=MYWORKSTATION;").unwrap();
1773 assert_eq!(config.workstation_id, Some("MYWORKSTATION".to_string()));
1774 }
1775
1776 #[test]
1777 fn test_language() {
1778 let config =
1779 Config::from_connection_string("Server=localhost;Language=us_english;").unwrap();
1780 assert_eq!(config.language, Some("us_english".to_string()));
1781 }
1782
1783 #[test]
1784 fn test_current_language_alias() {
1785 let config =
1786 Config::from_connection_string("Server=localhost;Current Language=Deutsch;").unwrap();
1787 assert_eq!(config.language, Some("Deutsch".to_string()));
1788 }
1789
1790 #[test]
1791 fn test_connect_retry_count() {
1792 let config =
1793 Config::from_connection_string("Server=localhost;ConnectRetryCount=5;").unwrap();
1794 assert_eq!(config.retry.max_retries, 5);
1795 }
1796
1797 #[test]
1798 fn test_connect_retry_interval() {
1799 let config =
1800 Config::from_connection_string("Server=localhost;ConnectRetryInterval=15;").unwrap();
1801 assert_eq!(config.retry.initial_backoff, Duration::from_secs(15));
1802 }
1803
1804 #[test]
1805 fn test_pool_keywords_accepted_without_error() {
1806 let result = Config::from_connection_string(
1808 "Server=localhost;Max Pool Size=10;Min Pool Size=2;Pooling=true;",
1809 );
1810 assert!(result.is_ok());
1811 }
1812
1813 #[test]
1814 fn test_known_unsupported_keywords_accepted() {
1815 let result = Config::from_connection_string(
1817 "Server=localhost;Failover Partner=backup;Persist Security Info=false;",
1818 );
1819 assert!(result.is_ok());
1820 }
1821
1822 #[test]
1823 fn test_multi_subnet_failover_connection_string() {
1824 let config =
1825 Config::from_connection_string("Server=ag-listener;MultiSubnetFailover=true;").unwrap();
1826 assert!(config.multi_subnet_failover);
1827
1828 let config =
1830 Config::from_connection_string("Server=ag-listener;Multi Subnet Failover=true;")
1831 .unwrap();
1832 assert!(config.multi_subnet_failover);
1833
1834 let config =
1836 Config::from_connection_string("Server=ag-listener;MultiSubnetFailover=false;")
1837 .unwrap();
1838 assert!(!config.multi_subnet_failover);
1839
1840 let config = Config::from_connection_string("Server=localhost;").unwrap();
1842 assert!(!config.multi_subnet_failover);
1843 }
1844
1845 #[test]
1846 fn test_multi_subnet_failover_builder() {
1847 let config = Config::new().multi_subnet_failover(true);
1848 assert!(config.multi_subnet_failover);
1849
1850 let config = Config::new().multi_subnet_failover(false);
1851 assert!(!config.multi_subnet_failover);
1852 }
1853
1854 #[test]
1855 fn test_multi_subnet_failover_invalid_value() {
1856 let result = Config::from_connection_string("Server=localhost;MultiSubnetFailover=banana;");
1857 assert!(result.is_err());
1858 }
1859
1860 #[test]
1861 fn test_application_intent_builder() {
1862 let config = Config::new().application_intent(ApplicationIntent::ReadOnly);
1863 assert_eq!(config.application_intent, ApplicationIntent::ReadOnly);
1864 }
1865
1866 #[test]
1867 fn test_workstation_id_builder() {
1868 let config = Config::new().workstation_id("MY-PC");
1869 assert_eq!(config.workstation_id, Some("MY-PC".to_string()));
1870 }
1871
1872 #[test]
1873 fn test_language_builder() {
1874 let config = Config::new().language("us_english");
1875 assert_eq!(config.language, Some("us_english".to_string()));
1876 }
1877
1878 #[test]
1879 fn test_send_string_parameters_as_unicode_connection_string() {
1880 let config =
1881 Config::from_connection_string("Server=localhost;SendStringParametersAsUnicode=false;")
1882 .unwrap();
1883 assert!(!config.send_string_parameters_as_unicode);
1884
1885 let config = Config::from_connection_string(
1887 "Server=localhost;Send String Parameters As Unicode=false;",
1888 )
1889 .unwrap();
1890 assert!(!config.send_string_parameters_as_unicode);
1891
1892 let config =
1894 Config::from_connection_string("Server=localhost;SendStringParametersAsUnicode=true;")
1895 .unwrap();
1896 assert!(config.send_string_parameters_as_unicode);
1897
1898 let config = Config::from_connection_string("Server=localhost;").unwrap();
1900 assert!(config.send_string_parameters_as_unicode);
1901 }
1902
1903 #[test]
1904 fn test_send_string_parameters_as_unicode_builder() {
1905 let config = Config::new().send_string_parameters_as_unicode(false);
1906 assert!(!config.send_string_parameters_as_unicode);
1907
1908 let config = Config::new().send_string_parameters_as_unicode(true);
1909 assert!(config.send_string_parameters_as_unicode);
1910 }
1911
1912 #[test]
1913 fn test_send_string_parameters_as_unicode_invalid_value() {
1914 let result = Config::from_connection_string(
1915 "Server=localhost;SendStringParametersAsUnicode=banana;",
1916 );
1917 assert!(result.is_err());
1918 }
1919
1920 #[test]
1921 fn test_empty_values_become_none() {
1922 let config =
1924 Config::from_connection_string("Server=localhost;Database=;Language=;").unwrap();
1925 assert_eq!(config.database, None);
1926 assert_eq!(config.language, None);
1927 }
1928}