@aaronshaf/ger
Version:
Gerrit CLI and SDK - A modern CLI tool and TypeScript SDK for Gerrit Code Review, built with Effect-TS
209 lines (180 loc) • 7 kB
text/typescript
import { describe, test, expect, beforeAll, afterAll, afterEach, mock, spyOn } from 'bun:test'
import { HttpResponse, http } from 'msw'
import { setupServer } from 'msw/node'
import { Effect, Layer } from 'effect'
import { cherryCommand } from '@/cli/commands/cherry'
import { GerritApiServiceLive } from '@/api/gerrit'
import { ConfigService } from '@/services/config'
import { createMockConfigService } from './helpers/config-mock'
import type { ChangeInfo, RevisionInfo } from '@/schemas/gerrit'
import * as childProcess from 'node:child_process'
import type { SpawnSyncReturns } from 'node:child_process'
const mockChange: ChangeInfo = {
id: 'test-project~main~I123',
_number: 12345,
project: 'test-project',
branch: 'main',
change_id: 'I123',
subject: 'Test cherry-pick change',
status: 'NEW',
created: '2024-01-15 10:00:00.000000000',
updated: '2024-01-15 10:00:00.000000000',
}
const mockRevision: RevisionInfo = {
_number: 1,
ref: 'refs/changes/45/12345/1',
created: '2024-01-15 10:00:00.000000000',
uploader: { _account_id: 1000, name: 'Test User', email: 'test@example.com' },
}
const server = setupServer(
http.get('*/a/accounts/self', ({ request }) => {
const auth = request.headers.get('Authorization')
if (!auth?.startsWith('Basic ')) return HttpResponse.text('Unauthorized', { status: 401 })
return HttpResponse.json({ _account_id: 1000, name: 'Test User', email: 'test@example.com' })
}),
http.get('*/a/changes/12345', () => HttpResponse.json(mockChange)),
http.get('*/a/changes/12345/revisions/current', () => HttpResponse.json(mockRevision)),
)
beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }))
afterAll(() => server.close())
const mockConfig = createMockConfigService({
host: 'https://test.gerrit.com',
username: 'testuser',
password: 'testpass',
})
describe('cherry command', () => {
let mockExecSync: ReturnType<typeof spyOn>
let mockSpawnSync: ReturnType<typeof spyOn>
afterEach(() => {
server.resetHandlers()
mockExecSync?.mockRestore()
mockSpawnSync?.mockRestore()
})
const setupGitMocks = (spawnOverrides: { failFetch?: boolean; failCherry?: boolean } = {}) => {
mockExecSync = spyOn(childProcess, 'execSync').mockImplementation(((command: string) => {
if (command.includes('rev-parse --git-dir')) return Buffer.from('.git')
if (command.includes('remote -v'))
return Buffer.from('origin\thttps://test.gerrit.com/project\t(fetch)\n')
return Buffer.from('')
}) as typeof childProcess.execSync)
mockSpawnSync = spyOn(childProcess, 'spawnSync').mockImplementation(((
_cmd: string,
args: string[],
) => {
const isCherry = args.includes('cherry-pick')
if (isCherry && spawnOverrides.failCherry) {
return {
status: 1,
stderr: Buffer.from('conflict during cherry-pick'),
} as unknown as SpawnSyncReturns<Buffer>
}
if (!isCherry && spawnOverrides.failFetch) {
return {
status: 1,
stderr: Buffer.from('fetch failed'),
} as unknown as SpawnSyncReturns<Buffer>
}
return { status: 0, stderr: Buffer.from('') } as unknown as SpawnSyncReturns<Buffer>
}) as typeof childProcess.spawnSync)
}
test('cherry-picks a change successfully', async () => {
setupGitMocks()
await Effect.runPromise(
cherryCommand('12345', {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(Layer.succeed(ConfigService, mockConfig)),
),
)
const spawnCalls = mockSpawnSync.mock.calls as unknown as [string, string[]][]
expect(spawnCalls.some(([, args]) => args.includes('fetch'))).toBe(true)
expect(
spawnCalls.some(([, args]) => args.includes('cherry-pick') && args.includes('FETCH_HEAD')),
).toBe(true)
expect(spawnCalls.some(([, args]) => args.includes('cherry-pick') && args.includes('-n'))).toBe(
false,
)
})
test('cherry-picks with --no-commit flag', async () => {
setupGitMocks()
await Effect.runPromise(
cherryCommand('12345', { noCommit: true }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(Layer.succeed(ConfigService, mockConfig)),
),
)
const spawnCalls = mockSpawnSync.mock.calls as unknown as [string, string[]][]
expect(spawnCalls.some(([, args]) => args.includes('cherry-pick') && args.includes('-n'))).toBe(
true,
)
})
test('parses 12345/3 patchset syntax', async () => {
setupGitMocks()
server.use(
http.get('*/a/changes/12345/revisions/3', () =>
HttpResponse.json({ ...mockRevision, _number: 3, ref: 'refs/changes/45/12345/3' }),
),
)
await Effect.runPromise(
cherryCommand('12345/3', {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(Layer.succeed(ConfigService, mockConfig)),
),
)
const spawnCalls = mockSpawnSync.mock.calls as unknown as [string, string[]][]
expect(spawnCalls.some(([, args]) => args.includes('refs/changes/45/12345/3'))).toBe(true)
})
test('fails when not in a git repo', async () => {
mockExecSync = spyOn(childProcess, 'execSync').mockImplementation((() => {
throw new Error('not a git repo')
}) as typeof childProcess.execSync)
const result = await Effect.runPromise(
cherryCommand('12345', {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(Layer.succeed(ConfigService, mockConfig)),
Effect.either,
),
)
expect(result._tag).toBe('Left')
})
test('fails when change not found', async () => {
setupGitMocks()
server.use(http.get('*/a/changes/99999', () => HttpResponse.json({}, { status: 404 })))
const result = await Effect.runPromise(
cherryCommand('99999', {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(Layer.succeed(ConfigService, mockConfig)),
Effect.either,
),
)
expect(result._tag).toBe('Left')
})
test('fails when git cherry-pick fails', async () => {
setupGitMocks({ failCherry: true })
let threw = false
try {
await Effect.runPromise(
cherryCommand('12345', {}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(Layer.succeed(ConfigService, mockConfig)),
),
)
} catch (e) {
threw = true
expect(String(e)).toContain('Cherry-pick failed')
}
expect(threw).toBe(true)
})
test('uses --remote option when provided', async () => {
setupGitMocks()
await Effect.runPromise(
cherryCommand('12345', { remote: 'upstream' }).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(Layer.succeed(ConfigService, mockConfig)),
),
)
const spawnCalls = mockSpawnSync.mock.calls as unknown as [string, string[]][]
expect(spawnCalls.some(([, args]) => args.includes('fetch') && args.includes('upstream'))).toBe(
true,
)
})
})