@aaronshaf/ger
Version:
Gerrit CLI and SDK - A modern CLI tool and TypeScript SDK for Gerrit Code Review, built with Effect-TS
422 lines (349 loc) • 14.4 kB
text/typescript
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, mock } from 'bun:test'
import { Effect, Layer } from 'effect'
import { HttpResponse, http } from 'msw'
import { setupServer } from 'msw/node'
import { GerritApiServiceLive } from '@/api/gerrit'
import { diffCommand } from '@/cli/commands/diff'
import { ConfigService } from '@/services/config'
import { createMockConfigService } from './helpers/config-mock'
// Create MSW server
const server = setupServer(
// Default handler for auth check
http.get('*/a/accounts/self', ({ request }) => {
const auth = request.headers.get('Authorization')
if (!auth || !auth.startsWith('Basic ')) {
return HttpResponse.text('Unauthorized', { status: 401 })
}
return HttpResponse.json({
_account_id: 1000,
name: 'Test User',
email: 'test@example.com',
})
}),
)
describe('diff command', () => {
let mockConsoleLog: ReturnType<typeof mock>
let mockConsoleError: ReturnType<typeof mock>
beforeAll(() => {
server.listen({ onUnhandledRequest: 'bypass' })
})
afterAll(() => {
server.close()
})
beforeEach(() => {
server.resetHandlers()
mockConsoleLog = mock(() => {})
mockConsoleError = mock(() => {})
console.log = mockConsoleLog
console.error = mockConsoleError
})
afterEach(() => {
server.resetHandlers()
})
const createMockConfigLayer = () => Layer.succeed(ConfigService, createMockConfigService())
describe('unified diff output', () => {
it('should fetch and display unified diff by default', async () => {
const mockDiff = `ZGlmZiAtLWdpdCBhL3NyYy9tYWluLmpzIGIvc3JjL21haW4uanMKaW5kZXggMTIzNDU2Ny4uYWJjZGVmZyAxMDA2NDQKLS0tIGEvc3JjL21haW4uanMKKysrIGIvc3JjL21haW4uanMKQEAgLTEwLDYgKzEwLDcgQEAgZXhwb3J0IGZ1bmN0aW9uIG1haW4oKSB7CiAgIGNvbnNvbGUubG9nKCdTdGFydGluZyBhcHBsaWNhdGlvbicpCisgIGNvbnNvbGUubG9nKCdEZWJ1ZyBpbmZvJykKICAgcmV0dXJuICdzdWNjZXNzJwogfQ==`
server.use(
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
return HttpResponse.text(mockDiff)
}),
)
const program = diffCommand('12345', {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await Effect.runPromise(program)
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
expect(output).toContain('diff --git a/src/main.js b/src/main.js')
expect(output).toContain("+ console.log('Debug info')")
})
it('should fetch diff for specific file', async () => {
const mockDiff = {
meta_a: {
name: 'src/utils.js',
content_type: 'text/plain',
},
meta_b: {
name: 'src/utils.js',
content_type: 'text/plain',
},
content: [
{
ab: ['export function helper() {', ' return true'],
},
{
b: [' // Added comment'],
},
{
ab: ['}'],
},
],
}
server.use(
http.get('*/a/changes/:changeId/revisions/current/files/*/diff', () => {
return HttpResponse.text(`)]}'\n${JSON.stringify(mockDiff)}`)
}),
)
const program = diffCommand('12345', { file: 'src/utils.js' }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await Effect.runPromise(program)
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
expect(output).toContain('--- a/src/utils.js')
expect(output).toContain('+++ b/src/utils.js')
expect(output).toContain('+ // Added comment')
})
it('should use specified format', async () => {
const mockFiles = {
'/COMMIT_MSG': { status: 'A' },
'src/main.js': { status: 'M' },
'src/utils.js': { status: 'A' },
}
server.use(
http.get('*/a/changes/:changeId/revisions/current/files', () => {
return HttpResponse.text(`)]}'\n${JSON.stringify(mockFiles)}`)
}),
)
const program = diffCommand('12345', { format: 'json' }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await Effect.runPromise(program)
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
expect(output).toContain('"src/main.js"')
expect(output).toContain('"status": "M"')
})
})
describe('files-only output', () => {
it('should fetch and display files list when filesOnly is true', async () => {
const mockFiles = {
'/COMMIT_MSG': { status: 'A' },
'src/main.js': { status: 'M' },
'src/utils.js': { status: 'A' },
'README.md': { status: 'M' },
}
server.use(
http.get('*/a/changes/:changeId/revisions/current/files', () => {
return HttpResponse.text(`)]}'\n${JSON.stringify(mockFiles)}`)
}),
)
const program = diffCommand('12345', { filesOnly: true }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await Effect.runPromise(program)
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
expect(output).toContain('Changed files')
expect(output).toContain('src/main.js')
expect(output).toContain('src/utils.js')
expect(output).toContain('README.md')
})
})
describe('XML output', () => {
it('should output XML format for unified diff', async () => {
const mockDiff = `Y29uc29sZS5sb2coJ3Rlc3QnKQorY29uc29sZS5sb2coJ25ldyBsaW5lJyk=`
server.use(
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
return HttpResponse.text(mockDiff)
}),
)
const program = diffCommand('12345', { xml: true }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await Effect.runPromise(program)
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
expect(output).toContain('<diff_result>')
expect(output).toContain('<status>success</status>')
expect(output).toContain('<change_id>12345</change_id>')
expect(output).toContain('<content><![CDATA[')
expect(output).toContain('console.log')
expect(output).toContain(']]></content>')
expect(output).toContain('</diff_result>')
})
it('should output XML format for files list', async () => {
const mockFiles = {
'/COMMIT_MSG': { status: 'A' },
'src/main.js': { status: 'M' },
'test.js': { status: 'A' },
}
server.use(
http.get('*/a/changes/:changeId/revisions/current/files', () => {
return HttpResponse.text(`)]}'\n${JSON.stringify(mockFiles)}`)
}),
)
const program = diffCommand('12345', { xml: true, filesOnly: true }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await Effect.runPromise(program)
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
expect(output).toContain('<diff_result>')
expect(output).toContain('<files>')
expect(output).toContain('<file>src/main.js</file>')
expect(output).toContain('<file>test.js</file>')
expect(output).toContain('</files>')
expect(output).toContain('</diff_result>')
})
it('should output XML format for JSON data', async () => {
const mockData = {
'/COMMIT_MSG': { status: 'A' },
'src/main.js': { status: 'M' },
'test.js': { status: 'A' },
}
server.use(
http.get('*/a/changes/:changeId/revisions/current/files', () => {
return HttpResponse.text(`)]}'\n${JSON.stringify(mockData)}`)
}),
)
const program = diffCommand('12345', { xml: true, format: 'json' }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await Effect.runPromise(program)
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
expect(output).toContain('<content><![CDATA[')
expect(output).toContain('"src/main.js"')
expect(output).toContain('"status": "M"')
expect(output).toContain(']]></content>')
})
})
describe('error handling', () => {
it('should handle 404 change not found', async () => {
server.use(
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
return HttpResponse.text('Change not found', { status: 404 })
}),
)
const program = diffCommand('12345', {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await expect(Effect.runPromise(program)).rejects.toThrow('Failed to get diff')
})
it('should handle 403 access denied', async () => {
server.use(
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
return HttpResponse.text('Access denied', { status: 403 })
}),
)
const program = diffCommand('12345', {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await expect(Effect.runPromise(program)).rejects.toThrow('Failed to get diff')
})
it('should handle network errors', async () => {
server.use(
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
return HttpResponse.error()
}),
)
const program = diffCommand('12345', {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await expect(Effect.runPromise(program)).rejects.toThrow('Failed to get diff')
})
it('should handle invalid options schema', async () => {
const program = diffCommand('12345', { format: 'invalid' } as never).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await expect(Effect.runPromise(program)).rejects.toThrow('Invalid diff command options')
})
})
describe('output formatting', () => {
it('should apply pretty formatting to unified diff by default', async () => {
const mockDiff = `ZGlmZiAtLWdpdCBhL3NyYy9tYWluLmpzIGIvc3JjL21haW4uanMKaW5kZXggMTIzNDU2Ny4uYWJjZGVmZyAxMDA2NDQKLS0tIGEvc3JjL21haW4uanMKKysrIGIvc3JjL21haW4uanMKQEAgLTEwLDYgKzEwLDcgQEAgZXhwb3J0IGZ1bmN0aW9uIG1haW4oKSB7CiAgIGNvbnNvbGUubG9nKCdTdGFydGluZyBhcHBsaWNhdGlvbicpCisgIGNvbnNvbGUubG9nKCdEZWJ1ZyBpbmZvJykKICAgcmV0dXJuICdzdWNjZXNzJwogfQ==`
server.use(
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
return HttpResponse.text(mockDiff)
}),
)
const program = diffCommand('12345', {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await Effect.runPromise(program)
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
// Check that pretty formatting is applied (colors would be removed in test output,
// but structure should be preserved)
expect(output).toContain('diff --git')
expect(output).toContain('index 1234567..abcdefg')
expect(output).toContain('--- a/src/main.js')
expect(output).toContain('+++ b/src/main.js')
})
it('should format files list prettily', async () => {
const mockFiles = {
'/COMMIT_MSG': { status: 'A' },
'src/main.js': { status: 'M' },
'src/utils.js': { status: 'A' },
'test/test.js': { status: 'M' },
'README.md': { status: 'M' },
}
server.use(
http.get('*/a/changes/:changeId/revisions/current/files', () => {
return HttpResponse.text(`)]}'\n${JSON.stringify(mockFiles)}`)
}),
)
const program = diffCommand('12345', { filesOnly: true }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await Effect.runPromise(program)
const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
expect(output).toContain('Changed files')
expect(output).toContain('src/main.js')
expect(output).toContain('src/utils.js')
expect(output).toContain('test/test.js')
expect(output).toContain('README.md')
})
})
describe('option validation', () => {
beforeEach(() => {
// Default mock handlers for validation tests
server.use(
http.get('*/a/changes/:changeId/revisions/current/patch', () => {
return HttpResponse.text('bW9jayBkaWZmIGNvbnRlbnQ=') // base64 for "mock diff content"
}),
http.get('*/a/changes/:changeId/revisions/current/files', () => {
return HttpResponse.text(`)]}'\n{"src/test.js": {"status": "M"}}`)
}),
http.get('*/a/changes/:changeId/revisions/current/files/*/diff', () => {
return HttpResponse.text(`)]}'\n{"content": [{"ab": ["test content"]}]}`)
}),
)
})
it('should accept valid format values', async () => {
// Test each valid format
for (const format of ['unified', 'json', 'files'] as const) {
const program = diffCommand('12345', { format }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await expect(Effect.runPromise(program)).resolves.toBeUndefined()
}
})
it('should accept optional parameters', async () => {
const program = diffCommand('12345', {
xml: true,
file: 'src/test.js',
filesOnly: false,
format: 'unified',
}).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(createMockConfigLayer()))
await expect(Effect.runPromise(program)).resolves.toBeUndefined()
})
it('should work with minimal options', async () => {
const program = diffCommand('12345', {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await expect(Effect.runPromise(program)).resolves.toBeUndefined()
})
})
})