@aaronshaf/ger
Version:
Gerrit CLI and SDK - A modern CLI tool and TypeScript SDK for Gerrit Code Review, built with Effect-TS
269 lines (222 loc) • 8.15 kB
text/typescript
import { describe, test, expect, beforeAll, afterAll, afterEach, mock } from 'bun:test'
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'
import { Effect, Layer } from 'effect'
import { showCommand } from '@/cli/commands/show'
import { commentCommand } from '@/cli/commands/comment'
import { diffCommand } from '@/cli/commands/diff'
import { GerritApiServiceLive } from '@/api/gerrit'
import { ConfigService } from '@/services/config'
import { generateMockChange } from '@/test-utils/mock-generator'
import { createMockConfigService } from './helpers/config-mock'
/**
* Integration tests to verify that commands accept both change number and Change-ID formats
*/
const CHANGE_NUMBER = '392385'
const CHANGE_ID = 'If5a3ae8cb5a107e187447802358417f311d0c4b1'
const mockChange = generateMockChange({
_number: 392385,
change_id: CHANGE_ID,
subject: 'WIP: test',
status: 'NEW',
project: 'my-project',
branch: 'master',
created: '2024-01-15 10:00:00.000000000',
updated: '2024-01-15 12:00:00.000000000',
owner: {
_account_id: 1001,
name: 'Test User',
email: 'test@example.com',
},
})
const mockDiff = `--- a/test.txt
+++ b/test.txt
@@ -1,1 +1,2 @@
original line
+new line`
const server = setupServer(
http.get('*/a/accounts/self', () => {
return HttpResponse.json({
_account_id: 1000,
name: 'Test User',
email: 'test@example.com',
})
}),
// Handler that matches both change number and Change-ID
http.get('*/a/changes/:changeId', ({ params }) => {
const { changeId } = params
// Accept both formats
if (changeId === CHANGE_NUMBER || changeId === CHANGE_ID) {
return HttpResponse.text(`)]}'
${JSON.stringify(mockChange)}`)
}
return HttpResponse.text('Not Found', { status: 404 })
}),
http.get('*/a/changes/:changeId/revisions/current/patch', ({ params }) => {
const { changeId } = params
if (changeId === CHANGE_NUMBER || changeId === CHANGE_ID) {
return HttpResponse.text(btoa(mockDiff))
}
return HttpResponse.text('Not Found', { status: 404 })
}),
http.get('*/a/changes/:changeId/revisions/current/comments', ({ params }) => {
const { changeId } = params
if (changeId === CHANGE_NUMBER || changeId === CHANGE_ID) {
return HttpResponse.text(`)]}'
{}`)
}
return HttpResponse.text('Not Found', { status: 404 })
}),
http.post('*/a/changes/:changeId/revisions/current/review', async ({ params }) => {
const { changeId } = params
if (changeId === CHANGE_NUMBER || changeId === CHANGE_ID) {
return HttpResponse.text(`)]}'
{}`)
}
return HttpResponse.text('Not Found', { status: 404 })
}),
)
let capturedLogs: string[] = []
let capturedErrors: string[] = []
const mockConsoleLog = mock((...args: any[]) => {
capturedLogs.push(args.join(' '))
})
const mockConsoleError = mock((...args: any[]) => {
capturedErrors.push(args.join(' '))
})
const originalConsoleLog = console.log
const originalConsoleError = console.error
beforeAll(() => {
server.listen({ onUnhandledRequest: 'bypass' })
// @ts-ignore
console.log = mockConsoleLog
// @ts-ignore
console.error = mockConsoleError
})
afterAll(() => {
server.close()
console.log = originalConsoleLog
console.error = originalConsoleError
})
afterEach(() => {
server.resetHandlers()
mockConsoleLog.mockClear()
mockConsoleError.mockClear()
capturedLogs = []
capturedErrors = []
})
const createMockConfigLayer = (): Layer.Layer<ConfigService, never, never> =>
Layer.succeed(ConfigService, createMockConfigService())
describe('Change ID format support', () => {
describe('show command', () => {
test('accepts numeric change number', async () => {
const effect = showCommand(CHANGE_NUMBER, {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await Effect.runPromise(effect)
const output = capturedLogs.join('\n')
expect(output).toContain('Change 392385')
expect(output).toContain('WIP: test')
expect(capturedErrors.length).toBe(0)
})
test('accepts Change-ID format', async () => {
const effect = showCommand(CHANGE_ID, {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await Effect.runPromise(effect)
const output = capturedLogs.join('\n')
expect(output).toContain('Change 392385')
expect(output).toContain('WIP: test')
expect(capturedErrors.length).toBe(0)
})
test('rejects invalid change identifier', async () => {
const effect = showCommand('invalid-id', {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await Effect.runPromise(effect)
const output = capturedErrors.join('\n')
expect(output).toContain('Invalid change identifier')
})
})
describe('diff command', () => {
test('accepts numeric change number', async () => {
const effect = diffCommand(CHANGE_NUMBER, {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await Effect.runPromise(effect)
const output = capturedLogs.join('\n')
expect(output).toContain('--- a/test.txt')
expect(output).toContain('+++ b/test.txt')
expect(capturedErrors.length).toBe(0)
})
test('accepts Change-ID format', async () => {
const effect = diffCommand(CHANGE_ID, {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await Effect.runPromise(effect)
const output = capturedLogs.join('\n')
expect(output).toContain('--- a/test.txt')
expect(output).toContain('+++ b/test.txt')
expect(capturedErrors.length).toBe(0)
})
})
describe('comment command', () => {
test('accepts numeric change number', async () => {
const effect = commentCommand(CHANGE_NUMBER, { message: 'LGTM' }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await Effect.runPromise(effect)
const output = capturedLogs.join('\n')
expect(output).toContain('Comment posted successfully')
expect(capturedErrors.length).toBe(0)
})
test('accepts Change-ID format', async () => {
const effect = commentCommand(CHANGE_ID, { message: 'LGTM' }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await Effect.runPromise(effect)
const output = capturedLogs.join('\n')
expect(output).toContain('Comment posted successfully')
expect(capturedErrors.length).toBe(0)
})
})
describe('edge cases', () => {
test('trims whitespace from change identifiers', async () => {
const effect = showCommand(` ${CHANGE_NUMBER} `, {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await Effect.runPromise(effect)
const output = capturedLogs.join('\n')
expect(output).toContain('Change 392385')
expect(capturedErrors.length).toBe(0)
})
test('validates Change-ID format strictly (uppercase I)', async () => {
const lowercaseChangeId = 'if5a3ae8cb5a107e187447802358417f311d0c4b1'
const effect = showCommand(lowercaseChangeId, {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await Effect.runPromise(effect)
const output = capturedErrors.join('\n')
expect(output).toContain('Invalid change identifier')
})
test('rejects Change-ID with incorrect length', async () => {
const shortChangeId = 'If5a3ae8cb5a107e18744780235841'
const effect = showCommand(shortChangeId, {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(createMockConfigLayer()),
)
await Effect.runPromise(effect)
const output = capturedErrors.join('\n')
expect(output).toContain('Invalid change identifier')
})
})
})