import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { mirrorPath, resolveWithin, safeComponent } from '../src/core/paths.ts'; describe('safeComponent', () => { it('keeps ordinary German titles intact', () => { assert.equal(safeComponent('Verschlüsselung & Sicherheit'), 'Verschlüsselung & Sicherheit'); }); it('neutralises separators rather than escaping them', () => { assert.equal(safeComponent('a/b\\c'), 'a-b-c'); }); it('defuses traversal in every shape', () => { assert.equal(safeComponent('..'), 'untitled'); assert.equal(safeComponent('../../etc/passwd'), 'etc-passwd'); assert.equal(safeComponent('...hidden'), 'hidden'); }); it('never leaves a ".." anywhere in the result', () => { for (const evil of ['..', '../..', 'a/../b', '....', '.. .. ..']) { assert.ok(!safeComponent(evil).includes('..'), `".." survived in ${evil}`); } }); it('strips control characters and NUL', () => { assert.equal(safeComponent('a\u0000b\u001fc'), 'abc'); }); it('avoids Windows reserved names and trailing-dot collisions', () => { assert.equal(safeComponent('CON'), '_CON'); assert.equal(safeComponent('report.'), 'report'); }); it('falls back when nothing survives', () => { assert.equal(safeComponent('///', 'fallback'), 'fallback'); assert.equal(safeComponent(' '), 'untitled'); }); it('truncates long names but keeps the extension', () => { const out = safeComponent('x'.repeat(300) + '.pdf'); assert.ok(out.length <= 100); assert.ok(out.endsWith('.pdf')); }); }); describe('mirrorPath', () => { it('builds a course/container/card/name path', () => { const path = mirrorPath( { courseId: 'c', courseTitle: 'Mathe', containerTitle: 'Board 1', cardTitle: 'Karte' }, 'blatt.pdf', 'f1', ); assert.equal(path, 'Mathe/Board 1/Karte/blatt.pdf'); }); it('omits missing breadcrumb levels', () => { assert.equal(mirrorPath({ courseId: 'c', courseTitle: 'Mathe' }, 'a.pdf', 'f1'), 'Mathe/a.pdf'); }); it('cannot be made to escape via any component', () => { const path = mirrorPath( { courseId: 'c', courseTitle: '../..', containerTitle: '/etc', cardTitle: '..' }, '../../.ssh/authorized_keys', 'f1', ); assert.ok(!path.includes('..'), `escaped: ${path}`); assert.ok(!path.startsWith('/'), `absolute: ${path}`); }); }); describe('resolveWithin', () => { it('resolves a normal relative path under the root', () => { assert.equal(resolveWithin('/mirror', 'Mathe/a.pdf'), '/mirror/Mathe/a.pdf'); }); it('refuses absolute paths, traversal and drive letters', () => { assert.throws(() => resolveWithin('/mirror', '/etc/passwd'), /absolute/); assert.throws(() => resolveWithin('/mirror', 'a/../../etc'), /traversal/); assert.throws(() => resolveWithin('/mirror', 'C:/windows'), /absolute/); }); });