Skip to main content
Glama
test-problem-reporter.test.ts18.2 kB
/** * Tests for TeamCity Test and Problem Reporter */ import { error as logError } from '@/utils'; import { type BuildProblem, type CategorizedProblems, TestProblemReporter, } from '../../../src/teamcity/test-problem-reporter'; import { type MockBuildApi, type MockProblemOccurrenceApi, type MockTeamCityClient, type MockTestOccurrenceApi, createMockTeamCityClient, } from '../../test-utils/mock-teamcity-client'; jest.mock('@/utils', () => ({ error: jest.fn(), })); describe('TestProblemReporter', () => { let reporter: TestProblemReporter; let mockClient: MockTeamCityClient; let http: jest.Mocked<ReturnType<MockTeamCityClient['getAxios']>>; let buildsApi: MockBuildApi; let testsApi: MockTestOccurrenceApi; let problemOccurrencesApi: MockProblemOccurrenceApi; const BASE_URL = 'http://localhost:8111'; beforeEach(() => { jest.mocked(logError).mockReset(); mockClient = createMockTeamCityClient(); http = mockClient.http as jest.Mocked<ReturnType<MockTeamCityClient['getAxios']>>; http.get.mockReset(); buildsApi = mockClient.mockModules.builds; testsApi = mockClient.mockModules.tests; problemOccurrencesApi = mockClient.mockModules.problemOccurrences; buildsApi.getBuild.mockImplementation((locator: string) => http.get(`/app/rest/builds/${locator}`) ); buildsApi.getAllBuilds.mockImplementation((locator?: string) => locator ? http.get(`/app/rest/builds?locator=${locator}`) : http.get('/app/rest/builds') ); testsApi.getAllTestOccurrences.mockImplementation((locator?: string) => locator ? http.get(`/app/rest/testOccurrences?locator=${locator}`) : http.get('/app/rest/testOccurrences') ); problemOccurrencesApi.getAllBuildProblemOccurrences.mockImplementation((locator?: string) => locator ? http.get(`/app/rest/problemOccurrences?locator=${locator}`) : http.get('/app/rest/problemOccurrences') ); mockClient.request.mockImplementation(async (fn) => fn({ axios: http, baseUrl: BASE_URL })); mockClient.getApiConfig.mockReturnValue({ baseUrl: BASE_URL, token: 'test-token', timeout: undefined, }); mockClient.getConfig.mockReturnValue({ connection: { baseUrl: BASE_URL, token: 'test-token', timeout: undefined, }, }); reporter = new TestProblemReporter(mockClient); }); describe('getTestStatistics', () => { it('should extract test statistics from build response', async () => { const buildId = '12345'; http.get.mockResolvedValue({ data: { id: buildId, testOccurrences: { count: 150, passed: 145, failed: 3, ignored: 2, muted: 0, newFailed: 1, }, }, }); const stats = await reporter.getTestStatistics(buildId); expect(stats).toEqual({ totalTests: 150, passedTests: 145, failedTests: 3, ignoredTests: 2, mutedTests: 0, newFailedTests: 1, successRate: 96.67, }); }); it('should handle builds without test data', async () => { const buildId = '12346'; http.get.mockResolvedValue({ data: { id: buildId, // No testOccurrences property }, }); const stats = await reporter.getTestStatistics(buildId); expect(stats).toEqual({ totalTests: 0, passedTests: 0, failedTests: 0, ignoredTests: 0, mutedTests: 0, newFailedTests: 0, successRate: 100, }); }); it('should calculate correct success rate', async () => { const buildId = '12347'; http.get.mockResolvedValue({ data: { id: buildId, testOccurrences: { count: 100, passed: 75, failed: 20, ignored: 5, }, }, }); const stats = await reporter.getTestStatistics(buildId); expect(stats.successRate).toBe(75); // 75 passed out of 100 total }); it('throws when TeamCity returns malformed statistics payload', async () => { http.get.mockResolvedValue({ data: 'oops' }); await expect(reporter.getTestStatistics('99999')).rejects.toThrow('test statistics'); }); }); describe('getFailedTests', () => { it('should retrieve failed test details', async () => { const buildId = '12348'; http.get.mockResolvedValue({ data: { testOccurrence: [ { id: 'test1', name: 'testLogin', status: 'FAILURE', duration: 1500, details: 'AssertionError: Expected true but got false', test: { id: 'LoginTest.testLogin', name: 'testLogin', className: 'LoginTest', }, }, { id: 'test2', name: 'testLogout', status: 'FAILURE', duration: 2000, details: 'NullPointerException at line 45', test: { id: 'LoginTest.testLogout', name: 'testLogout', className: 'LoginTest', }, }, ], }, }); const failedTests = await reporter.getFailedTests(buildId); expect(failedTests).toHaveLength(2); expect(failedTests[0]).toEqual({ id: 'test1', name: 'testLogin', className: 'LoginTest', status: 'FAILURE', duration: 1500, details: 'AssertionError: Expected true but got false', }); }); it('should handle no failed tests', async () => { const buildId = '12349'; http.get.mockResolvedValue({ data: { testOccurrence: [], }, }); const failedTests = await reporter.getFailedTests(buildId); expect(failedTests).toEqual([]); }); it('logs and returns empty array when failed test payload is malformed', async () => { const buildId = '12351'; http.get.mockResolvedValue({ data: { testOccurrence: 'oops', }, }); const failedTests = await reporter.getFailedTests(buildId); expect(failedTests).toEqual([]); expect(logError).toHaveBeenCalledWith('Failed to get failed tests', expect.any(Error), { buildId, }); }); it('should filter only failed tests', async () => { const buildId = '12350'; http.get.mockResolvedValue({ data: { testOccurrence: [ { id: 'test1', name: 'test1', status: 'SUCCESS', }, { id: 'test2', name: 'test2', status: 'FAILURE', details: 'Test failed', }, { id: 'test3', name: 'test3', status: 'IGNORED', }, ], }, }); const failedTests = await reporter.getFailedTests(buildId); expect(failedTests).toHaveLength(1); expect(failedTests[0]?.name).toBe('test2'); }); }); describe('getBuildProblems', () => { it('should retrieve build problems', async () => { const buildId = '12351'; http.get.mockResolvedValue({ data: { problemOccurrence: [ { id: 'problem1', type: 'TC_COMPILATION_ERROR', identity: 'compilation_error_Main.java', details: 'Cannot find symbol: variable x', additionalData: { sourceFile: 'Main.java', line: '42', }, }, { id: 'problem2', type: 'TC_EXIT_CODE', identity: 'exit_code_1', details: 'Process exited with code 1', }, ], }, }); const problems = await reporter.getBuildProblems(buildId); expect(problems).toHaveLength(2); expect((problems as BuildProblem[])[0]).toEqual({ id: 'problem1', type: 'TC_COMPILATION_ERROR', identity: 'compilation_error_Main.java', details: 'Cannot find symbol: variable x', additionalData: { sourceFile: 'Main.java', line: '42', }, }); }); it('should handle no problems', async () => { const buildId = '12352'; http.get.mockResolvedValue({ data: { problemOccurrence: [], }, }); const problems = await reporter.getBuildProblems(buildId); expect(problems).toEqual([]); }); it('logs and returns empty array when problem payload is malformed', async () => { const buildId = '12355'; http.get.mockResolvedValue({ data: { problemOccurrence: 'oops', }, }); const problems = await reporter.getBuildProblems(buildId); expect(problems).toEqual([]); expect(logError).toHaveBeenCalledWith('Failed to get build problems', expect.any(Error), { buildId, }); }); it('should categorize problems by type', async () => { const buildId = '12353'; http.get.mockResolvedValue({ data: { problemOccurrence: [ { id: 'p1', type: 'TC_COMPILATION_ERROR', identity: 'compilation_error_1', details: 'Compilation failed', }, { id: 'p2', type: 'TC_COMPILATION_ERROR', identity: 'compilation_error_2', details: 'Another compilation error', }, { id: 'p3', type: 'TC_FAILED_TESTS', identity: 'failed_tests', details: 'Tests failed', }, { id: 'p4', type: 'TC_EXIT_CODE', identity: 'exit_code', details: 'Bad exit code', }, ], }, }); const problems = await reporter.getBuildProblems(buildId, true); const categorized = problems as CategorizedProblems; expect(categorized.categorized['TC_COMPILATION_ERROR']).toHaveLength(2); expect(categorized.categorized['TC_FAILED_TESTS']).toHaveLength(1); expect(categorized.categorized['TC_EXIT_CODE']).toHaveLength(1); }); }); describe('getTestAndProblemSummary', () => { it('should provide complete test and problem summary', async () => { const buildId = '12354'; // Mock test statistics http.get.mockImplementation((path: string) => { if (path.includes(`/testOccurrences?locator=build:(id:${buildId}),status:FAILURE`)) { return Promise.resolve({ data: { testOccurrence: [ { id: 'test1', name: 'failedTest1', status: 'FAILURE', details: 'Test failure details', test: { className: 'TestClass' }, }, ], }, }); } if (path.includes('/problemOccurrences')) { return Promise.resolve({ data: { problemOccurrence: [ { id: 'problem1', type: 'TC_COMPILATION_ERROR', identity: 'comp_error', details: 'Compilation error', }, ], }, }); } if (path.includes(`/builds/id:${buildId}`)) { return Promise.resolve({ data: { id: buildId, testOccurrences: { count: 100, passed: 95, failed: 5, }, }, }); } return Promise.resolve({ data: {} }); }); const summary = await reporter.getTestAndProblemSummary(buildId); expect(summary).toMatchObject({ statistics: { totalTests: 100, passedTests: 95, failedTests: 5, successRate: 95, }, failedTests: expect.arrayContaining([ expect.objectContaining({ name: 'failedTest1', }), ]), problems: expect.arrayContaining([ expect.objectContaining({ type: 'TC_COMPILATION_ERROR', }), ]), hasIssues: true, }); }); it('should handle optional inclusion flags', async () => { const buildId = '12355'; http.get.mockResolvedValue({ data: { id: buildId, testOccurrences: { count: 50, passed: 50, failed: 0, }, }, }); const summary = await reporter.getTestAndProblemSummary(buildId, { includeFailedTests: false, includeProblems: false, }); expect(summary.statistics).toBeDefined(); expect(summary.failedTests).toBeUndefined(); expect(summary.problems).toBeUndefined(); expect(summary.hasIssues).toBe(false); }); it('should detect builds with issues', async () => { const buildId = '12356'; // Build with no test failures but problems http.get.mockImplementation((path: string) => { if (path.includes('/problemOccurrences')) { return Promise.resolve({ data: { problemOccurrence: [ { id: 'problem2', type: 'TC_EXIT_CODE', identity: 'exit_code', details: 'Non-zero exit', }, ], }, }); } return Promise.resolve({ data: { testOccurrence: [] } }); }); const summary = await reporter.getTestAndProblemSummary(buildId); expect(summary.hasIssues).toBe(true); // Has problems even with all tests passing }); }); describe('formatFailureReason', () => { it('should format comprehensive failure reason', async () => { const buildId = '12357'; http.get.mockImplementation((path: string) => { if (path.includes(`/testOccurrences?locator=build:(id:${buildId}),status:FAILURE`)) { return Promise.resolve({ data: { testOccurrence: [ { id: 'test1', name: 'test1', status: 'FAILURE' }, { id: 'test2', name: 'test2', status: 'FAILURE' }, { id: 'test3', name: 'test3', status: 'FAILURE' }, { id: 'test4', name: 'test4', status: 'FAILURE' }, { id: 'test5', name: 'test5', status: 'FAILURE' }, ], }, }); } if (path.includes('/problemOccurrences')) { return Promise.resolve({ data: { problemOccurrence: [ { id: 'problem3', type: 'TC_COMPILATION_ERROR', identity: 'compilation', details: 'Compilation failed', }, ], }, }); } if (path.includes(`/builds/id:${buildId}`)) { return Promise.resolve({ data: { id: buildId, testOccurrences: { count: 100, passed: 95, failed: 5, }, }, }); } return Promise.resolve({ data: {} }); }); const stats = await reporter.getTestStatistics(buildId); const failed = await reporter.getFailedTests(buildId, 10); const problemsResult = (await reporter.getBuildProblems(buildId)) as BuildProblem[]; expect(stats.failedTests).toBeGreaterThan(0); expect(failed.length).toBeGreaterThan(0); expect(problemsResult.length).toBeGreaterThan(0); const reason = await reporter.formatFailureReason(buildId); expect(reason).toContain('5 test(s) failed'); expect(reason).toContain('1 build problem(s)'); expect(reason).toContain('Failed tests: test1, test2, test3'); expect(reason).toContain('Build problems: Compilation failed'); }); it('should truncate long lists', async () => { const buildId = '12358'; const tests = Array.from({ length: 10 }, (_, i) => ({ id: `test${i + 1}`, name: `test${i + 1}`, status: 'FAILURE', })); http.get.mockImplementation((path: string) => { if (path.includes(`/testOccurrences?locator=build:(id:${buildId}),status:FAILURE`)) { return Promise.resolve({ data: { testOccurrence: tests }, }); } if (path.includes('/problemOccurrences')) { return Promise.resolve({ data: { problemOccurrence: [] }, }); } if (path.includes(`/builds/id:${buildId}`)) { return Promise.resolve({ data: { id: buildId, testOccurrences: { count: 100, failed: 10, passed: 90, }, }, }); } return Promise.resolve({ data: {} }); }); const reason = await reporter.formatFailureReason(buildId); expect(reason).toContain('test1, test2, test3, test4, test5... and 5 more'); }); }); describe('error handling', () => { it('should handle API errors gracefully', async () => { const buildId = '12359'; http.get.mockRejectedValue(new Error('API Error')); await expect(reporter.getTestStatistics(buildId)).rejects.toThrow('API Error'); }); it('should handle malformed responses', async () => { const buildId = '12360'; http.get.mockResolvedValue({ data: { // Missing expected structure unexpected: 'data', }, }); const failedTests = await reporter.getFailedTests(buildId); expect(failedTests).toEqual([]); }); }); });

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