Skip to content

Commit e351dac

Browse files
committed
use validToDate if present, otherwise more explicitly parse validTo format, add unit test
1 parent 9fbb9c4 commit e351dac

File tree

4 files changed

+96
-8
lines changed

4 files changed

+96
-8
lines changed

src/certificate-expiration.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { X509Certificate } from 'node:crypto'
2+
3+
const MONTHS = [
4+
'Jan',
5+
'Feb',
6+
'Mar',
7+
'Apr',
8+
'May',
9+
'Jun',
10+
'Jul',
11+
'Aug',
12+
'Sep',
13+
'Oct',
14+
'Nov',
15+
'Dec',
16+
]
17+
18+
export function isCertificateExpired(content: string): boolean {
19+
const cert = new X509Certificate(content)
20+
const expirationDate = getCertificateExpirationDate(cert)
21+
return new Date() > expirationDate
22+
}
23+
24+
function getCertificateExpirationDate(cert: X509Certificate): Date {
25+
// validToDate is not available until node 22
26+
if (cert.validToDate) {
27+
return cert.validToDate
28+
}
29+
30+
// validTo is a nonstandard format: %s %2d %02d:%02d:%02d %d%s GMT
31+
// https://github.com/nodejs/node/issues/52931
32+
const [month, day, time, year] = cert.validTo
33+
.split(' ')
34+
.filter((part) => !!part)
35+
// convert string month to number
36+
const monthIndex = MONTHS.indexOf(month) + 1
37+
return new Date(
38+
`${year}-${monthIndex.toString().padStart(2, '0')}-${day.padStart(2, '0')}T${time}Z`,
39+
)
40+
}

src/index.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import path from 'node:path'
2-
import { X509Certificate } from 'node:crypto'
32
import { promises as fsp } from 'node:fs'
43
import type { Plugin } from 'vite'
4+
import { isCertificateExpired } from './certificate-expiration'
55

66
const defaultCacheDir = 'node_modules/.vite'
77

@@ -43,13 +43,10 @@ export async function getCertificate(
4343

4444
try {
4545
const content = await fsp.readFile(cachePath, 'utf8')
46-
const cert = new X509Certificate(content);
46+
const isExpired = isCertificateExpired(content)
4747

48-
// validTo is a nonstandard format, but it successfully parses. validToDate
49-
// is not available until node 22
50-
// https://github.com/nodejs/node/issues/52931
51-
if (Date.now() > Date.parse(cert.validTo)) {
52-
throw new Error('cache is outdated.');
48+
if (isExpired) {
49+
throw new Error('cache is outdated.')
5350
}
5451

5552
return content

test/test.spec.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { test, expect } from 'vitest'
1+
import { beforeEach, describe, test, expect, vi, Mock } from 'vitest'
2+
import { X509Certificate } from 'node:crypto'
23
import { createCertificate } from '../src/certificate'
4+
import { isCertificateExpired } from '../src/certificate-expiration'
35

46
test('create certificate', () => {
57
const content = createCertificate()
@@ -10,3 +12,51 @@ test('create certificate', () => {
1012
/-----BEGIN CERTIFICATE-----(\n|\r|.)*-----END CERTIFICATE-----/,
1113
)
1214
})
15+
16+
describe('isCertificateExpired', () => {
17+
let validToDateMock: Mock
18+
let validToMock: Mock
19+
20+
beforeEach(() => {
21+
validToDateMock = vi.spyOn(X509Certificate.prototype, 'validToDate', 'get')
22+
validToMock = vi.spyOn(X509Certificate.prototype, 'validTo', 'get')
23+
})
24+
25+
describe('with validToDate', () => {
26+
test('returns false', () => {
27+
validToDateMock.mockReturnValue(new Date(Date.now() + 10000))
28+
29+
const content = createCertificate()
30+
const isExpired = isCertificateExpired(content)
31+
expect(isExpired).toBe(false)
32+
})
33+
34+
test('returns true', () => {
35+
validToDateMock.mockReturnValue(new Date(Date.now() - 10000))
36+
37+
const content = createCertificate()
38+
const isExpired = isCertificateExpired(content)
39+
expect(isExpired).toBe(true)
40+
})
41+
})
42+
43+
describe('with validTo', () => {
44+
test('returns false', () => {
45+
validToDateMock.mockReturnValue(undefined)
46+
validToMock.mockReturnValue('Sep 3 21:40:37 2296 GMT')
47+
48+
const content = createCertificate()
49+
const isExpired = isCertificateExpired(content)
50+
expect(isExpired).toBe(false)
51+
})
52+
53+
test('returns true', () => {
54+
validToDateMock.mockReturnValue(undefined)
55+
validToMock.mockReturnValue('Jan 22 08:20:44 2022 GMT')
56+
57+
const content = createCertificate()
58+
const isExpired = isCertificateExpired(content)
59+
expect(isExpired).toBe(true)
60+
})
61+
})
62+
})

test/vitest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ import { defineConfig } from 'vite'
44
export default defineConfig({
55
test: {
66
testTimeout: 100000,
7+
restoreMocks: true,
78
},
89
})

0 commit comments

Comments
 (0)