|
3 | 3 | /** |
4 | 4 | * Unit Tests: tls-config.ts |
5 | 5 | * |
6 | | - * Tests for system CA certificate loading |
| 6 | + * Tests for system CA certificate loading. |
| 7 | + * |
| 8 | + * The real-system tests below exercise the default (no-dependency) code path, |
| 9 | + * while the injected-dependency tests deterministically cover every branch — |
| 10 | + * including the Windows and unreadable-bundle paths — on any host/OS. |
7 | 11 | */ |
8 | 12 |
|
9 | 13 | import { strict as assert } from 'node:assert'; |
10 | 14 | import { fileURLToPath } from 'node:url'; |
11 | 15 | import { getSystemCACerts, getConnectOptions } from '../../src/tls-config.js'; |
12 | 16 | import { testFunction, createTestResults, printTestSummary } from '../helpers/test-utils.js'; |
13 | 17 |
|
| 18 | +const PEM = '-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n'; |
| 19 | + |
14 | 20 | const results = createTestResults(); |
15 | 21 |
|
16 | 22 | async function runTests() { |
17 | 23 | console.log('🧪 Testing: tls-config.ts\n'); |
18 | 24 |
|
| 25 | + // --- Real-system path (covers the default platform/fs dependencies) --- |
| 26 | + |
19 | 27 | await testFunction('getSystemCACerts returns string or null', () => { |
20 | 28 | const certs = getSystemCACerts(); |
21 | 29 | assert.ok(certs === null || typeof certs === 'string'); |
@@ -56,6 +64,86 @@ async function runTests() { |
56 | 64 | } |
57 | 65 | }, results); |
58 | 66 |
|
| 67 | + // --- Injected dependencies: deterministic branch coverage --- |
| 68 | + |
| 69 | + await testFunction('getSystemCACerts returns null on win32 without touching the filesystem', () => { |
| 70 | + let touched = false; |
| 71 | + const certs = getSystemCACerts({ |
| 72 | + platformName: 'win32', |
| 73 | + fileExists: () => { touched = true; return true; }, |
| 74 | + readFile: () => { touched = true; return PEM; }, |
| 75 | + caPaths: ['/should/not/be/read'], |
| 76 | + }); |
| 77 | + assert.equal(certs, null); |
| 78 | + assert.equal(touched, false, 'win32 short-circuits before any fs access'); |
| 79 | + }, results); |
| 80 | + |
| 81 | + await testFunction('getSystemCACerts returns the first readable bundle', () => { |
| 82 | + const reads: string[] = []; |
| 83 | + const certs = getSystemCACerts({ |
| 84 | + platformName: 'linux', |
| 85 | + fileExists: () => true, |
| 86 | + readFile: (p) => { reads.push(p); return PEM; }, |
| 87 | + caPaths: ['/etc/ssl/first.crt', '/etc/ssl/second.crt'], |
| 88 | + }); |
| 89 | + assert.equal(certs, PEM); |
| 90 | + assert.deepEqual(reads, ['/etc/ssl/first.crt'], 'stops at the first readable bundle'); |
| 91 | + }, results); |
| 92 | + |
| 93 | + await testFunction('getSystemCACerts skips an existing-but-unreadable bundle and tries the next', () => { |
| 94 | + const certs = getSystemCACerts({ |
| 95 | + platformName: 'linux', |
| 96 | + fileExists: () => true, |
| 97 | + readFile: (p) => { |
| 98 | + if (p === '/etc/ssl/locked.crt') { |
| 99 | + throw Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' }); |
| 100 | + } |
| 101 | + return PEM; |
| 102 | + }, |
| 103 | + caPaths: ['/etc/ssl/locked.crt', '/etc/ssl/readable.crt'], |
| 104 | + }); |
| 105 | + assert.equal(certs, PEM, 'falls through the unreadable path to the readable one'); |
| 106 | + }, results); |
| 107 | + |
| 108 | + await testFunction('getSystemCACerts returns null when no candidate path exists', () => { |
| 109 | + const certs = getSystemCACerts({ |
| 110 | + platformName: 'linux', |
| 111 | + fileExists: () => false, |
| 112 | + readFile: () => { throw new Error('should not be called'); }, |
| 113 | + caPaths: ['/nope/a.crt', '/nope/b.crt'], |
| 114 | + }); |
| 115 | + assert.equal(certs, null); |
| 116 | + }, results); |
| 117 | + |
| 118 | + await testFunction('getSystemCACerts returns null when every bundle is unreadable', () => { |
| 119 | + const certs = getSystemCACerts({ |
| 120 | + platformName: 'linux', |
| 121 | + fileExists: () => true, |
| 122 | + readFile: () => { throw new Error('EACCES'); }, |
| 123 | + caPaths: ['/etc/ssl/a.crt', '/etc/ssl/b.crt'], |
| 124 | + }); |
| 125 | + assert.equal(certs, null); |
| 126 | + }, results); |
| 127 | + |
| 128 | + await testFunction('getConnectOptions wraps the CA bundle when one is found', () => { |
| 129 | + const opts = getConnectOptions({ |
| 130 | + platformName: 'linux', |
| 131 | + fileExists: () => true, |
| 132 | + readFile: () => PEM, |
| 133 | + caPaths: ['/etc/ssl/found.crt'], |
| 134 | + }); |
| 135 | + assert.deepEqual(opts, { ca: PEM }); |
| 136 | + }, results); |
| 137 | + |
| 138 | + await testFunction('getConnectOptions returns empty object when no CA bundle is found', () => { |
| 139 | + const opts = getConnectOptions({ |
| 140 | + platformName: 'linux', |
| 141 | + fileExists: () => false, |
| 142 | + caPaths: ['/nope.crt'], |
| 143 | + }); |
| 144 | + assert.deepEqual(opts, {}); |
| 145 | + }, results); |
| 146 | + |
59 | 147 | printTestSummary(results, 'TLS Config Module'); |
60 | 148 | return results; |
61 | 149 | } |
|
0 commit comments