feat: 实现文件上传功能并完善测试覆盖
- 新增 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>
This commit is contained in:
246
tests/upload_test.rs
Normal file
246
tests/upload_test.rs
Normal file
@@ -0,0 +1,246 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user