Skip to main content
Glama
build-list-manager.test.ts5.81 kB
/** * Tests for BuildListManager */ import { BuildListManager } from '@/teamcity/build-list-manager'; import type { TeamCityUnifiedClient } from '@/teamcity/types/client'; const BASE_URL = 'https://teamcity.example.com'; type StubClient = { client: TeamCityUnifiedClient; builds: { getMultipleBuilds: jest.Mock; getAllBuilds: jest.Mock; }; http: { get: jest.Mock }; request: jest.Mock; }; const createStubClient = (): StubClient => { const builds = { getMultipleBuilds: jest.fn(), getAllBuilds: jest.fn(), }; const http = { get: jest.fn(), } as { get: jest.Mock }; const request = jest.fn( async (fn: (ctx: { axios: typeof http; baseUrl: string }) => Promise<unknown>) => fn({ axios: http, baseUrl: BASE_URL }) ) as jest.Mock; const client = { modules: { builds } as unknown as TeamCityUnifiedClient['modules'], http: http as unknown as TeamCityUnifiedClient['http'], request: request as unknown as TeamCityUnifiedClient['request'], getConfig: jest.fn(() => ({ connection: { baseUrl: BASE_URL, token: 'token' } })), getApiConfig: jest.fn(() => ({ baseUrl: BASE_URL, token: 'token' })), getAxios: jest.fn(() => http as unknown as TeamCityUnifiedClient['http']), } as TeamCityUnifiedClient; return { client, builds, http, request }; }; describe('BuildListManager', () => { let manager: BuildListManager; let stub: StubClient; const createBuild = (id: number, overrides: Partial<Record<string, unknown>> = {}) => ({ id, buildTypeId: 'MyBuildConfig', number: String(id), status: 'SUCCESS', state: 'finished', webUrl: `${BASE_URL}/viewLog.html?buildId=${id}`, ...overrides, }); beforeEach(() => { stub = createStubClient(); stub.builds.getMultipleBuilds.mockResolvedValue({ data: { count: 0, build: [], }, }); manager = new BuildListManager(stub.client); type PrivateAccess = { cache: Map<string, unknown> }; (manager as unknown as PrivateAccess).cache.clear(); }); describe('Basic Query', () => { it('should fetch builds with no filters', async () => { stub.builds.getMultipleBuilds.mockResolvedValue({ data: { count: 2, build: [createBuild(12345), createBuild(12346)], }, }); const result = await manager.listBuilds({}); expect(result.builds).toHaveLength(2); expect(result.metadata.count).toBe(2); expect(result.metadata.hasMore).toBe(false); }); it('should fetch builds with project filter', async () => { stub.builds.getMultipleBuilds.mockResolvedValue({ data: { count: 1, build: [createBuild(12345)], }, }); await manager.listBuilds({ project: 'MyProject' }); expect(stub.builds.getMultipleBuilds).toHaveBeenCalledWith( expect.stringContaining('project:'), expect.any(String) ); }); }); describe('Pagination', () => { it('should apply default limit', async () => { stub.builds.getMultipleBuilds.mockResolvedValue({ data: { count: 100, build: new Array(100).fill(null).map((_, i) => createBuild(10000 + i)), }, }); const result = await manager.listBuilds({}); expect(result.builds).toHaveLength(100); expect(result.metadata.limit).toBe(100); }); it('should enforce maximum limit', async () => { stub.builds.getMultipleBuilds.mockResolvedValue({ data: { count: 1000, build: [], }, }); await manager.listBuilds({ limit: 5000 }); const locator = stub.builds.getMultipleBuilds.mock.calls[0]?.[0] ?? ''; expect(locator).toContain('count:1000'); }); it('should apply offset when provided', async () => { stub.builds.getMultipleBuilds.mockResolvedValue({ data: { count: 10, build: new Array(10).fill(null).map((_, i) => createBuild(10000 + i)), }, }); await manager.listBuilds({ offset: 20 }); const locator = stub.builds.getMultipleBuilds.mock.calls[0]?.[0] ?? ''; expect(locator).toContain('start:20'); }); }); describe('Total Count', () => { it('requests total count when includeTotalCount is true', async () => { stub.builds.getMultipleBuilds.mockResolvedValue({ data: { count: 2, build: [createBuild(1), createBuild(2)], }, }); stub.builds.getAllBuilds.mockResolvedValueOnce({ data: { count: 42 } }); const result = await manager.listBuilds({ includeTotalCount: true }); expect(stub.builds.getAllBuilds).toHaveBeenCalledWith(undefined, 'count'); expect(result.metadata.totalCount).toBe(42); }); it('returns zero when total count fails', async () => { stub.builds.getMultipleBuilds.mockResolvedValue({ data: { count: 1, build: [createBuild(1)], }, }); stub.builds.getAllBuilds.mockRejectedValueOnce(new Error('count failed')); const result = await manager.listBuilds({ includeTotalCount: true }); expect(result.metadata.totalCount).toBe(0); }); }); describe('Error Handling', () => { it('throws when API returns validation errors', async () => { stub.builds.getMultipleBuilds.mockImplementationOnce(() => { throw new Error('Invalid date format'); }); await expect(manager.listBuilds({ sinceDate: 'bad-date' })).rejects.toThrow( 'Invalid date format' ); }); it('throws when TeamCity omits the build array', async () => { stub.builds.getMultipleBuilds.mockResolvedValue({ data: { count: 1 } }); await expect(manager.listBuilds({})).rejects.toThrow('build array'); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Daghis/teamcity-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server