use super::address_family::AddressFamily;
use super::authentication::{AuthMethod, ServerCheckMethod};
use super::connection::{Client, SshConnectionConfig, SshConnectionConfigResolver};
use crate::ssh::SshConfig;
#[test]
fn test_default_compression_advertises_none_only() {
let config = SshConnectionConfig::default();
assert!(!config.compression);
let russh_config = config.to_russh_config();
assert_eq!(
russh_config.preferred.compression.as_ref(),
[russh::compression::NONE],
"Compression no/unset must advertise only `none`"
);
}
#[test]
fn test_compression_no_advertises_none_only() {
let config = SshConnectionConfig::new().with_compression(false);
let russh_config = config.to_russh_config();
assert_eq!(
russh_config.preferred.compression.as_ref(),
[russh::compression::NONE],
"Compression no must advertise only `none`"
);
}
#[test]
fn test_compression_yes_advertises_zlib_then_none() {
let config = SshConnectionConfig::new().with_compression(true);
assert!(config.compression);
let russh_config = config.to_russh_config();
assert_eq!(
russh_config.preferred.compression.as_ref(),
[russh::compression::ZLIB, russh::compression::NONE],
"Compression yes must advertise zlib ahead of none"
);
}
#[test]
fn test_compression_yes_never_advertises_delayed_zlib() {
let config = SshConnectionConfig::new().with_compression(true);
let russh_config = config.to_russh_config();
assert!(
!russh_config
.preferred
.compression
.contains(&russh::compression::ZLIB_LEGACY),
"Compression yes must never advertise zlib@openssh.com (see #215)"
);
}
#[test]
fn test_with_compression_is_chainable_with_keepalive_settings() {
let config = SshConnectionConfig::new()
.with_keepalive_interval(Some(15))
.with_keepalive_max(5)
.with_compression(true);
assert_eq!(config.keepalive_interval, Some(15));
assert_eq!(config.keepalive_max, 5);
assert!(config.compression);
let russh_config = config.to_russh_config();
assert_eq!(
russh_config.preferred.compression.as_ref(),
[russh::compression::ZLIB, russh::compression::NONE]
);
}
#[test]
fn test_default_address_family_is_unconstrained() {
let config = SshConnectionConfig::default();
assert_eq!(config.address_family, AddressFamily::Any);
assert!(!config.address_family.is_forced());
}
#[test]
fn test_with_address_family_is_chainable_and_leaves_other_settings_alone() {
let config = SshConnectionConfig::new()
.with_keepalive_interval(Some(15))
.with_keepalive_max(5)
.with_compression(true)
.with_address_family(AddressFamily::V6);
assert_eq!(config.address_family, AddressFamily::V6);
assert_eq!(config.keepalive_interval, Some(15));
assert_eq!(config.keepalive_max, 5);
assert!(config.compression);
}
#[test]
fn test_connection_config_resolver_applies_ssh_config_per_host() {
let ssh_config = SshConfig::parse(
r#"
Host v4node
AddressFamily inet
Compression yes
ServerAliveInterval 11
ServerAliveCountMax 2
Host v6node
AddressFamily inet6
Compression no
ServerAliveInterval 22
ServerAliveCountMax 4
"#,
)
.expect("valid ssh_config");
let resolver = SshConnectionConfigResolver::new()
.with_ssh_config(Some(ssh_config))
.with_yaml_keepalive_interval(Some(30))
.with_yaml_keepalive_max(Some(3));
let v4 = resolver.resolve_for_host("v4node");
assert_eq!(v4.address_family, AddressFamily::V4);
assert!(v4.compression);
assert_eq!(v4.keepalive_interval, Some(11));
assert_eq!(v4.keepalive_max, 2);
let v6 = resolver.resolve_for_host("v6node");
assert_eq!(v6.address_family, AddressFamily::V6);
assert!(!v6.compression);
assert_eq!(v6.keepalive_interval, Some(22));
assert_eq!(v6.keepalive_max, 4);
let fallback = resolver.resolve_for_host("unconfigured");
assert_eq!(fallback.address_family, AddressFamily::Any);
assert!(!fallback.compression);
assert_eq!(fallback.keepalive_interval, Some(30));
assert_eq!(fallback.keepalive_max, 3);
}
#[test]
fn test_connection_config_resolver_preserves_cli_precedence() {
let ssh_config = SshConfig::parse(
r#"
Host target
AddressFamily inet6
ServerAliveInterval 11
ServerAliveCountMax 2
"#,
)
.expect("valid ssh_config");
let config = SshConnectionConfigResolver::new()
.with_ssh_config(Some(ssh_config))
.with_cli_keepalive_interval(Some(7))
.with_cli_keepalive_max(Some(9))
.with_yaml_keepalive_interval(Some(30))
.with_yaml_keepalive_max(Some(3))
.with_cli_address_family(Some(AddressFamily::V4))
.resolve_for_host("target");
assert_eq!(config.address_family, AddressFamily::V4);
assert_eq!(config.keepalive_interval, Some(7));
assert_eq!(config.keepalive_max, 9);
}
#[tokio::test]
async fn test_forced_ipv6_with_no_ipv6_candidate_fails_with_specific_error() {
let addr: std::net::SocketAddr = "127.0.0.1:22".parse().expect("valid IPv4 socket address");
let config = SshConnectionConfig::new().with_address_family(AddressFamily::V6);
let err = Client::connect_with_ssh_config(
addr,
"user",
AuthMethod::with_password("unused"),
ServerCheckMethod::NoCheck,
&config,
)
.await
.expect_err("forcing IPv6 against an IPv4-only candidate must fail");
assert!(
matches!(
err,
super::Error::NoAddressForFamily {
family: AddressFamily::V6,
..
}
),
"expected NoAddressForFamily, got: {err:?}"
);
assert_eq!(err.to_string(), "no IPv6 address found for 127.0.0.1");
}
#[tokio::test]
async fn test_forced_ipv4_with_no_ipv4_candidate_fails_with_specific_error() {
let addr: std::net::SocketAddr = "[::1]:22".parse().expect("valid IPv6 socket address");
let config = SshConnectionConfig::new().with_address_family(AddressFamily::V4);
let err = Client::connect_with_ssh_config(
addr,
"user",
AuthMethod::with_password("unused"),
ServerCheckMethod::NoCheck,
&config,
)
.await
.expect_err("forcing IPv4 against an IPv6-only candidate must fail");
assert!(
matches!(
err,
super::Error::NoAddressForFamily {
family: AddressFamily::V4,
..
}
),
"expected NoAddressForFamily, got: {err:?}"
);
assert_eq!(err.to_string(), "no IPv4 address found for ::1");
}
#[tokio::test]
async fn test_unforced_family_does_not_produce_the_family_error() {
let addr: std::net::SocketAddr = "127.0.0.1:0".parse().expect("valid IPv4 socket address");
let config = SshConnectionConfig::default();
let err = Client::connect_with_ssh_config(
addr,
"user",
AuthMethod::with_password("unused"),
ServerCheckMethod::NoCheck,
&config,
)
.await
.expect_err("connecting to port 0 must fail");
assert!(
!matches!(err, super::Error::NoAddressForFamily { .. }),
"the unconstrained path must never report a family mismatch, got: {err:?}"
);
}