use crate::{
ClientError, ErrorKind, Result,
client::{Config, Credentials, IntoConfig, ReconnectionConfig, SentinelConfig, ServerConfig},
};
use std::sync::Arc;
#[tokio::test]
async fn credentials_provider_wins_over_static() -> Result<()> {
let mut config = Config {
username: Some("static_user".to_owned()),
password: Some("static_pwd".to_owned()),
credentials_provider: Some(Arc::new(|| async {
Ok(Credentials {
username: Some("dynamic_user".to_owned()),
password: "dynamic_pwd".to_owned(),
})
})),
..Default::default()
};
let credentials = config.resolve_credentials().await?.unwrap();
assert_eq!(Some("dynamic_user"), credentials.username.as_deref());
assert_eq!("dynamic_pwd", credentials.password);
config.credentials_provider = None;
let credentials = config.resolve_credentials().await?.unwrap();
assert_eq!(Some("static_user"), credentials.username.as_deref());
assert_eq!("static_pwd", credentials.password);
Ok(())
}
#[test]
fn provider_debug_does_not_leak() -> Result<()> {
let config = Config {
credentials_provider: Some(Arc::new(|| async {
Ok(Credentials {
username: None,
password: "dynamic_pwd".to_owned(),
})
})),
..Default::default()
};
let debug = format!("{config:?}");
assert!(
!debug.contains("dynamic_pwd"),
"Debug leaked the password: {debug}"
);
let display = config.to_string();
assert!(
!display.contains("dynamic_pwd"),
"Display leaked the password: {display}"
);
Ok(())
}
#[test]
fn display_masks_password() -> Result<()> {
assert_eq!(
"redis://:***@127.0.0.1",
"redis://:pwd@127.0.0.1".into_config()?.to_string()
);
assert_eq!(
"redis://username:***@127.0.0.1",
"redis://username:pwd@127.0.0.1".into_config()?.to_string()
);
assert_eq!(
"redis+sentinel://127.0.0.1:6379/myservice?sentinel_username=foo&sentinel_password=***",
"redis+sentinel://127.0.0.1:6379/myservice?sentinel_username=foo&sentinel_password=bar"
.into_config()?
.to_string()
);
let debug = format!("{:?}", "redis://username:pwd@127.0.0.1".into_config()?);
assert!(!debug.contains("pwd"), "Debug leaked the password: {debug}");
Ok(())
}
#[test]
fn into_config() -> Result<()> {
assert_eq!("redis://127.0.0.1", "127.0.0.1".into_config()?.to_string());
assert_eq!(
"redis://127.0.0.1",
"127.0.0.1:6379".into_config()?.to_string()
);
assert_eq!(
"redis://127.0.0.1",
"127.0.0.1".to_owned().into_config()?.to_string()
);
assert_eq!(
"redis://127.0.0.1",
"redis://127.0.0.1:6379".into_config()?.to_string()
);
assert_eq!(
"redis://127.0.0.1",
"redis://127.0.0.1".into_config()?.to_string()
);
assert_eq!(
"redis://example.com",
"redis://example.com".into_config()?.to_string()
);
assert_eq!(
"redis://:***@127.0.0.1",
"redis://:pwd@127.0.0.1".into_config()?.to_string()
);
assert_eq!(
"redis://username:***@127.0.0.1",
"redis://username:pwd@127.0.0.1".into_config()?.to_string()
);
assert_eq!(
"redis://username:***@127.0.0.1/1",
"redis://username:pwd@127.0.0.1/1"
.into_config()?
.to_string()
);
#[cfg(any(feature = "native-tls", feature = "rustls"))]
assert_eq!(
"rediss://username:***@127.0.0.1/1",
"rediss://username:pwd@127.0.0.1/1"
.into_config()?
.to_string()
);
assert_eq!(
"redis://127.0.0.1?connect_timeout=100",
"redis://127.0.0.1?connect_timeout=100"
.into_config()?
.to_string()
);
assert_eq!(
"redis://127.0.0.1",
"redis://127.0.0.1?auto_resubscribe=true"
.into_config()?
.to_string()
);
assert_eq!(
"redis://127.0.0.1?auto_resubscribe=false",
"redis://127.0.0.1?auto_resubscribe=false"
.into_config()?
.to_string()
);
assert_eq!(
"redis://127.0.0.1",
"redis://127.0.0.1?auto_remonitor=true"
.into_config()?
.to_string()
);
assert_eq!(
"redis://127.0.0.1?auto_remonitor=false",
"redis://127.0.0.1?auto_remonitor=false"
.into_config()?
.to_string()
);
assert_eq!(
"redis://127.0.0.1?connection_name=myclient",
"redis://127.0.0.1?connection_name=myclient"
.into_config()?
.to_string()
);
assert_eq!(
"redis://127.0.0.1?keep_alive=60000",
"redis://127.0.0.1?keep_alive=60000"
.into_config()?
.to_string()
);
assert_eq!(
"redis://127.0.0.1",
"redis://127.0.0.1?keep_alive=30000"
.into_config()?
.to_string()
);
assert_eq!(
"redis://127.0.0.1?keep_alive=0",
"redis://127.0.0.1?keep_alive=0".into_config()?.to_string()
);
assert_eq!(
"redis://127.0.0.1?no_delay=false",
"redis://127.0.0.1?no_delay=false"
.into_config()?
.to_string()
);
assert_eq!(
"redis://127.0.0.1?retry_on_error=true",
"redis://127.0.0.1?retry_on_error=true"
.into_config()?
.to_string()
);
assert_eq!(
"redis://127.0.0.1?max_command_attempts=2",
"redis://127.0.0.1?max_command_attempts=2"
.into_config()?
.to_string()
);
assert_eq!(
"redis://127.0.0.1",
"redis://127.0.0.1?buffers.shrink_factor=8&limits.max_nesting_depth=128"
.into_config()?
.to_string()
);
assert_eq!(
"redis://127.0.0.1?buffers.read_capacity=1024&backpressure.max_push_bytes=0&limits.max_bulk_length=1048576",
"redis://127.0.0.1?buffers.read_capacity=1024&backpressure.max_push_bytes=0&limits.max_bulk_length=1048576"
.into_config()?
.to_string()
);
assert_eq!(
"redis://127.0.0.1",
"redis://127.0.0.1?reconnection=constant"
.into_config()?
.to_string()
);
assert_eq!(
"redis://127.0.0.1?reconnection=constant&reconnection.max_attempts=3&reconnection.delay=1000&reconnection.jitter=100",
"redis://127.0.0.1?reconnection=constant&reconnection.max_attempts=3"
.into_config()?
.to_string()
);
assert_eq!(
"redis://127.0.0.1?reconnection=linear&reconnection.max_attempts=0&reconnection.delay=100&reconnection.max_delay=5000&reconnection.jitter=100",
"redis://127.0.0.1?reconnection=linear&reconnection.delay=100&reconnection.max_delay=5000"
.into_config()?
.to_string()
);
assert_eq!(
"redis://127.0.0.1?reconnection=exponential&reconnection.max_attempts=0&reconnection.min_delay=50&reconnection.max_delay=10000&reconnection.multiplicative_factor=2&reconnection.jitter=100",
"redis://127.0.0.1?reconnection=exponential&reconnection.min_delay=50&reconnection.max_delay=10000&reconnection.multiplicative_factor=2"
.into_config()?
.to_string()
);
assert_eq!(
"redis+sentinel://127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381/myservice/1",
"redis+sentinel://127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381/myservice/1"
.into_config()?
.to_string()
);
assert_eq!(
"redis+sentinel://127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381/myservice/1",
"redis-sentinel://127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381/myservice/1"
.into_config()?
.to_string()
);
assert_eq!(
"redis+sentinel://127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381/myservice",
"redis+sentinel://127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381/myservice"
.into_config()?
.to_string()
);
assert_eq!(
"redis+sentinel://username:***@127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381/myservice",
"redis+sentinel://username:pwd@127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381/myservice"
.into_config()?
.to_string()
);
assert_eq!(
"redis+sentinel://:***@127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381/myservice",
"redis+sentinel://:pwd@127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381/myservice"
.into_config()?
.to_string()
);
assert_eq!(
"redis+sentinel://127.0.0.1:6379/myservice",
"redis+sentinel://127.0.0.1:6379/myservice"
.into_config()?
.to_string()
);
assert_eq!(
"redis+sentinel://127.0.0.1:6379/myservice?wait_between_failures=100&sentinel_username=foo&sentinel_password=***",
"redis+sentinel://127.0.0.1:6379/myservice?wait_between_failures=100&sentinel_username=foo&sentinel_password=***"
.into_config()?
.to_string()
);
assert_eq!(
"redis+sentinel://127.0.0.1:6379/myservice?sentinel_username=foo&sentinel_password=***",
"redis+sentinel://127.0.0.1:6379/myservice?wait_between_failures=250&sentinel_username=foo&sentinel_password=***"
.into_config()?
.to_string()
);
assert_eq!(
"redis+sentinel://127.0.0.1:6379/myservice?connect_timeout=100&wait_between_failures=100&sentinel_username=foo&sentinel_password=***",
"redis+sentinel://127.0.0.1:6379/myservice?connect_timeout=100&wait_between_failures=100&sentinel_username=foo&sentinel_password=***"
.into_config()?
.to_string()
);
assert_eq!(
"redis+cluster://127.0.0.1:7000,127.0.0.1:7001",
"redis+cluster://127.0.0.1:7000,127.0.0.1:7001"
.into_config()?
.to_string()
);
assert_eq!(
"redis+cluster://127.0.0.1:7000?read_preference=prefer_replica",
"redis+cluster://127.0.0.1:7000?read_preference=prefer_replica"
.into_config()?
.to_string()
);
assert_eq!(
"redis+cluster://127.0.0.1:7000",
"redis+cluster://127.0.0.1:7000?read_preference=master"
.into_config()?
.to_string()
);
assert_eq!(
"redis+cluster://127.0.0.1:7000?connect_timeout=100&read_preference=prefer_replica",
"redis+cluster://127.0.0.1:7000?connect_timeout=100&read_preference=prefer_replica"
.into_config()?
.to_string()
);
assert_eq!(
"redis+cluster://127.0.0.1:7000?topology_refresh_interval=5000",
"redis+cluster://127.0.0.1:7000?topology_refresh_interval=5000"
.into_config()?
.to_string()
);
assert_eq!(
"redis+cluster://127.0.0.1:7000?topology_refresh_interval=0",
"redis+cluster://127.0.0.1:7000?topology_refresh_interval=0"
.into_config()?
.to_string()
);
assert_eq!(
"redis+cluster://127.0.0.1:7000",
"redis+cluster://127.0.0.1:7000?topology_refresh_interval=60000"
.into_config()?
.to_string()
);
assert!("127.0.0.1:xyz".into_config().is_err());
assert!("redis://127.0.0.1:xyz".into_config().is_err());
assert!("redis://username@127.0.0.1".into_config().is_err());
assert!("http://username@127.0.0.1".into_config().is_err());
assert!(
"redis+sentinel://127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381"
.into_config()
.is_err()
);
assert!("redis://127.0.0.1?param".into_config().is_err());
Ok(())
}
#[test]
fn an_unknown_query_parameter_is_rejected() {
for uri in [
"redis://127.0.0.1?param=value",
"redis://127.0.0.1?commandtimeout=5000",
"redis://127.0.0.1?command_timeout=5000&read_timeout=5000",
"redis://127.0.0.1?buffers=1024",
"redis://127.0.0.1?limits.max_bulk_len=1024",
"redis+sentinel://127.0.0.1:6379/myservice?sentinel_user=foo",
] {
let error = uri.into_config().unwrap_err();
let ErrorKind::Client(ClientError::InvalidUri(message)) = error.kind() else {
panic!("`{uri}` should be rejected as an unknown query parameter");
};
assert!(
message.contains("unknown"),
"`{uri}`: unhelpful message `{message}`"
);
}
}
#[test]
fn a_query_parameter_of_another_server_type_names_the_uri_it_belongs_to() {
for (uri, belongs_to) in [
("redis://127.0.0.1?sentinel_password=secret", "sentinel"),
(
"redis+cluster://127.0.0.1:6379?sentinel_username=foo",
"sentinel",
),
(
"redis+cluster://127.0.0.1:6379?wait_between_failures=250",
"sentinel",
),
(
"redis://127.0.0.1?read_preference=prefer_replica",
"cluster",
),
(
"redis+sentinel://127.0.0.1:6379/myservice?topology_refresh_interval=60000",
"cluster",
),
("redis://127.0.0.1?db=5", "unix socket"),
] {
let error = uri.into_config().unwrap_err();
let ErrorKind::Client(ClientError::InvalidUri(message)) = error.kind() else {
panic!("`{uri}` should be rejected as a parameter of another server type");
};
let name = uri.rsplit_once('?').unwrap().1.split('=').next().unwrap();
assert!(
message.contains(name) && message.contains(belongs_to),
"`{uri}`: message `{message}` names neither `{name}` nor the {belongs_to} URI it \
belongs to"
);
}
}
#[test]
fn an_unparsable_query_parameter_value_is_rejected() {
for uri in [
"redis://127.0.0.1?command_timeout=5s",
"redis://127.0.0.1?connect_timeout=5000ms",
"redis://127.0.0.1?keep_alive=abc",
"redis://127.0.0.1?no_delay=yes",
"redis://127.0.0.1?auto_resubscribe=1",
"redis://127.0.0.1?auto_remonitor=",
"redis://127.0.0.1?retry_on_error=maybe",
"redis://127.0.0.1?max_command_attempts=-1",
"redis+sentinel://127.0.0.1:6379/myservice?wait_between_failures=250ms",
"redis+cluster://127.0.0.1:7000?read_preference=replica",
] {
let error = uri.into_config().unwrap_err();
let ErrorKind::Client(ClientError::InvalidUri(message)) = error.kind() else {
panic!("`{uri}` should be rejected as an unparsable parameter value");
};
let name = uri.rsplit_once('?').unwrap().1.split('=').next().unwrap();
assert!(
message.contains(name),
"`{uri}`: message `{message}` does not name the offending parameter"
);
}
}
#[test]
fn the_default_config_detects_a_half_open_connection() {
let config = Config::default();
assert_eq!(Some(std::time::Duration::from_secs(30)), config.keep_alive);
}
#[test]
fn tuning_defaults_preserve_the_historical_hardcoded_values() {
let config = Config::default();
assert_eq!(64 * 1024, config.buffers.read_capacity);
assert_eq!(64 * 1024, config.buffers.tape_capacity);
assert_eq!(8, config.buffers.shrink_factor);
assert_eq!(16, config.buffers.shrink_hysteresis);
assert_eq!(128, config.limits.max_nesting_depth);
assert_eq!(512 * 1024 * 1024, config.limits.max_bulk_length);
assert_eq!(128 * 1024 * 1024, config.limits.max_collection_length);
assert_eq!(48, config.max_messages_per_wave);
assert_eq!(10, SentinelConfig::default().max_discovery_rounds);
}
#[test]
fn a_default_config_validates() {
assert!(Config::default().validate().is_ok());
}
#[test]
fn validate_rejects_knobs_whose_zero_value_would_break_the_connection() {
fn assert_rejected(name: &str, zero_it: impl FnOnce(&mut Config)) {
let mut config = Config::default();
zero_it(&mut config);
let error = config.validate().unwrap_err();
assert!(
matches!(
error.kind(),
ErrorKind::Client(ClientError::InvalidConfig(_))
),
"{name} = 0 must be rejected"
);
}
assert_rejected("read_capacity", |c| c.buffers.read_capacity = 0);
assert_rejected("tape_capacity", |c| c.buffers.tape_capacity = 0);
assert_rejected("shrink_factor", |c| c.buffers.shrink_factor = 0);
assert_rejected("shrink_hysteresis", |c| c.buffers.shrink_hysteresis = 0);
assert_rejected("max_nesting_depth", |c| c.limits.max_nesting_depth = 0);
assert_rejected("max_bulk_length", |c| c.limits.max_bulk_length = 0);
assert_rejected("max_collection_length", |c| {
c.limits.max_collection_length = 0
});
assert_rejected("max_messages_per_wave", |c| c.max_messages_per_wave = 0);
}
#[test]
fn validate_rejects_a_zero_sentinel_discovery_round_cap() {
let mut config = Config::default();
let mut sentinel_config = SentinelConfig {
instances: vec![("127.0.0.1".to_owned(), 26379)],
service_name: "myservice".to_owned(),
..Default::default()
};
sentinel_config.max_discovery_rounds = 0;
config.server = ServerConfig::Sentinel(sentinel_config);
let error = config.validate().unwrap_err();
assert!(matches!(
error.kind(),
ErrorKind::Client(ClientError::InvalidConfig(_))
));
}
#[test]
fn validate_names_the_offending_knob() {
let mut config = Config::default();
config.limits.max_bulk_length = 0;
let error = config.validate().unwrap_err();
let ErrorKind::Client(ClientError::InvalidConfig(message)) = error.kind() else {
panic!("expected an InvalidConfig error");
};
assert!(
message.contains("max_bulk_length"),
"message did not name the knob: {message}"
);
}
#[cfg(feature = "json")]
#[test]
fn a_config_file_sets_every_knob() {
let config: Config = serde_json::from_str(
r#"{
"server": { "Standalone": { "host": "example.com", "port": 6380 } },
"database": 3,
"backpressure": { "max_queued_bytes": 4096 },
"reconnection": { "Constant": { "max_attempts": 7, "delay": 250, "jitter": 10 } }
}"#,
)
.unwrap();
assert!(matches!(
&config.server,
ServerConfig::Standalone { host, port } if host == "example.com" && *port == 6380
));
assert_eq!(3, config.database);
assert_eq!(4096, config.backpressure.max_queued_bytes);
assert!(matches!(
config.reconnection,
ReconnectionConfig::Constant {
max_attempts: 7,
delay: 250,
jitter: 10
}
));
assert_eq!(
Config::default().max_messages_per_wave,
config.max_messages_per_wave
);
}
#[cfg(feature = "json")]
#[test]
fn a_serialized_config_round_trips() {
let config = Config {
connection_name: "round-trip".to_owned(),
limits: crate::client::RespLimits {
max_bulk_length: 1234,
..Default::default()
},
server: ServerConfig::Cluster(crate::client::ClusterConfig {
nodes: vec![("node".to_owned(), 7000)],
..Default::default()
}),
..Default::default()
};
let json = serde_json::to_string(&config).unwrap();
let back: Config = serde_json::from_str(&json).unwrap();
assert_eq!(config.connection_name, back.connection_name);
assert_eq!(config.limits.max_bulk_length, back.limits.max_bulk_length);
assert_eq!(format!("{:?}", config.server), format!("{:?}", back.server));
}
#[test]
fn the_tuning_knobs_are_addressable_in_a_uri() -> Result<()> {
let config = "redis://127.0.0.1\
?buffers.read_capacity=1024\
&buffers.tape_capacity=2048\
&buffers.shrink_factor=4\
&buffers.shrink_hysteresis=32\
&backpressure.max_queued_bytes=4096\
&backpressure.max_pubsub_bytes=8192\
&backpressure.max_push_bytes=0\
&limits.max_nesting_depth=16\
&limits.max_bulk_length=1048576\
&limits.max_collection_length=1000"
.into_config()?;
assert_eq!(1024, config.buffers.read_capacity);
assert_eq!(2048, config.buffers.tape_capacity);
assert_eq!(4, config.buffers.shrink_factor);
assert_eq!(32, config.buffers.shrink_hysteresis);
assert_eq!(4096, config.backpressure.max_queued_bytes);
assert_eq!(8192, config.backpressure.max_pubsub_bytes);
assert_eq!(0, config.backpressure.max_push_bytes);
assert_eq!(16, config.limits.max_nesting_depth);
assert_eq!(1048576, config.limits.max_bulk_length);
assert_eq!(1000, config.limits.max_collection_length);
Ok(())
}
#[test]
fn a_reconnection_policy_is_addressable_in_a_uri() -> Result<()> {
let config = "redis://127.0.0.1?reconnection=constant".into_config()?;
assert!(matches!(
config.reconnection,
ReconnectionConfig::Constant {
max_attempts: 0,
delay: 1000,
jitter: 100
}
));
let config =
"redis://127.0.0.1?reconnection=constant&reconnection.delay=250&reconnection.jitter=25&reconnection.max_attempts=3"
.into_config()?;
assert!(matches!(
config.reconnection,
ReconnectionConfig::Constant {
max_attempts: 3,
delay: 250,
jitter: 25
}
));
let config =
"redis://127.0.0.1?reconnection=linear&reconnection.delay=100&reconnection.max_delay=5000"
.into_config()?;
assert!(matches!(
config.reconnection,
ReconnectionConfig::Linear {
max_attempts: 0,
max_delay: 5000,
delay: 100,
jitter: 100
}
));
let config = "redis://127.0.0.1\
?reconnection=exponential\
&reconnection.min_delay=50\
&reconnection.max_delay=10000\
&reconnection.multiplicative_factor=2"
.into_config()?;
assert!(matches!(
config.reconnection,
ReconnectionConfig::Exponential {
max_attempts: 0,
min_delay: 50,
max_delay: 10000,
multiplicative_factor: 2,
jitter: 100
}
));
Ok(())
}
#[test]
fn a_reconnection_uri_that_shapes_nothing_is_rejected() {
for (uri, expected) in [
(
"redis://127.0.0.1?reconnection=linear&reconnection.delay=100",
"reconnection.max_delay",
),
(
"redis://127.0.0.1?reconnection=exponential&reconnection.min_delay=50&reconnection.max_delay=1000",
"reconnection.multiplicative_factor",
),
(
"redis://127.0.0.1?reconnection=constant&reconnection.max_delay=1000",
"reconnection.max_delay",
),
(
"redis://127.0.0.1?reconnection.delay=100",
"reconnection.delay",
),
("redis://127.0.0.1?reconnection=quadratic", "quadratic"),
] {
let error = uri.into_config().unwrap_err();
let ErrorKind::Client(ClientError::InvalidUri(message)) = error.kind() else {
panic!("`{uri}` should be rejected");
};
assert!(
message.contains(expected),
"`{uri}`: message `{message}` does not name `{expected}`"
);
}
}