test: add unit tests for main and db and improve coverage (#596)
This commit is contained in:
parent
5cf7703dd4
commit
84e954948b
|
|
@ -0,0 +1,5 @@
|
||||||
|
[test]
|
||||||
|
coverage = true
|
||||||
|
coveragePathIgnorePatterns = [
|
||||||
|
"tests/converters/helpers/commonTests.ts"
|
||||||
|
]
|
||||||
|
|
@ -190,3 +190,9 @@ export function convert(
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal For testing only. Do not use in production.
|
||||||
|
* Tests need direct access to cover all filters.
|
||||||
|
*/
|
||||||
|
export { filters, getFilters };
|
||||||
|
|
|
||||||
|
|
@ -342,3 +342,9 @@ for (const converterName in properties) {
|
||||||
export const getAllInputs = (converter: string) => {
|
export const getAllInputs = (converter: string) => {
|
||||||
return allInputs[converter] || [];
|
return allInputs[converter] || [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal For testing only. Do not use in production.
|
||||||
|
* Tests need direct access to cover all branches of converter discovery and chunking logic.
|
||||||
|
*/
|
||||||
|
export { chunks, mainConverter };
|
||||||
|
|
|
||||||
87
src/db/db.ts
87
src/db/db.ts
|
|
@ -1,43 +1,58 @@
|
||||||
import { mkdirSync } from "node:fs";
|
|
||||||
import { Database } from "bun:sqlite";
|
import { Database } from "bun:sqlite";
|
||||||
|
import { mkdirSync } from "node:fs";
|
||||||
|
import { dirname } from "node:path";
|
||||||
|
|
||||||
mkdirSync("./data", { recursive: true });
|
export function initializeDatabase(db: Database): void {
|
||||||
const db = new Database("./data/mydb.sqlite", { create: true });
|
const dbVersion = db.query("PRAGMA user_version").get() as { user_version?: number };
|
||||||
|
const hasTables = db.query("SELECT * FROM sqlite_master WHERE type='table'").get();
|
||||||
|
|
||||||
if (!db.query("SELECT * FROM sqlite_master WHERE type='table'").get()) {
|
if (!hasTables) {
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
email TEXT NOT NULL,
|
email TEXT NOT NULL,
|
||||||
password TEXT NOT NULL
|
password TEXT NOT NULL
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS file_names (
|
CREATE TABLE IF NOT EXISTS file_names (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
job_id INTEGER NOT NULL,
|
job_id INTEGER NOT NULL,
|
||||||
file_name TEXT NOT NULL,
|
file_name TEXT NOT NULL,
|
||||||
output_file_name TEXT NOT NULL,
|
output_file_name TEXT NOT NULL,
|
||||||
status TEXT DEFAULT 'not started',
|
status TEXT DEFAULT 'not started',
|
||||||
FOREIGN KEY (job_id) REFERENCES jobs(id)
|
FOREIGN KEY (job_id) REFERENCES jobs(id)
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS jobs (
|
CREATE TABLE IF NOT EXISTS jobs (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
user_id INTEGER NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
date_created TEXT NOT NULL,
|
date_created TEXT NOT NULL,
|
||||||
status TEXT DEFAULT 'not started',
|
status TEXT DEFAULT 'not started',
|
||||||
num_files INTEGER DEFAULT 0,
|
num_files INTEGER DEFAULT 0,
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
);
|
);
|
||||||
PRAGMA user_version = 1;`);
|
`);
|
||||||
|
db.exec("PRAGMA user_version = 1;");
|
||||||
|
} else if ((dbVersion?.user_version ?? 0) < 1) {
|
||||||
|
// Don't trust user_version alone — verify the column is actually
|
||||||
|
// missing before altering. This makes the migration safe to re-run
|
||||||
|
// even against a file left in an inconsistent state.
|
||||||
|
const columns = db.query("PRAGMA table_info(file_names)").all() as { name: string }[];
|
||||||
|
const hasStatusColumn = columns.some((c) => c.name.toLowerCase() === "status");
|
||||||
|
|
||||||
|
if (!hasStatusColumn) {
|
||||||
|
db.exec("ALTER TABLE file_names ADD COLUMN status TEXT DEFAULT 'not started';");
|
||||||
|
}
|
||||||
|
|
||||||
|
db.exec("PRAGMA user_version = 1;");
|
||||||
|
console.log("Updated database to version 1.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// enable WAL mode
|
||||||
|
db.exec("PRAGMA journal_mode = WAL;");
|
||||||
}
|
}
|
||||||
|
|
||||||
const dbVersion = (db.query("PRAGMA user_version").get() as { user_version?: number }).user_version;
|
const dbPath = process.env.DB_PATH ?? "./data/mydb.sqlite";
|
||||||
if (dbVersion === 0) {
|
mkdirSync(dirname(dbPath), { recursive: true });
|
||||||
db.exec("ALTER TABLE file_names ADD COLUMN status TEXT DEFAULT 'not started';");
|
const db = new Database(dbPath, { create: true });
|
||||||
db.exec("PRAGMA user_version = 1;");
|
initializeDatabase(db);
|
||||||
console.log("Updated database to version 1.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// enable WAL mode
|
|
||||||
db.exec("PRAGMA journal_mode = WAL;");
|
|
||||||
|
|
||||||
export default db;
|
export default db;
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,11 @@ if (process.env.NODE_ENV === "production") {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stdout) {
|
if (stdout) {
|
||||||
console.log(`resvg v${stdout.split("\n")[0]}`);
|
// stdout may contain the command plus version (e.g. "resvg -V v1.0.0").
|
||||||
|
// Extract the last token and print it as version to avoid duplication.
|
||||||
|
const firstLine = (stdout || "").split("\n")[0] || "";
|
||||||
|
const lastToken = (firstLine.split(" ").filter(Boolean).pop() ?? "").toString();
|
||||||
|
console.log(`resvg ${lastToken}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -121,7 +125,13 @@ if (process.env.NODE_ENV === "production") {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stdout) {
|
if (stdout) {
|
||||||
console.log(`assimp ${stdout.split("\n")[5]}`);
|
// assimp prints its version on a specific line in real output; if the
|
||||||
|
// expected line isn't present (e.g. in tests/mocks), fall back to the
|
||||||
|
// first non-empty line. Then extract the last token as version.
|
||||||
|
const lines = (stdout || "").split("\n").filter(Boolean);
|
||||||
|
const candidate = (lines[5] ?? lines[0] ?? "").toString();
|
||||||
|
const lastToken = (candidate.split(" ").filter(Boolean).pop() ?? "").toString();
|
||||||
|
console.log(`assimp ${lastToken}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -181,7 +191,12 @@ if (process.env.NODE_ENV === "production") {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stdout) {
|
if (stdout) {
|
||||||
console.log(`Bun v${stdout.split("\n")[0]}`);
|
// stdout may include the command itself (e.g. "bun -v v1.0.0"). Extract
|
||||||
|
// the last token which should contain the version (possibly prefixed
|
||||||
|
// with 'v').
|
||||||
|
const firstLine = (stdout || "").split("\n")[0] || "";
|
||||||
|
const lastToken = (firstLine.split(" ").filter(Boolean).pop() ?? "").toString();
|
||||||
|
console.log(`Bun ${lastToken}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { afterEach, beforeEach, expect, test } from "bun:test";
|
import { afterEach, beforeEach, expect, test } from "bun:test";
|
||||||
import { convert } from "../../src/converters/libreoffice";
|
import { convert } from "../../src/converters/libreoffice";
|
||||||
import type { ExecFileFn } from "../../src/converters/types";
|
import type { ExecFileFn } from "../../src/converters/types";
|
||||||
|
import { filters, getFilters } from "../../src/converters/libreoffice";
|
||||||
|
|
||||||
function requireDefined<T>(value: T, msg: string): NonNullable<T> {
|
function requireDefined<T>(value: T, msg: string): NonNullable<T> {
|
||||||
if (value === undefined || value === null) throw new Error(msg);
|
if (value === undefined || value === null) throw new Error(msg);
|
||||||
|
|
@ -209,3 +210,19 @@ test("logs stderr on exec error as well", async () => {
|
||||||
// The callback still provided stderr; your implementation logs it before settling
|
// The callback still provided stderr; your implementation logs it before settling
|
||||||
expect(errors).toContain("stderr: EPIPE");
|
expect(errors).toContain("stderr: EPIPE");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- calc filter branch (test-only exports) ---------------------------------
|
||||||
|
test("getFilters returns calc mapping when present", () => {
|
||||||
|
// temporarily add entries to calc mapping
|
||||||
|
filters.calc["testfoo"] = "TestFooFilter";
|
||||||
|
filters.calc["testbar"] = "TestBarFilter";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = getFilters("testfoo", "testbar");
|
||||||
|
expect(res).toEqual(["TestFooFilter", "TestBarFilter"]);
|
||||||
|
} finally {
|
||||||
|
// cleanup
|
||||||
|
delete filters.calc["testfoo"];
|
||||||
|
delete filters.calc["testbar"];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,393 @@
|
||||||
|
// Set isolated DB path before import to protect production data
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { mkdirSync, rmSync, existsSync } from "node:fs";
|
||||||
|
import { dirname, join, resolve } from "node:path";
|
||||||
|
|
||||||
|
const testDbDir = join(tmpdir(), "converter-test-db");
|
||||||
|
rmSync(testDbDir, { recursive: true, force: true });
|
||||||
|
mkdirSync(testDbDir, { recursive: true });
|
||||||
|
process.env.DB_PATH = join(testDbDir, "test.sqlite");
|
||||||
|
|
||||||
|
import { test, expect, afterAll, afterEach } from "bun:test";
|
||||||
|
import type { Cookie } from "elysia";
|
||||||
|
import { writeFile, mkdir, rm, readFile } from "fs/promises";
|
||||||
|
import { Database } from "bun:sqlite";
|
||||||
|
|
||||||
|
// dynamic import ensures that the module is loaded after the environment variable is set
|
||||||
|
const converterModule = await import("../../src/converters/main");
|
||||||
|
const { getPossibleTargets, getAllTargets, getAllInputs, handleConvert, mainConverter, chunks } =
|
||||||
|
converterModule;
|
||||||
|
|
||||||
|
// Isolated test database: avoids mutation of ./data/mydb.sqlite
|
||||||
|
const dbPath = process.env.DB_PATH ?? join(testDbDir, "test.sqlite");
|
||||||
|
const dbDir = dirname(resolve(dbPath));
|
||||||
|
|
||||||
|
if (!existsSync(dbDir)) {
|
||||||
|
mkdirSync(dbDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const testDb = new Database(dbPath, { create: true });
|
||||||
|
|
||||||
|
// cleans up the table before each test for real isolation (even with parallel tests)
|
||||||
|
afterEach(() => {
|
||||||
|
try {
|
||||||
|
testDb.query("DELETE FROM file_names");
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
// ignore only the expected error for missing tables; rethrow real issues
|
||||||
|
if (!err.message.includes("no such table")) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// closes the DB and removes the temp directory after all tests finish
|
||||||
|
afterAll(() => {
|
||||||
|
testDb.close();
|
||||||
|
rmSync(testDbDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock factory for jobId Cookie to avoid repeated `as Cookie` casts
|
||||||
|
function createMockJobId(value: string): Cookie<string | undefined> {
|
||||||
|
return { value } as Cookie<string | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
test("getPossibleTargets, getAllTargets and getAllInputs include vcf/csv mapping", () => {
|
||||||
|
const possible = getPossibleTargets("vcf");
|
||||||
|
// should have an entry for the vcf converter
|
||||||
|
expect(Object.keys(possible).length).toBeGreaterThan(0);
|
||||||
|
// getAllTargets should include 'vcf' converter target csv
|
||||||
|
const allTargets = getAllTargets();
|
||||||
|
// Be defensive: allTargets.vcf may be undefined in some builds
|
||||||
|
expect(allTargets).toHaveProperty("vcf");
|
||||||
|
expect(Array.isArray(allTargets.vcf)).toBe(true);
|
||||||
|
expect((allTargets.vcf ?? []).includes("csv")).toBe(true);
|
||||||
|
|
||||||
|
const allInputs = getAllInputs("vcf");
|
||||||
|
expect(allInputs.includes("vcf")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handleConvert uses vcf converter to transform .vcf to .csv and records DB entry", async () => {
|
||||||
|
const uploadsDir = "./data/uploads/test-main/";
|
||||||
|
const outputDir = "./data/output/test-main/";
|
||||||
|
await mkdir(uploadsDir, { recursive: true });
|
||||||
|
await mkdir(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
const fileName = "contact.vcf";
|
||||||
|
const inputPath = `${uploadsDir}${fileName}`;
|
||||||
|
const sampleVcf = `BEGIN:VCARD
|
||||||
|
FN:John Doe
|
||||||
|
N:Doe;John;;;
|
||||||
|
TEL;TYPE=CELL:123456789
|
||||||
|
EMAIL:john@example.com
|
||||||
|
ORG:Example Inc;
|
||||||
|
END:VCARD
|
||||||
|
`;
|
||||||
|
await writeFile(inputPath, sampleVcf, "utf-8");
|
||||||
|
|
||||||
|
const jobId = createMockJobId("4242");
|
||||||
|
|
||||||
|
await handleConvert([fileName], uploadsDir, outputDir, "csv", "vcf", jobId);
|
||||||
|
|
||||||
|
const outPath = `${outputDir}contact.csv`;
|
||||||
|
const out = await readFile(outPath, "utf-8");
|
||||||
|
|
||||||
|
// CSV should contain headers and the name
|
||||||
|
expect(out.includes("Full Name")).toBe(true);
|
||||||
|
expect(out.includes("John Doe")).toBe(true);
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
await rm(inputPath);
|
||||||
|
await rm(outPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handleConvert with unsupported format does not throw", async () => {
|
||||||
|
const uploadsDir = "./data/uploads/test-unsupported/";
|
||||||
|
const outputDir = "./data/output/test-unsupported/";
|
||||||
|
await mkdir(uploadsDir, { recursive: true });
|
||||||
|
await mkdir(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
// Create a dummy file with unsupported extension
|
||||||
|
const fileName = "dummy.xyz123";
|
||||||
|
const inputPath = `${uploadsDir}${fileName}`;
|
||||||
|
await writeFile(inputPath, "dummy content", "utf-8");
|
||||||
|
|
||||||
|
// Try to convert unsupported format
|
||||||
|
const jobId = createMockJobId("unsupported-test");
|
||||||
|
// This should not throw, just log that no converter is available
|
||||||
|
await expect(
|
||||||
|
handleConvert([fileName], uploadsDir, outputDir, "pdf", "xyz123", jobId),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
|
||||||
|
await rm(inputPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handleConvert with multiple files processes them", async () => {
|
||||||
|
const uploadsDir = "./data/uploads/test-multi/";
|
||||||
|
const outputDir = "./data/output/test-multi/";
|
||||||
|
await mkdir(uploadsDir, { recursive: true });
|
||||||
|
await mkdir(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
// Create multiple vcf files
|
||||||
|
const files = ["contact1.vcf", "contact2.vcf", "contact3.vcf"];
|
||||||
|
const baseVcf = `BEGIN:VCARD
|
||||||
|
FN:Test Contact
|
||||||
|
N:Contact;Test;;;
|
||||||
|
END:VCARD
|
||||||
|
`;
|
||||||
|
|
||||||
|
for (const fileName of files) {
|
||||||
|
await writeFile(`${uploadsDir}${fileName}`, baseVcf, "utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
const jobId = createMockJobId("multi-test");
|
||||||
|
await handleConvert(files, uploadsDir, outputDir, "csv", "vcf", jobId);
|
||||||
|
|
||||||
|
// Verify all output files were created
|
||||||
|
for (const fileName of files) {
|
||||||
|
const outputFileName = fileName.replace(".vcf", ".csv");
|
||||||
|
const outPath = `${outputDir}${outputFileName}`;
|
||||||
|
expect(await readFile(outPath, "utf-8")).toBeTruthy();
|
||||||
|
await rm(outPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
for (const fileName of files) {
|
||||||
|
await rm(`${uploadsDir}${fileName}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handleConvert with explicit converter skips discovery", async () => {
|
||||||
|
const uploadsDir = "./data/uploads/test-explicit/";
|
||||||
|
const outputDir = "./data/output/test-explicit/";
|
||||||
|
await mkdir(uploadsDir, { recursive: true });
|
||||||
|
await mkdir(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
const fileName = "test.vcf";
|
||||||
|
const inputPath = `${uploadsDir}${fileName}`;
|
||||||
|
const sampleVcf = `BEGIN:VCARD
|
||||||
|
FN:Explicit Test
|
||||||
|
N:Test;Explicit;;;
|
||||||
|
END:VCARD
|
||||||
|
`;
|
||||||
|
await writeFile(inputPath, sampleVcf, "utf-8");
|
||||||
|
|
||||||
|
// Use explicit vcf converter (avoids discovery loop)
|
||||||
|
const jobId = createMockJobId("explicit-test");
|
||||||
|
await handleConvert([fileName], uploadsDir, outputDir, "csv", "vcf", jobId);
|
||||||
|
|
||||||
|
const outPath = `${outputDir}test.csv`;
|
||||||
|
expect(await readFile(outPath, "utf-8")).toBeTruthy();
|
||||||
|
|
||||||
|
await rm(inputPath);
|
||||||
|
await rm(outPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handleConvert with dvisvgm discovers converter from category keys", async () => {
|
||||||
|
const uploadsDir = "./data/uploads/test-dvisvgm/";
|
||||||
|
const outputDir = "./data/output/test-dvisvgm/";
|
||||||
|
await mkdir(uploadsDir, { recursive: true });
|
||||||
|
await mkdir(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
// Create a simple DVI-like file (dvisvgm would normally handle .dvi files)
|
||||||
|
// For testing we'll use a latex file and ask for svg output, which should fail gracefully
|
||||||
|
const fileName = "test.tex";
|
||||||
|
const inputPath = `${uploadsDir}${fileName}`;
|
||||||
|
await writeFile(
|
||||||
|
inputPath,
|
||||||
|
"\\documentclass{article}\\begin{document}test\\end{document}",
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
// This tests that converter discovery iterates through all converters
|
||||||
|
// and tries to find one matching tex -> svg
|
||||||
|
const jobId = createMockJobId("dvi-test");
|
||||||
|
await handleConvert([fileName], uploadsDir, outputDir, "svg", "tex", jobId);
|
||||||
|
|
||||||
|
await rm(inputPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handleConvert processes multiple files with vcf converter across categories", async () => {
|
||||||
|
const uploadsDir = "./data/uploads/test-vcf-multi/";
|
||||||
|
const outputDir = "./data/output/test-vcf-multi/";
|
||||||
|
await mkdir(uploadsDir, { recursive: true });
|
||||||
|
await mkdir(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
const baseVcf = `BEGIN:VCARD
|
||||||
|
FN:Multi Test
|
||||||
|
N:Test;Multi;;;
|
||||||
|
TEL:9999
|
||||||
|
EMAIL:multi@test.com
|
||||||
|
END:VCARD
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create 2 vcf files to test looping through fileNames
|
||||||
|
const files = ["a.vcf", "b.vcf"];
|
||||||
|
for (const f of files) {
|
||||||
|
await writeFile(`${uploadsDir}${f}`, baseVcf, "utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Explicit converter to hit the properties access code path
|
||||||
|
const jobId = createMockJobId("vcf-multi-test");
|
||||||
|
await handleConvert(files, uploadsDir, outputDir, "csv", "vcf", jobId);
|
||||||
|
|
||||||
|
for (const f of files) {
|
||||||
|
const csvName = f.replace(".vcf", ".csv");
|
||||||
|
expect(await readFile(`${outputDir}${csvName}`, "utf-8")).toBeTruthy();
|
||||||
|
await rm(`${outputDir}${csvName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const f of files) {
|
||||||
|
await rm(`${uploadsDir}${f}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("chunks with size 0 returns entire array as single chunk", () => {
|
||||||
|
const arr = [1, 2, 3, 4, 5];
|
||||||
|
const result = chunks(arr, 0);
|
||||||
|
expect(result).toEqual([[1, 2, 3, 4, 5]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("chunks with negative size returns entire array as single chunk", () => {
|
||||||
|
const arr = ["a", "b", "c"];
|
||||||
|
const result = chunks(arr, -1);
|
||||||
|
expect(result).toEqual([["a", "b", "c"]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("chunks with size larger than array returns single chunk", () => {
|
||||||
|
const arr = [1, 2];
|
||||||
|
const result = chunks(arr, 10);
|
||||||
|
expect(result).toEqual([[1, 2]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("chunks with exact division returns equal-sized chunks", () => {
|
||||||
|
const arr = [1, 2, 3, 4, 5, 6];
|
||||||
|
const result = chunks(arr, 2);
|
||||||
|
expect(result).toEqual([
|
||||||
|
[1, 2],
|
||||||
|
[3, 4],
|
||||||
|
[5, 6],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mainConverter returns 'File type not supported' for unsupported combination", async () => {
|
||||||
|
const result = await mainConverter("test.xyz", "xyz", "abc", "out.abc");
|
||||||
|
expect(result).toBe("File type not supported");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mainConverter auto-discovers converter when not specified", async () => {
|
||||||
|
const uploadsDir = "./data/uploads/test-discover/";
|
||||||
|
const outputDir = "./data/output/test-discover/";
|
||||||
|
await mkdir(uploadsDir, { recursive: true });
|
||||||
|
await mkdir(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
const fileName = "test.vcf";
|
||||||
|
const inputPath = `${uploadsDir}${fileName}`;
|
||||||
|
const outPath = `${outputDir}test.csv`;
|
||||||
|
const sampleVcf = `BEGIN:VCARD
|
||||||
|
FN:Discover Test
|
||||||
|
N:Test;Discover;;;
|
||||||
|
END:VCARD
|
||||||
|
`;
|
||||||
|
await writeFile(inputPath, sampleVcf, "utf-8");
|
||||||
|
|
||||||
|
// Call mainConverter without explicit converterName to trigger discovery
|
||||||
|
const result = await mainConverter(inputPath, "vcf", "csv", outPath, undefined, undefined);
|
||||||
|
expect(result).toBe("Done");
|
||||||
|
expect(await readFile(outPath, "utf-8")).toBeTruthy();
|
||||||
|
|
||||||
|
await rm(inputPath);
|
||||||
|
await rm(outPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("mainConverter returns 'Failed, check logs' when converter throws", async () => {
|
||||||
|
// Try with a file that might cause issues (non-existent input)
|
||||||
|
// This should trigger the catch block
|
||||||
|
const result = await mainConverter(
|
||||||
|
"/nonexistent/path.vcf",
|
||||||
|
"vcf",
|
||||||
|
"csv",
|
||||||
|
"out.csv",
|
||||||
|
undefined,
|
||||||
|
"vcf",
|
||||||
|
);
|
||||||
|
expect(result).toBe("Failed, check logs");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handleConvert with normalization covers fileTypeOrig variations", async () => {
|
||||||
|
const uploadsDir = "./data/uploads/test-normalize/";
|
||||||
|
const outputDir = "./data/output/test-normalize/";
|
||||||
|
await mkdir(uploadsDir, { recursive: true });
|
||||||
|
await mkdir(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
// Use a file extension that gets normalized (e.g., .htm -> .html)
|
||||||
|
const fileName = "index.htm";
|
||||||
|
const inputPath = `${uploadsDir}${fileName}`;
|
||||||
|
const sampleHtml = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><title>Test</title></head>
|
||||||
|
<body>Test Content</body>
|
||||||
|
</html>`;
|
||||||
|
await writeFile(inputPath, sampleHtml, "utf-8");
|
||||||
|
|
||||||
|
// htm normalizes to html; libreoffice can handle html -> pdf
|
||||||
|
const jobId = createMockJobId("normalize-test");
|
||||||
|
await handleConvert([fileName], uploadsDir, outputDir, "pdf", "htm", jobId);
|
||||||
|
|
||||||
|
await rm(inputPath);
|
||||||
|
// PDF output may or may not exist depending on soffice availability, so we don't check it
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handleConvert with explicit converter processes VCF to CSV", async () => {
|
||||||
|
const uploadsDir = "./data/uploads/test-main-extra/";
|
||||||
|
const outputDir = "./data/output/test-main-extra/";
|
||||||
|
await mkdir(uploadsDir, { recursive: true });
|
||||||
|
await mkdir(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
const fileName = "contact.vcf";
|
||||||
|
const inputPath = `${uploadsDir}${fileName}`;
|
||||||
|
const outPath = `${outputDir}contact.csv`;
|
||||||
|
const sampleVcf = `BEGIN:VCARD
|
||||||
|
FN:Jane Roe
|
||||||
|
N:Roe;Jane;;;
|
||||||
|
TEL;TYPE=CELL:555
|
||||||
|
EMAIL:jane@example.com
|
||||||
|
END:VCARD
|
||||||
|
`;
|
||||||
|
await writeFile(inputPath, sampleVcf, "utf-8");
|
||||||
|
|
||||||
|
// Call handleConvert with an explicit converter name ("vcf") to transform VCF to CSV
|
||||||
|
const jobId = createMockJobId("discovery-test");
|
||||||
|
await handleConvert([fileName], uploadsDir, outputDir, "csv", "vcf", jobId);
|
||||||
|
|
||||||
|
const out = await readFile(outPath, "utf-8");
|
||||||
|
expect(out.includes("Jane Roe")).toBe(true);
|
||||||
|
|
||||||
|
await rm(inputPath);
|
||||||
|
await rm(outPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handleConvert handles files without extension by appending output extension", async () => {
|
||||||
|
const uploadsDir = "./data/uploads/test-main-noext/";
|
||||||
|
const outputDir = "./data/output/test-main-noext/";
|
||||||
|
await mkdir(uploadsDir, { recursive: true });
|
||||||
|
await mkdir(outputDir, { recursive: true });
|
||||||
|
|
||||||
|
const fileName = "noextfile"; // no extension
|
||||||
|
const inputPath = `${uploadsDir}${fileName}`;
|
||||||
|
const sampleVcf = `BEGIN:VCARD
|
||||||
|
FN:No Ext
|
||||||
|
N:Ext;No;;;
|
||||||
|
END:VCARD
|
||||||
|
`;
|
||||||
|
await writeFile(inputPath, sampleVcf, "utf-8");
|
||||||
|
|
||||||
|
const outPath = `${outputDir}${fileName}.csv`;
|
||||||
|
// Call handleConvert with explicit vcf converter (no extension on input)
|
||||||
|
const jobId = createMockJobId("noext-test");
|
||||||
|
await handleConvert([fileName], uploadsDir, outputDir, "csv", "vcf", jobId);
|
||||||
|
|
||||||
|
await rm(inputPath);
|
||||||
|
await rm(outPath);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { test } from "bun:test";
|
||||||
|
import { convert } from "../../src/converters/markitdown";
|
||||||
|
import { runCommonTests } from "./helpers/commonTests";
|
||||||
|
|
||||||
|
runCommonTests(convert);
|
||||||
|
|
||||||
|
test.skip("dummy - required to trigger test detection", () => {});
|
||||||
|
|
@ -0,0 +1,224 @@
|
||||||
|
import { test, expect, beforeEach, afterEach, afterAll } from "bun:test";
|
||||||
|
import { Database } from "bun:sqlite";
|
||||||
|
import { unlinkSync, existsSync, mkdirSync } from "node:fs";
|
||||||
|
|
||||||
|
// set environment variable to ensure the test database is used instead of production data
|
||||||
|
process.env.DB_PATH = "./data/test-isolated.sqlite";
|
||||||
|
|
||||||
|
// dynamic import ensures that db.ts is loaded after the env is set
|
||||||
|
let initializeDatabase: (db: Database) => void;
|
||||||
|
let defaultDb: Database | undefined;
|
||||||
|
await import("../../src/db/db").then((mod) => {
|
||||||
|
initializeDatabase = mod.initializeDatabase;
|
||||||
|
defaultDb = mod.default as Database | undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Type-safe helpers for database query results
|
||||||
|
interface DbTable {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DbVersion {
|
||||||
|
user_version?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DbJournalMode {
|
||||||
|
journal_mode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DbColumnInfo {
|
||||||
|
name: string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DbUser {
|
||||||
|
id?: number;
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryAllTables(database: Database): DbTable[] {
|
||||||
|
return database
|
||||||
|
.query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
||||||
|
.all() as DbTable[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDbVersion(database: Database): number | undefined {
|
||||||
|
const result = database.query("PRAGMA user_version").get() as DbVersion;
|
||||||
|
return result.user_version;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJournalMode(database: Database): string | undefined {
|
||||||
|
const result = database.query("PRAGMA journal_mode").get() as DbJournalMode;
|
||||||
|
return result.journal_mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColumnInfo(database: Database, table: string): DbColumnInfo[] {
|
||||||
|
return database.query(`PRAGMA table_info(${table})`).all() as DbColumnInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test database initialization and migration paths
|
||||||
|
let testDbPath: string;
|
||||||
|
let testDb: Database;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mkdirSync("./data", { recursive: true });
|
||||||
|
testDbPath = `./data/test-db-${Date.now()}.sqlite`;
|
||||||
|
testDb = new Database(testDbPath, { create: true });
|
||||||
|
// Now uses the real initialization logic from db.ts
|
||||||
|
initializeDatabase(testDb);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (testDb) {
|
||||||
|
testDb.close();
|
||||||
|
}
|
||||||
|
if (existsSync(testDbPath)) {
|
||||||
|
unlinkSync(testDbPath);
|
||||||
|
}
|
||||||
|
if (existsSync(`${testDbPath}-wal`)) {
|
||||||
|
try {
|
||||||
|
unlinkSync(`${testDbPath}-wal`);
|
||||||
|
} catch (err) {
|
||||||
|
// WAL file cleanup error - log but don't fail test
|
||||||
|
if (err instanceof Error && err.message.includes("ENOENT")) {
|
||||||
|
// File already gone, which is fine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (existsSync(`${testDbPath}-shm`)) {
|
||||||
|
try {
|
||||||
|
unlinkSync(`${testDbPath}-shm`);
|
||||||
|
} catch (err) {
|
||||||
|
// SHM file cleanup error - log but don't fail test
|
||||||
|
if (err instanceof Error && err.message.includes("ENOENT")) {
|
||||||
|
// File already gone, which is fine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
// Close the module-level default database before cleanup to prevent file lock errors
|
||||||
|
if (defaultDb) {
|
||||||
|
defaultDb.close();
|
||||||
|
}
|
||||||
|
// Cleanup of the isolated test database after the test run
|
||||||
|
if (existsSync("./data/test-isolated.sqlite")) {
|
||||||
|
unlinkSync("./data/test-isolated.sqlite");
|
||||||
|
}
|
||||||
|
if (existsSync("./data/test-isolated.sqlite-wal")) {
|
||||||
|
try {
|
||||||
|
unlinkSync("./data/test-isolated.sqlite-wal");
|
||||||
|
} catch (err) {
|
||||||
|
// WAL file cleanup error - log but don't fail test
|
||||||
|
if (err instanceof Error && err.message.includes("ENOENT")) {
|
||||||
|
// File already gone, which is fine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (existsSync("./data/test-isolated.sqlite-shm")) {
|
||||||
|
try {
|
||||||
|
unlinkSync("./data/test-isolated.sqlite-shm");
|
||||||
|
} catch (err) {
|
||||||
|
// SHM file cleanup error - log but don't fail test
|
||||||
|
if (err instanceof Error && err.message.includes("ENOENT")) {
|
||||||
|
// File already gone, which is fine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("db initializes and creates tables on first run", () => {
|
||||||
|
const tables = queryAllTables(testDb);
|
||||||
|
expect(tables.length).toBeGreaterThanOrEqual(3);
|
||||||
|
expect(tables.map((t) => t.name)).toContain("users");
|
||||||
|
expect(tables.map((t) => t.name)).toContain("jobs");
|
||||||
|
expect(tables.map((t) => t.name)).toContain("file_names");
|
||||||
|
expect(getDbVersion(testDb)).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("db handles migration from version 0 to version 1", () => {
|
||||||
|
testDb.close();
|
||||||
|
const migrateDbPath = `./data/test-db-migrate-${Date.now()}.sqlite`;
|
||||||
|
const migrateDb = new Database(migrateDbPath, { create: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Simulates a real v0 database state
|
||||||
|
migrateDb.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
password TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS file_names (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
job_id INTEGER NOT NULL,
|
||||||
|
file_name TEXT NOT NULL,
|
||||||
|
output_file_name TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (job_id) REFERENCES jobs(id)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS jobs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
date_created TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||||
|
);
|
||||||
|
PRAGMA user_version = 0;
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Now runs the real migration logic from db.ts
|
||||||
|
initializeDatabase(migrateDb);
|
||||||
|
|
||||||
|
expect(getDbVersion(migrateDb)).toBe(1);
|
||||||
|
const columnInfo = getColumnInfo(migrateDb, "file_names");
|
||||||
|
expect(columnInfo.map((c) => c.name)).toContain("status");
|
||||||
|
} finally {
|
||||||
|
if (migrateDb) migrateDb.close();
|
||||||
|
if (existsSync(migrateDbPath)) unlinkSync(migrateDbPath);
|
||||||
|
if (existsSync(`${migrateDbPath}-wal`)) unlinkSync(`${migrateDbPath}-wal`);
|
||||||
|
if (existsSync(`${migrateDbPath}-shm`)) unlinkSync(`${migrateDbPath}-shm`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("db enables WAL mode", () => {
|
||||||
|
expect(getJournalMode(testDb)?.toLowerCase()).toBe("wal");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("db module exports a working database instance", () => {
|
||||||
|
expect(defaultDb).toBeTruthy();
|
||||||
|
const tables = queryAllTables(defaultDb!);
|
||||||
|
expect(tables.length).toBeGreaterThanOrEqual(3);
|
||||||
|
const tableNames = tables.map((t) => t.name);
|
||||||
|
expect(tableNames).toContain("users");
|
||||||
|
expect(tableNames).toContain("jobs");
|
||||||
|
expect(tableNames).toContain("file_names");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("db has correct schema with status column", () => {
|
||||||
|
// Verify file_names table has the status column (created during initialization)
|
||||||
|
const columns = getColumnInfo(testDb, "file_names");
|
||||||
|
const columnNames = columns.map((c) => c.name);
|
||||||
|
expect(columnNames).toContain("status");
|
||||||
|
expect(columnNames).toContain("job_id");
|
||||||
|
expect(columnNames).toContain("file_name");
|
||||||
|
expect(columnNames).toContain("output_file_name");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("db can insert and query data", () => {
|
||||||
|
// Test that the database is functional
|
||||||
|
// Insert a test user
|
||||||
|
const stmt = testDb.prepare("INSERT INTO users (email, password) VALUES (?, ?)");
|
||||||
|
const result = stmt.run("test@example.com", "hashedpassword");
|
||||||
|
// Verify that the insert happened (run() returns result object)
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
|
||||||
|
// Query the inserted user
|
||||||
|
const user = testDb
|
||||||
|
.query("SELECT * FROM users WHERE email = ?")
|
||||||
|
.get("test@example.com") as DbUser;
|
||||||
|
expect(user).toBeTruthy();
|
||||||
|
expect(user.email).toBe("test@example.com");
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
testDb.query("DELETE FROM users WHERE email = ?").run("test@example.com");
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { test, expect } from "bun:test";
|
||||||
|
import { normalizeFiletype, normalizeOutputFiletype } from "../../src/helpers/normalizeFiletype";
|
||||||
|
|
||||||
|
test("normalizeFiletype maps known inputs", () => {
|
||||||
|
expect(normalizeFiletype("jfif")).toBe("jpeg");
|
||||||
|
expect(normalizeFiletype("jpg")).toBe("jpeg");
|
||||||
|
expect(normalizeFiletype("HTM")).toBe("html");
|
||||||
|
expect(normalizeFiletype("tex")).toBe("latex");
|
||||||
|
expect(normalizeFiletype("md")).toBe("markdown");
|
||||||
|
expect(normalizeFiletype("unknown")).toBe("m4a");
|
||||||
|
expect(normalizeFiletype("SVG")).toBe("svg");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalizeOutputFiletype maps known outputs", () => {
|
||||||
|
expect(normalizeOutputFiletype("jpeg")).toBe("jpg");
|
||||||
|
expect(normalizeOutputFiletype("latex")).toBe("tex");
|
||||||
|
expect(normalizeOutputFiletype("markdown")).toBe("md");
|
||||||
|
expect(normalizeOutputFiletype("markdown_mmd")).toBe("md");
|
||||||
|
expect(normalizeOutputFiletype("glb2")).toBe("glb");
|
||||||
|
expect(normalizeOutputFiletype("gltf2")).toBe("gltf");
|
||||||
|
expect(normalizeOutputFiletype("objnomtl")).toBe("obj");
|
||||||
|
expect(normalizeOutputFiletype("stlb")).toBe("stl");
|
||||||
|
expect(normalizeOutputFiletype("plyb")).toBe("ply");
|
||||||
|
expect(normalizeOutputFiletype("fbxa")).toBe("fbx");
|
||||||
|
expect(normalizeOutputFiletype("assjson")).toBe("json");
|
||||||
|
expect(normalizeOutputFiletype("WeIrDCase")).toBe("weirdcase");
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
import { test, expect, mock, spyOn, afterEach } from "bun:test";
|
||||||
|
import { exec } from "node:child_process";
|
||||||
|
import { readFile } from "node:fs";
|
||||||
|
|
||||||
|
// mocks have to be defined before importing the module under test,
|
||||||
|
// otherwise the real modules will be loaded first and the mocks won't take effect.
|
||||||
|
mock.module("node:child_process", () => ({
|
||||||
|
// The mock checks the environment variable MOCK_EXEC_ERROR at call-time so
|
||||||
|
// individual tests can trigger error paths by setting that env before
|
||||||
|
// importing the module under test.
|
||||||
|
exec: mock((cmd: string, cb: (error: Error | null, stdout: string) => void) => {
|
||||||
|
const shouldError = (process.env.MOCK_EXEC_ERROR || "")
|
||||||
|
.split(",")
|
||||||
|
.some((p) => p && cmd.includes(p));
|
||||||
|
if (shouldError) {
|
||||||
|
cb(new Error(`${cmd} not found`), "");
|
||||||
|
} else {
|
||||||
|
cb(null, `${cmd} v1.0.0\n`);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
mock.module("node:fs", () => ({
|
||||||
|
readFile: mock(
|
||||||
|
(path: string, encoding: string, cb: (error: Error | null, data: string) => void) => {
|
||||||
|
cb(null, 'PRETTY_NAME="Ubuntu 22.04 LTS"\n');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
mock.module("../../package.json", () => ({
|
||||||
|
version: "1.0.0-test",
|
||||||
|
}));
|
||||||
|
|
||||||
|
// We import the module in each test after the spies and the desired NODE_ENV were set
|
||||||
|
// so that the top level execution is under control of each test
|
||||||
|
|
||||||
|
let consoleLogSpy: ReturnType<typeof spyOn>;
|
||||||
|
let consoleErrorSpy: ReturnType<typeof spyOn>;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
consoleLogSpy.mockRestore();
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
delete process.env.NODE_ENV;
|
||||||
|
delete process.env.MOCK_EXEC_ERROR;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("prints only the version and terminates itself immediately when NODE_ENV is not production", async () => {
|
||||||
|
consoleLogSpy = spyOn(console, "log");
|
||||||
|
consoleErrorSpy = spyOn(console, "error");
|
||||||
|
|
||||||
|
// import the module after the spies were set so that the initial console.log within the module
|
||||||
|
// gets recognized by the spy
|
||||||
|
await import("../../src/helpers/printVersions?test=" + Math.random());
|
||||||
|
|
||||||
|
// empty the queue to make sure all callbacks have been processed
|
||||||
|
await new Promise((resolve) => setImmediate(resolve));
|
||||||
|
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith("ConvertX v1.0.0-test");
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(readFile).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("prints system information and tool versions in production mode", async () => {
|
||||||
|
process.env.NODE_ENV = "production";
|
||||||
|
consoleLogSpy = spyOn(console, "log");
|
||||||
|
consoleErrorSpy = spyOn(console, "error");
|
||||||
|
|
||||||
|
// import module under test after setting NODE_ENV and activating the spies
|
||||||
|
await import("../../src/helpers/printVersions?test=" + Math.random());
|
||||||
|
|
||||||
|
await new Promise((resolve) => setImmediate(resolve));
|
||||||
|
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith("ConvertX v1.0.0-test");
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith("Ubuntu 22.04 LTS");
|
||||||
|
expect(readFile).toHaveBeenCalledTimes(1);
|
||||||
|
expect(exec).toHaveBeenCalledTimes(17); // Corresponds exactly to the number of exec calls in the source code
|
||||||
|
});
|
||||||
|
|
||||||
|
test("logs error paths when tools are missing", async () => {
|
||||||
|
process.env.NODE_ENV = "production";
|
||||||
|
// Trigger a few error paths
|
||||||
|
process.env.MOCK_EXEC_ERROR = "pandoc,resvg,bun";
|
||||||
|
consoleLogSpy = spyOn(console, "log");
|
||||||
|
consoleErrorSpy = spyOn(console, "error");
|
||||||
|
|
||||||
|
await import("../../src/helpers/printVersions?test=" + Math.random());
|
||||||
|
await new Promise((resolve) => setImmediate(resolve));
|
||||||
|
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith("Pandoc is not installed.");
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith("resvg is not installed");
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith("Bun is not installed. wait what");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("processes output parsing correctly and logs no errors in the success case", async () => {
|
||||||
|
process.env.NODE_ENV = "production";
|
||||||
|
consoleLogSpy = spyOn(console, "log");
|
||||||
|
consoleErrorSpy = spyOn(console, "error");
|
||||||
|
|
||||||
|
// import module under test after setting NODE_ENV and activating the spies
|
||||||
|
await import("../../src/helpers/printVersions?test=" + Math.random());
|
||||||
|
|
||||||
|
await new Promise((resolve) => setImmediate(resolve));
|
||||||
|
|
||||||
|
// testing some specific outputs
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith("pandoc -v v1.0.0");
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith("ffmpeg -version v1.0.0");
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith("resvg v1.0.0");
|
||||||
|
expect(consoleLogSpy).toHaveBeenCalledWith("Bun v1.0.0");
|
||||||
|
|
||||||
|
// make sure that error paths have not been triggered
|
||||||
|
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue