/**
* 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'
);
});
});
});