Add certificate file reading support for SEA vs non-SEA modes

Co-authored-by: mountaindude <1029262+mountaindude@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-07-27 20:02:30 +00:00
parent 9a43a65d19
commit 177a5417ce
4 changed files with 468 additions and 10 deletions

View File

@@ -0,0 +1,382 @@
import { jest, describe, test, beforeEach, afterEach } from '@jest/globals';
// Mock dependencies
jest.unstable_mockModule('fs', () => ({
default: {
readFileSync: jest.fn(),
},
}));
jest.unstable_mockModule('../sea-wrapper.js', () => ({
default: {
getAsset: jest.fn(),
isSea: jest.fn(),
},
}));
jest.unstable_mockModule('../../globals.js', () => ({
default: {
logger: {
verbose: jest.fn(),
error: jest.fn(),
info: jest.fn(),
},
isSea: false,
},
}));
// Import mocked modules
const fs = (await import('fs')).default;
const sea = (await import('../sea-wrapper.js')).default;
const globals = (await import('../../globals.js')).default;
// Import modules under test
const { getCertificates: getProxyCertificates } = await import('../proxysessionmetrics.js');
const { getCertificates: getHealthCertificates } = await import('../healthmetrics.js');
describe('Certificate loading in SEA vs non-SEA modes', () => {
const mockCertificateOptions = {
Certificate: '/path/to/client.crt',
CertificateKey: '/path/to/client.key',
CertificateCA: '/path/to/ca.crt',
};
const mockCertData = '-----BEGIN CERTIFICATE-----\nMIIBIjANBgkqhkiG9w0BAQEFA...';
const mockKeyData = '-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFA...';
const mockCaData = '-----BEGIN CERTIFICATE-----\nMIICIjANBgkqhkiG9w0BAQEFA...';
beforeEach(() => {
jest.clearAllMocks();
// Reset globals.isSea to default
globals.isSea = false;
});
afterEach(() => {
jest.clearAllMocks();
// Reset globals.isSea to default
globals.isSea = false;
});
describe('Non-SEA mode certificate loading', () => {
beforeEach(() => {
globals.isSea = false;
});
test('should load certificates from filesystem using fs.readFileSync - proxysessionmetrics', () => {
// Mock filesystem operations for this specific test
fs.readFileSync
.mockReturnValueOnce(mockCertData)
.mockReturnValueOnce(mockKeyData)
.mockReturnValueOnce(mockCaData);
const certificates = getProxyCertificates(mockCertificateOptions);
expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/client.crt');
expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/client.key');
expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/ca.crt');
expect(fs.readFileSync).toHaveBeenCalledTimes(3);
expect(certificates).toEqual({
cert: mockCertData,
key: mockKeyData,
ca: mockCaData,
});
// Should not call SEA functions
expect(sea.getAsset).not.toHaveBeenCalled();
});
test('should load certificates from filesystem using fs.readFileSync - healthmetrics', () => {
// Mock filesystem operations for this specific test
fs.readFileSync
.mockReturnValueOnce(mockCertData)
.mockReturnValueOnce(mockKeyData)
.mockReturnValueOnce(mockCaData);
const certificates = getHealthCertificates(mockCertificateOptions);
expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/client.crt');
expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/client.key');
expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/ca.crt');
expect(fs.readFileSync).toHaveBeenCalledTimes(3);
expect(certificates).toEqual({
cert: mockCertData,
key: mockKeyData,
ca: mockCaData,
});
// Should not call SEA functions
expect(sea.getAsset).not.toHaveBeenCalled();
});
test('should handle filesystem errors gracefully - proxysessionmetrics', () => {
fs.readFileSync.mockImplementation((path) => {
throw new Error(`ENOENT: no such file or directory, open '${path}'`);
});
expect(() => getProxyCertificates(mockCertificateOptions)).toThrow(
"ENOENT: no such file or directory, open '/path/to/client.crt'"
);
expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/client.crt');
});
test('should handle filesystem errors gracefully - healthmetrics', () => {
fs.readFileSync.mockImplementation((path) => {
throw new Error(`ENOENT: no such file or directory, open '${path}'`);
});
expect(() => getHealthCertificates(mockCertificateOptions)).toThrow(
"ENOENT: no such file or directory, open '/path/to/client.crt'"
);
expect(fs.readFileSync).toHaveBeenCalledWith('/path/to/client.crt');
});
});
describe('SEA mode certificate loading', () => {
beforeEach(() => {
globals.isSea = true;
});
test('should load certificates from SEA assets using sea.getAsset - proxysessionmetrics', () => {
// Mock SEA asset operations for this specific test
sea.getAsset
.mockReturnValueOnce(mockCertData)
.mockReturnValueOnce(mockKeyData)
.mockReturnValueOnce(mockCaData);
const certificates = getProxyCertificates(mockCertificateOptions);
expect(sea.getAsset).toHaveBeenCalledWith('/path/to/client.crt', 'utf8');
expect(sea.getAsset).toHaveBeenCalledWith('/path/to/client.key', 'utf8');
expect(sea.getAsset).toHaveBeenCalledWith('/path/to/ca.crt', 'utf8');
expect(sea.getAsset).toHaveBeenCalledTimes(3);
expect(certificates).toEqual({
cert: mockCertData,
key: mockKeyData,
ca: mockCaData,
});
// Should not call filesystem functions
expect(fs.readFileSync).not.toHaveBeenCalled();
});
test('should load certificates from SEA assets using sea.getAsset - healthmetrics', () => {
// Mock SEA asset operations for this specific test
sea.getAsset
.mockReturnValueOnce(mockCertData)
.mockReturnValueOnce(mockKeyData)
.mockReturnValueOnce(mockCaData);
const certificates = getHealthCertificates(mockCertificateOptions);
expect(sea.getAsset).toHaveBeenCalledWith('/path/to/client.crt', 'utf8');
expect(sea.getAsset).toHaveBeenCalledWith('/path/to/client.key', 'utf8');
expect(sea.getAsset).toHaveBeenCalledWith('/path/to/ca.crt', 'utf8');
expect(sea.getAsset).toHaveBeenCalledTimes(3);
expect(certificates).toEqual({
cert: mockCertData,
key: mockKeyData,
ca: mockCaData,
});
// Should not call filesystem functions
expect(fs.readFileSync).not.toHaveBeenCalled();
});
test('should handle missing SEA assets gracefully - proxysessionmetrics', () => {
sea.getAsset.mockReturnValue(undefined);
const certificates = getProxyCertificates(mockCertificateOptions);
expect(sea.getAsset).toHaveBeenCalledWith('/path/to/client.crt', 'utf8');
expect(certificates).toEqual({
cert: undefined,
key: undefined,
ca: undefined,
});
});
test('should handle missing SEA assets gracefully - healthmetrics', () => {
sea.getAsset.mockReturnValue(undefined);
const certificates = getHealthCertificates(mockCertificateOptions);
expect(sea.getAsset).toHaveBeenCalledWith('/path/to/client.crt', 'utf8');
expect(certificates).toEqual({
cert: undefined,
key: undefined,
ca: undefined,
});
});
test('should handle mixed success/failure scenarios - proxysessionmetrics', () => {
sea.getAsset
.mockReturnValueOnce(mockCertData) // cert succeeds
.mockReturnValueOnce(undefined) // key fails
.mockReturnValueOnce(mockCaData); // ca succeeds
const certificates = getProxyCertificates(mockCertificateOptions);
expect(certificates).toEqual({
cert: mockCertData,
key: undefined,
ca: mockCaData,
});
});
test('should handle mixed success/failure scenarios - healthmetrics', () => {
sea.getAsset
.mockReturnValueOnce(mockCertData) // cert succeeds
.mockReturnValueOnce(undefined) // key fails
.mockReturnValueOnce(mockCaData); // ca succeeds
const certificates = getHealthCertificates(mockCertificateOptions);
expect(certificates).toEqual({
cert: mockCertData,
key: undefined,
ca: mockCaData,
});
});
});
describe('Certificate format consistency', () => {
test('should handle various certificate formats in both modes', () => {
const formats = [
'-----BEGIN CERTIFICATE-----\nMIIBIjANBgkqhkiG9w0BAQEFA...\n-----END CERTIFICATE-----',
'-----BEGIN CERTIFICATE-----\nMIICIjANBgkqhkiG9w0BAQEFA...\n-----END CERTIFICATE-----\n',
'MIIC...', // base64 without headers
];
formats.forEach((certFormat, index) => {
// Test non-SEA mode
globals.isSea = false;
fs.readFileSync.mockClear();
fs.readFileSync.mockReturnValue(certFormat);
const nonSeaCerts = getProxyCertificates({
Certificate: `/cert${index}.crt`,
CertificateKey: `/key${index}.key`,
CertificateCA: `/ca${index}.crt`,
});
expect(nonSeaCerts.cert).toBe(certFormat);
// Test SEA mode
globals.isSea = true;
sea.getAsset.mockClear();
sea.getAsset.mockReturnValue(certFormat);
const seaCerts = getProxyCertificates({
Certificate: `/cert${index}.crt`,
CertificateKey: `/key${index}.key`,
CertificateCA: `/ca${index}.crt`,
});
expect(seaCerts.cert).toBe(certFormat);
});
});
});
describe('Certificate path handling', () => {
test('should handle different certificate path formats', () => {
const testPaths = [
{ Certificate: 'client.crt', CertificateKey: 'client.key', CertificateCA: 'ca.crt' },
{ Certificate: './certs/client.crt', CertificateKey: './certs/client.key', CertificateCA: './certs/ca.crt' },
{ Certificate: '/absolute/path/client.crt', CertificateKey: '/absolute/path/client.key', CertificateCA: '/absolute/path/ca.crt' },
{ Certificate: 'certs\\client.crt', CertificateKey: 'certs\\client.key', CertificateCA: 'certs\\ca.crt' }, // Windows paths
];
testPaths.forEach((paths, index) => {
// Test non-SEA mode
globals.isSea = false;
fs.readFileSync.mockClear();
fs.readFileSync.mockReturnValue('dummy-cert');
getProxyCertificates(paths);
expect(fs.readFileSync).toHaveBeenCalledWith(paths.Certificate);
expect(fs.readFileSync).toHaveBeenCalledWith(paths.CertificateKey);
expect(fs.readFileSync).toHaveBeenCalledWith(paths.CertificateCA);
// Test SEA mode
globals.isSea = true;
sea.getAsset.mockClear();
sea.getAsset.mockReturnValue('dummy-cert');
getHealthCertificates(paths);
expect(sea.getAsset).toHaveBeenCalledWith(paths.Certificate, 'utf8');
expect(sea.getAsset).toHaveBeenCalledWith(paths.CertificateKey, 'utf8');
expect(sea.getAsset).toHaveBeenCalledWith(paths.CertificateCA, 'utf8');
});
});
});
describe('Error handling consistency', () => {
test('should provide consistent error handling patterns across modes', () => {
// Non-SEA mode error
globals.isSea = false;
fs.readFileSync.mockImplementation(() => {
throw new Error('Permission denied');
});
expect(() => getProxyCertificates(mockCertificateOptions)).toThrow('Permission denied');
// SEA mode should not throw for missing assets, but return undefined
globals.isSea = true;
sea.getAsset.mockClear();
sea.getAsset.mockReturnValue(undefined);
const certificates = getHealthCertificates(mockCertificateOptions);
expect(certificates.cert).toBeUndefined();
expect(certificates.key).toBeUndefined();
expect(certificates.ca).toBeUndefined();
});
});
describe('Integration with TLS configuration', () => {
test('should produce certificates compatible with HTTPS agent configuration', () => {
const mockHttpsAgentOptions = {};
// Test non-SEA mode
globals.isSea = false;
fs.readFileSync.mockClear();
fs.readFileSync
.mockReturnValueOnce(mockCertData)
.mockReturnValueOnce(mockKeyData)
.mockReturnValueOnce(mockCaData);
const nonSeaCerts = getProxyCertificates(mockCertificateOptions);
// Verify that certificate structure is suitable for HTTPS agent
expect(typeof nonSeaCerts.cert).toBe('string');
expect(typeof nonSeaCerts.key).toBe('string');
expect(typeof nonSeaCerts.ca).toBe('string');
// These should be assignable to HTTPS agent options
Object.assign(mockHttpsAgentOptions, nonSeaCerts);
expect(mockHttpsAgentOptions.cert).toBe(mockCertData);
expect(mockHttpsAgentOptions.key).toBe(mockKeyData);
expect(mockHttpsAgentOptions.ca).toBe(mockCaData);
// Test SEA mode
globals.isSea = true;
sea.getAsset.mockClear();
sea.getAsset
.mockReturnValueOnce(mockCertData)
.mockReturnValueOnce(mockKeyData)
.mockReturnValueOnce(mockCaData);
const seaCerts = getHealthCertificates(mockCertificateOptions);
// Should produce identical structure
expect(seaCerts).toEqual(nonSeaCerts);
});
});
});

View File

@@ -205,4 +205,62 @@ describe('SEA vs non-SEA file interactions integration tests', () => {
expect(encoding2).toBeUndefined();
});
test('should handle certificate file loading patterns in both modes', () => {
// Certificate file loading should work in both modes
const certificatePaths = {
Certificate: '/path/to/client.crt',
CertificateKey: '/path/to/client.key',
CertificateCA: '/path/to/ca.crt'
};
// Non-SEA mode: should use filesystem
mockGlobals.isSea = false;
const shouldUseFilesystem = !mockGlobals.isSea;
expect(shouldUseFilesystem).toBe(true);
// Mock filesystem certificate reading
mockFs.readFileSync
.mockReturnValueOnce('-----BEGIN CERTIFICATE-----\ncert data...')
.mockReturnValueOnce('-----BEGIN PRIVATE KEY-----\nkey data...')
.mockReturnValueOnce('-----BEGIN CERTIFICATE-----\nca data...');
if (!mockGlobals.isSea) {
const certs = {
cert: mockFs.readFileSync(certificatePaths.Certificate),
key: mockFs.readFileSync(certificatePaths.CertificateKey),
ca: mockFs.readFileSync(certificatePaths.CertificateCA)
};
expect(certs.cert).toBe('-----BEGIN CERTIFICATE-----\ncert data...');
expect(certs.key).toBe('-----BEGIN PRIVATE KEY-----\nkey data...');
expect(certs.ca).toBe('-----BEGIN CERTIFICATE-----\nca data...');
}
// SEA mode: should use assets
mockGlobals.isSea = true;
const shouldUseAssets = mockGlobals.isSea;
expect(shouldUseAssets).toBe(true);
// Mock SEA asset certificate reading
const mockSeaGetAsset = jest.fn()
.mockReturnValueOnce('-----BEGIN CERTIFICATE-----\ncert data...')
.mockReturnValueOnce('-----BEGIN PRIVATE KEY-----\nkey data...')
.mockReturnValueOnce('-----BEGIN CERTIFICATE-----\nca data...');
if (mockGlobals.isSea) {
const certs = {
cert: mockSeaGetAsset(certificatePaths.Certificate, 'utf8'),
key: mockSeaGetAsset(certificatePaths.CertificateKey, 'utf8'),
ca: mockSeaGetAsset(certificatePaths.CertificateCA, 'utf8')
};
expect(certs.cert).toBe('-----BEGIN CERTIFICATE-----\ncert data...');
expect(certs.key).toBe('-----BEGIN PRIVATE KEY-----\nkey data...');
expect(certs.ca).toBe('-----BEGIN CERTIFICATE-----\nca data...');
expect(mockSeaGetAsset).toHaveBeenCalledWith('/path/to/client.crt', 'utf8');
expect(mockSeaGetAsset).toHaveBeenCalledWith('/path/to/client.key', 'utf8');
expect(mockSeaGetAsset).toHaveBeenCalledWith('/path/to/ca.crt', 'utf8');
}
});
});

View File

@@ -6,6 +6,7 @@ import path from 'path';
import https from 'https';
import fs from 'fs';
import axios from 'axios';
import sea from './sea-wrapper.js';
import globals from '../globals.js';
import { postHealthMetricsToInfluxdb } from './post-to-influxdb.js';
@@ -16,7 +17,7 @@ import { getServerTags } from './servertags.js';
import { saveHealthMetricsToPrometheus } from './prom-client.js';
/**
* Loads TLS certificates from the filesystem based on the provided options.
* Loads TLS certificates from the filesystem or SEA assets based on the provided options.
*
* @param {object} options - Certificate options
* @param {string} options.Certificate - Path to the client certificate file
@@ -25,12 +26,20 @@ import { saveHealthMetricsToPrometheus } from './prom-client.js';
* @param {string} [options.CertificatePassphrase] - Optional passphrase for the certificate
* @returns {object} Object containing cert, key, and ca properties with certificate contents
*/
function getCertificates(options) {
export function getCertificates(options) {
const certificate = {};
certificate.cert = fs.readFileSync(options.Certificate);
certificate.key = fs.readFileSync(options.CertificateKey);
certificate.ca = fs.readFileSync(options.CertificateCA);
if (globals.isSea) {
// In SEA mode, get certificates from embedded assets
certificate.cert = sea.getAsset(options.Certificate, 'utf8');
certificate.key = sea.getAsset(options.CertificateKey, 'utf8');
certificate.ca = sea.getAsset(options.CertificateCA, 'utf8');
} else {
// In non-SEA mode, read certificates from filesystem
certificate.cert = fs.readFileSync(options.Certificate);
certificate.key = fs.readFileSync(options.CertificateKey);
certificate.ca = fs.readFileSync(options.CertificateCA);
}
return certificate;
}

View File

@@ -7,6 +7,7 @@ import fs from 'fs';
import path from 'path';
import axios from 'axios';
import { Point } from '@influxdata/influxdb-client';
import sea from './sea-wrapper.js';
import globals from '../globals.js';
import { postProxySessionsToInfluxdb } from './post-to-influxdb.js';
@@ -16,7 +17,7 @@ import { getServerTags } from './servertags.js';
import { saveUserSessionMetricsToPrometheus } from './prom-client.js';
/**
* Loads TLS certificates from the filesystem based on the provided options.
* Loads TLS certificates from the filesystem or SEA assets based on the provided options.
*
* @param {object} options - Certificate options
* @param {string} options.Certificate - Path to the client certificate file
@@ -24,12 +25,20 @@ import { saveUserSessionMetricsToPrometheus } from './prom-client.js';
* @param {string} options.CertificateCA - Path to the certificate authority file
* @returns {object} Object containing cert, key, and ca properties with certificate contents
*/
function getCertificates(options) {
export function getCertificates(options) {
const certificate = {};
certificate.cert = fs.readFileSync(options.Certificate);
certificate.key = fs.readFileSync(options.CertificateKey);
certificate.ca = fs.readFileSync(options.CertificateCA);
if (globals.isSea) {
// In SEA mode, get certificates from embedded assets
certificate.cert = sea.getAsset(options.Certificate, 'utf8');
certificate.key = sea.getAsset(options.CertificateKey, 'utf8');
certificate.ca = sea.getAsset(options.CertificateCA, 'utf8');
} else {
// In non-SEA mode, read certificates from filesystem
certificate.cert = fs.readFileSync(options.Certificate);
certificate.key = fs.readFileSync(options.CertificateKey);
certificate.ca = fs.readFileSync(options.CertificateCA);
}
return certificate;
}