feat: publish av-tools 0.1.0
This commit is contained in:
176
tests/unit/export-output.test.ts
Normal file
176
tests/unit/export-output.test.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user