/** * ───────────────────────────────────────────── * SPB Git — Personal Git Platform * ───────────────────────────────────────────── * Author : Simon-Pierre Boucher * Contact : contact@spboucher.ai * File : test/unit/util.test.mjs * Purpose : Unit tests — validation, path safety, formatting * License : MIT © Simon-Pierre Boucher * ───────────────────────────────────────────── */ import { describe, it, expect } from 'vitest'; import { isValidRepoName, safeJoin, isValidTreePath, formatBytes, escapeHtml, identiconSvg, mapLimit, } from '../../src/lib/util.mjs'; describe('isValidRepoName', () => { it('accepts sane names', () => { for (const name of ['demo', 'api-tools', 'a', 'repo.name', 'x_1', 'a2.b-c_d']) { expect(isValidRepoName(name), name).toBe(true); } }); it('rejects traversal, uppercase, and junk', () => { for (const name of ['../etc', 'a/../b', 'A', '-lead', '.hidden', '', 'a'.repeat(65), 'name.git', 'a..b', 'a b']) { expect(isValidRepoName(name), String(name)).toBe(false); } }); }); describe('safeJoin', () => { it('keeps paths inside the root', () => { expect(safeJoin('/srv/git', 'demo.git')).toBe('/srv/git/demo.git'); }); it('throws on escape attempts', () => { expect(() => safeJoin('/srv/git', '../../etc/passwd')).toThrow(); expect(() => safeJoin('/srv/git', 'a', '..', '..', 'b')).toThrow(); }); }); describe('isValidTreePath', () => { it('accepts normal repo paths', () => { expect(isValidTreePath('src/index.js')).toBe(true); expect(isValidTreePath('a.txt')).toBe(true); }); it('rejects traversal and absolutes', () => { for (const path of ['../x', 'a/../b', '/abs', 'a//b', 'a/./b', 'nul\0byte']) { expect(isValidTreePath(path), path).toBe(false); } }); }); describe('formatBytes', () => { it('formats units', () => { expect(formatBytes(0)).toBe('0 B'); expect(formatBytes(1024)).toBe('1.0 KB'); expect(formatBytes(1536)).toBe('1.5 KB'); expect(formatBytes(5 * 1024 * 1024)).toBe('5.0 MB'); }); }); describe('escapeHtml', () => { it('escapes the five metacharacters', () => { expect(escapeHtml('&\'')).toBe('<a href="x">&'</a>'); }); }); describe('identiconSvg', () => { it('is deterministic and symmetric-sized', () => { const a = identiconSvg('contact@spboucher.ai'); const b = identiconSvg('contact@spboucher.ai'); const c = identiconSvg('other@example.com'); expect(a).toBe(b); expect(a).not.toBe(c); expect(a).toContain(' { it('preserves order and limits concurrency', async () => { let inFlight = 0; let peak = 0; const result = await mapLimit([1, 2, 3, 4, 5, 6], 2, async (n) => { inFlight += 1; peak = Math.max(peak, inFlight); await new Promise((r) => setTimeout(r, 5)); inFlight -= 1; return n * 10; }); expect(result).toEqual([10, 20, 30, 40, 50, 60]); expect(peak).toBeLessThanOrEqual(2); }); });