text-extract/tests/docx_roundtrip.rs
flemming-it 36b175d334
Some checks failed
CI / Linux x86_64 (Forgejo) (push) Failing after 2s
refactor: depend on renamed chain-module-sdk
Signed-off-by: flemming-it <sf@flemming.it>
2026-06-16 11:28:59 +02:00

135 lines
4.7 KiB
Rust

//! End-to-end test for the DOCX path.
//!
//! Builds a minimal valid DOCX in memory, hands it to the
//! `invoke` entry point, and asserts the JSON output matches the
//! expected schema. The DOCX is a ZIP archive containing
//! `word/document.xml` plus the minimum `[Content_Types].xml`
//! that Office tools expect — that is the bare floor for a
//! parser to accept the file.
//!
//! The PDF path is covered by unit tests on the page splitter;
//! end-to-end PDF coverage will land once the platform's hub
//! integration test loads the wasm component and feeds it a
//! real PDF fixture.
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::io::Write;
use chain_module_sdk::prelude::*;
use serde::Deserialize;
use text_extract::invoke;
#[derive(Debug, Deserialize)]
struct ExtractedDocument {
engine: String,
engine_version: String,
pages: Vec<Page>,
}
#[derive(Debug, Deserialize)]
struct Page {
number: u32,
text: String,
}
const DOCX_MIME: &str = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
const CONTENT_TYPES_XML: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="xml" ContentType="application/xml"/>
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
</Types>"#;
const RELS_XML: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
</Relationships>"#;
const DOCUMENT_XML: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
<w:p><w:r><w:t>The quick brown fox</w:t></w:r></w:p>
<w:p><w:r><w:t xml:space="preserve">jumps over </w:t></w:r><w:r><w:t>the lazy dog.</w:t></w:r></w:p>
</w:body>
</w:document>"#;
fn build_minimal_docx() -> Vec<u8> {
let mut buf = Vec::new();
{
let mut w = zip::ZipWriter::new(std::io::Cursor::new(&mut buf));
let opts: zip::write::SimpleFileOptions = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
w.start_file("[Content_Types].xml", opts).unwrap();
w.write_all(CONTENT_TYPES_XML.as_bytes()).unwrap();
w.start_file("_rels/.rels", opts).unwrap();
w.write_all(RELS_XML.as_bytes()).unwrap();
w.start_file("word/document.xml", opts).unwrap();
w.write_all(DOCUMENT_XML.as_bytes()).unwrap();
w.finish().unwrap();
}
buf
}
fn dummy_context() -> Context {
Context {
invocation_id: "test".into(),
capability_namespace: "text".into(),
capability_name: "extract".into(),
deadline_ms: 0,
}
}
#[test]
fn docx_round_trip_produces_expected_schema() {
let docx = build_minimal_docx();
let inputs = Inputs::from_pairs([("document", Payload::bytes(DOCX_MIME, docx))]);
let outputs = invoke(dummy_context(), inputs).expect("invoke ok");
let pairs = outputs.into_pairs();
assert_eq!(pairs.len(), 1, "exactly one output expected");
assert_eq!(pairs[0].0, "extracted");
let raw = match &pairs[0].1 {
Payload::Json(s) => s,
other => panic!("expected Json payload, got {other:?}"),
};
let doc: ExtractedDocument = serde_json::from_str(raw).expect("valid JSON");
assert_eq!(doc.engine, "zip+quick-xml");
assert_eq!(doc.engine_version, "0.1");
assert_eq!(doc.pages.len(), 1);
assert_eq!(doc.pages[0].number, 1);
assert!(
doc.pages[0].text.contains("quick brown fox"),
"missing first paragraph: {}",
doc.pages[0].text
);
assert!(
doc.pages[0].text.contains("jumps over the lazy dog"),
"missing second paragraph: {}",
doc.pages[0].text
);
}
#[test]
fn unsupported_mime_type_returns_invalid_input() {
let inputs =
Inputs::from_pairs([("document", Payload::bytes("application/zip", vec![0u8; 32]))]);
let err = invoke(dummy_context(), inputs).expect_err("must fail");
let msg = format!("{err}");
assert!(msg.contains("unsupported MIME type"), "msg = {msg}");
}
#[test]
fn missing_document_input_returns_invalid_input() {
let inputs = Inputs::default();
let err = invoke(dummy_context(), inputs).expect_err("must fail");
let msg = format!("{err}");
assert!(msg.contains("missing input"), "msg = {msg}");
}