@aaronshaf/ger
Version:
Gerrit CLI and SDK - A modern CLI tool and TypeScript SDK for Gerrit Code Review, built with Effect-TS
335 lines (280 loc) • 10.7 kB
text/typescript
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 { groupsCommand } from '@/cli/commands/groups'
import { ConfigService } from '@/services/config'
import { createMockConfigService } from './helpers/config-mock'
const mockGroups = {
administrators: {
id: 'administrators',
name: 'Administrators',
description: 'Site administrators with full access',
owner: 'Administrators',
owner_id: 'administrators',
group_id: 1,
options: { visible_to_all: true },
created_on: '2024-01-01 10:00:00.000000000',
},
'project-reviewers': {
id: 'project-reviewers',
name: 'Project Reviewers',
description: 'Code reviewers for the project',
owner: 'Project Owners',
owner_id: 'project-owners',
group_id: 2,
options: { visible_to_all: false },
},
developers: {
id: 'developers',
name: 'Developers',
description: 'Development team members',
owner: 'Administrators',
owner_id: 'administrators',
group_id: 3,
},
}
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('groups 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 list all groups', async () => {
server.use(
http.get('*/a/groups/', () => {
return HttpResponse.text(`)]}'\n${JSON.stringify(mockGroups)}`)
}),
)
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
const program = groupsCommand({}).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('Available groups (3)')
expect(output).toContain('Administrators')
expect(output).toContain('Site administrators with full access')
expect(output).toContain('Project Reviewers')
expect(output).toContain('Developers')
})
it('should list groups with pattern filter', async () => {
server.use(
http.get('*/a/groups/', ({ request }) => {
const url = new URL(request.url)
const pattern = url.searchParams.get('r')
expect(pattern).toBe('project-.*')
const filtered = {
'project-reviewers': mockGroups['project-reviewers'],
}
return HttpResponse.text(`)]}'\n${JSON.stringify(filtered)}`)
}),
)
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
const program = groupsCommand({ pattern: 'project-.*' }).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('Project Reviewers')
expect(output).not.toContain('Administrators')
expect(output).not.toContain('Developers')
})
it('should list owned groups only', async () => {
server.use(
http.get('*/a/groups/', ({ request }) => {
const url = new URL(request.url)
expect(url.searchParams.has('owned')).toBe(true)
const filtered = {
administrators: mockGroups['administrators'],
}
return HttpResponse.text(`)]}'\n${JSON.stringify(filtered)}`)
}),
)
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
const program = groupsCommand({ owned: 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('Administrators')
})
it('should list groups for specific project', async () => {
server.use(
http.get('*/a/groups/', ({ request }) => {
const url = new URL(request.url)
const project = url.searchParams.get('p')
expect(project).toBe('my-project')
return HttpResponse.text(`)]}'\n${JSON.stringify(mockGroups)}`)
}),
)
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
const program = groupsCommand({ project: 'my-project' }).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('Available groups')
})
it('should output XML format when --xml flag is used', async () => {
server.use(
http.get('*/a/groups/', () => {
return HttpResponse.text(`)]}'\n${JSON.stringify(mockGroups)}`)
}),
)
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
const program = groupsCommand({ 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('<groups_result>')
expect(output).toContain('<status>success</status>')
expect(output).toContain('<count>3</count>')
expect(output).toContain('<groups>')
expect(output).toContain('<group>')
expect(output).toContain('<id><![CDATA[administrators]]></id>')
expect(output).toContain('<name><![CDATA[Administrators]]></name>')
expect(output).toContain(
'<description><![CDATA[Site administrators with full access]]></description>',
)
expect(output).toContain('<owner><![CDATA[Administrators]]></owner>')
expect(output).toContain('<visible_to_all>true</visible_to_all>')
expect(output).toContain('</group>')
expect(output).toContain('</groups>')
expect(output).toContain('</groups_result>')
})
it('should handle empty results', async () => {
server.use(
http.get('*/a/groups/', () => {
return HttpResponse.text(`)]}'\n${JSON.stringify({})}`)
}),
)
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
const program = groupsCommand({}).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 groups found')
})
it('should handle empty results with XML format', async () => {
server.use(
http.get('*/a/groups/', () => {
return HttpResponse.text(`)]}'\n${JSON.stringify({})}`)
}),
)
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
const program = groupsCommand({ 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('<groups_result>')
expect(output).toContain('<status>success</status>')
expect(output).toContain('<count>0</count>')
expect(output).toContain('<groups />')
})
it('should handle network errors gracefully', async () => {
server.use(
http.get('*/a/groups/', () => {
return HttpResponse.error()
}),
)
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
const program = groupsCommand({}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
await expect(Effect.runPromise(program)).rejects.toThrow()
})
it('should handle API errors (403)', async () => {
server.use(
http.get('*/a/groups/', () => {
return HttpResponse.text('Forbidden', { status: 403 })
}),
)
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
const program = groupsCommand({}).pipe(
Effect.provide(GerritApiServiceLive),
Effect.provide(mockConfigLayer),
)
await expect(Effect.runPromise(program)).rejects.toThrow()
})
it('should handle groups without optional fields', async () => {
const minimalGroups = {
'minimal-group': {
id: 'minimal-group',
name: 'Minimal Group',
},
}
server.use(
http.get('*/a/groups/', () => {
return HttpResponse.text(`)]}'\n${JSON.stringify(minimalGroups)}`)
}),
)
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
const program = groupsCommand({ 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('<id><![CDATA[minimal-group]]></id>')
expect(output).toContain('<name><![CDATA[Minimal Group]]></name>')
expect(output).not.toContain('<description>')
expect(output).not.toContain('<owner>')
})
it('should respect limit parameter', async () => {
server.use(
http.get('*/a/groups/', ({ request }) => {
const url = new URL(request.url)
const limit = url.searchParams.get('n')
expect(limit).toBe('10')
return HttpResponse.text(`)]}'\n${JSON.stringify(mockGroups)}`)
}),
)
const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
const program = groupsCommand({ limit: '10' }).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('Available groups')
})
})