UNPKG

@aaronshaf/ger

Version:

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

369 lines (308 loc) 13.3 kB
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 { rebaseCommand } from '@/cli/commands/rebase' import { ConfigService } from '@/services/config' import { createMockConfigService } from './helpers/config-mock' import type { ChangeInfo } from '@/schemas/gerrit' const mockChange: ChangeInfo = { id: 'test-project~master~I123', _number: 12345, change_id: 'I123', project: 'test-project', branch: 'master', subject: 'Test change to rebase', status: 'NEW', created: '2024-01-01 10:00:00.000000000', updated: '2024-01-01 12:00:00.000000000', owner: { _account_id: 1000, name: 'Test User', email: 'test@example.com', }, labels: { 'Code-Review': { value: 0, }, Verified: { value: 0, }, }, work_in_progress: false, submittable: false, } // 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('rebase command', () => { let mockConsoleLog: ReturnType<typeof mock> let mockConsoleError: ReturnType<typeof mock> beforeAll(() => { server.listen({ onUnhandledRequest: 'bypass' }) }) afterAll(() => { server.close() }) beforeEach(() => { mockConsoleLog = mock(() => {}) mockConsoleError = mock(() => {}) console.log = mockConsoleLog console.error = mockConsoleError }) afterEach(() => { server.resetHandlers() }) it('should rebase a change without a base', async () => { server.use( http.post('*/a/changes/12345/revisions/current/rebase', async ({ request }) => { const body = (await request.json()) as { base?: string } expect(body.base).toBeUndefined() return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`) }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = rebaseCommand('12345', {}).pipe( Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer), ) await Effect.runPromise(program) const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n') expect(output).toContain('Rebased change 12345') expect(output).toContain('Test change to rebase') expect(output).toContain('Branch: master') expect(output).not.toContain('Base:') }) it('should rebase a change with a specified base', async () => { server.use( http.post('*/a/changes/12345/revisions/current/rebase', async ({ request }) => { const body = (await request.json()) as { base?: string } expect(body.base).toBe('refs/heads/main') return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`) }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = rebaseCommand('12345', { base: 'refs/heads/main', }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer)) await Effect.runPromise(program) const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n') expect(output).toContain('Rebased change 12345') expect(output).toContain('Test change to rebase') expect(output).toContain('Branch: master') expect(output).toContain('Base: refs/heads/main') }) it('should output XML format when --xml flag is used', async () => { server.use( http.post('*/a/changes/12345/revisions/current/rebase', async () => { return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`) }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = rebaseCommand('12345', { xml: true }).pipe( Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer), ) 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('<rebase_result>') expect(output).toContain('<status>success</status>') expect(output).toContain('<change_number>12345</change_number>') expect(output).toContain('<subject><![CDATA[Test change to rebase]]></subject>') expect(output).toContain('<branch>master</branch>') expect(output).toContain('</rebase_result>') }) it('should output XML format with base when --base is provided', async () => { server.use( http.post('*/a/changes/12345/revisions/current/rebase', async ({ request }) => { const body = (await request.json()) as { base?: string } expect(body.base).toBe('refs/heads/develop') return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`) }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = rebaseCommand('12345', { xml: true, base: 'refs/heads/develop', }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer)) await Effect.runPromise(program) const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n') expect(output).toContain('<rebase_result>') expect(output).toContain('<status>success</status>') expect(output).toContain('<base><![CDATA[refs/heads/develop]]></base>') expect(output).toContain('</rebase_result>') }) it('should handle not found errors gracefully with pretty output', async () => { server.use( http.post('*/a/changes/99999/revisions/current/rebase', () => { return HttpResponse.text('Change not found', { status: 404 }) }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = rebaseCommand('99999', {}).pipe( Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer), ) // Error boundary catches and outputs to console.error await Effect.runPromise(program) const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n') expect(errorOutput).toContain('Error:') }) it('should handle not found errors with XML output when --xml flag is used', async () => { server.use( http.post('*/a/changes/99999/revisions/current/rebase', () => { return HttpResponse.text('Change not found', { status: 404 }) }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = rebaseCommand('99999', { xml: true }).pipe( Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer), ) // Error boundary catches and outputs XML error 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('<rebase_result>') expect(output).toContain('<status>error</status>') expect(output).toContain('<error><![CDATA[') expect(output).toContain('</rebase_result>') }) it('should output error to console.error when no change ID and HEAD has no Change-Id', async () => { const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = rebaseCommand(undefined, {}).pipe( Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer), ) // Error boundary catches NoChangeIdError and outputs to console.error await Effect.runPromise(program) const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n') expect(errorOutput).toContain('Error:') expect(errorOutput).toContain('No Change-ID found in HEAD commit') }) it('should output XML error when no change ID and HEAD has no Change-Id with --xml flag', async () => { const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = rebaseCommand(undefined, { xml: true }).pipe( Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer), ) // Error boundary catches NoChangeIdError and outputs XML error 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('<rebase_result>') expect(output).toContain('<status>error</status>') expect(output).toContain('No Change-ID found in HEAD commit') expect(output).toContain('</rebase_result>') }) it('should treat empty string as missing change ID and auto-detect', async () => { const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = rebaseCommand('', {}).pipe( Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer), ) // Empty string triggers auto-detection, which fails with NoChangeIdError await Effect.runPromise(program) const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n') expect(errorOutput).toContain('Error:') expect(errorOutput).toContain('No Change-ID found in HEAD commit') }) it('should handle rebase conflicts gracefully', async () => { server.use( http.post('*/a/changes/12345/revisions/current/rebase', () => { return HttpResponse.text('Rebase conflict detected', { status: 409 }) }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = rebaseCommand('12345', {}).pipe( Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer), ) // Error boundary catches and outputs to console.error await Effect.runPromise(program) const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n') expect(errorOutput).toContain('Error:') }) it('should handle API errors gracefully', async () => { server.use( http.post('*/a/changes/12345/revisions/current/rebase', () => { return HttpResponse.text('Forbidden', { status: 403 }) }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = rebaseCommand('12345', {}).pipe( Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer), ) // Error boundary catches and outputs to console.error await Effect.runPromise(program) const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n') expect(errorOutput).toContain('Error:') }) it('should handle changes that are already up to date', async () => { server.use( http.post('*/a/changes/12345/revisions/current/rebase', () => { return HttpResponse.text('Change is already up to date', { status: 409 }) }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = rebaseCommand('12345', {}).pipe( Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer), ) // Error boundary catches and outputs to console.error await Effect.runPromise(program) const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n') expect(errorOutput).toContain('Error:') }) it('should handle network errors gracefully', async () => { server.use( http.post('*/a/changes/12345/revisions/current/rebase', () => { return HttpResponse.error() }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = rebaseCommand('12345', {}).pipe( Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer), ) // Error boundary catches and outputs to console.error await Effect.runPromise(program) const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n') expect(errorOutput).toContain('Error:') }) it('should handle network errors with XML output', async () => { server.use( http.post('*/a/changes/12345/revisions/current/rebase', () => { return HttpResponse.error() }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = rebaseCommand('12345', { xml: true }).pipe( Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer), ) // Error boundary catches and outputs XML error 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('<rebase_result>') expect(output).toContain('<status>error</status>') expect(output).toContain('<error><![CDATA[') expect(output).toContain('</rebase_result>') }) })