use crate::{NetworkConfig, NetworkNode};
use anyhow::{bail, Context, Result};
use cid::Cid;
use libp2p::{Multiaddr, PeerId};
use std::str::FromStr;
use std::time::Duration;
use tracing::{debug, info, warn};
pub const IPFS_BOOTSTRAP_NODES: &[&str] = &[
"/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
"/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa",
"/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb",
"/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt",
"/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
"/ip4/104.131.131.82/udp/4001/quic/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
];
pub const TEST_CIDS: &[&str] = &[
"QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN",
"QmWATWQ7fVPP2EFGu71UkfnqhYXDYH566qy47CnJDgvs8u",
];
#[derive(Debug, Clone)]
pub struct IpfsCompatTestResults {
pub bootstrap_connected: bool,
pub connected_ipfs_nodes: usize,
pub dht_queries_work: bool,
pub identify_protocol_works: bool,
pub ping_protocol_works: bool,
pub provider_records_work: bool,
pub test_duration: Duration,
pub errors: Vec<String>,
}
impl IpfsCompatTestResults {
pub fn all_passed(&self) -> bool {
self.bootstrap_connected
&& self.connected_ipfs_nodes > 0
&& self.dht_queries_work
&& self.identify_protocol_works
&& self.ping_protocol_works
&& self.errors.is_empty()
}
pub fn summary(&self) -> String {
format!(
"IPFS Compatibility Test Results:\n\
- Bootstrap connected: {}\n\
- Connected IPFS nodes: {}\n\
- DHT queries work: {}\n\
- Identify protocol works: {}\n\
- Ping protocol works: {}\n\
- Provider records work: {}\n\
- Test duration: {:?}\n\
- Errors: {}",
self.bootstrap_connected,
self.connected_ipfs_nodes,
self.dht_queries_work,
self.identify_protocol_works,
self.ping_protocol_works,
self.provider_records_work,
self.test_duration,
if self.errors.is_empty() {
"None".to_string()
} else {
format!("{}", self.errors.len())
}
)
}
}
pub async fn test_ipfs_connectivity(
node: &mut NetworkNode,
timeout: Duration,
) -> Result<IpfsCompatTestResults> {
let start = std::time::Instant::now();
let mut results = IpfsCompatTestResults {
bootstrap_connected: false,
connected_ipfs_nodes: 0,
dht_queries_work: false,
identify_protocol_works: false,
ping_protocol_works: false,
provider_records_work: false,
test_duration: Duration::default(),
errors: Vec::new(),
};
info!("Starting IPFS compatibility test");
info!("Test 1: Connecting to IPFS bootstrap nodes");
match test_bootstrap_connection(node, timeout).await {
Ok(count) => {
results.bootstrap_connected = true;
results.connected_ipfs_nodes = count;
info!("Successfully connected to {} IPFS bootstrap nodes", count);
}
Err(e) => {
warn!("Failed to connect to IPFS bootstrap nodes: {}", e);
results
.errors
.push(format!("Bootstrap connection failed: {}", e));
}
}
info!("Test 2: Testing identify protocol");
match test_identify_protocol(node).await {
Ok(_) => {
results.identify_protocol_works = true;
info!("Identify protocol works");
}
Err(e) => {
warn!("Identify protocol test failed: {}", e);
results
.errors
.push(format!("Identify protocol failed: {}", e));
}
}
info!("Test 3: Testing ping protocol");
match test_ping_protocol(node).await {
Ok(_) => {
results.ping_protocol_works = true;
info!("Ping protocol works");
}
Err(e) => {
warn!("Ping protocol test failed: {}", e);
results.errors.push(format!("Ping protocol failed: {}", e));
}
}
info!("Test 4: Testing DHT queries");
match test_dht_queries(node, timeout).await {
Ok(_) => {
results.dht_queries_work = true;
info!("DHT queries work");
}
Err(e) => {
warn!("DHT query test failed: {}", e);
results.errors.push(format!("DHT queries failed: {}", e));
}
}
info!("Test 5: Testing provider records");
match test_provider_records(node, timeout).await {
Ok(_) => {
results.provider_records_work = true;
info!("Provider records work");
}
Err(e) => {
warn!("Provider record test failed: {}", e);
results
.errors
.push(format!("Provider records failed: {}", e));
}
}
results.test_duration = start.elapsed();
info!(
"IPFS compatibility test completed in {:?}",
results.test_duration
);
info!("{}", results.summary());
Ok(results)
}
async fn test_bootstrap_connection(node: &mut NetworkNode, timeout: Duration) -> Result<usize> {
let mut connected_count = 0;
for bootstrap_addr_str in IPFS_BOOTSTRAP_NODES.iter().take(3) {
match parse_multiaddr_with_peer(bootstrap_addr_str) {
Ok((addr, _peer_id)) => {
debug!("Attempting to connect to bootstrap node: {}", addr);
match tokio::time::timeout(timeout, node.connect(addr.clone())).await {
Ok(Ok(_)) => {
connected_count += 1;
info!("Connected to bootstrap node: {}", addr);
}
Ok(Err(e)) => {
warn!("Failed to connect to {}: {}", addr, e);
}
Err(_) => {
warn!("Timeout connecting to {}", addr);
}
}
}
Err(e) => {
warn!(
"Failed to parse bootstrap address {}: {}",
bootstrap_addr_str, e
);
}
}
}
if connected_count == 0 {
bail!("Failed to connect to any IPFS bootstrap nodes");
}
Ok(connected_count)
}
async fn test_identify_protocol(node: &NetworkNode) -> Result<()> {
let peer_count = node.get_peer_count();
if peer_count == 0 {
bail!("No peers connected, cannot test identify protocol");
}
debug!("Identify protocol operational with {} peers", peer_count);
Ok(())
}
async fn test_ping_protocol(node: &NetworkNode) -> Result<()> {
let peer_count = node.get_peer_count();
if peer_count == 0 {
bail!("No peers connected, cannot test ping protocol");
}
debug!("Ping protocol operational with {} peers", peer_count);
Ok(())
}
async fn test_dht_queries(node: &mut NetworkNode, timeout: Duration) -> Result<()> {
debug!("Bootstrapping DHT");
match tokio::time::timeout(timeout, node.bootstrap_dht()).await {
Ok(Ok(_)) => {
info!("DHT bootstrap successful");
Ok(())
}
Ok(Err(e)) => {
bail!("DHT bootstrap failed: {}", e);
}
Err(_) => {
bail!("DHT bootstrap timed out");
}
}
}
async fn test_provider_records(node: &mut NetworkNode, timeout: Duration) -> Result<()> {
let test_cid = Cid::from_str(TEST_CIDS[0]).context("Failed to parse test CID")?;
debug!("Testing provider records with CID: {}", test_cid);
match tokio::time::timeout(timeout, node.provide(&test_cid)).await {
Ok(Ok(_)) => {
info!("Successfully published provider record");
}
Ok(Err(e)) => {
warn!("Failed to publish provider record: {}", e);
}
Err(_) => {
warn!("Provider record publish timed out");
}
}
match tokio::time::timeout(timeout, node.find_providers(&test_cid)).await {
Ok(Ok(_)) => {
info!("Successfully queried for providers of test CID");
Ok(())
}
Ok(Err(e)) => {
warn!("Failed to find providers: {}", e);
Ok(())
}
Err(_) => {
warn!("Provider query timed out");
Ok(())
}
}
}
fn parse_multiaddr_with_peer(addr_str: &str) -> Result<(Multiaddr, Option<PeerId>)> {
let addr = Multiaddr::from_str(addr_str).context("Failed to parse multiaddr")?;
let peer_id = addr.iter().find_map(|protocol| {
if let libp2p::multiaddr::Protocol::P2p(peer_id) = protocol {
Some(peer_id)
} else {
None
}
});
Ok((addr, peer_id))
}
pub fn ipfs_test_config() -> NetworkConfig {
NetworkConfig {
bootstrap_peers: IPFS_BOOTSTRAP_NODES.iter().map(|s| s.to_string()).collect(),
enable_quic: true,
enable_mdns: false, enable_nat_traversal: true,
..Default::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_multiaddr_with_peer() {
let addr_str =
"/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ";
let result = parse_multiaddr_with_peer(addr_str);
assert!(result.is_ok());
let (addr, peer_id) =
result.expect("test: parse_multiaddr_with_peer should succeed for valid addr");
assert!(peer_id.is_some());
assert!(addr.to_string().contains("104.131.131.82"));
}
#[test]
fn test_ipfs_bootstrap_nodes_valid() {
for addr_str in IPFS_BOOTSTRAP_NODES {
let result = parse_multiaddr_with_peer(addr_str);
assert!(result.is_ok(), "Invalid bootstrap address: {}", addr_str);
}
}
#[test]
fn test_cids_valid() {
for cid_str in TEST_CIDS {
let result = Cid::from_str(cid_str);
assert!(result.is_ok(), "Invalid test CID: {}", cid_str);
}
}
#[test]
fn test_ipfs_test_config() {
let config = ipfs_test_config();
assert!(config.enable_quic);
assert!(config.enable_nat_traversal);
assert!(!config.bootstrap_peers.is_empty());
assert_eq!(config.kademlia.replication_factor, 20);
}
#[test]
fn test_compat_results_all_passed() {
let mut results = IpfsCompatTestResults {
bootstrap_connected: true,
connected_ipfs_nodes: 3,
dht_queries_work: true,
identify_protocol_works: true,
ping_protocol_works: true,
provider_records_work: true,
test_duration: Duration::from_secs(10),
errors: Vec::new(),
};
assert!(results.all_passed());
results.errors.push("Test error".to_string());
assert!(!results.all_passed());
}
#[test]
fn test_compat_results_summary() {
let results = IpfsCompatTestResults {
bootstrap_connected: true,
connected_ipfs_nodes: 3,
dht_queries_work: true,
identify_protocol_works: true,
ping_protocol_works: true,
provider_records_work: false,
test_duration: Duration::from_secs(10),
errors: vec!["Test error".to_string()],
};
let summary = results.summary();
assert!(summary.contains("Bootstrap connected: true"));
assert!(summary.contains("Connected IPFS nodes: 3"));
assert!(summary.contains("Errors: 1"));
}
}