Skip to main content
Glama
build-trigger-manager.test.ts34.7 kB
/** * Unit tests for BuildTriggerManager */ import { BuildTriggerManager, type DependencyTriggerProperties, type ScheduleTriggerProperties, type VcsTriggerProperties, } from '@/teamcity/build-trigger-manager'; import { BuildConfigurationNotFoundError, CircularDependencyError, TeamCityAPIError, TriggerNotFoundError, ValidationError, } from '@/teamcity/errors'; import { type MockBuildTypeApi, type MockTeamCityClient, createMockTeamCityClient, } from '../../test-utils/mock-teamcity-client'; describe('BuildTriggerManager', () => { let manager: BuildTriggerManager; let mockClient: MockTeamCityClient; let http: jest.Mocked<ReturnType<MockTeamCityClient['getAxios']>>; let buildTypesApi: MockBuildTypeApi; beforeEach(() => { mockClient = createMockTeamCityClient(); http = mockClient.http as jest.Mocked<ReturnType<MockTeamCityClient['getAxios']>>; http.get.mockReset(); http.post.mockReset(); http.put.mockReset(); http.delete.mockReset(); buildTypesApi = mockClient.mockModules.buildTypes; buildTypesApi.getAllTriggers.mockImplementation((configId: string, fields?: string) => fields ? http.get(`/app/rest/buildTypes/${configId}/triggers?fields=${fields}`) : http.get(`/app/rest/buildTypes/${configId}/triggers`) ); buildTypesApi.addTriggerToBuildType.mockImplementation( (configId: string, _fields?: string, body?: unknown) => http.post(`/app/rest/buildTypes/${configId}/triggers`, body) ); buildTypesApi.getTrigger.mockImplementation( (configId: string, triggerId: string, fields?: string) => fields ? http.get(`/app/rest/buildTypes/${configId}/triggers/${triggerId}?fields=${fields}`) : http.get(`/app/rest/buildTypes/${configId}/triggers/${triggerId}`) ); buildTypesApi.replaceTrigger.mockImplementation( (configId: string, triggerId: string, _fields?: string, body?: unknown) => http.put(`/app/rest/buildTypes/${configId}/triggers/${triggerId}`, body) ); buildTypesApi.deleteTrigger.mockImplementation((configId: string, triggerId: string) => http.delete(`/app/rest/buildTypes/${configId}/triggers/${triggerId}`) ); buildTypesApi.getAllVcsRootsOfBuildType.mockImplementation( (configId: string, fields?: string) => fields ? http.get(`/app/rest/buildTypes/${configId}/vcs-root-entries?fields=${fields}`) : http.get(`/app/rest/buildTypes/${configId}/vcs-root-entries`) ); manager = new BuildTriggerManager(mockClient); }); describe('listTriggers', () => { it('should retrieve all triggers for a build configuration', async () => { const mockTriggers = { trigger: [ { id: 'TRIGGER_1', type: 'vcsTrigger', disabled: false, properties: { property: [ { name: 'branchFilter', value: '+:refs/heads/main' }, { name: 'quietPeriodMode', value: 'USE_DEFAULT' }, ], }, }, { id: 'TRIGGER_2', type: 'schedulingTrigger', disabled: false, properties: { property: [ { name: 'schedulingPolicy', value: '0 0 2 * * ?' }, { name: 'timezone', value: 'UTC' }, ], }, }, ], }; http.get.mockResolvedValue({ data: mockTriggers }); const result = await manager.listTriggers({ configId: 'MyProject_Build' }); expect(result.success).toBe(true); expect(result.triggers).toHaveLength(2); expect(result.triggers[0]).toEqual({ id: 'TRIGGER_1', type: 'vcsTrigger', enabled: true, properties: { branchFilter: '+:refs/heads/main', quietPeriodMode: 'USE_DEFAULT', }, }); expect(result.triggers[1]?.type).toBe('schedulingTrigger'); // Behavior-first: avoid verifying internal HTTP call shape }); it('should handle empty trigger list', async () => { http.get.mockResolvedValue({ data: {} }); const result = await manager.listTriggers({ configId: 'MyProject_Build' }); expect(result.success).toBe(true); expect(result.triggers).toEqual([]); }); it('should handle build configuration not found', async () => { http.get.mockRejectedValue({ response: { status: 404, data: { message: 'Build configuration not found' } }, }); await expect(manager.listTriggers({ configId: 'Invalid' })).rejects.toThrow( BuildConfigurationNotFoundError ); }); it('should surface invalid trigger payloads from the API', async () => { http.get.mockResolvedValue({ data: { trigger: [ { id: 'BROKEN_TRIGGER', type: 123, }, ], }, }); await expect(manager.listTriggers({ configId: 'MyProject_Build' })).rejects.toThrow( TeamCityAPIError ); }); }); describe('createTrigger', () => { describe('VCS Trigger', () => { it('should create a VCS trigger with branch filter', async () => { const newTrigger = { id: 'TRIGGER_3', type: 'vcsTrigger', properties: { property: [{ name: 'branchFilter', value: '+:refs/heads/*' }], }, }; http.post.mockResolvedValue({ data: newTrigger }); const result = await manager.createTrigger({ configId: 'MyProject_Build', type: 'vcsTrigger', properties: { branchFilter: '+:refs/heads/*', } as VcsTriggerProperties, }); expect(result.success).toBe(true); expect(result.trigger?.id).toBe('TRIGGER_3'); // Behavior-first: avoid verifying request payload shape }); it('should validate branch filter patterns', async () => { await expect( manager.createTrigger({ configId: 'MyProject_Build', type: 'vcsTrigger', properties: { branchFilter: 'invalid-pattern', } as VcsTriggerProperties, }) ).rejects.toThrow(ValidationError); }); it('should create VCS trigger with quiet period configuration', async () => { const newTrigger = { id: 'TRIGGER_VCS_QUIET', type: 'vcsTrigger', properties: { property: [ { name: 'branchFilter', value: '+:refs/heads/main' }, { name: 'quietPeriodMode', value: 'USE_CUSTOM' }, { name: 'quietPeriod', value: '60' }, ], }, }; http.post.mockResolvedValue({ data: newTrigger }); const result = await manager.createTrigger({ configId: 'MyProject_Build', type: 'vcsTrigger', properties: { branchFilter: '+:refs/heads/main', quietPeriodMode: 'USE_CUSTOM', quietPeriod: 60, } as VcsTriggerProperties, }); expect(result.success).toBe(true); // Behavior-first: avoid verifying request payload shape }); it('should create VCS trigger with path filter rules', async () => { const newTrigger = { id: 'TRIGGER_VCS_PATH', type: 'vcsTrigger', properties: { property: [ { name: 'branchFilter', value: '+:refs/heads/develop' }, { name: 'triggerRules', value: '+:src/**\n-:*.md' }, ], }, }; http.post.mockResolvedValue({ data: newTrigger }); const result = await manager.createTrigger({ configId: 'MyProject_Build', type: 'vcsTrigger', properties: { branchFilter: '+:refs/heads/develop', triggerRules: '+:src/**\n-:*.md', } as VcsTriggerProperties, }); expect(result.success).toBe(true); // Behavior-first: avoid verifying request payload shape }); it('should validate multiple branch filters', async () => { const newTrigger = { id: 'TRIGGER_VCS_MULTI', type: 'vcsTrigger', properties: { property: [ { name: 'branchFilter', value: '+:refs/heads/main +:refs/heads/develop -:refs/heads/experimental/*', }, ], }, }; http.post.mockResolvedValue({ data: newTrigger }); const result = await manager.createTrigger({ configId: 'MyProject_Build', type: 'vcsTrigger', properties: { branchFilter: '+:refs/heads/main +:refs/heads/develop -:refs/heads/experimental/*', } as VcsTriggerProperties, }); expect(result.success).toBe(true); }); it('should reject invalid quiet period', async () => { await expect( manager.createTrigger({ configId: 'MyProject_Build', type: 'vcsTrigger', properties: { branchFilter: '+:refs/heads/main', quietPeriodMode: 'USE_CUSTOM', quietPeriod: -1, } as VcsTriggerProperties, }) ).rejects.toThrow(ValidationError); }); it('should handle enableQueueOptimization flag', async () => { const newTrigger = { id: 'TRIGGER_VCS_QUEUE', type: 'vcsTrigger', properties: { property: [ { name: 'branchFilter', value: '+:refs/heads/main' }, { name: 'enableQueueOptimization', value: 'true' }, ], }, }; http.post.mockResolvedValue({ data: newTrigger }); const result = await manager.createTrigger({ configId: 'MyProject_Build', type: 'vcsTrigger', properties: { branchFilter: '+:refs/heads/main', enableQueueOptimization: true, } as VcsTriggerProperties, }); expect(result.success).toBe(true); // Behavior-first: avoid verifying request payload shape }); it('should validate path filter rules syntax', () => { // Test invalid path filter syntax const result = manager.validateTrigger({ type: 'vcsTrigger', properties: { triggerRules: 'invalid::syntax', } as VcsTriggerProperties, }); expect(result.valid).toBe(false); expect(result.errors).toContain('Invalid path filter rules syntax'); }); it('should validate VCS root attachment when specified', async () => { // Mock VCS root entries response - root not attached http.get.mockResolvedValue({ data: { 'vcs-root-entry': [{ 'vcs-root': { id: 'OTHER_ROOT' } }], }, }); await expect( manager.createTrigger({ configId: 'MyProject_Build', type: 'vcsTrigger', properties: { branchFilter: '+:refs/heads/main', vcsRootId: 'MY_VCS_ROOT', } as VcsTriggerProperties, }) ).rejects.toThrow(ValidationError); // Behavior-first: avoid verifying GET shape; error thrown is sufficient }); it('should create VCS trigger with specific VCS root', async () => { // Mock VCS root entries response - root is attached http.get.mockResolvedValue({ data: { 'vcs-root-entry': [{ 'vcs-root': { id: 'MY_VCS_ROOT' } }], }, }); const newTrigger = { id: 'TRIGGER_VCS_ROOT', type: 'vcsTrigger', properties: { property: [ { name: 'branchFilter', value: '+:refs/heads/main' }, { name: 'vcsRootId', value: 'MY_VCS_ROOT' }, ], }, }; http.post.mockResolvedValue({ data: newTrigger }); const result = await manager.createTrigger({ configId: 'MyProject_Build', type: 'vcsTrigger', properties: { branchFilter: '+:refs/heads/main', vcsRootId: 'MY_VCS_ROOT', } as VcsTriggerProperties, }); expect(result.success).toBe(true); // Behavior-first: avoid verifying request payload shape }); it('throws when API responds with invalid trigger payload', async () => { http.post.mockResolvedValue({ data: { id: 'BROKEN_TRIGGER', type: 42 } }); await expect( manager.createTrigger({ configId: 'MyProject_Build', type: 'vcsTrigger', properties: { branchFilter: '+:refs/heads/main', } as VcsTriggerProperties, }) ).rejects.toThrow(TeamCityAPIError); }); }); describe('Schedule Trigger', () => { it('should create a schedule trigger with cron expression', async () => { const newTrigger = { id: 'TRIGGER_4', type: 'schedulingTrigger', properties: { property: [ { name: 'schedulingPolicy', value: '0 0 2 * * ?' }, { name: 'timezone', value: 'America/New_York' }, ], }, }; http.post.mockResolvedValue({ data: newTrigger }); const result = await manager.createTrigger({ configId: 'MyProject_Build', type: 'schedulingTrigger', properties: { schedulingPolicy: '0 0 2 * * ?', timezone: 'America/New_York', } as ScheduleTriggerProperties, }); expect(result.success).toBe(true); expect(result.trigger?.type).toBe('schedulingTrigger'); }); it('should validate cron expressions', async () => { await expect( manager.createTrigger({ configId: 'MyProject_Build', type: 'schedulingTrigger', properties: { schedulingPolicy: 'invalid cron', } as ScheduleTriggerProperties, }) ).rejects.toThrow(ValidationError); }); it('should support TeamCity schedule format', async () => { const newTrigger = { id: 'TRIGGER_5', type: 'schedulingTrigger', properties: { property: [{ name: 'schedulingPolicy', value: 'daily' }], }, }; http.post.mockResolvedValue({ data: newTrigger }); const result = await manager.createTrigger({ configId: 'MyProject_Build', type: 'schedulingTrigger', properties: { schedulingPolicy: 'daily', } as ScheduleTriggerProperties, }); expect(result.success).toBe(true); }); it('should create schedule trigger with timezone', async () => { const newTrigger = { id: 'TRIGGER_SCHEDULE_TZ', type: 'schedulingTrigger', properties: { property: [ { name: 'schedulingPolicy', value: '0 30 14 * * ?' }, { name: 'timezone', value: 'Europe/London' }, ], }, }; http.post.mockResolvedValue({ data: newTrigger }); const result = await manager.createTrigger({ configId: 'MyProject_Build', type: 'schedulingTrigger', properties: { schedulingPolicy: '0 30 14 * * ?', timezone: 'Europe/London', } as ScheduleTriggerProperties, }); expect(result.success).toBe(true); expect(http.post).toHaveBeenCalledWith( expect.stringContaining('/app/rest/buildTypes/MyProject_Build/triggers'), expect.objectContaining({ properties: expect.objectContaining({ property: expect.arrayContaining([ expect.objectContaining({ name: 'timezone', value: 'Europe/London' }), ]), }), }) ); }); it('should handle build parameters for scheduled runs', async () => { const newTrigger = { id: 'TRIGGER_SCHEDULE_PARAMS', type: 'schedulingTrigger', properties: { property: [ { name: 'schedulingPolicy', value: '0 0 3 * * ?' }, { name: 'buildParams.env', value: 'production' }, { name: 'buildParams.deploy', value: 'true' }, ], }, }; http.post.mockResolvedValue({ data: newTrigger }); const result = await manager.createTrigger({ configId: 'MyProject_Build', type: 'schedulingTrigger', properties: { schedulingPolicy: '0 0 3 * * ?', buildParameters: { env: 'production', deploy: 'true', }, } as ScheduleTriggerProperties, }); expect(result.success).toBe(true); }); it('should support trigger only with pending changes', async () => { const newTrigger = { id: 'TRIGGER_SCHEDULE_PENDING', type: 'schedulingTrigger', properties: { property: [ { name: 'schedulingPolicy', value: '0 0 * * * ?' }, { name: 'triggerBuildWithPendingChangesOnly', value: 'true' }, ], }, }; http.post.mockResolvedValue({ data: newTrigger }); const result = await manager.createTrigger({ configId: 'MyProject_Build', type: 'schedulingTrigger', properties: { schedulingPolicy: '0 0 * * * ?', triggerBuildWithPendingChangesOnly: true, } as ScheduleTriggerProperties, }); expect(result.success).toBe(true); expect(http.post).toHaveBeenCalledWith( expect.stringContaining('/app/rest/buildTypes/MyProject_Build/triggers'), expect.objectContaining({ properties: expect.objectContaining({ property: expect.arrayContaining([ expect.objectContaining({ name: 'triggerBuildWithPendingChangesOnly', value: 'true', }), ]), }), }) ); }); it('should validate cron expression format', () => { const result = manager.validateTrigger({ type: 'schedulingTrigger', properties: { schedulingPolicy: '0 0 2 * * ?', } as ScheduleTriggerProperties, }); expect(result.valid).toBe(true); expect(result.errors).toEqual([]); }); it('should reject invalid cron expressions', () => { const result = manager.validateTrigger({ type: 'schedulingTrigger', properties: { schedulingPolicy: '0 0 25 * * ?', // Invalid hour } as ScheduleTriggerProperties, }); expect(result.valid).toBe(false); expect(result.errors).toContain('Invalid schedule format'); }); it('should support all TeamCity schedule keywords', async () => { const keywords = ['daily', 'weekly', 'nightly', 'hourly']; for (const keyword of keywords) { const result = manager.validateTrigger({ type: 'schedulingTrigger', properties: { schedulingPolicy: keyword, } as ScheduleTriggerProperties, }); expect(result.valid).toBe(true); } }); it('should handle promote watched build option', async () => { const newTrigger = { id: 'TRIGGER_SCHEDULE_PROMOTE', type: 'schedulingTrigger', properties: { property: [ { name: 'schedulingPolicy', value: 'weekly' }, { name: 'promoteWatchedBuild', value: 'true' }, ], }, }; http.post.mockResolvedValue({ data: newTrigger }); const result = await manager.createTrigger({ configId: 'MyProject_Build', type: 'schedulingTrigger', properties: { schedulingPolicy: 'weekly', promoteWatchedBuild: true, } as ScheduleTriggerProperties, }); expect(result.success).toBe(true); }); it('should calculate next run time for schedule trigger', () => { const nextRun = manager.calculateNextRunTime('0 0 2 * * ?'); expect(nextRun).toBeInstanceOf(Date); expect(nextRun.getHours()).toBe(2); expect(nextRun.getMinutes()).toBe(0); }); it('should calculate next run time with timezone', () => { const nextRun = manager.calculateNextRunTime('0 0 14 * * ?', 'America/New_York'); expect(nextRun).toBeInstanceOf(Date); }); }); describe('Dependency Trigger', () => { it('should create a dependency trigger', async () => { const newTrigger = { id: 'TRIGGER_6', type: 'buildDependencyTrigger', properties: { property: [ { name: 'dependsOn', value: 'MyProject_OtherBuild' }, { name: 'afterSuccessfulBuildOnly', value: 'true' }, ], }, }; http.post.mockResolvedValue({ data: newTrigger }); const result = await manager.createTrigger({ configId: 'MyProject_Build', type: 'buildDependencyTrigger', properties: { dependsOn: 'MyProject_OtherBuild', afterSuccessfulBuildOnly: true, } as DependencyTriggerProperties, }); expect(result.success).toBe(true); expect(result.trigger?.type).toBe('buildDependencyTrigger'); }); it('should detect circular dependencies', async () => { // Mock checking for existing triggers that would create a cycle http.get.mockResolvedValue({ data: { trigger: [ { id: 'EXISTING', type: 'buildDependencyTrigger', properties: { property: [{ name: 'dependsOn', value: 'MyProject_OtherBuild' }], }, }, ], }, }); // Mock POST in case it gets called (it shouldn't due to circular dependency) http.post.mockResolvedValue({ data: {} }); await expect( manager.createTrigger({ configId: 'MyProject_OtherBuild', type: 'buildDependencyTrigger', properties: { dependsOn: 'MyProject_Build', } as DependencyTriggerProperties, }) ).rejects.toThrow(CircularDependencyError); }); }); }); describe('updateTrigger', () => { it('should update an existing trigger', async () => { // Mock GET to fetch existing trigger http.get.mockResolvedValue({ data: { id: 'TRIGGER_1', type: 'vcsTrigger', disabled: false, properties: { property: [{ name: 'branchFilter', value: '+:refs/heads/main' }], }, }, }); const updatedTrigger = { id: 'TRIGGER_1', type: 'vcsTrigger', disabled: true, properties: { property: [{ name: 'branchFilter', value: '+:refs/heads/develop' }], }, }; http.put.mockResolvedValue({ data: updatedTrigger }); const result = await manager.updateTrigger({ configId: 'MyProject_Build', triggerId: 'TRIGGER_1', enabled: false, properties: { branchFilter: '+:refs/heads/develop', }, }); expect(result.success).toBe(true); expect(result.trigger?.enabled).toBe(false); // Behavior-first: avoid verifying internal HTTP call shape }); it('should handle trigger not found', async () => { // Mock GET to return 404 http.get.mockRejectedValue({ response: { status: 404, data: { message: 'Trigger not found' } }, }); await expect( manager.updateTrigger({ configId: 'MyProject_Build', triggerId: 'INVALID', }) ).rejects.toThrow(TriggerNotFoundError); }); }); describe('deleteTrigger', () => { it('should delete a trigger', async () => { http.delete.mockResolvedValue({ status: 204 }); const result = await manager.deleteTrigger({ configId: 'MyProject_Build', triggerId: 'TRIGGER_1', }); expect(result.success).toBe(true); // Behavior-first: avoid verifying internal HTTP call shape }); it('should handle trigger not found', async () => { http.delete.mockRejectedValue({ response: { status: 404 }, }); await expect( manager.deleteTrigger({ configId: 'MyProject_Build', triggerId: 'INVALID', }) ).rejects.toThrow(TriggerNotFoundError); }); }); describe('validateTrigger', () => { it('should validate VCS trigger properties', () => { const result = manager.validateTrigger({ type: 'vcsTrigger', properties: { branchFilter: '+:refs/heads/main', quietPeriodMode: 'USE_DEFAULT', } as VcsTriggerProperties, }); expect(result.valid).toBe(true); expect(result.errors).toEqual([]); }); it('should return validation errors for invalid properties', () => { const result = manager.validateTrigger({ type: 'schedulingTrigger', properties: { schedulingPolicy: 'invalid', } as ScheduleTriggerProperties, }); expect(result.valid).toBe(false); expect(result.errors).toContain('Invalid schedule format'); }); }); describe('Dependency Triggers', () => { it('should create build dependency trigger', async () => { const newTrigger = { id: 'TRIGGER_DEP_1', type: 'buildDependencyTrigger', properties: { property: [ { name: 'dependsOn', value: 'Project_Build1' }, { name: 'afterSuccessfulBuildOnly', value: 'true' }, ], }, }; http.post.mockResolvedValueOnce({ data: newTrigger }); const result = await manager.createTrigger({ configId: 'Build_Config_1', type: 'buildDependencyTrigger', properties: { dependsOn: 'Project_Build1', afterSuccessfulBuildOnly: true, }, }); expect(result.trigger).toEqual( expect.objectContaining({ id: 'TRIGGER_DEP_1', type: 'buildDependencyTrigger', }) ); // Behavior-first: avoid verifying request payload shape }); it('should create dependency trigger with artifact dependencies', async () => { const newTrigger = { id: 'TRIGGER_DEP_2', type: 'buildDependencyTrigger', properties: { property: [ { name: 'dependsOn', value: 'Project_Build1' }, { name: 'afterSuccessfulBuildOnly', value: 'true' }, { name: 'artifactRules', value: 'dist/**/*.jar => lib' }, { name: 'branchFilter', value: '+:main' }, ], }, }; http.post.mockResolvedValueOnce({ data: newTrigger }); const result = await manager.createTrigger({ configId: 'Build_Config_1', type: 'buildDependencyTrigger', properties: { dependsOn: 'Project_Build1', afterSuccessfulBuildOnly: true, artifactRules: 'dist/**/*.jar => lib', branchFilter: '+:main', }, }); expect(result.trigger).toEqual( expect.objectContaining({ id: 'TRIGGER_DEP_2', type: 'buildDependencyTrigger', }) ); }); it('should validate dependency trigger configuration', async () => { const validation = manager.validateTrigger({ type: 'buildDependencyTrigger', properties: { dependsOn: 'Project_Build1', afterSuccessfulBuildOnly: true, }, }); expect(validation).toEqual({ valid: true, errors: [], warnings: [], }); }); it('should detect invalid dependency configuration', async () => { const validation = manager.validateTrigger({ type: 'buildDependencyTrigger', properties: {}, }); expect(validation.valid).toBe(false); expect(validation.errors).toContain('Dependency trigger requires dependsOn property'); }); it('should list dependency triggers with source info', async () => { const mockTriggers = { trigger: [ { id: 'TRIGGER_DEP_1', type: 'buildDependencyTrigger', properties: { property: [ { name: 'dependsOn', value: 'Project_Build1' }, { name: 'afterSuccessfulBuildOnly', value: 'true' }, ], }, }, ], }; http.get.mockResolvedValueOnce({ data: mockTriggers }); const result = await manager.listTriggers({ configId: 'Build_Config_1' }); expect(result.triggers).toHaveLength(1); expect(result.triggers[0]?.dependsOn).toBe('Project_Build1'); expect(result.triggers[0]?.afterSuccessfulBuildOnly).toBe(true); }); it('should handle multiple dependency configurations', async () => { const newTrigger = { id: 'TRIGGER_DEP_3', type: 'buildDependencyTrigger', properties: { property: [ { name: 'dependsOn', value: 'Project_Build1,Project_Build2' }, { name: 'afterSuccessfulBuildOnly', value: 'false' }, { name: 'dependOnStartedBuild', value: 'true' }, ], }, }; http.post.mockResolvedValueOnce({ data: newTrigger }); const result = await manager.createTrigger({ configId: 'Build_Config_1', type: 'buildDependencyTrigger', properties: { dependsOn: ['Project_Build1', 'Project_Build2'], afterSuccessfulBuildOnly: false, dependOnStartedBuild: true, }, }); expect(result.trigger).toEqual( expect.objectContaining({ id: 'TRIGGER_DEP_3', type: 'buildDependencyTrigger', }) ); }); it('should validate artifact rules format', async () => { const validation = manager.validateTrigger({ type: 'buildDependencyTrigger', properties: { dependsOn: 'Project_Build1', artifactRules: 'invalid artifact rule format', }, }); expect(validation.valid).toBe(false); expect(validation.errors).toContain('Invalid artifact rule format'); }); it('should detect circular dependencies', async () => { // Mock getting existing dependencies // Build_Config_1 depends on Build_Config_2 // Build_Config_2 depends on Build_Config_3 // So if Build_Config_3 tries to depend on Build_Config_1, it creates a cycle http.get .mockResolvedValueOnce({ data: { // Dependencies for Build_Config_1 trigger: [ { id: 'trigger1', type: 'buildDependencyTrigger', properties: { property: [{ name: 'dependsOn', value: 'Build_Config_2' }], }, }, ], }, }) .mockResolvedValueOnce({ data: { // Dependencies for Build_Config_2 trigger: [ { id: 'trigger2', type: 'buildDependencyTrigger', properties: { property: [{ name: 'dependsOn', value: 'Build_Config_3' }], }, }, ], }, }); const validation = await manager.validateDependencyChain('Build_Config_3', 'Build_Config_1'); expect(validation.hasCircularDependency).toBe(true); expect(validation.chain).toEqual([ 'Build_Config_3', 'Build_Config_1', 'Build_Config_2', 'Build_Config_3', ]); }); it('should handle cross-project dependencies', async () => { const newTrigger = { id: 'TRIGGER_DEP_4', type: 'buildDependencyTrigger', properties: { property: [ { name: 'dependsOn', value: 'OtherProject_Build' }, { name: 'afterSuccessfulBuildOnly', value: 'true' }, { name: 'promoteArtifacts', value: 'true' }, ], }, }; http.post.mockResolvedValueOnce({ data: newTrigger }); const result = await manager.createTrigger({ configId: 'Build_Config_1', type: 'buildDependencyTrigger', properties: { dependsOn: 'OtherProject_Build', afterSuccessfulBuildOnly: true, promoteArtifacts: true, }, }); expect(result.trigger).toEqual( expect.objectContaining({ id: 'TRIGGER_DEP_4', type: 'buildDependencyTrigger', }) ); }); it('should update dependency trigger configuration', async () => { const updatedTrigger = { id: 'TRIGGER_DEP_1', type: 'buildDependencyTrigger', properties: { property: [ { name: 'dependsOn', value: 'Project_Build2' }, { name: 'afterSuccessfulBuildOnly', value: 'false' }, ], }, }; http.get.mockResolvedValueOnce({ data: { id: 'TRIGGER_DEP_1', type: 'buildDependencyTrigger', properties: { property: [ { name: 'dependsOn', value: 'Project_Build1' }, { name: 'afterSuccessfulBuildOnly', value: 'true' }, ], }, }, }); http.put.mockResolvedValueOnce({ data: updatedTrigger }); const result = await manager.updateTrigger({ configId: 'Build_Config_1', triggerId: 'TRIGGER_DEP_1', properties: { dependsOn: 'Project_Build2', afterSuccessfulBuildOnly: false, }, }); expect(result.trigger).toEqual( expect.objectContaining({ id: 'TRIGGER_DEP_1', type: 'buildDependencyTrigger', }) ); }); it('should delete dependency trigger', async () => { http.delete.mockResolvedValueOnce({}); const result = await manager.deleteTrigger({ configId: 'Build_Config_1', triggerId: 'TRIGGER_DEP_1', }); expect(result.success).toBe(true); // Behavior-first: avoid verifying delete call shape }); }); });

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