#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::approx_constant
)]
use mssql_client::Error;
fn make_tls_error(msg: &str) -> Error {
Error::Tls(mssql_tls::TlsError::Configuration(msg.to_string()))
}
#[test]
fn test_connection_error_display() {
let err = Error::Connection("network unreachable".into());
let msg = err.to_string();
assert!(msg.contains("connection failed"));
assert!(msg.contains("network unreachable"));
}
#[test]
fn test_connection_closed_display() {
let err = Error::ConnectionClosed;
assert_eq!(err.to_string(), "connection closed");
}
#[test]
fn test_tls_error_display() {
let err = make_tls_error("certificate expired");
let msg = err.to_string();
assert!(msg.contains("TLS error"));
assert!(msg.contains("certificate expired"));
}
#[test]
fn test_protocol_error_display() {
let err = Error::Protocol("unexpected token".into());
let msg = err.to_string();
assert!(msg.contains("protocol error"));
assert!(msg.contains("unexpected token"));
}
#[test]
fn test_query_error_display() {
let err = Error::Query("division by zero".into());
let msg = err.to_string();
assert!(msg.contains("query error"));
assert!(msg.contains("division by zero"));
}
#[test]
fn test_server_error_display() {
let err = Error::Server {
number: 8134,
class: 16,
state: 1,
message: "Divide by zero error encountered.".into(),
server: Some("SQLSERVER01".into()),
procedure: Some("sp_calculate".into()),
line: 42,
};
let msg = err.to_string();
assert!(msg.contains("8134"));
assert!(msg.contains("Divide by zero"));
}
#[test]
fn test_server_error_without_optional_fields() {
let err = Error::Server {
number: 102,
class: 15,
state: 1,
message: "Syntax error".into(),
server: None,
procedure: None,
line: 1,
};
let msg = err.to_string();
assert!(msg.contains("102"));
assert!(msg.contains("Syntax error"));
}
#[test]
fn test_config_error_display() {
let err = Error::Config("invalid port number".into());
let msg = err.to_string();
assert!(msg.contains("configuration error"));
assert!(msg.contains("invalid port number"));
}
#[test]
fn test_timeout_errors_display() {
let connect = Error::ConnectTimeout {
host: "localhost".into(),
port: 1433,
};
assert!(connect.to_string().contains("timed out"));
assert!(connect.to_string().contains("localhost"));
assert!(connect.to_string().contains("1433"));
let tls = Error::TlsTimeout {
host: "localhost".into(),
port: 1433,
};
assert!(tls.to_string().contains("timed out"));
let login = Error::LoginTimeout {
host: "localhost".into(),
port: 1433,
};
assert!(login.to_string().contains("timed out"));
assert_eq!(Error::CommandTimeout.to_string(), "command timed out");
}
#[test]
fn test_routing_error_display() {
let err = Error::Routing {
host: "replica.database.windows.net".into(),
port: 11000,
};
let msg = err.to_string();
assert!(msg.contains("routing required"));
assert!(msg.contains("replica.database.windows.net"));
assert!(msg.contains("11000"));
}
#[test]
fn test_too_many_redirects_display() {
let err = Error::TooManyRedirects { max: 5 };
let msg = err.to_string();
assert!(msg.contains("too many redirects"));
assert!(msg.contains("5"));
}
#[test]
fn test_io_error_display() {
let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
let err = Error::from(io_err);
let msg = err.to_string();
assert!(msg.contains("IO error"));
assert!(msg.contains("refused"));
}
#[test]
fn test_invalid_identifier_display() {
let err = Error::InvalidIdentifier("DROP TABLE--".into());
let msg = err.to_string();
assert!(msg.contains("invalid identifier"));
assert!(msg.contains("DROP TABLE--"));
}
#[test]
fn test_pool_exhausted_display() {
assert_eq!(
Error::PoolExhausted.to_string(),
"connection pool exhausted"
);
}
#[test]
fn test_cancel_error_display() {
let err = Error::Cancel("connection not found".into());
let msg = err.to_string();
assert!(msg.contains("cancellation failed"));
assert!(msg.contains("connection not found"));
}
#[test]
fn test_cancelled_display() {
assert_eq!(Error::Cancelled.to_string(), "query cancelled");
}
#[test]
fn test_io_error_conversion() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotConnected, "not connected");
let err: Error = io_err.into();
match &err {
Error::Io(shared_err) => {
let msg = shared_err.to_string();
assert!(msg.contains("not connected"));
}
_ => panic!("Expected IO error"),
}
}
#[test]
fn test_io_error_is_clone() {
let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timed out");
let err: Error = io_err.into();
if let Error::Io(shared) = &err {
let cloned = shared.clone();
assert_eq!(shared.to_string(), cloned.to_string());
} else {
panic!("Expected IO error");
}
}
#[test]
fn test_is_protocol_error() {
assert!(Error::Protocol("test".into()).is_protocol_error());
assert!(!Error::Query("test".into()).is_protocol_error());
assert!(
!Error::Server {
number: 102,
class: 16,
state: 1,
message: "test".into(),
server: None,
procedure: None,
line: 1,
}
.is_protocol_error()
);
}
#[test]
fn test_error_class_severity_equivalence() {
let err = Error::Server {
number: 547,
class: 16,
state: 0,
message: "Constraint violation".into(),
server: None,
procedure: None,
line: 1,
};
assert_eq!(err.class(), err.severity());
assert_eq!(err.class(), Some(16));
}
#[test]
fn test_error_severity_ranges() {
let info = Error::Server {
number: 0,
class: 5,
state: 0,
message: "Info".into(),
server: None,
procedure: None,
line: 1,
};
assert!(info.severity().unwrap() <= 10);
let user_err = Error::Server {
number: 102,
class: 15,
state: 1,
message: "Syntax error".into(),
server: None,
procedure: None,
line: 1,
};
let sev = user_err.severity().unwrap();
assert!((11..=16).contains(&sev));
let resource_err = Error::Server {
number: 1105,
class: 17,
state: 2,
message: "Could not allocate space".into(),
server: None,
procedure: None,
line: 1,
};
let sev = resource_err.severity().unwrap();
assert!((17..=19).contains(&sev));
let system_err = Error::Server {
number: 20,
class: 20,
state: 0,
message: "Fatal error".into(),
server: None,
procedure: None,
line: 1,
};
let sev = system_err.severity().unwrap();
assert!((20..=25).contains(&sev));
}
#[test]
fn test_non_server_error_has_no_class() {
assert!(
Error::LoginTimeout {
host: "test".into(),
port: 1433
}
.class()
.is_none()
);
assert!(Error::Query("test".into()).class().is_none());
assert!(Error::Config("test".into()).class().is_none());
assert!(Error::PoolExhausted.class().is_none());
}
#[test]
fn test_mutually_exclusive_transient_terminal() {
let transient_err = Error::LoginTimeout {
host: "test".into(),
port: 1433,
};
assert!(transient_err.is_transient());
assert!(!transient_err.is_terminal());
let terminal_err = Error::Config("bad".into());
assert!(terminal_err.is_terminal());
assert!(!terminal_err.is_transient());
}
#[test]
fn test_all_timeout_types_are_transient() {
let timeout_errors = [
Error::ConnectTimeout {
host: "test".into(),
port: 1433,
},
Error::TlsTimeout {
host: "test".into(),
port: 1433,
},
Error::LoginTimeout {
host: "test".into(),
port: 1433,
},
Error::CommandTimeout,
];
for err in timeout_errors {
assert!(err.is_transient(), "{err:?} should be transient");
}
}
#[test]
fn test_routing_is_transient() {
let routing = Error::Routing {
host: "newhost".into(),
port: 1433,
};
assert!(routing.is_transient());
assert!(!routing.is_terminal());
}
fn make_azure_error(number: i32, message: &str) -> Error {
Error::Server {
number,
class: 16,
state: 1,
message: message.into(),
server: Some("myserver.database.windows.net".into()),
procedure: None,
line: 1,
}
}
#[test]
fn test_azure_resource_limit_errors() {
let err = make_azure_error(10928, "Resource ID : 1. Request limit exceeded");
assert!(err.is_transient());
let err = make_azure_error(10929, "Resource ID : 1. The minimum guarantee is 10");
assert!(err.is_transient());
}
#[test]
fn test_azure_service_errors() {
let err = make_azure_error(40197, "The service has encountered an error");
assert!(err.is_transient());
let err = make_azure_error(40501, "The service is currently busy");
assert!(err.is_transient());
let err = make_azure_error(40613, "Database is not currently available");
assert!(err.is_transient());
}
#[test]
fn test_azure_cannot_process_errors() {
for number in [49918, 49919, 49920] {
let err = make_azure_error(number, "Cannot process request");
assert!(err.is_transient(), "Error {number} should be transient");
}
}
fn make_server_error(number: i32, class: u8, message: &str) -> Error {
Error::Server {
number,
class,
state: 1,
message: message.into(),
server: Some("SQLSERVER01".into()),
procedure: None,
line: 1,
}
}
#[test]
fn test_deadlock_is_transient() {
let err = make_server_error(1205, 13, "Transaction was deadlocked");
assert!(err.is_transient());
assert!(!err.is_terminal());
}
#[test]
fn test_syntax_errors_are_terminal() {
let err = make_server_error(102, 15, "Incorrect syntax near 'SELEC'");
assert!(err.is_terminal());
assert!(!err.is_transient());
}
#[test]
fn test_object_errors_are_terminal() {
let err = make_server_error(207, 16, "Invalid column name 'foo'");
assert!(err.is_terminal());
let err = make_server_error(208, 16, "Invalid object name 'dbo.nonexistent'");
assert!(err.is_terminal());
}
#[test]
fn test_constraint_errors_are_terminal() {
let err = make_server_error(
547,
16,
"The INSERT statement conflicted with the FOREIGN KEY constraint",
);
assert!(err.is_terminal());
let err = make_server_error(2627, 14, "Violation of UNIQUE KEY constraint");
assert!(err.is_terminal());
let err = make_server_error(2601, 14, "Cannot insert duplicate key row");
assert!(err.is_terminal());
}
#[test]
fn test_error_debug_format() {
let err = Error::Server {
number: 102,
class: 15,
state: 1,
message: "Syntax error".into(),
server: Some("SERVER".into()),
procedure: Some("sp_test".into()),
line: 42,
};
let debug = format!("{err:?}");
assert!(debug.contains("Server"));
assert!(debug.contains("102"));
assert!(debug.contains("Syntax error"));
}
#[test]
fn test_all_error_variants_are_debug() {
let errors: Vec<Error> = vec![
Error::Connection("test".into()),
Error::ConnectionClosed,
make_tls_error("test"),
Error::Protocol("test".into()),
Error::Query("test".into()),
Error::Server {
number: 1,
class: 1,
state: 1,
message: "test".into(),
server: None,
procedure: None,
line: 1,
},
Error::Config("test".into()),
Error::ConnectTimeout {
host: "test".into(),
port: 1433,
},
Error::TlsTimeout {
host: "test".into(),
port: 1433,
},
Error::LoginTimeout {
host: "test".into(),
port: 1433,
},
Error::CommandTimeout,
Error::Routing {
host: "h".into(),
port: 1,
},
Error::TooManyRedirects { max: 1 },
Error::from(std::io::Error::other("test")),
Error::InvalidIdentifier("test".into()),
Error::PoolExhausted,
Error::Cancel("test".into()),
Error::Cancelled,
];
for err in errors {
let _ = format!("{err:?}");
let _ = format!("{err}");
}
}
#[test]
fn test_io_error_source() {
use std::error::Error as StdError;
let io_err = std::io::Error::other("inner error");
let err = Error::from(io_err);
let _ = err.source();
}
#[test]
fn test_protocol_errors_are_terminal() {
let err = Error::Protocol("unexpected EOF".into());
assert!(err.is_terminal());
assert!(!err.is_transient());
}
#[test]
fn test_tls_errors_are_terminal() {
let err = make_tls_error("certificate expired");
assert!(err.is_terminal());
assert!(!err.is_transient());
}
#[test]
fn test_cancel_errors_are_terminal() {
let err = Error::Cancel("connection not found".into());
assert!(err.is_terminal());
assert!(!err.is_transient());
}
#[test]
fn test_connection_errors_are_transient() {
let err = Error::Connection("network unreachable".into());
assert!(err.is_transient());
assert!(!err.is_terminal());
}
#[test]
fn test_is_tls_error() {
assert!(make_tls_error("cert expired").is_tls_error());
assert!(
Error::TlsTimeout {
host: "test".into(),
port: 1433
}
.is_tls_error()
);
assert!(
!Error::LoginTimeout {
host: "test".into(),
port: 1433
}
.is_tls_error()
);
assert!(!Error::Protocol("test".into()).is_tls_error());
}
#[test]
fn test_is_authentication_error() {
assert!(
!Error::LoginTimeout {
host: "test".into(),
port: 1433
}
.is_authentication_error()
);
assert!(!Error::Config("test".into()).is_authentication_error());
}
#[test]
fn test_is_config_error() {
assert!(Error::Config("bad port".into()).is_config_error());
assert!(
!Error::LoginTimeout {
host: "test".into(),
port: 1433
}
.is_config_error()
);
assert!(!Error::Protocol("test".into()).is_config_error());
}
#[test]
fn test_every_variant_classified() {
let classified_variants: Vec<(Error, &str)> = vec![
(Error::Connection("test".into()), "transient"),
(Error::ConnectionClosed, "transient"),
(make_tls_error("test"), "terminal"),
(Error::Protocol("test".into()), "terminal"),
(Error::Config("test".into()), "terminal"),
(
Error::ConnectTimeout {
host: "test".into(),
port: 1433,
},
"transient",
),
(
Error::TlsTimeout {
host: "test".into(),
port: 1433,
},
"transient",
),
(
Error::LoginTimeout {
host: "test".into(),
port: 1433,
},
"transient",
),
(Error::CommandTimeout, "transient"),
(
Error::Routing {
host: "h".into(),
port: 1,
},
"transient",
),
(Error::PoolExhausted, "transient"),
(Error::from(std::io::Error::other("test")), "transient"),
(Error::InvalidIdentifier("test".into()), "terminal"),
(Error::Cancel("test".into()), "terminal"),
];
for (err, expected) in classified_variants {
match expected {
"transient" => {
assert!(err.is_transient(), "{err:?} should be transient");
assert!(!err.is_terminal(), "{err:?} should not be terminal");
}
"terminal" => {
assert!(err.is_terminal(), "{err:?} should be terminal");
assert!(!err.is_transient(), "{err:?} should not be transient");
}
_ => unreachable!(),
}
}
}