Files
LWLT-AIBOT/control-plane/test/document-converter.test.ts
2026-08-29 15:02:51 +08:00

185 lines
7.9 KiB
TypeScript

import assert from 'node:assert/strict';
import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import test from 'node:test';
import { loadConfig } from '../src/config.js';
import { convertDocumentToPdf, convertDocumentToXlsx } from '../src/document-converter.js';
function testConfig(converterPath: string, fontConfigFile?: string) {
return loadConfig({
NODE_ENV: 'test',
FIELD_ENCRYPTION_KEY: Buffer.alloc(32, 21).toString('base64'),
DOCUMENT_CONVERTER_PATH: converterPath,
...(fontConfigFile ? { DOCUMENT_FONTCONFIG_FILE: fontConfigFile } : {})
});
}
test('PDF sources pass through with PDF metadata', async () => {
const content = Buffer.from('%PDF-1.7\nsource');
const result = await convertDocumentToPdf(testConfig('/path/that/is/not/used'), {
fileName: 'team.doc',
contentType: 'application/msword',
content,
maxBytes: 10_000
});
assert.equal(result.fileName, 'team.pdf');
assert.equal(result.contentType, 'application/pdf');
assert.equal(result.converted, true);
assert.equal(result.conversion_status, 'already_pdf');
assert.deepEqual(result.content, content);
});
test('conversion failure falls back to the source file', async () => {
const content = Buffer.from('not a valid office document');
const result = await convertDocumentToPdf(testConfig('/path/that/does/not/exist'), {
fileName: 'team.doc',
contentType: 'application/msword',
content,
maxBytes: 10_000
});
assert.equal(result.fileName, 'team.doc');
assert.equal(result.contentType, 'application/msword');
assert.equal(result.converted, false);
assert.equal(result.conversion_status, 'source_fallback');
assert.equal(result.conversion_error_code, 'converter_unavailable');
assert.deepEqual(result.content, content);
});
test('a valid converter output replaces the source with a PDF', {
skip: process.platform === 'win32' ? 'Unix fake converter fixture is not executable through Windows execFile.' : false
}, async () => {
const temporaryDirectory = await mkdtemp(join(tmpdir(), 'ltjt-converter-test-'));
const converterPath = join(temporaryDirectory, 'fake-soffice.sh');
const fontConfigFile = join(temporaryDirectory, 'fonts.conf');
await writeFile(fontConfigFile, '<fontconfig/>');
await writeFile(converterPath, [
'#!/bin/sh',
'outdir=""',
'convert_to=""',
'next_is_outdir=0',
'next_is_convert_to=0',
'for arg in "$@"; do',
' if [ "$next_is_convert_to" = "1" ]; then convert_to="$arg"; next_is_convert_to=0; continue; fi',
' if [ "$next_is_outdir" = "1" ]; then outdir="$arg"; next_is_outdir=0; continue; fi',
' if [ "$arg" = "--convert-to" ]; then next_is_convert_to=1; fi',
' if [ "$arg" = "--outdir" ]; then next_is_outdir=1; fi',
'done',
'[ "$convert_to" = "pdf:writer_pdf_Export" ] || exit 41',
'[ "$FONTCONFIG_FILE" = "' + fontConfigFile + '" ] || exit 42',
'printf "%s" "%PDF-1.7\\nconverted" > "$outdir/team.pdf"'
].join('\n'), { mode: 0o700 });
await chmod(converterPath, 0o700);
try {
const result = await convertDocumentToPdf(testConfig(converterPath, fontConfigFile), {
fileName: 'team.docx',
contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
content: Buffer.from('source'),
maxBytes: 10_000
});
assert.equal(result.fileName, 'team.pdf');
assert.equal(result.contentType, 'application/pdf');
assert.equal(result.converted, true);
assert.equal(result.conversion_status, 'converted');
assert.match(result.content.toString(), /^%PDF-1\.7/);
} finally {
await rm(temporaryDirectory, { recursive: true, force: true });
}
});
test('HTML-in-XLS visitor sources become real XLSX workbooks', {
skip: process.platform === 'win32' ? 'Unix fake converter fixture is not executable through Windows execFile.' : false
}, async () => {
const temporaryDirectory = await mkdtemp(join(tmpdir(), 'ltjt-converter-xlsx-test-'));
const converterPath = join(temporaryDirectory, 'fake-soffice.sh');
await writeFile(converterPath, [
'#!/bin/sh',
'outdir=""',
'convert_to=""',
'input_path=""',
'next_is_outdir=0',
'next_is_convert_to=0',
'for arg in "$@"; do',
' if [ "$next_is_convert_to" = "1" ]; then convert_to="$arg"; next_is_convert_to=0; continue; fi',
' if [ "$next_is_outdir" = "1" ]; then outdir="$arg"; next_is_outdir=0; continue; fi',
' if [ "$arg" = "--convert-to" ]; then next_is_convert_to=1; continue; fi',
' if [ "$arg" = "--outdir" ]; then next_is_outdir=1; continue; fi',
' case "$arg" in *.xls) input_path="$arg";; esac',
'done',
'[ "$convert_to" = "xlsx:Calc MS Excel 2007 XML" ] || exit 41',
'case "$input_path" in *.xls) ;; *) exit 42;; esac',
'printf "PK\\003\\004fake-xlsx" > "$outdir/Visitor.xlsx"'
].join('\n'), { mode: 0o700 });
await chmod(converterPath, 0o700);
try {
const content = Buffer.from('<!doctype html><html><body><table><tr><td>游客</td></tr></table></body></html>', 'utf8');
const result = await convertDocumentToXlsx(testConfig(converterPath), {
fileName: 'Visitor.xls',
contentType: 'application/vnd.ms-excel; Charset=UTF-8',
content,
maxBytes: 10_000
});
assert.equal(result.fileName, 'Visitor.xlsx');
assert.equal(result.contentType, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
assert.equal(result.converted, true);
assert.equal(result.conversion_status, 'converted_xlsx');
assert.deepEqual(result.content.subarray(0, 4), Buffer.from([0x50, 0x4b, 0x03, 0x04]));
} finally {
await rm(temporaryDirectory, { recursive: true, force: true });
}
});
test('visitor XLSX conversion failure remains a source fallback', async () => {
const content = Buffer.from('<html><body>游客</body></html>', 'utf8');
const result = await convertDocumentToXlsx(testConfig('/path/that/does/not/exist'), {
fileName: 'Visitor.xls',
contentType: 'application/vnd.ms-excel',
content,
maxBytes: 10_000
});
assert.equal(result.fileName, 'Visitor.xls');
assert.equal(result.converted, false);
assert.equal(result.conversion_status, 'source_fallback');
assert.equal(result.conversion_error_code, 'converter_unavailable');
assert.deepEqual(result.content, content);
});
test('wide Word HTML tables are scaled to the printable width before conversion', {
skip: process.platform === 'win32' ? 'Unix fake converter fixture is not executable through Windows execFile.' : false
}, async () => {
const temporaryDirectory = await mkdtemp(join(tmpdir(), 'ltjt-converter-html-test-'));
const converterPath = join(temporaryDirectory, 'fake-soffice.sh');
await writeFile(converterPath, [
'#!/bin/sh',
'outdir=""',
'input_path=""',
'next_is_outdir=0',
'for arg in "$@"; do',
' if [ "$next_is_outdir" = "1" ]; then outdir="$arg"; next_is_outdir=0; continue; fi',
' if [ "$arg" = "--outdir" ]; then next_is_outdir=1; continue; fi',
' case "$arg" in *.doc|*.docx|*.html) input_path="$arg";; esac',
'done',
'grep -q "width=733" "$input_path" || exit 43',
'grep -q "width=\\\"154\\\"" "$input_path" || exit 44',
'grep -q "width: 550pt" "$input_path" || exit 45',
'printf "%s" "%PDF-1.7\\nconverted" > "$outdir/team.pdf"'
].join('\n'), { mode: 0o700 });
await chmod(converterPath, 0o700);
try {
const result = await convertDocumentToPdf(testConfig(converterPath), {
fileName: 'team.doc',
contentType: 'application/msword',
content: Buffer.from(
'<html><head><style>table { width: 760px; } .inner { width: 570pt; }</style></head>'
+ '<body><table width=760><tr><td width="160" class="inner">内容</td></tr></table></body></html>',
'utf8'
),
maxBytes: 10_000
});
assert.equal(result.converted, true);
assert.equal(result.conversion_status, 'converted');
} finally {
await rm(temporaryDirectory, { recursive: true, force: true });
}
});