SPB Git

spb/spbgit Public MIT

SPB Git — the platform hosting itself

JavaScript 73.9% CSS 11.7% Nunjucks 11.6% Shell 2.7%
3.3 KB · 94 lines javascript
Raw Blame History
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('&lt;a href=&quot;x&quot;&gt;&amp;&#39;&lt;/a&gt;');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