#![allow(unused_imports)]
use std::net::SocketAddr;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpListener;
use tokio::time::{Duration, timeout};
use crate::ftp::connection::{FtpClient, FtpMode, FtpResponse};
async fn start_mock_ftp_server()
-> std::result::Result<(SocketAddr, tokio::task::JoinHandle<()>), Box<dyn std::error::Error>> {
let listener = TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;
let handle = tokio::spawn(async move {
if let Ok((stream, _)) = listener.accept().await {
let mut stream = BufReader::new(stream);
stream
.write_all(b"220 Mock FTP Server Ready\r\n")
.await
.ok();
stream.flush().await.ok();
let mut line = String::new();
loop {
line.clear();
match stream.read_line(&mut line).await {
Ok(0) | Err(_) => break,
_ => {}
}
let cmd = line.trim();
let response = if cmd.to_uppercase().starts_with("USER ") {
"331 Please specify password\r\n".to_string()
} else if cmd.to_uppercase().starts_with("PASS ") {
"230 Login successful\r\n".to_string()
} else if cmd.to_uppercase() == "EPSV" {
format!(
"229 Entering Extended Passive Mode (|||{}|)\r\n",
addr.port() + 1
)
} else if cmd.to_uppercase() == "PASV" {
"227 Entering Passive Mode (127,0,0,1,195,123)\r\n".to_string()
} else if cmd.to_uppercase() == "TYPE I" {
"200 Switching to Binary mode\r\n".to_string()
} else if cmd.to_uppercase() == "TYPE A" {
"200 Switching to ASCII mode\r\n".to_string()
} else if cmd.to_uppercase() == "PWD" {
"257 \"/\" is current directory\r\n".to_string()
} else if cmd.to_uppercase().starts_with("CWD ") {
"250 Directory successfully changed\r\n".to_string()
} else if cmd.to_uppercase() == "QUIT" {
"221 Goodbye\r\n".to_string()
} else if cmd.to_uppercase().starts_with("REST ") {
"350 Restart position accepted\r\n".to_string()
} else if cmd.to_uppercase().starts_with("RETR ") {
"150 Opening BINARY mode data connection\r\n".to_string()
} else if cmd.to_uppercase().starts_with("LIST ") || cmd.to_uppercase() == "LIST" {
"150 Here comes the directory listing\r\n".to_string()
} else if cmd.to_uppercase() == "ABOR" {
"226 Abort successful\r\n".to_string()
} else if cmd.to_uppercase().starts_with("EPRT ")
|| cmd.to_uppercase().starts_with("PORT ")
{
"200 Command successful\r\n".to_string()
} else {
format!("502 Command not implemented: {}\r\n", cmd)
};
stream.write_all(response.as_bytes()).await.ok();
stream.flush().await.ok();
if cmd.to_uppercase() == "QUIT" {
break;
}
}
}
});
tokio::time::sleep(Duration::from_millis(50)).await;
Ok((addr, handle))
}
#[tokio::test]
async fn test_passive_mode_connection() -> Result<(), Box<dyn std::error::Error>> {
let (server_addr, server_handle) = start_mock_ftp_server().await?;
let mut client = FtpClient::connect(
server_addr.ip().to_string().as_str(),
server_addr.port(),
FtpMode::Passive,
)
.await?;
client.login("anonymous", "test@test.com").await?;
let pwd = client.pwd().await?;
assert_eq!(pwd, "/", "PWD 应返回根目录");
client.cwd("/").await?;
client.set_binary_mode(true).await?;
client.quit().await?;
server_handle.await?;
println!("\u{2705} 被动模式连接测试通过");
Ok(())
}
#[tokio::test]
async fn test_active_mode_connection() -> Result<(), Box<dyn std::error::Error>> {
let (server_addr, server_handle) = start_mock_ftp_server().await?;
let mut client = FtpClient::connect(
server_addr.ip().to_string().as_str(),
server_addr.port(),
FtpMode::Active,
)
.await?;
client.login("admin", "password123").await?;
assert_eq!(client.mode, FtpMode::Active, "客户端应该处于主动模式");
client.set_binary_mode(true).await?;
client.quit().await?;
server_handle.await?;
println!("\u{2705} 主动模式连接测试通过");
Ok(())
}
#[tokio::test]
async fn test_binary_type_setting() -> Result<(), Box<dyn std::error::Error>> {
let (server_addr, server_handle) = start_mock_ftp_server().await?;
let mut client = FtpClient::connect(
server_addr.ip().to_string().as_str(),
server_addr.port(),
FtpMode::Passive,
)
.await?;
client.login("user", "pass").await?;
assert!(!client.binary_mode, "初始状态应为 ASCII 模式");
client.set_binary_mode(true).await?;
assert!(client.binary_mode, "应该已切换到二进制模式");
client.set_binary_mode(false).await?;
assert!(!client.binary_mode, "应该已切换回 ASCII 模式");
client.set_binary_mode(true).await?;
assert!(client.binary_mode, "应该再次为二进制模式");
client.quit().await?;
server_handle.await?;
println!("\u{2705} 二进制模式设置测试通过");
Ok(())
}
#[test]
fn test_directory_listing_parse() {
println!("\n=== 目录列表解析测试 ===\n");
let unix_file = "-rw-r--r-- 1 owner group 1024 Mar 15 2024 document.pdf";
let file_info = FtpClient::parse_list_line(unix_file);
assert!(file_info.is_some(), "应该能解析 Unix 文件行");
let info = file_info.unwrap();
assert_eq!(info.name, "document.pdf");
assert_eq!(info.size, 1024);
assert!(!info.is_dir, "文件不应该被识别为目录");
println!("✓ Unix 文件解析: {} ({} bytes)", info.name, info.size);
let unix_dir = "drwxr-xr-x 2 owner staff 4096 Jan 1 00:00 my_folder";
let dir_info = FtpClient::parse_list_line(unix_dir);
assert!(dir_info.is_some(), "应该能解析 Unix 目录行");
let dir = dir_info.unwrap();
assert_eq!(dir.name, "my_folder");
assert_eq!(dir.size, 4096);
assert!(dir.is_dir, "目录应该被正确识别");
println!("✓ Unix 目录解析: {} [DIR]", dir.name);
let unix_link = "lrwxrwxrwx 1 user staff 8 Feb 28 14:30 link.txt -> target.txt";
let link_info = FtpClient::parse_list_line(unix_link);
assert!(link_info.is_some(), "应该能解析符号链接");
let link = link_info.unwrap();
assert_eq!(
link.name, "link.txt",
"符号链接名应该是 'link.txt' 而不是目标"
);
assert!(!link.is_dir, "符号链接本身不应是目录");
println!("✓ Unix 符号链接解析: {} -> (target stripped)", link.name);
let unix_hidden = "-rw------- 1 user staff 512 Apr 10 09:15 .bashrc";
let hidden_info = FtpClient::parse_list_line(unix_hidden);
assert!(hidden_info.is_some(), "应该能解析隐藏文件");
let hidden = hidden_info.unwrap();
assert_eq!(hidden.name, ".bashrc");
assert_eq!(hidden.size, 512);
println!("✓ Unix 隐藏文件解析: {}", hidden.name);
let dot = "drwxr-xr-x 2 user staff 4096 Jan 1 00:00 .";
let dotdot = "drwxr-xr-x 2 user staff 4096 Jan 1 00:00 ..";
assert!(
FtpClient::parse_list_line(dot).is_none(),
"'.' 条目应该被忽略"
);
assert!(
FtpClient::parse_list_line(dotdot).is_none(),
"'..' 条目应该被忽略"
);
println!("✓ 特殊目录条目 (. 和 ..) 正确忽略");
let win_file = "03-15-24 10:30PM 1024 report.docx";
let win_file_info = FtpClient::parse_list_line(win_file);
assert!(win_file_info.is_some(), "应该能解析 Windows 文件行");
let win_f = win_file_info.unwrap();
assert_eq!(win_f.name, "report.docx");
assert_eq!(win_f.size, 1024);
assert!(!win_f.is_dir);
println!("✓ Windows 文件解析: {} ({} bytes)", win_f.name, win_f.size);
let win_dir = "01-01-24 10:00AM <DIR> Documents";
let win_dir_info = FtpClient::parse_list_line(win_dir);
assert!(win_dir_info.is_some(), "应该能解析 Windows 目录行");
let win_d = win_dir_info.unwrap();
assert_eq!(win_d.name, "Documents");
assert!(win_d.is_dir, "Windows 目录应该被正确识别");
println!("✓ Windows 目录解析: {} [DIR]", win_d.name);
let mlsd_file = "type=file;size=2048;modify=20240315143000;perm=r;unique=U1FE90; readme.txt";
let mlsd_file_info = FtpClient::parse_list_line(mlsd_file);
assert!(mlsd_file_info.is_some(), "应该能解析 MLSD 文件行");
let mlsd_f = mlsd_file_info.unwrap();
assert_eq!(mlsd_f.name, "readme.txt");
assert_eq!(mlsd_f.size, 2048);
assert!(!mlsd_f.is_dir);
println!("✓ MLSD 文件解析: {} ({} bytes)", mlsd_f.name, mlsd_f.size);
let mlsd_dir = "type=dir;size=4096;modify=20240101000000;perm=elcmf;unique=U1FE91; uploads";
let mlsd_dir_info = FtpClient::parse_list_line(mlsd_dir);
assert!(mlsd_dir_info.is_some(), "应该能解析 MLSD 目录行");
let mlsd_d = mlsd_dir_info.unwrap();
assert_eq!(mlsd_d.name, "uploads");
assert!(mlsd_d.is_dir, "MLSD 目录应该被正确识别");
println!("✓ MLSD 目录解析: {} [DIR]", mlsd_d.name);
let space_name = "-rw-r--r-- 1 user staff 5678 Apr 20 11:00 my document with spaces.txt";
let space_info = FtpClient::parse_list_line(space_name);
assert!(space_info.is_some(), "应该能处理带空格的文件名");
let space_f = space_info.unwrap();
assert_eq!(space_f.name, "my document with spaces.txt");
assert_eq!(space_f.size, 5678);
println!("✓ 带空格文件名解析: '{}'", space_f.name);
let invalid = "this is not a valid ftp listing line";
assert!(
FtpClient::parse_list_line(invalid).is_none(),
"无法识别的格式应返回 None"
);
println!("✓ 无效格式正确返回 None");
println!("\n=== 所有目录列表解析测试通过 \u{2705} ===\n");
}
#[tokio::test]
async fn test_resume_download_rest_command() -> Result<(), Box<dyn std::error::Error>> {
use tracing::debug;
let (server_addr, server_handle) = start_mock_ftp_server().await?;
let mut client = FtpClient::connect(
server_addr.ip().to_string().as_str(),
server_addr.port(),
FtpMode::Passive,
)
.await?;
client.login("user", "pass").await?;
client.set_binary_mode(true).await?;
let result = client.download_file("large_file.bin", Some(1024)).await;
match result {
Err(e) => {
debug!("预期的下载错误(REST 已发送): {}", e);
}
Ok(_) => {
}
}
client.quit().await?;
server_handle.await?;
println!("\u{2705} 断点续传 REST 命令测试通过");
Ok(())
}
#[tokio::test]
async fn test_ftp_error_code_handling() -> Result<(), Box<dyn std::error::Error>> {
use crate::error::{Aria2Error, RecoverableError};
println!("\n=== FTP 错误码处理测试 ===\n");
println!("测试 425 错误...");
let resp_425 = FtpResponse {
code: 425,
message: "Can't open data connection".to_string(),
};
assert!(!resp_425.is_success());
assert!(!resp_425.is_positive_completion());
println!("✓ 425 响应正确识别为错误");
println!("测试 426 错误...");
let resp_426 = FtpResponse {
code: 426,
message: "Connection closed; transfer aborted".to_string(),
};
assert!(!resp_426.is_success());
println!("✓ 426 响应正确识别为错误");
println!("测试 550 错误...");
let resp_550 = FtpResponse {
code: 550,
message: "File not found".to_string(),
};
assert!(!resp_550.is_success());
let error_550 = Aria2Error::Recoverable(RecoverableError::ServerError { code: 550 });
match error_550 {
Aria2Error::Recoverable(RecoverableError::ServerError { code }) => {
assert_eq!(code, 550);
println!("✓ 550 正确映射为 RecoverableError::ServerError {{ code: 550 }}");
}
_ => panic!("550 应该映射为 ServerError"),
}
println!("✓ 550 错误处理正确");
println!("测试 530 错误...");
let resp_530 = FtpResponse {
code: 530,
message: "Please login with USER and PASS".to_string(),
};
assert!(!resp_530.is_success());
let error_530 = Aria2Error::Recoverable(RecoverableError::ServerError { code: 530 });
match error_530 {
Aria2Error::Recoverable(RecoverableError::ServerError { code }) => {
assert_eq!(code, 530);
println!("✓ 530 正确映射为 RecoverableError::ServerError {{ code: 530 }}");
}
_ => panic!("530 应该映射为 ServerError"),
}
println!("✓ 530 未登录错误处理正确");
println!("\n测试超时错误...");
let timeout_error = Aria2Error::Recoverable(RecoverableError::Timeout);
match timeout_error {
Aria2Error::Recoverable(RecoverableError::Timeout) => {
println!("✓ 超时错误正确创建");
}
_ => panic!("应该是 Timeout 错误"),
}
println!("\n=== 所有 FTP 错误码处理测试通过 \u{2705} ===\n");
Ok(())
}