use super::*;
use std::collections::HashMap;
use std::fs;
use tempfile::TempDir;
#[cfg(unix)]
static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn test_empty_config() {
let config = Config::new();
assert!(config.node.identity.nsec.is_none());
assert!(!config.has_identity());
}
#[test]
fn test_parse_yaml_with_nsec() {
let yaml = r#"
node:
identity:
nsec: nsec1qyqsqypqxqszqg9qyqsqypqxqszqg9qyqsqypqxqszqg9qyqsqypqxfnm5g9
"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
assert!(config.node.identity.nsec.is_some());
assert!(config.has_identity());
}
#[test]
fn test_parse_yaml_with_hex() {
let yaml = r#"
node:
identity:
nsec: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
assert!(config.node.identity.nsec.is_some());
let identity = config.create_identity().unwrap();
assert!(!identity.npub().is_empty());
}
#[test]
fn test_parse_yaml_empty() {
let yaml = "";
let config: Config = serde_yaml::from_str(yaml).unwrap();
assert!(config.node.identity.nsec.is_none());
}
#[test]
fn test_parse_yaml_partial() {
let yaml = r#"
node:
identity: {}
"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
assert!(config.node.identity.nsec.is_none());
}
#[test]
fn test_merge_configs() {
let mut base = Config::new();
base.node.identity.nsec = Some("base_nsec".to_string());
let mut override_config = Config::new();
override_config.node.identity.nsec = Some("override_nsec".to_string());
base.merge(override_config);
assert_eq!(base.node.identity.nsec, Some("override_nsec".to_string()));
}
#[test]
fn test_merge_preserves_base_when_override_empty() {
let mut base = Config::new();
base.node.identity.nsec = Some("base_nsec".to_string());
let override_config = Config::new();
base.merge(override_config);
assert_eq!(base.node.identity.nsec, Some("base_nsec".to_string()));
}
#[test]
fn test_create_identity_from_nsec() {
let mut config = Config::new();
config.node.identity.nsec =
Some("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20".to_string());
let identity = config.create_identity().unwrap();
assert!(!identity.npub().is_empty());
}
#[test]
fn test_create_identity_generates_new() {
let config = Config::new();
let identity = config.create_identity().unwrap();
assert!(!identity.npub().is_empty());
}
#[test]
fn test_load_from_file() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("fips.yaml");
let yaml = r#"
node:
identity:
nsec: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
"#;
fs::write(&config_path, yaml).unwrap();
let config = Config::load_file(&config_path).unwrap();
assert!(config.node.identity.nsec.is_some());
}
#[test]
fn test_load_from_paths_merges() {
let temp_dir = TempDir::new().unwrap();
let low_priority = temp_dir.path().join("low.yaml");
let high_priority = temp_dir.path().join("high.yaml");
fs::write(
&low_priority,
r#"
node:
identity:
nsec: "low_priority_nsec"
"#,
)
.unwrap();
fs::write(
&high_priority,
r#"
node:
identity:
nsec: "high_priority_nsec"
"#,
)
.unwrap();
let paths = vec![low_priority.clone(), high_priority.clone()];
let (config, loaded) = Config::load_from_paths(&paths).unwrap();
assert_eq!(loaded.len(), 2);
assert_eq!(
config.node.identity.nsec,
Some("high_priority_nsec".to_string())
);
}
#[test]
fn test_load_from_paths_deep_merges_partial_node_sections() {
let temp_dir = TempDir::new().unwrap();
let low_priority = temp_dir.path().join("low.yaml");
let high_priority = temp_dir.path().join("high.yaml");
fs::write(
&low_priority,
r#"
node:
limits:
max_peers: 4096
connected_udp:
max_peers: 256
fd_reserve: 2048
"#,
)
.unwrap();
fs::write(
&high_priority,
r#"
node:
identity:
nsec: "high_priority_nsec"
connected_udp:
enabled: false
"#,
)
.unwrap();
let (config, loaded) =
Config::load_from_paths(&[low_priority.clone(), high_priority.clone()]).unwrap();
assert_eq!(loaded, vec![low_priority, high_priority]);
assert_eq!(config.node.limits.max_peers, 4096);
assert!(!config.node.connected_udp.enabled);
assert_eq!(config.node.connected_udp.max_peers, 256);
assert_eq!(config.node.connected_udp.fd_reserve, 2048);
assert_eq!(
config.node.identity.nsec,
Some("high_priority_nsec".to_string())
);
}
#[test]
fn test_load_skips_missing_files() {
let temp_dir = TempDir::new().unwrap();
let existing = temp_dir.path().join("exists.yaml");
let missing = temp_dir.path().join("missing.yaml");
fs::write(
&existing,
r#"
node:
identity:
nsec: "existing_nsec"
"#,
)
.unwrap();
let paths = vec![missing, existing.clone()];
let (config, loaded) = Config::load_from_paths(&paths).unwrap();
assert_eq!(loaded.len(), 1);
assert_eq!(loaded[0], existing);
assert_eq!(config.node.identity.nsec, Some("existing_nsec".to_string()));
}
#[test]
fn test_search_paths_includes_expected() {
let paths = Config::search_paths();
assert!(paths.iter().any(|p| p.ends_with("fips.yaml")));
#[cfg(unix)]
assert!(
paths
.iter()
.any(|p| p.starts_with("/etc/fips") && p.ends_with("fips.yaml"))
);
}
#[test]
fn test_to_yaml() {
let mut config = Config::new();
config.node.identity.nsec = Some("test_nsec".to_string());
let yaml = config.to_yaml().unwrap();
assert!(yaml.contains("node:"));
assert!(yaml.contains("identity:"));
assert!(yaml.contains("nsec:"));
assert!(yaml.contains("test_nsec"));
}
#[test]
fn test_key_file_write_read_roundtrip() {
let temp_dir = TempDir::new().unwrap();
let key_path = temp_dir.path().join("fips.key");
let identity = crate::Identity::generate();
let nsec = crate::encode_nsec(&identity.keypair().secret_key());
write_key_file(&key_path, &nsec).unwrap();
let loaded_nsec = read_key_file(&key_path).unwrap();
assert_eq!(loaded_nsec, nsec);
let loaded_identity = crate::Identity::from_secret_str(&loaded_nsec).unwrap();
assert_eq!(loaded_identity.npub(), identity.npub());
}
#[cfg(unix)]
#[test]
fn test_key_file_permissions() {
use std::os::unix::fs::MetadataExt;
let temp_dir = TempDir::new().unwrap();
let key_path = temp_dir.path().join("fips.key");
write_key_file(&key_path, "nsec1test").unwrap();
let metadata = fs::metadata(&key_path).unwrap();
assert_eq!(metadata.mode() & 0o777, 0o600);
}
#[cfg(unix)]
#[test]
fn test_key_file_permissions_are_tightened_on_overwrite() {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let temp_dir = TempDir::new().unwrap();
let key_path = temp_dir.path().join("fips.key");
fs::write(&key_path, "old\n").unwrap();
fs::set_permissions(&key_path, fs::Permissions::from_mode(0o644)).unwrap();
write_key_file(&key_path, "nsec1test").unwrap();
let metadata = fs::metadata(&key_path).unwrap();
assert_eq!(metadata.mode() & 0o777, 0o600);
assert_eq!(read_key_file(&key_path).unwrap(), "nsec1test");
}
#[cfg(unix)]
#[test]
fn test_pub_file_permissions() {
use std::os::unix::fs::MetadataExt;
let temp_dir = TempDir::new().unwrap();
let pub_path = temp_dir.path().join("fips.pub");
write_pub_file(&pub_path, "npub1test").unwrap();
let metadata = fs::metadata(&pub_path).unwrap();
assert_eq!(metadata.mode() & 0o777, 0o644);
}
#[test]
fn test_key_file_empty_error() {
let temp_dir = TempDir::new().unwrap();
let key_path = temp_dir.path().join("fips.key");
fs::write(&key_path, "").unwrap();
let result = read_key_file(&key_path);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("empty"));
}
#[test]
fn test_key_file_whitespace_trimmed() {
let temp_dir = TempDir::new().unwrap();
let key_path = temp_dir.path().join("fips.key");
fs::write(&key_path, " nsec1test \n").unwrap();
let nsec = read_key_file(&key_path).unwrap();
assert_eq!(nsec, "nsec1test");
}
#[test]
fn test_key_file_path_derivation() {
let config_path = PathBuf::from("/etc/fips/fips.yaml");
assert_eq!(
key_file_path(&config_path),
PathBuf::from("/etc/fips/fips.key")
);
assert_eq!(
pub_file_path(&config_path),
PathBuf::from("/etc/fips/fips.pub")
);
}
#[cfg(windows)]
#[test]
fn test_key_file_write_read_roundtrip_windows() {
let temp_dir = TempDir::new().unwrap();
let key_path = temp_dir.path().join("fips.key");
let identity = crate::Identity::generate();
let nsec = crate::encode_nsec(&identity.keypair().secret_key());
write_key_file(&key_path, &nsec).unwrap();
let loaded_nsec = read_key_file(&key_path).unwrap();
assert_eq!(loaded_nsec, nsec);
let loaded_identity = crate::Identity::from_secret_str(&loaded_nsec).unwrap();
assert_eq!(loaded_identity.npub(), identity.npub());
}
#[test]
fn test_resolve_identity_from_config() {
let mut config = Config::new();
config.node.identity.nsec =
Some("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20".to_string());
let resolved = resolve_identity(&config, &[]).unwrap();
assert!(matches!(resolved.source, IdentitySource::Config));
}
#[test]
fn test_resolve_identity_ephemeral_by_default() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("fips.yaml");
fs::write(&config_path, "node:\n identity: {}\n").unwrap();
let config = Config::load_file(&config_path).unwrap();
assert!(!config.node.identity.persistent);
let resolved = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
assert!(matches!(resolved.source, IdentitySource::Ephemeral));
let key_path = temp_dir.path().join("fips.key");
let pub_path = temp_dir.path().join("fips.pub");
assert!(key_path.exists());
assert!(pub_path.exists());
}
#[test]
fn test_resolve_identity_ephemeral_changes_each_call() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("fips.yaml");
fs::write(&config_path, "node:\n identity: {}\n").unwrap();
let config = Config::load_file(&config_path).unwrap();
let first = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
let second = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
assert_ne!(first.nsec, second.nsec);
}
#[test]
fn test_resolve_identity_persistent_from_key_file() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("fips.yaml");
let key_path = temp_dir.path().join("fips.key");
fs::write(&config_path, "node:\n identity:\n persistent: true\n").unwrap();
let identity = crate::Identity::generate();
let nsec = crate::encode_nsec(&identity.keypair().secret_key());
write_key_file(&key_path, &nsec).unwrap();
let config = Config::load_file(&config_path).unwrap();
assert!(config.node.identity.persistent);
let resolved = resolve_identity(&config, &[config_path]).unwrap();
assert!(matches!(resolved.source, IdentitySource::KeyFile(_)));
assert_eq!(resolved.nsec, nsec);
}
#[test]
fn test_resolve_identity_persistent_generates_and_persists() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("fips.yaml");
fs::write(&config_path, "node:\n identity:\n persistent: true\n").unwrap();
let config = Config::load_file(&config_path).unwrap();
let resolved = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
assert!(matches!(resolved.source, IdentitySource::Generated(_)));
let key_path = temp_dir.path().join("fips.key");
let pub_path = temp_dir.path().join("fips.pub");
assert!(key_path.exists());
assert!(pub_path.exists());
let resolved2 = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
assert!(matches!(resolved2.source, IdentitySource::KeyFile(_)));
assert_eq!(resolved.nsec, resolved2.nsec);
}
#[test]
fn test_to_yaml_empty_nsec_omitted() {
let config = Config::new();
let yaml = config.to_yaml().unwrap();
assert!(!yaml.contains("nsec:"));
}
#[test]
fn test_parse_transport_single_instance() {
let yaml = r#"
transports:
udp:
bind_addr: "0.0.0.0:2121"
mtu: 1400
"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
assert_eq!(config.transports.udp.len(), 1);
let instances: Vec<_> = config.transports.udp.iter().collect();
assert_eq!(instances.len(), 1);
assert_eq!(instances[0].0, None); assert_eq!(instances[0].1.bind_addr(), "0.0.0.0:2121");
assert_eq!(instances[0].1.mtu(), 1400);
}
#[test]
fn test_parse_transport_named_instances() {
let yaml = r#"
transports:
udp:
main:
bind_addr: "0.0.0.0:2121"
backup:
bind_addr: "192.168.1.100:2122"
mtu: 1280
"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
assert_eq!(config.transports.udp.len(), 2);
let instances: std::collections::HashMap<_, _> = config.transports.udp.iter().collect();
assert!(instances.contains_key(&Some("main")));
assert!(instances.contains_key(&Some("backup")));
assert_eq!(instances[&Some("main")].bind_addr(), "0.0.0.0:2121");
assert_eq!(instances[&Some("backup")].bind_addr(), "192.168.1.100:2122");
assert_eq!(instances[&Some("backup")].mtu(), 1280);
}
#[test]
fn test_parse_transport_empty() {
let yaml = r#"
transports: {}
"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
assert!(config.transports.udp.is_empty());
assert!(config.transports.is_empty());
}
#[test]
fn test_transport_instances_iter() {
let single = TransportInstances::Single(UdpConfig {
bind_addr: Some("0.0.0.0:2121".to_string()),
mtu: None,
..Default::default()
});
let items: Vec<_> = single.iter().collect();
assert_eq!(items.len(), 1);
assert_eq!(items[0].0, None);
let mut map = HashMap::new();
map.insert("a".to_string(), UdpConfig::default());
map.insert("b".to_string(), UdpConfig::default());
let named = TransportInstances::Named(map);
let items: Vec<_> = named.iter().collect();
assert_eq!(items.len(), 2);
assert!(items.iter().all(|(name, _)| name.is_some()));
}
#[test]
fn test_parse_peer_config() {
let yaml = r#"
peers:
- npub: "npub1abc123"
alias: "gateway"
addresses:
- transport: udp
addr: "192.168.1.1:2121"
priority: 1
- transport: tor
addr: "xyz.onion:2121"
priority: 2
connect_policy: auto_connect
"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
assert_eq!(config.peers.len(), 1);
let peer = &config.peers[0];
assert_eq!(peer.npub, "npub1abc123");
assert_eq!(peer.alias, Some("gateway".to_string()));
assert_eq!(peer.addresses.len(), 2);
assert!(peer.is_auto_connect());
let sorted = peer.addresses_by_priority();
assert_eq!(sorted[0].transport, "udp");
assert_eq!(sorted[0].priority, 1);
assert_eq!(sorted[1].transport, "tor");
assert_eq!(sorted[1].priority, 2);
}
#[test]
fn test_parse_peer_minimal() {
let yaml = r#"
peers:
- npub: "npub1xyz"
addresses:
- transport: udp
addr: "10.0.0.1:2121"
"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
assert_eq!(config.peers.len(), 1);
let peer = &config.peers[0];
assert_eq!(peer.npub, "npub1xyz");
assert!(peer.alias.is_none());
assert!(peer.is_auto_connect());
assert_eq!(peer.addresses[0].priority, 100);
}
#[test]
fn test_parse_multiple_peers() {
let yaml = r#"
peers:
- npub: "npub1peer1"
addresses:
- transport: udp
addr: "10.0.0.1:2121"
- npub: "npub1peer2"
addresses:
- transport: udp
addr: "10.0.0.2:2121"
connect_policy: on_demand
"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
assert_eq!(config.peers.len(), 2);
assert_eq!(config.auto_connect_peers().count(), 1);
}
#[test]
fn test_peer_config_builder() {
let peer = PeerConfig::new("npub1test", "udp", "192.168.1.1:2121")
.with_alias("test-peer")
.with_address(PeerAddress::with_priority("tor", "xyz.onion:2121", 50));
assert_eq!(peer.npub, "npub1test");
assert_eq!(peer.alias, Some("test-peer".to_string()));
assert_eq!(peer.addresses.len(), 2);
assert!(peer.is_auto_connect());
}
#[test]
fn test_parse_nostr_discovery_config() {
let yaml = r#"
node:
discovery:
nostr:
enabled: true
advertise: false
policy: configured_only
open_discovery_max_pending: 12
app: "fips.nat.test.v1"
signal_ttl_secs: 45
advert_relays:
- "wss://relay-a.example"
dm_relays:
- "wss://relay-b.example"
stun_servers:
- "stun:stun.example.org:3478"
peers:
- npub: "npub1peer"
addresses:
- transport: udp
addr: "nat"
"#;
let config: Config = serde_yaml::from_str(yaml).unwrap();
assert!(config.node.discovery.nostr.enabled);
assert!(!config.node.discovery.nostr.advertise);
assert_eq!(config.node.discovery.nostr.app, "fips.nat.test.v1");
assert_eq!(config.node.discovery.nostr.signal_ttl_secs, 45);
assert_eq!(
config.node.discovery.nostr.policy,
NostrDiscoveryPolicy::ConfiguredOnly
);
assert_eq!(config.node.discovery.nostr.open_discovery_max_pending, 12);
assert_eq!(
config.node.discovery.nostr.advert_relays,
vec!["wss://relay-a.example".to_string()]
);
assert_eq!(
config.node.discovery.nostr.dm_relays,
vec!["wss://relay-b.example".to_string()]
);
assert_eq!(
config.node.discovery.nostr.stun_servers,
vec!["stun:stun.example.org:3478".to_string()]
);
assert_eq!(
config.peers[0].addresses[0].addr, "nat",
"udp:nat address should parse without special-casing in YAML"
);
}
#[test]
fn test_validate_transport_advert_requires_nostr_enabled() {
let mut config = Config::default();
config.transports.udp = TransportInstances::Single(UdpConfig {
advertise_on_nostr: Some(true),
..Default::default()
});
config.node.discovery.nostr.enabled = false;
let err = config.validate().expect_err("validation should fail");
assert!(err.to_string().contains("advertise_on_nostr"));
config.transports.udp = TransportInstances::default();
config.transports.webrtc = TransportInstances::Single(WebRtcConfig {
advertise_on_nostr: Some(true),
..Default::default()
});
let err = config.validate().expect_err("validation should fail");
assert!(err.to_string().contains("advertise_on_nostr"));
}
#[test]
fn test_validate_empty_peer_addresses_require_nostr_enabled() {
let mut config = Config {
peers: vec![PeerConfig {
npub: "npub1peer".to_string(),
..Default::default()
}],
..Default::default()
};
config.node.discovery.nostr.enabled = false;
let err = config.validate().expect_err("validation should fail");
assert!(err.to_string().contains("node.discovery.nostr"));
}
#[test]
fn test_validate_peer_addresses_optional_with_nostr_enabled() {
let mut config = Config {
peers: vec![PeerConfig {
npub: "npub1peer".to_string(),
..Default::default()
}],
..Default::default()
};
let err = config.validate().expect_err("validation should fail");
assert!(err.to_string().contains("at least one address"));
config.node.discovery.nostr.enabled = true;
config
.validate()
.expect("Nostr discovery should allow empty addresses");
}
#[test]
fn test_validate_nat_udp_advert_requires_relays_and_stun() {
let mut config = Config::default();
config.node.discovery.nostr.enabled = true;
config.node.discovery.nostr.dm_relays.clear();
config.transports.udp = TransportInstances::Single(UdpConfig {
advertise_on_nostr: Some(true),
public: Some(false),
..Default::default()
});
let err = config.validate().expect_err("validation should fail");
assert!(err.to_string().contains("dm_relays"));
config.node.discovery.nostr.dm_relays = vec!["wss://relay.example".to_string()];
config.node.discovery.nostr.stun_servers.clear();
let err = config.validate().expect_err("validation should fail");
assert!(err.to_string().contains("stun_servers"));
}
#[test]
fn test_validate_webrtc_advert_requires_relays() {
let mut config = Config::default();
config.node.discovery.nostr.enabled = true;
config.node.discovery.nostr.dm_relays.clear();
config.transports.webrtc = TransportInstances::Single(WebRtcConfig {
advertise_on_nostr: Some(true),
..Default::default()
});
let err = config.validate().expect_err("validation should fail");
assert!(err.to_string().contains("dm_relays"));
if let TransportInstances::Single(cfg) = &mut config.transports.webrtc {
cfg.signal_relays = Some(vec!["wss://relay.example".to_string()]);
}
config
.validate()
.expect("WebRTC transport-specific relays should satisfy validation");
}
#[test]
fn test_is_loopback_addr_str() {
assert!(is_loopback_addr_str("127.0.0.1:2121"));
assert!(is_loopback_addr_str("127.0.0.5:9999"));
assert!(is_loopback_addr_str("[::1]:2121"));
assert!(is_loopback_addr_str("::1:2121"));
assert!(is_loopback_addr_str("localhost:80"));
assert!(!is_loopback_addr_str("0.0.0.0:2121"));
assert!(!is_loopback_addr_str("192.168.1.1:2121"));
assert!(!is_loopback_addr_str("[fd00::1]:2121"));
assert!(!is_loopback_addr_str("core-vm.tail65015.ts.net:2121"));
assert!(!is_loopback_addr_str("example.com:443"));
}
#[cfg(unix)]
#[test]
fn test_resolve_default_socket_call_sites_agree() {
let _guard = ENV_MUTEX.lock().unwrap();
let control_client = default_control_path().to_string_lossy().into_owned();
let gateway_client = default_gateway_path().to_string_lossy().into_owned();
let control_daemon = ControlConfig::default().socket_path;
assert_eq!(control_daemon, control_client);
let control_dir = Path::new(&control_client)
.parent()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default();
let gateway_dir = Path::new(&gateway_client)
.parent()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default();
assert_eq!(control_dir, gateway_dir);
}
#[cfg(unix)]
#[test]
fn test_resolve_default_socket_xdg_when_no_run_fips() {
let _guard = ENV_MUTEX.lock().unwrap();
let temp_dir = TempDir::new().unwrap();
let prev_xdg = std::env::var("XDG_RUNTIME_DIR").ok();
unsafe {
std::env::set_var("XDG_RUNTIME_DIR", temp_dir.path());
}
let path = resolve_default_socket("control.sock");
unsafe {
match prev_xdg {
Some(value) => std::env::set_var("XDG_RUNTIME_DIR", value),
None => std::env::remove_var("XDG_RUNTIME_DIR"),
}
}
assert!(
path.starts_with("/run/fips/")
|| path.starts_with(&format!("{}/fips/", temp_dir.path().display())),
"expected /run/fips or XDG path, got: {path}"
);
}
#[cfg(unix)]
#[test]
fn test_resolve_default_socket_tmp_when_xdg_invalid() {
let _guard = ENV_MUTEX.lock().unwrap();
let prev_xdg = std::env::var("XDG_RUNTIME_DIR").ok();
let bogus = "/nonexistent-xdg-runtime-dir-for-fips-test-zzz";
unsafe {
std::env::set_var("XDG_RUNTIME_DIR", bogus);
}
let path = resolve_default_socket("gateway.sock");
unsafe {
match prev_xdg {
Some(value) => std::env::set_var("XDG_RUNTIME_DIR", value),
None => std::env::remove_var("XDG_RUNTIME_DIR"),
}
}
assert!(
path.starts_with("/run/fips/") || path == "/tmp/fips-gateway.sock",
"expected /run/fips or /tmp fallback, got: {path}"
);
assert!(
!path.starts_with(bogus),
"stale XDG_RUNTIME_DIR leaked into resolver: {path}"
);
}
#[test]
fn test_validate_loopback_bind_with_external_peer_rejected() {
use crate::config::PeerAddress;
let mut config = Config::default();
config.transports.udp = TransportInstances::Single(UdpConfig {
bind_addr: Some("127.0.0.1:2121".to_string()),
..Default::default()
});
config.peers = vec![PeerConfig {
npub: "npub1peer".to_string(),
addresses: vec![PeerAddress::new("udp", "core-vm.tail65015.ts.net:2121")],
..Default::default()
}];
let err = config.validate().expect_err("validation should fail");
let msg = err.to_string();
assert!(msg.contains("loopback"), "got: {msg}");
assert!(msg.contains("non-loopback"), "got: {msg}");
}
#[test]
fn test_validate_loopback_bind_with_loopback_peer_ok() {
use crate::config::PeerAddress;
let mut config = Config::default();
config.transports.udp = TransportInstances::Single(UdpConfig {
bind_addr: Some("127.0.0.1:2121".to_string()),
..Default::default()
});
config.peers = vec![PeerConfig {
npub: "npub1peer".to_string(),
addresses: vec![PeerAddress::new("udp", "127.0.0.2:2121")],
..Default::default()
}];
config
.validate()
.expect("loopback peer with loopback bind should validate");
}
#[test]
fn test_validate_outbound_only_exempt_from_loopback_check() {
use crate::config::PeerAddress;
let mut config = Config::default();
config.transports.udp = TransportInstances::Single(UdpConfig {
bind_addr: Some("127.0.0.1:2121".to_string()),
outbound_only: Some(true),
..Default::default()
});
config.peers = vec![PeerConfig {
npub: "npub1peer".to_string(),
addresses: vec![PeerAddress::new("udp", "core-vm.tail65015.ts.net:2121")],
..Default::default()
}];
config
.validate()
.expect("outbound_only should be exempt from the loopback check");
}
#[test]
fn test_outbound_only_forces_ephemeral_bind() {
let cfg = UdpConfig {
bind_addr: Some("127.0.0.1:2121".to_string()),
outbound_only: Some(true),
..Default::default()
};
assert_eq!(cfg.bind_addr(), "0.0.0.0:0");
assert!(cfg.outbound_only());
}
#[test]
fn test_outbound_only_forces_advertise_off() {
let cfg = UdpConfig {
advertise_on_nostr: Some(true),
outbound_only: Some(true),
..Default::default()
};
assert!(!cfg.advertise_on_nostr());
}
#[test]
fn test_udp_accept_connections_default_true() {
let cfg = UdpConfig::default();
assert!(cfg.accept_connections());
}