Files
mock-server/tests/upload_test.rs
CNWei ab368ead1b 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>
2026-03-19 22:11:42 +08:00

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);
});
}