feature/grpc-connector #1
@@ -79,3 +79,84 @@ pub enum Commands {
|
|||||||
ca_cert: Option<String>,
|
ca_cert: Option<String>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
use super::{Cli, Commands};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_serve_flag_without_subcommand() {
|
||||||
|
let parsed = Cli::try_parse_from(["nxmesh-agent", "--serve"]);
|
||||||
|
assert!(parsed.is_ok());
|
||||||
|
|
||||||
|
let parsed = parsed.ok();
|
||||||
|
assert!(parsed.is_some());
|
||||||
|
let parsed = parsed.unwrap_or_else(|| unreachable!());
|
||||||
|
|
||||||
|
assert!(parsed.serve);
|
||||||
|
assert!(parsed.command.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_import_certs_with_zip_defaults() {
|
||||||
|
let parsed = Cli::try_parse_from(["nxmesh-agent", "import-certs", "bundle.zip"]);
|
||||||
|
assert!(parsed.is_ok());
|
||||||
|
|
||||||
|
let parsed = parsed.ok();
|
||||||
|
assert!(parsed.is_some());
|
||||||
|
let parsed = parsed.unwrap_or_else(|| unreachable!());
|
||||||
|
|
||||||
|
match parsed.command {
|
||||||
|
Some(Commands::ImportCerts {
|
||||||
|
zip,
|
||||||
|
cert_name,
|
||||||
|
key_name,
|
||||||
|
ca_name,
|
||||||
|
cert,
|
||||||
|
key,
|
||||||
|
ca_cert,
|
||||||
|
}) => {
|
||||||
|
assert_eq!(zip.as_deref(), Some("bundle.zip"));
|
||||||
|
assert_eq!(cert_name.as_deref(), Some("cert.pem"));
|
||||||
|
assert_eq!(key_name.as_deref(), Some("key.pem"));
|
||||||
|
assert_eq!(ca_name.as_deref(), Some("ca.pem"));
|
||||||
|
assert!(cert.is_none());
|
||||||
|
assert!(key.is_none());
|
||||||
|
assert!(ca_cert.is_none());
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_import_certs_with_separate_files() {
|
||||||
|
let parsed = Cli::try_parse_from([
|
||||||
|
"nxmesh-agent",
|
||||||
|
"import-certs",
|
||||||
|
"--cert",
|
||||||
|
"agent.crt",
|
||||||
|
"--key",
|
||||||
|
"agent.key",
|
||||||
|
"--ca-cert",
|
||||||
|
"ca.crt",
|
||||||
|
]);
|
||||||
|
assert!(parsed.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_conflicting_zip_and_separate_inputs() {
|
||||||
|
let parsed = Cli::try_parse_from([
|
||||||
|
"nxmesh-agent",
|
||||||
|
"import-certs",
|
||||||
|
"bundle.zip",
|
||||||
|
"--cert",
|
||||||
|
"agent.crt",
|
||||||
|
"--key",
|
||||||
|
"agent.key",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(parsed.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -318,6 +318,14 @@ where
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
os::unix::fs::PermissionsExt,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -330,4 +338,223 @@ mod tests {
|
|||||||
assert_send_sync::<LogSettings>();
|
assert_send_sync::<LogSettings>();
|
||||||
assert_send_sync::<NginxSettings>();
|
assert_send_sync::<NginxSettings>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_file(path: &Path) {
|
||||||
|
let result = fs::write(path, b"content");
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_exec_file(path: &Path) {
|
||||||
|
write_file(path);
|
||||||
|
let metadata = fs::metadata(path);
|
||||||
|
assert!(metadata.is_ok());
|
||||||
|
let metadata = metadata.ok();
|
||||||
|
assert!(metadata.is_some());
|
||||||
|
let metadata = metadata.unwrap_or_else(|| unreachable!());
|
||||||
|
|
||||||
|
let mut perms = metadata.permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
let result = fs::set_permissions(path, perms);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_non_exec_file(path: &Path) {
|
||||||
|
write_file(path);
|
||||||
|
let metadata = fs::metadata(path);
|
||||||
|
assert!(metadata.is_ok());
|
||||||
|
let metadata = metadata.ok();
|
||||||
|
assert!(metadata.is_some());
|
||||||
|
let metadata = metadata.unwrap_or_else(|| unreachable!());
|
||||||
|
|
||||||
|
let mut perms = metadata.permissions();
|
||||||
|
perms.set_mode(0o644);
|
||||||
|
let result = fs::set_permissions(path, perms);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid_tls_raw_paths(temp_dir: &TempDir) -> (PathBuf, PathBuf, PathBuf) {
|
||||||
|
let ca_path = temp_dir.path().join("ca.pem");
|
||||||
|
let cert_path = temp_dir.path().join("cert.pem");
|
||||||
|
let key_path = temp_dir.path().join("key.pem");
|
||||||
|
|
||||||
|
write_file(&ca_path);
|
||||||
|
write_file(&cert_path);
|
||||||
|
write_file(&key_path);
|
||||||
|
|
||||||
|
(ca_path, cert_path, key_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tls_raw_path_validate_succeeds_when_all_files_exist() {
|
||||||
|
let temp_dir = TempDir::new();
|
||||||
|
assert!(temp_dir.is_ok());
|
||||||
|
let temp_dir = temp_dir.ok();
|
||||||
|
assert!(temp_dir.is_some());
|
||||||
|
let temp_dir = temp_dir.unwrap_or_else(|| unreachable!());
|
||||||
|
|
||||||
|
let (ca_path, cert_path, key_path) = valid_tls_raw_paths(&temp_dir);
|
||||||
|
let settings = TLSSettings::RawPath {
|
||||||
|
ca_path: ca_path.to_string_lossy().to_string(),
|
||||||
|
cert_path: cert_path.to_string_lossy().to_string(),
|
||||||
|
key_path: key_path.to_string_lossy().to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(settings.validate().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tls_raw_path_validate_fails_when_ca_missing() {
|
||||||
|
let settings = TLSSettings::RawPath {
|
||||||
|
ca_path: "/tmp/does-not-exist-ca.pem".into(),
|
||||||
|
cert_path: "/tmp/does-not-exist-cert.pem".into(),
|
||||||
|
key_path: "/tmp/does-not-exist-key.pem".into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = settings.validate();
|
||||||
|
assert!(result.is_err());
|
||||||
|
let msg = result.err().unwrap_or_else(|| unreachable!());
|
||||||
|
assert!(msg.contains("CA file not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tls_zip_path_validate_fails_when_zip_missing() {
|
||||||
|
let settings = TLSSettings::ZipPath {
|
||||||
|
cert_zip_path: "/tmp/missing-certs.zip".into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = settings.validate();
|
||||||
|
assert!(result.is_err());
|
||||||
|
let msg = result.err().unwrap_or_else(|| unreachable!());
|
||||||
|
assert!(msg.contains("Certificate zip file not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grpc_validate_fails_when_connection_string_empty() {
|
||||||
|
let settings = GrpcSettings {
|
||||||
|
connection_string: "".into(),
|
||||||
|
m_auth: MAuthSettings::Tls(TLSSettings::ZipPath {
|
||||||
|
cert_zip_path: "/tmp/does-not-exist.zip".into(),
|
||||||
|
}),
|
||||||
|
cors: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = settings.validate();
|
||||||
|
assert!(result.is_err());
|
||||||
|
let msg = result.err().unwrap_or_else(|| unreachable!());
|
||||||
|
assert!(msg.contains("gRPC connection string cannot be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nginx_validate_succeeds_for_valid_paths_and_commands() {
|
||||||
|
let temp_dir = TempDir::new();
|
||||||
|
assert!(temp_dir.is_ok());
|
||||||
|
let temp_dir = temp_dir.ok();
|
||||||
|
assert!(temp_dir.is_some());
|
||||||
|
let temp_dir = temp_dir.unwrap_or_else(|| unreachable!());
|
||||||
|
|
||||||
|
let nginx_binary = temp_dir.path().join("nginx");
|
||||||
|
let nginx_config = temp_dir.path().join("nginx.conf");
|
||||||
|
|
||||||
|
create_exec_file(&nginx_binary);
|
||||||
|
write_file(&nginx_config);
|
||||||
|
|
||||||
|
let nginx = NginxSettings {
|
||||||
|
nginx_config_path: nginx_config.to_string_lossy().to_string(),
|
||||||
|
nginx_binary_path: Some(nginx_binary.to_string_lossy().to_string()),
|
||||||
|
override_nginx_reload_command: default_nginx_reload_command(),
|
||||||
|
override_nginx_test_command: default_nginx_test_command(),
|
||||||
|
nginx_reload_timeout_seconds: 30,
|
||||||
|
nginx_test_timeout_seconds: 30,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(nginx.validate().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nginx_validate_fails_for_non_executable_binary() {
|
||||||
|
let temp_dir = TempDir::new();
|
||||||
|
assert!(temp_dir.is_ok());
|
||||||
|
let temp_dir = temp_dir.ok();
|
||||||
|
assert!(temp_dir.is_some());
|
||||||
|
let temp_dir = temp_dir.unwrap_or_else(|| unreachable!());
|
||||||
|
|
||||||
|
let nginx_binary = temp_dir.path().join("nginx");
|
||||||
|
let nginx_config = temp_dir.path().join("nginx.conf");
|
||||||
|
|
||||||
|
create_non_exec_file(&nginx_binary);
|
||||||
|
write_file(&nginx_config);
|
||||||
|
|
||||||
|
let nginx = NginxSettings {
|
||||||
|
nginx_config_path: nginx_config.to_string_lossy().to_string(),
|
||||||
|
nginx_binary_path: Some(nginx_binary.to_string_lossy().to_string()),
|
||||||
|
override_nginx_reload_command: default_nginx_reload_command(),
|
||||||
|
override_nginx_test_command: default_nginx_test_command(),
|
||||||
|
nginx_reload_timeout_seconds: 30,
|
||||||
|
nginx_test_timeout_seconds: 30,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = nginx.validate();
|
||||||
|
assert!(result.is_err());
|
||||||
|
let msg = result.err().unwrap_or_else(|| unreachable!());
|
||||||
|
assert!(msg.contains("Nginx binary is not executable"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nginx_validate_fails_when_reload_command_lacks_template() {
|
||||||
|
let temp_dir = TempDir::new();
|
||||||
|
assert!(temp_dir.is_ok());
|
||||||
|
let temp_dir = temp_dir.ok();
|
||||||
|
assert!(temp_dir.is_some());
|
||||||
|
let temp_dir = temp_dir.unwrap_or_else(|| unreachable!());
|
||||||
|
|
||||||
|
let nginx_binary = temp_dir.path().join("nginx");
|
||||||
|
let nginx_config = temp_dir.path().join("nginx.conf");
|
||||||
|
|
||||||
|
create_exec_file(&nginx_binary);
|
||||||
|
write_file(&nginx_config);
|
||||||
|
|
||||||
|
let nginx = NginxSettings {
|
||||||
|
nginx_config_path: nginx_config.to_string_lossy().to_string(),
|
||||||
|
nginx_binary_path: Some(nginx_binary.to_string_lossy().to_string()),
|
||||||
|
override_nginx_reload_command: vec!["nginx".into(), "-s".into(), "reload".into()],
|
||||||
|
override_nginx_test_command: default_nginx_test_command(),
|
||||||
|
nginx_reload_timeout_seconds: 30,
|
||||||
|
nginx_test_timeout_seconds: 30,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = nginx.validate();
|
||||||
|
assert!(result.is_err());
|
||||||
|
let msg = result.err().unwrap_or_else(|| unreachable!());
|
||||||
|
assert!(msg.contains("Nginx reload command must contain the binary path template"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn level_filter_round_trip_serialization() {
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct Wrapper {
|
||||||
|
#[serde(
|
||||||
|
deserialize_with = "deserialize_level_filter",
|
||||||
|
serialize_with = "serialize_level_filter"
|
||||||
|
)]
|
||||||
|
level: LevelFilter,
|
||||||
|
}
|
||||||
|
|
||||||
|
let original = Wrapper {
|
||||||
|
level: LevelFilter::DEBUG,
|
||||||
|
};
|
||||||
|
|
||||||
|
let encoded = serde_json::to_string(&original);
|
||||||
|
assert!(encoded.is_ok());
|
||||||
|
let encoded = encoded.ok();
|
||||||
|
assert!(encoded.is_some());
|
||||||
|
let encoded = encoded.unwrap_or_else(|| unreachable!());
|
||||||
|
assert!(encoded.to_lowercase().contains("debug"));
|
||||||
|
|
||||||
|
let decoded = serde_json::from_str::<Wrapper>(&encoded);
|
||||||
|
assert!(decoded.is_ok());
|
||||||
|
let decoded = decoded.ok();
|
||||||
|
assert!(decoded.is_some());
|
||||||
|
let decoded = decoded.unwrap_or_else(|| unreachable!());
|
||||||
|
assert_eq!(decoded.level, LevelFilter::DEBUG);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ use tokio::sync::Mutex;
|
|||||||
|
|
||||||
pub mod ssh;
|
pub mod ssh;
|
||||||
|
|
||||||
pub type AgentClient = nxmesh_proto::agent_service_client::AgentServiceClient<tonic::transport::Channel>;
|
pub type AgentClient =
|
||||||
|
nxmesh_proto::agent_service_client::AgentServiceClient<tonic::transport::Channel>;
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait MasterConnectorTrait: Send + Sync {
|
pub trait MasterConnectorTrait: Send + Sync {
|
||||||
@@ -38,3 +39,105 @@ impl MasterConnectorTrait for MasterConnector {
|
|||||||
self.connector.get_client()
|
self.connector.get_client()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
};
|
||||||
|
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::config::settings::{
|
||||||
|
GrpcSettings, LogSettings, MAuthSettings, Settings, TLSSettings,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{AgentClient, MasterConnector, MasterConnectorTrait};
|
||||||
|
|
||||||
|
struct FakeConnector {
|
||||||
|
called: Arc<AtomicBool>,
|
||||||
|
fail: bool,
|
||||||
|
client: Arc<Mutex<AgentClient>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MasterConnectorTrait for FakeConnector {
|
||||||
|
async fn connect(
|
||||||
|
&mut self,
|
||||||
|
_settings: &Settings,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
self.called.store(true, Ordering::SeqCst);
|
||||||
|
if self.fail {
|
||||||
|
return Err("connector failed".into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_client(&self) -> Arc<Mutex<AgentClient>> {
|
||||||
|
self.client.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_settings() -> Settings {
|
||||||
|
Settings {
|
||||||
|
grpc: GrpcSettings {
|
||||||
|
connection_string: "https://localhost:50051".to_string(),
|
||||||
|
m_auth: MAuthSettings::Tls(TLSSettings::ZipPath {
|
||||||
|
cert_zip_path: "/tmp/certs.zip".to_string(),
|
||||||
|
}),
|
||||||
|
cors: None,
|
||||||
|
},
|
||||||
|
log: LogSettings::default(),
|
||||||
|
nginx: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_client() -> Arc<Mutex<AgentClient>> {
|
||||||
|
let channel =
|
||||||
|
tonic::transport::Channel::from_static("http://127.0.0.1:50051").connect_lazy();
|
||||||
|
Arc::new(Mutex::new(AgentClient::new(channel)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn master_connector_delegates_connect_successfully() {
|
||||||
|
let called = Arc::new(AtomicBool::new(false));
|
||||||
|
let fake = FakeConnector {
|
||||||
|
called: called.clone(),
|
||||||
|
fail: false,
|
||||||
|
client: test_client(),
|
||||||
|
};
|
||||||
|
let mut master = MasterConnector::new(Box::new(fake));
|
||||||
|
|
||||||
|
let result = master.connect(&test_settings()).await;
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert!(called.load(Ordering::SeqCst));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn master_connector_propagates_connect_errors() {
|
||||||
|
let fake = FakeConnector {
|
||||||
|
called: Arc::new(AtomicBool::new(false)),
|
||||||
|
fail: true,
|
||||||
|
client: test_client(),
|
||||||
|
};
|
||||||
|
let mut master = MasterConnector::new(Box::new(fake));
|
||||||
|
|
||||||
|
let result = master.connect(&test_settings()).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn master_connector_returns_underlying_client() {
|
||||||
|
let shared_client = test_client();
|
||||||
|
let fake = FakeConnector {
|
||||||
|
called: Arc::new(AtomicBool::new(false)),
|
||||||
|
fail: false,
|
||||||
|
client: shared_client.clone(),
|
||||||
|
};
|
||||||
|
let master = MasterConnector::new(Box::new(fake));
|
||||||
|
|
||||||
|
let client = master.get_client();
|
||||||
|
assert!(Arc::ptr_eq(&client, &shared_client));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use nxmesh_proto::agent_service_client::AgentServiceClient;
|
|||||||
use tonic::transport::{Certificate, ClientTlsConfig, Identity};
|
use tonic::transport::{Certificate, ClientTlsConfig, Identity};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::config::settings::{self, MAuthSettings, TLSSettings};
|
use crate::config::settings::{MAuthSettings, TLSSettings};
|
||||||
|
|
||||||
use super::{AgentClient, MasterConnectorTrait};
|
use super::{AgentClient, MasterConnectorTrait};
|
||||||
|
|
||||||
@@ -130,3 +130,151 @@ impl MasterConnectorTrait for SshMasterConnector {
|
|||||||
self.client.clone()
|
self.client.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
|
mod tests {
|
||||||
|
use std::{
|
||||||
|
fs::{self, File},
|
||||||
|
io::Write,
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
use crate::config::settings::{MAuthSettings, TLSSettings};
|
||||||
|
|
||||||
|
use super::SshMasterConnector;
|
||||||
|
|
||||||
|
const CERT_PEM: &[u8] = b"-----BEGIN CERTIFICATE-----\nAQ==\n-----END CERTIFICATE-----\n";
|
||||||
|
const KEY_PEM: &[u8] = b"-----BEGIN PRIVATE KEY-----\nAQ==\n-----END PRIVATE KEY-----\n";
|
||||||
|
const CA_PEM: &[u8] = b"-----BEGIN CERTIFICATE-----\nAQ==\n-----END CERTIFICATE-----\n";
|
||||||
|
|
||||||
|
fn create_zip_with_entries(
|
||||||
|
dir: &TempDir,
|
||||||
|
file_name: &str,
|
||||||
|
entries: &[(&str, &[u8])],
|
||||||
|
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
let zip_path = dir.path().join(file_name);
|
||||||
|
let file = File::create(&zip_path)?;
|
||||||
|
let mut zip = zip::ZipWriter::new(file);
|
||||||
|
let options = zip::write::SimpleFileOptions::default()
|
||||||
|
.compression_method(zip::CompressionMethod::Deflated)
|
||||||
|
.unix_permissions(0o600);
|
||||||
|
|
||||||
|
for (entry_name, contents) in entries {
|
||||||
|
zip.start_file(entry_name, options)?;
|
||||||
|
zip.write_all(contents)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
zip.finish()?;
|
||||||
|
Ok(zip_path.to_string_lossy().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_file(
|
||||||
|
path: &Path,
|
||||||
|
contents: &[u8],
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
fs::write(path, contents)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn extract_certificate_succeeds_with_expected_files() {
|
||||||
|
let temp_dir = TempDir::new().expect("failed to create temp dir");
|
||||||
|
let zip_path = create_zip_with_entries(
|
||||||
|
&temp_dir,
|
||||||
|
"certs.zip",
|
||||||
|
&[
|
||||||
|
("cert.pem", CERT_PEM),
|
||||||
|
("key.pem", KEY_PEM),
|
||||||
|
("ca.pem", CA_PEM),
|
||||||
|
("ignored.txt", b"ignored"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.expect("failed to create zip");
|
||||||
|
|
||||||
|
let (ca, cert, key) = SshMasterConnector::extract_certificate(&zip_path)
|
||||||
|
.await
|
||||||
|
.expect("expected cert extraction to succeed");
|
||||||
|
|
||||||
|
assert_eq!(ca, CA_PEM);
|
||||||
|
assert_eq!(cert, CERT_PEM);
|
||||||
|
assert_eq!(key, KEY_PEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn extract_certificate_fails_when_required_files_are_missing() {
|
||||||
|
let temp_dir = TempDir::new().expect("failed to create temp dir");
|
||||||
|
let zip_path = create_zip_with_entries(
|
||||||
|
&temp_dir,
|
||||||
|
"missing-key.zip",
|
||||||
|
&[("cert.pem", CERT_PEM), ("ca.pem", CA_PEM)],
|
||||||
|
)
|
||||||
|
.expect("failed to create zip");
|
||||||
|
|
||||||
|
let err = SshMasterConnector::extract_certificate(&zip_path)
|
||||||
|
.await
|
||||||
|
.expect_err("expected extraction to fail when key.pem is missing");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
err.to_string()
|
||||||
|
.contains("Certificate zip must contain cert.pem, key.pem and ca.pem")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn generate_tls_config_succeeds_for_raw_paths() {
|
||||||
|
let temp_dir = TempDir::new().expect("failed to create temp dir");
|
||||||
|
let cert_path = temp_dir.path().join("cert.pem");
|
||||||
|
let key_path = temp_dir.path().join("key.pem");
|
||||||
|
let ca_path = temp_dir.path().join("ca.pem");
|
||||||
|
|
||||||
|
write_file(&cert_path, CERT_PEM).expect("failed to write cert.pem");
|
||||||
|
write_file(&key_path, KEY_PEM).expect("failed to write key.pem");
|
||||||
|
write_file(&ca_path, CA_PEM).expect("failed to write ca.pem");
|
||||||
|
|
||||||
|
let settings = MAuthSettings::Tls(TLSSettings::RawPath {
|
||||||
|
ca_path: ca_path.to_string_lossy().to_string(),
|
||||||
|
cert_path: cert_path.to_string_lossy().to_string(),
|
||||||
|
key_path: key_path.to_string_lossy().to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = SshMasterConnector::generate_tls_config(&settings).await;
|
||||||
|
assert!(result.is_ok(), "expected raw path TLS config to succeed");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn generate_tls_config_succeeds_for_zip_path() {
|
||||||
|
let temp_dir = TempDir::new().expect("failed to create temp dir");
|
||||||
|
let zip_path = create_zip_with_entries(
|
||||||
|
&temp_dir,
|
||||||
|
"certs.zip",
|
||||||
|
&[
|
||||||
|
("cert.pem", CERT_PEM),
|
||||||
|
("key.pem", KEY_PEM),
|
||||||
|
("ca.pem", CA_PEM),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.expect("failed to create zip");
|
||||||
|
|
||||||
|
let settings = MAuthSettings::Tls(TLSSettings::ZipPath {
|
||||||
|
cert_zip_path: zip_path,
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = SshMasterConnector::generate_tls_config(&settings).await;
|
||||||
|
assert!(result.is_ok(), "expected zip path TLS config to succeed");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn generate_tls_config_fails_for_missing_raw_files() {
|
||||||
|
let settings = MAuthSettings::Tls(TLSSettings::RawPath {
|
||||||
|
ca_path: "/tmp/non-existent-ca.pem".to_string(),
|
||||||
|
cert_path: "/tmp/non-existent-cert.pem".to_string(),
|
||||||
|
key_path: "/tmp/non-existent-key.pem".to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = SshMasterConnector::generate_tls_config(&settings).await;
|
||||||
|
assert!(result.is_err(), "expected raw path TLS config to fail");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user