#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use mssql_client::Config;
#[test]
fn test_empty_connection_string() {
let config = Config::from_connection_string("");
assert!(config.is_ok());
let config = config.unwrap();
assert_eq!(config.host, "localhost");
}
#[test]
fn test_whitespace_only_connection_string() {
let config = Config::from_connection_string(" \t\n ");
assert!(config.is_ok());
}
#[test]
fn test_single_semicolon() {
let config = Config::from_connection_string(";");
assert!(config.is_ok());
}
#[test]
fn test_multiple_semicolons() {
let config = Config::from_connection_string(";;;");
assert!(config.is_ok());
}
#[test]
fn test_key_without_value() {
let result = Config::from_connection_string("Server=");
assert!(result.is_ok());
let config = result.unwrap();
assert_eq!(config.host, "");
}
#[test]
fn test_missing_equals_sign() {
let result = Config::from_connection_string("Serverlocalhost;");
assert!(result.is_err());
}
#[test]
fn test_multiple_equals_in_value() {
let _config =
Config::from_connection_string("Server=localhost;Password=pass=word=with=equals;").unwrap();
}
#[test]
fn test_case_insensitive_keys() {
let config1 = Config::from_connection_string("SERVER=host1;").unwrap();
let config2 = Config::from_connection_string("server=host1;").unwrap();
let config3 = Config::from_connection_string("Server=host1;").unwrap();
assert_eq!(config1.host, config2.host);
assert_eq!(config2.host, config3.host);
}
#[test]
fn test_alternative_key_names() {
let config1 = Config::from_connection_string("Server=host1;").unwrap();
let config2 = Config::from_connection_string("Data Source=host1;").unwrap();
let config3 = Config::from_connection_string("Host=host1;").unwrap();
assert_eq!(config1.host, "host1");
assert_eq!(config2.host, "host1");
assert_eq!(config3.host, "host1");
let config4 = Config::from_connection_string("Database=db1;").unwrap();
let config5 = Config::from_connection_string("Initial Catalog=db1;").unwrap();
assert_eq!(config4.database, config5.database);
let _config6 = Config::from_connection_string("User Id=user1;").unwrap();
let _config7 = Config::from_connection_string("UID=user1;").unwrap();
let _config8 = Config::from_connection_string("User=user1;").unwrap();
let _config9 = Config::from_connection_string("Password=pass1;").unwrap();
let _config10 = Config::from_connection_string("PWD=pass1;").unwrap();
}
#[test]
fn test_server_with_port() {
let config = Config::from_connection_string("Server=myserver,1434;").unwrap();
assert_eq!(config.host, "myserver");
assert_eq!(config.port, 1434);
}
#[test]
fn test_server_with_instance() {
let config = Config::from_connection_string("Server=myserver\\SQLEXPRESS;").unwrap();
assert_eq!(config.host, "myserver");
assert_eq!(config.instance, Some("SQLEXPRESS".to_string()));
}
#[test]
fn test_server_ipv4() {
let config = Config::from_connection_string("Server=192.168.1.100;").unwrap();
assert_eq!(config.host, "192.168.1.100");
}
#[test]
fn test_server_ipv4_with_port() {
let config = Config::from_connection_string("Server=192.168.1.100,1434;").unwrap();
assert_eq!(config.host, "192.168.1.100");
assert_eq!(config.port, 1434);
}
#[test]
fn test_azure_server_name() {
let config = Config::from_connection_string("Server=myserver.database.windows.net;").unwrap();
assert_eq!(config.host, "myserver.database.windows.net");
}
#[test]
fn test_invalid_port_number() {
let result = Config::from_connection_string("Server=localhost,abc;");
assert!(result.is_err());
}
#[test]
fn test_port_overflow() {
let result = Config::from_connection_string("Server=localhost,999999;");
assert!(result.is_err());
}
#[test]
fn test_trust_server_certificate_true_values() {
let cases = ["true", "True", "TRUE", "yes", "Yes", "YES", "1"];
for case in cases {
let conn_str = format!("TrustServerCertificate={case};");
let config = Config::from_connection_string(&conn_str).unwrap();
assert!(config.trust_server_certificate, "Failed for: {case}");
}
}
#[test]
fn test_trust_server_certificate_false_values() {
let cases = ["false", "False", "FALSE", "no", "No", "NO", "0"];
for case in cases {
let conn_str = format!("TrustServerCertificate={case};");
let config = Config::from_connection_string(&conn_str).unwrap();
assert!(!config.trust_server_certificate, "Failed for: {case}");
}
}
#[test]
fn test_mars_boolean_values() {
let config_true = Config::from_connection_string("MARS=true;").unwrap();
assert!(config_true.mars);
let config_false = Config::from_connection_string("MARS=false;").unwrap();
assert!(!config_false.mars);
}
#[test]
fn test_connect_timeout_parsing() {
let config = Config::from_connection_string("Connect Timeout=30;").unwrap();
assert_eq!(config.connect_timeout.as_secs(), 30);
let config2 = Config::from_connection_string("Connection Timeout=60;").unwrap();
assert_eq!(config2.connect_timeout.as_secs(), 60);
}
#[test]
fn test_command_timeout_parsing() {
let config = Config::from_connection_string("Command Timeout=120;").unwrap();
assert_eq!(config.command_timeout.as_secs(), 120);
}
#[test]
fn test_invalid_timeout_value() {
let result = Config::from_connection_string("Connect Timeout=abc;");
assert!(result.is_err());
}
#[test]
fn test_negative_timeout_rejected() {
let result = Config::from_connection_string("Connect Timeout=-1;");
assert!(result.is_err());
}
#[test]
fn test_packet_size_parsing() {
let config = Config::from_connection_string("Packet Size=8192;").unwrap();
assert_eq!(config.packet_size, 8192);
}
#[test]
fn test_invalid_packet_size() {
let result = Config::from_connection_string("Packet Size=invalid;");
assert!(result.is_err());
}
#[test]
fn test_encrypt_strict() {
let config = Config::from_connection_string("Encrypt=strict;").unwrap();
assert!(config.strict_mode);
}
#[test]
fn test_encrypt_strict_case_insensitive() {
let config = Config::from_connection_string("Encrypt=STRICT;").unwrap();
assert!(config.strict_mode);
}
#[test]
fn test_whitespace_in_values() {
let config = Config::from_connection_string("Server= localhost ;").unwrap();
assert_eq!(config.host, "localhost");
}
#[test]
fn test_whitespace_around_equals() {
let config = Config::from_connection_string("Server = localhost ;").unwrap();
assert_eq!(config.host, "localhost");
}
#[test]
fn test_unknown_keys_ignored() {
let config = Config::from_connection_string(
"Server=localhost;UnknownOption=value;FutureFeature=enabled;",
);
assert!(config.is_ok());
}
#[test]
fn test_full_azure_connection_string() {
let conn_str = "Server=myserver.database.windows.net;\
Database=mydb;\
User Id=admin@myserver;\
Password=P@ssw0rd!;\
Encrypt=strict;\
TrustServerCertificate=false;\
Connect Timeout=30;\
Application Name=MyApp;";
let config = Config::from_connection_string(conn_str).unwrap();
assert_eq!(config.host, "myserver.database.windows.net");
assert_eq!(config.database, Some("mydb".to_string()));
assert!(config.strict_mode);
assert!(!config.trust_server_certificate);
assert_eq!(config.connect_timeout.as_secs(), 30);
assert_eq!(config.application_name, "MyApp");
}
#[test]
fn test_connection_string_without_trailing_semicolon() {
let config = Config::from_connection_string("Server=localhost;Database=test").unwrap();
assert_eq!(config.host, "localhost");
assert_eq!(config.database, Some("test".to_string()));
}
#[test]
fn test_repeated_keys_last_wins() {
let config =
Config::from_connection_string("Server=first;Server=second;Server=third;").unwrap();
assert_eq!(config.host, "third");
}
#[tokio::test]
async fn test_azure_credentials_rejected_with_no_tls() {
let config = Config::from_connection_string(
"Server=host-that-must-never-be-contacted.invalid;Encrypt=no_tls",
)
.unwrap()
.credentials(mssql_client::Credentials::AzureAccessToken {
token: "test-token".into(),
});
let err = mssql_client::Client::connect(config)
.await
.expect_err("FEDAUTH over no_tls must be rejected at connect time");
let msg = err.to_string();
assert!(
matches!(err, mssql_client::Error::Config(_)),
"expected Error::Config, got: {msg}"
);
assert!(
msg.contains("no_tls") && msg.contains("plaintext"),
"error should explain the plaintext-token hazard: {msg}"
);
}
#[tokio::test]
async fn test_azure_credentials_rejected_with_empty_token() {
let config = Config::new()
.host("host-that-must-never-be-contacted.invalid")
.credentials(mssql_client::Credentials::AzureAccessToken { token: "".into() });
let err = mssql_client::Client::connect(config)
.await
.expect_err("an empty Azure access token must be rejected");
let msg = err.to_string();
assert!(
matches!(err, mssql_client::Error::Config(_)),
"expected Error::Config, got: {msg}"
);
assert!(
msg.contains("empty"),
"error should say the token is empty: {msg}"
);
}
#[tokio::test]
async fn test_azure_credentials_rejected_below_tds_74() {
let config = Config::from_connection_string(
"Server=host-that-must-never-be-contacted.invalid;TdsVersion=7.3",
)
.unwrap()
.credentials(mssql_client::Credentials::AzureAccessToken {
token: "test-token".into(),
});
let err = mssql_client::Client::connect(config)
.await
.expect_err("FEDAUTH below TDS 7.4 must be rejected");
let msg = err.to_string();
assert!(
matches!(err, mssql_client::Error::Config(_)),
"expected Error::Config, got: {msg}"
);
assert!(
msg.contains("7.4"),
"error should name the TDS floor: {msg}"
);
}
#[test]
fn test_authentication_sql_password_keeps_sql_credentials() {
let config = Config::from_connection_string(
"Server=s;User Id=sa;Password=pw;Authentication=SqlPassword",
)
.unwrap();
match config.credentials {
mssql_client::Credentials::SqlServer { username, password } => {
assert_eq!(username.as_ref(), "sa");
assert_eq!(password.as_ref(), "pw");
}
other => panic!("expected SqlServer credentials, got {other:?}"),
}
}
#[test]
fn test_authentication_unsupported_ad_value_errors_with_token_guidance() {
for value in [
"ActiveDirectoryPassword",
"ActiveDirectoryInteractive",
"ActiveDirectoryDeviceCodeFlow",
] {
let err = Config::from_connection_string(&format!(
"Server=s;User Id=u;Password=p;Authentication={value}"
))
.expect_err("interactive Entra value should not be supported");
let msg = err.to_string();
assert!(msg.contains("not supported"), "{value}: {msg}");
assert!(
msg.contains("azure_token"),
"{value} should point to the bring-your-own-token escape hatch: {msg}"
);
}
}
#[test]
fn test_authentication_invalid_value_errors() {
let err = Config::from_connection_string("Server=s;Authentication=BogusMethod")
.expect_err("unknown Authentication value must error");
let msg = err.to_string();
assert!(msg.contains("invalid Authentication value"), "{msg}");
}
#[cfg(feature = "azure-identity")]
mod authentication_azure_identity {
use super::*;
#[test]
fn test_service_principal_parses_client_and_tenant() {
let config = Config::from_connection_string(
"Server=s;Authentication=Active Directory Service Principal;\
User Id=client-guid@tenant-guid;Password=s3cret",
)
.unwrap();
match config.credentials {
mssql_client::Credentials::AzureServicePrincipal {
tenant_id,
client_id,
client_secret,
} => {
assert_eq!(client_id.as_ref(), "client-guid");
assert_eq!(tenant_id.as_ref(), "tenant-guid");
assert_eq!(client_secret.as_ref(), "s3cret");
}
other => panic!("expected AzureServicePrincipal, got {other:?}"),
}
}
#[test]
fn test_service_principal_requires_tenant_in_user_id() {
let err = Config::from_connection_string(
"Server=s;User Id=client-only;Password=s3cret;\
Authentication=ActiveDirectoryServicePrincipal",
)
.expect_err("missing tenant id must error");
let msg = err.to_string();
assert!(
msg.contains("<client-id>@<tenant-id>"),
"error must show the expected User Id format: {msg}"
);
}
#[test]
fn test_service_principal_requires_secret() {
let err = Config::from_connection_string(
"Server=s;User Id=c@t;Authentication=ActiveDirectoryServicePrincipal",
)
.expect_err("missing client secret must error");
assert!(err.to_string().contains("client secret"), "{err}");
}
#[test]
fn test_managed_identity_system_assigned() {
let config = Config::from_connection_string(
"Server=s;Authentication=ActiveDirectoryManagedIdentity",
)
.unwrap();
assert!(matches!(
config.credentials,
mssql_client::Credentials::AzureManagedIdentity { client_id: None }
));
}
#[test]
fn test_managed_identity_user_assigned_via_user_id_and_msi_alias() {
let config = Config::from_connection_string(
"Server=s;User Id=uami-client-id;Authentication=Active Directory MSI",
)
.unwrap();
match config.credentials {
mssql_client::Credentials::AzureManagedIdentity {
client_id: Some(id),
} => assert_eq!(id.as_ref(), "uami-client-id"),
other => panic!("expected user-assigned AzureManagedIdentity, got {other:?}"),
}
}
}
#[cfg(any(feature = "integrated-auth", feature = "sspi-auth"))]
#[test]
fn test_authentication_conflicts_with_integrated_security() {
let err = Config::from_connection_string(
"Server=s;Integrated Security=true;Authentication=SqlPassword",
)
.expect_err("Authentication + Integrated Security must error");
assert!(err.to_string().contains("Integrated Security"), "{err}");
}