diff --git a/packages/dashmate/src/helper/scheduleRenewZeroSslCertificateFactory.js b/packages/dashmate/src/helper/scheduleRenewZeroSslCertificateFactory.js index 0526bcc1f97..c84929eef46 100644 --- a/packages/dashmate/src/helper/scheduleRenewZeroSslCertificateFactory.js +++ b/packages/dashmate/src/helper/scheduleRenewZeroSslCertificateFactory.js @@ -53,7 +53,14 @@ export default function scheduleRenewZeroSslCertificateFactory( } let expiresAt; - if (certificate.isExpiredInDays(Certificate.EXPIRATION_LIMIT_DAYS)) { + if (certificate.expires === null) { + // A draft or pending certificate has no expiry date yet. Resume its + // obtain flow instead of passing a date in the past to cron. + expiresAt = new Date(Date.now() + 3000); + + // eslint-disable-next-line no-console + console.log(`SSL certificate ${certificate.id} has not been issued yet. Schedule to obtain it NOW.`); + } else if (certificate.isExpiredInDays(Certificate.EXPIRATION_LIMIT_DAYS)) { // Obtain new certificate right away expiresAt = new Date(Date.now() + 3000); diff --git a/packages/dashmate/src/ssl/zerossl/Certificate.js b/packages/dashmate/src/ssl/zerossl/Certificate.js index f21e6226555..dae3a0b2863 100644 --- a/packages/dashmate/src/ssl/zerossl/Certificate.js +++ b/packages/dashmate/src/ssl/zerossl/Certificate.js @@ -16,7 +16,7 @@ export default class Certificate { created; /** - * @type {Date} + * @type {Date|null} */ expires; @@ -35,7 +35,8 @@ export default class Certificate { * @param {string} object.common_name - The common name of the certificate. * @param {string} object.additional_domains - Any additional domains in the certificate. * @param {string} object.created - The exact time the certificate was created. - * @param {string} object.expires - The exact time the certificate will expire. + * @param {string|null} object.expires - The exact time the certificate will expire, + * or null until the certificate is issued. * @param {string} object.status - The current certificate status. * @param {string|null} object.validation_type - The selected verification type, or null * if not initiated. @@ -64,7 +65,7 @@ export default class Certificate { * the CNAME-record for domain verification. */ constructor(object) { - const expires = convertDate(object.expires); + const expires = object.expires === null ? null : convertDate(object.expires); const created = convertDate(object.created); Object.assign(this, { @@ -105,6 +106,10 @@ export default class Certificate { * @param {number} days */ isExpiredInDays(days) { + if (this.expires === null) { + return false; + } + const expiresInDays = new Date(this.expires); expiresInDays.setDate(expiresInDays.getDate() - days); diff --git a/packages/dashmate/test/unit/helper/scheduleRenewZeroSslCertificateFactory.spec.js b/packages/dashmate/test/unit/helper/scheduleRenewZeroSslCertificateFactory.spec.js index 46eebf74f9c..44e2df9631e 100644 --- a/packages/dashmate/test/unit/helper/scheduleRenewZeroSslCertificateFactory.spec.js +++ b/packages/dashmate/test/unit/helper/scheduleRenewZeroSslCertificateFactory.spec.js @@ -65,4 +65,33 @@ describe('scheduleRenewZeroSslCertificateFactory', () => { expect(setTimeoutStub.firstCall.args[1]).to.equal(60 * 60 * 1000); }); }); + + it('should resume obtaining a pending certificate that has no expiry date', async function it() { + const clock = this.sinon.useFakeTimers({ + now: new Date('2026-08-19T00:00:00.000Z'), + }); + this.sinon.stub(console, 'log'); + + const certificate = { + id: 'pending-certificate-id', + status: 'pending_validation', + expires: null, + isExpiredInDays: this.sinon.stub().returns(false), + }; + const tasks = { + run: this.sinon.stub().resolves(), + }; + + getCertificate.resolves(certificate); + obtainZeroSSLCertificateTask.returns(tasks); + + await scheduleRenewZeroSslCertificate(config); + await clock.tickAsync(3000); + + expect(obtainZeroSSLCertificateTask).to.have.been.calledOnceWithExactly(config); + expect(tasks.run).to.have.been.calledOnceWithExactly({ + expirationDays: 3, + noRetry: true, + }); + }); }); diff --git a/packages/dashmate/test/unit/ssl/zerossl/Certificate.spec.js b/packages/dashmate/test/unit/ssl/zerossl/Certificate.spec.js new file mode 100644 index 00000000000..1bcc1396f22 --- /dev/null +++ b/packages/dashmate/test/unit/ssl/zerossl/Certificate.spec.js @@ -0,0 +1,14 @@ +import Certificate from '../../../../src/ssl/zerossl/Certificate.js'; + +describe('Certificate', () => { + it('should handle a pending certificate without an expiration date', () => { + const certificate = new Certificate({ + status: 'pending_validation', + created: '2026-08-18 09:04:19', + expires: null, + }); + + expect(certificate.expires).to.be.null(); + expect(certificate.isExpiredInDays(30)).to.be.false(); + }); +});