//! 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 fai_module_sdk::prelude::*; use serde::Deserialize; use text_extract::invoke; #[derive(Debug, Deserialize)] struct ExtractedDocument { engine: String, engine_version: String, pages: Vec, } #[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#" "#; const RELS_XML: &str = r#" "#; const DOCUMENT_XML: &str = r#" The quick brown fox jumps over the lazy dog. "#; fn build_minimal_docx() -> Vec { 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}"); }