#[cfg(test)]
#[allow(unused_imports, dead_code)]
mod tests {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use tracing::info;
use crate::engine::bt_post_download_handler::{
DownloadStats as HookDownloadStats, DownloadStatus, HookContext, HookManager,
PostDownloadHook,
};
use crate::engine::bt_progress_info_file::{
BtProgress, BtProgressManager, DownloadStats as ProgressDownloadStats,
};
use crate::engine::lpd_manager::{LpdManager, LpdPeer};
use crate::request::request_group::{DownloadOptions, GroupId};
fn create_test_torrent_data() -> Vec<u8> {
vec![]
}
fn create_test_options() -> DownloadOptions {
DownloadOptions {
dir: Some(std::env::temp_dir().to_string_lossy().to_string()),
..Default::default()
}
}
#[test]
fn test_command_with_progress_manager_field() {
let temp_dir = std::env::temp_dir().join("bt_integration_test");
let _ = std::fs::create_dir_all(&temp_dir);
let manager = BtProgressManager::new(&temp_dir).expect("Failed to create progress manager");
assert!(!manager.get_progress_file_path(&[0u8; 20]).exists());
}
#[test]
fn test_command_default_has_no_progress() {
let option_field: Option<i32> = None;
assert!(
option_field.is_none(),
"Option field should default to None"
);
}
#[test]
fn test_command_default_has_no_lpd() {
let lpd_field: Option<Arc<LpdManager>> = None;
assert!(lpd_field.is_none(), "LPD manager should default to None");
}
#[test]
fn test_command_default_has_no_hooks() {
let hook_field: Option<Arc<HookManager>> = None;
assert!(hook_field.is_none(), "Hook manager should default to None");
}
#[test]
fn test_build_current_progress_returns_valid_struct() {
let temp_dir = std::env::temp_dir().join("bt_progress_build_test");
let _ = std::fs::create_dir_all(&temp_dir);
let manager = BtProgressManager::new(&temp_dir).unwrap();
let info_hash = [0xABu8; 20];
let progress = BtProgress {
info_hash,
bitfield: vec![0xFF, 0xFF],
peers: vec![],
stats: ProgressDownloadStats {
downloaded_bytes: 1024,
uploaded_bytes: 512,
upload_speed: 100.0,
download_speed: 200.0,
elapsed_seconds: 10,
},
piece_length: 256 * 1024,
total_size: 512 * 1024,
num_pieces: 2,
save_time: std::time::SystemTime::now(),
version: 1,
};
assert_eq!(progress.info_hash, info_hash);
assert_eq!(progress.num_pieces, 2);
assert_eq!(progress.piece_length, 256 * 1024);
assert!(
progress.completion_ratio() >= 1.0,
"Should be at least complete"
);
let hex = progress.to_hex_hash();
assert_eq!(hex.len(), 40, "Hex hash should be 40 characters");
assert!(
hex.starts_with("ababab"),
"Hex should start with expected pattern"
);
manager.save_progress(&info_hash, &progress).unwrap();
let loaded = manager.load_progress(&info_hash).unwrap();
assert_eq!(loaded.info_hash, info_hash);
assert_eq!(loaded.num_pieces, 2);
assert_eq!(loaded.stats.downloaded_bytes, 1024);
let _ = manager.remove_progress(&info_hash);
}
#[test]
fn test_lpd_peer_conversion_to_peeraddr() {
let lpd_peer = LpdPeer::new(
"0123456789abcdef0123456789abcdef01234567",
6881,
std::net::IpAddr::V4(std::net::Ipv4Addr::new(192, 168, 1, 100)),
);
let ip_str = lpd_peer.addr.to_string();
let port = lpd_peer.port;
assert_eq!(ip_str, "192.168.1.100");
assert_eq!(port, 6881);
let peer_addr_str = format!("{}:{}", ip_str, port);
assert_eq!(peer_addr_str, "192.168.1.100:6881");
}
#[test]
fn test_hook_context_construction_from_command() {
let gid = GroupId::new(42);
let file_path = std::path::PathBuf::from("/downloads/test_file.zip");
let ctx = HookContext {
gid,
file_path: file_path.clone(),
status: DownloadStatus::Complete,
stats: HookDownloadStats {
uploaded_bytes: 2048,
downloaded_bytes: 4096,
upload_speed: 150.5,
download_speed: 300.25,
elapsed_seconds: 15,
},
error: None,
};
assert_eq!(ctx.gid.value(), 42);
assert_eq!(ctx.file_path, file_path);
assert_eq!(ctx.status, DownloadStatus::Complete);
assert_eq!(ctx.filename(), "test_file.zip");
assert_eq!(ctx.extension(), "zip");
assert!(ctx.error.is_none());
assert_eq!(ctx.stats.downloaded_bytes, 4096);
assert_eq!(ctx.stats.uploaded_bytes, 2048);
assert!((ctx.stats.download_speed - 300.25).abs() < 0.01);
assert_eq!(ctx.stats.elapsed_seconds, 15);
}
#[tokio::test]
async fn test_progress_save_load_roundtrip_via_command() {
let temp_dir = std::env::temp_dir().join("bt_progress_roundtrip_test");
let _ = std::fs::create_dir_all(&temp_dir);
let manager = BtProgressManager::new(&temp_dir).unwrap();
let info_hash = [0xCDu8; 20];
let original = BtProgress {
info_hash,
bitfield: vec![0xF0], peers: vec![],
stats: ProgressDownloadStats {
downloaded_bytes: 1024 * 1024,
uploaded_bytes: 512 * 1024,
upload_speed: 0.0,
download_speed: 0.0,
elapsed_seconds: 60,
},
piece_length: 256 * 1024,
total_size: 1024 * 1024,
num_pieces: 4,
save_time: std::time::SystemTime::now(),
version: 1,
};
manager
.save_progress(&info_hash, &original)
.expect("Save should succeed");
let loaded = manager
.load_progress(&info_hash)
.expect("Load should succeed");
assert_eq!(loaded.info_hash, original.info_hash);
assert_eq!(loaded.num_pieces, original.num_pieces);
assert_eq!(loaded.piece_length, original.piece_length);
assert_eq!(loaded.total_size, original.total_size);
assert_eq!(loaded.version, original.version);
assert_eq!(
loaded.stats.downloaded_bytes,
original.stats.downloaded_bytes
);
assert_eq!(loaded.stats.uploaded_bytes, original.stats.uploaded_bytes);
assert_eq!(loaded.stats.elapsed_seconds, original.stats.elapsed_seconds);
info!(
hash = %loaded.to_hex_hash(),
ratio = loaded.completion_ratio(),
"Progress round-trip successful"
);
let _ = manager.remove_progress(&info_hash);
}
#[tokio::test]
async fn test_lpd_register_and_discover_via_command() {
let lpd = LpdManager::new();
let info_hash = "efefefefefefefefefefefefefefefefefefefef";
lpd.register_torrent(info_hash).await.unwrap();
let active = lpd.active_hashes.read().await;
assert!(
active.contains(info_hash),
"Should have 1 active download after registration"
);
drop(active);
let peers = lpd.get_peers_for(info_hash).await;
assert!(
peers.is_empty(),
"No peers should be discovered without real network"
);
lpd.unregister_torrent(info_hash).await;
let active_after = lpd.active_hashes.read().await;
assert!(
!active_after.contains(info_hash),
"Should have 0 active downloads after unregistration"
);
info!("LPD register/unregister cycle completed successfully");
}
#[tokio::test]
async fn test_hook_manager_on_complete_called() {
use std::sync::atomic::{AtomicBool, Ordering};
static HOOK_CALLED: AtomicBool = AtomicBool::new(false);
struct TestHook;
#[async_trait::async_trait]
impl PostDownloadHook for TestHook {
async fn on_complete(&self, _context: &HookContext) -> crate::error::Result<()> {
HOOK_CALLED.store(true, Ordering::SeqCst);
Ok(())
}
async fn on_error(
&self,
_context: &HookContext,
_error: &str,
) -> crate::error::Result<()> {
Ok(())
}
fn name(&self) -> &'static str {
"TestHook"
}
}
let mut manager =
HookManager::new(crate::engine::bt_post_download_handler::HookConfig::default());
manager.add_hook(Box::new(TestHook));
let ctx = HookContext {
gid: GroupId::new(99),
file_path: std::path::PathBuf::from("/tmp/test_complete.txt"),
status: DownloadStatus::Complete,
stats: HookDownloadStats {
downloaded_bytes: 9999,
..Default::default()
},
error: None,
};
let result = manager.fire_complete(&ctx).await;
assert!(result.is_ok(), "fire_complete should succeed");
assert!(
HOOK_CALLED.load(Ordering::SeqCst),
"TestHook.on_complete should have been called"
);
info!(
hook_count = manager.hook_count(),
"Hook manager fire_complete executed successfully"
);
}
#[test]
fn test_multiple_hooks_execution_order() {
use std::sync::atomic::{AtomicUsize, Ordering};
static CALL_ORDER: AtomicUsize = AtomicUsize::new(0);
static FIRST_CALLED: AtomicBool = AtomicBool::new(false);
static SECOND_CALLED: AtomicBool = AtomicBool::new(false);
struct FirstHook;
struct SecondHook;
#[async_trait::async_trait]
impl PostDownloadHook for FirstHook {
async fn on_complete(&self, _context: &HookContext) -> crate::error::Result<()> {
CALL_ORDER.fetch_add(1, Ordering::SeqCst);
FIRST_CALLED.store(true, Ordering::SeqCst);
Ok(())
}
async fn on_error(
&self,
_context: &HookContext,
_error: &str,
) -> crate::error::Result<()> {
Ok(())
}
fn name(&self) -> &'static str {
"FirstHook"
}
}
#[async_trait::async_trait]
impl PostDownloadHook for SecondHook {
async fn on_complete(&self, _context: &HookContext) -> crate::error::Result<()> {
CALL_ORDER.fetch_add(1, Ordering::SeqCst);
SECOND_CALLED.store(true, Ordering::SeqCst);
Ok(())
}
async fn on_error(
&self,
_context: &HookContext,
_error: &str,
) -> crate::error::Result<()> {
Ok(())
}
fn name(&self) -> &'static str {
"SecondHook"
}
}
let _ = (FirstHook, SecondHook);
assert!(CALL_ORDER.load(Ordering::SeqCst) == 0);
}
}