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

275 lines
8.4 KiB
Rust

use nomifun_api_types::ConversionTarget;
use nomifun_office::ConversionService;
use rust_xlsxwriter::{Format, Workbook};
use std::path::PathBuf;
use tempfile::TempDir;
fn create_simple_xlsx(dir: &TempDir) -> PathBuf {
let path = dir.path().join("test.xlsx");
let mut wb = Workbook::new();
let sheet = wb.add_worksheet();
sheet.set_name("Sheet1").unwrap();
sheet.write_string(0, 0, "Name").unwrap();
sheet.write_string(0, 1, "Age").unwrap();
sheet.write_string(1, 0, "Alice").unwrap();
sheet.write_number(1, 1, 30.0).unwrap();
sheet.write_string(2, 0, "Bob").unwrap();
sheet.write_number(2, 1, 25.0).unwrap();
wb.save(&path).unwrap();
path
}
fn create_multi_sheet_xlsx(dir: &TempDir) -> PathBuf {
let path = dir.path().join("multi.xlsx");
let mut wb = Workbook::new();
let s1 = wb.add_worksheet();
s1.set_name("Users").unwrap();
s1.write_string(0, 0, "Name").unwrap();
s1.write_number(0, 1, 1.0).unwrap();
let s2 = wb.add_worksheet();
s2.set_name("Products").unwrap();
s2.write_string(0, 0, "Item").unwrap();
s2.write_number(0, 1, 9.99).unwrap();
let s3 = wb.add_worksheet();
s3.set_name("Empty").unwrap();
wb.save(&path).unwrap();
path
}
fn create_xlsx_with_merges(dir: &TempDir) -> PathBuf {
let path = dir.path().join("merged.xlsx");
let mut wb = Workbook::new();
let sheet = wb.add_worksheet();
sheet.set_name("Merged").unwrap();
let fmt = Format::new();
sheet.merge_range(0, 0, 1, 2, "Merged Title", &fmt).unwrap();
sheet.write_string(2, 0, "A").unwrap();
sheet.write_string(2, 1, "B").unwrap();
sheet.write_string(2, 2, "C").unwrap();
wb.save(&path).unwrap();
path
}
#[allow(clippy::approx_constant)] // 3.14 is test data, not an approximation of PI
fn create_xlsx_with_types(dir: &TempDir) -> PathBuf {
let path = dir.path().join("types.xlsx");
let mut wb = Workbook::new();
let sheet = wb.add_worksheet();
sheet.set_name("Types").unwrap();
sheet.write_string(0, 0, "text").unwrap();
sheet.write_number(0, 1, 42.0).unwrap();
sheet.write_number(0, 2, 3.14).unwrap();
sheet.write_boolean(0, 3, true).unwrap();
wb.save(&path).unwrap();
path
}
// DC-1: Excel → JSON (normal)
#[tokio::test]
async fn dc1_excel_to_json_simple() {
let dir = TempDir::new().unwrap();
let path = create_simple_xlsx(&dir);
let svc = ConversionService::new(None);
let resp = svc
.convert(path.to_str().unwrap(), ConversionTarget::ExcelJson)
.await
.unwrap();
assert_eq!(resp.to, "excel-json");
assert!(resp.result.success);
assert!(resp.result.error.is_none());
let data = resp.result.data.unwrap();
let sheets = data["sheets"].as_array().unwrap();
assert_eq!(sheets.len(), 1);
assert_eq!(sheets[0]["name"], "Sheet1");
let rows = sheets[0]["data"].as_array().unwrap();
assert_eq!(rows.len(), 3);
assert_eq!(rows[0][0], "Name");
assert_eq!(rows[0][1], "Age");
assert_eq!(rows[1][0], "Alice");
assert_eq!(rows[1][1], 30.0);
assert_eq!(rows[2][0], "Bob");
assert_eq!(rows[2][1], 25.0);
}
// DC-2: Excel → JSON (multiple sheets)
#[tokio::test]
async fn dc2_excel_to_json_multi_sheet() {
let dir = TempDir::new().unwrap();
let path = create_multi_sheet_xlsx(&dir);
let svc = ConversionService::new(None);
let resp = svc
.convert(path.to_str().unwrap(), ConversionTarget::ExcelJson)
.await
.unwrap();
assert!(resp.result.success);
let data = resp.result.data.unwrap();
let sheets = data["sheets"].as_array().unwrap();
assert_eq!(sheets.len(), 3);
assert_eq!(sheets[0]["name"], "Users");
assert_eq!(sheets[1]["name"], "Products");
assert_eq!(sheets[2]["name"], "Empty");
}
// DC-3: Excel → JSON (with merged cells)
#[tokio::test]
async fn dc3_excel_to_json_with_merges() {
let dir = TempDir::new().unwrap();
let path = create_xlsx_with_merges(&dir);
let svc = ConversionService::new(None);
let resp = svc
.convert(path.to_str().unwrap(), ConversionTarget::ExcelJson)
.await
.unwrap();
assert!(resp.result.success);
let data = resp.result.data.unwrap();
let sheet = &data["sheets"][0];
assert_eq!(sheet["name"], "Merged");
let merges = sheet["merges"].as_array().unwrap();
assert!(!merges.is_empty());
let merge = &merges[0];
assert_eq!(merge["s"]["r"], 0);
assert_eq!(merge["s"]["c"], 0);
assert_eq!(merge["e"]["r"], 1);
assert_eq!(merge["e"]["c"], 2);
}
// DC-4: Excel → JSON (file not found)
#[tokio::test]
async fn dc4_excel_to_json_file_not_found() {
let svc = ConversionService::new(None);
let resp = svc
.convert("/nonexistent/file.xlsx", ConversionTarget::ExcelJson)
.await
.unwrap();
assert_eq!(resp.to, "excel-json");
assert!(!resp.result.success);
assert!(resp.result.data.is_none());
assert!(resp.result.error.as_ref().unwrap().contains("file not found"));
}
// DC-6: Word → Markdown (pandoc not available)
#[tokio::test]
async fn dc6_word_to_markdown_file_not_found() {
let svc = ConversionService::new(None);
let resp = svc
.convert("/nonexistent/file.docx", ConversionTarget::Markdown)
.await
.unwrap();
assert_eq!(resp.to, "markdown");
assert!(!resp.result.success);
assert!(resp.result.error.as_ref().unwrap().contains("file not found"));
}
// DC-8: PPT → JSON (officecli not available)
#[tokio::test]
async fn dc8_ppt_to_json_file_not_found() {
let svc = ConversionService::new(None);
let resp = svc
.convert("/nonexistent/file.pptx", ConversionTarget::PptJson)
.await
.unwrap();
assert_eq!(resp.to, "ppt-json");
assert!(!resp.result.success);
assert!(resp.result.error.as_ref().unwrap().contains("file not found"));
}
// DC-8b: PPT → JSON (officecli not installed — configured path invalid and not in PATH)
#[tokio::test]
async fn dc8b_ppt_to_json_officecli_not_installed() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("slides.pptx");
std::fs::write(&path, b"fake pptx content").unwrap();
// Use a wrapper that ensures officecli is not found
let svc = ConversionService::new(Some("/nonexistent/officecli".into()));
let resp = svc
.convert(path.to_str().unwrap(), ConversionTarget::PptJson)
.await
.unwrap();
// Conversion should fail — either because officecli is not found or because
// it fails to parse the fake file. Either way, success must be false.
assert!(!resp.result.success);
assert!(resp.result.error.is_some());
}
// Excel cell type handling
#[tokio::test]
#[allow(clippy::approx_constant)] // 3.14 is test data, not an approximation of PI
async fn excel_to_json_cell_types() {
let dir = TempDir::new().unwrap();
let path = create_xlsx_with_types(&dir);
let svc = ConversionService::new(None);
let resp = svc
.convert(path.to_str().unwrap(), ConversionTarget::ExcelJson)
.await
.unwrap();
assert!(resp.result.success);
let data = resp.result.data.unwrap();
let row = &data["sheets"][0]["data"][0];
assert_eq!(row[0], "text");
assert_eq!(row[1], 42.0);
assert!((row[2].as_f64().unwrap() - 3.14).abs() < 0.01);
assert_eq!(row[3], true);
}
// Excel empty file
#[tokio::test]
async fn excel_to_json_empty_workbook() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("empty.xlsx");
let mut wb = Workbook::new();
wb.add_worksheet().set_name("Empty").unwrap();
wb.save(&path).unwrap();
let svc = ConversionService::new(None);
let resp = svc
.convert(path.to_str().unwrap(), ConversionTarget::ExcelJson)
.await
.unwrap();
assert!(resp.result.success);
let data = resp.result.data.unwrap();
let sheets = data["sheets"].as_array().unwrap();
assert_eq!(sheets.len(), 1);
assert_eq!(sheets[0]["name"], "Empty");
}
// Response always returns success=true at convert() level (error wrapped in result)
#[tokio::test]
async fn convert_always_returns_ok_with_result_wrapper() {
let svc = ConversionService::new(None);
let targets = [
ConversionTarget::Markdown,
ConversionTarget::ExcelJson,
ConversionTarget::PptJson,
];
for target in targets {
let resp = svc.convert("/nonexistent/path", target).await;
assert!(resp.is_ok());
assert!(!resp.unwrap().result.success);
}
}