rings-node 0.20.0

Rings is a structured peer-to-peer network implementation using WebRTC, Chord algorithm, and full WebAssembly (WASM) support.
Documentation
use rings_core::dht::DEFAULT_STORAGE_VIRTUAL_POSITIONS_PER_OWNER;
use rings_core::dht::MAX_STORAGE_VIRTUAL_POSITIONS_PER_OWNER;

use super::common::*;
use super::*;
use crate::processor::config::parse_webrtc_udp_port_range;

#[test]
fn test_webrtc_udp_port_range_absent_by_default() {
    let range = parse_webrtc_udp_port_range(None, None);

    assert!(matches!(range, core::result::Result::Ok(None)));
}

#[test]
fn test_webrtc_udp_port_range_accepts_valid_bounds() {
    let range = parse_webrtc_udp_port_range(Some(49160), Some(49200));

    assert!(matches!(
        range,
        Ok(Some(range)) if range.min() == 49160 && range.max() == 49200
    ));
}

#[test]
fn test_webrtc_udp_port_range_rejects_partial_bounds() {
    let range = parse_webrtc_udp_port_range(Some(49160), None);

    assert!(matches!(
        range,
        Err(Error::IncompleteWebrtcUdpPortRange {
            min: Some(49160),
            max: None
        })
    ));
}

#[test]
fn test_webrtc_udp_port_range_rejects_zero_bound() {
    let range = parse_webrtc_udp_port_range(Some(0), Some(49200));

    assert!(matches!(
        range,
        Err(Error::InvalidWebrtcUdpPortRange(
            rings_transport::webrtc_config::WebrtcUdpPortRangeError::ZeroBound {
                min: 0,
                max: 49200
            }
        ))
    ));
}

#[test]
fn test_webrtc_udp_port_range_rejects_inverted_bounds() {
    let range = parse_webrtc_udp_port_range(Some(49200), Some(49160));

    assert!(matches!(
        range,
        Err(Error::InvalidWebrtcUdpPortRange(
            rings_transport::webrtc_config::WebrtcUdpPortRangeError::Inverted {
                min: 49200,
                max: 49160
            }
        ))
    ));
}

#[test]
fn test_online_node_timing_requires_heartbeat_interval_less_than_ttl_when_enabled() {
    let key = SecretKey::random();
    let session_sk = SessionSk::new_with_seckey(&key).unwrap();
    let serialized = ProcessorConfigSerialized::new(
        0,
        "stun://stun.l.google.com:19302".to_string(),
        session_sk.dump().unwrap(),
        3,
    )
    .online_node_heartbeat_interval_secs(90)
    .online_node_ttl_secs(30);

    assert!(matches!(
        ProcessorConfig::try_from(serialized),
        Err(Error::InvalidConfig(message))
            if message.contains("online_node_heartbeat_interval")
                && message.contains("online_node_ttl")
    ));
}

#[test]
fn test_presence_advertisement_can_be_disabled() {
    let key = SecretKey::random();
    let session_sk = SessionSk::new_with_seckey(&key).unwrap();
    let serialized = ProcessorConfigSerialized::new(
        0,
        "stun://stun.l.google.com:19302".to_string(),
        session_sk.dump().unwrap(),
        3,
    )
    .online_node_heartbeat_interval_secs(90)
    .online_node_ttl_secs(30)
    .advertise_presence(false);

    let config = ProcessorConfig::try_from(serialized).unwrap();
    let builder = ProcessorBuilder::from_config(&config).unwrap();

    assert!(!builder.advertise_presence);
    assert!(builder.registration_tasks.is_empty());
}

#[test]
fn test_presence_advertisement_is_enabled_by_default() {
    let key = SecretKey::random();
    let session_sk = SessionSk::new_with_seckey(&key).unwrap();
    let serialized = ProcessorConfigSerialized::new(
        0,
        "stun://stun.l.google.com:19302".to_string(),
        session_sk.dump().unwrap(),
        3,
    );

    let config = ProcessorConfig::try_from(serialized).unwrap();
    let builder = ProcessorBuilder::from_config(&config).unwrap();

    assert!(builder.advertise_presence);
    assert_eq!(
        builder.dht_virtual_nodes,
        DEFAULT_STORAGE_VIRTUAL_POSITIONS_PER_OWNER
    );
}

#[test]
fn test_dht_virtual_nodes_rejects_values_above_cost_bound() {
    let key = SecretKey::random();
    let session_sk = SessionSk::new_with_seckey(&key).unwrap();
    let serialized = ProcessorConfigSerialized::new(
        0,
        "stun://stun.l.google.com:19302".to_string(),
        session_sk.dump().unwrap(),
        3,
    )
    .dht_virtual_nodes(MAX_STORAGE_VIRTUAL_POSITIONS_PER_OWNER.saturating_add(1));

    assert!(matches!(
        ProcessorConfig::try_from(serialized),
        Err(Error::InvalidConfig(message))
            if message.contains("dht_virtual_nodes")
                && message.contains(&MAX_STORAGE_VIRTUAL_POSITIONS_PER_OWNER.to_string())
    ));
}

#[test]
fn test_serialized_processor_config_defaults_dht_virtual_nodes() {
    let key = SecretKey::random();
    let session_sk = SessionSk::new_with_seckey(&key).unwrap();
    let yaml = format!(
        r#"
network_id: 0
ice_servers: stun://stun.l.google.com:19302
external_address: null
webrtc_udp_port_min: null
webrtc_udp_port_max: null
session_sk: "{}"
stabilize_interval: 15
online_node_heartbeat_interval_secs: 30
online_node_ttl_secs: 60
online_node_type: Native
advertise_presence: true
"#,
        session_sk.dump().unwrap()
    );

    let serialized = serde_yaml::from_str::<ProcessorConfigSerialized>(&yaml).unwrap();
    let config = ProcessorConfig::try_from(serialized).unwrap();
    let builder = ProcessorBuilder::from_config(&config).unwrap();

    assert_eq!(
        builder.dht_virtual_nodes,
        DEFAULT_STORAGE_VIRTUAL_POSITIONS_PER_OWNER
    );
}

#[test]
fn test_onion_relay_requires_presence_advertisement() {
    let key = SecretKey::random();
    let session_sk = SessionSk::new_with_seckey(&key).unwrap();
    let serialized = ProcessorConfigSerialized::new(
        0,
        "stun://stun.l.google.com:19302".to_string(),
        session_sk.dump().unwrap(),
        3,
    )
    .advertise_presence(false)
    .advertise_onion_relay(true);

    assert!(matches!(
        ProcessorConfig::try_from(serialized),
        Err(Error::InvalidConfig(message))
            if message.contains("advertise_onion_relay")
                && message.contains("advertise_presence")
    ));
}

#[test]
fn test_advertised_onion_exit_requires_open_policy() {
    let key = SecretKey::random();
    let session_sk = SessionSk::new_with_seckey(&key).unwrap();
    let serialized = ProcessorConfigSerialized::new(
        0,
        "stun://stun.l.google.com:19302".to_string(),
        session_sk.dump().unwrap(),
        3,
    )
    .advertise_onion_exit(true);

    assert!(matches!(
        ProcessorConfig::try_from(serialized),
        Err(Error::InvalidConfig(message)) if message.contains("allowed target")
    ));
}

#[test]
fn test_onion_exit_registration_task_can_run_without_presence_advertisement() -> Result<()> {
    let key = SecretKey::random();
    let session_sk = SessionSk::new_with_seckey(&key).unwrap();
    let serialized = ProcessorConfigSerialized::new(
        0,
        "stun://stun.l.google.com:19302".to_string(),
        session_sk.dump().unwrap(),
        3,
    )
    .advertise_presence(false)
    .advertise_onion_exit(true)
    .onion_exit_policy(onion_policy(&["example.com:443"], &[])?);

    let config = ProcessorConfig::try_from(serialized).unwrap();
    let processor = ProcessorBuilder::from_config(&config)
        .unwrap()
        .storage(Box::new(MemStorage::new()))
        .dht_finger_table_size(8)
        .build()
        .unwrap();

    assert_eq!(processor.registration_tasks.len(), 1);
    Ok(())
}

#[tokio::test]
async fn test_onion_relay_capability_is_advertised_in_online_descriptor() {
    let key = SecretKey::random();
    let session_sk = SessionSk::new_with_seckey(&key).unwrap();
    let config = ProcessorConfig::new(
        0,
        "stun://stun.l.google.com:19302".to_string(),
        session_sk,
        3,
    )
    .advertise_onion_relay(true);

    let processor = ProcessorBuilder::from_config(&config)
        .unwrap()
        .storage(Box::new(MemStorage::new()))
        .dht_finger_table_size(8)
        .build()
        .unwrap();
    let descriptor = processor.online_node_descriptor_at(get_epoch_ms()).unwrap();

    assert!(descriptor
        .capabilities
        .iter()
        .any(|capability| capability == ONION_RELAY_CAPABILITY));
}

#[test]
fn test_https_onion_exit_config_uses_https_only_service() {
    let key = SecretKey::random();
    let session_sk = SessionSk::new_with_seckey(&key).unwrap();
    let config = ProcessorConfig::new(
        0,
        "stun://stun.l.google.com:19302".to_string(),
        session_sk,
        3,
    )
    .enable_https_onion_exit();

    assert!(config.advertise_onion_exit);
    assert_eq!(config.onion_exit_services, https_onion_exit_services());
}

#[test]
fn test_default_onion_exit_config_uses_native_tcp_backed_services() {
    let key = SecretKey::random();
    let session_sk = SessionSk::new_with_seckey(&key).unwrap();
    let config = ProcessorConfig::new(
        0,
        "stun://stun.l.google.com:19302".to_string(),
        session_sk,
        3,
    )
    .enable_default_onion_exit();

    assert!(config.advertise_onion_exit);
    assert_eq!(config.onion_exit_services, default_onion_exit_services());
    assert_eq!(config.onion_exit_services, vec![
        OnionExitService::tcp(),
        OnionExitService::https()
    ]);
}

#[test]
fn test_reserved_https_onion_exit_service_accepts_tcp_transport() -> Result<()> {
    let key = SecretKey::random();
    let session_sk = SessionSk::new_with_seckey(&key).unwrap();
    let mut config = ProcessorConfig::new(
        0,
        "stun://stun.l.google.com:19302".to_string(),
        session_sk,
        3,
    )
    .advertise_onion_exit(true);
    config.onion_exit_services =
        vec![OnionExitService::new("https", OnionExitTransport::Tcp).expect("valid service")];
    config.onion_exit_policy = onion_policy(&["example.com:443"], &[])?;

    assert!(ProcessorBuilder::from_config(&config).is_ok());
    Ok(())
}

#[test]
fn test_custom_onion_exit_service_allows_explicit_transport() -> Result<()> {
    let key = SecretKey::random();
    let session_sk = SessionSk::new_with_seckey(&key).unwrap();
    let mut config = ProcessorConfig::new(
        0,
        "stun://stun.l.google.com:19302".to_string(),
        session_sk,
        3,
    )
    .advertise_onion_exit(true);
    config.onion_exit_services = vec![OnionExitService::new("web", OnionExitTransport::Tcp)?];
    config.onion_exit_policy = onion_policy(&["example.com:443"], &[])?;

    assert!(ProcessorBuilder::from_config(&config).is_ok());
    Ok(())
}