185 lines
7.9 KiB
TypeScript
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 });
|
|
}
|
|
});
|