convertor/tests/converters/pdfpackager.test.ts
Your Name 944fe18b75 Add unit tests for PDF Packager converter functionality
- Implement tests for converter properties, ensuring correct input and output formats.
- Add tests for chip parsing, covering various image, pdf_image, and pdfa chip formats.
- Include tests for the isArchiveOutput and getOutputFileName functions.
- Mock execFile for testing the convert function, verifying command execution and output file creation.
- Ensure all chips are parseable and validate the completeness of chip categories.
2026-01-24 14:39:22 +08:00

504 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { test, expect, describe, beforeEach, afterEach } from "bun:test";
import {
convert,
properties,
parseChip,
isArchiveOutput,
getOutputFileName,
ALL_CHIPS,
} from "../../src/converters/pdfpackager";
import type { ExecFileException } from "node:child_process";
import type { ExecFileFn } from "../../src/converters/types";
import { mkdirSync, existsSync, writeFileSync, rmSync, readFileSync } from "node:fs";
import { join } from "node:path";
// =============================================================================
// 屬性測試
// =============================================================================
describe("PDF Packager converter properties", () => {
test("should have correct input format (pdf only)", () => {
expect(properties.from.document).toContain("pdf");
expect(properties.from.document).toHaveLength(1);
});
test("should have all chips as output formats", () => {
expect(properties.to.document).toHaveLength(ALL_CHIPS.length);
// 驗證部分代表性 chips
expect(properties.to.document).toContain("png-300");
expect(properties.to.document).toContain("pdf-600-np-s");
expect(properties.to.document).toContain("pdfa1b-i-300-p");
expect(properties.to.document).toContain("pdfa2b-o-600");
expect(properties.to.document).toContain("all-300");
});
test("should have correct number of chips (102 total)", () => {
// A) 圖片: 3格式 × 3dpi = 9
// B) PDF: 3dpi × 6 variants = 18
// C) PDF/A-1b: 2source × 3dpi × 6 = 36
// D) PDF/A-2b: 2source × 3dpi × 6 = 36
// E) all: 3
// 總計: 9 + 18 + 36 + 36 + 3 = 102
expect(ALL_CHIPS.length).toBe(102);
});
});
// =============================================================================
// Chip 解析測試
// =============================================================================
describe("parseChip function", () => {
describe("images chips", () => {
test("should parse png-300", () => {
const result = parseChip("png-300");
expect(result).not.toBeNull();
expect(result?.kind).toBe("images");
expect(result?.dpi).toBe(300);
expect(result?.imageFormat).toBe("png");
});
test("should parse jpg-150", () => {
const result = parseChip("jpg-150");
expect(result).not.toBeNull();
expect(result?.kind).toBe("images");
expect(result?.dpi).toBe(150);
expect(result?.imageFormat).toBe("jpg");
});
test("should parse jpeg-600", () => {
const result = parseChip("jpeg-600");
expect(result).not.toBeNull();
expect(result?.kind).toBe("images");
expect(result?.dpi).toBe(600);
expect(result?.imageFormat).toBe("jpeg");
});
});
describe("pdf_image chips", () => {
test("should parse pdf-300", () => {
const result = parseChip("pdf-300");
expect(result).not.toBeNull();
expect(result?.kind).toBe("pdf_image");
expect(result?.dpi).toBe(300);
expect(result?.protect).toBeUndefined();
expect(result?.sign).toBe(false);
});
test("should parse pdf-300-p", () => {
const result = parseChip("pdf-300-p");
expect(result).not.toBeNull();
expect(result?.kind).toBe("pdf_image");
expect(result?.dpi).toBe(300);
expect(result?.protect).toBe("p");
expect(result?.sign).toBe(false);
});
test("should parse pdf-600-np-s", () => {
const result = parseChip("pdf-600-np-s");
expect(result).not.toBeNull();
expect(result?.kind).toBe("pdf_image");
expect(result?.dpi).toBe(600);
expect(result?.protect).toBe("np");
expect(result?.sign).toBe(true);
});
test("should parse pdf-150-s (sign without protect)", () => {
const result = parseChip("pdf-150-s");
expect(result).not.toBeNull();
expect(result?.kind).toBe("pdf_image");
expect(result?.dpi).toBe(150);
expect(result?.protect).toBeUndefined();
expect(result?.sign).toBe(true);
});
});
describe("pdfa chips", () => {
test("should parse pdfa1b-i-300", () => {
const result = parseChip("pdfa1b-i-300");
expect(result).not.toBeNull();
expect(result?.kind).toBe("pdfa");
expect(result?.pdfaLevel).toBe("1b");
expect(result?.pdfaSource).toBe("i");
expect(result?.dpi).toBe(300);
});
test("should parse pdfa2b-o-600-np-s", () => {
const result = parseChip("pdfa2b-o-600-np-s");
expect(result).not.toBeNull();
expect(result?.kind).toBe("pdfa");
expect(result?.pdfaLevel).toBe("2b");
expect(result?.pdfaSource).toBe("o");
expect(result?.dpi).toBe(600);
expect(result?.protect).toBe("np");
expect(result?.sign).toBe(true);
});
test("should parse pdfa1b-o-150-p", () => {
const result = parseChip("pdfa1b-o-150-p");
expect(result).not.toBeNull();
expect(result?.kind).toBe("pdfa");
expect(result?.pdfaLevel).toBe("1b");
expect(result?.pdfaSource).toBe("o");
expect(result?.dpi).toBe(150);
expect(result?.protect).toBe("p");
});
});
describe("all chips", () => {
test("should parse all-300", () => {
const result = parseChip("all-300");
expect(result).not.toBeNull();
expect(result?.kind).toBe("all");
expect(result?.dpi).toBe(300);
});
test("should parse all-150", () => {
const result = parseChip("all-150");
expect(result).not.toBeNull();
expect(result?.kind).toBe("all");
expect(result?.dpi).toBe(150);
});
test("should parse all-600", () => {
const result = parseChip("all-600");
expect(result).not.toBeNull();
expect(result?.kind).toBe("all");
expect(result?.dpi).toBe(600);
});
});
describe("invalid chips", () => {
test("should return null for invalid chip", () => {
expect(parseChip("invalid")).toBeNull();
expect(parseChip("png-400")).toBeNull(); // invalid DPI
expect(parseChip("pdf-300-x")).toBeNull(); // invalid protect
expect(parseChip("pdfa3b-i-300")).toBeNull(); // invalid level
expect(parseChip("all-300-p")).toBeNull(); // all doesn't support protect
});
});
});
// =============================================================================
// 輔助函式測試
// =============================================================================
describe("isArchiveOutput function", () => {
test("should return true for image chips", () => {
expect(isArchiveOutput("png-300")).toBe(true);
expect(isArchiveOutput("jpg-600")).toBe(true);
expect(isArchiveOutput("jpeg-150")).toBe(true);
});
test("should return true for all chips", () => {
expect(isArchiveOutput("all-150")).toBe(true);
expect(isArchiveOutput("all-300")).toBe(true);
expect(isArchiveOutput("all-600")).toBe(true);
});
test("should return false for pdf chips", () => {
expect(isArchiveOutput("pdf-300")).toBe(false);
expect(isArchiveOutput("pdf-300-p")).toBe(false);
expect(isArchiveOutput("pdf-600-np-s")).toBe(false);
});
test("should return false for pdfa chips", () => {
expect(isArchiveOutput("pdfa1b-i-300")).toBe(false);
expect(isArchiveOutput("pdfa2b-o-600-np-s")).toBe(false);
});
});
describe("getOutputFileName function", () => {
test("should return .tar for image chips", () => {
expect(getOutputFileName("png-300")).toBe("pack_png-300.tar");
expect(getOutputFileName("jpg-600")).toBe("pack_jpg-600.tar");
});
test("should return .tar for all chips", () => {
expect(getOutputFileName("all-300")).toBe("pack_all-300.tar");
});
test("should return .pdf for pdf chips", () => {
expect(getOutputFileName("pdf-300")).toBe("pack_pdf-300.pdf");
expect(getOutputFileName("pdf-600-np-s")).toBe("pack_pdf-600-np-s.pdf");
});
test("should return .pdf for pdfa chips", () => {
expect(getOutputFileName("pdfa1b-i-300")).toBe("pack_pdfa1b-i-300.pdf");
expect(getOutputFileName("pdfa2b-o-600-np-s")).toBe("pack_pdfa2b-o-600-np-s.pdf");
});
});
// =============================================================================
// 轉換測試(使用 mock execFile
// =============================================================================
describe("PDF Packager convert function", () => {
const testDir = "./test-output-pdfpackager";
beforeEach(() => {
if (!existsSync(testDir)) {
mkdirSync(testDir, { recursive: true });
}
});
afterEach(() => {
if (existsSync(testDir)) {
rmSync(testDir, { recursive: true, force: true });
}
});
test("should process png-300 chip correctly", async () => {
const commands: Array<{ cmd: string; args: string[] }> = [];
const mockExecFile: ExecFileFn = (
cmd: string,
args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
commands.push({ cmd, args });
if (cmd === "pdftoppm") {
// Simulate creating image files
const outputPrefix = args[args.length - 1];
const outputDir = join(outputPrefix, "..");
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true });
}
writeFileSync(`${outputPrefix}-01.png`, "fake png content");
writeFileSync(`${outputPrefix}-02.png`, "fake png content");
callback(null, "", "");
} else if (cmd === "tar") {
// Simulate tar creation
const outputTar = args[1]; // -cf <output>
writeFileSync(outputTar, "fake tar content");
callback(null, "", "");
} else {
callback(null, "", "");
}
};
const inputPdf = join(testDir, "input.pdf");
writeFileSync(inputPdf, "fake pdf content");
const targetPath = join(testDir, "output");
await convert(inputPdf, "pdf", "png-300", targetPath, undefined, mockExecFile);
// Verify pdftoppm was called with correct arguments
const pdftoppmCall = commands.find((c) => c.cmd === "pdftoppm");
expect(pdftoppmCall).toBeDefined();
expect(pdftoppmCall?.args).toContain("-r");
expect(pdftoppmCall?.args).toContain("300");
expect(pdftoppmCall?.args).toContain("-png");
// Verify tar was called
const tarCall = commands.find((c) => c.cmd === "tar");
expect(tarCall).toBeDefined();
expect(tarCall?.args).toContain("-cf");
});
test("should process pdf-300-np chip correctly (with qpdf)", async () => {
const commands: Array<{ cmd: string; args: string[] }> = [];
const mockExecFile: ExecFileFn = (
cmd: string,
args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
commands.push({ cmd, args });
if (cmd === "pdftoppm") {
const outputPrefix = args[args.length - 1];
const outputDir = join(outputPrefix, "..");
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true });
}
writeFileSync(`${outputPrefix}-01.png`, "fake png");
callback(null, "", "");
} else if (cmd === "img2pdf") {
const outputIndex = args.indexOf("-o");
if (outputIndex !== -1) {
const outputPdf = args[outputIndex + 1];
const outputDir = join(outputPdf, "..");
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true });
}
writeFileSync(outputPdf, "fake pdf");
}
callback(null, "", "");
} else if (cmd === "qpdf") {
const outputPdf = args[args.length - 1];
writeFileSync(outputPdf, "fake protected pdf");
callback(null, "", "");
} else {
callback(null, "", "");
}
};
const inputPdf = join(testDir, "input.pdf");
writeFileSync(inputPdf, "fake pdf content");
const targetPath = join(testDir, "output");
await convert(inputPdf, "pdf", "pdf-300-np", targetPath, undefined, mockExecFile);
// Verify qpdf was called with no-print option
const qpdfCall = commands.find((c) => c.cmd === "qpdf");
expect(qpdfCall).toBeDefined();
expect(qpdfCall?.args).toContain("--print=none");
expect(qpdfCall?.args).toContain("--modify=none");
});
test("should process pdfa2b-o-300 chip correctly (from original)", async () => {
const commands: Array<{ cmd: string; args: string[] }> = [];
const mockExecFile: ExecFileFn = (
cmd: string,
args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
commands.push({ cmd, args });
if (cmd === "gs") {
// Find output file from args
const outputArg = args.find((a) => a.startsWith("-sOutputFile="));
if (outputArg) {
const outputPdf = outputArg.replace("-sOutputFile=", "");
const outputDir = join(outputPdf, "..");
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true });
}
writeFileSync(outputPdf, "fake pdfa pdf");
}
callback(null, "", "");
} else {
callback(null, "", "");
}
};
const inputPdf = join(testDir, "input.pdf");
writeFileSync(inputPdf, "fake pdf content");
const targetPath = join(testDir, "output");
await convert(inputPdf, "pdf", "pdfa2b-o-300", targetPath, undefined, mockExecFile);
// Verify gs was called with PDF/A-2 option
const gsCall = commands.find((c) => c.cmd === "gs");
expect(gsCall).toBeDefined();
expect(gsCall?.args).toContain("-dPDFA=2");
// Verify pdftoppm was NOT called (source=o means from original)
const pdftoppmCall = commands.find((c) => c.cmd === "pdftoppm");
expect(pdftoppmCall).toBeUndefined();
});
test("should process pdf-300-s chip correctly (with signing)", async () => {
const mockExecFile: ExecFileFn = (
cmd: string,
args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
if (cmd === "pdftoppm") {
const outputPrefix = args[args.length - 1];
const outputDir = join(outputPrefix, "..");
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true });
}
writeFileSync(`${outputPrefix}-01.png`, "fake png");
callback(null, "", "");
} else if (cmd === "img2pdf") {
const outputIndex = args.indexOf("-o");
if (outputIndex !== -1) {
const outputPdf = args[outputIndex + 1];
const outputDir = join(outputPdf, "..");
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true });
}
writeFileSync(outputPdf, "fake pdf");
}
callback(null, "", "");
} else if (cmd === "python3") {
// Mock Python signing script - creates signed.pdf
const inputPdf = args[1];
const outputPdf = args[2];
const outputDir = join(outputPdf, "..");
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true });
}
// Copy input to output with "signed" marker
if (existsSync(inputPdf)) {
writeFileSync(outputPdf, "signed pdf content");
}
callback(null, "", "");
} else {
callback(null, "", "");
}
};
const inputPdf = join(testDir, "input.pdf");
writeFileSync(inputPdf, "fake pdf content");
const targetPath = join(testDir, "output");
await convert(inputPdf, "pdf", "pdf-300-s", targetPath, undefined, mockExecFile);
// Verify output file exists (outDir is dirname(targetPath) = testDir)
const expectedOutput = join(testDir, "pack_pdf-300-s.pdf");
expect(existsSync(expectedOutput)).toBe(true);
// Verify file content contains signed marker
const content = readFileSync(expectedOutput, "utf-8");
expect(content).toContain("signed");
});
test("should throw error for invalid chip", async () => {
const mockExecFile: ExecFileFn = (
_cmd: string,
_args: string[],
callback: (err: ExecFileException | null, stdout: string, stderr: string) => void,
) => {
callback(null, "", "");
};
const inputPdf = join(testDir, "input.pdf");
writeFileSync(inputPdf, "fake pdf content");
const targetPath = join(testDir, "output");
await expect(
convert(inputPdf, "pdf", "invalid-chip", targetPath, undefined, mockExecFile),
).rejects.toThrow("INVALID_CHIP");
});
});
// =============================================================================
// Chip 完整性測試
// =============================================================================
describe("ALL_CHIPS completeness", () => {
test("all chips should be parseable", () => {
for (const chip of ALL_CHIPS) {
const parsed = parseChip(chip);
expect(parsed).not.toBeNull();
expect(parsed?.rawChip).toBe(chip);
}
});
test("should contain all expected image chips", () => {
const imageChips = ALL_CHIPS.filter((c) => parseChip(c)?.kind === "images");
expect(imageChips).toHaveLength(9); // 3 formats × 3 dpis
});
test("should contain all expected pdf_image chips", () => {
const pdfChips = ALL_CHIPS.filter((c) => parseChip(c)?.kind === "pdf_image");
expect(pdfChips).toHaveLength(18); // 3 dpis × 6 variants
});
test("should contain all expected pdfa chips", () => {
const pdfaChips = ALL_CHIPS.filter((c) => parseChip(c)?.kind === "pdfa");
expect(pdfaChips).toHaveLength(72); // 2 levels × 2 sources × 3 dpis × 6 variants
});
test("should contain all expected all chips", () => {
const allChips = ALL_CHIPS.filter((c) => parseChip(c)?.kind === "all");
expect(allChips).toHaveLength(3); // 3 dpis
});
});