UNPKG

@aaronshaf/ger

Version:

Gerrit CLI and SDK - A modern CLI tool and TypeScript SDK for Gerrit Code Review, built with Effect-TS

296 lines (236 loc) 9.72 kB
import { describe, test, expect, mock, beforeEach, afterEach } from 'bun:test' // Mock external dependencies const mockFs = { existsSync: mock(() => false), mkdirSync: mock(), } const mockExecSync = mock() const mockSpawnSync = mock() const mockConsole = { log: mock(), error: mock(), } // Mock modules mock.module('node:fs', () => mockFs) mock.module('node:child_process', () => ({ execSync: mockExecSync, spawnSync: mockSpawnSync, })) // Mock global console global.console = mockConsole as any describe('Workspace Command', () => { beforeEach(() => { // Reset all mocks Object.values(mockFs).forEach((mock) => mock.mockReset()) mockExecSync.mockReset() mockSpawnSync.mockReset() mockConsole.log.mockReset() mockConsole.error.mockReset() // Set default mock behaviors mockFs.existsSync.mockReturnValue(false) mockExecSync.mockImplementation((command: string) => { if (command === 'git rev-parse --git-dir') { return '.git' } if (command === 'git rev-parse --show-toplevel') { return '/repo/root' } if (command === 'git remote -v') { return 'origin\thttps://gerrit.example.com/project\t(fetch)\n' } return '' }) mockSpawnSync.mockReturnValue({ status: 0, stderr: '' }) }) afterEach(() => { mock.restore() }) describe('git repository validation', () => { test('should detect git repository', () => { mockExecSync.mockReturnValue('.git') const result = mockExecSync('git rev-parse --git-dir') expect(result).toBe('.git') }) test('should handle non-git directory', () => { mockExecSync.mockImplementation(() => { throw new Error('Not a git repository') }) expect(() => { mockExecSync('git rev-parse --git-dir') }).toThrow('Not a git repository') }) test('should find repository root', () => { mockExecSync.mockReturnValue('/custom/repo/path') const result = mockExecSync('git rev-parse --show-toplevel') expect(result).toBe('/custom/repo/path') }) }) describe('change specification parsing', () => { test('should parse numeric change ID', () => { const changeSpec = '12345' const parts = changeSpec.split(':') expect(parts[0]).toBe('12345') expect(parts[1]).toBeUndefined() }) test('should parse change ID with patchset', () => { const changeSpec = '12345:3' const parts = changeSpec.split(':') expect(parts[0]).toBe('12345') expect(parts[1]).toBe('3') }) test('should handle Change-Id format', () => { const changeId = 'I1234567890abcdef1234567890abcdef12345678' expect(changeId.startsWith('I')).toBe(true) expect(changeId.length).toBe(41) }) }) describe('remote matching', () => { test('should parse git remotes output', () => { const remoteOutput = 'origin\thttps://gerrit.example.com/project\t(fetch)\n' const lines = remoteOutput.split('\n') const match = lines[0].match(/^(\S+)\s+(\S+)\s+\(fetch\)$/) expect(match).toBeDefined() expect(match?.[1]).toBe('origin') expect(match?.[2]).toBe('https://gerrit.example.com/project') }) test('should match HTTP URLs', () => { const gerritHost = 'https://gerrit.example.com' const remoteUrl = 'https://gerrit.example.com/project' const gerritHostname = new URL(gerritHost).hostname const remoteHostname = new URL(remoteUrl).hostname expect(gerritHostname).toBe(remoteHostname) }) test('should match SSH URLs', () => { const gerritHost = 'https://gerrit.example.com' const sshUrl = 'git@gerrit.example.com:project' const gerritHostname = new URL(gerritHost).hostname const sshHostname = sshUrl.split('@')[1].split(':')[0] expect(gerritHostname).toBe(sshHostname) }) }) describe('workspace directory management', () => { test('should check if directory exists', () => { mockFs.existsSync.mockReturnValue(true) const exists = mockFs.existsSync() expect(exists).toBe(true) }) test('should create directory recursively', () => { mockFs.mkdirSync('/repo/root/.ger', { recursive: true }) expect(mockFs.mkdirSync).toHaveBeenCalledWith('/repo/root/.ger', { recursive: true }) }) test('should validate change numbers', () => { const validChangeNumber = '12345' const invalidChangeNumber = '../../../etc/passwd' expect(/^\d+$/.test(validChangeNumber)).toBe(true) expect(/^\d+$/.test(invalidChangeNumber)).toBe(false) }) }) describe('git operations', () => { test('should execute git fetch', () => { mockSpawnSync.mockReturnValue({ status: 0, stderr: '' }) const result = mockSpawnSync('git', ['fetch', 'origin', 'refs/changes/45/12345/1'], { encoding: 'utf8', cwd: '/repo/root', }) expect(result.status).toBe(0) expect(mockSpawnSync).toHaveBeenCalledWith( 'git', ['fetch', 'origin', 'refs/changes/45/12345/1'], { encoding: 'utf8', cwd: '/repo/root' }, ) }) test('should handle git fetch failure', () => { mockSpawnSync.mockReturnValue({ status: 1, stderr: 'fetch failed' }) const result = mockSpawnSync('git', ['fetch', 'origin', 'refs/changes/45/12345/1']) expect(result.status).toBe(1) expect(result.stderr).toBe('fetch failed') }) test('should create git worktree', () => { mockSpawnSync.mockReturnValue({ status: 0, stderr: '' }) const result = mockSpawnSync('git', ['worktree', 'add', '/workspace/path', 'FETCH_HEAD']) expect(result.status).toBe(0) }) test('should handle worktree creation failure', () => { mockSpawnSync.mockReturnValue({ status: 1, stderr: 'worktree add failed' }) const result = mockSpawnSync('git', ['worktree', 'add', '/workspace/path', 'FETCH_HEAD']) expect(result.status).toBe(1) expect(result.stderr).toBe('worktree add failed') }) }) describe('output formats', () => { test('should output pretty format messages', () => { console.log('Fetching change 12345: Test change subject') console.log('Creating worktree at: /repo/root/.ger/12345') console.log('✓ Workspace created successfully!') console.log(' Run: cd /repo/root/.ger/12345') expect(mockConsole.log).toHaveBeenCalledWith('Fetching change 12345: Test change subject') expect(mockConsole.log).toHaveBeenCalledWith('Creating worktree at: /repo/root/.ger/12345') expect(mockConsole.log).toHaveBeenCalledWith('✓ Workspace created successfully!') expect(mockConsole.log).toHaveBeenCalledWith(' Run: cd /repo/root/.ger/12345') }) test('should output XML format', () => { console.log('<?xml version="1.0" encoding="UTF-8"?>') console.log('<workspace>') console.log(' <path>/repo/root/.ger/12345</path>') console.log(' <change_number>12345</change_number>') console.log(' <subject><![CDATA[Test change subject]]></subject>') console.log(' <created>true</created>') console.log('</workspace>') expect(mockConsole.log).toHaveBeenCalledWith('<?xml version="1.0" encoding="UTF-8"?>') expect(mockConsole.log).toHaveBeenCalledWith('<workspace>') expect(mockConsole.log).toHaveBeenCalledWith(' <path>/repo/root/.ger/12345</path>') expect(mockConsole.log).toHaveBeenCalledWith(' <change_number>12345</change_number>') expect(mockConsole.log).toHaveBeenCalledWith( ' <subject><![CDATA[Test change subject]]></subject>', ) expect(mockConsole.log).toHaveBeenCalledWith(' <created>true</created>') expect(mockConsole.log).toHaveBeenCalledWith('</workspace>') }) test('should output XML format for existing workspace', () => { console.log('<?xml version="1.0" encoding="UTF-8"?>') console.log('<workspace>') console.log(' <path>/repo/root/.ger/12345</path>') console.log(' <exists>true</exists>') console.log('</workspace>') expect(mockConsole.log).toHaveBeenCalledWith('<?xml version="1.0" encoding="UTF-8"?>') expect(mockConsole.log).toHaveBeenCalledWith('<workspace>') expect(mockConsole.log).toHaveBeenCalledWith(' <path>/repo/root/.ger/12345</path>') expect(mockConsole.log).toHaveBeenCalledWith(' <exists>true</exists>') expect(mockConsole.log).toHaveBeenCalledWith('</workspace>') }) }) describe('path safety', () => { test('should prevent path traversal in workspace names', () => { const maliciousPath = '../../../etc/passwd' const safePath = '12345' expect(/^\d+$/.test(maliciousPath)).toBe(false) expect(/^\d+$/.test(safePath)).toBe(true) }) test('should use safe path joining', () => { const repoRoot = '/repo/root' const changeNumber = '12345' const workspacePath = `${repoRoot}/.ger/${changeNumber}` expect(workspacePath).toBe('/repo/root/.ger/12345') expect(workspacePath).not.toContain('..') }) }) describe('error handling', () => { test('should handle command execution errors', () => { mockExecSync.mockImplementation(() => { throw new Error('Command failed') }) expect(() => { mockExecSync('git status') }).toThrow('Command failed') }) test('should handle spawn errors', () => { mockSpawnSync.mockReturnValue({ status: 1, stderr: 'Command not found', }) const result = mockSpawnSync('nonexistent-command') expect(result.status).toBe(1) expect(result.stderr).toBe('Command not found') }) }) })