UNPKG

@aaronshaf/ger

Version:

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

260 lines (217 loc) 9.01 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 { reviewersCommand } from '@/cli/commands/reviewers' import { ConfigService } from '@/services/config' import { createMockConfigService } from './helpers/config-mock' const mockReviewersResponse = [ { _account_id: 1001, name: 'Alice Smith', email: 'alice@example.com', username: 'alice', approvals: { 'Code-Review': '0' }, }, { _account_id: 1002, name: 'Bob Jones', email: 'bob@example.com', username: 'bob', approvals: { 'Code-Review': '+1' }, }, ] const server = setupServer( 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('reviewers command', () => { let mockConsoleLog: ReturnType<typeof mock> let mockConsoleError: ReturnType<typeof mock> let mockProcessExit: ReturnType<typeof mock> beforeAll(() => { server.listen({ onUnhandledRequest: 'bypass' }) }) afterAll(() => { server.close() }) beforeEach(() => { mockConsoleLog = mock(() => {}) mockConsoleError = mock(() => {}) mockProcessExit = mock(() => {}) console.log = mockConsoleLog console.error = mockConsoleError process.exit = mockProcessExit as unknown as typeof process.exit }) afterEach(() => { server.resetHandlers() }) it('should list reviewers with plain output', async () => { server.use( http.get('*/a/changes/12345/reviewers', () => { return HttpResponse.text(`)]}'\n${JSON.stringify(mockReviewersResponse)}`) }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = reviewersCommand('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('Alice Smith') expect(output).toContain('alice@example.com') expect(output).toContain('Bob Jones') expect(output).toContain('bob@example.com') }) it('should handle email-only reviewer (no _account_id, no name)', async () => { const emailOnlyReviewer = [{ email: 'ext@external.com', approvals: {} }] server.use( http.get('*/a/changes/12345/reviewers', () => { return HttpResponse.text(`)]}'\n${JSON.stringify(emailOnlyReviewer)}`) }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = reviewersCommand('12345', {}).pipe( Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer), ) await Effect.runPromise(program) const lines = mockConsoleLog.mock.calls.map((call) => call[0] as string) expect(lines).toContain('ext@external.com') // Must not produce "ext@external.com <ext@external.com>" expect(lines.join('\n')).not.toContain('ext@external.com <ext@external.com>') }) it('should output JSON format', async () => { server.use( http.get('*/a/changes/12345/reviewers', () => { return HttpResponse.text(`)]}'\n${JSON.stringify(mockReviewersResponse)}`) }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = reviewersCommand('12345', { json: true }).pipe( Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer), ) await Effect.runPromise(program) const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n') const parsed = JSON.parse(output) as { status: string change_id: string reviewers: Array<{ account_id?: number; name?: string; email?: string }> } expect(parsed.status).toBe('success') expect(parsed.change_id).toBe('12345') expect(parsed.reviewers).toBeArray() expect(parsed.reviewers.length).toBe(2) expect(parsed.reviewers[0].name).toBe('Alice Smith') expect(parsed.reviewers[1].email).toBe('bob@example.com') }) it('should output XML format', async () => { server.use( http.get('*/a/changes/12345/reviewers', () => { return HttpResponse.text(`)]}'\n${JSON.stringify(mockReviewersResponse)}`) }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = reviewersCommand('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('<reviewers_result>') expect(output).toContain('<status>success</status>') expect(output).toContain('<change_id><![CDATA[12345]]></change_id>') expect(output).toContain('<name><![CDATA[Alice Smith]]></name>') expect(output).toContain('<email><![CDATA[bob@example.com]]></email>') expect(output).toContain('</reviewers_result>') }) it('should handle empty reviewers list', async () => { server.use( http.get('*/a/changes/12345/reviewers', () => { return HttpResponse.text(`)]}'\n${JSON.stringify([])}`) }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = reviewersCommand('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('No reviewers') }) it('should exit 1 on API error', async () => { server.use( http.get('*/a/changes/99999/reviewers', () => { return HttpResponse.text('Not Found', { status: 404 }) }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = reviewersCommand('99999', {}).pipe( Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer), ) await Effect.runPromise(program) const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n') expect(errorOutput).toContain('Error:') expect(mockProcessExit.mock.calls[0][0]).toBe(1) }) it('should exit 1 and output XML on API error with --xml', async () => { server.use( http.get('*/a/changes/99999/reviewers', () => { return HttpResponse.text('Forbidden', { status: 403 }) }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = reviewersCommand('99999', { 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('<reviewers_result>') expect(output).toContain('<status>error</status>') expect(output).toContain('</reviewers_result>') expect(mockProcessExit.mock.calls[0][0]).toBe(1) }) it('should exit 1 and output JSON on API error with --json', async () => { server.use( http.get('*/a/changes/99999/reviewers', () => { return HttpResponse.text('Not Found', { status: 404 }) }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = reviewersCommand('99999', { json: true }).pipe( Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer), ) await Effect.runPromise(program) const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n') const parsed = JSON.parse(output) as { status: string; error: string } expect(parsed.status).toBe('error') expect(typeof parsed.error).toBe('string') expect(parsed.error.length).toBeGreaterThan(0) expect(mockProcessExit.mock.calls[0][0]).toBe(1) }) it('should exit 1 when no change-id and HEAD has no Change-Id', async () => { const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = reviewersCommand(undefined, {}).pipe( Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer), ) 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') expect(mockProcessExit.mock.calls[0][0]).toBe(1) }) })