Skip to content

Commit 5afed11

Browse files
authored
fix(dashmate): handle nullable ZeroSSL expiry dates (#4415)
1 parent 69b85c8 commit 5afed11

4 files changed

Lines changed: 59 additions & 4 deletions

File tree

‎packages/dashmate/src/helper/scheduleRenewZeroSslCertificateFactory.js‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,14 @@ export default function scheduleRenewZeroSslCertificateFactory(
5353
}
5454

5555
let expiresAt;
56-
if (certificate.isExpiredInDays(Certificate.EXPIRATION_LIMIT_DAYS)) {
56+
if (certificate.expires === null) {
57+
// A draft or pending certificate has no expiry date yet. Resume its
58+
// obtain flow instead of passing a date in the past to cron.
59+
expiresAt = new Date(Date.now() + 3000);
60+
61+
// eslint-disable-next-line no-console
62+
console.log(`SSL certificate ${certificate.id} has not been issued yet. Schedule to obtain it NOW.`);
63+
} else if (certificate.isExpiredInDays(Certificate.EXPIRATION_LIMIT_DAYS)) {
5764
// Obtain new certificate right away
5865
expiresAt = new Date(Date.now() + 3000);
5966

‎packages/dashmate/src/ssl/zerossl/Certificate.js‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default class Certificate {
1616
created;
1717

1818
/**
19-
* @type {Date}
19+
* @type {Date|null}
2020
*/
2121
expires;
2222

@@ -35,7 +35,8 @@ export default class Certificate {
3535
* @param {string} object.common_name - The common name of the certificate.
3636
* @param {string} object.additional_domains - Any additional domains in the certificate.
3737
* @param {string} object.created - The exact time the certificate was created.
38-
* @param {string} object.expires - The exact time the certificate will expire.
38+
* @param {string|null} object.expires - The exact time the certificate will expire,
39+
* or null until the certificate is issued.
3940
* @param {string} object.status - The current certificate status.
4041
* @param {string|null} object.validation_type - The selected verification type, or null
4142
* if not initiated.
@@ -64,7 +65,7 @@ export default class Certificate {
6465
* the CNAME-record for domain verification.
6566
*/
6667
constructor(object) {
67-
const expires = convertDate(object.expires);
68+
const expires = object.expires === null ? null : convertDate(object.expires);
6869
const created = convertDate(object.created);
6970

7071
Object.assign(this, {
@@ -105,6 +106,10 @@ export default class Certificate {
105106
* @param {number} days
106107
*/
107108
isExpiredInDays(days) {
109+
if (this.expires === null) {
110+
return false;
111+
}
112+
108113
const expiresInDays = new Date(this.expires);
109114
expiresInDays.setDate(expiresInDays.getDate() - days);
110115

‎packages/dashmate/test/unit/helper/scheduleRenewZeroSslCertificateFactory.spec.js‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,33 @@ describe('scheduleRenewZeroSslCertificateFactory', () => {
6565
expect(setTimeoutStub.firstCall.args[1]).to.equal(60 * 60 * 1000);
6666
});
6767
});
68+
69+
it('should resume obtaining a pending certificate that has no expiry date', async function it() {
70+
const clock = this.sinon.useFakeTimers({
71+
now: new Date('2026-08-19T00:00:00.000Z'),
72+
});
73+
this.sinon.stub(console, 'log');
74+
75+
const certificate = {
76+
id: 'pending-certificate-id',
77+
status: 'pending_validation',
78+
expires: null,
79+
isExpiredInDays: this.sinon.stub().returns(false),
80+
};
81+
const tasks = {
82+
run: this.sinon.stub().resolves(),
83+
};
84+
85+
getCertificate.resolves(certificate);
86+
obtainZeroSSLCertificateTask.returns(tasks);
87+
88+
await scheduleRenewZeroSslCertificate(config);
89+
await clock.tickAsync(3000);
90+
91+
expect(obtainZeroSSLCertificateTask).to.have.been.calledOnceWithExactly(config);
92+
expect(tasks.run).to.have.been.calledOnceWithExactly({
93+
expirationDays: 3,
94+
noRetry: true,
95+
});
96+
});
6897
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Certificate from '../../../../src/ssl/zerossl/Certificate.js';
2+
3+
describe('Certificate', () => {
4+
it('should handle a pending certificate without an expiration date', () => {
5+
const certificate = new Certificate({
6+
status: 'pending_validation',
7+
created: '2026-08-18 09:04:19',
8+
expires: null,
9+
});
10+
11+
expect(certificate.expires).to.be.null();
12+
expect(certificate.isExpiredInDays(30)).to.be.false();
13+
});
14+
});

0 commit comments

Comments
 (0)