Skip to main content
Glama
build-configuration-manager.test.ts20.4 kB
/** * Tests for BuildConfigurationManager */ import { BuildConfigurationManager } from '@/teamcity/build-configuration-manager'; import { type MockTeamCityClient, createMockTeamCityClient, } from '../../test-utils/mock-teamcity-client'; // Mock dependencies jest.mock('@/config', () => ({ getTeamCityUrl: jest.fn(() => 'https://teamcity.example.com'), getTeamCityToken: jest.fn(() => 'test-token'), getMCPMode: jest.fn(() => 'full'), })); describe('BuildConfigurationManager', () => { let manager: BuildConfigurationManager; let mockClient: MockTeamCityClient; beforeEach(() => { jest.clearAllMocks(); mockClient = createMockTeamCityClient(); manager = new BuildConfigurationManager(mockClient); }); describe('Build Step Transformations', () => { describe('Script Step', () => { it('should transform script step correctly', () => { const steps = [ { type: 'script', name: 'Run Tests', script: 'npm test', }, ]; const result = manager.transformBuildSteps(steps); expect(result).toHaveLength(1); expect(result[0]).toEqual({ id: 'RUNNER_1', name: 'Run Tests', type: 'simpleRunner', properties: { property: [ { name: 'script.content', value: 'npm test' }, { name: 'teamcity.step.mode', value: 'default' }, { name: 'use.custom.script', value: 'true' }, ], }, }); }); it('should handle empty script', () => { const steps = [ { type: 'script', name: 'Empty Script', script: undefined, }, ]; const result = manager.transformBuildSteps(steps); expect(result[0]?.properties?.property?.[0]?.value).toBe(''); }); }); describe('PowerShell Step', () => { it('should transform PowerShell step correctly', () => { const steps = [ { type: 'powershell', name: 'PS Script', script: 'Write-Host "Hello"', }, ]; const result = manager.transformBuildSteps(steps); expect(result).toHaveLength(1); expect(result[0]).toEqual({ id: 'RUNNER_1', name: 'PS Script', type: 'jetbrains.powershell', properties: { property: [ { name: 'script.content', value: 'Write-Host "Hello"' }, { name: 'teamcity.powershell.bitness', value: 'x64' }, { name: 'teamcity.powershell.edition', value: 'Desktop' }, ], }, }); }); }); describe('Maven Step', () => { it('should transform Maven step correctly', () => { const steps = [ { type: 'maven', name: 'Maven Build', goals: 'clean package', }, ]; const result = manager.transformBuildSteps(steps); expect(result).toHaveLength(1); expect(result[0]).toEqual({ id: 'RUNNER_1', name: 'Maven Build', type: 'Maven2', properties: { property: [ { name: 'goals', value: 'clean package' }, { name: 'teamcity.step.mode', value: 'default' }, { name: 'pomLocation', value: 'pom.xml' }, ], }, }); }); it('should use default goals if not specified', () => { const steps = [ { type: 'maven', name: 'Maven Default', goals: undefined, }, ]; const result = manager.transformBuildSteps(steps); expect(result[0]?.properties?.property?.[0]?.value).toBe('clean install'); }); }); describe('Gradle Step', () => { it('should transform Gradle step correctly', () => { const steps = [ { type: 'gradle', name: 'Gradle Build', tasks: 'build test', }, ]; const result = manager.transformBuildSteps(steps); expect(result).toHaveLength(1); expect(result[0]).toEqual({ id: 'RUNNER_1', name: 'Gradle Build', type: 'gradle-runner', properties: { property: [ { name: 'tasks', value: 'build test' }, { name: 'gradle.wrapper.useWrapper', value: 'true' }, { name: 'teamcity.step.mode', value: 'default' }, ], }, }); }); it('should use default tasks if not specified', () => { const steps = [ { type: 'gradle', name: 'Gradle Default', tasks: undefined, }, ]; const result = manager.transformBuildSteps(steps); expect(result[0]?.properties?.property?.[0]?.value).toBe('build'); }); }); describe('npm Step', () => { it('should transform npm step correctly', () => { const steps = [ { type: 'npm', name: 'NPM Install', script: 'install', }, ]; const result = manager.transformBuildSteps(steps); expect(result).toHaveLength(1); expect(result[0]).toEqual({ id: 'RUNNER_1', name: 'NPM Install', type: 'nodejs-runner', properties: { property: [ { name: 'npm_commands', value: 'install' }, { name: 'teamcity.step.mode', value: 'default' }, ], }, }); }); it('should use default script if not specified', () => { const steps = [ { type: 'npm', name: 'NPM Default', script: undefined, }, ]; const result = manager.transformBuildSteps(steps); expect(result[0]?.properties?.property?.[0]?.value).toBe('install'); }); }); describe('Docker Step', () => { it('should transform Docker step correctly', () => { const steps = [ { type: 'docker', name: 'Docker Build', dockerfile: './docker/Dockerfile', }, ]; const result = manager.transformBuildSteps(steps); expect(result).toHaveLength(1); expect(result[0]).toEqual({ id: 'RUNNER_1', name: 'Docker Build', type: 'DockerBuild', properties: { property: [ { name: 'dockerfile', value: './docker/Dockerfile' }, { name: 'teamcity.step.mode', value: 'default' }, ], }, }); }); it('should use default dockerfile if not specified', () => { const steps = [ { type: 'docker', name: 'Docker Default', dockerfile: undefined, }, ]; const result = manager.transformBuildSteps(steps); expect(result[0]?.properties?.property?.[0]?.value).toBe('Dockerfile'); }); }); describe('Multiple Steps', () => { it('should transform multiple steps with correct IDs', () => { const steps = [ { type: 'script', name: 'Step 1', script: 'echo "1"', }, { type: 'maven', name: 'Step 2', goals: 'test', }, { type: 'docker', name: 'Step 3', dockerfile: 'Dockerfile', }, ]; const result = manager.transformBuildSteps(steps); expect(result).toHaveLength(3); expect(result[0]?.id).toBe('RUNNER_1'); expect(result[1]?.id).toBe('RUNNER_2'); expect(result[2]?.id).toBe('RUNNER_3'); expect(result[0]?.type).toBe('simpleRunner'); expect(result[1]?.type).toBe('Maven2'); expect(result[2]?.type).toBe('DockerBuild'); }); }); describe('Invalid Step Type', () => { it('should throw error for unknown step type', () => { const steps = [ { type: 'unknown', name: 'Invalid Step', }, ]; expect(() => manager.transformBuildSteps(steps)).toThrow( 'Unknown build step type: unknown' ); }); }); }); describe('Trigger Transformations', () => { describe('VCS Trigger', () => { it('should transform VCS trigger correctly', () => { const triggers = [ { type: 'vcs', rules: '+:refs/heads/develop', }, ]; const result = manager.transformTriggers(triggers); expect(result).toHaveLength(1); expect(result[0]).toEqual({ id: 'TRIGGER_1', type: 'vcsTrigger', properties: { property: [ { name: 'branchFilter', value: '+:refs/heads/develop' }, { name: 'quietPeriodMode', value: 'DO_NOT_USE' }, ], }, }); }); it('should use default rules if not specified', () => { const triggers = [ { type: 'vcs', rules: undefined, }, ]; const result = manager.transformTriggers(triggers); expect(result[0]?.properties?.property?.[0]?.value).toBe('+:*'); }); }); describe('Schedule Trigger', () => { it('should transform schedule trigger correctly', () => { const triggers = [ { type: 'schedule', schedule: '0 2 * * *', }, ]; const result = manager.transformTriggers(triggers); expect(result).toHaveLength(1); expect(result[0]).toEqual({ id: 'TRIGGER_1', type: 'schedulingTrigger', properties: { property: [ { name: 'schedulingPolicy', value: 'cron' }, { name: 'cronExpression', value: '0 2 * * *' }, { name: 'triggerBuildWithPendingChangesOnly', value: 'false' }, ], }, }); }); it('should use default schedule if not specified', () => { const triggers = [ { type: 'schedule', schedule: undefined, }, ]; const result = manager.transformTriggers(triggers); expect(result[0]?.properties?.property?.[1]?.value).toBe('0 0 * * *'); }); }); describe('Finish Build Trigger', () => { it('should transform finish-build trigger correctly', () => { const triggers = [ { type: 'finish-build', buildType: 'OtherBuild', branchFilter: '+:feature/*', }, ]; const result = manager.transformTriggers(triggers); expect(result).toHaveLength(1); expect(result[0]).toEqual({ id: 'TRIGGER_1', type: 'buildDependencyTrigger', properties: { property: [ { name: 'dependsOn', value: 'OtherBuild' }, { name: 'afterSuccessfulBuildOnly', value: 'true' }, { name: 'branchFilter', value: '+:feature/*' }, ], }, }); }); it('should use default branch filter if not specified', () => { const triggers = [ { type: 'finish-build', buildType: 'OtherBuild', branchFilter: undefined, }, ]; const result = manager.transformTriggers(triggers); expect(result[0]?.properties?.property?.[2]?.value).toBe('+:*'); }); }); describe('Maven Snapshot Trigger', () => { it('should transform maven-snapshot trigger correctly', () => { const triggers = [ { type: 'maven-snapshot', }, ]; const result = manager.transformTriggers(triggers); expect(result).toHaveLength(1); expect(result[0]).toEqual({ id: 'TRIGGER_1', type: 'mavenSnapshotDependencyTrigger', properties: { property: [{ name: 'skipPollingIfNoChangesInBuildChain', value: 'true' }], }, }); }); }); describe('Multiple Triggers', () => { it('should transform multiple triggers with correct IDs', () => { const triggers = [ { type: 'vcs', rules: '+:refs/heads/*', }, { type: 'schedule', schedule: '0 0 * * *', }, { type: 'finish-build', buildType: 'Build1', }, ]; const result = manager.transformTriggers(triggers); expect(result).toHaveLength(3); expect(result[0]?.id).toBe('TRIGGER_1'); expect(result[1]?.id).toBe('TRIGGER_2'); expect(result[2]?.id).toBe('TRIGGER_3'); expect(result[0]?.type).toBe('vcsTrigger'); expect(result[1]?.type).toBe('schedulingTrigger'); expect(result[2]?.type).toBe('buildDependencyTrigger'); }); }); describe('Invalid Trigger Type', () => { it('should throw error for unknown trigger type', () => { const triggers = [ { type: 'unknown', }, ]; expect(() => manager.transformTriggers(triggers)).toThrow('Unknown trigger type: unknown'); }); }); }); describe('VCS Root Creation', () => { it('should create Git VCS root', async () => { mockClient.vcsRoots.addVcsRoot.mockResolvedValue({ data: { id: 'VcsRoot1' }, }); const result = await manager.createVcsRoot({ projectId: 'Project1', name: 'Git Repository', url: 'https://github.com/test/repo.git', branch: 'main', type: 'git', }); expect(result).toEqual({ id: 'VcsRoot1' }); // Behavior-first: validated by returned result }); it('should handle password authentication', async () => { mockClient.vcsRoots.addVcsRoot.mockResolvedValue({ data: { id: 'VcsRoot1' }, }); await manager.createVcsRoot({ projectId: 'Project1', name: 'Auth Repo', url: 'https://github.com/test/repo.git', type: 'git', authentication: { type: 'password', username: 'user', password: 'pass', }, }); // Behavior-first: avoid internal API payload assertions }); it('should handle token authentication', async () => { mockClient.vcsRoots.addVcsRoot.mockResolvedValue({ data: { id: 'VcsRoot1' }, }); await manager.createVcsRoot({ projectId: 'Project1', name: 'Token Repo', url: 'https://github.com/test/repo.git', type: 'git', authentication: { type: 'token', password: 'github-token', }, }); // Behavior-first: avoid internal API payload assertions }); it('should handle SSH authentication', async () => { mockClient.vcsRoots.addVcsRoot.mockResolvedValue({ data: { id: 'VcsRoot1' }, }); await manager.createVcsRoot({ projectId: 'Project1', name: 'SSH Repo', url: 'git@github.com:test/repo.git', type: 'git', authentication: { type: 'ssh', privateKey: 'ssh-key-content', }, }); // Behavior-first: avoid internal API payload assertions }); }); describe('Build Configuration Creation', () => { it('should create basic configuration', async () => { mockClient.buildTypes.createBuildType.mockResolvedValue({ data: { id: 'Project1_TestBuild', name: 'Test Build', projectId: 'Project1', }, }); const result = await manager.createConfiguration({ projectId: 'Project1', name: 'Test Build', description: 'Test Description', }); expect(result).toEqual({ id: 'Project1_TestBuild', name: 'Test Build', projectId: 'Project1', url: 'https://teamcity.example.com/viewType.html?buildTypeId=Project1_TestBuild', }); // Behavior-first: validated by returned result }); it('should handle template configuration', async () => { mockClient.buildTypes.createBuildType.mockResolvedValue({ data: { id: 'Project1_TemplatedBuild', name: 'Templated Build', templateId: 'Template1', }, }); await manager.createConfiguration({ projectId: 'Project1', name: 'Templated Build', templateId: 'Template1', }); // Behavior-first: validated by returned result }); it('should handle configuration with VCS root', async () => { mockClient.buildTypes.createBuildType.mockResolvedValue({ data: { id: 'Project1_VcsBuild', name: 'VCS Build', }, }); await manager.createConfiguration({ projectId: 'Project1', name: 'VCS Build', vcsRootId: 'VcsRoot1', }); // Behavior-first: validated by returned result }); it('should handle configuration with parameters', async () => { mockClient.buildTypes.createBuildType.mockResolvedValue({ data: { id: 'Project1_ParamBuild', name: 'Param Build', }, }); await manager.createConfiguration({ projectId: 'Project1', name: 'Param Build', parameters: { 'env.NODE_ENV': 'production', 'system.debug': 'true', }, }); // Behavior-first: validated by returned result }); it('should handle duplicate configuration error', async () => { mockClient.buildTypes.createBuildType.mockRejectedValue({ response: { status: 409 }, }); await expect( manager.createConfiguration({ projectId: 'Project1', name: 'Duplicate Build', }) ).rejects.toThrow('Build configuration already exists'); }); it('should handle permission error', async () => { mockClient.buildTypes.createBuildType.mockRejectedValue({ response: { status: 403 }, }); await expect( manager.createConfiguration({ projectId: 'Project1', name: 'No Permission Build', }) ).rejects.toThrow('Permission denied'); }); }); describe('Project Validation', () => { it('should validate existing project', async () => { mockClient.projects.getProject.mockResolvedValue({ data: { id: 'Project1', name: 'Test Project', }, }); const result = await manager.validateProject('Project1'); expect(result).toEqual({ id: 'Project1', name: 'Test Project', }); }); it('should return null for non-existent project', async () => { mockClient.projects.getProject.mockRejectedValue({ response: { status: 404 }, }); const result = await manager.validateProject('NonExistent'); expect(result).toBeNull(); }); it('should throw permission error', async () => { mockClient.projects.getProject.mockRejectedValue({ response: { status: 403 }, }); await expect(manager.validateProject('Forbidden')).rejects.toThrow('Permission denied'); }); }); describe('Build Configuration ID Generation', () => { it('should generate valid IDs', () => { const manager = new BuildConfigurationManager(mockClient); // Access private method through typed private access type PrivateMgr = { generateBuildConfigId: (projectId: string, name: string) => string }; const generateId = (manager as unknown as PrivateMgr).generateBuildConfigId.bind(manager); expect(generateId('Project1', 'Simple Build')).toBe('Project1_Simple_Build'); expect(generateId('Project1', 'Build-With-Dashes')).toBe('Project1_Build_With_Dashes'); expect(generateId('Project1', 'Build@#$Special')).toBe('Project1_Build_Special'); expect(generateId('Project1', '_Leading_Trailing_')).toBe('Project1_Leading_Trailing'); expect(generateId('Project1', 'Multiple___Underscores')).toBe( 'Project1_Multiple_Underscores' ); }); }); });

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