UNPKG

@aaronshaf/ger

Version:

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

318 lines (261 loc) 12.1 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 { voteCommand } from '@/cli/commands/vote' 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('vote 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 vote with Code-Review only', async () => { server.use( http.post('*/a/changes/12345/revisions/current/review', async ({ request }) => { const body = (await request.json()) as { labels?: Record<string, number>; message?: string } expect(body.labels).toEqual({ 'Code-Review': 2 }) expect(body.message).toBeUndefined() return HttpResponse.text(")]}'\n{}") }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = voteCommand('12345', { codeReview: 2, }).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('Voted on change 12345') expect(output).toContain('Code-Review: +2') }) it('should vote with Code-Review and Verified', async () => { server.use( http.post('*/a/changes/12345/revisions/current/review', async ({ request }) => { const body = (await request.json()) as { labels?: Record<string, number>; message?: string } expect(body.labels).toEqual({ 'Code-Review': 1, Verified: 1 }) return HttpResponse.text(")]}'\n{}") }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = voteCommand('12345', { codeReview: 1, verified: 1, }).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('Code-Review: +1') expect(output).toContain('Verified: +1') }) it('should vote with negative values', async () => { server.use( http.post('*/a/changes/12345/revisions/current/review', async ({ request }) => { const body = (await request.json()) as { labels?: Record<string, number>; message?: string } expect(body.labels).toEqual({ 'Code-Review': -2, Verified: -1 }) return HttpResponse.text(")]}'\n{}") }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = voteCommand('12345', { codeReview: -2, verified: -1, }).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('Code-Review: -2') expect(output).toContain('Verified: -1') }) it('should vote with message', async () => { server.use( http.post('*/a/changes/12345/revisions/current/review', async ({ request }) => { const body = (await request.json()) as { labels?: Record<string, number>; message?: string } expect(body.labels).toEqual({ 'Code-Review': 2 }) expect(body.message).toBe('Looks good to me!') return HttpResponse.text(")]}'\n{}") }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = voteCommand('12345', { codeReview: 2, message: 'Looks good to me!', }).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('Code-Review: +2') expect(output).toContain('Message: Looks good to me!') }) it('should vote with custom labels', async () => { server.use( http.post('*/a/changes/12345/revisions/current/review', async ({ request }) => { const body = (await request.json()) as { labels?: Record<string, number>; message?: string } expect(body.labels).toEqual({ 'Code-Review': 2, 'Custom-Label': 1 }) return HttpResponse.text(")]}'\n{}") }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = voteCommand('12345', { codeReview: 2, label: ['Custom-Label', '1'], }).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('Code-Review: +2') expect(output).toContain('Custom-Label: +1') }) it('should vote with multiple custom labels', async () => { server.use( http.post('*/a/changes/12345/revisions/current/review', async ({ request }) => { const body = (await request.json()) as { labels?: Record<string, number>; message?: string } expect(body.labels).toEqual({ 'Label-A': 1, 'Label-B': -1 }) return HttpResponse.text(")]}'\n{}") }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = voteCommand('12345', { label: ['Label-A', '1', 'Label-B', '-1'], }).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('Label-A: +1') expect(output).toContain('Label-B: -1') }) it('should output XML format when --xml flag is used', async () => { server.use( http.post('*/a/changes/12345/revisions/current/review', async ({ request }) => { const body = (await request.json()) as { labels?: Record<string, number>; message?: string } expect(body.labels).toEqual({ 'Code-Review': 2, Verified: 1 }) expect(body.message).toBe('LGTM') return HttpResponse.text(")]}'\n{}") }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = voteCommand('12345', { xml: true, codeReview: 2, verified: 1, message: 'LGTM', }).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('<vote_result>') expect(output).toContain('<status>success</status>') expect(output).toContain('<change_id>12345</change_id>') expect(output).toContain('<label name="Code-Review">2</label>') expect(output).toContain('<label name="Verified">1</label>') expect(output).toContain('<message><![CDATA[LGTM]]></message>') expect(output).toContain('</vote_result>') }) it('should output XML format without message when no message provided', async () => { server.use( http.post('*/a/changes/12345/revisions/current/review', async () => { return HttpResponse.text(")]}'\n{}") }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = voteCommand('12345', { xml: true, codeReview: 1, }).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('<vote_result>') expect(output).toContain('<status>success</status>') expect(output).not.toContain('<message>') }) it('should show error when change ID is not provided', async () => { const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = voteCommand(undefined, { codeReview: 2 }).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('Change ID is required') expect(errorOutput).toContain('Usage: ger vote <change-id>') }) it('should show error when no labels are provided', async () => { const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = voteCommand('12345', {}).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('At least one label is required') }) it('should handle vote API failure', async () => { server.use( http.post('*/a/changes/12345/revisions/current/review', () => { return HttpResponse.text('Forbidden', { status: 403 }) }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = voteCommand('12345', { codeReview: 2, }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer)) // Should throw/fail await expect(Effect.runPromise(program)).rejects.toThrow() }) it('should handle not found errors gracefully', async () => { server.use( http.post('*/a/changes/99999/revisions/current/review', () => { return HttpResponse.text('Change not found', { status: 404 }) }), ) const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = voteCommand('99999', { codeReview: 2, }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer)) // Should fail when change is not found await expect(Effect.runPromise(program)).rejects.toThrow() }) it('should reject invalid custom label value', async () => { const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = voteCommand('12345', { label: ['Custom-Label', 'not-a-number'], }).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('Invalid label value') expect(errorOutput).toContain('Label values must be integers') }) it('should reject odd number of label arguments', async () => { const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService()) const program = voteCommand('12345', { label: ['Custom-Label', '1', 'Another-Label'], }).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('Invalid label format') expect(errorOutput).toContain('name-value pairs') }) })