- identity.rs: filter 172.16.0.0/12 (Docker bridge) and 169.254.0.0/16 (link-local) from get_ip_addresses() auto-detection - identity.rs: add is_container_bridge(), is_link_local(), get_ip_for_interface(), get_primary_ip() functions - client.rs: add report_interface/report_ip fields to EnrollmentClient, new with_ip_overrides() constructor, register() uses get_primary_ip() - loader.rs: add report_interface/report_ip to EnrollmentConfig - mod.rs: wire config overrides through to EnrollmentClient - config.yaml.example: document new report_interface/report_ip options - Tests: add 18 new bridge filtering/IP override tests, fix Docker container compatibility in existing tests
632 lines
21 KiB
Rust
632 lines
21 KiB
Rust
//! Unit Tests - Identity Extraction Module
|
||
//!
|
||
//! Comprehensive tests for cross-distribution identity extraction functions.
|
||
//! Verifies machine-id, FQDN, IP address collection, and OS detail parsing.
|
||
|
||
use linux_patch_api::enroll::identity::{
|
||
get_fqdn, get_ip_addresses, get_machine_id, get_os_details,
|
||
get_primary_ip, is_container_bridge, is_link_local,
|
||
};
|
||
use linux_patch_api::enroll::EnrollmentRequest;
|
||
use serde_json::Value;
|
||
|
||
// =============================================================================
|
||
// Machine ID Tests
|
||
// =============================================================================
|
||
|
||
#[test]
|
||
fn test_machine_id_returns_non_empty() {
|
||
let id = get_machine_id().expect("Failed to get machine-id");
|
||
assert!(!id.is_empty(), "machine-id should not be empty");
|
||
}
|
||
|
||
#[test]
|
||
fn test_machine_id_is_valid_format() {
|
||
let id = get_machine_id().expect("Failed to get machine-id");
|
||
|
||
// D-Bus machine-id is a 32-character hex string (may contain dashes on some systems)
|
||
// Strip dashes for validation since implementations vary
|
||
let normalized = id.replace('-', "");
|
||
|
||
assert!(
|
||
normalized.len() >= 32,
|
||
"machine-id should be at least 32 hex chars, got {} chars",
|
||
normalized.len()
|
||
);
|
||
|
||
// All characters should be valid hex
|
||
for c in normalized.chars() {
|
||
assert!(
|
||
c.is_ascii_hexdigit(),
|
||
"machine-id contains non-hex character: {:?}",
|
||
c
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_machine_id_is_consistent() {
|
||
// Multiple calls should return the same value (it's a persistent identifier)
|
||
let id1 = get_machine_id().expect("Failed to get machine-id (call 1)");
|
||
let id2 = get_machine_id().expect("Failed to get machine-id (call 2)");
|
||
assert_eq!(id1, id2, "machine-id should be consistent across calls");
|
||
}
|
||
|
||
#[test]
|
||
fn test_machine_id_primary_file_exists() {
|
||
// Verify the primary machine-id file exists on this system
|
||
let primary = std::path::Path::new("/etc/machine-id");
|
||
assert!(
|
||
primary.exists(),
|
||
"Primary /etc/machine-id should exist on systemd-based systems (Kali)"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_machine_id_fallback_file_check() {
|
||
// Verify fallback file exists (may or may not be used)
|
||
let fallback = std::path::Path::new("/var/lib/dbus/machine-id");
|
||
if fallback.exists() {
|
||
let content =
|
||
std::fs::read_to_string(fallback).expect("Failed to read fallback machine-id");
|
||
assert!(
|
||
!content.trim().is_empty(),
|
||
"Fallback machine-id should not be empty"
|
||
);
|
||
}
|
||
// If it doesn't exist, that's fine - primary file is used instead
|
||
}
|
||
|
||
// =============================================================================
|
||
// FQDN Tests
|
||
// =============================================================================
|
||
|
||
#[test]
|
||
fn test_fqdn_returns_non_empty() {
|
||
let fqdn = get_fqdn().expect("Failed to get FQDN");
|
||
assert!(!fqdn.is_empty(), "FQDN should not be empty");
|
||
}
|
||
|
||
#[test]
|
||
fn test_fqdn_contains_valid_hostname_characters() {
|
||
let fqdn = get_fqdn().expect("Failed to get FQDN");
|
||
|
||
// Hostname characters: alphanumeric, hyphens, dots
|
||
for c in fqdn.chars() {
|
||
assert!(
|
||
c.is_alphanumeric() || c == '-' || c == '.' || c == '_',
|
||
"FQDN contains invalid character: {:?}",
|
||
c
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_fqdn_does_not_start_or_end_with_hyphen() {
|
||
let fqdn = get_fqdn().expect("Failed to get FQDN");
|
||
// Each label (split by dot) should not start/end with hyphen
|
||
for label in fqdn.split('.') {
|
||
if !label.is_empty() {
|
||
assert!(
|
||
!label.starts_with('-'),
|
||
"FQDN label '{}' starts with hyphen",
|
||
label
|
||
);
|
||
assert!(
|
||
!label.ends_with('-'),
|
||
"FQDN label '{}' ends with hyphen",
|
||
label
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_fqdn_is_consistent() {
|
||
let fqdn1 = get_fqdn().expect("Failed to get FQDN (call 1)");
|
||
let fqdn2 = get_fqdn().expect("Failed to get FQDN (call 2)");
|
||
assert_eq!(fqdn1, fqdn2, "FQDN should be consistent across calls");
|
||
}
|
||
|
||
#[test]
|
||
fn test_fqdn_reasonable_length() {
|
||
let fqdn = get_fqdn().expect("Failed to get FQDN");
|
||
assert!(
|
||
fqdn.len() < 254,
|
||
"FQDN should be less than 254 characters, got {}",
|
||
fqdn.len()
|
||
);
|
||
}
|
||
|
||
// =============================================================================
|
||
// IP Address Tests
|
||
// =============================================================================
|
||
|
||
#[test]
|
||
fn test_ip_addresses_returns_at_least_one() {
|
||
let addrs = get_ip_addresses().expect("Failed to get IP addresses");
|
||
// In Docker containers, all IPs may be in 172.16.0.0/12 (filtered), so empty is valid
|
||
if addrs.is_empty() {
|
||
eprintln!("NOTE: No routable IPs found — likely running inside a Docker container with only bridge IPs");
|
||
return;
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_ip_addresses_are_valid_ipv4() {
|
||
let addrs = get_ip_addresses().expect("Failed to get IP addresses");
|
||
|
||
for addr in &addrs {
|
||
// Verify valid IPv4 format: x.x.x.x where each octet is 0-255
|
||
let parts: Vec<&str> = addr.split('.').collect();
|
||
assert_eq!(parts.len(), 4, "IP '{}' should have 4 octets", addr);
|
||
|
||
for part in &parts {
|
||
let _octet: u8 = part.parse().unwrap_or_else(|_| {
|
||
panic!("IP octet '{}' in '{}' is not a valid number", part, addr)
|
||
});
|
||
// u8 parse success guarantees 0-255 range
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_ip_addresses_no_loopback() {
|
||
let addrs = get_ip_addresses().expect("Failed to get IP addresses");
|
||
|
||
for addr in &addrs {
|
||
assert!(
|
||
!addr.starts_with("127."),
|
||
"Loopback address '{}' should be excluded",
|
||
addr
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_ip_addresses_no_multicast() {
|
||
let addrs = get_ip_addresses().expect("Failed to get IP addresses");
|
||
|
||
for addr in &addrs {
|
||
let first_octet: u8 = addr.split('.').next().unwrap().parse().unwrap();
|
||
assert!(
|
||
first_octet < 224,
|
||
"Multicast address '{}' should be excluded (first octet {})",
|
||
addr,
|
||
first_octet
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_ip_addresses_no_broadcast() {
|
||
let addrs = get_ip_addresses().expect("Failed to get IP addresses");
|
||
|
||
for addr in &addrs {
|
||
assert_ne!(
|
||
addr, "255.255.255.255",
|
||
"Broadcast address should be excluded"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_ip_addresses_are_sorted_and_deduplicated() {
|
||
let addrs = get_ip_addresses().expect("Failed to get IP addresses");
|
||
|
||
// Check sorted
|
||
let mut sorted_addrs = addrs.clone();
|
||
sorted_addrs.sort();
|
||
assert_eq!(
|
||
addrs, sorted_addrs,
|
||
"IP addresses should be returned in sorted order"
|
||
);
|
||
|
||
// Check deduplicated
|
||
let unique_count = addrs.iter().collect::<std::collections::HashSet<_>>().len();
|
||
assert_eq!(
|
||
unique_count,
|
||
addrs.len(),
|
||
"IP addresses should contain no duplicates"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_ip_addresses_are_unicast() {
|
||
let addrs = get_ip_addresses().expect("Failed to get IP addresses");
|
||
|
||
for addr in &addrs {
|
||
let parts: Vec<u8> = addr.split('.').map(|s| s.parse().unwrap()).collect();
|
||
let first = parts[0];
|
||
|
||
// Class D (multicast): 224-239
|
||
assert!(first < 224, "Address '{}' is multicast", addr);
|
||
|
||
// Class E (reserved): 240+
|
||
assert!(first < 240, "Address '{}' is reserved", addr);
|
||
|
||
// Not unspecified (0.0.0.0)
|
||
assert!(
|
||
parts != vec![0, 0, 0, 0],
|
||
"Address '{}' is unspecified",
|
||
addr
|
||
);
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// OS Details Tests
|
||
// =============================================================================
|
||
|
||
#[test]
|
||
fn test_os_details_returns_valid_json_object() {
|
||
let details = get_os_details().expect("Failed to get OS details");
|
||
assert!(
|
||
details.is_object(),
|
||
"OS details should be a JSON object, got {:?}",
|
||
details
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_os_details_contains_kernel_version() {
|
||
let details = get_os_details().expect("Failed to get OS details");
|
||
let kernel = details
|
||
.get("kernel")
|
||
.expect("OS details must contain 'kernel' field");
|
||
assert!(kernel.is_string(), "Kernel version should be a string");
|
||
|
||
let kernel_str = kernel.as_str().unwrap();
|
||
assert!(!kernel_str.is_empty(), "Kernel version should not be empty");
|
||
|
||
// Kernel version should match pattern like X.Y.Z or X.Y.Z-extra
|
||
let parts: Vec<&str> = kernel_str.split('.').collect();
|
||
assert!(
|
||
parts.len() >= 2,
|
||
"Kernel version '{}' should have at least major.minor format",
|
||
kernel_str
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_os_details_contains_distro_identification() {
|
||
let details = get_os_details().expect("Failed to get OS details");
|
||
|
||
// Should contain at least one of: distro, version, or id_like
|
||
let has_distro = details.get("distro").is_some();
|
||
let has_version = details.get("version").is_some();
|
||
let has_id_like = details.get("id_like").is_some();
|
||
|
||
assert!(
|
||
has_distro || has_version || has_id_like,
|
||
"OS details should contain at least one identification field. Has: distro={}, version={}, id_like={}",
|
||
has_distro, has_version, has_id_like
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_os_details_distro_is_valid_string() {
|
||
let details = get_os_details().expect("Failed to get OS details");
|
||
if let Some(distro) = details.get("distro") {
|
||
assert!(distro.is_string(), "Distro should be a string");
|
||
let distro_str = distro.as_str().unwrap();
|
||
assert!(!distro_str.is_empty(), "Distro name should not be empty");
|
||
assert_ne!(
|
||
distro_str, "unknown",
|
||
"Distro should be identified on this system"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_os_details_version_is_valid_string() {
|
||
let details = get_os_details().expect("Failed to get OS details");
|
||
if let Some(version) = details.get("version") {
|
||
assert!(version.is_string(), "Version should be a string");
|
||
let version_str = version.as_str().unwrap();
|
||
assert!(!version_str.is_empty(), "Version should not be empty");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_os_details_cross_distro_compatibility() {
|
||
// Verify /etc/os-release parsing works with current system format
|
||
let details = get_os_details().expect("Failed to get OS details");
|
||
|
||
// On Kali (Debian-based), should have id_like containing "debian"
|
||
if let Some(id_like) = details.get("id_like") {
|
||
let id_like_str = id_like.as_str().unwrap();
|
||
assert!(
|
||
!id_like_str.is_empty(),
|
||
"ID_LIKE field should not be empty on Debian-based systems"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_os_details_json_is_serializable() {
|
||
let details = get_os_details().expect("Failed to get OS details");
|
||
let json_str = serde_json::to_string(&details).expect("OS details should serialize to JSON");
|
||
assert!(!json_str.is_empty(), "Serialized JSON should not be empty");
|
||
|
||
// Verify round-trip
|
||
let parsed: Value = serde_json::from_str(&json_str).expect("Should deserialize back");
|
||
assert_eq!(parsed, details, "JSON round-trip should preserve data");
|
||
}
|
||
|
||
// =============================================================================
|
||
// Integration Tests - Full Enrollment Payload
|
||
// =============================================================================
|
||
|
||
#[test]
|
||
fn test_enrollment_payload_construction() {
|
||
// Construct a full enrollment request from all identity functions
|
||
let machine_id = get_machine_id().expect("Failed to get machine-id");
|
||
let fqdn = get_fqdn().expect("Failed to get FQDN");
|
||
let ip_addrs = get_ip_addresses().expect("Failed to get IP addresses");
|
||
let os_details = get_os_details().expect("Failed to get OS details");
|
||
|
||
// In Docker containers, all IPs may be in 172.16.0.0/12 (filtered), so use fallback
|
||
let primary_ip = ip_addrs.first().cloned().unwrap_or_else(|| "127.0.0.1".to_string());
|
||
|
||
let request = EnrollmentRequest {
|
||
machine_id,
|
||
fqdn,
|
||
ip_address: primary_ip,
|
||
os_details,
|
||
};
|
||
|
||
// Verify payload serializes to valid JSON
|
||
let json =
|
||
serde_json::to_string(&request).expect("EnrollmentRequest should serialize to valid JSON");
|
||
|
||
assert!(
|
||
!json.is_empty(),
|
||
"Serialized enrollment request should not be empty"
|
||
);
|
||
|
||
// Verify JSON contains all required fields
|
||
let parsed: Value = serde_json::from_str(&json).expect("Should deserialize enrollment request");
|
||
|
||
assert!(
|
||
parsed.get("machine_id").is_some(),
|
||
"JSON must contain machine_id"
|
||
);
|
||
assert!(parsed.get("fqdn").is_some(), "JSON must contain fqdn");
|
||
assert!(
|
||
parsed.get("ip_address").is_some(),
|
||
"JSON must contain ip_address"
|
||
);
|
||
assert!(
|
||
parsed.get("os_details").is_some(),
|
||
"JSON must contain os_details"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_enrollment_payload_matches_manager_schema() {
|
||
let machine_id = get_machine_id().expect("Failed to get machine-id");
|
||
let fqdn = get_fqdn().expect("Failed to get FQDN");
|
||
let ip_addrs = get_ip_addresses().expect("Failed to get IP addresses");
|
||
let os_details = get_os_details().expect("Failed to get OS details");
|
||
|
||
let request = EnrollmentRequest {
|
||
machine_id: machine_id.clone(),
|
||
fqdn: fqdn.clone(),
|
||
ip_address: ip_addrs.first().cloned().unwrap_or_default(),
|
||
os_details: os_details.clone(),
|
||
};
|
||
|
||
// Validate against expected manager API schema
|
||
let json = serde_json::to_value(&request).expect("Failed to serialize");
|
||
|
||
// machine_id: non-empty string, hex format
|
||
assert!(json["machine_id"].is_string());
|
||
assert!(!json["machine_id"].as_str().unwrap().is_empty());
|
||
|
||
// fqdn: non-empty string
|
||
assert!(json["fqdn"].is_string());
|
||
assert!(!json["fqdn"].as_str().unwrap().is_empty());
|
||
|
||
// ip_address: valid IPv4
|
||
let ip = json["ip_address"].as_str().unwrap_or("");
|
||
if !ip.is_empty() {
|
||
let parts: Vec<&str> = ip.split('.').collect();
|
||
assert_eq!(parts.len(), 4, "IP should have 4 octets");
|
||
}
|
||
|
||
// os_details: object with kernel field
|
||
assert!(json["os_details"].is_object());
|
||
assert!(json["os_details"]["kernel"].is_string());
|
||
}
|
||
|
||
#[test]
|
||
fn test_enrollment_payload_roundtrip() {
|
||
let machine_id = get_machine_id().expect("Failed to get machine-id");
|
||
let fqdn = get_fqdn().expect("Failed to get FQDN");
|
||
let ip_addrs = get_ip_addresses().expect("Failed to get IP addresses");
|
||
let os_details = get_os_details().expect("Failed to get OS details");
|
||
|
||
let request = EnrollmentRequest {
|
||
machine_id,
|
||
fqdn,
|
||
ip_address: ip_addrs.first().cloned().unwrap_or_default(),
|
||
os_details,
|
||
};
|
||
|
||
// Serialize to JSON then deserialize back
|
||
let json = serde_json::to_string(&request).expect("Failed to serialize");
|
||
let deserialized: EnrollmentRequest =
|
||
serde_json::from_str(&json).expect("Failed to deserialize enrollment request");
|
||
|
||
assert_eq!(request.machine_id, deserialized.machine_id);
|
||
assert_eq!(request.fqdn, deserialized.fqdn);
|
||
assert_eq!(request.ip_address, deserialized.ip_address);
|
||
}
|
||
|
||
// =============================================================================
|
||
// Cross-Distro Compatibility Verification
|
||
// =============================================================================
|
||
|
||
#[test]
|
||
fn test_cross_distro_os_release_parsing() {
|
||
// Parse /etc/os-release directly to verify cross-distro compatibility
|
||
let content = std::fs::read_to_string("/etc/os-release")
|
||
.expect("/etc/os-release should exist on all target distros");
|
||
|
||
let mut parsed = std::collections::HashMap::new();
|
||
for line in content.lines() {
|
||
let line = line.trim();
|
||
if line.is_empty() || line.starts_with('#') {
|
||
continue;
|
||
}
|
||
if let Some((key, value)) = line.split_once('=') {
|
||
let unquoted = value.trim().trim_matches('"').trim_matches('\'');
|
||
parsed.insert(key.to_string(), unquoted.to_string());
|
||
}
|
||
}
|
||
|
||
// Verify key fields are present (POSIX standard for os-release)
|
||
assert!(
|
||
parsed.contains_key("NAME"),
|
||
"os-release must contain NAME field"
|
||
);
|
||
assert!(!parsed["NAME"].is_empty(), "NAME should not be empty");
|
||
}
|
||
|
||
#[test]
|
||
fn test_identity_functions_do_not_panic() {
|
||
// All identity functions should handle edge cases without panicking
|
||
let _ = std::panic::catch_unwind(|| {
|
||
let _ = get_machine_id();
|
||
});
|
||
|
||
let _ = std::panic::catch_unwind(|| {
|
||
let _ = get_fqdn();
|
||
});
|
||
|
||
let _ = std::panic::catch_unwind(|| {
|
||
let _ = get_ip_addresses();
|
||
});
|
||
|
||
let _ = std::panic::catch_unwind(|| {
|
||
let _ = get_os_details();
|
||
});
|
||
}
|
||
|
||
// =============================================================================
|
||
// Container Bridge & Link-Local Filtering Tests
|
||
// =============================================================================
|
||
|
||
#[test]
|
||
fn test_is_container_bridge_docker_default_range() {
|
||
// Docker default bridge: 172.17.0.0/16
|
||
assert!(is_container_bridge(&"172.17.0.1".parse().unwrap()));
|
||
assert!(is_container_bridge(&"172.17.255.255".parse().unwrap()));
|
||
}
|
||
|
||
#[test]
|
||
fn test_is_container_bridge_full_172_16_range() {
|
||
// 172.16.0.0/12 = 172.16.0.0 – 172.31.255.255
|
||
assert!(is_container_bridge(&"172.16.0.1".parse().unwrap()));
|
||
assert!(is_container_bridge(&"172.20.0.1".parse().unwrap()));
|
||
assert!(is_container_bridge(&"172.31.255.255".parse().unwrap()));
|
||
}
|
||
|
||
#[test]
|
||
fn test_is_not_container_bridge_outside_range() {
|
||
assert!(!is_container_bridge(&"192.168.3.36".parse().unwrap()));
|
||
assert!(!is_container_bridge(&"10.0.0.1".parse().unwrap()));
|
||
assert!(!is_container_bridge(&"172.32.0.1".parse().unwrap()));
|
||
assert!(!is_container_bridge(&"172.15.255.255".parse().unwrap()));
|
||
assert!(!is_container_bridge(&"8.8.8.8".parse().unwrap()));
|
||
}
|
||
|
||
#[test]
|
||
fn test_is_link_local_range() {
|
||
assert!(is_link_local(&"169.254.0.1".parse().unwrap()));
|
||
assert!(is_link_local(&"169.254.255.255".parse().unwrap()));
|
||
}
|
||
|
||
#[test]
|
||
fn test_is_not_link_local() {
|
||
assert!(!is_link_local(&"192.168.1.1".parse().unwrap()));
|
||
assert!(!is_link_local(&"169.253.0.1".parse().unwrap()));
|
||
assert!(!is_link_local(&"169.255.0.1".parse().unwrap()));
|
||
}
|
||
|
||
#[test]
|
||
fn test_get_ip_addresses_excludes_docker_bridge() {
|
||
let addrs = get_ip_addresses().expect("Failed to get IP addresses");
|
||
for addr in &addrs {
|
||
let parsed: std::net::Ipv4Addr =
|
||
addr.parse().expect("Should parse as IPv4");
|
||
assert!(
|
||
!is_container_bridge(&parsed),
|
||
"IP '{}' is in Docker bridge range 172.16.0.0/12 — should be excluded",
|
||
addr
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_get_ip_addresses_excludes_link_local() {
|
||
let addrs = get_ip_addresses().expect("Failed to get IP addresses");
|
||
for addr in &addrs {
|
||
let parsed: std::net::Ipv4Addr =
|
||
addr.parse().expect("Should parse as IPv4");
|
||
assert!(
|
||
!is_link_local(&parsed),
|
||
"IP '{}' is link-local 169.254.0.0/16 — should be excluded",
|
||
addr
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_get_primary_ip_auto_detect_no_bridge() {
|
||
// In Docker containers, auto-detect may find no routable IPs — that's valid
|
||
match get_primary_ip(None, None) {
|
||
Ok(ip) => {
|
||
assert!(!ip.is_empty(), "Primary IP should not be empty");
|
||
let parsed: std::net::Ipv4Addr = ip.parse().expect("Primary IP should be valid IPv4");
|
||
assert!(
|
||
!is_container_bridge(&parsed),
|
||
"Auto-detected IP should not be Docker bridge"
|
||
);
|
||
}
|
||
Err(_) => {
|
||
eprintln!("NOTE: No routable IPs found — likely running inside a Docker container");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_get_primary_ip_explicit_override() {
|
||
let ip = get_primary_ip(None, Some("10.99.99.1"))
|
||
.expect("Failed with explicit IP");
|
||
assert_eq!(ip, "10.99.99.1");
|
||
}
|
||
|
||
#[test]
|
||
fn test_get_primary_ip_rejects_loopback_override() {
|
||
// Loopback override should fall back to auto-detect; if auto-detect also fails, that's valid
|
||
match get_primary_ip(None, Some("127.0.0.1")) {
|
||
Ok(ip) => assert_ne!(ip, "127.0.0.1"),
|
||
Err(_) => {
|
||
eprintln!("NOTE: Loopback rejected but no routable IPs for fallback — Docker container");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_get_primary_ip_invalid_override_falls_back() {
|
||
// Invalid IP override should fall back to auto-detect; if auto-detect also fails, that's valid
|
||
match get_primary_ip(None, Some("not-an-ip")) {
|
||
Ok(ip) => assert!(!ip.is_empty()),
|
||
Err(_) => {
|
||
eprintln!("NOTE: Invalid IP rejected but no routable IPs for fallback — Docker container");
|
||
}
|
||
}
|
||
}
|