Files
MyAiDesk/nomifun-tauri/crates/backend/nomifun-file/tests/zip_packaging.rs
T
freedak f7a720204a Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
2026-07-04 19:20:46 +08:00

364 lines
12 KiB
Rust

//! Integration tests for ZIP packaging operations (task 7.7).
//!
//! These tests exercise `create_zip` and `cancel_zip` through the
//! `IFileService` trait, covering text content packaging, disk file
//! packaging, mixed entries, cancellation, sandbox validation,
//! and archive verification.
use std::fs;
use std::io::Read;
use std::sync::Arc;
use nomifun_api_types::WebSocketMessage;
use nomifun_file::{FileService, IFileService, ZipEntry};
use nomifun_realtime::EventBroadcaster;
/// No-op broadcaster for tests that don't need event verification.
struct NoopBroadcaster;
impl EventBroadcaster for NoopBroadcaster {
fn broadcast(&self, _event: WebSocketMessage<serde_json::Value>) {}
}
fn make_service(root: &std::path::Path) -> FileService {
FileService::new(Arc::new(NoopBroadcaster), vec![root.to_path_buf()])
}
// -----------------------------------------------------------------------
// create_zip — test-plan 5.1
// -----------------------------------------------------------------------
#[tokio::test]
async fn create_zip_text_content() {
let dir = tempfile::tempdir().unwrap();
let zip_path = dir.path().join("text.zip");
let svc = make_service(dir.path());
let entries = vec![
ZipEntry::Text {
name: "a.txt".into(),
content: "hello".into(),
},
ZipEntry::Text {
name: "dir/b.txt".into(),
content: "world".into(),
},
];
let result = svc.create_zip(zip_path.to_str().unwrap(), entries, None).await.unwrap();
assert!(result);
// Verify the ZIP can be opened and contains correct data
let file = fs::File::open(&zip_path).unwrap();
let mut archive = zip::ZipArchive::new(file).unwrap();
assert_eq!(archive.len(), 2);
{
let mut entry = archive.by_name("a.txt").unwrap();
let mut buf = String::new();
entry.read_to_string(&mut buf).unwrap();
assert_eq!(buf, "hello");
}
{
let mut entry = archive.by_name("dir/b.txt").unwrap();
let mut buf = String::new();
entry.read_to_string(&mut buf).unwrap();
assert_eq!(buf, "world");
}
}
#[tokio::test]
async fn create_zip_disk_files() {
let dir = tempfile::tempdir().unwrap();
let src_a = dir.path().join("src_a.txt");
let src_b = dir.path().join("src_b.bin");
fs::write(&src_a, "file A content").unwrap();
fs::write(&src_b, b"\x00\x01\x02\x03").unwrap();
let zip_path = dir.path().join("disk.zip");
let svc = make_service(dir.path());
let entries = vec![
ZipEntry::Disk {
name: "a.txt".into(),
file_path: src_a.to_string_lossy().into_owned(),
},
ZipEntry::Disk {
name: "b.bin".into(),
file_path: src_b.to_string_lossy().into_owned(),
},
];
let result = svc.create_zip(zip_path.to_str().unwrap(), entries, None).await.unwrap();
assert!(result);
let file = fs::File::open(&zip_path).unwrap();
let mut archive = zip::ZipArchive::new(file).unwrap();
assert_eq!(archive.len(), 2);
{
let mut entry = archive.by_name("a.txt").unwrap();
let mut buf = String::new();
entry.read_to_string(&mut buf).unwrap();
assert_eq!(buf, "file A content");
}
{
let mut entry = archive.by_name("b.bin").unwrap();
let mut buf = Vec::new();
entry.read_to_end(&mut buf).unwrap();
assert_eq!(buf, b"\x00\x01\x02\x03");
}
}
#[tokio::test]
async fn create_zip_mixed_content_and_disk() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("real.txt");
fs::write(&src, "real file").unwrap();
let zip_path = dir.path().join("mixed.zip");
let svc = make_service(dir.path());
let entries = vec![
ZipEntry::Text {
name: "virtual.txt".into(),
content: "in-memory".into(),
},
ZipEntry::Disk {
name: "real.txt".into(),
file_path: src.to_string_lossy().into_owned(),
},
];
let result = svc.create_zip(zip_path.to_str().unwrap(), entries, None).await.unwrap();
assert!(result);
let file = fs::File::open(&zip_path).unwrap();
let mut archive = zip::ZipArchive::new(file).unwrap();
assert_eq!(archive.len(), 2);
{
let mut entry = archive.by_name("virtual.txt").unwrap();
let mut buf = String::new();
entry.read_to_string(&mut buf).unwrap();
assert_eq!(buf, "in-memory");
}
{
let mut entry = archive.by_name("real.txt").unwrap();
let mut buf = String::new();
entry.read_to_string(&mut buf).unwrap();
assert_eq!(buf, "real file");
}
}
#[tokio::test]
async fn create_zip_with_request_id() {
let dir = tempfile::tempdir().unwrap();
let zip_path = dir.path().join("req.zip");
let svc = make_service(dir.path());
let entries = vec![ZipEntry::Text {
name: "data.txt".into(),
content: "test data".into(),
}];
let result = svc
.create_zip(zip_path.to_str().unwrap(), entries, Some("req-123".into()))
.await
.unwrap();
assert!(result);
assert!(zip_path.exists());
}
// -----------------------------------------------------------------------
// cancel_zip — test-plan 5.2
// -----------------------------------------------------------------------
#[tokio::test]
async fn cancel_zip_nonexistent_request() {
let dir = tempfile::tempdir().unwrap();
let svc = make_service(dir.path());
// Cancelling a request that doesn't exist returns false
let result = svc.cancel_zip("no-such-id").await;
assert!(!result);
}
#[tokio::test]
async fn cancel_zip_completed_request_returns_false() {
let dir = tempfile::tempdir().unwrap();
let zip_path = dir.path().join("done.zip");
let svc = make_service(dir.path());
let entries = vec![ZipEntry::Text {
name: "a.txt".into(),
content: "data".into(),
}];
// Complete the ZIP first
svc.create_zip(zip_path.to_str().unwrap(), entries, Some("req-done".into()))
.await
.unwrap();
// After completion, the token is cleaned up — cancel returns false
let result = svc.cancel_zip("req-done").await;
assert!(!result);
}
// -----------------------------------------------------------------------
// Error cases
// -----------------------------------------------------------------------
#[tokio::test]
async fn create_zip_disk_entry_nonexistent_source() {
let dir = tempfile::tempdir().unwrap();
let zip_path = dir.path().join("fail.zip");
let svc = make_service(dir.path());
let entries = vec![ZipEntry::Disk {
name: "missing.txt".into(),
file_path: "/nonexistent/path/file.txt".into(),
}];
let result = svc.create_zip(zip_path.to_str().unwrap(), entries, None).await;
assert!(result.is_err());
}
#[tokio::test]
async fn create_zip_empty_entries_produces_valid_archive() {
let dir = tempfile::tempdir().unwrap();
let zip_path = dir.path().join("empty.zip");
let svc = make_service(dir.path());
let result = svc.create_zip(zip_path.to_str().unwrap(), vec![], None).await.unwrap();
assert!(result);
let file = fs::File::open(&zip_path).unwrap();
let archive = zip::ZipArchive::new(file).unwrap();
assert_eq!(archive.len(), 0);
}
// -----------------------------------------------------------------------
// Sandbox validation
// -----------------------------------------------------------------------
#[tokio::test]
async fn create_zip_rejects_output_outside_sandbox() {
let dir = tempfile::tempdir().unwrap();
let other = tempfile::tempdir().unwrap();
let svc = make_service(dir.path());
// Output path is outside the allowed root
let zip_path = other.path().join("escape.zip");
let entries = vec![ZipEntry::Text {
name: "a.txt".into(),
content: "data".into(),
}];
let result = svc.create_zip(zip_path.to_str().unwrap(), entries, None).await;
assert!(result.is_err());
assert!(!zip_path.exists());
}
#[tokio::test]
async fn create_zip_rejects_disk_entry_outside_sandbox() {
let dir = tempfile::tempdir().unwrap();
let other = tempfile::tempdir().unwrap();
let outside_file = other.path().join("secret.txt");
fs::write(&outside_file, "sensitive data").unwrap();
let svc = make_service(dir.path());
let zip_path = dir.path().join("steal.zip");
let entries = vec![ZipEntry::Disk {
name: "stolen.txt".into(),
file_path: outside_file.to_string_lossy().into_owned(),
}];
let result = svc.create_zip(zip_path.to_str().unwrap(), entries, None).await;
assert!(result.is_err());
assert!(!zip_path.exists());
}
#[tokio::test]
async fn create_zip_rejects_nonexistent_disk_entry_in_sandbox() {
let dir = tempfile::tempdir().unwrap();
let zip_path = dir.path().join("fail.zip");
let svc = make_service(dir.path());
// Disk entry points to a non-existent file inside the sandbox —
// validate_path rejects it before any ZIP is created.
let entries = vec![ZipEntry::Disk {
name: "missing.txt".into(),
file_path: dir.path().join("no_such.txt").to_string_lossy().into_owned(),
}];
let result = svc.create_zip(zip_path.to_str().unwrap(), entries, None).await;
assert!(result.is_err());
assert!(!zip_path.exists());
}
// -----------------------------------------------------------------------
// cancel_zip — in-progress cancellation (test-plan 5.2)
// -----------------------------------------------------------------------
#[tokio::test]
async fn cancel_zip_in_progress() {
let dir = tempfile::tempdir().unwrap();
// Create many small source files to give time for cancellation
let src_dir = dir.path().join("sources");
fs::create_dir(&src_dir).unwrap();
let entry_count = 500;
let mut entries = Vec::with_capacity(entry_count);
for i in 0..entry_count {
let name = format!("file_{i:04}.txt");
let path = src_dir.join(&name);
fs::write(&path, format!("content of file {i}")).unwrap();
entries.push(ZipEntry::Disk {
name,
file_path: path.to_string_lossy().into_owned(),
});
}
let zip_path = dir.path().join("big.zip");
let svc = Arc::new(make_service(dir.path()));
let svc_cancel = Arc::clone(&svc);
let zip_path_str = zip_path.to_string_lossy().into_owned();
let request_id = "cancel-me".to_owned();
// Spawn ZIP creation in a separate task
let create_handle = tokio::spawn({
let request_id = request_id.clone();
async move { svc.create_zip(&zip_path_str, entries, Some(request_id)).await }
});
// Give a brief moment for the creation to start, then cancel
tokio::task::yield_now().await;
let cancelled = svc_cancel.cancel_zip(&request_id).await;
let result = create_handle.await.unwrap();
// Either the cancel signal was picked up (Ok(false)) or it completed
// before the signal arrived (Ok(true)). The key assertion is that
// cancel_zip returned true (it found the token).
if cancelled {
// cancel_zip found and set the flag
match result {
Ok(false) => {
// Successfully cancelled — partial file should be removed
assert!(
!zip_path.exists(),
"partial ZIP should be cleaned up after cancellation"
);
}
Ok(true) => {
// ZIP completed before cancellation took effect — file exists
assert!(zip_path.exists());
}
Err(e) => panic!("unexpected error: {e}"),
}
} else {
// Token was already cleaned up, meaning create_zip finished first
assert!(result.is_ok());
}
}