From 43d4ef8ddaed5b31bc789ef0682be9bf9af2987c Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Wed, 16 Sep 2026 19:21:44 -0700 Subject: [PATCH] Test certificate cleanup renewal flow --- src/user/system/CertbotManager.ts | 3 + tests/LoadBalancerManagerRenewal.test.ts | 88 ++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 tests/LoadBalancerManagerRenewal.test.ts diff --git a/src/user/system/CertbotManager.ts b/src/user/system/CertbotManager.ts index fc5c8209..b3111acc 100644 --- a/src/user/system/CertbotManager.ts +++ b/src/user/system/CertbotManager.ts @@ -19,6 +19,9 @@ const ORPHAN_CERTIFICATE_EXPIRY_GRACE_PERIOD_MS = 24 * 60 * 60 * 1000 const CERTBOT_RENEWAL_CONFIG_DIRECTORY = CaptainConstants.letsEncryptEtcPath + '/renewal' +/** + * Returns whether an inactive certificate has been expired for at least one day. + */ export function isExpiredOrphanedCertificateEligibleForDeletion( certificateName: string, activeDomains: string[], diff --git a/tests/LoadBalancerManagerRenewal.test.ts b/tests/LoadBalancerManagerRenewal.test.ts new file mode 100644 index 00000000..0ce65d1f --- /dev/null +++ b/tests/LoadBalancerManagerRenewal.test.ts @@ -0,0 +1,88 @@ +import type DataStore from '../src/datastore/DataStore' +import type DockerApi from '../src/docker/DockerApi' +import type CertbotManager from '../src/user/system/CertbotManager' +import LoadBalancerManager from '../src/user/system/LoadBalancerManager' +import Logger from '../src/utils/Logger' + +describe('certificate renewal flow', () => { + beforeEach(() => { + jest.useFakeTimers() + }) + + afterEach(() => { + jest.restoreAllMocks() + jest.useRealTimers() + }) + + test('passes active domains to cleanup before renewal and reload', async () => { + const calls: string[] = [] + const deleteExpiredOrphanedCertificates = jest + .fn() + .mockImplementation(async (activeDomains: string[]) => { + calls.push('cleanup') + expect(activeDomains).toEqual(['active.example.com']) + }) + const renewAllCerts = jest.fn().mockImplementation(async () => { + calls.push('renew') + }) + const manager = new LoadBalancerManager( + {} as DockerApi, + { + deleteExpiredOrphanedCertificates, + renewAllCerts, + } as unknown as CertbotManager, + {} as DataStore + ) + jest.spyOn(manager, 'getActiveSslDomains').mockImplementation( + async () => { + calls.push('get-active-domains') + return ['active.example.com'] + } + ) + jest.spyOn(manager, 'rePopulateNginxConfigFile').mockImplementation( + async () => { + calls.push('reload') + } + ) + + await manager.renewAllCertsAndReload() + + expect(calls).toEqual([ + 'get-active-domains', + 'cleanup', + 'renew', + 'reload', + ]) + }) + + test('continues renewal and reload when cleanup fails', async () => { + const cleanupError = new Error('cleanup failed') + const deleteExpiredOrphanedCertificates = jest + .fn() + .mockRejectedValue(cleanupError) + const renewAllCerts = jest.fn().mockResolvedValue(undefined) + const manager = new LoadBalancerManager( + {} as DockerApi, + { + deleteExpiredOrphanedCertificates, + renewAllCerts, + } as unknown as CertbotManager, + {} as DataStore + ) + jest.spyOn(manager, 'getActiveSslDomains').mockResolvedValue([ + 'active.example.com', + ]) + const reload = jest + .spyOn(manager, 'rePopulateNginxConfigFile') + .mockResolvedValue() + const errorLog = jest.spyOn(Logger, 'e').mockImplementation() + + await manager.renewAllCertsAndReload() + + expect(errorLog).toHaveBeenCalledWith( + `Orphaned certificate cleanup failed: ${cleanupError}` + ) + expect(renewAllCerts).toHaveBeenCalledTimes(1) + expect(reload).toHaveBeenCalledTimes(1) + }) +})