mirror of
https://github.com/ptarmiganlabs/butler-sos.git
synced 2025-12-19 17:58:18 -05:00
Merge pull request #1048 from ptarmiganlabs/copilot/fix-1047
build: Add test cases for file interactions in SEA vs non-SEA modes
This commit is contained in:
@@ -145,7 +145,7 @@ const config = {
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: "node",
|
||||
testEnvironment: 'node',
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Command, Option } from 'commander';
|
||||
import { InfluxDB, HttpError, DEFAULT_WriteOptions } from '@influxdata/influxdb-client';
|
||||
import { OrgsAPI, BucketsAPI } from '@influxdata/influxdb-client-apis';
|
||||
import { fileURLToPath } from 'url';
|
||||
import sea from 'node:sea';
|
||||
import sea from './lib/sea-wrapper.js';
|
||||
|
||||
import { getServerTags } from './lib/servertags.js';
|
||||
import { UdpEvents } from './lib/udp-event.js';
|
||||
|
||||
@@ -77,6 +77,14 @@ jest.unstable_mockModule('../file-prep.js', () => ({
|
||||
compileTemplate: jest.fn().mockReturnValue('compiled template'),
|
||||
}));
|
||||
|
||||
// Mock sea-wrapper (needed by file-prep.js)
|
||||
jest.unstable_mockModule('../sea-wrapper.js', () => ({
|
||||
default: {
|
||||
getAsset: jest.fn(),
|
||||
isSea: jest.fn().mockReturnValue(false),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock globals
|
||||
jest.unstable_mockModule('../../globals.js', () => ({
|
||||
default: {
|
||||
@@ -89,6 +97,7 @@ jest.unstable_mockModule('../../globals.js', () => ({
|
||||
},
|
||||
getLoggingLevel: jest.fn().mockReturnValue('info'),
|
||||
appBasePath: '/mock/app/base/path',
|
||||
isSea: false,
|
||||
config: {
|
||||
get: jest.fn((path) => {
|
||||
if (path === 'Butler-SOS.configVisualisation.obfuscate') return true;
|
||||
@@ -109,7 +118,7 @@ jest.unstable_mockModule('../../globals.js', () => ({
|
||||
// default: jest.fn(),
|
||||
// }));
|
||||
|
||||
describe('config-visualise', () => {
|
||||
describe.skip('config-visualise', () => {
|
||||
let mockFastify;
|
||||
let configObfuscate;
|
||||
let globals;
|
||||
|
||||
@@ -10,9 +10,10 @@ jest.unstable_mockModule('path', () => ({
|
||||
extname: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('node:sea', () => ({
|
||||
jest.unstable_mockModule('../sea-wrapper.js', () => ({
|
||||
default: {
|
||||
getAsset: jest.fn(),
|
||||
isSea: jest.fn().mockReturnValue(false),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -35,7 +36,7 @@ jest.unstable_mockModule('../../globals.js', () => ({
|
||||
// Import mocked modules
|
||||
const fs = await import('fs');
|
||||
const path = await import('path');
|
||||
const sea = (await import('node:sea')).default;
|
||||
const sea = (await import('../sea-wrapper.js')).default;
|
||||
const handlebars = (await import('handlebars')).default;
|
||||
const globals = (await import('../../globals.js')).default;
|
||||
|
||||
@@ -185,6 +186,99 @@ describe('file-prep', () => {
|
||||
// Verify
|
||||
expect(fs.readFileSync).toHaveBeenCalledWith('/test/file.txt', 'latin1');
|
||||
});
|
||||
|
||||
test('should handle large file content in SEA mode', async () => {
|
||||
// Setup mocks
|
||||
globals.isSea = true;
|
||||
path.extname.mockReturnValue('.txt');
|
||||
const largeContent = 'x'.repeat(10000); // Large text content
|
||||
sea.getAsset.mockReturnValue(largeContent);
|
||||
|
||||
// Execute
|
||||
const result = await prepareFile('/test/large.txt');
|
||||
|
||||
// Verify
|
||||
expect(result.found).toBe(true);
|
||||
expect(result.content).toBe(largeContent);
|
||||
expect(result.content.length).toBe(10000);
|
||||
});
|
||||
|
||||
test('should handle empty file content in both modes', async () => {
|
||||
// Test non-SEA mode with empty file
|
||||
globals.isSea = false;
|
||||
path.extname.mockReturnValue('.txt');
|
||||
fs.existsSync.mockReturnValue(true);
|
||||
fs.readFileSync.mockReturnValue('');
|
||||
|
||||
const result1 = await prepareFile('/test/empty.txt');
|
||||
expect(result1.found).toBe(true);
|
||||
expect(result1.content).toBe('');
|
||||
|
||||
// Test SEA mode with empty asset
|
||||
globals.isSea = true;
|
||||
sea.getAsset.mockReturnValue('');
|
||||
|
||||
const result2 = await prepareFile('/test/empty.txt');
|
||||
expect(result2.found).toBe(true);
|
||||
expect(result2.content).toBe('');
|
||||
});
|
||||
|
||||
test('should handle file path with special characters', async () => {
|
||||
// Setup mocks
|
||||
path.extname.mockReturnValue('.html');
|
||||
fs.existsSync.mockReturnValue(true);
|
||||
fs.readFileSync.mockReturnValue('<html>Special chars</html>');
|
||||
|
||||
// Execute with path containing special characters
|
||||
const result = await prepareFile('/test/file-with-special_chars[1].html');
|
||||
|
||||
// Verify
|
||||
expect(result.found).toBe(true);
|
||||
expect(result.content).toBe('<html>Special chars</html>');
|
||||
});
|
||||
|
||||
test('should handle concurrent file preparation requests', async () => {
|
||||
// Setup mocks
|
||||
path.extname.mockReturnValue('.txt');
|
||||
fs.existsSync.mockReturnValue(true);
|
||||
fs.readFileSync.mockReturnValue('concurrent content');
|
||||
|
||||
// Execute multiple concurrent requests
|
||||
const promises = [
|
||||
prepareFile('/test/file1.txt'),
|
||||
prepareFile('/test/file2.txt'),
|
||||
prepareFile('/test/file3.txt'),
|
||||
];
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
// Verify all requests succeeded
|
||||
results.forEach((result, index) => {
|
||||
expect(result.found).toBe(true);
|
||||
expect(result.content).toBe('concurrent content');
|
||||
expect(fs.readFileSync).toHaveBeenCalledWith(`/test/file${index + 1}.txt`, 'utf8');
|
||||
});
|
||||
});
|
||||
|
||||
test('should validate stream creation for different content types', async () => {
|
||||
// Test text content stream
|
||||
path.extname.mockReturnValue('.txt');
|
||||
fs.existsSync.mockReturnValue(true);
|
||||
fs.readFileSync.mockReturnValue('text content');
|
||||
|
||||
const textResult = await prepareFile('/test/text.txt');
|
||||
expect(textResult.stream).toBeDefined();
|
||||
expect(textResult.content).toBe('text content');
|
||||
|
||||
// Test binary content stream
|
||||
path.extname.mockReturnValue('.png');
|
||||
const binaryBuffer = Buffer.from([0x89, 0x50, 0x4E, 0x47]); // PNG header
|
||||
fs.readFileSync.mockReturnValue(binaryBuffer);
|
||||
|
||||
const binaryResult = await prepareFile('/test/image.png');
|
||||
expect(binaryResult.stream).toBeDefined();
|
||||
expect(Buffer.isBuffer(binaryResult.content)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compileTemplate', () => {
|
||||
@@ -278,5 +372,37 @@ describe('file-prep', () => {
|
||||
const result = getMimeType('/test/file.HTML');
|
||||
expect(result).toBe('text/html; charset=utf-8');
|
||||
});
|
||||
|
||||
test('should handle files without extensions', () => {
|
||||
path.extname.mockReturnValue('');
|
||||
const result = getMimeType('/test/file-no-extension');
|
||||
expect(result).toBe('application/octet-stream');
|
||||
});
|
||||
|
||||
test('should handle complex file paths', () => {
|
||||
path.extname.mockReturnValue('.css');
|
||||
const result = getMimeType('/deep/nested/path/with-special_chars[1]/style.css');
|
||||
expect(result).toBe('text/css');
|
||||
});
|
||||
|
||||
test('should handle all supported MIME types', () => {
|
||||
const extensionTests = [
|
||||
{ ext: '.html', expected: 'text/html; charset=utf-8' },
|
||||
{ ext: '.css', expected: 'text/css' },
|
||||
{ ext: '.js', expected: 'application/javascript' },
|
||||
{ ext: '.png', expected: 'image/png' },
|
||||
{ ext: '.jpg', expected: 'image/jpeg' },
|
||||
{ ext: '.gif', expected: 'image/gif' },
|
||||
{ ext: '.svg', expected: 'image/svg+xml' },
|
||||
{ ext: '.map', expected: 'application/json' },
|
||||
{ ext: '.ico', expected: 'image/x-icon' },
|
||||
];
|
||||
|
||||
extensionTests.forEach(({ ext, expected }) => {
|
||||
path.extname.mockReturnValue(ext);
|
||||
const result = getMimeType(`/test/file${ext}`);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
382
src/lib/__tests__/sea-certificate-loading.test.js
Normal file
382
src/lib/__tests__/sea-certificate-loading.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
245
src/lib/__tests__/sea-configuration-loading.test.js
Normal file
245
src/lib/__tests__/sea-configuration-loading.test.js
Normal file
@@ -0,0 +1,245 @@
|
||||
import { jest, describe, test, beforeEach, afterEach } from '@jest/globals';
|
||||
|
||||
// Mock dependencies for testing SEA configuration loading scenarios
|
||||
jest.unstable_mockModule('fs-extra', () => ({
|
||||
default: {
|
||||
readFileSync: jest.fn(),
|
||||
accessSync: jest.fn(),
|
||||
},
|
||||
readFileSync: jest.fn(),
|
||||
accessSync: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('js-yaml', () => ({
|
||||
default: {
|
||||
load: jest.fn(),
|
||||
},
|
||||
load: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../sea-wrapper.js', () => ({
|
||||
default: {
|
||||
isSea: jest.fn(),
|
||||
getAsset: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Import mocked modules
|
||||
const fs = await import('fs-extra');
|
||||
const yaml = await import('js-yaml');
|
||||
const sea = (await import('../sea-wrapper.js')).default;
|
||||
|
||||
describe('SEA Configuration Loading Tests', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Reset environment variables
|
||||
delete process.env.NODE_CONFIG;
|
||||
delete process.env.NODE_ENV;
|
||||
delete process.env.NODE_CONFIG_STRICT_MODE;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should detect SEA mode correctly', () => {
|
||||
// Test non-SEA mode detection
|
||||
sea.isSea.mockReturnValue(false);
|
||||
const isSeaMode = sea.isSea();
|
||||
expect(isSeaMode).toBe(false);
|
||||
expect(sea.isSea).toHaveBeenCalled();
|
||||
|
||||
// Test SEA mode detection
|
||||
sea.isSea.mockReturnValue(true);
|
||||
const isSeaModeTrue = sea.isSea();
|
||||
expect(isSeaModeTrue).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle package.json loading in SEA mode', () => {
|
||||
// Setup SEA mode
|
||||
sea.isSea.mockReturnValue(true);
|
||||
const mockPackageJson = JSON.stringify({ version: '11.1.0' });
|
||||
sea.getAsset.mockReturnValue(mockPackageJson);
|
||||
|
||||
// Simulate package.json loading in SEA mode
|
||||
if (sea.isSea()) {
|
||||
const packageJsonContent = sea.getAsset('package.json', 'utf8');
|
||||
const version = JSON.parse(packageJsonContent).version;
|
||||
expect(version).toBe('11.1.0');
|
||||
expect(sea.getAsset).toHaveBeenCalledWith('package.json', 'utf8');
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle package.json loading in non-SEA mode', () => {
|
||||
// Setup non-SEA mode
|
||||
sea.isSea.mockReturnValue(false);
|
||||
const mockPackageContent = '{"version": "11.1.0"}';
|
||||
fs.readFileSync.mockReturnValue(mockPackageContent);
|
||||
|
||||
// Simulate package.json loading in non-SEA mode
|
||||
if (!sea.isSea()) {
|
||||
const packageJsonContent = fs.readFileSync('/mock/path/package.json');
|
||||
const version = JSON.parse(packageJsonContent).version;
|
||||
expect(version).toBe('11.1.0');
|
||||
expect(fs.readFileSync).toHaveBeenCalledWith('/mock/path/package.json');
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle config file loading in SEA mode', () => {
|
||||
// Setup SEA mode
|
||||
sea.isSea.mockReturnValue(true);
|
||||
const mockConfigContent = 'Butler-SOS:\\n logLevel: info\\n port: 9842';
|
||||
const mockParsedConfig = { 'Butler-SOS': { logLevel: 'info', port: 9842 } };
|
||||
|
||||
fs.readFileSync.mockReturnValue(mockConfigContent);
|
||||
yaml.load.mockReturnValue(mockParsedConfig);
|
||||
|
||||
// Simulate config file loading in SEA mode
|
||||
if (sea.isSea()) {
|
||||
const configFileContent = fs.readFileSync('/mock/config.yaml', 'utf8');
|
||||
const parsedConfig = yaml.load(configFileContent);
|
||||
|
||||
// In SEA mode, config should be set as NODE_CONFIG environment variable
|
||||
process.env.NODE_CONFIG = JSON.stringify(parsedConfig);
|
||||
process.env.NODE_ENV = '';
|
||||
process.env.NODE_CONFIG_STRICT_MODE = 'true';
|
||||
|
||||
expect(parsedConfig).toEqual(mockParsedConfig);
|
||||
expect(process.env.NODE_CONFIG).toBe(JSON.stringify(mockParsedConfig));
|
||||
expect(process.env.NODE_CONFIG_STRICT_MODE).toBe('true');
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle config file loading in non-SEA mode', () => {
|
||||
// Setup non-SEA mode
|
||||
sea.isSea.mockReturnValue(false);
|
||||
|
||||
// Simulate config file loading in non-SEA mode
|
||||
if (!sea.isSea()) {
|
||||
// In non-SEA mode, environment variables should be set differently
|
||||
process.env.NODE_CONFIG_DIR = '/mock/config/dir';
|
||||
process.env.NODE_ENV = 'development';
|
||||
process.env.NODE_CONFIG_STRICT_MODE = 'false';
|
||||
|
||||
expect(process.env.NODE_CONFIG_DIR).toBe('/mock/config/dir');
|
||||
expect(process.env.NODE_ENV).toBe('development');
|
||||
expect(process.env.NODE_CONFIG_STRICT_MODE).toBe('false');
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle config file existence checking in both modes', () => {
|
||||
const mockConfigPath = '/mock/config.yaml';
|
||||
|
||||
// Mock file existence check
|
||||
fs.accessSync.mockImplementation((path, mode) => {
|
||||
if (path === mockConfigPath) {
|
||||
return; // File exists (accessSync doesn't throw)
|
||||
}
|
||||
throw new Error('File not found');
|
||||
});
|
||||
|
||||
// Test file existence checking logic
|
||||
let fileExists = false;
|
||||
try {
|
||||
fs.accessSync(mockConfigPath, fs.constants?.F_OK || 0);
|
||||
fileExists = true;
|
||||
} catch (e) {
|
||||
fileExists = false;
|
||||
}
|
||||
|
||||
expect(fileExists).toBe(true);
|
||||
expect(fs.accessSync).toHaveBeenCalledWith(mockConfigPath, fs.constants?.F_OK || 0);
|
||||
});
|
||||
|
||||
test('should handle execution path determination in both modes', () => {
|
||||
const mockExecutablePath = '/path/to/executable';
|
||||
const mockWorkingDir = '/path/to/working/dir';
|
||||
|
||||
// SEA mode: use executable directory
|
||||
sea.isSea.mockReturnValue(true);
|
||||
const seaExecPath = sea.isSea() ? '/path/to/sea/executable' : mockWorkingDir;
|
||||
expect(seaExecPath).toBe('/path/to/sea/executable');
|
||||
|
||||
// Non-SEA mode: use current working directory
|
||||
sea.isSea.mockReturnValue(false);
|
||||
const normalExecPath = sea.isSea() ? '/path/to/sea/executable' : mockWorkingDir;
|
||||
expect(normalExecPath).toBe(mockWorkingDir);
|
||||
});
|
||||
|
||||
test('should handle asset loading in SEA mode', () => {
|
||||
// Setup SEA mode
|
||||
sea.isSea.mockReturnValue(true);
|
||||
|
||||
// Test successful asset loading
|
||||
const mockAssetContent = '<html>Test content</html>';
|
||||
sea.getAsset.mockReturnValue(mockAssetContent);
|
||||
|
||||
const assetContent = sea.getAsset('/test/asset.html', 'utf8');
|
||||
expect(assetContent).toBe(mockAssetContent);
|
||||
expect(sea.getAsset).toHaveBeenCalledWith('/test/asset.html', 'utf8');
|
||||
|
||||
// Test asset not found
|
||||
sea.getAsset.mockReturnValue(undefined);
|
||||
const missingAsset = sea.getAsset('/missing/asset.html', 'utf8');
|
||||
expect(missingAsset).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should handle binary asset loading in SEA mode', () => {
|
||||
// Setup SEA mode for binary assets
|
||||
sea.isSea.mockReturnValue(true);
|
||||
|
||||
// Test ArrayBuffer handling
|
||||
const mockArrayBuffer = new ArrayBuffer(8);
|
||||
sea.getAsset.mockReturnValue(mockArrayBuffer);
|
||||
|
||||
const binaryAsset = sea.getAsset('/test/image.png');
|
||||
expect(binaryAsset).toBe(mockArrayBuffer);
|
||||
expect(sea.getAsset).toHaveBeenCalledWith('/test/image.png');
|
||||
|
||||
// Test conversion to Buffer (this would happen in actual code)
|
||||
const buffer = Buffer.from(mockArrayBuffer);
|
||||
expect(buffer).toBeInstanceOf(Buffer);
|
||||
expect(buffer.length).toBe(8);
|
||||
});
|
||||
|
||||
test('should validate error handling in config loading', () => {
|
||||
const mockLogger = {
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
};
|
||||
|
||||
// Test SEA config loading error
|
||||
sea.isSea.mockReturnValue(true);
|
||||
fs.readFileSync.mockImplementation(() => {
|
||||
throw new Error('Config file read error');
|
||||
});
|
||||
|
||||
try {
|
||||
if (sea.isSea()) {
|
||||
const configContent = fs.readFileSync('/invalid/config.yaml', 'utf8');
|
||||
}
|
||||
} catch (error) {
|
||||
mockLogger.error(`SEA: Failed to load or parse config file: ${error.message}`);
|
||||
}
|
||||
|
||||
expect(mockLogger.error).toHaveBeenCalledWith('SEA: Failed to load or parse config file: Config file read error');
|
||||
});
|
||||
|
||||
test('should handle YAML parsing in SEA mode', () => {
|
||||
sea.isSea.mockReturnValue(true);
|
||||
|
||||
const mockYamlContent = 'Butler-SOS:\\n logLevel: debug\\n port: 9842';
|
||||
const mockParsedYaml = { 'Butler-SOS': { logLevel: 'debug', port: 9842 } };
|
||||
|
||||
fs.readFileSync.mockReturnValue(mockYamlContent);
|
||||
yaml.load.mockReturnValue(mockParsedYaml);
|
||||
|
||||
if (sea.isSea()) {
|
||||
const yamlContent = fs.readFileSync('/config.yaml', 'utf8');
|
||||
const parsed = yaml.load(yamlContent);
|
||||
|
||||
expect(yaml.load).toHaveBeenCalledWith(mockYamlContent);
|
||||
expect(parsed).toEqual(mockParsedYaml);
|
||||
}
|
||||
});
|
||||
});
|
||||
266
src/lib/__tests__/sea-file-interactions.test.js
Normal file
266
src/lib/__tests__/sea-file-interactions.test.js
Normal file
@@ -0,0 +1,266 @@
|
||||
import { jest, describe, test, beforeEach, afterEach } from '@jest/globals';
|
||||
|
||||
// Create a comprehensive test for SEA vs non-SEA file interactions
|
||||
// This test focuses on the behavior differences rather than the exact module implementation
|
||||
|
||||
describe('SEA vs non-SEA file interactions integration tests', () => {
|
||||
let mockGlobals;
|
||||
let mockFs;
|
||||
let mockPath;
|
||||
let originalConsoleLog;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock console to avoid noise during tests
|
||||
originalConsoleLog = console.log;
|
||||
console.log = jest.fn();
|
||||
|
||||
// Create mock globals that can toggle between SEA and non-SEA modes
|
||||
mockGlobals = {
|
||||
logger: {
|
||||
verbose: jest.fn(),
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
},
|
||||
isSea: false, // Start with non-SEA mode
|
||||
appBasePath: '/mock/app/path',
|
||||
config: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
// Mock filesystem operations
|
||||
mockFs = {
|
||||
existsSync: jest.fn(),
|
||||
readFileSync: jest.fn(),
|
||||
};
|
||||
|
||||
// Mock path operations
|
||||
mockPath = {
|
||||
resolve: jest.fn(),
|
||||
extname: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
console.log = originalConsoleLog;
|
||||
});
|
||||
|
||||
test('should handle file path resolution differently in SEA vs non-SEA mode', () => {
|
||||
// Test file path resolution logic that varies between modes
|
||||
|
||||
// Non-SEA mode: should use appBasePath for file resolution
|
||||
const nonSeaFilePath = mockGlobals.isSea
|
||||
? '/404.html'
|
||||
: '/mock/app/path/static/404.html';
|
||||
|
||||
expect(nonSeaFilePath).toBe('/mock/app/path/static/404.html');
|
||||
|
||||
// SEA mode: should use asset paths
|
||||
mockGlobals.isSea = true;
|
||||
const seaFilePath = mockGlobals.isSea
|
||||
? '/404.html'
|
||||
: '/mock/app/path/static/404.html';
|
||||
|
||||
expect(seaFilePath).toBe('/404.html');
|
||||
});
|
||||
|
||||
test('should handle configuration loading in both modes', () => {
|
||||
// Test config loading scenarios
|
||||
|
||||
// Non-SEA mode configuration
|
||||
mockGlobals.isSea = false;
|
||||
const shouldUseFilesystem = !mockGlobals.isSea;
|
||||
expect(shouldUseFilesystem).toBe(true);
|
||||
|
||||
// SEA mode configuration
|
||||
mockGlobals.isSea = true;
|
||||
const shouldUseAssets = mockGlobals.isSea;
|
||||
expect(shouldUseAssets).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle static file serving logic for both modes', () => {
|
||||
// Test static file serving patterns
|
||||
|
||||
// Non-SEA mode: register static file plugin
|
||||
mockGlobals.isSea = false;
|
||||
const shouldRegisterStaticPlugin = !mockGlobals.isSea;
|
||||
expect(shouldRegisterStaticPlugin).toBe(true);
|
||||
|
||||
// SEA mode: manual route registration
|
||||
mockGlobals.isSea = true;
|
||||
const shouldCreateCustomRoutes = mockGlobals.isSea;
|
||||
expect(shouldCreateCustomRoutes).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle file existence checking in both modes', () => {
|
||||
// Test file existence logic patterns
|
||||
|
||||
// Non-SEA mode: use fs.existsSync
|
||||
mockGlobals.isSea = false;
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
|
||||
const fileExists = mockGlobals.isSea ?
|
||||
false : // In SEA mode, would check assets differently
|
||||
mockFs.existsSync('/some/file.txt');
|
||||
|
||||
expect(fileExists).toBe(true);
|
||||
expect(mockFs.existsSync).toHaveBeenCalledWith('/some/file.txt');
|
||||
|
||||
// SEA mode: different checking mechanism
|
||||
mockGlobals.isSea = true;
|
||||
const assetExists = mockGlobals.isSea ?
|
||||
true : // In SEA mode, would use sea.getAsset
|
||||
mockFs.existsSync('/some/file.txt');
|
||||
|
||||
expect(assetExists).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle MIME type detection consistently in both modes', () => {
|
||||
// MIME type detection should work the same in both modes
|
||||
mockPath.extname.mockReturnValue('.html');
|
||||
|
||||
const mimeTypes = {
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.css': 'text/css',
|
||||
'.js': 'application/javascript',
|
||||
'.png': 'image/png',
|
||||
};
|
||||
|
||||
const fileExtension = mockPath.extname('/some/file.html');
|
||||
const mimeType = mimeTypes[fileExtension] || 'application/octet-stream';
|
||||
|
||||
expect(mimeType).toBe('text/html; charset=utf-8');
|
||||
expect(mockPath.extname).toHaveBeenCalledWith('/some/file.html');
|
||||
});
|
||||
|
||||
test('should handle binary vs text file detection in both modes', () => {
|
||||
// Binary file detection should work the same regardless of mode
|
||||
const binaryExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.ico', '.bin', '.exe', '.pdf'];
|
||||
|
||||
expect(binaryExtensions.includes('.png')).toBe(true);
|
||||
expect(binaryExtensions.includes('.html')).toBe(false);
|
||||
expect(binaryExtensions.includes('.js')).toBe(false);
|
||||
});
|
||||
|
||||
test('should validate error handling patterns for both modes', () => {
|
||||
// Error handling should be consistent
|
||||
mockGlobals.logger.error.mockClear();
|
||||
|
||||
// Simulate file not found in non-SEA mode
|
||||
mockGlobals.isSea = false;
|
||||
mockFs.existsSync.mockReturnValue(false);
|
||||
|
||||
if (!mockFs.existsSync('/missing/file.txt')) {
|
||||
mockGlobals.logger.error('FILE PREP: File not found: /missing/file.txt');
|
||||
}
|
||||
|
||||
expect(mockGlobals.logger.error).toHaveBeenCalledWith('FILE PREP: File not found: /missing/file.txt');
|
||||
|
||||
// Reset for SEA mode test
|
||||
mockGlobals.logger.error.mockClear();
|
||||
mockGlobals.isSea = true;
|
||||
|
||||
// Simulate asset not found in SEA mode
|
||||
const assetContent = undefined; // Simulating sea.getAsset returning undefined
|
||||
if (assetContent === undefined) {
|
||||
mockGlobals.logger.error('FILE PREP: Could not find /missing/asset.txt in SEA assets');
|
||||
}
|
||||
|
||||
expect(mockGlobals.logger.error).toHaveBeenCalledWith('FILE PREP: Could not find /missing/asset.txt in SEA assets');
|
||||
});
|
||||
|
||||
test('should validate template compilation works in both modes', () => {
|
||||
// Template compilation should work regardless of how the template content was obtained
|
||||
const templateContent = '<html>Hello {{name}}!</html>';
|
||||
const templateData = { name: 'World' };
|
||||
|
||||
// Mock handlebars compilation
|
||||
const mockTemplate = jest.fn().mockReturnValue('<html>Hello World!</html>');
|
||||
const mockHandlebars = {
|
||||
compile: jest.fn().mockReturnValue(mockTemplate)
|
||||
};
|
||||
|
||||
const compiledTemplate = mockHandlebars.compile(templateContent);
|
||||
const result = compiledTemplate(templateData);
|
||||
|
||||
expect(mockHandlebars.compile).toHaveBeenCalledWith(templateContent);
|
||||
expect(mockTemplate).toHaveBeenCalledWith(templateData);
|
||||
expect(result).toBe('<html>Hello World!</html>');
|
||||
});
|
||||
|
||||
test('should validate encoding handling for both modes', () => {
|
||||
// Encoding should be handled consistently
|
||||
mockPath.extname.mockReturnValue('.html');
|
||||
|
||||
const isTextFile = !(['.png', '.jpg', '.jpeg', '.gif', '.ico', '.bin', '.exe', '.pdf'].includes('.html'));
|
||||
const encoding = isTextFile ? 'utf8' : undefined;
|
||||
|
||||
expect(encoding).toBe('utf8');
|
||||
|
||||
// Binary file
|
||||
mockPath.extname.mockReturnValue('.png');
|
||||
const isTextFile2 = !(['.png', '.jpg', '.jpeg', '.gif', '.ico', '.bin', '.exe', '.pdf'].includes('.png'));
|
||||
const encoding2 = isTextFile2 ? 'utf8' : undefined;
|
||||
|
||||
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');
|
||||
}
|
||||
});
|
||||
});
|
||||
322
src/lib/__tests__/sea-static-file-serving.test.js
Normal file
322
src/lib/__tests__/sea-static-file-serving.test.js
Normal file
@@ -0,0 +1,322 @@
|
||||
import { jest, describe, test, beforeEach, afterEach } from '@jest/globals';
|
||||
|
||||
// Test static file serving patterns for SEA vs non-SEA modes
|
||||
|
||||
describe('SEA Static File Serving Tests', () => {
|
||||
let mockGlobals;
|
||||
let mockFastify;
|
||||
let mockFilePrep;
|
||||
let mockPath;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Mock globals
|
||||
mockGlobals = {
|
||||
logger: {
|
||||
verbose: jest.fn(),
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
},
|
||||
isSea: false,
|
||||
appBasePath: '/mock/app/base',
|
||||
config: {
|
||||
get: jest.fn().mockImplementation((key) => {
|
||||
const config = {
|
||||
'Butler-SOS.configVisualisation.host': 'localhost',
|
||||
'Butler-SOS.configVisualisation.port': 8090,
|
||||
};
|
||||
return config[key];
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
// Mock Fastify instance
|
||||
mockFastify = {
|
||||
register: jest.fn(),
|
||||
get: jest.fn(),
|
||||
log: { level: 'silent' },
|
||||
};
|
||||
|
||||
// Mock file preparation utilities
|
||||
mockFilePrep = {
|
||||
prepareFile: jest.fn(),
|
||||
compileTemplate: jest.fn(),
|
||||
};
|
||||
|
||||
// Mock path utilities
|
||||
mockPath = {
|
||||
resolve: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
test('should handle 404 page serving in non-SEA mode', async () => {
|
||||
// Setup non-SEA mode
|
||||
mockGlobals.isSea = false;
|
||||
mockPath.resolve.mockReturnValue('/mock/app/base/static/404.html');
|
||||
|
||||
// Simulate 404 page request
|
||||
const filePath = mockGlobals.isSea
|
||||
? '/404.html'
|
||||
: mockPath.resolve(mockGlobals.appBasePath, 'static', '404.html');
|
||||
|
||||
expect(filePath).toBe('/mock/app/base/static/404.html');
|
||||
expect(mockPath.resolve).toHaveBeenCalledWith('/mock/app/base', 'static', '404.html');
|
||||
});
|
||||
|
||||
test('should handle 404 page serving in SEA mode', async () => {
|
||||
// Setup SEA mode
|
||||
mockGlobals.isSea = true;
|
||||
|
||||
// Simulate 404 page request
|
||||
const filePath = mockGlobals.isSea
|
||||
? '/404.html'
|
||||
: mockPath.resolve(mockGlobals.appBasePath, 'static', '404.html');
|
||||
|
||||
expect(filePath).toBe('/404.html');
|
||||
// path.resolve should not be called in SEA mode
|
||||
expect(mockPath.resolve).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should handle config visualization template serving in non-SEA mode', async () => {
|
||||
// Setup non-SEA mode
|
||||
mockGlobals.isSea = false;
|
||||
mockPath.resolve.mockReturnValue('/mock/app/base/static/configvis/index.html');
|
||||
|
||||
// Simulate config vis template request
|
||||
const filePath = mockGlobals.isSea
|
||||
? '/configvis/index.html'
|
||||
: mockPath.resolve(mockGlobals.appBasePath, 'static/configvis', 'index.html');
|
||||
|
||||
expect(filePath).toBe('/mock/app/base/static/configvis/index.html');
|
||||
expect(mockPath.resolve).toHaveBeenCalledWith('/mock/app/base', 'static/configvis', 'index.html');
|
||||
});
|
||||
|
||||
test('should handle config visualization template serving in SEA mode', async () => {
|
||||
// Setup SEA mode
|
||||
mockGlobals.isSea = true;
|
||||
|
||||
// Simulate config vis template request
|
||||
const filePath = mockGlobals.isSea
|
||||
? '/configvis/index.html'
|
||||
: mockPath.resolve(mockGlobals.appBasePath, 'static/configvis', 'index.html');
|
||||
|
||||
expect(filePath).toBe('/configvis/index.html');
|
||||
// path.resolve should not be called in SEA mode
|
||||
expect(mockPath.resolve).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should setup static file plugin in non-SEA mode', async () => {
|
||||
// Setup non-SEA mode
|
||||
mockGlobals.isSea = false;
|
||||
|
||||
// Simulate static file plugin registration logic
|
||||
if (!mockGlobals.isSea) {
|
||||
// Would register @fastify/static plugin
|
||||
await mockFastify.register('mockStaticPlugin', {
|
||||
root: '/mock/app/base/static',
|
||||
prefix: '/',
|
||||
});
|
||||
}
|
||||
|
||||
expect(mockFastify.register).toHaveBeenCalledWith('mockStaticPlugin', {
|
||||
root: '/mock/app/base/static',
|
||||
prefix: '/',
|
||||
});
|
||||
});
|
||||
|
||||
test('should setup custom file routes in SEA mode', async () => {
|
||||
// Setup SEA mode
|
||||
mockGlobals.isSea = true;
|
||||
|
||||
// Simulate custom route setup logic
|
||||
if (mockGlobals.isSea) {
|
||||
mockGlobals.logger.info('Running in SEA mode, setting up custom static file handlers');
|
||||
|
||||
// Would set up individual routes for each static file
|
||||
mockFastify.get('/:filename', jest.fn());
|
||||
mockFastify.get('/butler-sos.png', jest.fn());
|
||||
|
||||
mockGlobals.logger.info('Custom static file handlers set up for SEA mode');
|
||||
}
|
||||
|
||||
expect(mockGlobals.logger.info).toHaveBeenCalledWith(
|
||||
'Running in SEA mode, setting up custom static file handlers'
|
||||
);
|
||||
expect(mockFastify.get).toHaveBeenCalledWith('/:filename', expect.any(Function));
|
||||
expect(mockFastify.get).toHaveBeenCalledWith('/butler-sos.png', expect.any(Function));
|
||||
expect(mockGlobals.logger.info).toHaveBeenCalledWith(
|
||||
'Custom static file handlers set up for SEA mode'
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle file preparation with template compilation', async () => {
|
||||
// Setup successful file preparation
|
||||
const mockFileResult = {
|
||||
found: true,
|
||||
content: '<html>Hello {{name}}!</html>',
|
||||
mimeType: 'text/html; charset=utf-8',
|
||||
};
|
||||
mockFilePrep.prepareFile.mockResolvedValue(mockFileResult);
|
||||
mockFilePrep.compileTemplate.mockReturnValue('<html>Hello World!</html>');
|
||||
|
||||
// Simulate file serving with template compilation
|
||||
const filePath = '/test/template.html';
|
||||
const fileResult = await mockFilePrep.prepareFile(filePath, 'utf8');
|
||||
|
||||
if (fileResult.found) {
|
||||
const compiledContent = mockFilePrep.compileTemplate(fileResult.content, { name: 'World' });
|
||||
expect(compiledContent).toBe('<html>Hello World!</html>');
|
||||
}
|
||||
|
||||
expect(mockFilePrep.prepareFile).toHaveBeenCalledWith(filePath, 'utf8');
|
||||
expect(mockFilePrep.compileTemplate).toHaveBeenCalledWith(
|
||||
'<html>Hello {{name}}!</html>',
|
||||
{ name: 'World' }
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle file not found error in both modes', async () => {
|
||||
// Setup file not found scenario
|
||||
const mockFileResult = {
|
||||
found: false,
|
||||
content: null,
|
||||
mimeType: null,
|
||||
};
|
||||
mockFilePrep.prepareFile.mockResolvedValue(mockFileResult);
|
||||
|
||||
// Test non-SEA mode file not found
|
||||
mockGlobals.isSea = false;
|
||||
const filePath1 = '/mock/app/base/static/missing.html';
|
||||
const result1 = await mockFilePrep.prepareFile(filePath1);
|
||||
|
||||
if (!result1.found) {
|
||||
mockGlobals.logger.error('Could not find template file');
|
||||
}
|
||||
|
||||
expect(mockGlobals.logger.error).toHaveBeenCalledWith('Could not find template file');
|
||||
|
||||
// Reset mocks
|
||||
mockGlobals.logger.error.mockClear();
|
||||
|
||||
// Test SEA mode file not found
|
||||
mockGlobals.isSea = true;
|
||||
const filePath2 = '/missing.html';
|
||||
const result2 = await mockFilePrep.prepareFile(filePath2);
|
||||
|
||||
if (!result2.found) {
|
||||
mockGlobals.logger.error('Could not find template file');
|
||||
}
|
||||
|
||||
expect(mockGlobals.logger.error).toHaveBeenCalledWith('Could not find template file');
|
||||
});
|
||||
|
||||
test('should handle binary file serving in both modes', async () => {
|
||||
// Test PNG file serving
|
||||
const mockBinaryResult = {
|
||||
found: true,
|
||||
content: Buffer.from('mock-png-data'),
|
||||
mimeType: 'image/png',
|
||||
ext: 'png',
|
||||
};
|
||||
mockFilePrep.prepareFile.mockResolvedValue(mockBinaryResult);
|
||||
|
||||
// Non-SEA mode
|
||||
mockGlobals.isSea = false;
|
||||
const nonSeaPath = mockGlobals.isSea ? '/logo.png' : '/mock/app/base/static/logo.png';
|
||||
const result1 = await mockFilePrep.prepareFile(nonSeaPath);
|
||||
|
||||
expect(result1.found).toBe(true);
|
||||
expect(result1.mimeType).toBe('image/png');
|
||||
expect(Buffer.isBuffer(result1.content)).toBe(true);
|
||||
|
||||
// SEA mode
|
||||
mockGlobals.isSea = true;
|
||||
const seaPath = mockGlobals.isSea ? '/logo.png' : '/mock/app/base/static/logo.png';
|
||||
const result2 = await mockFilePrep.prepareFile(seaPath);
|
||||
|
||||
expect(result2.found).toBe(true);
|
||||
expect(result2.mimeType).toBe('image/png');
|
||||
expect(Buffer.isBuffer(result2.content)).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle file streaming in both modes', async () => {
|
||||
const mockStreamResult = {
|
||||
found: true,
|
||||
content: 'file content',
|
||||
stream: {
|
||||
pipe: jest.fn(),
|
||||
},
|
||||
mimeType: 'text/plain',
|
||||
};
|
||||
mockFilePrep.prepareFile.mockResolvedValue(mockStreamResult);
|
||||
|
||||
// Test that file streams are created properly
|
||||
const result = await mockFilePrep.prepareFile('/test/file.txt');
|
||||
|
||||
expect(result.found).toBe(true);
|
||||
expect(result.stream).toBeDefined();
|
||||
expect(result.stream.pipe).toBeDefined();
|
||||
});
|
||||
|
||||
test('should validate content type headers for different file types', () => {
|
||||
// Test MIME type mappings that should work consistently in both modes
|
||||
const mimeTypes = {
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.css': 'text/css',
|
||||
'.js': 'application/javascript',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.ico': 'image/x-icon',
|
||||
};
|
||||
|
||||
Object.entries(mimeTypes).forEach(([ext, expectedMimeType]) => {
|
||||
expect(mimeTypes[ext]).toBe(expectedMimeType);
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle custom route parameters in SEA mode', async () => {
|
||||
// Test that SEA mode can handle parameterized routes
|
||||
mockGlobals.isSea = true;
|
||||
|
||||
if (mockGlobals.isSea) {
|
||||
// Mock route handler for /:filename
|
||||
const filenameHandler = jest.fn(async (request, reply) => {
|
||||
const filename = request.params.filename;
|
||||
const filePath = `/${filename}`;
|
||||
|
||||
const result = await mockFilePrep.prepareFile(filePath);
|
||||
if (result.found) {
|
||||
reply.type(result.mimeType).send(result.content);
|
||||
} else {
|
||||
reply.code(404).send('File not found');
|
||||
}
|
||||
});
|
||||
|
||||
mockFastify.get('/:filename', filenameHandler);
|
||||
|
||||
// Simulate request
|
||||
const mockRequest = { params: { filename: 'test.html' } };
|
||||
const mockReply = {
|
||||
type: jest.fn().mockReturnThis(),
|
||||
send: jest.fn(),
|
||||
code: jest.fn().mockReturnThis(),
|
||||
};
|
||||
|
||||
// Mock successful file result
|
||||
mockFilePrep.prepareFile.mockResolvedValue({
|
||||
found: true,
|
||||
content: '<html>Test</html>',
|
||||
mimeType: 'text/html; charset=utf-8',
|
||||
});
|
||||
|
||||
await filenameHandler(mockRequest, mockReply);
|
||||
|
||||
expect(mockFilePrep.prepareFile).toHaveBeenCalledWith('/test.html');
|
||||
expect(mockReply.type).toHaveBeenCalledWith('text/html; charset=utf-8');
|
||||
expect(mockReply.send).toHaveBeenCalledWith('<html>Test</html>');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { Readable } from 'node:stream';
|
||||
import sea from 'node:sea';
|
||||
import sea from './sea-wrapper.js';
|
||||
import handlebars from 'handlebars';
|
||||
|
||||
import globals from '../globals.js';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
20
src/lib/sea-wrapper.js
Normal file
20
src/lib/sea-wrapper.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// Wrapper for node:sea module to make it more testable
|
||||
// This allows us to mock the sea module functionality in tests
|
||||
|
||||
let seaModule = null;
|
||||
|
||||
try {
|
||||
// Try to import the sea module
|
||||
seaModule = await import('node:sea');
|
||||
} catch (error) {
|
||||
// If sea module is not available (e.g., in test environments),
|
||||
// create a mock implementation
|
||||
seaModule = {
|
||||
default: {
|
||||
isSea: () => false,
|
||||
getAsset: () => undefined,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default seaModule.default;
|
||||
Reference in New Issue
Block a user