OverviewFilesCommitsIssuesDocsReleasesPeers

hyperident / test / socks-server.test.js

544 lines · javascript ref:
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')
})