import { tap, expect } from '@git.zone/tstest/tapbundle'; import { ServiceManager } from '../ts/manager.service/classes.servicemanager.js'; const createDeleteable = (labelArg: string, callsArg: string[]) => ({ id: labelArg, delete: async () => callsArg.push(`delete:${labelArg}`), }); const createManager = (optionsArg: { calls: string[]; failBackups?: boolean; }) => { const calls = optionsArg.calls; const service = { id: 'service-1', data: { name: 'ghost', imageId: 'image-1', appTemplateId: 'ghost', secretBundleId: 'bundle-1', registryTarget: { repository: 'workloads/ghost-service-1' }, domains: [{ name: 'ghost.example.com' }], }, removeDnsEntries: async () => calls.push('delete:dns'), delete: async () => calls.push('delete:service'), }; const manager = Object.create(ServiceManager.prototype) as any; manager.CService = { getInstance: async () => service, }; manager.cloudlyRef = { appStoreManager: { clearOperationsForService: (serviceIdArg: string) => calls.push(`clear-upgrades:${serviceIdArg}`), }, deploymentManager: { CDeployment: { getInstances: async () => [createDeleteable('deployment-1', calls)], }, }, platformManager: { CPlatformBinding: { getInstances: async (queryArg: { serviceId: string }) => queryArg.serviceId === 'service-1' ? [createDeleteable('platform-binding-1', calls)] : [], }, }, backupManager: { deleteBackupsForService: async (serviceIdArg: string) => { calls.push(`delete-backups:${serviceIdArg}`); if (optionsArg.failBackups) { throw new Error('backup cleanup failed'); } }, }, registryManager: { deleteServiceRepository: async () => calls.push('delete:registry-repository'), }, secretManager: { CSecretBundle: { getInstance: async () => ({ id: 'bundle-1', data: { serviceId: 'service-1', includedSecretGroupIds: ['group-1'], }, delete: async () => calls.push('delete:secret-bundle'), }), getInstances: async () => [], }, CSecretGroup: { getInstance: async () => createDeleteable('secret-group-1', calls), }, }, imageManager: { deleteImageIfUnreferenced: async (imageIdArg: string, serviceIdArg: string) => { calls.push(`delete-image:${imageIdArg}:${serviceIdArg}`); return true; }, }, settingsManager: { getSettings: async () => ({ dcrouterGatewayUrl: 'https://dcrouter.example.com', dcrouterGatewayApiToken: 'token', dcrouterWorkHosterId: 'cloudly-main', }), }, clusterManager: { getAllClusters: async () => [], }, coreflowManager: { pushClusterConfigToConnectedCoreflows: async () => calls.push('push:coreflow'), }, }; manager.fireDcRouterRequest = async (_url: string, methodArg: string, payloadArg: any) => { calls.push(`${methodArg}:${payloadArg.ownership.hostname}:${payloadArg.ownership.workHosterId}`); return { success: true, action: 'deleted' }; }; return { manager, service }; }; tap.test('should delete Cloudly service-owned resources before deleting the service row', async () => { const calls: string[] = []; const { manager } = createManager({ calls }); await manager.deleteServiceById('service-1'); expect(calls).toContain('syncWorkAppRoute:ghost.example.com:cloudly-main'); expect(calls).toContain('clear-upgrades:service-1'); expect(calls).toContain('delete:deployment-1'); expect(calls).toContain('delete:dns'); expect(calls).toContain('delete:platform-binding-1'); expect(calls).toContain('delete-backups:service-1'); expect(calls).toContain('delete:registry-repository'); expect(calls).toContain('delete:secret-bundle'); expect(calls).toContain('delete:secret-group-1'); expect(calls).toContain('delete-image:image-1:service-1'); expect(calls.at(-2)).toEqual('delete:service'); expect(calls.at(-1)).toEqual('push:coreflow'); }); tap.test('should keep Cloudly service row when required cleanup fails', async () => { const calls: string[] = []; const { manager } = createManager({ calls, failBackups: true }); let errorMessage = ''; try { await manager.deleteServiceById('service-1'); } catch (error) { errorMessage = (error as Error).message; } expect(errorMessage).toEqual('backup cleanup failed'); expect(calls).not.toContain('delete:service'); expect(calls).not.toContain('push:coreflow'); }); export default tap.start();