import { describe, expect, it } from 'vitest'; import { DRIVE_FILE_COLLECTION, DRIVE_FOLDER_COLLECTION, driveFileLexicon, driveFolderLexicon, folderAncestry, folderPath, isValidName, MAX_NAME_LENGTH, normalizeFolderKey, } from '../src/lexicon.js'; /** @param {Array<[string, string, string]>} rows - rkey, name, parent */ function treeOf(rows) { return new Map(rows.map(([rkey, name, parent]) => [rkey, { name, parent }])); } describe('the lexicons', () => { it('describe the collections they are named for', () => { expect(driveFileLexicon.id).toBe(DRIVE_FILE_COLLECTION); expect(driveFolderLexicon.id).toBe(DRIVE_FOLDER_COLLECTION); expect(driveFileLexicon.defs.main.record.required).toContain('parts'); expect(driveFolderLexicon.defs.main.record.required).toContain('name'); }); it('key a file to its folder by record key, not by path', () => { const folder = driveFileLexicon.defs.main.record.properties.folder; expect(folder.description).toMatch(/[Rr]ecord key/); }); }); describe('isValidName', () => { it('accepts an ordinary name', () => { expect(isValidName('notes.txt')).toBe(true); expect(isValidName('a name with spaces.pdf')).toBe(true); expect(isValidName('résumé.pdf')).toBe(true); }); it('rejects a name that is not one path segment', () => { expect(isValidName('a/b')).toBe(false); expect(isValidName('a\\b')).toBe(false); expect(isValidName('.')).toBe(false); expect(isValidName('..')).toBe(false); }); it('rejects an empty, over-long, or non-string name', () => { expect(isValidName('')).toBe(false); expect(isValidName('x'.repeat(MAX_NAME_LENGTH + 1))).toBe(false); expect(isValidName(undefined)).toBe(false); expect(isValidName(42)).toBe(false); }); it('rejects a control character, which would display as a shorter name', () => { expect(isValidName(`safe${String.fromCharCode(0)}.txt`)).toBe(false); expect(isValidName(`safe${String.fromCharCode(0x1f)}.txt`)).toBe(false); expect(isValidName(`safe${String.fromCharCode(0x7f)}.txt`)).toBe(false); expect(isValidName(`safe${String.fromCharCode(0x0a)}.txt`)).toBe(false); }); }); describe('normalizeFolderKey', () => { it('reads the top level from every empty form', () => { expect(normalizeFolderKey('')).toBe(''); expect(normalizeFolderKey(undefined)).toBe(''); expect(normalizeFolderKey(null)).toBe(''); }); it('keeps a record key as it is', () => { expect(normalizeFolderKey('3lfoldertid')).toBe('3lfoldertid'); }); it('refuses anything that is not one key', () => { expect(normalizeFolderKey('photos/2026')).toBe(null); expect(normalizeFolderKey('..')).toBe(null); expect(normalizeFolderKey(7)).toBe(null); }); }); describe('folderAncestry', () => { const tree = treeOf([ ['a', 'photos', ''], ['b', '2026', 'a'], ['c', 'june', 'b'], ]); it('gives nothing for the top level', () => { expect(folderAncestry('', tree)).toEqual([]); }); it('walks from the top level down to the folder', () => { expect(folderAncestry('c', tree)).toEqual([ { rkey: 'a', name: 'photos' }, { rkey: 'b', name: '2026' }, { rkey: 'c', name: 'june' }, ]); }); it('reports a chain that does not resolve', () => { expect(folderAncestry('missing', tree)).toBe(null); }); it('refuses to follow a loop rather than hanging', () => { const looped = treeOf([ ['x', 'one', 'y'], ['y', 'two', 'x'], ]); expect(folderAncestry('x', looped)).toBe(null); }); }); describe('folderPath', () => { const tree = treeOf([ ['a', 'photos', ''], ['b', '2026', 'a'], ]); it('joins the chain for display', () => { expect(folderPath('b', tree)).toBe('photos/2026'); expect(folderPath('', tree)).toBe(''); }); it('reports a broken chain as no path', () => { expect(folderPath('nope', tree)).toBe(null); }); });