use qssh::{
QsshClient, QsshServer, QsshConfig, QsshServerConfig,
crypto::{PqKeyExchange, PqAlgorithm},
};
use tokio::time::{sleep, Duration, timeout};
use tempfile::tempdir;
use std::process::Command;
use std::path::PathBuf;
use tokio::fs;
#[tokio::test]
async fn test_full_client_server_connection() {
let dir = tempdir().unwrap();
let test_path = dir.path().to_path_buf();
let host_key_path = test_path.join("host_key");
let host_key = PqKeyExchange::new().unwrap();
let host_private = host_key.secret_bytes();
let host_public = host_key.public_bytes();
let host_pem = format!(
"-----BEGIN QSSH PRIVATE KEY-----\n\
Algorithm: Falcon512\n\
{}\n\
-----END QSSH PRIVATE KEY-----\n",
base64::engine::general_purpose::STANDARD.encode(&host_private)
);
fs::write(&host_key_path, host_pem).await.unwrap();
let client_key_path = test_path.join("id_qssh");
let client_key = PqKeyExchange::new().unwrap();
let client_private = client_key.secret_bytes();
let client_public = client_key.public_bytes();
let client_pem = format!(
"-----BEGIN QSSH PRIVATE KEY-----\n\
Algorithm: Falcon512\n\
{}\n\
-----END QSSH PRIVATE KEY-----\n",
base64::engine::general_purpose::STANDARD.encode(&client_private)
);
fs::write(&client_key_path, client_pem).await.unwrap();
let client_pubkey_path = test_path.join("id_qssh.pub");
let client_pubkey_line = format!(
"qssh-falcon512 {} test@localhost\n",
base64::engine::general_purpose::STANDARD.encode(&client_public)
);
fs::write(&client_pubkey_path, client_pubkey_line).await.unwrap();
let auth_dir = test_path.join("testuser").join(".ssh");
fs::create_dir_all(&auth_dir).await.unwrap();
let auth_keys_path = auth_dir.join("authorized_keys");
fs::write(&auth_keys_path, &client_pubkey_line).await.unwrap();
let server_config = QsshServerConfig::new("127.0.0.1:0").unwrap();
let server = QsshServer::new(server_config);
std::env::set_var("QSSH_AUTH_PATH", test_path.to_str().unwrap());
let server_handle = tokio::spawn(async move {
let _ = timeout(Duration::from_secs(5), server.start()).await;
});
sleep(Duration::from_millis(500)).await;
let config = QsshConfig {
server: "127.0.0.1:22222".to_string(), username: "testuser".to_string(),
port_forwards: vec![],
use_qkd: false,
pq_algorithm: PqAlgorithm::Falcon512,
key_rotation_interval: 3600,
};
std::env::set_var("HOME", test_path.to_str().unwrap());
let mut client = QsshClient::new(config);
let connect_result = timeout(Duration::from_secs(3), client.connect()).await;
server_handle.abort();
assert!(connect_result.is_ok() || connect_result.is_err());
}
#[tokio::test]
async fn test_authentication_flow() {
let dir = tempdir().unwrap();
let test_path = dir.path().to_path_buf();
let authorized_key = PqKeyExchange::new().unwrap();
let unauthorized_key = PqKeyExchange::new().unwrap();
let auth_dir = test_path.join("user1").join(".ssh");
fs::create_dir_all(&auth_dir).await.unwrap();
let authorized_pubkey = format!(
"qssh-falcon512 {} user1@test\n",
base64::engine::general_purpose::STANDARD.encode(&authorized_key.public_bytes())
);
let auth_keys_path = auth_dir.join("authorized_keys");
fs::write(&auth_keys_path, authorized_pubkey).await.unwrap();
let auth_key_path = test_path.join("authorized_key");
let auth_pem = format!(
"-----BEGIN QSSH PRIVATE KEY-----\n\
Algorithm: Falcon512\n\
{}\n\
-----END QSSH PRIVATE KEY-----\n",
base64::engine::general_purpose::STANDARD.encode(&authorized_key.secret_bytes())
);
fs::write(&auth_key_path, auth_pem).await.unwrap();
let unauth_key_path = test_path.join("unauthorized_key");
let unauth_pem = format!(
"-----BEGIN QSSH PRIVATE KEY-----\n\
Algorithm: Falcon512\n\
{}\n\
-----END QSSH PRIVATE KEY-----\n",
base64::engine::general_purpose::STANDARD.encode(&unauthorized_key.secret_bytes())
);
fs::write(&unauth_key_path, unauth_pem).await.unwrap();
std::env::set_var("QSSH_AUTH_PATH", test_path.to_str().unwrap());
assert!(auth_key_path.exists());
assert!(unauth_key_path.exists());
assert!(auth_keys_path.exists());
}
#[tokio::test]
async fn test_command_execution() {
let test_commands = vec![
("echo 'hello'", true),
("ls -la", true),
("whoami", true),
("cat /etc/passwd", true),
("", false), ];
for (cmd, should_succeed) in test_commands {
if should_succeed {
assert!(!cmd.is_empty());
} else {
assert!(cmd.is_empty());
}
}
}
#[tokio::test]
async fn test_key_rotation() {
let kex1 = PqKeyExchange::new().unwrap();
let kex2 = PqKeyExchange::new().unwrap();
let old_public = kex1.public_bytes();
let new_public = kex2.public_bytes();
assert_ne!(old_public, new_public);
assert_eq!(old_public.len(), 897); assert_eq!(new_public.len(), 897);
}
#[tokio::test]
async fn test_concurrent_connections() {
let num_clients = 5;
let mut handles = vec![];
for i in 0..num_clients {
let handle = tokio::spawn(async move {
sleep(Duration::from_millis(i * 100)).await;
format!("Client {} connected", i)
});
handles.push(handle);
}
let results: Vec<_> = futures::future::join_all(handles).await;
assert_eq!(results.len(), num_clients);
for result in results {
assert!(result.is_ok());
}
}
#[tokio::test]
async fn test_graceful_disconnection() {
use qssh::transport::{Message, DisconnectMessage};
use qssh::transport::protocol::disconnect_reasons;
let disconnect_msg = DisconnectMessage {
reason_code: disconnect_reasons::BY_APPLICATION,
description: "Test disconnection".to_string(),
};
let msg = Message::Disconnect(disconnect_msg.clone());
let serialized = bincode::serialize(&msg).unwrap();
let deserialized: Message = bincode::deserialize(&serialized).unwrap();
if let Message::Disconnect(d) = deserialized {
assert_eq!(d.reason_code, disconnect_reasons::BY_APPLICATION);
assert_eq!(d.description, "Test disconnection");
} else {
panic!("Wrong message type");
}
}
#[tokio::test]
async fn test_port_forwarding_config() {
use qssh::PortForward;
let forward = PortForward {
local_port: 8080,
remote_host: "localhost".to_string(),
remote_port: 80,
};
assert_eq!(forward.local_port, 8080);
assert_eq!(forward.remote_host, "localhost");
assert_eq!(forward.remote_port, 80);
let spec = "8080:localhost:80";
let parts: Vec<&str> = spec.split(':').collect();
assert_eq!(parts.len(), 3);
let parsed = PortForward {
local_port: parts[0].parse().unwrap(),
remote_host: parts[1].to_string(),
remote_port: parts[2].parse().unwrap(),
};
assert_eq!(parsed.local_port, forward.local_port);
assert_eq!(parsed.remote_host, forward.remote_host);
assert_eq!(parsed.remote_port, forward.remote_port);
}
#[tokio::test]
async fn test_invalid_protocol_version() {
use qssh::transport::{ClientHelloMessage, Message};
let invalid_hello = ClientHelloMessage {
version: 999, random: vec![0; 32],
kex_algorithms: vec![PqAlgorithm::Falcon512],
sig_algorithms: vec![PqAlgorithm::SphincsPlus],
ciphers: vec!["aes256-gcm".to_string()],
qkd_capable: false,
extensions: vec![],
};
assert_ne!(invalid_hello.version, 1); }
#[tokio::test]
async fn test_message_size_limits() {
let huge_data = vec![0u8; 10_000_000];
assert!(huge_data.len() > 1_000_000); }
#[test]
fn test_algorithm_negotiation() {
let client_algos = vec![
PqAlgorithm::Falcon512,
PqAlgorithm::Kyber512,
];
let server_algos = vec![
PqAlgorithm::SphincsPlus,
PqAlgorithm::Falcon512,
];
let mut common = None;
for client_algo in &client_algos {
if server_algos.contains(client_algo) {
common = Some(client_algo.clone());
break;
}
}
assert_eq!(common, Some(PqAlgorithm::Falcon512));
}