import { describe, expect, it } from 'vitest'; import { createExportReport, deriveOutputFileName, exportReportBlob, isBrowserPreviewMimeType, isSafeArchiveLeafName, mimeTypeFromOutputName, normalizeOutputMimeType, preferredExtensionForMimeType, redactPrivatePaths, safeExportFileName, serializeExportReport, uniqueExportFileNames, type ExportResult, } from '../../src/export'; describe('safe export output names', () => { it('removes directory traversal, device names, and unsafe punctuation', () => { expect(safeExportFileName('../../CON.mp4')).toBe('_CON.mp4'); expect(safeExportFileName('..\\..\\holiday 🎬.MP4')).toBe('holiday.mp4'); expect( safeExportFileName('/private/video.mov', { extension: '.webm' }) ).toBe('video.webm'); expect(isSafeArchiveLeafName('../unsafe.mp4')).toBe(false); expect(isSafeArchiveLeafName('safe-output.mp4')).toBe(true); }); it('derives numbered names and rejects invalid part positions', () => { expect( deriveOutputFileName({ sourceName: 'My recording.mov', operation: 'split', extension: 'mp4', partIndex: 1, partCount: 12, }) ).toBe('My-recording-split-002.mp4'); expect(() => deriveOutputFileName({ sourceName: 'source.mp4', operation: 'split', partIndex: 2, partCount: 2, }) ).toThrow(/part index/iu); }); it('deduplicates names for case-insensitive filesystems', () => { expect( uniqueExportFileNames([ 'Clip.mp4', 'clip.mp4', '../../CLIP.mp4', 'other.mp4', ]) ).toEqual(['Clip.mp4', 'clip-2.mp4', 'CLIP-3.mp4', 'other.mp4']); }); }); describe('output MIME helpers', () => { it('normalizes base types and falls back to known extensions', () => { expect(normalizeOutputMimeType('Video/MP4; codecs=avc1')).toBe('video/mp4'); expect(normalizeOutputMimeType('not a mime', 'frame.png')).toBe( 'image/png' ); expect(normalizeOutputMimeType(undefined, 'unknown.xyz')).toBe( 'application/octet-stream' ); expect(mimeTypeFromOutputName('captions.vtt')).toBe('text/vtt'); expect(preferredExtensionForMimeType('image/jpeg')).toBe('.jpg'); expect(isBrowserPreviewMimeType('audio/mpeg')).toBe(true); expect(isBrowserPreviewMimeType('application/json')).toBe(false); }); }); describe('privacy-safe JSON export reports', () => { const result = { id: 'result', planId: 'plan', outputId: 'media', fileName: '../../converted.mp4', blob: new Blob(['private bytes']), mimeType: 'video/mp4', role: 'media', size: 13, createdAt: '2026-01-01T00:00:00.000Z', } satisfies ExportResult; it('includes plan diagnostics and output metadata without source data', async () => { const privateRuntimeResult = { ...result, // Extra runtime fields must never be copied into the report. sourceBlob: new Blob(['source secret']), hostPath: '/home/zemion/private/source.mp4', }; const report = createExportReport({ plan: { id: 'plan-/home/zemion/private/source.mp4', operation: 'convert', diagnostics: [ { code: 'codec-warning', severity: 'warning', message: 'Read /mnt/DATA/Videos/secret.mov via blob:https://local/id', field: 'C:\\Users\\Person\\Videos\\secret.mov', }, ], }, results: [privateRuntimeResult], execution: { status: 'completed', engineMode: 'single-thread', ffmpegVersion: '6.0', elapsedSeconds: 1.25, conversionMilliseconds: 1_000, outputReadMilliseconds: 25, cleanupMilliseconds: 5, exitCode: 0, }, generatedAt: '2026-07-24T12:00:00+02:00', }); expect(report.generatedAt).toBe('2026-07-24T10:00:00.000Z'); expect(report.plan.id).toContain('[local path]'); expect(report.outputs[0]).toEqual({ id: 'result', outputId: 'media', fileName: 'converted.mp4', mimeType: 'video/mp4', role: 'media', sizeBytes: 13, }); expect(report.diagnostics[0]?.message).toBe( 'Read [local path] via [object URL]' ); expect(report.diagnostics[0]?.field).toBe('[local path]'); expect(report.privacy).toEqual({ includesSourceMedia: false, includesCommandArguments: false, privatePathsRedacted: true, }); expect(report.execution).toMatchObject({ conversionMilliseconds: 1_000, outputReadMilliseconds: 25, cleanupMilliseconds: 5, }); const serialized = serializeExportReport(report); expect(serialized.endsWith('\n')).toBe(true); expect(serialized).not.toContain('sourceBlob'); expect(serialized).not.toContain('hostPath'); expect(serialized).not.toContain('/home/zemion'); const blob = exportReportBlob(report); expect(blob.type).toBe('application/json'); expect(await blob.text()).toBe(serialized); }); it('redacts common local path and URL forms', () => { expect( redactPrivatePaths( 'file:///home/me/a.mp4 C:\\fakepath\\b.mp4 /Users/me/c.mp4' ) ).toBe('[local path] [local path] [local path]'); }); it('rejects invalid numeric report metadata', () => { expect(() => createExportReport({ plan: { id: 'plan', operation: 'convert', diagnostics: [] }, results: [{ ...result, size: Number.NaN }], }) ).toThrow(/output size/iu); }); });