package jnpf.elnService.template.docx; import feign.Request; import feign.Response; import jnpf.exception.DataException; import jnpf.file.FileUploadApi; import jnpf.model.upload.UploadFileModel; import org.junit.Test; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; import java.lang.reflect.Proxy; import java.nio.charset.Charset; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class DocxContentControlReaderTest { private static final String W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; private static final String W14 = "http://schemas.microsoft.com/office/word/2010/wordml"; private final DocxContentControlReader reader = new DocxContentControlReader(); @Test public void readsBodyHeaderAndFooterWithAllSupportedControlTypes() throws Exception { Map parts = new LinkedHashMap<>(); parts.put("word/document.xml", part("document", "body", control("eln.field.sample_code", "样品编号", "") + control("eln.field.remark", "备注", "") + control("eln.field.result", "结果", "" + "" + "") + control("eln.field.checked", "确认", ""))); parts.put("word/header1.xml", part("hdr", null, control("eln.field.test_date", "检验日期", ""))); parts.put("word/footer1.xml", part("ftr", null, control("eln.ref.sample_code.0123456789abcdef0123456789abcdef", "样品编号引用", "") + control("eln.field.footer_note", "页脚", ""))); List controls = reader.read(stream(docx(parts))); assertEquals(7, controls.size()); assertControl(controls, "eln.field.sample_code", "TEXT", "PRIMARY", "word/document.xml"); assertControl(controls, "eln.field.remark", "MULTILINE", "PRIMARY", "word/document.xml"); assertControl(controls, "eln.field.result", "DROPDOWN", "PRIMARY", "word/document.xml"); assertControl(controls, "eln.field.checked", "CHECKBOX", "PRIMARY", "word/document.xml"); assertControl(controls, "eln.field.test_date", "DATE", "PRIMARY", "word/header1.xml"); assertControl(controls, "eln.ref.sample_code.0123456789abcdef0123456789abcdef", "TEXT", "REFERENCE", "word/footer1.xml"); DocxControlSnapshot dropdown = find(controls, "eln.field.result"); assertEquals("合格", dropdown.getControlConfig().get("PASS")); assertEquals("value", dropdown.getCurrentValue()); } @Test public void readsCurrentValuesFromTextCheckboxAndDateControls() throws Exception { String controls = control("eln.field.text", "文本", "") + "" + "" + "已确认" + "" + "" + "2026-08-23"; List result = reader.read(stream(docx(singleDocument(controls)))); assertEquals("value", find(result, "eln.field.text").getCurrentValue()); assertEquals("true", find(result, "eln.field.checked").getCurrentValue()); assertEquals("2026-08-23T12:00:00Z", find(result, "eln.field.date").getCurrentValue()); } @Test public void treatsDisplayedPlaceholderTextAsEmptyValue() throws Exception { String xml = "" + "" + "请输入数值"; DocxControlSnapshot result = reader.read(stream(docx(singleDocument(xml)))).get(0); assertEquals(null, result.getCurrentValue()); } @Test public void preservesParagraphBreaksAndSignificantSpacesInMultilineValue() throws Exception { String xml = "" + "" + "第一行" + " 第二行 " + ""; DocxControlSnapshot result = reader.read(stream(docx(singleDocument(xml)))).get(0); assertEquals("第一行\n 第二行 ", result.getCurrentValue()); } @Test public void marksControlsNestedInsideAnotherContentControl() throws Exception { String nested = control("eln.field.inner", "内部字段", ""); String xml = "" + "" + nested + ""; List result = reader.read(stream(docx(singleDocument(xml)))); assertTrue(find(result, "eln.field.outer").isNestedControl()); assertTrue(find(result, "eln.field.inner").isNestedControl()); } @Test public void marksTaggedOuterControlWhenNestedControlHasNoTag() throws Exception { String nested = "" + "内部内容"; String xml = "" + "" + nested + ""; List result = reader.read(stream(docx(singleDocument(xml)))); assertEquals(1, result.size()); assertTrue(find(result, "eln.field.outer").isNestedControl()); } @Test public void returnsDuplicateUnknownAndRepeatControlsWithoutDroppingThem() throws Exception { String controls = control("eln.field.unknown_code", "未知字段", "") + control("eln.field.unknown_code", "未知字段副本", "") + control("eln.repeat.samples", "样品列表", "") + control("ordinary.word.control", "普通控件", ""); List result = reader.read(stream(docx(singleDocument(controls)))); assertEquals(3, result.size()); assertEquals(2, result.stream() .filter(item -> "eln.field.unknown_code".equals(item.getTag())).count()); assertEquals("OK", find(result, "eln.repeat.samples").getStatus()); } @Test public void capturesRepeatPrototypeTableLocationAndUnsupportedMergeEvidence() throws Exception { String repeat = control("eln.repeat.samples.prototype.result", "结果", ""); String xml = "" + "" + repeat + ""; DocxControlSnapshot result = reader.read(stream(docx(singleDocument(xml)))).get(0); assertEquals(Integer.valueOf(0), result.getTableIndex()); assertEquals(Integer.valueOf(0), result.getTableRowIndex()); assertFalse(result.isNestedTable()); assertTrue(result.isVerticalMerge()); assertEquals("REPEAT", result.getControlRole()); } @Test public void capturesMergeEvidenceFromUnboundCellInRepeatPrototypeRow() throws Exception { String repeat = control("eln.repeat.samples.prototype.result", "结果", ""); String xml = "" + "" + repeat + ""; DocxControlSnapshot result = reader.read(stream(docx(singleDocument(xml)))).get(0); assertTrue(result.isVerticalMerge()); } @Test public void capturesAliasLockAndDateConfiguration() throws Exception { String xml = "" + "" + ""; DocxControlSnapshot result = reader.read(stream(docx(singleDocument(xml)))).get(0); assertEquals("采样日期", result.getAlias()); assertEquals("sdtLocked", result.getLock()); assertEquals("yyyy-MM-dd", result.getControlConfig().get("dateFormat")); } @Test public void treatsUntypedInlineControlCreatedByOnlyOfficeAsText() throws Exception { String xml = "" + control("eln.field.sample_code", "样品编号", "") + ""; DocxControlSnapshot result = reader.read(stream(docx(singleDocument(xml)))).get(0); assertEquals("TEXT", result.getControlType()); } @Test public void treatsUntypedInlineControlNestedInParagraphMarkupAsText() throws Exception { String xml = "" + control("eln.field.sample_code", "样品编号", "") + ""; DocxControlSnapshot result = reader.read(stream(docx(singleDocument(xml)))).get(0); assertEquals("TEXT", result.getControlType()); } @Test public void treatsUntypedBlockControlCreatedByOnlyOfficeAsMultiline() throws Exception { String xml = control("eln.field.remark", "备注", ""); DocxControlSnapshot result = reader.read(stream(docx(singleDocument(xml)))).get(0); assertEquals("MULTILINE", result.getControlType()); } @Test public void keepsExplicitUnsupportedControlTypeUnknown() throws Exception { String xml = "" + control("eln.field.photo", "照片", "") + ""; DocxControlSnapshot result = reader.read(stream(docx(singleDocument(xml)))).get(0); assertEquals("UNKNOWN", result.getControlType()); } @Test public void ignoresUnsupportedTypeNamesFromForeignNamespaces() throws Exception { String xml = "" + control("eln.field.sample_code", "样品编号", "") + ""; DocxControlSnapshot result = reader.read(stream(docx(singleDocument(xml)))).get(0); assertEquals("TEXT", result.getControlType()); } @Test public void recognizesUnsupportedTypesFromOfficialWordExtensions() throws Exception { String xml = "" + control("eln.field.repeat", "重复节", "") + ""; DocxControlSnapshot result = reader.read(stream(docx(singleDocument(xml)))).get(0); assertEquals("UNKNOWN", result.getControlType()); } @Test public void rejectsMalformedZipAndMissingDocumentPart() throws Exception { expectDataException(() -> reader.read(stream("not-a-zip".getBytes(StandardCharsets.UTF_8)))); Map onlyHeader = new LinkedHashMap<>(); onlyHeader.put("word/header1.xml", part("hdr", null, "")); expectDataException(() -> reader.read(stream(docx(onlyHeader)))); } @Test public void rejectsDtdAndNeverResolvesExternalEntities() throws Exception { String xml = "" + "]>" + "" + control("eln.field.sample_code", "&xxe;", "") + ""; expectDataException(() -> reader.read(stream(docx(singleDocumentXml(xml))))); } @Test public void hidesParserImplementationDetailsForMalformedXml() throws Exception { String xml = ""; try { reader.read(stream(docx(singleDocumentXml(xml)))); fail("预期拒绝损坏的 DOCX XML"); } catch (DataException expected) { assertEquals("DOCX 内容控件解析失败", expected.getMessage()); } } @Test public void rejectsPathTraversalEntries() throws Exception { Map parts = singleDocument(""); parts.put("word/../outside.xml", ""); expectDataException(() -> reader.read(stream(docx(parts)))); } @Test public void rejectsZipBombRatio() throws Exception { StringBuilder repeated = new StringBuilder(2 * 1024 * 1024); for (int i = 0; i < 2 * 1024 * 1024; i++) { repeated.append('A'); } expectDataException(() -> reader.read(stream(docx(singleDocument(repeated.toString()))))); } @Test public void rejectsOversizedXmlPart() throws Exception { StringBuilder oversized = new StringBuilder(10 * 1024 * 1024 + 128); for (int i = 0; i < 10 * 1024 * 1024 + 128; i++) { oversized.append(i % 10); } expectDataException(() -> reader.read(stream(docx(singleDocument(oversized.toString()))))); } @Test public void downloadsAnnexOnceAndClosesTheResponseBeforeReturningSnapshots() throws Exception { byte[] bytes = docx(singleDocument( control("eln.field.sample_code", "样品编号", ""))); DownloadProbe probe = new DownloadProbe(bytes); FileUploadApi api = (FileUploadApi) Proxy.newProxyInstance( FileUploadApi.class.getClassLoader(), new Class[]{FileUploadApi.class}, (proxy, method, args) -> { if ("downFile".equals(method.getName())) { probe.model = (UploadFileModel) args[0]; probe.calls++; return probe.response(); } Class type = method.getReturnType(); return type == boolean.class ? false : type == int.class ? 0 : null; }); List controls = new DocxContentControlReader(api).readControls("tenant-a", "template.docx"); assertEquals(1, controls.size()); assertEquals(1, probe.calls); assertEquals(1, probe.closed); assertEquals("annex", probe.model.getFolderName()); assertEquals("template.docx", probe.model.getObjectName()); } private static void assertControl(List controls, String tag, String type, String role, String part) { DocxControlSnapshot control = find(controls, tag); assertEquals(type, control.getControlType()); assertEquals(role, control.getControlRole()); assertEquals(part, control.getPartName()); assertEquals("OK", control.getStatus()); } private static DocxControlSnapshot find(List controls, String tag) { return controls.stream().filter(item -> tag.equals(item.getTag())).findFirst() .orElseThrow(() -> new AssertionError("未找到控件: " + tag)); } private static Map singleDocument(String controls) { return singleDocumentXml(part("document", "body", controls)); } private static Map singleDocumentXml(String xml) { Map parts = new LinkedHashMap<>(); parts.put("word/document.xml", xml); return parts; } private static String part(String root, String body, String content) { String bodyStart = body == null ? "" : ""; String bodyEnd = body == null ? "" : ""; return "" + "" + bodyStart + content + bodyEnd + ""; } private static String control(String tag, String alias, String property) { return "" + "" + property + "value" + ""; } private static byte[] docx(Map parts) throws Exception { ByteArrayOutputStream output = new ByteArrayOutputStream(); try (ZipOutputStream zip = new ZipOutputStream(output)) { for (Map.Entry part : parts.entrySet()) { zip.putNextEntry(new ZipEntry(part.getKey())); zip.write(part.getValue().getBytes(StandardCharsets.UTF_8)); zip.closeEntry(); } } return output.toByteArray(); } private static ByteArrayInputStream stream(byte[] bytes) { return new ByteArrayInputStream(bytes); } private static void expectDataException(ThrowingRunnable action) { try { action.run(); fail("预期抛出 DataException"); } catch (DataException expected) { assertFalse(expected.getMessage().trim().isEmpty()); } catch (Exception error) { throw new AssertionError(error); } } private interface ThrowingRunnable { void run() throws Exception; } private static final class DownloadProbe { private final byte[] bytes; private UploadFileModel model; private int calls; private int closed; private DownloadProbe(byte[] bytes) { this.bytes = bytes; } private Response response() { return Response.builder().status(200).reason("OK") .request(Request.create(Request.HttpMethod.GET, "http://file/download", Collections.>emptyMap(), null, Charset.defaultCharset())) .body(new Response.Body() { @Override public Integer length() { return bytes.length; } @Override public boolean isRepeatable() { return true; } @Override public java.io.InputStream asInputStream() { return new ByteArrayInputStream(bytes); } @Override public java.io.Reader asReader(Charset charset) { throw new UnsupportedOperationException(); } @Override public void close() { closed++; } }).build(); } } }