/** * Tests for the first-run ffmpeg acquisition (ffmpegSetup.ts). The shared streaming + * checksum loop is mocked here — it's already pinned by updaterDownload.test.ts — so these * focus on the ffmpeg-specific behaviour: the upstream trust gate, readiness + dev seed, * the download → tar-extract → install-into-managed-dir flow, and the manual-locate * fallback (accept a valid folder, reject a missing/blocked one). */ import { describe, it, expect, beforeEach, afterAll, vi } from 'vitest' import { mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync } from 'node:fs' import { join } from 'node:path' import { tmpdir } from 'node:os' import type { WebContents } from 'electron' const ROOT = mkdtempSync(join(tmpdir(), 'aerofetch-ffmpeg-test-')) const USERDATA = join(ROOT, 'userData') const TEMP = join(ROOT, 'temp') const RES = join(ROOT, 'resources') const MANAGED = join(USERDATA, 'bin') // getBinDir() resolves the dev seed under /resources/bin (its is.dev branch), // so mocking is.dev=true + app.getAppPath()=ROOT points it at RES/bin. Deliberately NOT // mutating the process-global process.resourcesPath, which would leak across test files. // Shared, per-test-tunable control surface (hoisted so the vi.mock factories can read it). const h = vi.hoisted(() => ({ streamOk: true, streamError: 'network boom', tarFail: false, extractWrites: true, validateOk: true, dialog: { canceled: false, filePaths: [] as string[] } })) vi.mock('@electron-toolkit/utils', () => ({ is: { dev: true } })) vi.mock('electron', () => ({ app: { getPath: (k: string) => (k === 'userData' ? join(ROOT, 'userData') : join(ROOT, 'temp')), getAppPath: () => ROOT }, dialog: { showOpenDialog: async () => h.dialog }, BrowserWindow: class {} })) vi.mock('../src/main/logger', () => ({ logger: { error: vi.fn(), info: vi.fn(), warn: vi.fn() } })) // ffmpeg version read is display-only; stub it so readiness comes purely from file presence. vi.mock('../src/main/ffmpeg', () => ({ getFfmpegVersions: async () => ({ ffmpeg: 'mock', ffprobe: 'mock' }) })) // The streamer is pinned elsewhere: simulate a written archive + one progress tick. vi.mock('../src/main/lib/verifiedDownload', () => ({ streamVerifiedFile: async (opts: { filePath: string onProgress: (p: { received: number; total: number; fraction: number }) => void }) => { if (!h.streamOk) return { ok: false, error: h.streamError } writeFileSync(opts.filePath, 'FAKE-ZIP') opts.onProgress({ received: 100, total: 100, fraction: 1 }) return { ok: true, filePath: opts.filePath } } })) // tar.exe extract + ` -version` validation both go through execFile. vi.mock('child_process', () => ({ execFile: ( file: string, args: string[], _opts: unknown, cb: (err: Error | null, stdout?: string) => void ) => { if (file.endsWith('tar.exe')) { if (h.tarFail) return cb(new Error('tar failed')) const dest = args[args.indexOf('-C') + 1] if (h.extractWrites) { writeFileSync(join(dest, 'ffmpeg.exe'), 'FF') writeFileSync(join(dest, 'ffprobe.exe'), 'FP') } return cb(null) } if (args.includes('-version')) { return h.validateOk ? cb(null, 'ffmpeg version 8.1.2') : cb(new Error('blocked')) } return cb(null) } })) const { isTrustedFfmpegUrl, getFfmpegSetupStatus, downloadFfmpeg, locateFfmpegManually } = await import('../src/main/ffmpegSetup') function fakeWc(): { wc: WebContents; sends: unknown[] } { const sends: unknown[] = [] const wc = { isDestroyed: () => false, send: (_ch: string, p: unknown) => sends.push(p) } as unknown as WebContents return { wc, sends } } beforeEach(() => { rmSync(MANAGED, { recursive: true, force: true }) rmSync(join(RES, 'bin'), { recursive: true, force: true }) mkdirSync(TEMP, { recursive: true }) Object.assign(h, { streamOk: true, streamError: 'network boom', tarFail: false, extractWrites: true, validateOk: true, dialog: { canceled: false, filePaths: [] } }) }) afterAll(() => rmSync(ROOT, { recursive: true, force: true })) describe('isTrustedFfmpegUrl', () => { it('accepts HTTPS on an allowlisted upstream host', () => { expect( isTrustedFfmpegUrl('https://github.com/GyanD/codexffmpeg/releases/download/x/f.zip') ).toBe(true) expect(isTrustedFfmpegUrl('https://release-assets.githubusercontent.com/abc?x=1')).toBe(true) }) it('rejects http, an off-list host, and garbage', () => { expect(isTrustedFfmpegUrl('http://github.com/x.zip')).toBe(false) expect(isTrustedFfmpegUrl('https://evil.example/x.zip')).toBe(false) expect(isTrustedFfmpegUrl('not a url')).toBe(false) }) }) describe('getFfmpegSetupStatus', () => { it('is ready only when both binaries are present', async () => { mkdirSync(MANAGED, { recursive: true }) writeFileSync(join(MANAGED, 'ffmpeg.exe'), 'x') expect((await getFfmpegSetupStatus()).ready).toBe(false) // ffprobe missing writeFileSync(join(MANAGED, 'ffprobe.exe'), 'x') expect((await getFfmpegSetupStatus()).ready).toBe(true) }) it('seeds the managed copies from the dev bundle when present', async () => { mkdirSync(join(RES, 'bin'), { recursive: true }) writeFileSync(join(RES, 'bin', 'ffmpeg.exe'), 'seed') writeFileSync(join(RES, 'bin', 'ffprobe.exe'), 'seed') const status = await getFfmpegSetupStatus() expect(status.ready).toBe(true) expect(existsSync(join(MANAGED, 'ffmpeg.exe'))).toBe(true) }) }) describe('downloadFfmpeg', () => { it('downloads, extracts, and installs both binaries into the managed dir', async () => { const { wc, sends } = fakeWc() const r = await downloadFfmpeg(wc) expect(r).toEqual({ ok: true }) expect(existsSync(join(MANAGED, 'ffmpeg.exe'))).toBe(true) expect(existsSync(join(MANAGED, 'ffprobe.exe'))).toBe(true) // A downloading tick and the extracting phase both reach the renderer. expect(sends).toContainEqual(expect.objectContaining({ phase: 'downloading' })) expect(sends).toContainEqual(expect.objectContaining({ phase: 'extracting' })) }) it('surfaces a download failure and installs nothing', async () => { h.streamOk = false const { wc } = fakeWc() const r = await downloadFfmpeg(wc) expect(r.ok).toBe(false) expect(r.error).toMatch(/network boom/) expect(existsSync(join(MANAGED, 'ffmpeg.exe'))).toBe(false) }) it('fails when extraction fails, leaving the managed dir empty', async () => { h.tarFail = true const { wc } = fakeWc() const r = await downloadFfmpeg(wc) expect(r.ok).toBe(false) expect(existsSync(join(MANAGED, 'ffmpeg.exe'))).toBe(false) }) it('fails when the archive lacks the expected binaries', async () => { h.extractWrites = false const { wc } = fakeWc() const r = await downloadFfmpeg(wc) expect(r.ok).toBe(false) expect(r.error).toMatch(/missing/i) }) }) describe('locateFfmpegManually', () => { function folderWith(files: string[]): string { const dir = mkdtempSync(join(ROOT, 'located-')) for (const f of files) writeFileSync(join(dir, f), 'x') return dir } it('installs from a folder that holds both runnable binaries', async () => { h.dialog = { canceled: false, filePaths: [folderWith(['ffmpeg.exe', 'ffprobe.exe'])] } const r = await locateFfmpegManually() expect(r).toEqual({ ok: true }) expect(existsSync(join(MANAGED, 'ffprobe.exe'))).toBe(true) }) it('rejects a folder missing a binary', async () => { h.dialog = { canceled: false, filePaths: [folderWith(['ffmpeg.exe'])] } const r = await locateFfmpegManually() expect(r.ok).toBe(false) expect(r.error).toMatch(/doesn't contain/i) }) it('rejects binaries that will not run', async () => { h.validateOk = false h.dialog = { canceled: false, filePaths: [folderWith(['ffmpeg.exe', 'ffprobe.exe'])] } const r = await locateFfmpegManually() expect(r.ok).toBe(false) expect(r.error).toMatch(/could not be run/i) }) it('reports a dismissed folder picker without installing anything', async () => { h.dialog = { canceled: true, filePaths: [] } const r = await locateFfmpegManually() expect(r.ok).toBe(false) expect(r.error).toMatch(/no folder selected/i) }) })