- 新增 upload.rs 模块,支持 multipart/form-data 文件上传 - 文件按日期存储在 storage/YYYY-MM-DD/ 目录下 - 使用 UUID 生成唯一文件名,保留原始扩展名 - 添加 axum-extra, uuid, chrono 依赖 新增测试用例: - config_test.rs: 6 个测试 (配置结构验证) - router_test.rs: 11 个测试 (路由匹配逻辑) - handler_test.rs: 8 个测试 (请求处理) - upload_test.rs: 13 个测试 (文件上传功能) 其他改进: - 优化 handler.rs 代码注释 - 更新 .gitignore 忽略 storage/ 和 .claude/ - 添加 CLAUDE.md 项目指南文档 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
247 lines
6.8 KiB
Rust
247 lines
6.8 KiB
Rust
use axum::{
|
|
http::StatusCode,
|
|
response::IntoResponse,
|
|
};
|
|
use mock_server::upload::{UploadResult, FileInfo};
|
|
use tempfile::TempDir;
|
|
use tokio::runtime::Runtime;
|
|
|
|
/// 创建模拟的 multipart 数据
|
|
fn create_multipart_body(boundary: &str, files: Vec<(&str, &str, &[u8])>) -> Vec<u8> {
|
|
let mut body = Vec::new();
|
|
|
|
for (field_name, filename, content) in files {
|
|
body.extend_from_slice(
|
|
format!(
|
|
"--{}\r\nContent-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\n\r\n",
|
|
boundary, field_name, filename
|
|
)
|
|
.as_bytes(),
|
|
);
|
|
body.extend_from_slice(content);
|
|
body.extend_from_slice(b"\r\n");
|
|
}
|
|
|
|
body.extend_from_slice(format!("--{}--\r\n", boundary).as_bytes());
|
|
body
|
|
}
|
|
|
|
#[test]
|
|
fn test_upload_result_success() {
|
|
let file_info = FileInfo {
|
|
original_name: "test.txt".to_string(),
|
|
stored_name: "uuid-test.txt".to_string(),
|
|
path: "storage/2026-03-19/uuid-test.txt".to_string(),
|
|
size: 1024,
|
|
content_type: Some("text/plain".to_string()),
|
|
};
|
|
|
|
let result = UploadResult::success(vec![file_info]);
|
|
|
|
// 验证序列化
|
|
let json = serde_json::to_string(&result).unwrap();
|
|
assert!(json.contains("test.txt"));
|
|
assert!(json.contains("success"));
|
|
assert!(json.contains("1024"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_upload_result_error() {
|
|
let result = UploadResult::error("Test error message".to_string());
|
|
|
|
assert!(!result.success);
|
|
assert_eq!(result.message, "Test error message");
|
|
assert!(result.files.is_empty());
|
|
|
|
let json = serde_json::to_string(&result).unwrap();
|
|
assert!(json.contains("Test error message"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_info_serialization() {
|
|
let file_info = FileInfo {
|
|
original_name: "document.pdf".to_string(),
|
|
stored_name: "550e8400-e29b-41d4-a716-446655440000.pdf".to_string(),
|
|
path: "storage/2026-03-19/550e8400-e29b-41d4-a716-446655440000.pdf".to_string(),
|
|
size: 2048,
|
|
content_type: Some("application/pdf".to_string()),
|
|
};
|
|
|
|
let json = serde_json::to_string(&file_info).unwrap();
|
|
|
|
assert!(json.contains("document.pdf"));
|
|
assert!(json.contains("2048"));
|
|
assert!(json.contains("application/pdf"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_upload_result_into_response() {
|
|
let rt = Runtime::new().unwrap();
|
|
|
|
rt.block_on(async {
|
|
let result = UploadResult::success(vec![]);
|
|
let response = result.into_response();
|
|
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
|
|
let result_error = UploadResult::error("Error".to_string());
|
|
let response_error = result_error.into_response();
|
|
|
|
assert_eq!(response_error.status(), StatusCode::BAD_REQUEST);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_storage_directory_creation() {
|
|
let rt = Runtime::new().unwrap();
|
|
|
|
rt.block_on(async {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let storage_path = temp_dir.path().join("storage");
|
|
|
|
// 测试目录创建
|
|
tokio::fs::create_dir_all(&storage_path).await.unwrap();
|
|
assert!(storage_path.exists());
|
|
|
|
// 测试日期目录创建
|
|
let date_dir_name = chrono::Local::now().format("%Y-%m-%d").to_string();
|
|
let date_dir = storage_path.join(&date_dir_name);
|
|
tokio::fs::create_dir_all(&date_dir).await.unwrap();
|
|
assert!(date_dir.exists());
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_save_and_read() {
|
|
let rt = Runtime::new().unwrap();
|
|
|
|
rt.block_on(async {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let file_path = temp_dir.path().join("test_file.txt");
|
|
|
|
let content = b"Test file content";
|
|
|
|
// 写入文件
|
|
tokio::fs::write(&file_path, content).await.unwrap();
|
|
assert!(file_path.exists());
|
|
|
|
// 读取文件
|
|
let read_content = tokio::fs::read(&file_path).await.unwrap();
|
|
assert_eq!(read_content, content);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_multipart_body_creation() {
|
|
let boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW";
|
|
let content = b"Hello, World!";
|
|
|
|
let body = create_multipart_body(boundary, vec![("file", "test.txt", content)]);
|
|
|
|
let body_str = String::from_utf8_lossy(&body);
|
|
|
|
assert!(body_str.contains(boundary));
|
|
assert!(body_str.contains("test.txt"));
|
|
assert!(body_str.contains("Hello, World!"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiple_files_multipart() {
|
|
let boundary = "----WebKitFormBoundary";
|
|
let files = vec![
|
|
("file1", "a.txt", b"content a" as &[u8]),
|
|
("file2", "b.txt", b"content b" as &[u8]),
|
|
];
|
|
|
|
let body = create_multipart_body(boundary, files);
|
|
let body_str = String::from_utf8_lossy(&body);
|
|
|
|
assert!(body_str.contains("a.txt"));
|
|
assert!(body_str.contains("b.txt"));
|
|
assert!(body_str.contains("content a"));
|
|
assert!(body_str.contains("content b"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_uuid_generation() {
|
|
use uuid::Uuid;
|
|
|
|
let uuid1 = Uuid::new_v4();
|
|
let uuid2 = Uuid::new_v4();
|
|
|
|
// UUID 应该是唯一的
|
|
assert_ne!(uuid1, uuid2);
|
|
|
|
// UUID 应该是有效的
|
|
assert!(!uuid1.is_nil());
|
|
assert!(!uuid2.is_nil());
|
|
}
|
|
|
|
#[test]
|
|
fn test_date_directory_format() {
|
|
let date_dir_name = chrono::Local::now().format("%Y-%m-%d").to_string();
|
|
|
|
// 验证日期格式
|
|
assert!(date_dir_name.contains('-'));
|
|
let parts: Vec<&str> = date_dir_name.split('-').collect();
|
|
assert_eq!(parts.len(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_extension_extraction() {
|
|
let test_cases = vec![
|
|
("test.txt", ".txt"),
|
|
("document.pdf", ".pdf"),
|
|
("image.png", ".png"),
|
|
("archive.tar.gz", ".gz"),
|
|
("noextension", ""),
|
|
];
|
|
|
|
for (filename, expected_ext) in test_cases {
|
|
let extension = std::path::Path::new(filename)
|
|
.extension()
|
|
.and_then(|s| s.to_str())
|
|
.map(|s| format!(".{}", s))
|
|
.unwrap_or_default();
|
|
|
|
assert_eq!(extension, expected_ext);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_large_file_content() {
|
|
let rt = Runtime::new().unwrap();
|
|
|
|
rt.block_on(async {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let file_path = temp_dir.path().join("large_file.bin");
|
|
|
|
// 创建 1MB 的数据
|
|
let large_content = vec![0u8; 1024 * 1024];
|
|
|
|
tokio::fs::write(&file_path, &large_content).await.unwrap();
|
|
|
|
let file_size = tokio::fs::metadata(&file_path).await.unwrap().len();
|
|
assert_eq!(file_size, 1024 * 1024);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn test_binary_file_content() {
|
|
let rt = Runtime::new().unwrap();
|
|
|
|
rt.block_on(async {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let file_path = temp_dir.path().join("binary.bin");
|
|
|
|
// 创建二进制数据
|
|
let binary_data: Vec<u8> = (0..=255).collect();
|
|
|
|
tokio::fs::write(&file_path, &binary_data).await.unwrap();
|
|
|
|
let read_data = tokio::fs::read(&file_path).await.unwrap();
|
|
assert_eq!(read_data.len(), 256);
|
|
assert_eq!(read_data, binary_data);
|
|
});
|
|
}
|