import { describe, it, expect } from 'vitest'; import { extractRepoName, getCloneDir, validateGitUrl } from '../../src/server/git-clone.js'; describe('git-clone', () => { describe('extractRepoName', () => { it('extracts name from HTTPS URL', () => { expect(extractRepoName('https://github.com/user/my-repo.git')).toBe('my-repo'); }); it('extracts name from HTTPS URL without .git suffix', () => { expect(extractRepoName('https://github.com/user/my-repo')).toBe('my-repo'); }); it('extracts name from SSH URL', () => { expect(extractRepoName('git@github.com:user/my-repo.git')).toBe('my-repo'); }); it('handles trailing slashes', () => { expect(extractRepoName('https://github.com/user/my-repo/')).toBe('my-repo'); }); it('handles nested paths', () => { expect(extractRepoName('https://gitlab.com/group/subgroup/repo.git')).toBe('repo'); }); }); describe('getCloneDir', () => { it('returns path under ~/.gitnexus/repos/', () => { const dir = getCloneDir('my-repo'); expect(dir).toContain('.gitnexus'); expect(dir).toMatch(/repos/); expect(dir).toContain('my-repo'); }); }); describe('validateGitUrl', () => { it('allows valid HTTPS GitHub URLs', () => { expect(() => validateGitUrl('https://github.com/user/repo.git')).not.toThrow(); expect(() => validateGitUrl('https://github.com/user/repo')).not.toThrow(); }); it('allows valid HTTP URLs', () => { expect(() => validateGitUrl('http://gitlab.com/user/repo.git')).not.toThrow(); }); it('blocks SSH protocol', () => { expect(() => validateGitUrl('ssh://git@github.com/user/repo.git')).toThrow( 'Only https:// and http://', ); }); it('blocks file:// protocol', () => { expect(() => validateGitUrl('file:///etc/passwd')).toThrow('Only https:// and http://'); }); it('blocks IPv4 loopback', () => { expect(() => validateGitUrl('http://127.0.0.1/repo.git')).toThrow('private/internal'); expect(() => validateGitUrl('http://127.255.0.1/repo.git')).toThrow('private/internal'); }); it('blocks IPv6 loopback ::1', () => { // Node URL parser strips brackets: hostname is "::1" not "[::1]" expect(() => validateGitUrl('http://[::1]/repo.git')).toThrow('private/internal'); }); it('blocks IPv4 private ranges (10.x, 172.16-31.x, 192.168.x)', () => { expect(() => validateGitUrl('http://10.0.0.1/repo.git')).toThrow('private/internal'); expect(() => validateGitUrl('http://172.16.0.1/repo.git')).toThrow('private/internal'); expect(() => validateGitUrl('http://172.31.255.255/repo.git')).toThrow('private/internal'); expect(() => validateGitUrl('http://192.168.1.1/repo.git')).toThrow('private/internal'); }); it('blocks link-local addresses', () => { expect(() => validateGitUrl('http://169.254.1.1/repo.git')).toThrow('private/internal'); }); it('blocks cloud metadata hostname', () => { expect(() => validateGitUrl('http://metadata.google.internal/repo')).toThrow( 'private/internal', ); expect(() => validateGitUrl('http://metadata.azure.com/repo')).toThrow('private/internal'); }); it('blocks IPv6 ULA (fc/fd)', () => { expect(() => validateGitUrl('http://[fc00::1]/repo.git')).toThrow('private/internal'); expect(() => validateGitUrl('http://[fd12::1]/repo.git')).toThrow('private/internal'); }); it('blocks IPv6 link-local (fe80)', () => { expect(() => validateGitUrl('http://[fe80::1]/repo.git')).toThrow('private/internal'); }); it('blocks IPv4-mapped IPv6', () => { expect(() => validateGitUrl('http://[::ffff:127.0.0.1]/repo.git')).toThrow( 'private/internal', ); }); it('does not block valid public IPs', () => { expect(() => validateGitUrl('https://140.82.121.4/repo.git')).not.toThrow(); }); it('blocks CGN range (100.64.0.0/10)', () => { expect(() => validateGitUrl('http://100.64.0.1/repo.git')).toThrow('private/internal'); expect(() => validateGitUrl('http://100.127.255.255/repo.git')).toThrow('private/internal'); }); it('blocks benchmarking range (198.18.0.0/15)', () => { expect(() => validateGitUrl('http://198.18.0.1/repo.git')).toThrow('private/internal'); expect(() => validateGitUrl('http://198.19.255.255/repo.git')).toThrow('private/internal'); }); it('blocks numeric decimal IP encoding', () => { expect(() => validateGitUrl('http://2130706433/repo.git')).toThrow('private/internal'); }); it('blocks hex IP encoding', () => { expect(() => validateGitUrl('http://0x7f000001/repo.git')).toThrow('private/internal'); }); it('blocks 0.0.0.0', () => { expect(() => validateGitUrl('http://0.0.0.0/repo.git')).toThrow('private/internal'); }); }); });