1376c2dee8
Audit-pass over CODE-AUDIT.md (~48 items closed this pass; all verified — typecheck + 234 tests + eslint + prettier green). Correctness / bugs: - B3: match the release checksum to the asset's filename line (no wrong-hash verify) - B4: newline-safe metadata probe (one --print with a unit-separator delimiter) - B5 / L88: guard the meta event against canceled items; progress no longer promotes a queued item outside pump() - B7: cookie-login promise always resolves (handles destroy-without-close) - L146: trim parser rejects >2 colon-group times; M36: Library selection counts only actionable rows - L11 / L50 / L156 / L57 / L159 / L15 / L3: live queue count, empty-cookie message, schedule picker min, dead-code/comment cleanup Type safety: - Enable noUncheckedIndexedAccess + noFallthroughCasesInSwitch (15 real edge cases fixed) Resilience / Windows / metadata: - R5: settings write failure handled (no unhandled IPC rejection; reconciles to truth) - W1 / W5 / W6: min window size, seeded folder picker, parented sign-in window; L147 dead macOS branches removed - CL1: shared stdout markers; package/builder metadata (license, homepage, repository, copyright, tsbuildinfo glob) Copy / docs / tests: - M37 / SR9 dev-jargon cleanup in hints; M8 / M25 / M26 / L66 / L80 / L81 reconciled - New unit tests for L35 (isValidMediaItem) and L36 (compareVersions) This commit also checkpoints the previously-uncommitted feat/tray-background-clipboard work it builds on: background running + auto-download, library clipboard detection, tray, binary management & library scale, credential encryption at rest, the shared jsonStore and ui/ primitives, and the eslint/prettier tooling. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
261 lines
8.0 KiB
TypeScript
261 lines
8.0 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import {
|
|
isSafeFilenameTemplate,
|
|
isSafeOutputDir,
|
|
isValidHistoryEntry,
|
|
isValidErrorLogEntry,
|
|
isTemplateLike,
|
|
isValidSource,
|
|
isValidMediaItem
|
|
} from '../src/main/validation'
|
|
import type { HistoryEntry, ErrorLogEntry, Source, MediaItem } from '@shared/ipc'
|
|
|
|
// --- S4: filename template path-traversal -----------------------------------
|
|
|
|
describe('isSafeFilenameTemplate', () => {
|
|
it('accepts the default template', () => {
|
|
expect(isSafeFilenameTemplate('%(title)s.%(ext)s')).toBe(true)
|
|
})
|
|
|
|
it('accepts sub-folder templates with yt-dlp fields', () => {
|
|
expect(isSafeFilenameTemplate('%(uploader)s/%(title)s.%(ext)s')).toBe(true)
|
|
expect(isSafeFilenameTemplate('%(uploader)s\\%(title)s.%(ext)s')).toBe(true)
|
|
})
|
|
|
|
it('rejects forward-slash traversal', () => {
|
|
expect(isSafeFilenameTemplate('%(title)s/../../evil.%(ext)s')).toBe(false)
|
|
})
|
|
|
|
it('rejects backslash traversal', () => {
|
|
expect(isSafeFilenameTemplate('%(title)s\\..\\..\\win32.exe')).toBe(false)
|
|
})
|
|
|
|
it('rejects a bare .. segment', () => {
|
|
expect(isSafeFilenameTemplate('..')).toBe(false)
|
|
})
|
|
|
|
it('rejects absolute paths', () => {
|
|
expect(isSafeFilenameTemplate('C:\\Windows\\system32\\%(title)s')).toBe(false)
|
|
expect(isSafeFilenameTemplate('/etc/%(title)s')).toBe(false)
|
|
})
|
|
|
|
it('allows .. only when embedded in a longer segment (not a real traversal)', () => {
|
|
// '..foo' / 'foo..bar' are filenames, not parent-dir references.
|
|
expect(isSafeFilenameTemplate('my..video.%(ext)s')).toBe(true)
|
|
expect(isSafeFilenameTemplate('.%(title)s.%(ext)s')).toBe(true) // leading dot = hidden file
|
|
})
|
|
|
|
// --- T1: Windows-specific bypasses --------------------------------------
|
|
|
|
it('rejects a drive-relative prefix that path.isAbsolute misses', () => {
|
|
// 'C:foo' is NOT absolute per Node, but still escapes the output dir.
|
|
expect(isSafeFilenameTemplate('C:foo\\%(title)s.%(ext)s')).toBe(false)
|
|
expect(isSafeFilenameTemplate('c:%(title)s')).toBe(false)
|
|
})
|
|
|
|
it('rejects a .. segment dressed up with trailing/leading dots or spaces', () => {
|
|
// Windows trims trailing dots/spaces, so these all resolve to '..'.
|
|
expect(isSafeFilenameTemplate('%(title)s/.. /x')).toBe(false)
|
|
expect(isSafeFilenameTemplate('%(title)s/.. ./x')).toBe(false)
|
|
expect(isSafeFilenameTemplate('%(title)s/ ../x')).toBe(false)
|
|
expect(isSafeFilenameTemplate('%(title)s\\.. \\x')).toBe(false)
|
|
expect(isSafeFilenameTemplate('...')).toBe(false)
|
|
})
|
|
})
|
|
|
|
// --- S4: output directory ----------------------------------------------------
|
|
|
|
describe('isSafeOutputDir', () => {
|
|
it('accepts an empty string (resolves to OS Downloads)', () => {
|
|
expect(isSafeOutputDir('')).toBe(true)
|
|
})
|
|
|
|
it('accepts an absolute Windows path', () => {
|
|
expect(isSafeOutputDir('C:\\Users\\me\\Downloads')).toBe(true)
|
|
})
|
|
|
|
it('rejects a relative path', () => {
|
|
expect(isSafeOutputDir('Downloads')).toBe(false)
|
|
expect(isSafeOutputDir('..\\elsewhere')).toBe(false)
|
|
})
|
|
})
|
|
|
|
// --- S5: persisted JSON entry validation -------------------------------------
|
|
|
|
const validHistory: HistoryEntry = {
|
|
id: 'a',
|
|
title: 'A video',
|
|
url: 'https://example.com/v',
|
|
kind: 'video',
|
|
quality: '1080p',
|
|
completedAt: 1700000000000
|
|
}
|
|
|
|
describe('isValidHistoryEntry', () => {
|
|
it('accepts a well-formed entry', () => {
|
|
expect(isValidHistoryEntry(validHistory)).toBe(true)
|
|
})
|
|
|
|
it('accepts optional string fields', () => {
|
|
expect(
|
|
isValidHistoryEntry({
|
|
...validHistory,
|
|
filePath: 'C:\\x.mp4',
|
|
channel: 'ch',
|
|
sizeLabel: '10 MB',
|
|
thumbnail: 'https://x/y.jpg'
|
|
})
|
|
).toBe(true)
|
|
})
|
|
|
|
it('rejects a wrong kind', () => {
|
|
expect(isValidHistoryEntry({ ...validHistory, kind: 'image' })).toBe(false)
|
|
})
|
|
|
|
it('rejects a missing required field', () => {
|
|
const { url: _url, ...noUrl } = validHistory
|
|
expect(isValidHistoryEntry(noUrl)).toBe(false)
|
|
})
|
|
|
|
it('rejects a non-string filePath (e.g. injected number/object)', () => {
|
|
expect(isValidHistoryEntry({ ...validHistory, filePath: 42 })).toBe(false)
|
|
})
|
|
|
|
it('rejects non-objects', () => {
|
|
expect(isValidHistoryEntry(null)).toBe(false)
|
|
expect(isValidHistoryEntry('nope')).toBe(false)
|
|
expect(isValidHistoryEntry(undefined)).toBe(false)
|
|
})
|
|
})
|
|
|
|
const validError: ErrorLogEntry = {
|
|
id: 'e',
|
|
url: 'https://example.com/v',
|
|
kind: 'audio',
|
|
error: 'boom',
|
|
occurredAt: 1700000000000
|
|
}
|
|
|
|
describe('isValidErrorLogEntry', () => {
|
|
it('accepts a well-formed entry', () => {
|
|
expect(isValidErrorLogEntry(validError)).toBe(true)
|
|
expect(isValidErrorLogEntry({ ...validError, title: 'optional' })).toBe(true)
|
|
})
|
|
|
|
it('rejects a missing error message', () => {
|
|
const { error: _error, ...noError } = validError
|
|
expect(isValidErrorLogEntry(noError)).toBe(false)
|
|
})
|
|
|
|
it('rejects a non-numeric occurredAt', () => {
|
|
expect(isValidErrorLogEntry({ ...validError, occurredAt: 'yesterday' })).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('isTemplateLike', () => {
|
|
it('accepts an object with a string or numeric id', () => {
|
|
expect(isTemplateLike({ id: 'x', name: 'n', args: '--foo' })).toBe(true)
|
|
expect(isTemplateLike({ id: 1 })).toBe(true)
|
|
})
|
|
|
|
it('rejects entries without an id', () => {
|
|
expect(isTemplateLike({ name: 'n', args: '--foo' })).toBe(false)
|
|
expect(isTemplateLike(null)).toBe(false)
|
|
expect(isTemplateLike('str')).toBe(false)
|
|
})
|
|
})
|
|
|
|
// --- T7: source row validation (feedUrl drives a network fetch) --------------
|
|
|
|
const validSource: Source = {
|
|
id: 's1',
|
|
url: 'https://www.youtube.com/@x',
|
|
kind: 'channel',
|
|
title: 'X',
|
|
addedAt: 1700000000000,
|
|
itemCount: 5
|
|
}
|
|
|
|
describe('isValidSource', () => {
|
|
it('accepts a well-formed source and its optional fields', () => {
|
|
expect(isValidSource(validSource)).toBe(true)
|
|
expect(
|
|
isValidSource({
|
|
...validSource,
|
|
channel: 'X',
|
|
watched: true,
|
|
feedUrl: 'https://www.youtube.com/feeds/videos.xml?channel_id=UC',
|
|
lastIndexedAt: 1700000000001
|
|
})
|
|
).toBe(true)
|
|
})
|
|
|
|
it('rejects a non-string feedUrl (audit T7)', () => {
|
|
expect(isValidSource({ ...validSource, feedUrl: 123 })).toBe(false)
|
|
})
|
|
|
|
it('rejects a non-boolean watched (audit T7)', () => {
|
|
expect(isValidSource({ ...validSource, watched: 'yes' })).toBe(false)
|
|
})
|
|
|
|
it('rejects a wrong kind or a missing required field', () => {
|
|
expect(isValidSource({ ...validSource, kind: 'video' })).toBe(false)
|
|
const { id: _id, ...noId } = validSource
|
|
expect(isValidSource(noId)).toBe(false)
|
|
})
|
|
|
|
it('rejects non-objects', () => {
|
|
expect(isValidSource(null)).toBe(false)
|
|
expect(isValidSource('nope')).toBe(false)
|
|
})
|
|
})
|
|
|
|
// --- L35: isValidMediaItem — the persisted media-items.json row validator ----
|
|
|
|
describe('isValidMediaItem', () => {
|
|
const valid: MediaItem = {
|
|
id: 'src1:vid1',
|
|
sourceId: 'src1',
|
|
videoId: 'vid1',
|
|
title: 'A video',
|
|
url: 'https://youtube.com/watch?v=vid1',
|
|
playlistTitle: 'Uploads',
|
|
playlistIndex: 1,
|
|
downloaded: false
|
|
}
|
|
|
|
it('accepts a minimal valid item', () => {
|
|
expect(isValidMediaItem(valid)).toBe(true)
|
|
})
|
|
|
|
it('accepts the optional fields when well-typed', () => {
|
|
expect(
|
|
isValidMediaItem({
|
|
...valid,
|
|
durationLabel: '3:21',
|
|
downloaded: true,
|
|
downloadedAt: 1_700_000_000_000,
|
|
filePath: 'C:/Videos/a.mp4'
|
|
})
|
|
).toBe(true)
|
|
})
|
|
|
|
it('rejects a missing or wrong-typed required field', () => {
|
|
const { videoId: _v, ...noVideoId } = valid
|
|
expect(isValidMediaItem(noVideoId)).toBe(false)
|
|
expect(isValidMediaItem({ ...valid, playlistIndex: '1' })).toBe(false)
|
|
expect(isValidMediaItem({ ...valid, downloaded: 'no' })).toBe(false)
|
|
})
|
|
|
|
it('rejects wrong-typed optional fields', () => {
|
|
expect(isValidMediaItem({ ...valid, durationLabel: 42 })).toBe(false)
|
|
expect(isValidMediaItem({ ...valid, downloadedAt: 'soon' })).toBe(false)
|
|
expect(isValidMediaItem({ ...valid, filePath: 99 })).toBe(false)
|
|
})
|
|
|
|
it('rejects non-objects', () => {
|
|
expect(isValidMediaItem(null)).toBe(false)
|
|
expect(isValidMediaItem('nope')).toBe(false)
|
|
})
|
|
})
|