import test from 'brittle'
import { createConnection } from 'net'
import { SocksProxy, parseArgs, showHelp } from '../socks-server.js'
import { IdentityManager } from '../lib/identity-manager.js'
import { FileManager } from '../lib/file-ops.js'
/**
* Test suite for SOCKS proxy server with Hyperident integration
*/
// Mock console methods for testing
const originalConsoleLog = console.log
const originalConsoleError = console.error
let consoleOutput = []
let consoleErrors = []
function mockConsole() {
consoleOutput = []
consoleErrors = []
console.log = (...args) => consoleOutput.push(args.join(' '))
console.error = (...args) => consoleErrors.push(args.join(' '))
}
function restoreConsole() {
console.log = originalConsoleLog
console.error = originalConsoleError
}
// Test data
const testProfile = 'test-profile'
const testDescription = 'Test profile for SOCKS proxy'
// Helper function to create temporary test environment
async function createTestEnvironment() {
const { mkdtemp } = await import('fs/promises')
const { tmpdir } = await import('os')
const { join } = await import('path')
const tempDir = await mkdtemp(join(tmpdir(), 'hyperident-socks-test-'))
const originalHome = process.env.HOME
process.env.HOME = tempDir
return { tempDir, originalHome }
}
async function cleanupTestEnvironment(tempDir, originalHome) {
process.env.HOME = originalHome
if (tempDir) {
const { rm } = await import('fs/promises')
try {
await rm(tempDir, { recursive: true, force: true })
} catch (error) {
// Ignore cleanup errors
}
}
}
test('parseArgs - server command', async (t) => {
t.plan(3)
const result = parseArgs(['node', 'socks-server.js', 'server'])
t.is(result.command, 'server')
t.alike(result.flags, {})
t.alike(result.positional, [])
})
test('parseArgs - server command with profile flag', async (t) => {
t.plan(3)
const result = parseArgs(['node', 'socks-server.js', 'server', '--profile', 'work'])
t.is(result.command, 'server')
t.is(result.flags.profile, 'work')
t.alike(result.positional, [])
})
test('parseArgs - server command with port flag', async (t) => {
t.plan(3)
const result = parseArgs(['node', 'socks-server.js', 'server', '--port', '8080'])
t.is(result.command, 'server')
t.is(result.flags.port, 8080)
t.alike(result.positional, [])
})
test('parseArgs - server command with multiple flags', async (t) => {
t.plan(4)
const result = parseArgs(['node', 'socks-server.js', 'server', '--profile', 'work', '--port', '8080'])
t.is(result.command, 'server')
t.is(result.flags.profile, 'work')
t.is(result.flags.port, 8080)
t.alike(result.positional, [])
})
test('parseArgs - help command', async (t) => {
t.plan(1)
const result = parseArgs(['node', 'socks-server.js', '--help'])
t.is(result.command, 'help')
})
test('parseArgs - empty arguments', async (t) => {
t.plan(1)
const result = parseArgs(['node', 'socks-server.js'])
t.is(result.command, null)
})
test('parseArgs - profile flag without value throws error', async (t) => {
t.plan(1)
try {
parseArgs(['node', 'socks-server.js', 'server', '--profile'])
t.fail('Should throw error')
} catch (error) {
t.ok(error.message.includes('--profile flag requires a profile name'))
}
})
test('parseArgs - port flag without value throws error', async (t) => {
t.plan(1)
try {
parseArgs(['node', 'socks-server.js', 'server', '--port'])
t.fail('Should throw error')
} catch (error) {
t.ok(error.message.includes('--port flag requires a port number'))
}
})
test('showHelp - displays help message', async (t) => {
t.plan(6)
mockConsole()
showHelp()
restoreConsole()
const output = consoleOutput.join('\n')
t.ok(output.includes('Hyperswarm SOCKS Proxy'))
t.ok(output.includes('USAGE:'))
t.ok(output.includes('--profile <name>'))
t.ok(output.includes('--port <number>'))
t.ok(output.includes('EXAMPLES:'))
t.ok(output.includes('HYPERIDENT INTEGRATION:'))
})
test('SocksProxy - create with default options', async (t) => {
t.plan(4)
const proxy = new SocksProxy()
t.is(proxy.port, 1080)
t.is(proxy.host, '127.0.0.1')
t.is(proxy.topic, null)
t.is(proxy.profileName, null)
})
test('SocksProxy - create with custom options', async (t) => {
t.plan(4)
const topic = Buffer.from('test-topic')
const proxy = new SocksProxy({
port: 8080,
host: '0.0.0.0',
topic,
profileName: 'test'
})
t.is(proxy.port, 8080)
t.is(proxy.host, '0.0.0.0')
t.is(proxy.topic, topic)
t.is(proxy.profileName, 'test')
})
test('SocksProxy - start and stop server', async (t) => {
t.plan(3)
const proxy = new SocksProxy({ port: 0 }) // Use random port
await proxy.start()
t.ok(proxy.server)
t.ok(proxy.server.listening)
await proxy.stop()
t.not(proxy.server.listening)
})
test('SocksProxy - handle connection errors gracefully', async (t) => {
t.plan(1)
const proxy = new SocksProxy({ port: 0 })
await proxy.start()
// Create a connection and immediately close it
const client = createConnection(proxy.server.address().port, '127.0.0.1')
client.destroy()
await proxy.stop()
t.pass('Should handle connection errors without crashing')
})
test('SOCKS5 - authentication negotiation', async (t) => {
t.plan(2)
const proxy = new SocksProxy({ port: 0 })
await proxy.start()
const serverPort = proxy.server.address().port
return new Promise((resolve) => {
const client = createConnection(serverPort, '127.0.0.1')
client.on('connect', () => {
// Send SOCKS5 authentication request
const authRequest = Buffer.from([0x05, 0x01, 0x00]) // Version 5, 1 method, no auth
client.write(authRequest)
})
client.on('data', async (data) => {
// Should receive authentication response
t.is(data.length, 2)
t.is(data[0], 0x05) // SOCKS version
client.destroy()
await proxy.stop()
resolve()
})
client.on('error', async (err) => {
await proxy.stop()
t.fail('Connection error: ' + err.message)
resolve()
})
})
})
test('SOCKS5 - reject unsupported version', async (t) => {
t.plan(1)
const proxy = new SocksProxy({ port: 0 })
await proxy.start()
const serverPort = proxy.server.address().port
return new Promise((resolve) => {
const client = createConnection(serverPort, '127.0.0.1')
client.on('connect', () => {
// Send SOCKS4 request (unsupported)
const request = Buffer.from([0x04, 0x01, 0x00]) // Version 4
client.write(request)
})
client.on('close', async () => {
// Connection should be closed for unsupported version
t.pass('Connection closed for unsupported version')
await proxy.stop()
resolve()
})
client.on('error', async () => {
// Connection error is expected
t.pass('Connection error for unsupported version')
await proxy.stop()
resolve()
})
})
})
test('SOCKS5 - handle CONNECT command', async (t) => {
t.plan(3)
const proxy = new SocksProxy({ port: 0 })
await proxy.start()
const serverPort = proxy.server.address().port
return new Promise((resolve) => {
const client = createConnection(serverPort, '127.0.0.1')
let authComplete = false
client.on('connect', () => {
// Send authentication request first
const authRequest = Buffer.from([0x05, 0x01, 0x00])
client.write(authRequest)
})
client.on('data', async (data) => {
if (!authComplete) {
// Authentication response
t.is(data[0], 0x05)
authComplete = true
// Send CONNECT request
const connectRequest = Buffer.from([
0x05, // Version
0x01, // CONNECT command
0x00, // Reserved
0x01, // IPv4 address type
127, 0, 0, 1, // IP address (127.0.0.1)
0x00, 0x50 // Port 80
])
client.write(connectRequest)
} else {
// CONNECT response
t.is(data[0], 0x05) // Version
t.is(data[1], 0x00) // Success
client.destroy()
await proxy.stop()
resolve()
}
})
client.on('error', async (err) => {
await proxy.stop()
t.fail('Connection error: ' + err.message)
resolve()
})
})
})
test('Hyperident Integration - load profile address', async (t) => {
t.plan(3)
const { tempDir, originalHome } = await createTestEnvironment()
try {
// Create test identity and profile
await IdentityManager.generateIdentity()
await IdentityManager.createProfile(testProfile, testDescription)
const proxy = new SocksProxy({
port: 0,
profileName: testProfile
})
// Get the expected address
const expectedAddress = await IdentityManager.getProfileAddress(testProfile)
const expectedTopic = Buffer.from(expectedAddress, 'hex')
// Start proxy
await proxy.start()
// Verify address properties
t.is(expectedAddress.length, 64) // 32 bytes as hex
t.ok(Buffer.isBuffer(expectedTopic))
t.is(expectedTopic.length, 32)
await proxy.stop()
} finally {
await cleanupTestEnvironment(tempDir, originalHome)
}
})
test('Hyperident Integration - handle missing identity', async (t) => {
t.plan(1)
const { tempDir, originalHome } = await createTestEnvironment()
try {
// Don't create identity - should fail
await t.exception(async () => {
await IdentityManager.getProfileAddress(testProfile)
}, /Identity not found/)
} finally {
await cleanupTestEnvironment(tempDir, originalHome)
}
})
test('Integration Workflow - complete workflow', async (t) => {
t.plan(5)
const { tempDir, originalHome } = await createTestEnvironment()
try {
// 1. Initialize identity
await IdentityManager.generateIdentity()
// 2. Create profile
await IdentityManager.createProfile(testProfile, testDescription)
// 3. Get profile address
const address = await IdentityManager.getProfileAddress(testProfile)
t.is(address.length, 64)
// 4. Create SOCKS proxy with profile
const proxy = new SocksProxy({
port: 0,
topic: Buffer.from(address, 'hex'),
profileName: testProfile
})
// 5. Start proxy
await proxy.start()
t.ok(proxy.server.listening)
// 6. Verify proxy is accessible
const serverPort = proxy.server.address().port
t.is(typeof serverPort, 'number')
t.ok(serverPort > 0)
// 7. Stop proxy
await proxy.stop()
t.not(proxy.server.listening)
} finally {
await cleanupTestEnvironment(tempDir, originalHome)
}
})
test('Integration Workflow - consistent addresses', async (t) => {
t.plan(4)
const { tempDir, originalHome } = await createTestEnvironment()
try {
// Initialize identity
await IdentityManager.generateIdentity()
await IdentityManager.createProfile(testProfile, testDescription)
// Get address multiple times
const address1 = await IdentityManager.getProfileAddress(testProfile)
const address2 = await IdentityManager.getProfileAddress(testProfile)
const address3 = await IdentityManager.getProfileAddress(testProfile)
// All addresses should be identical
t.is(address1, address2)
t.is(address2, address3)
t.is(address1.length, 64)
t.ok(address1.match(/^[0-9a-f]{64}$/)) // Valid hex string
} finally {
await cleanupTestEnvironment(tempDir, originalHome)
}
})
test('Integration Workflow - encrypted identity', async (t) => {
t.plan(2)
const { tempDir, originalHome } = await createTestEnvironment()
try {
const passphrase = 'test-passphrase-123'
// Create encrypted identity
await IdentityManager.generateIdentity(passphrase)
await IdentityManager.createProfile(testProfile, testDescription)
// Get address with passphrase
const address = await IdentityManager.getProfileAddress(testProfile, passphrase)
t.is(address.length, 64)
// Should fail without passphrase
await t.exception(async () => {
await IdentityManager.getProfileAddress(testProfile)
}, /encrypted but no passphrase provided/)
} finally {
await cleanupTestEnvironment(tempDir, originalHome)
}
})
test('Error Handling - server start failures', async (t) => {
t.plan(1)
// Try to bind to a port that's likely in use
const proxy1 = new SocksProxy({ port: 0 })
await proxy1.start()
const usedPort = proxy1.server.address().port
const proxy2 = new SocksProxy({ port: usedPort })
// Second proxy should fail to start on same port
await t.exception(async () => {
await proxy2.start()
})
await proxy1.stop()
})
test('Error Handling - malformed SOCKS data', async (t) => {
t.plan(1)
const proxy = new SocksProxy({ port: 0 })
await proxy.start()
return new Promise((resolve) => {
const client = createConnection(proxy.server.address().port, '127.0.0.1')
client.on('connect', () => {
// Send malformed data
client.write(Buffer.from([0x01])) // Too short
})
client.on('close', async () => {
// Connection should be closed
t.pass('Connection closed for malformed data')
await proxy.stop()
resolve()
})
client.on('error', async () => {
// Error is expected
t.pass('Connection error for malformed data')
await proxy.stop()
resolve()
})
})
})
test('Performance - startup time', async (t) => {
t.plan(1)
const startTime = Date.now()
const proxy = new SocksProxy({ port: 0 })
await proxy.start()
const duration = Date.now() - startTime
t.ok(duration < 1000, `Startup took ${duration}ms, should be under 1000ms`)
await proxy.stop()
})
test('Performance - multiple connections', async (t) => {
t.plan(1)
const proxy = new SocksProxy({ port: 0 })
await proxy.start()
const serverPort = proxy.server.address().port
const connections = []
// Create multiple connections
for (let i = 0; i < 10; i++) {
const client = createConnection(serverPort, '127.0.0.1')
connections.push(client)
}
// Wait a bit for connections to establish
await new Promise(resolve => setTimeout(resolve, 100))
// Close all connections
connections.forEach(client => client.destroy())
await proxy.stop()
t.pass('Should handle multiple connections')
})