pub mod command_execution_tests;
pub mod configuration_tests;
pub mod connectivity_tests;
use std::net::{IpAddr, SocketAddr};
use std::path::PathBuf;
use std::time::{Duration, Instant};
use torrust_tracker_deployer_lib::adapters::ssh::{
SshClient, SshConfig, SshConnectionConfig, SshCredentials,
};
use torrust_tracker_deployer_lib::shared::Username;
use torrust_tracker_deployer_lib::testing::integration::ssh_server::{
print_docker_debug_info, MockSshServerContainer, RealSshServerContainer,
};
use torrust_tracker_deployer_lib::testing::network::PortChecker;
pub const UNREACHABLE_IP: &str = "192.0.2.1"; pub const TEST_USERNAME: &str = "testuser";
pub const REAL_SSH_PRIVATE_KEY: &str = "fixtures/testing_rsa";
pub const REAL_SSH_PUBLIC_KEY: &str = "fixtures/testing_rsa.pub";
pub struct SshTestBuilder {
host_ip: Option<IpAddr>,
port: Option<u16>,
username: Option<String>,
private_key_path: Option<PathBuf>,
public_key_path: Option<PathBuf>,
}
impl SshTestBuilder {
pub fn new() -> Self {
Self {
host_ip: None,
port: None,
username: None,
private_key_path: None,
public_key_path: None,
}
}
pub fn with_mock_container(mut self, container: &MockSshServerContainer) -> Self {
self.host_ip = Some(container.host_ip());
self.port = Some(container.ssh_port());
self.username = Some(container.username().to_string());
self.private_key_path = Some(PathBuf::from(REAL_SSH_PRIVATE_KEY));
self.public_key_path = Some(PathBuf::from(REAL_SSH_PUBLIC_KEY));
self
}
pub fn with_real_container(mut self, container: &RealSshServerContainer) -> Self {
self.host_ip = Some(container.host_ip());
self.port = Some(container.ssh_port());
self.username = Some(container.username().to_string());
self.private_key_path = Some(PathBuf::from(REAL_SSH_PRIVATE_KEY));
self.public_key_path = Some(PathBuf::from(REAL_SSH_PUBLIC_KEY));
self
}
pub fn with_unreachable_host(mut self) -> Self {
self.host_ip = Some(IpAddr::V4(UNREACHABLE_IP.parse().unwrap()));
self.port = Some(22);
self.username = Some(TEST_USERNAME.to_string());
self.private_key_path = Some(PathBuf::from("/nonexistent/key"));
self.public_key_path = Some(PathBuf::from("/nonexistent/key.pub"));
self
}
pub fn build_client(self) -> SshClient {
let private_key_path = self.private_key_path.unwrap();
let public_key_path = self.public_key_path.unwrap();
normalize_private_key_permissions(&private_key_path);
let ssh_credentials = SshCredentials::new(
private_key_path,
public_key_path,
Username::new(self.username.unwrap()).unwrap(),
);
let ssh_config = SshConfig::new(
ssh_credentials,
SocketAddr::new(self.host_ip.unwrap(), self.port.unwrap()),
);
SshClient::new(ssh_config)
}
}
impl Default for SshTestBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(unix)]
fn normalize_private_key_permissions(private_key_path: &std::path::Path) {
use std::fs;
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = fs::metadata(private_key_path) {
let perms = metadata.permissions();
let mode = perms.mode();
if mode & 0o077 != 0 {
let restricted = fs::Permissions::from_mode(0o600);
drop(fs::set_permissions(private_key_path, restricted));
}
}
}
#[cfg(not(unix))]
fn normalize_private_key_permissions(_private_key_path: &std::path::Path) {
}
pub fn assert_connectivity_fails_quickly(client: &SshClient, max_seconds: u64) {
let start_time = Instant::now();
let result = client.test_connectivity();
let duration = start_time.elapsed();
assert!(
result.is_err() || !result.unwrap(),
"Expected connectivity to fail for unreachable/mock server"
);
assert!(
duration <= Duration::from_secs(max_seconds),
"Connectivity should fail within {max_seconds}s, but took: {duration:?}"
);
}
pub async fn assert_connectivity_succeeds_eventually(client: &SshClient, max_seconds: u64) {
let retry_interval_secs = 1;
let max_attempts = u32::try_from(max_seconds / u64::from(retry_interval_secs))
.expect("max_attempts should fit in u32");
let connection_config = SshConnectionConfig::new(
1, max_attempts,
retry_interval_secs,
2, );
let test_client = SshClient::new(SshConfig::with_connection_config(
client.ssh_config().credentials.clone(),
client.ssh_config().socket_addr,
connection_config,
));
let result = test_client.wait_for_connectivity().await;
if let Err(error) = result {
let socket_addr = test_client.ssh_config().socket_addr;
let tcp_probe_result = PortChecker::new().is_port_open(socket_addr);
let one_shot_ssh_result = test_client.test_connectivity();
let one_shot_execute_result = test_client.execute("echo 'SSH connected'");
eprintln!(
"\n=== SSH Connectivity Failure Diagnostics ===\n\
target: {socket_addr}\n\
retry_window_secs: {max_seconds}\n\
raw_tcp_port_open: {tcp_probe_result:?}\n\
one_shot_ssh_connectivity: {one_shot_ssh_result:?}\n\
one_shot_ssh_execute: {one_shot_execute_result:?}\n"
);
print_docker_debug_info(socket_addr.port());
panic!(
"Expected connectivity to succeed eventually within {max_seconds}s, but got error: {error:?}"
);
}
}