Skip to main content
Glama
transport-security-unit.test.ts43.5 kB
/** * Transport Security Unit Tests * * Unit tests for the transport security configuration logic. * These tests verify configuration parsing, credential creation, * and error handling using real OpenSSL-generated certificates. */ import { describe, expect, it, beforeEach, afterEach, beforeAll, afterAll } from 'vitest'; import * as grpc from '@grpc/grpc-js'; import { existsSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { execSync } from 'node:child_process'; import { join } from 'node:path'; import * as net from 'node:net'; /** * Verify openssl is available - fail immediately if not. * Security tests MUST NOT be silently skipped. */ function verifyOpenSslAvailable(): void { try { execSync('openssl version', { stdio: 'pipe' }); } catch { throw new Error( 'OpenSSL is required for transport security tests but was not found. ' + 'Install OpenSSL and ensure it is in your PATH. ' + 'Security tests must not be skipped.' ); } } // Fail fast if OpenSSL is not available // Hardcoded ports for each test suite - src uses 55xxx, dist uses 57xxx const isDistBuild = import.meta.url.includes('/dist/'); const PORT_BASE = isDistBuild ? 57000 : 55000; verifyOpenSslAvailable(); async function getFreeTcpPort(host: string): Promise<number> { return await new Promise<number>((resolve, reject) => { const server = net.createServer(); server.unref(); server.on('error', reject); server.listen(0, host, () => { const addr = server.address(); if (!addr || typeof addr === 'string') { server.close(() => reject(new Error('Failed to allocate a TCP port'))); return; } const port = addr.port; server.close((err) => (err ? reject(err) : resolve(port))); }); }); } /** * Generate real self-signed certificates using OpenSSL. * Creates CA, server, and client certificates. */ function generateTestCertificates(dir: string): { caPath: string; certPath: string; keyPath: string; clientCertPath: string; clientKeyPath: string; } { mkdirSync(dir, { recursive: true }); const caKeyPath = join(dir, 'ca.key'); const caPath = join(dir, 'ca.crt'); const keyPath = join(dir, 'server.key'); const csrPath = join(dir, 'server.csr'); const certPath = join(dir, 'server.crt'); const clientKeyPath = join(dir, 'client.key'); const clientCsrPath = join(dir, 'client.csr'); const clientCertPath = join(dir, 'client.crt'); const serverExtPath = join(dir, 'server.ext'); const clientExtPath = join(dir, 'client.ext'); // Generate CA private key execSync(`openssl genrsa -out ${caKeyPath} 2048 2>/dev/null`); // Generate CA certificate execSync(`openssl req -x509 -new -nodes -key ${caKeyPath} -sha256 -days 1 -out ${caPath} \ -subj "/C=US/ST=Test/L=Test/O=Test/OU=Test/CN=test-ca" 2>/dev/null`); // Generate server private key execSync(`openssl genrsa -out ${keyPath} 2048 2>/dev/null`); // Generate server CSR execSync(`openssl req -new -key ${keyPath} -out ${csrPath} \ -subj "/C=US/ST=Test/L=Test/O=Test/OU=Test/CN=localhost" 2>/dev/null`); // Create server extensions file for SAN writeFileSync(serverExtPath, ` subjectAltName = @alt_names extendedKeyUsage = serverAuth [alt_names] DNS.1 = localhost DNS.2 = 127.0.0.1 IP.1 = 127.0.0.1 `); // Generate server certificate signed by CA execSync(`openssl x509 -req -in ${csrPath} -CA ${caPath} -CAkey ${caKeyPath} \ -CAcreateserial -out ${certPath} -days 1 -sha256 -extfile ${serverExtPath} 2>/dev/null`); // Generate client private key execSync(`openssl genrsa -out ${clientKeyPath} 2048 2>/dev/null`); // Generate client CSR execSync(`openssl req -new -key ${clientKeyPath} -out ${clientCsrPath} \ -subj "/C=US/ST=Test/L=Test/O=Test/OU=Test/CN=test-client" 2>/dev/null`); // Create client extensions file writeFileSync(clientExtPath, ` extendedKeyUsage = clientAuth `); // Generate client certificate signed by CA execSync(`openssl x509 -req -in ${clientCsrPath} -CA ${caPath} -CAkey ${caKeyPath} \ -CAcreateserial -out ${clientCertPath} -days 1 -sha256 -extfile ${clientExtPath} 2>/dev/null`); return { caPath, certPath, keyPath, clientCertPath, clientKeyPath, }; } // ============================================================================= // TransportMode Configuration Tests // ============================================================================= describe('TransportMode Configuration - Server', () => { // Store original env vars const originalEnv: Record<string, string | undefined> = {}; beforeEach(() => { // Save and clear relevant env vars const vars = [ 'SANDBOX_TRANSPORT_MODE', 'SANDBOX_TLS_CERT_PATH', 'SANDBOX_TLS_KEY_PATH', 'SANDBOX_TLS_CA_PATH', 'SANDBOX_USE_TCP', 'SANDBOX_TCP_HOST', 'SANDBOX_TCP_PORT', 'SANDBOX_SOCKET_PATH', ]; for (const v of vars) { originalEnv[v] = process.env[v]; delete process.env[v]; } }); afterEach(() => { // Restore env vars for (const [key, value] of Object.entries(originalEnv)) { if (value !== undefined) { process.env[key] = value; } else { delete process.env[key]; } } }); describe('getTransportMode behavior', () => { it('defaults to insecure when no config or env var', async () => { // We test through startServer by checking that it starts in insecure mode // This is a behavioral test since we can't directly access getTransportMode const { startServer } = await import('../server/index.js'); // Create a server with no transport mode specified // It should default to insecure (which doesn't require TLS config) const testPort = PORT_BASE + 1; const server = await startServer({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: testPort, // No transportMode specified - should default to insecure }); expect(server).toBeDefined(); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); }); it('uses transportMode from config when provided', async () => { const { startServer } = await import('../server/index.js'); // Set env to insecure, but config should override process.env.SANDBOX_TRANSPORT_MODE = 'insecure'; // When tls mode is specified without certs, it should throw const testPort = PORT_BASE + 101; await expect( startServer({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: testPort, transportMode: 'tls', // This should override env // No TLS config - should error }) ).rejects.toThrow(/TLS configuration required/); }); it('uses SANDBOX_TRANSPORT_MODE env var when config not provided', async () => { const { startServer } = await import('../server/index.js'); process.env.SANDBOX_TRANSPORT_MODE = 'tls'; // Should fail because TLS mode requires certs const testPort = PORT_BASE + 102; await expect( startServer({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: testPort, // No transportMode in config - should read from env }) ).rejects.toThrow(/TLS configuration required/); }); it('ignores invalid SANDBOX_TRANSPORT_MODE values', async () => { const { startServer } = await import('../server/index.js'); process.env.SANDBOX_TRANSPORT_MODE = 'invalid_mode'; // Should default to insecure when env var is invalid const testPort = PORT_BASE + 103; const server = await startServer({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: testPort, }); expect(server).toBeDefined(); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); }); it('validates all transport mode values', () => { const validModes = ['insecure', 'tls', 'mtls']; for (const mode of validModes) { expect(validModes).toContain(mode); } expect(validModes).not.toContain('ssl'); expect(validModes).not.toContain('none'); expect(validModes).not.toContain(''); }); }); }); describe('TransportMode Configuration - Client', () => { const originalEnv: Record<string, string | undefined> = {}; beforeEach(() => { const vars = [ 'SANDBOX_TRANSPORT_MODE', 'SANDBOX_TLS_CA_PATH', 'SANDBOX_TLS_CLIENT_CERT_PATH', 'SANDBOX_TLS_CLIENT_KEY_PATH', 'SANDBOX_TLS_SERVER_NAME', 'SANDBOX_USE_TCP', 'SANDBOX_TCP_HOST', 'SANDBOX_TCP_PORT', 'SANDBOX_SOCKET_PATH', ]; for (const v of vars) { originalEnv[v] = process.env[v]; delete process.env[v]; } }); afterEach(() => { for (const [key, value] of Object.entries(originalEnv)) { if (value !== undefined) { process.env[key] = value; } else { delete process.env[key]; } } }); describe('getTransportMode behavior', () => { it('defaults to insecure when no options or env var', async () => { const { SandboxClient } = await import('../client/index.js'); // Creating a client with no options should work (insecure mode) const client = new SandboxClient({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: 50051, // No transportMode - defaults to insecure }); expect(client).toBeDefined(); client.close(); }); it('uses transportMode from options when provided', async () => { const { SandboxClient } = await import('../client/index.js'); process.env.SANDBOX_TRANSPORT_MODE = 'insecure'; // mTLS without certs should throw expect(() => new SandboxClient({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: 50051, transportMode: 'mtls', // Override env // No TLS config - should error }) ).toThrow(/Client certificate and key required/); }); it('uses SANDBOX_TRANSPORT_MODE env var when options not provided', async () => { const { SandboxClient } = await import('../client/index.js'); process.env.SANDBOX_TRANSPORT_MODE = 'mtls'; // Should fail because mTLS requires client certs expect(() => new SandboxClient({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: 50051, // No transportMode in options - should read from env }) ).toThrow(/Client certificate and key required/); }); it('ignores invalid SANDBOX_TRANSPORT_MODE values', async () => { const { SandboxClient } = await import('../client/index.js'); process.env.SANDBOX_TRANSPORT_MODE = 'invalid_mode'; // Should default to insecure when env var is invalid const client = new SandboxClient({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: 50051, }); expect(client).toBeDefined(); client.close(); }); }); }); // ============================================================================= // TLS Configuration Tests // ============================================================================= describe('TLS Configuration - Server', () => { // Use different temp directories for src (.ts) vs dist (.js) to avoid collisions when both run in parallel const testDir = `/tmp/prodisco-unit-test-server-${isDistBuild ? 'dist' : 'src'}-${Date.now()}`; let certs: ReturnType<typeof generateTestCertificates>; const originalEnv: Record<string, string | undefined> = {}; beforeEach(() => { certs = generateTestCertificates(testDir); const vars = [ 'SANDBOX_TRANSPORT_MODE', 'SANDBOX_TLS_CERT_PATH', 'SANDBOX_TLS_KEY_PATH', 'SANDBOX_TLS_CA_PATH', 'SANDBOX_USE_TCP', 'SANDBOX_TCP_HOST', 'SANDBOX_TCP_PORT', ]; for (const v of vars) { originalEnv[v] = process.env[v]; delete process.env[v]; } }); afterEach(() => { for (const [key, value] of Object.entries(originalEnv)) { if (value !== undefined) { process.env[key] = value; } else { delete process.env[key]; } } if (existsSync(testDir)) { rmSync(testDir, { recursive: true }); } }); describe('getTlsConfig behavior', () => { it('uses tls config from options when provided', async () => { const { startServer } = await import('../server/index.js'); const testPort = PORT_BASE + 110; const server = await startServer({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: testPort, transportMode: 'tls', tls: { certPath: certs.certPath, keyPath: certs.keyPath, }, }); expect(server).toBeDefined(); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); }); it('uses TLS paths from env vars when config not provided', async () => { const { startServer } = await import('../server/index.js'); process.env.SANDBOX_TLS_CERT_PATH = certs.certPath; process.env.SANDBOX_TLS_KEY_PATH = certs.keyPath; const testPort = PORT_BASE + 111; const server = await startServer({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: testPort, transportMode: 'tls', // No tls config - should read from env }); expect(server).toBeDefined(); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); }); it('throws when TLS mode specified without cert paths', async () => { const { startServer } = await import('../server/index.js'); const testPort = PORT_BASE + 112; await expect( startServer({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: testPort, transportMode: 'tls', // No TLS config }) ).rejects.toThrow(/TLS configuration required/); }); it('throws when mTLS mode specified without CA path', async () => { const { startServer } = await import('../server/index.js'); const testPort = PORT_BASE + 113; // Note: mTLS requires caPath for client verification, but the server // will still start - it just won't verify clients properly // The actual behavior depends on gRPC implementation const server = await startServer({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: testPort, transportMode: 'mtls', tls: { certPath: certs.certPath, keyPath: certs.keyPath, // No caPath - server will start but won't verify clients }, }); expect(server).toBeDefined(); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); }); it('throws when cert file does not exist', async () => { const { startServer } = await import('../server/index.js'); const testPort = PORT_BASE + 114; await expect( startServer({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: testPort, transportMode: 'tls', tls: { certPath: '/nonexistent/cert.crt', keyPath: '/nonexistent/key.key', }, }) ).rejects.toThrow(/ENOENT/); }); it('throws when key file does not exist', async () => { const { startServer } = await import('../server/index.js'); const testPort = PORT_BASE + 115; await expect( startServer({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: testPort, transportMode: 'tls', tls: { certPath: certs.certPath, keyPath: '/nonexistent/key.key', }, }) ).rejects.toThrow(/ENOENT/); }); }); }); describe('TLS Configuration - Client', () => { // Use different temp directories for src (.ts) vs dist (.js) to avoid collisions when both run in parallel const testDir = `/tmp/prodisco-unit-test-client-${isDistBuild ? 'dist' : 'src'}-${Date.now()}`; let certs: ReturnType<typeof generateTestCertificates>; const originalEnv: Record<string, string | undefined> = {}; beforeEach(() => { certs = generateTestCertificates(testDir); const vars = [ 'SANDBOX_TRANSPORT_MODE', 'SANDBOX_TLS_CA_PATH', 'SANDBOX_TLS_CLIENT_CERT_PATH', 'SANDBOX_TLS_CLIENT_KEY_PATH', 'SANDBOX_TLS_SERVER_NAME', 'SANDBOX_USE_TCP', 'SANDBOX_TCP_HOST', 'SANDBOX_TCP_PORT', ]; for (const v of vars) { originalEnv[v] = process.env[v]; delete process.env[v]; } }); afterEach(() => { for (const [key, value] of Object.entries(originalEnv)) { if (value !== undefined) { process.env[key] = value; } else { delete process.env[key]; } } if (existsSync(testDir)) { rmSync(testDir, { recursive: true }); } }); describe('getTlsConfig behavior', () => { it('uses tls config from options when provided', async () => { const { SandboxClient } = await import('../client/index.js'); const client = new SandboxClient({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: 50051, transportMode: 'tls', tls: { caPath: certs.caPath, }, }); expect(client).toBeDefined(); client.close(); }); it('uses TLS paths from env vars when options not provided', async () => { const { SandboxClient } = await import('../client/index.js'); process.env.SANDBOX_TLS_CA_PATH = certs.caPath; const client = new SandboxClient({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: 50051, transportMode: 'tls', // No tls config - should read from env }); expect(client).toBeDefined(); client.close(); }); it('creates TLS client without CA (uses system roots)', async () => { const { SandboxClient } = await import('../client/index.js'); // TLS mode without CA should work (uses system CA roots) const client = new SandboxClient({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: 50051, transportMode: 'tls', // No CA - will use system roots }); expect(client).toBeDefined(); client.close(); }); it('throws when mTLS mode specified without client cert', async () => { const { SandboxClient } = await import('../client/index.js'); expect(() => new SandboxClient({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: 50051, transportMode: 'mtls', tls: { caPath: certs.caPath, // No certPath or keyPath }, }) ).toThrow(/Client certificate and key required/); }); it('throws when mTLS mode specified without client key', async () => { const { SandboxClient } = await import('../client/index.js'); expect(() => new SandboxClient({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: 50051, transportMode: 'mtls', tls: { caPath: certs.caPath, certPath: certs.clientCertPath, // No keyPath }, }) ).toThrow(/Client certificate and key required/); }); it('creates mTLS client with all required certs', async () => { const { SandboxClient } = await import('../client/index.js'); const client = new SandboxClient({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: 50051, transportMode: 'mtls', tls: { caPath: certs.caPath, certPath: certs.clientCertPath, keyPath: certs.clientKeyPath, }, }); expect(client).toBeDefined(); client.close(); }); it('throws when cert file does not exist', async () => { const { SandboxClient } = await import('../client/index.js'); expect(() => new SandboxClient({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: 50051, transportMode: 'mtls', tls: { caPath: certs.caPath, certPath: '/nonexistent/cert.crt', keyPath: certs.clientKeyPath, }, }) ).toThrow(/ENOENT/); }); it('throws when key file does not exist', async () => { const { SandboxClient } = await import('../client/index.js'); expect(() => new SandboxClient({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: 50051, transportMode: 'mtls', tls: { caPath: certs.caPath, certPath: certs.clientCertPath, keyPath: '/nonexistent/key.key', }, }) ).toThrow(/ENOENT/); }); }); describe('serverName override', () => { it('accepts serverName in TLS config', async () => { const { SandboxClient } = await import('../client/index.js'); const client = new SandboxClient({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: 50051, transportMode: 'tls', tls: { caPath: certs.caPath, serverName: 'custom.server.name', }, }); expect(client).toBeDefined(); client.close(); }); it('uses SANDBOX_TLS_SERVER_NAME env var', async () => { const { SandboxClient } = await import('../client/index.js'); process.env.SANDBOX_TLS_CA_PATH = certs.caPath; process.env.SANDBOX_TLS_SERVER_NAME = 'env.server.name'; const client = new SandboxClient({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: 50051, transportMode: 'tls', // No tls config - should read serverName from env }); expect(client).toBeDefined(); client.close(); }); }); }); // ============================================================================= // Unix Socket vs TCP Transport Tests // ============================================================================= describe('Transport Type Selection - Server', () => { const originalEnv: Record<string, string | undefined> = {}; beforeEach(() => { const vars = [ 'SANDBOX_USE_TCP', 'SANDBOX_TCP_HOST', 'SANDBOX_TCP_PORT', 'SANDBOX_SOCKET_PATH', 'SANDBOX_TRANSPORT_MODE', ]; for (const v of vars) { originalEnv[v] = process.env[v]; delete process.env[v]; } }); afterEach(() => { for (const [key, value] of Object.entries(originalEnv)) { if (value !== undefined) { process.env[key] = value; } else { delete process.env[key]; } } }); it('defaults to Unix socket when no config specified', async () => { const { startServer } = await import('../server/index.js'); const socketPath = `/tmp/test-socket-${Date.now()}.sock`; const server = await startServer({ socketPath, // No useTcp, no TCP config }); expect(server).toBeDefined(); // Verify socket file was created expect(existsSync(socketPath)).toBe(true); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); // Cleanup if (existsSync(socketPath)) { rmSync(socketPath); } }); it('uses TCP when useTcp is true', async () => { const { startServer } = await import('../server/index.js'); const testPort = PORT_BASE + 120; const server = await startServer({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: testPort, }); expect(server).toBeDefined(); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); }); it('uses TCP when SANDBOX_USE_TCP env is "true"', async () => { const { startServer } = await import('../server/index.js'); process.env.SANDBOX_USE_TCP = 'true'; process.env.SANDBOX_TCP_PORT = '50821'; process.env.SANDBOX_TCP_HOST = '127.0.0.1'; const server = await startServer({}); expect(server).toBeDefined(); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); }); it('uses TCP when SANDBOX_USE_TCP env is "1"', async () => { const { startServer } = await import('../server/index.js'); process.env.SANDBOX_USE_TCP = '1'; process.env.SANDBOX_TCP_PORT = '50822'; process.env.SANDBOX_TCP_HOST = '127.0.0.1'; const server = await startServer({}); expect(server).toBeDefined(); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); }); it('uses TCP when tcpHost is specified', async () => { const { startServer } = await import('../server/index.js'); const testPort = PORT_BASE + 123; const server = await startServer({ tcpHost: '127.0.0.1', tcpPort: testPort, // useTcp not specified, but tcpHost implies TCP }); expect(server).toBeDefined(); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); }); it('uses TCP when tcpPort is specified', async () => { const { startServer } = await import('../server/index.js'); const testPort = PORT_BASE + 124; const server = await startServer({ tcpPort: testPort, // useTcp not specified, but tcpPort implies TCP }); expect(server).toBeDefined(); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); }); it('uses Unix socket when socketPath is specified', async () => { const { startServer } = await import('../server/index.js'); const socketPath = `/tmp/explicit-socket-${Date.now()}.sock`; const server = await startServer({ socketPath, }); expect(server).toBeDefined(); expect(existsSync(socketPath)).toBe(true); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); if (existsSync(socketPath)) { rmSync(socketPath); } }); it('useTcp=false forces Unix socket even with TCP env vars', async () => { const { startServer } = await import('../server/index.js'); process.env.SANDBOX_TCP_HOST = '127.0.0.1'; process.env.SANDBOX_TCP_PORT = '50825'; const socketPath = `/tmp/forced-socket-${Date.now()}.sock`; const server = await startServer({ useTcp: false, // Explicit Unix socket socketPath, }); expect(server).toBeDefined(); expect(existsSync(socketPath)).toBe(true); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); if (existsSync(socketPath)) { rmSync(socketPath); } }); }); describe('Transport Type Selection - Client', () => { const originalEnv: Record<string, string | undefined> = {}; beforeEach(() => { const vars = [ 'SANDBOX_USE_TCP', 'SANDBOX_TCP_HOST', 'SANDBOX_TCP_PORT', 'SANDBOX_SOCKET_PATH', 'SANDBOX_TRANSPORT_MODE', ]; for (const v of vars) { originalEnv[v] = process.env[v]; delete process.env[v]; } }); afterEach(() => { for (const [key, value] of Object.entries(originalEnv)) { if (value !== undefined) { process.env[key] = value; } else { delete process.env[key]; } } }); it('defaults to Unix socket when no options specified', async () => { const { SandboxClient } = await import('../client/index.js'); // Client creation should work without options (uses default socket path) const client = new SandboxClient({}); expect(client).toBeDefined(); client.close(); }); it('uses TCP when useTcp is true', async () => { const { SandboxClient } = await import('../client/index.js'); const client = new SandboxClient({ useTcp: true, tcpHost: 'localhost', tcpPort: 50051, }); expect(client).toBeDefined(); client.close(); }); it('uses TCP when SANDBOX_USE_TCP env is set', async () => { const { SandboxClient } = await import('../client/index.js'); process.env.SANDBOX_USE_TCP = 'true'; process.env.SANDBOX_TCP_HOST = 'localhost'; process.env.SANDBOX_TCP_PORT = '50051'; const client = new SandboxClient({}); expect(client).toBeDefined(); client.close(); }); it('uses custom socket path from options', async () => { const { SandboxClient } = await import('../client/index.js'); const client = new SandboxClient({ socketPath: '/custom/socket.sock', }); expect(client).toBeDefined(); client.close(); }); it('uses socket path from SANDBOX_SOCKET_PATH env', async () => { const { SandboxClient } = await import('../client/index.js'); process.env.SANDBOX_SOCKET_PATH = '/env/socket.sock'; const client = new SandboxClient({}); expect(client).toBeDefined(); client.close(); }); }); // ============================================================================= // Credential Creation Tests // ============================================================================= describe('Credential Creation', () => { // Use different temp directories for src (.ts) vs dist (.js) to avoid collisions when both run in parallel const testDir = `/tmp/prodisco-cred-test-${isDistBuild ? 'dist' : 'src'}-${Date.now()}`; let certs: ReturnType<typeof generateTestCertificates>; beforeEach(() => { certs = generateTestCertificates(testDir); }); afterEach(() => { if (existsSync(testDir)) { rmSync(testDir, { recursive: true }); } }); describe('Server Credentials', () => { it('creates insecure credentials for insecure mode', async () => { const { startServer } = await import('../server/index.js'); const testPort = PORT_BASE + 130; const server = await startServer({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: testPort, transportMode: 'insecure', }); expect(server).toBeDefined(); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); }); it('creates SSL credentials for TLS mode', async () => { const { startServer } = await import('../server/index.js'); const testPort = PORT_BASE + 131; const server = await startServer({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: testPort, transportMode: 'tls', tls: { certPath: certs.certPath, keyPath: certs.keyPath, }, }); expect(server).toBeDefined(); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); }); it('creates SSL credentials with client verification for mTLS mode', async () => { const { startServer } = await import('../server/index.js'); const testPort = PORT_BASE + 132; const server = await startServer({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: testPort, transportMode: 'mtls', tls: { certPath: certs.certPath, keyPath: certs.keyPath, caPath: certs.caPath, }, }); expect(server).toBeDefined(); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); }); }); describe('Client Credentials', () => { it('creates insecure credentials for insecure mode', async () => { const { SandboxClient } = await import('../client/index.js'); const client = new SandboxClient({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: 50051, transportMode: 'insecure', }); expect(client).toBeDefined(); client.close(); }); it('creates SSL credentials for TLS mode', async () => { const { SandboxClient } = await import('../client/index.js'); const client = new SandboxClient({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: 50051, transportMode: 'tls', tls: { caPath: certs.caPath, }, }); expect(client).toBeDefined(); client.close(); }); it('creates SSL credentials with client cert for mTLS mode', async () => { const { SandboxClient } = await import('../client/index.js'); const client = new SandboxClient({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: 50051, transportMode: 'mtls', tls: { caPath: certs.caPath, certPath: certs.clientCertPath, keyPath: certs.clientKeyPath, }, }); expect(client).toBeDefined(); client.close(); }); }); }); // ============================================================================= // Type Definitions Tests // ============================================================================= describe('Type Definitions', () => { it('TransportMode type allows valid values', () => { // Type-level test - if this compiles, the test passes const insecure: 'insecure' | 'tls' | 'mtls' = 'insecure'; const tls: 'insecure' | 'tls' | 'mtls' = 'tls'; const mtls: 'insecure' | 'tls' | 'mtls' = 'mtls'; expect(insecure).toBe('insecure'); expect(tls).toBe('tls'); expect(mtls).toBe('mtls'); }); it('ServerConfig TlsConfig has correct shape', async () => { const { startServer } = await import('../server/index.js'); // Type check: TlsConfig should have certPath, keyPath, and optional caPath const tlsConfig = { certPath: '/path/to/cert', keyPath: '/path/to/key', caPath: '/path/to/ca', // Optional }; // This should type-check correctly expect(tlsConfig.certPath).toBeDefined(); expect(tlsConfig.keyPath).toBeDefined(); expect(tlsConfig.caPath).toBeDefined(); }); it('ClientOptions TlsConfig has correct shape', async () => { const { SandboxClient } = await import('../client/index.js'); // Type check: Client TlsConfig should have caPath, certPath, keyPath, serverName const tlsConfig = { caPath: '/path/to/ca', certPath: '/path/to/cert', keyPath: '/path/to/key', serverName: 'server.example.com', }; expect(tlsConfig.caPath).toBeDefined(); expect(tlsConfig.certPath).toBeDefined(); expect(tlsConfig.keyPath).toBeDefined(); expect(tlsConfig.serverName).toBeDefined(); }); }); // ============================================================================= // Edge Cases and Error Handling // ============================================================================= describe('Edge Cases and Error Handling', () => { // Use different temp directories for src (.ts) vs dist (.js) to avoid collisions when both run in parallel const testDir = `/tmp/prodisco-edge-test-${isDistBuild ? 'dist' : 'src'}-${Date.now()}`; let certs: ReturnType<typeof generateTestCertificates>; const originalEnv: Record<string, string | undefined> = {}; beforeEach(() => { certs = generateTestCertificates(testDir); const vars = [ 'SANDBOX_TRANSPORT_MODE', 'SANDBOX_TLS_CERT_PATH', 'SANDBOX_TLS_KEY_PATH', 'SANDBOX_TLS_CA_PATH', 'SANDBOX_TLS_CLIENT_CERT_PATH', 'SANDBOX_TLS_CLIENT_KEY_PATH', 'SANDBOX_TLS_SERVER_NAME', 'SANDBOX_USE_TCP', 'SANDBOX_TCP_HOST', 'SANDBOX_TCP_PORT', ]; for (const v of vars) { originalEnv[v] = process.env[v]; delete process.env[v]; } }); afterEach(() => { for (const [key, value] of Object.entries(originalEnv)) { if (value !== undefined) { process.env[key] = value; } else { delete process.env[key]; } } if (existsSync(testDir)) { rmSync(testDir, { recursive: true }); } }); describe('Server edge cases', () => { it('handles empty string transport mode as default', async () => { const { startServer } = await import('../server/index.js'); process.env.SANDBOX_TRANSPORT_MODE = ''; const testPort = PORT_BASE + 200; const server = await startServer({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: testPort, }); expect(server).toBeDefined(); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); }); it('handles whitespace-only transport mode as default', async () => { const { startServer } = await import('../server/index.js'); process.env.SANDBOX_TRANSPORT_MODE = ' '; const testPort = PORT_BASE + 201; const server = await startServer({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: testPort, }); expect(server).toBeDefined(); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); }); it('cleans up existing socket file before binding', async () => { const { startServer } = await import('../server/index.js'); const socketPath = `/tmp/cleanup-test-${Date.now()}.sock`; // Create a dummy file at the socket path writeFileSync(socketPath, 'dummy'); expect(existsSync(socketPath)).toBe(true); // Start server - should cleanup the old file const server = await startServer({ socketPath, }); expect(server).toBeDefined(); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); if (existsSync(socketPath)) { rmSync(socketPath); } }); }); describe('Client edge cases', () => { it('handles empty string transport mode as default', async () => { const { SandboxClient } = await import('../client/index.js'); process.env.SANDBOX_TRANSPORT_MODE = ''; const client = new SandboxClient({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: 50051, }); expect(client).toBeDefined(); client.close(); }); it('handles case-sensitive transport mode values', async () => { const { SandboxClient } = await import('../client/index.js'); // 'TLS' (uppercase) should be treated as invalid and default to insecure process.env.SANDBOX_TRANSPORT_MODE = 'TLS'; const client = new SandboxClient({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: 50051, }); expect(client).toBeDefined(); client.close(); }); it('partial TLS env config without cert does not create TLS config', async () => { const { SandboxClient } = await import('../client/index.js'); // Set only server name, not CA path process.env.SANDBOX_TLS_SERVER_NAME = 'some.server'; // This should still pick up the server name from env const client = new SandboxClient({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: 50051, transportMode: 'tls', }); expect(client).toBeDefined(); client.close(); }); }); describe('Mixed config and env vars', () => { it('config TLS paths take precedence over env vars', async () => { const { startServer } = await import('../server/index.js'); // Set env vars to non-existent paths process.env.SANDBOX_TLS_CERT_PATH = '/nonexistent/env-cert.crt'; process.env.SANDBOX_TLS_KEY_PATH = '/nonexistent/env-key.key'; // Config with valid paths should work const testPort = PORT_BASE + 210; const server = await startServer({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: testPort, transportMode: 'tls', tls: { certPath: certs.certPath, // Config takes precedence keyPath: certs.keyPath, }, }); expect(server).toBeDefined(); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); }); it('config transport mode takes precedence over env var', async () => { const { SandboxClient } = await import('../client/index.js'); process.env.SANDBOX_TRANSPORT_MODE = 'mtls'; // Config with insecure should work (doesn't require mTLS certs) const client = new SandboxClient({ useTcp: true, tcpHost: '127.0.0.1', tcpPort: 50051, transportMode: 'insecure', // Config takes precedence }); expect(client).toBeDefined(); client.close(); }); }); }); // ============================================================================= // Default Values Tests // ============================================================================= describe('Default Values', () => { const originalEnv: Record<string, string | undefined> = {}; beforeEach(() => { const vars = [ 'SANDBOX_SOCKET_PATH', 'SANDBOX_TCP_HOST', 'SANDBOX_TCP_PORT', 'SANDBOX_USE_TCP', 'SANDBOX_TRANSPORT_MODE', ]; for (const v of vars) { originalEnv[v] = process.env[v]; delete process.env[v]; } }); afterEach(() => { for (const [key, value] of Object.entries(originalEnv)) { if (value !== undefined) { process.env[key] = value; } else { delete process.env[key]; } } }); it('server defaults to 0.0.0.0 for TCP host', async () => { const { startServer } = await import('../server/index.js'); // Avoid hard-coded ports that can be in use on CI runners. const testPort = await getFreeTcpPort('0.0.0.0'); process.env.SANDBOX_USE_TCP = 'true'; process.env.SANDBOX_TCP_PORT = String(testPort); // No TCP_HOST set const server = await startServer({}); expect(server).toBeDefined(); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); }); it('server defaults to port 50051 for TCP', async () => { const { startServer } = await import('../server/index.js'); process.env.SANDBOX_USE_TCP = 'true'; process.env.SANDBOX_TCP_HOST = '127.0.0.1'; // No TCP_PORT set - should use 50051 // This might fail if port 50051 is in use, so we wrap in try-catch try { const server = await startServer({}); expect(server).toBeDefined(); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); } catch (e) { // If port 50051 is in use, that's expected - the default is still 50051 expect((e as Error).message).toMatch(/EADDRINUSE|already in use/); } }); it('server defaults to /tmp/prodisco-sandbox.sock for socket path', async () => { // We can't easily test this without potentially conflicting with other tests // So we just verify the constant exists in the module behavior const { startServer } = await import('../server/index.js'); const customSocket = `/tmp/custom-default-test-${Date.now()}.sock`; const server = await startServer({ socketPath: customSocket, }); expect(server).toBeDefined(); await new Promise<void>((resolve) => { server.tryShutdown(() => resolve()); }); if (existsSync(customSocket)) { rmSync(customSocket); } }); it('client defaults to localhost for TCP host', async () => { const { SandboxClient } = await import('../client/index.js'); process.env.SANDBOX_USE_TCP = 'true'; // No TCP_HOST set - should use localhost const client = new SandboxClient({}); expect(client).toBeDefined(); client.close(); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/harche/ProDisco'

If you have feedback or need assistance with the MCP directory API, please join our Discord server