From 109d693d59f73d2153cb1495f2f7c930e3327098 Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Sat, 21 Mar 2026 03:42:47 +0000 Subject: [PATCH] feat: Add comprehensive unit tests for CLI and CertificateService, covering command parsing and certificate generation --- apps/nxmesh-master/src/cli/mod.rs | 70 ++++ apps/nxmesh-master/src/config/settings.rs | 235 +++++++++++ apps/nxmesh-master/src/db/mod.rs | 13 + .../src/service/certificate/mod.rs | 389 ++++++++++++++++++ 4 files changed, 707 insertions(+) diff --git a/apps/nxmesh-master/src/cli/mod.rs b/apps/nxmesh-master/src/cli/mod.rs index dc3a2c0..3cdada7 100644 --- a/apps/nxmesh-master/src/cli/mod.rs +++ b/apps/nxmesh-master/src/cli/mod.rs @@ -61,3 +61,73 @@ pub async fn handle_sub_command( } => Ok(gen_agent_certs(settings, output, agent_id, zip).await?), } } + +#[cfg(test)] +mod tests { + use clap::Parser; + + use super::{Cli, Commands}; + + #[test] + fn parses_serve_mode() { + let parsed = Cli::try_parse_from(["nxmesh-master", "--serve"]); + assert!(parsed.is_ok()); + let parsed = parsed.unwrap_or_else(|_| unreachable!()); + + assert!(parsed.serve); + assert!(!parsed.generate_ca); + assert!(parsed.command.is_none()); + } + + #[test] + fn parses_generate_ca_flag() { + let parsed = Cli::try_parse_from(["nxmesh-master", "--generate-ca", "--serve"]); + assert!(parsed.is_ok()); + let parsed = parsed.unwrap_or_else(|_| unreachable!()); + + assert!(parsed.generate_ca); + assert!(parsed.serve); + } + + #[test] + fn parses_gen_certs_with_default_output() { + let parsed = Cli::try_parse_from(["nxmesh-master", "gen-certs"]); + assert!(parsed.is_ok()); + let parsed = parsed.unwrap_or_else(|_| unreachable!()); + + match parsed.command { + Some(Commands::GenCerts { output }) => { + assert_eq!(output, "./certs"); + } + _ => unreachable!(), + } + } + + #[test] + fn parses_gen_agent_certs_with_custom_values() { + let parsed = Cli::try_parse_from([ + "nxmesh-master", + "gen-agent-certs", + "--output", + "./out", + "--agent-id", + "agent-123", + "--zip", + ]); + assert!(parsed.is_ok()); + let parsed = parsed.unwrap_or_else(|_| unreachable!()); + + match parsed.command { + Some(Commands::GenAgentCerts { + output, + agent_id, + zip, + }) => { + assert_eq!(output, "./out"); + assert_eq!(agent_id, "agent-123"); + assert!(zip); + } + _ => unreachable!(), + } + } +} diff --git a/apps/nxmesh-master/src/config/settings.rs b/apps/nxmesh-master/src/config/settings.rs index c578fec..64f01e2 100644 --- a/apps/nxmesh-master/src/config/settings.rs +++ b/apps/nxmesh-master/src/config/settings.rs @@ -334,6 +334,13 @@ where #[cfg(test)] mod tests { + use std::{ + fs, + net::{IpAddr, Ipv4Addr}, + path::PathBuf, + time::{SystemTime, UNIX_EPOCH}, + }; + use super::*; #[test] @@ -348,4 +355,232 @@ mod tests { assert_send_sync::(); assert_send_sync::(); } + + fn make_temp_dir(prefix: &str) -> PathBuf { + let ts = SystemTime::now().duration_since(UNIX_EPOCH); + assert!(ts.is_ok()); + let ts = ts.unwrap_or_default(); + let path = std::env::temp_dir().join(format!( + "{}_{}_{}", + prefix, + std::process::id(), + ts.as_nanos() + )); + let created = fs::create_dir_all(&path); + assert!(created.is_ok()); + path + } + + #[test] + fn certificate_paths_include_cert_dir() { + let cert = CertificateSettings { + cert_dir: "./certs".to_string(), + san_dns: Vec::new(), + san_ip: Vec::new(), + cert_path: Some("server.crt".to_string()), + key_path: Some("server.key".to_string()), + }; + + assert_eq!(cert.cert_path(), Some("./certs/server.crt".to_string())); + assert_eq!(cert.key_path(), Some("./certs/server.key".to_string())); + } + + #[test] + fn certificate_validate_creates_directory_when_missing() { + let cert_dir = make_temp_dir("nxmesh-master-cert-create").join("nested"); + let san = Ia5String::try_from("localhost".to_string()); + assert!(san.is_ok()); + let san = san.unwrap_or_else(|_| unreachable!()); + let cert = CertificateSettings { + cert_dir: cert_dir.to_string_lossy().to_string(), + san_dns: vec![san], + san_ip: Vec::new(), + cert_path: None, + key_path: None, + }; + + let result = cert.validate(); + assert!(result.is_ok()); + assert!(cert_dir.exists()); + + let _ = fs::remove_dir_all(cert_dir.parent().unwrap_or(&cert_dir)); + } + + #[test] + fn certificate_validate_fails_when_only_cert_path_is_set() { + let cert_dir = make_temp_dir("nxmesh-master-cert-partial"); + let san = Ia5String::try_from("localhost".to_string()); + assert!(san.is_ok()); + let san = san.unwrap_or_else(|_| unreachable!()); + let cert = CertificateSettings { + cert_dir: cert_dir.to_string_lossy().to_string(), + san_dns: vec![san], + san_ip: Vec::new(), + cert_path: Some("server.crt".to_string()), + key_path: None, + }; + + let result = cert.validate(); + assert!(result.is_err()); + let msg = result.err().unwrap_or_default(); + assert!(msg.contains("Both certificate and key paths must be provided")); + + let _ = fs::remove_dir_all(&cert_dir); + } + + #[test] + fn certificate_validate_fails_with_unspecified_ip() { + let cert_dir = make_temp_dir("nxmesh-master-cert-unspecified-ip"); + let cert = CertificateSettings { + cert_dir: cert_dir.to_string_lossy().to_string(), + san_dns: Vec::new(), + san_ip: vec![IpAddr::V4(Ipv4Addr::UNSPECIFIED)], + cert_path: None, + key_path: None, + }; + + let result = cert.validate(); + assert!(result.is_err()); + let msg = result.err().unwrap_or_default(); + assert!(msg.contains("SAN IP entries cannot be unspecified")); + + let _ = fs::remove_dir_all(&cert_dir); + } + + #[test] + fn certificate_validate_fails_without_any_san_entries() { + let cert_dir = make_temp_dir("nxmesh-master-cert-no-san"); + let cert = CertificateSettings { + cert_dir: cert_dir.to_string_lossy().to_string(), + san_dns: Vec::new(), + san_ip: Vec::new(), + cert_path: None, + key_path: None, + }; + + let result = cert.validate(); + assert!(result.is_err()); + let msg = result.err().unwrap_or_default(); + assert!(msg.contains("At least one SAN entry")); + + let _ = fs::remove_dir_all(&cert_dir); + } + + #[test] + fn database_validate_fails_for_zero_max_connections() { + let db = DatabaseSettings { + url: "postgres://localhost/db".to_string(), + max_connections: Some(0), + }; + + let result = db.validate(); + assert!(result.is_err()); + let msg = result.err().unwrap_or_default(); + assert!(msg.contains("Max database connections must be greater than 0")); + } + + #[test] + fn auth_validate_fails_for_empty_secret() { + let auth = AuthSettings { + jwt_secret: "".to_string(), + jwt_expiration_hours: 24, + }; + + let result = auth.validate(); + assert!(result.is_err()); + let msg = result.err().unwrap_or_default(); + assert!(msg.contains("JWT secret cannot be empty")); + } + + #[test] + fn server_validate_fails_for_zero_port() { + let cert_dir = make_temp_dir("nxmesh-master-server-validate"); + let san = Ia5String::try_from("localhost".to_string()); + assert!(san.is_ok()); + let san = san.unwrap_or_else(|_| unreachable!()); + let server = ServerSettings { + bind_address: "0.0.0.0".to_string(), + port: 0, + certificate: CertificateSettings { + cert_dir: cert_dir.to_string_lossy().to_string(), + san_dns: vec![san], + san_ip: Vec::new(), + cert_path: None, + key_path: None, + }, + cors: None, + }; + + let result = server.validate(); + assert!(result.is_err()); + let msg = result.err().unwrap_or_default(); + assert!(msg.contains("Server port must be greater than 0")); + + let _ = fs::remove_dir_all(&cert_dir); + } + + #[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 data = Wrapper { + level: LevelFilter::DEBUG, + }; + + let encoded = serde_json::to_string(&data); + assert!(encoded.is_ok()); + let encoded = encoded.unwrap_or_default(); + assert!(encoded.to_lowercase().contains("debug")); + + let decoded: Result = serde_json::from_str(&encoded); + assert!(decoded.is_ok()); + let decoded = decoded.unwrap_or(Wrapper { + level: LevelFilter::ERROR, + }); + assert_eq!(decoded.level, LevelFilter::DEBUG); + } + + #[test] + fn ia5string_vec_round_trip_serialization() { + #[derive(Serialize, Deserialize)] + struct Wrapper { + #[serde( + deserialize_with = "deserialize_ia5string_vec", + serialize_with = "serialize_ia5string_vec" + )] + san_dns: Vec, + } + + let first = Ia5String::try_from("localhost".to_string()); + assert!(first.is_ok()); + let second = Ia5String::try_from("example.com".to_string()); + assert!(second.is_ok()); + + let first = first.unwrap_or_else(|_| unreachable!()); + let second = second.unwrap_or_else(|_| unreachable!()); + + let data = Wrapper { + san_dns: vec![first, second], + }; + + let encoded = serde_json::to_string(&data); + assert!(encoded.is_ok()); + let encoded = encoded.unwrap_or_default(); + assert!(encoded.contains("localhost")); + assert!(encoded.contains("example.com")); + + let decoded: Result = serde_json::from_str(&encoded); + assert!(decoded.is_ok()); + let decoded = decoded.unwrap_or(Wrapper { + san_dns: Vec::new(), + }); + assert_eq!(decoded.san_dns.len(), 2); + } } diff --git a/apps/nxmesh-master/src/db/mod.rs b/apps/nxmesh-master/src/db/mod.rs index 084546f..28e19c0 100644 --- a/apps/nxmesh-master/src/db/mod.rs +++ b/apps/nxmesh-master/src/db/mod.rs @@ -9,3 +9,16 @@ pub(crate) async fn establish_connection( .await .map_err(|e| format!("Failed to connect to database: {}", e).into()) } + +#[cfg(test)] +mod tests { + use super::establish_connection; + + #[tokio::test] + async fn establish_connection_fails_for_invalid_url_scheme() { + let result = establish_connection("invalid://not-a-db").await; + assert!(result.is_err()); + let msg = result.err().map(|e| e.to_string()).unwrap_or_default(); + assert!(msg.contains("Failed to connect to database")); + } +} diff --git a/apps/nxmesh-master/src/service/certificate/mod.rs b/apps/nxmesh-master/src/service/certificate/mod.rs index d7e483b..952ac5f 100644 --- a/apps/nxmesh-master/src/service/certificate/mod.rs +++ b/apps/nxmesh-master/src/service/certificate/mod.rs @@ -347,3 +347,392 @@ fn validity_period() -> (OffsetDateTime, OffsetDateTime) { }; (not_before, not_after) } + +#[cfg(test)] +mod tests { + use std::{ + fs, + net::{IpAddr, Ipv4Addr}, + os::unix::fs::PermissionsExt, + path::{Path, PathBuf}, + sync::Arc, + time::{SystemTime, UNIX_EPOCH}, + }; + + use rcgen::string::Ia5String; + use sea_orm::DatabaseConnection; + use serde_json::json; + + use crate::config::settings::Settings; + + use super::{CertificateService, CertificateServiceImpl, ConnectionType, validity_period}; + + fn unique_temp_dir(prefix: &str) -> PathBuf { + let now = SystemTime::now().duration_since(UNIX_EPOCH); + assert!(now.is_ok()); + let now = now.unwrap_or_default(); + + let dir = std::env::temp_dir().join(format!( + "{}_{}_{}", + prefix, + std::process::id(), + now.as_nanos() + )); + let created = fs::create_dir_all(&dir); + assert!(created.is_ok()); + dir + } + + fn mock_db_connection() -> DatabaseConnection { + DatabaseConnection::default() + } + + fn parse_ia5(input: &str) -> Ia5String { + let value = Ia5String::try_from(input.to_string()); + assert!(value.is_ok()); + value.unwrap_or_else(|_| unreachable!()) + } + + fn test_settings(grpc_dns: &str, http_dns: &str) -> Arc { + let parsed = serde_json::from_value::(json!({ + "server": { + "bind_address": "127.0.0.1", + "port": 8080, + "certificate": { + "cert_dir": "./certs", + "san_dns": [http_dns], + "san_ip": ["127.0.0.2"] + }, + "cors": null + }, + "database": { + "url": "postgres://user:pass@localhost/db", + "max_connections": 5 + }, + "grpc": { + "bind_address": "127.0.0.1", + "port": 50051, + "certificate": { + "cert_dir": "./certs", + "san_dns": [grpc_dns], + "san_ip": ["127.0.0.1"] + }, + "cors": null + }, + "auth": { + "jwt_secret": "secret", + "jwt_expiration_hours": 24 + }, + "log": { + "level": "INFO" + } + })); + + assert!(parsed.is_ok()); + Arc::new(parsed.unwrap_or_else(|_| unreachable!())) + } + + fn new_service(cert_dir: &str) -> CertificateServiceImpl { + CertificateServiceImpl::new( + mock_db_connection(), + cert_dir.to_string(), + test_settings("grpc.local", "http.local"), + ) + } + + #[test] + fn validity_period_is_forward_and_about_one_year() { + let (not_before, not_after) = validity_period(); + assert!(not_after > not_before); + + let diff = not_after - not_before; + assert!(diff.whole_days() >= 364); + assert!(diff.whole_days() <= 366); + } + + #[tokio::test] + async fn get_ca_cert_fails_when_folder_missing() { + let dir = unique_temp_dir("nxmesh-master-ca-missing"); + let removed = fs::remove_dir_all(&dir); + assert!(removed.is_ok()); + + let service = new_service(&dir.to_string_lossy()); + let result = service.get_ca_cert().await; + assert!(result.is_err()); + + let msg = result.err().map(|e| e.to_string()).unwrap_or_default(); + assert!(msg.contains("CA certificate folder not found")); + } + + #[tokio::test] + async fn generate_ca_cert_creates_files_and_is_retrievable() { + let dir = unique_temp_dir("nxmesh-master-generate-ca"); + let service = new_service(&dir.to_string_lossy()); + + let generated = service.generate_ca_cert().await; + assert!(generated.is_ok()); + let generated = generated.unwrap_or_else(|_| unreachable!()); + assert!(!generated.cert_pem.is_empty()); + assert!(!generated.private_key.is_empty()); + assert!(!generated.public_key.is_empty()); + + let ca_cert = dir.join("ca.crt"); + let ca_key = dir.join("ca.key"); + let ca_pub = dir.join("ca.pub"); + + assert!(ca_cert.exists()); + assert!(ca_key.exists()); + assert!(ca_pub.exists()); + + let cert_meta = fs::metadata(&ca_cert); + assert!(cert_meta.is_ok()); + let cert_meta = cert_meta.unwrap_or_else(|_| unreachable!()); + assert_eq!(cert_meta.permissions().mode() & 0o777, 0o600); + + let key_meta = fs::metadata(&ca_key); + assert!(key_meta.is_ok()); + let key_meta = key_meta.unwrap_or_else(|_| unreachable!()); + assert_eq!(key_meta.permissions().mode() & 0o777, 0o600); + + let pub_meta = fs::metadata(&ca_pub); + assert!(pub_meta.is_ok()); + let pub_meta = pub_meta.unwrap_or_else(|_| unreachable!()); + assert_eq!(pub_meta.permissions().mode() & 0o777, 0o600); + + let retrieved = service.get_ca_cert().await; + assert!(retrieved.is_ok()); + let (cert_path, key_path) = retrieved.unwrap_or_else(|_| unreachable!()); + assert_eq!(cert_path, ca_cert.to_string_lossy()); + assert_eq!(key_path, ca_key.to_string_lossy()); + + let _ = fs::remove_dir_all(&dir); + } + + #[tokio::test] + async fn get_ca_cert_fails_when_folder_exists_but_files_missing() { + let dir = unique_temp_dir("nxmesh-master-ca-partial"); + let service = new_service(&dir.to_string_lossy()); + + let ca_cert_path = dir.join("ca.crt"); + let write_result = fs::write(&ca_cert_path, "dummy cert"); + assert!(write_result.is_ok()); + + let result = service.get_ca_cert().await; + assert!(result.is_err()); + let msg = result.err().map(|e| e.to_string()).unwrap_or_default(); + assert!(msg.contains("CA certificate or key not found")); + + let _ = fs::remove_dir_all(&dir); + } + + #[tokio::test] + async fn generate_ca_cert_fails_when_ca_exists() { + let dir = unique_temp_dir("nxmesh-master-ca-exists"); + let service = new_service(&dir.to_string_lossy()); + + let first = service.generate_ca_cert().await; + assert!(first.is_ok()); + + let second = service.generate_ca_cert().await; + assert!(second.is_err()); + let msg = second.err().map(|e| e.to_string()).unwrap_or_default(); + assert!(msg.contains("CA certificate already exists")); + + let _ = fs::remove_dir_all(&dir); + } + + #[tokio::test] + async fn generate_pub_cert_pair_requires_ca_cert() { + let dir = unique_temp_dir("nxmesh-master-pub-cert-missing-ca"); + let service = new_service(&dir.to_string_lossy()); + + let result = service + .generate_pub_cert_pair( + vec![IpAddr::V4(Ipv4Addr::LOCALHOST)], + vec![parse_ia5("localhost")], + ) + .await; + assert!(result.is_err()); + + let _ = fs::remove_dir_all(&dir); + } + + #[tokio::test] + async fn generate_pub_cert_pair_succeeds_after_ca_generation() { + let dir = unique_temp_dir("nxmesh-master-pub-cert-ok"); + let service = new_service(&dir.to_string_lossy()); + + let ca = service.generate_ca_cert().await; + assert!(ca.is_ok()); + + let cert_pair = service + .generate_pub_cert_pair( + vec![IpAddr::V4(Ipv4Addr::LOCALHOST)], + vec![parse_ia5("localhost")], + ) + .await; + assert!(cert_pair.is_ok()); + let (cert, key) = cert_pair.unwrap_or_else(|_| unreachable!()); + + assert!(cert.contains("BEGIN CERTIFICATE")); + assert!(key.contains("BEGIN PRIVATE KEY")); + + let _ = fs::remove_dir_all(&dir); + } + + #[tokio::test] + async fn generate_agent_certs_fails_when_output_parent_missing() { + let ca_dir = unique_temp_dir("nxmesh-master-agent-certs-ca"); + let missing_output = unique_temp_dir("nxmesh-master-agent-certs-missing"); + let removed = fs::remove_dir_all(&missing_output); + assert!(removed.is_ok()); + + let service = new_service(&ca_dir.to_string_lossy()); + let ca = service.generate_ca_cert().await; + assert!(ca.is_ok()); + + let result = service + .generate_agent_certs("agent-id", &missing_output.to_string_lossy()) + .await; + assert!(result.is_err()); + let msg = result.err().map(|e| e.to_string()).unwrap_or_default(); + assert!(msg.contains("Output parent directory does not exist")); + + let _ = fs::remove_dir_all(&ca_dir); + } + + #[tokio::test] + async fn generate_agent_certs_fails_when_ca_is_missing() { + let ca_dir = unique_temp_dir("nxmesh-master-agent-certs-no-ca"); + let output_parent = unique_temp_dir("nxmesh-master-agent-certs-no-ca-out"); + let service = new_service(&ca_dir.to_string_lossy()); + + let result = service + .generate_agent_certs("agent-1", &output_parent.to_string_lossy()) + .await; + assert!(result.is_err()); + let msg = result.err().map(|e| e.to_string()).unwrap_or_default(); + assert!( + msg.contains("CA certificate") + || msg.contains("CA certificate folder not found") + || msg.contains("CA certificate or key not found") + ); + + let _ = fs::remove_dir_all(&ca_dir); + let _ = fs::remove_dir_all(&output_parent); + } + + #[tokio::test] + async fn generate_agent_certs_and_zip_certificates_succeeds() { + let ca_dir = unique_temp_dir("nxmesh-master-agent-certs-zip-ca"); + let output_parent = unique_temp_dir("nxmesh-master-agent-certs-zip-out"); + let service = new_service(&ca_dir.to_string_lossy()); + + let ca = service.generate_ca_cert().await; + assert!(ca.is_ok()); + + let generated = service + .generate_agent_certs("agent-42", &output_parent.to_string_lossy()) + .await; + assert!(generated.is_ok()); + let generated = generated.unwrap_or_else(|_| unreachable!()); + + assert!(Path::new(&generated.cert_path).exists()); + assert!(Path::new(&generated.key_path).exists()); + assert!(Path::new(&generated.ca_cert_path).exists()); + + assert!(generated.cert_path.ends_with("agent-42/cert.pem")); + assert!(generated.key_path.ends_with("agent-42/key.pem")); + + let cert_meta = fs::metadata(&generated.cert_path); + assert!(cert_meta.is_ok()); + let cert_meta = cert_meta.unwrap_or_else(|_| unreachable!()); + assert_eq!(cert_meta.permissions().mode() & 0o777, 0o600); + + let key_meta = fs::metadata(&generated.key_path); + assert!(key_meta.is_ok()); + let key_meta = key_meta.unwrap_or_else(|_| unreachable!()); + assert_eq!(key_meta.permissions().mode() & 0o777, 0o600); + + let zip = service + .zip_certificates( + &generated.cert_path, + &generated.key_path, + &generated.ca_cert_path, + ) + .await; + assert!(zip.is_ok()); + let zip = zip.unwrap_or_else(|_| unreachable!()); + assert!(Path::new(&zip).exists()); + + let zip_file = std::fs::File::open(&zip); + assert!(zip_file.is_ok()); + let zip_file = zip_file.unwrap_or_else(|_| unreachable!()); + let archive = zip::ZipArchive::new(zip_file); + assert!(archive.is_ok()); + let mut archive = archive.unwrap_or_else(|_| unreachable!()); + + assert!(archive.by_name("cert.pem").is_ok()); + assert!(archive.by_name("key.pem").is_ok()); + assert!(archive.by_name("ca.pem").is_ok()); + + assert!(zip.ends_with("cert.zip")); + + let _ = fs::remove_dir_all(&ca_dir); + let _ = fs::remove_dir_all(&output_parent); + } + + #[tokio::test] + async fn zip_certificates_fails_when_input_files_are_missing() { + let cert_dir = unique_temp_dir("nxmesh-master-zip-missing-input"); + let service = new_service(&cert_dir.to_string_lossy()); + + let missing_cert = cert_dir.join("missing-cert.pem"); + let missing_key = cert_dir.join("missing-key.pem"); + let missing_ca = cert_dir.join("missing-ca.pem"); + + let result = service + .zip_certificates( + &missing_cert.to_string_lossy(), + &missing_key.to_string_lossy(), + &missing_ca.to_string_lossy(), + ) + .await; + assert!(result.is_err()); + + let _ = fs::remove_dir_all(&cert_dir); + } + + #[tokio::test] + async fn get_sans_returns_values_for_each_connection_type() { + let dir = unique_temp_dir("nxmesh-master-get-sans"); + let service = CertificateServiceImpl::new( + mock_db_connection(), + dir.to_string_lossy().to_string(), + test_settings("grpc.example.test", "http.example.test"), + ); + + let (grpc_ips, grpc_dns) = service.get_sans(ConnectionType::GRPC); + assert_eq!(grpc_ips.len(), 1); + assert_eq!(grpc_ips[0], IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); + assert_eq!(grpc_dns.len(), 1); + assert_eq!(grpc_dns[0].to_string(), "grpc.example.test"); + + let (http_ips, http_dns) = service.get_sans(ConnectionType::HTTP); + assert_eq!(http_ips.len(), 1); + assert_eq!(http_ips[0], IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2))); + assert_eq!(http_dns.len(), 1); + assert_eq!(http_dns[0].to_string(), "http.example.test"); + + let mut grpc_ips_mut = grpc_ips.clone(); + grpc_ips_mut.clear(); + let mut grpc_dns_mut = grpc_dns.clone(); + grpc_dns_mut.clear(); + + let (grpc_ips_again, grpc_dns_again) = service.get_sans(ConnectionType::GRPC); + assert_eq!(grpc_ips_again.len(), 1); + assert_eq!(grpc_dns_again.len(), 1); + + let _ = fs::remove_dir_all(&dir); + } +}