Skip to main content
Glama
build-config-navigator-more.test.ts8.51 kB
import { BuildConfigNavigator } from '@/teamcity/build-config-navigator'; import { type MockTeamCityClient, createMockTeamCityClient, } from '../../test-utils/mock-teamcity-client'; describe('BuildConfigNavigator (more branches)', () => { let navigator: BuildConfigNavigator; let mockClient: MockTeamCityClient; beforeEach(() => { mockClient = createMockTeamCityClient(); mockClient.resetAllMocks(); navigator = new BuildConfigNavigator(mockClient); // clear cache type PrivateNav = { cache: Map<string, unknown> }; (navigator as unknown as PrivateNav).cache.clear(); }); it('extracts parameters when includeParameters is true', async () => { mockClient.buildTypes.getAllBuildTypes.mockResolvedValue({ data: { count: 1, buildType: [ { id: 'B1', name: 'With Params', projectId: 'P', projectName: 'Proj', parameters: { property: [ { name: 'env.FOO', value: 'bar' }, { name: 'system.debug', value: 'true' }, ], }, }, ], }, }); const res = await navigator.listBuildConfigs({ includeParameters: true }); expect(res.buildConfigs[0]?.parameters).toEqual({ 'env.FOO': 'bar', 'system.debug': 'true' }); }); it('extracts project hierarchy when includeProjectHierarchy is true', async () => { mockClient.buildTypes.getAllBuildTypes.mockResolvedValue({ data: { count: 1, buildType: [{ id: 'B1', name: 'With Hier', projectId: 'P2', projectName: 'Project 2' }], }, }); // parent chain: _Root -> P1 -> P2 mockClient.projects.getProject.mockImplementation(async (id: string) => { if (id === 'P2') { return { data: { id: 'P2', name: 'Project 2', parentProject: { id: 'P1', name: 'Project 1', parentProject: { id: '_Root', name: 'Root' }, }, }, }; } return { data: { id, name: id } }; }); const res = await navigator.listBuildConfigs({ includeProjectHierarchy: true }); expect(res.buildConfigs[0]?.projectHierarchy).toEqual([ { id: '_Root', name: 'Root' }, { id: 'P1', name: 'Project 1' }, { id: 'P2', name: 'Project 2' }, ]); }); it('applies statusFilter on lastBuildStatus, paused, hasRecentActivity and activeSince', async () => { const buildType = ( overrides: Partial<{ lastBuildStatus: string; paused: boolean; lastBuildDate: string }> ) => ({ id: 'B', name: 'N', projectId: 'P', projectName: 'Proj', ...overrides, }); mockClient.buildTypes.getAllBuildTypes.mockResolvedValue({ data: { count: 4, buildType: [ buildType({ lastBuildStatus: 'SUCCESS', paused: false, lastBuildDate: '2025-01-02T00:00:00Z', }), buildType({ lastBuildStatus: 'FAILURE', paused: true, lastBuildDate: '2025-01-01T00:00:00Z', }), buildType({ lastBuildStatus: 'SUCCESS', paused: true }), // no lastBuildDate buildType({ lastBuildStatus: 'SUCCESS', paused: false, lastBuildDate: '2024-12-31T00:00:00Z', }), ], }, }); // Filter: only SUCCESS, not paused, has recent activity since Jan 1, 2025 const res = await navigator.listBuildConfigs({ statusFilter: { lastBuildStatus: 'SUCCESS', paused: false, hasRecentActivity: true, activeSince: new Date('2025-01-01T00:00:00Z'), }, }); expect(res.buildConfigs).toHaveLength(1); expect(res.buildConfigs[0]?.lastBuildDate).toBe('2025-01-02T00:00:00Z'); }); it('filters by vcsRootFilter url/branch/vcsName and handles missing roots', async () => { const make = (url?: string, branch?: string, vcsName: string = 'git') => ({ id: 'B', name: 'N', projectId: 'P', projectName: 'Proj', 'vcs-root-entries': { 'vcs-root-entry': [ { 'vcs-root': { id: 'R', name: 'Repo', vcsName, properties: { property: [ { name: 'url', value: url }, { name: 'branch', value: branch }, ], }, }, }, ], }, }); // Two configs: only the second matches url and branch mockClient.buildTypes.getAllBuildTypes.mockResolvedValue({ data: { count: 2, buildType: [make('https://a', 'main'), make('https://match', 'dev')], }, }); const res = await navigator.listBuildConfigs({ includeVcsRoots: true, vcsRootFilter: { url: 'match', branch: 'dev', vcsName: 'git' }, }); expect(res.buildConfigs).toHaveLength(1); // No roots present → filter cannot apply; item passes through mockClient.buildTypes.getAllBuildTypes.mockResolvedValue({ data: { count: 1, buildType: [{ id: 'X', name: 'X' }] }, }); const res2 = await navigator.listBuildConfigs({ includeVcsRoots: true, vcsRootFilter: { url: 'x' }, }); expect(res2.buildConfigs).toHaveLength(1); }); it('sorts by project then name, and lastModified combinations', async () => { const b = (name: string, projectName: string, last?: string) => ({ id: name, name, projectId: projectName, projectName, lastBuildDate: last, }); mockClient.buildTypes.getAllBuildTypes.mockResolvedValue({ data: { count: 4, buildType: [b('B', 'P1'), b('A', 'P1'), b('C', 'P2', '2025-01-01'), b('D', 'P3')], }, }); // Sort by project with tiebreaker on name const res = await navigator.listBuildConfigs({ sortBy: 'project', sortOrder: 'asc' }); expect(res.buildConfigs.map((x) => x.name)).toEqual(['A', 'B', 'C', 'D']); // Sort by lastModified with missing values: current impl places entries without dates first for desc const res2 = await navigator.listBuildConfigs({ sortBy: 'lastModified', sortOrder: 'desc' }); expect(['A', 'B', 'D']).toContain(res2.buildConfigs[0]?.name); expect(res2.buildConfigs.map((x) => x.name)).toContain('C'); }); it('calculateHasMore returns false if limit missing or count below limit', async () => { mockClient.buildTypes.getAllBuildTypes.mockResolvedValue({ data: { count: 3, buildType: [{ id: '1', name: '1' }] }, }); const res = await navigator.listBuildConfigs(); expect(res.hasMore).toBe(false); const res2 = await navigator.listBuildConfigs({ pagination: { limit: 10, offset: 0 } }); expect(res2.hasMore).toBe(false); }); it('transformError covers various cases (401/403/404/timeout/5xx/generic)', async () => { const err401 = { response: { status: 401 } }; mockClient.buildTypes.getAllBuildTypes.mockRejectedValueOnce(err401); await expect(navigator.listBuildConfigs()).rejects.toThrow(/Authentication failed/); const err403 = { response: { status: 403 } }; mockClient.buildTypes.getAllBuildTypes.mockRejectedValueOnce(err403); await expect(navigator.listBuildConfigs()).rejects.toThrow(/Permission denied/); const err404 = { response: { status: 404 } }; mockClient.buildTypes.getAllBuildTypes.mockRejectedValueOnce(err404); await expect(navigator.listBuildConfigs({ projectId: 'P' })).rejects.toThrow( /Project P not found/ ); const timeoutByName = { name: 'ECONNABORTED' }; mockClient.buildTypes.getAllBuildTypes.mockRejectedValueOnce(timeoutByName); await expect(navigator.listBuildConfigs()).rejects.toThrow(/Request timed out/); const timeoutByMessage = new Error('socket timeout'); mockClient.buildTypes.getAllBuildTypes.mockRejectedValueOnce(timeoutByMessage); await expect(navigator.listBuildConfigs()).rejects.toThrow(/Request timed out/); const serverErr = { response: { status: 500 }, message: 'Internal' }; mockClient.buildTypes.getAllBuildTypes.mockRejectedValueOnce(serverErr); await expect(navigator.listBuildConfigs()).rejects.toThrow(/TeamCity API error/); const genericErr = new Error('oops'); mockClient.buildTypes.getAllBuildTypes.mockRejectedValueOnce(genericErr); await expect(navigator.listBuildConfigs()).rejects.toThrow(/oops/); }); });

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