spb/spbgit Public MIT
SPB Git — the platform hosting itself
JavaScript 73.9%
CSS 11.7%
Nunjucks 11.6%
Shell 2.7%
1/**2 * ─────────────────────────────────────────────3 * SPB Git — Personal Git Platform4 * ─────────────────────────────────────────────5 * Author : Simon-Pierre Boucher6 * Contact : contact@spboucher.ai7 * File : test/unit/util.test.mjs8 * Purpose : Unit tests — validation, path safety, formatting9 * License : MIT © Simon-Pierre Boucher10 * ─────────────────────────────────────────────11 */1213import { describe, it, expect } from 'vitest';14import {15 isValidRepoName, safeJoin, isValidTreePath, formatBytes, escapeHtml, identiconSvg, mapLimit,16} from '../../src/lib/util.mjs';1718describe('isValidRepoName', () => {19 it('accepts sane names', () => {20 for (const name of ['demo', 'api-tools', 'a', 'repo.name', 'x_1', 'a2.b-c_d']) {21 expect(isValidRepoName(name), name).toBe(true);22 }23 });24 it('rejects traversal, uppercase, and junk', () => {25 for (const name of ['../etc', 'a/../b', 'A', '-lead', '.hidden', '', 'a'.repeat(65), 'name.git', 'a..b', 'a b']) {26 expect(isValidRepoName(name), String(name)).toBe(false);27 }28 });29});3031describe('safeJoin', () => {32 it('keeps paths inside the root', () => {33 expect(safeJoin('/srv/git', 'demo.git')).toBe('/srv/git/demo.git');34 });35 it('throws on escape attempts', () => {36 expect(() => safeJoin('/srv/git', '../../etc/passwd')).toThrow();37 expect(() => safeJoin('/srv/git', 'a', '..', '..', 'b')).toThrow();38 });39});4041describe('isValidTreePath', () => {42 it('accepts normal repo paths', () => {43 expect(isValidTreePath('src/index.js')).toBe(true);44 expect(isValidTreePath('a.txt')).toBe(true);45 });46 it('rejects traversal and absolutes', () => {47 for (const path of ['../x', 'a/../b', '/abs', 'a//b', 'a/./b', 'nul\0byte']) {48 expect(isValidTreePath(path), path).toBe(false);49 }50 });51});5253describe('formatBytes', () => {54 it('formats units', () => {55 expect(formatBytes(0)).toBe('0 B');56 expect(formatBytes(1024)).toBe('1.0 KB');57 expect(formatBytes(1536)).toBe('1.5 KB');58 expect(formatBytes(5 * 1024 * 1024)).toBe('5.0 MB');59 });60});6162describe('escapeHtml', () => {63 it('escapes the five metacharacters', () => {64 expect(escapeHtml('<a href="x">&\'</a>')).toBe('<a href="x">&'</a>');65 });66});6768describe('identiconSvg', () => {69 it('is deterministic and symmetric-sized', () => {70 const a = identiconSvg('contact@spboucher.ai');71 const b = identiconSvg('contact@spboucher.ai');72 const c = identiconSvg('other@example.com');73 expect(a).toBe(b);74 expect(a).not.toBe(c);75 expect(a).toContain('<svg');76 });77});7879describe('mapLimit', () => {80 it('preserves order and limits concurrency', async () => {81 let inFlight = 0;82 let peak = 0;83 const result = await mapLimit([1, 2, 3, 4, 5, 6], 2, async (n) => {84 inFlight += 1;85 peak = Math.max(peak, inFlight);86 await new Promise((r) => setTimeout(r, 5));87 inFlight -= 1;88 return n * 10;89 });90 expect(result).toEqual([10, 20, 30, 40, 50, 60]);91 expect(peak).toBeLessThanOrEqual(2);92 });93});94