import { describe, it, expect, beforeEach, vi } from 'vitest'; import { registerEbookConverter, EBOOK_FORMATS } from '../../src/converters/ebook.js'; import { resolve, _resetForTests } from '../../src/converters/registry.js'; const { execFileMock } = vi.hoisted(() => ({ execFileMock: vi.fn() })); vi.mock('node:child_process', () => ({ execFile: execFileMock })); beforeEach(() => { _resetForTests(); execFileMock.mockReset(); execFileMock.mockImplementation((file, args, options, callback) => callback(null, '', '')); }); describe('registerEbookConverter — registration', () => { it('registers every pair among the 10 ebook formats, and nothing for source === target', () => { registerEbookConverter(); for (const sourceFormat of EBOOK_FORMATS) { for (const targetFormat of EBOOK_FORMATS) { const entry = resolve(sourceFormat, targetFormat); if (sourceFormat === targetFormat) { expect(entry).toBeNull(); } else { expect(entry).not.toBeNull(); expect(entry.family).toBe('ebook'); } } } }); it('exposes exactly the 10 requested formats', () => { expect(EBOOK_FORMATS.sort()).toEqual( ['azw3', 'epub', 'fb2', 'lrf', 'mobi', 'pdb', 'pdf', 'rb', 'snb', 'tcr'].sort() ); }); }); describe('registerEbookConverter — subprocess invocation', () => { beforeEach(() => { registerEbookConverter(); }); it('calls ebook-convert with the input and output paths, and no timeout when none is given', async () => { await resolve('epub', 'pdf').convert('/tmp/in.epub', '/tmp/out.pdf'); expect(execFileMock).toHaveBeenCalledTimes(1); const [file, args, options] = execFileMock.mock.calls[0]; expect(file).toBe('ebook-convert'); expect(args).toEqual(['/tmp/in.epub', '/tmp/out.pdf']); expect(options).toEqual({ timeout: undefined }); }); it('forwards options.timeoutMs to execFile as its timeout', async () => { await resolve('mobi', 'epub').convert('/tmp/in.mobi', '/tmp/out.epub', { timeoutMs: 180000 }); const [, , options] = execFileMock.mock.calls[0]; expect(options).toEqual({ timeout: 180000 }); }); it('uses CALIBRE_PATH from the environment when set', async () => { const previous = process.env.CALIBRE_PATH; process.env.CALIBRE_PATH = '/opt/calibre/ebook-convert'; await resolve('fb2', 'pdf').convert('/tmp/in.fb2', '/tmp/out.pdf'); expect(execFileMock.mock.calls[0][0]).toBe('/opt/calibre/ebook-convert'); if (previous === undefined) delete process.env.CALIBRE_PATH; else process.env.CALIBRE_PATH = previous; }); it('propagates a rejection from execFile as a rejected promise', async () => { execFileMock.mockImplementation((file, args, options, callback) => callback(new Error('ebook-convert exited with code 1'), '', 'error: unknown format') ); await expect(resolve('epub', 'pdf').convert('/tmp/in.epub', '/tmp/out.pdf')).rejects.toThrow( /exited with code 1/ ); }); });