import test from 'brittle'
import { parseArgs, validateArgs, displayError, showHelp, showVersion, handleInit, handleCreateProfile, handleListProfiles, handleDeleteProfile, handleAddress } from '../index.js'
import { IdentityManager } from '../lib/identity-manager.js'
import { FileManager } from '../lib/file-ops.js'
import fs from 'fs'
import path from 'path'
import os from 'os'
// ISOLATION (hyper-social #47): this suite used to rmSync the operator's REAL ~/.hyperident. Redirect
// the vault into a temp dir for the whole run; the guard in cleanupTestIdentity() is the second line
// of defence if this is ever removed.
const TEST_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'hyperident-idx-'))
process.env.HYPERIDENT_HOME = TEST_HOME
FileManager.setBasePath(path.join(TEST_HOME, '.hyperident'))
// Mock console methods for testing output
let consoleOutput = []
let consoleErrorOutput = []
function mockConsole() {
consoleOutput = []
consoleErrorOutput = []
console.log = (...args) => {
consoleOutput.push(args.join(' '))
}
console.error = (...args) => {
consoleErrorOutput.push(args.join(' '))
}
}
function restoreConsole() {
console.log = process.stdout.write.bind(process.stdout)
console.error = process.stderr.write.bind(process.stderr)
}
// Argument Parsing Tests
test('parseArgs - empty arguments', (t) => {
t.plan(4)
const result = parseArgs(['node', 'script.js'])
t.is(result.command, 'help', 'Should default to help command')
t.is(result.flags.length, 0, 'Should have no flags')
t.is(result.positional.length, 0, 'Should have no positional arguments')
t.is(result.raw.length, 0, 'Should have empty raw array')
})
test('parseArgs - simple command', (t) => {
t.plan(4)
const result = parseArgs(['node', 'script.js', 'init'])
t.is(result.command, 'init', 'Should parse command correctly')
t.is(result.flags.length, 0, 'Should have no flags')
t.is(result.positional.length, 0, 'Should have no positional arguments')
t.alike(result.raw, ['init'], 'Should have correct raw array')
})
test('parseArgs - command with flags', (t) => {
t.plan(4)
const result = parseArgs(['node', 'script.js', 'init', '--with-passphrase'])
t.is(result.command, 'init', 'Should parse command correctly')
t.alike(result.flags, ['--with-passphrase'], 'Should parse flags correctly')
t.is(result.positional.length, 0, 'Should have no positional arguments')
t.alike(result.raw, ['init', '--with-passphrase'], 'Should have correct raw array')
})
test('parseArgs - command with positional arguments', (t) => {
t.plan(4)
const result = parseArgs(['node', 'script.js', 'create-profile', 'work', 'Work profile'])
t.is(result.command, 'create-profile', 'Should parse command correctly')
t.is(result.flags.length, 0, 'Should have no flags')
t.alike(result.positional, ['work', 'Work profile'], 'Should parse positional arguments correctly')
t.alike(result.raw, ['create-profile', 'work', 'Work profile'], 'Should have correct raw array')
})
test('parseArgs - mixed flags and positional', (t) => {
t.plan(4)
const result = parseArgs(['node', 'script.js', 'create-profile', '--verbose', 'work', 'description'])
t.is(result.command, 'create-profile', 'Should parse command correctly')
t.alike(result.flags, ['--verbose'], 'Should parse flags correctly')
t.alike(result.positional, ['work', 'description'], 'Should parse positional arguments correctly')
t.alike(result.raw, ['create-profile', '--verbose', 'work', 'description'], 'Should have correct raw array')
})
test('parseArgs - short flags', (t) => {
t.plan(2)
const result1 = parseArgs(['node', 'script.js', '-h'])
t.is(result1.command, '-h', 'Should parse short flag as command')
const result2 = parseArgs(['node', 'script.js', 'init', '-v'])
t.alike(result2.flags, ['-v'], 'Should parse short flags correctly')
})
// Argument Validation Tests
test('validateArgs - valid init command', (t) => {
t.plan(2)
const result1 = validateArgs({ command: 'init', flags: [], positional: [] })
t.ok(result1.valid, 'Init without flags should be valid')
const result2 = validateArgs({ command: 'init', flags: ['--with-passphrase'], positional: [] })
t.ok(result2.valid, 'Init with --with-passphrase should be valid')
})
test('validateArgs - invalid init command', (t) => {
t.plan(4)
const result1 = validateArgs({ command: 'init', flags: ['--invalid'], positional: [] })
t.ok(!result1.valid, 'Init with invalid flag should be invalid')
t.ok(result1.error.includes('Invalid flag(s) for init'), 'Should have appropriate error message')
const result2 = validateArgs({ command: 'init', flags: [], positional: ['arg'] })
t.ok(!result2.valid, 'Init with positional args should be invalid')
t.ok(result2.error.includes('does not accept positional arguments'), 'Should have appropriate error message')
})
test('validateArgs - valid create-profile command', (t) => {
t.plan(2)
const result1 = validateArgs({ command: 'create-profile', flags: [], positional: ['work'] })
t.ok(result1.valid, 'Create-profile with name should be valid')
const result2 = validateArgs({ command: 'create-profile', flags: [], positional: ['work', 'description'] })
t.ok(result2.valid, 'Create-profile with name and description should be valid')
})
test('validateArgs - invalid create-profile command', (t) => {
t.plan(6)
const result1 = validateArgs({ command: 'create-profile', flags: [], positional: [] })
t.ok(!result1.valid, 'Create-profile without name should be invalid')
t.ok(result1.error.includes('Profile name is required'), 'Should have appropriate error message')
const result2 = validateArgs({ command: 'create-profile', flags: [], positional: ['a', 'b', 'c'] })
t.ok(!result2.valid, 'Create-profile with too many args should be invalid')
t.ok(result2.error.includes('Too many arguments'), 'Should have appropriate error message')
const result3 = validateArgs({ command: 'create-profile', flags: ['--flag'], positional: ['work'] })
t.ok(!result3.valid, 'Create-profile with flags should be invalid')
t.ok(result3.error.includes('does not accept flags'), 'Should have appropriate error message')
})
test('validateArgs - valid list-profiles command', (t) => {
t.plan(1)
const result = validateArgs({ command: 'list-profiles', flags: [], positional: [] })
t.ok(result.valid, 'List-profiles without args should be valid')
})
test('validateArgs - invalid list-profiles command', (t) => {
t.plan(4)
const result1 = validateArgs({ command: 'list-profiles', flags: [], positional: ['arg'] })
t.ok(!result1.valid, 'List-profiles with args should be invalid')
t.ok(result1.error.includes('does not accept arguments'), 'Should have appropriate error message')
const result2 = validateArgs({ command: 'list-profiles', flags: ['--flag'], positional: [] })
t.ok(!result2.valid, 'List-profiles with flags should be invalid')
t.ok(result2.error.includes('does not accept flags'), 'Should have appropriate error message')
})
test('validateArgs - valid delete-profile command', (t) => {
t.plan(1)
const result = validateArgs({ command: 'delete-profile', flags: [], positional: ['work'] })
t.ok(result.valid, 'Delete-profile with name should be valid')
})
test('validateArgs - invalid delete-profile command', (t) => {
t.plan(6)
const result1 = validateArgs({ command: 'delete-profile', flags: [], positional: [] })
t.ok(!result1.valid, 'Delete-profile without name should be invalid')
t.ok(result1.error.includes('Profile name is required'), 'Should have appropriate error message')
const result2 = validateArgs({ command: 'delete-profile', flags: [], positional: ['a', 'b'] })
t.ok(!result2.valid, 'Delete-profile with too many args should be invalid')
t.ok(result2.error.includes('Too many arguments'), 'Should have appropriate error message')
const result3 = validateArgs({ command: 'delete-profile', flags: ['--flag'], positional: ['work'] })
t.ok(!result3.valid, 'Delete-profile with flags should be invalid')
t.ok(result3.error.includes('does not accept flags'), 'Should have appropriate error message')
})
test('validateArgs - help and version commands', (t) => {
t.plan(6)
t.ok(validateArgs({ command: 'help', flags: [], positional: [] }).valid, 'Help should be valid')
t.ok(validateArgs({ command: '--help', flags: [], positional: [] }).valid, '--help should be valid')
t.ok(validateArgs({ command: '-h', flags: [], positional: [] }).valid, '-h should be valid')
t.ok(validateArgs({ command: 'version', flags: [], positional: [] }).valid, 'Version should be valid')
t.ok(validateArgs({ command: '--version', flags: [], positional: [] }).valid, '--version should be valid')
t.ok(validateArgs({ command: '-v', flags: [], positional: [] }).valid, '-v should be valid')
})
test('validateArgs - valid address command', (t) => {
t.plan(1)
const result = validateArgs({ command: 'address', flags: [], positional: ['work'] })
t.ok(result.valid, 'Address with profile name should be valid')
})
test('validateArgs - invalid address command', (t) => {
t.plan(6)
const result1 = validateArgs({ command: 'address', flags: [], positional: [] })
t.ok(!result1.valid, 'Address without profile name should be invalid')
t.ok(result1.error.includes('Profile name is required'), 'Should have appropriate error message')
const result2 = validateArgs({ command: 'address', flags: [], positional: ['a', 'b'] })
t.ok(!result2.valid, 'Address with too many args should be invalid')
t.ok(result2.error.includes('Too many arguments'), 'Should have appropriate error message')
const result3 = validateArgs({ command: 'address', flags: ['--invalid-flag'], positional: ['work'] })
t.ok(!result3.valid, 'Address with invalid flags should be invalid')
t.ok(result3.error.includes('Invalid flag(s) for address'), 'Should have appropriate error message')
})
test('validateArgs - unknown command', (t) => {
t.plan(2)
const result = validateArgs({ command: 'unknown', flags: [], positional: [] })
t.ok(!result.valid, 'Unknown command should be invalid')
t.ok(result.error.includes('Unknown command'), 'Should have appropriate error message')
})
// Error Display Tests
test('displayError - general error', (t) => {
t.plan(2)
mockConsole()
displayError('Test error message')
t.ok(consoleErrorOutput[0].includes('❌ Error: Test error message'), 'Should display error message')
t.ok(consoleErrorOutput[1].includes('💡 Use "hyperident --help"'), 'Should display help suggestion')
restoreConsole()
})
test('displayError - create-profile specific error', (t) => {
t.plan(3)
mockConsole()
displayError('Profile name required', 'create-profile')
t.ok(consoleErrorOutput[0].includes('❌ Error: Profile name required'), 'Should display error message')
t.ok(consoleErrorOutput[1].includes('💡 Usage: hyperident create-profile'), 'Should display usage')
t.ok(consoleErrorOutput[2].includes('Example: hyperident create-profile work'), 'Should display example')
restoreConsole()
})
test('displayError - delete-profile specific error', (t) => {
t.plan(3)
mockConsole()
displayError('Profile name required', 'delete-profile')
t.ok(consoleErrorOutput[0].includes('❌ Error: Profile name required'), 'Should display error message')
t.ok(consoleErrorOutput[1].includes('💡 Usage: hyperident delete-profile'), 'Should display usage')
t.ok(consoleErrorOutput[2].includes('Example: hyperident delete-profile work'), 'Should display example')
restoreConsole()
})
test('displayError - init specific error', (t) => {
t.plan(2)
mockConsole()
displayError('Invalid flag', 'init')
t.ok(consoleErrorOutput[0].includes('❌ Error: Invalid flag'), 'Should display error message')
t.ok(consoleErrorOutput[1].includes('💡 Usage: hyperident init'), 'Should display usage')
restoreConsole()
})
// Help and Version Display Tests
test('showHelp - displays help message', (t) => {
t.plan(4)
mockConsole()
showHelp()
const output = consoleOutput.join('\n')
t.ok(output.includes('🔑 Hyperident'), 'Should display title')
t.ok(output.includes('USAGE:'), 'Should display usage section')
t.ok(output.includes('COMMANDS:'), 'Should display commands section')
t.ok(output.includes('EXAMPLES:'), 'Should display examples section')
restoreConsole()
})
test('showVersion - displays version', (t) => {
t.plan(1)
mockConsole()
showVersion()
t.ok(consoleOutput[0].includes('hyperident v0.1.0'), 'Should display version')
restoreConsole()
})
// Integration Tests
test('parseArgs and validateArgs integration', (t) => {
t.plan(4)
// Test valid command flow
const parsed1 = parseArgs(['node', 'script.js', 'init', '--with-passphrase'])
const validation1 = validateArgs(parsed1)
t.ok(validation1.valid, 'Valid init command should pass validation')
// Test invalid command flow
const parsed2 = parseArgs(['node', 'script.js', 'init', '--invalid-flag'])
const validation2 = validateArgs(parsed2)
t.ok(!validation2.valid, 'Invalid init command should fail validation')
// Test create-profile flow
const parsed3 = parseArgs(['node', 'script.js', 'create-profile', 'work', 'Work profile'])
const validation3 = validateArgs(parsed3)
t.ok(validation3.valid, 'Valid create-profile command should pass validation')
// Test missing required argument
const parsed4 = parseArgs(['node', 'script.js', 'delete-profile'])
const validation4 = validateArgs(parsed4)
t.ok(!validation4.valid, 'Delete-profile without name should fail validation')
})
test('Complex argument parsing scenarios', (t) => {
t.plan(6)
// Test multiple flags
const result1 = parseArgs(['node', 'script.js', 'init', '--with-passphrase', '--verbose'])
t.alike(result1.flags, ['--with-passphrase', '--verbose'], 'Should parse multiple flags')
// Test mixed short and long flags
const result2 = parseArgs(['node', 'script.js', 'command', '-v', '--flag'])
t.alike(result2.flags, ['-v', '--flag'], 'Should parse mixed flag types')
// Test arguments with spaces (quoted)
const result3 = parseArgs(['node', 'script.js', 'create-profile', 'work', 'Work profile description'])
t.alike(result3.positional, ['work', 'Work profile description'], 'Should handle space-separated arguments')
// Test empty string arguments
const result4 = parseArgs(['node', 'script.js', 'create-profile', '', 'description'])
t.alike(result4.positional, ['', 'description'], 'Should handle empty string arguments')
// Test flag-like positional arguments
const result5 = parseArgs(['node', 'script.js', 'create-profile', '--not-a-flag'])
t.alike(result5.flags, ['--not-a-flag'], 'Should treat flag-like strings as flags')
// Test single character arguments
const result6 = parseArgs(['node', 'script.js', 'create-profile', 'a'])
t.alike(result6.positional, ['a'], 'Should handle single character arguments')
})
// Init Command Tests
// Helper function to clean up test identity
async function cleanupTestIdentity() {
try {
// See identity-manager.test.js: this used to rmSync the operator's REAL vault (#47).
const hyperidentPath = process.env.HYPERIDENT_HOME
? path.join(process.env.HYPERIDENT_HOME, '.hyperident')
: path.join(os.homedir(), '.hyperident')
if (path.resolve(hyperidentPath) === path.resolve(path.join(os.homedir(), '.hyperident'))) {
throw new Error('REFUSING to delete the real vault — set HYPERIDENT_HOME to a temp dir first')
}
if (fs.existsSync(hyperidentPath)) {
fs.rmSync(hyperidentPath, { recursive: true, force: true })
}
} catch (error) {
// Ignore cleanup errors
}
}
// Mock process.exit for testing
let mockExitCode = null
const originalExit = process.exit
function mockProcessExit() {
mockExitCode = null
process.exit = (code) => {
mockExitCode = code
throw new Error(`Process exit called with code ${code}`)
}
}
function restoreProcessExit() {
process.exit = originalExit
mockExitCode = null
}
test('handleInit - successful init without passphrase', async (t) => {
t.plan(6)
await cleanupTestIdentity()
mockConsole()
try {
const parsed = { command: 'init', flags: [], positional: [] }
await handleInit(parsed)
// Check that identity was created
t.ok(IdentityManager.identityExists(), 'Identity should be created')
// Check console output
const output = consoleOutput.join('\n')
t.ok(output.includes('🔑 Generating new identity'), 'Should show generating message')
t.ok(output.includes('✅ Identity created successfully'), 'Should show success message')
t.ok(output.includes('📋 Identity Details'), 'Should show identity details')
t.ok(output.includes('Encrypted: No'), 'Should show not encrypted')
t.ok(output.includes('🚀 Next steps'), 'Should show next steps')
} catch (error) {
t.fail(`Should not throw error: ${error.message}`)
} finally {
restoreConsole()
await cleanupTestIdentity()
}
})
// Profile Command Tests
test('handleCreateProfile - successful profile creation', async (t) => {
t.plan(5)
await cleanupTestIdentity()
mockConsole()
try {
// Create identity first
await IdentityManager.generateIdentity()
const parsed = { command: 'create-profile', flags: [], positional: ['work', 'Work profile'] }
await handleCreateProfile(parsed)
// Check console output
const output = consoleOutput.join('\n')
t.ok(output.includes('📝 Creating profile \'work\''), 'Should show creating message')
t.ok(output.includes('✅ Profile created successfully'), 'Should show success message')
t.ok(output.includes('Name: work'), 'Should show profile name')
t.ok(output.includes('Description: Work profile'), 'Should show profile description')
t.ok(output.includes('🚀 Next steps'), 'Should show next steps')
} catch (error) {
t.fail(`Should not throw error: ${error.message}`)
} finally {
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleCreateProfile - profile creation without description', async (t) => {
t.plan(4)
await cleanupTestIdentity()
mockConsole()
try {
// Create identity first
await IdentityManager.generateIdentity()
const parsed = { command: 'create-profile', flags: [], positional: ['work'] }
await handleCreateProfile(parsed)
// Check console output
const output = consoleOutput.join('\n')
t.ok(output.includes('📝 Creating profile \'work\''), 'Should show creating message')
t.ok(output.includes('✅ Profile created successfully'), 'Should show success message')
t.ok(output.includes('Name: work'), 'Should show profile name')
t.ok(output.includes('Description: (no description)'), 'Should show no description')
} catch (error) {
t.fail(`Should not throw error: ${error.message}`)
} finally {
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleCreateProfile - fails without identity', async (t) => {
t.plan(3)
await cleanupTestIdentity()
mockConsole()
mockProcessExit()
try {
const parsed = { command: 'create-profile', flags: [], positional: ['work'] }
try {
await handleCreateProfile(parsed)
t.fail('Should have thrown an error')
} catch (error) {
t.ok(error.message.includes('Process exit called with code 2'), 'Should exit with code 2')
}
// Check console output
const errorOutput = consoleErrorOutput.join('\n')
t.ok(errorOutput.includes('❌ Failed to create profile'), 'Should show error message')
t.ok(errorOutput.includes('💡 Run "hyperident init" first'), 'Should show helpful guidance')
} finally {
restoreProcessExit()
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleCreateProfile - fails with invalid profile name', async (t) => {
t.plan(3)
await cleanupTestIdentity()
mockConsole()
mockProcessExit()
try {
// Create identity first
await IdentityManager.generateIdentity()
const parsed = { command: 'create-profile', flags: [], positional: ['invalid name'] }
try {
await handleCreateProfile(parsed)
t.fail('Should have thrown an error')
} catch (error) {
t.ok(error.message.includes('Process exit called with code 2'), 'Should exit with code 2')
}
// Check console output
const errorOutput = consoleErrorOutput.join('\n')
t.ok(errorOutput.includes('❌ Failed to create profile'), 'Should show error message')
t.ok(errorOutput.includes('💡 Profile names must be 1-32 characters'), 'Should show validation guidance')
} finally {
restoreProcessExit()
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleCreateProfile - fails with duplicate profile name', async (t) => {
t.plan(3)
await cleanupTestIdentity()
mockConsole()
mockProcessExit()
try {
// Create identity and profile first
await IdentityManager.generateIdentity()
await IdentityManager.createProfile('work', 'Work profile')
const parsed = { command: 'create-profile', flags: [], positional: ['work'] }
try {
await handleCreateProfile(parsed)
t.fail('Should have thrown an error')
} catch (error) {
t.ok(error.message.includes('Process exit called with code 2'), 'Should exit with code 2')
}
// Check console output
const errorOutput = consoleErrorOutput.join('\n')
t.ok(errorOutput.includes('❌ Failed to create profile'), 'Should show error message')
t.ok(errorOutput.includes('💡 Choose a different profile name'), 'Should show duplicate guidance')
} finally {
restoreProcessExit()
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleListProfiles - lists profiles successfully', async (t) => {
t.plan(6)
await cleanupTestIdentity()
mockConsole()
try {
// Create identity and profiles first
await IdentityManager.generateIdentity()
await IdentityManager.createProfile('work', 'Work profile')
await IdentityManager.createProfile('personal', 'Personal projects')
const parsed = { command: 'list-profiles', flags: [], positional: [] }
await handleListProfiles(parsed)
// Check console output
const output = consoleOutput.join('\n')
t.ok(output.includes('📋 Listing profiles'), 'Should show listing message')
t.ok(output.includes('✅ Found 2 profiles'), 'Should show profile count')
t.ok(output.includes('📁 personal'), 'Should show personal profile')
t.ok(output.includes('📁 work'), 'Should show work profile')
t.ok(output.includes('Description: Work profile'), 'Should show profile descriptions')
t.ok(output.includes('🚀 Next steps'), 'Should show next steps')
} catch (error) {
t.fail(`Should not throw error: ${error.message}`)
} finally {
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleListProfiles - shows empty state', async (t) => {
t.plan(4)
await cleanupTestIdentity()
mockConsole()
try {
// Create identity but no profiles
await IdentityManager.generateIdentity()
const parsed = { command: 'list-profiles', flags: [], positional: [] }
await handleListProfiles(parsed)
// Check console output
const output = consoleOutput.join('\n')
t.ok(output.includes('📋 Listing profiles'), 'Should show listing message')
t.ok(output.includes('📭 No profiles found'), 'Should show empty state')
t.ok(output.includes('🚀 Get started'), 'Should show get started section')
t.ok(output.includes('Create a profile: hyperident create-profile'), 'Should show creation guidance')
} catch (error) {
t.fail(`Should not throw error: ${error.message}`)
} finally {
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleListProfiles - fails without identity', async (t) => {
t.plan(3)
await cleanupTestIdentity()
mockConsole()
mockProcessExit()
try {
const parsed = { command: 'list-profiles', flags: [], positional: [] }
try {
await handleListProfiles(parsed)
t.fail('Should have thrown an error')
} catch (error) {
t.ok(error.message.includes('Process exit called with code 2'), 'Should exit with code 2')
}
// Check console output
const errorOutput = consoleErrorOutput.join('\n')
t.ok(errorOutput.includes('❌ Failed to list profiles'), 'Should show error message')
t.ok(errorOutput.includes('💡 Run "hyperident init" first'), 'Should show helpful guidance')
} finally {
restoreProcessExit()
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleDeleteProfile - deletes profile successfully', async (t) => {
t.plan(5)
await cleanupTestIdentity()
mockConsole()
try {
// Create identity and profile first
await IdentityManager.generateIdentity()
await IdentityManager.createProfile('work', 'Work profile')
const parsed = { command: 'delete-profile', flags: [], positional: ['work'] }
await handleDeleteProfile(parsed)
// Check console output
const output = consoleOutput.join('\n')
t.ok(output.includes('🗑️ Deleting profile \'work\''), 'Should show deleting message')
t.ok(output.includes('✅ Profile deleted successfully'), 'Should show success message')
t.ok(output.includes('Profile \'work\' has been removed'), 'Should show removal confirmation')
t.ok(output.includes('🚀 Next steps'), 'Should show next steps')
t.ok(output.includes('List remaining profiles'), 'Should suggest listing profiles')
} catch (error) {
t.fail(`Should not throw error: ${error.message}`)
} finally {
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleDeleteProfile - fails without identity', async (t) => {
t.plan(3)
await cleanupTestIdentity()
mockConsole()
mockProcessExit()
try {
const parsed = { command: 'delete-profile', flags: [], positional: ['work'] }
try {
await handleDeleteProfile(parsed)
t.fail('Should have thrown an error')
} catch (error) {
t.ok(error.message.includes('Process exit called with code 2'), 'Should exit with code 2')
}
// Check console output
const errorOutput = consoleErrorOutput.join('\n')
t.ok(errorOutput.includes('❌ Failed to delete profile'), 'Should show error message')
t.ok(errorOutput.includes('💡 Run "hyperident init" first'), 'Should show helpful guidance')
} finally {
restoreProcessExit()
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleDeleteProfile - fails with non-existent profile', async (t) => {
t.plan(3)
await cleanupTestIdentity()
mockConsole()
mockProcessExit()
try {
// Create identity but no profiles
await IdentityManager.generateIdentity()
const parsed = { command: 'delete-profile', flags: [], positional: ['nonexistent'] }
try {
await handleDeleteProfile(parsed)
t.fail('Should have thrown an error')
} catch (error) {
t.ok(error.message.includes('Process exit called with code 2'), 'Should exit with code 2')
}
// Check console output
const errorOutput = consoleErrorOutput.join('\n')
t.ok(errorOutput.includes('❌ Failed to delete profile'), 'Should show error message')
t.ok(errorOutput.includes('💡 Check available profiles'), 'Should show helpful guidance')
} finally {
restoreProcessExit()
restoreConsole()
await cleanupTestIdentity()
}
})
// Profile Command Integration Tests
test('Profile commands integration - full workflow', async (t) => {
t.plan(8)
await cleanupTestIdentity()
mockConsole()
try {
// Create identity first
await IdentityManager.generateIdentity()
// Create a profile
const createParsed = { command: 'create-profile', flags: [], positional: ['test', 'Test profile'] }
await handleCreateProfile(createParsed)
let output = consoleOutput.join('\n')
t.ok(output.includes('✅ Profile created successfully'), 'Should create profile successfully')
// Clear console output
consoleOutput = []
// List profiles
const listParsed = { command: 'list-profiles', flags: [], positional: [] }
await handleListProfiles(listParsed)
output = consoleOutput.join('\n')
t.ok(output.includes('✅ Found 1 profile'), 'Should list 1 profile')
t.ok(output.includes('📁 test'), 'Should show test profile')
// Clear console output
consoleOutput = []
// Create another profile
const createParsed2 = { command: 'create-profile', flags: [], positional: ['work'] }
await handleCreateProfile(createParsed2)
output = consoleOutput.join('\n')
t.ok(output.includes('✅ Profile created successfully'), 'Should create second profile successfully')
// Clear console output
consoleOutput = []
// List profiles again
await handleListProfiles(listParsed)
output = consoleOutput.join('\n')
t.ok(output.includes('✅ Found 2 profiles'), 'Should list 2 profiles')
t.ok(output.includes('📁 test'), 'Should show test profile')
t.ok(output.includes('📁 work'), 'Should show work profile')
// Clear console output
consoleOutput = []
// Delete a profile
const deleteParsed = { command: 'delete-profile', flags: [], positional: ['test'] }
await handleDeleteProfile(deleteParsed)
output = consoleOutput.join('\n')
t.ok(output.includes('✅ Profile deleted successfully'), 'Should delete profile successfully')
} catch (error) {
t.fail(`Should not throw error: ${error.message}`)
} finally {
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleInit - successful init with passphrase', async (t) => {
t.plan(4)
await cleanupTestIdentity()
mockConsole()
// Mock stdin for passphrase input
const originalStdin = process.stdin
let inputIndex = 0
const inputs = ['testpass123\r', 'testpass123\r'] // passphrase and confirmation
process.stdin = {
setRawMode: () => {},
resume: () => {},
pause: () => {},
setEncoding: () => {},
on: (event, callback) => {
if (event === 'data') {
// Simulate typing each character
const input = inputs[Math.floor(inputIndex / 12)] || '\r'
setTimeout(() => callback(input[inputIndex % input.length] || '\r'), 10)
inputIndex++
}
},
removeListener: () => {}
}
try {
const parsed = { command: 'init', flags: ['--with-passphrase'], positional: [] }
await handleInit(parsed)
// Check that identity was created
t.ok(IdentityManager.identityExists(), 'Identity should be created')
// Check console output
const output = consoleOutput.join('\n')
t.ok(output.includes('🔐 Creating encrypted identity'), 'Should show encrypted message')
t.ok(output.includes('✅ Identity created successfully'), 'Should show success message')
t.ok(output.includes('🔒 Important: Your identity is encrypted'), 'Should show encryption warning')
} catch (error) {
t.fail(`Should not throw error: ${error.message}`)
} finally {
process.stdin = originalStdin
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleInit - fails when identity already exists', async (t) => {
t.plan(3)
await cleanupTestIdentity()
mockConsole()
mockProcessExit()
try {
// Create an identity first
await IdentityManager.generateIdentity()
// Try to init again
const parsed = { command: 'init', flags: [], positional: [] }
try {
await handleInit(parsed)
t.fail('Should have thrown an error')
} catch (error) {
t.ok(error.message.includes('Process exit called with code 1'), 'Should exit with code 1')
}
// Check console output
const output = consoleOutput.join('\n')
t.ok(output.includes('❌ Identity already exists'), 'Should show error message')
t.ok(output.includes('💡 If you want to create a new identity'), 'Should show suggestion')
} finally {
restoreProcessExit()
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleInit - validates passphrase length', async (t) => {
t.plan(2)
await cleanupTestIdentity()
mockConsole()
mockProcessExit()
// Mock stdin for short passphrase
const originalStdin = process.stdin
process.stdin = {
setRawMode: () => {},
resume: () => {},
pause: () => {},
setEncoding: () => {},
on: (event, callback) => {
if (event === 'data') {
setTimeout(() => callback('short\r'), 10)
}
},
removeListener: () => {}
}
try {
const parsed = { command: 'init', flags: ['--with-passphrase'], positional: [] }
try {
await handleInit(parsed)
t.fail('Should have thrown an error')
} catch (error) {
t.ok(error.message.includes('Process exit called with code 1'), 'Should exit with code 1')
}
// Check console output
const output = consoleOutput.join('\n')
t.ok(output.includes('❌ Passphrase must be at least 8 characters'), 'Should show passphrase length error')
} finally {
process.stdin = originalStdin
restoreProcessExit()
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleInit - validates passphrase confirmation', async (t) => {
t.plan(2)
await cleanupTestIdentity()
mockConsole()
mockProcessExit()
// Mock stdin for mismatched passphrases
const originalStdin = process.stdin
let callCount = 0
process.stdin = {
setRawMode: () => {},
resume: () => {},
pause: () => {},
setEncoding: () => {},
on: (event, callback) => {
if (event === 'data') {
const responses = ['testpass123\r', 'different456\r']
setTimeout(() => callback(responses[callCount++] || '\r'), 10)
}
},
removeListener: () => {}
}
try {
const parsed = { command: 'init', flags: ['--with-passphrase'], positional: [] }
try {
await handleInit(parsed)
t.fail('Should have thrown an error')
} catch (error) {
t.ok(error.message.includes('Process exit called with code 1'), 'Should exit with code 1')
}
// Check console output
const output = consoleOutput.join('\n')
t.ok(output.includes('❌ Passphrases do not match'), 'Should show passphrase mismatch error')
} finally {
process.stdin = originalStdin
restoreProcessExit()
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleInit - handles identity generation errors', async (t) => {
t.plan(2)
await cleanupTestIdentity()
mockConsole()
mockProcessExit()
// Mock IdentityManager.generateIdentity to throw an error
const originalGenerateIdentity = IdentityManager.generateIdentity
IdentityManager.generateIdentity = async () => {
throw new Error('Test generation error')
}
try {
const parsed = { command: 'init', flags: [], positional: [] }
try {
await handleInit(parsed)
t.fail('Should have thrown an error')
} catch (error) {
t.ok(error.message.includes('Process exit called with code 2'), 'Should exit with code 2')
}
// Check console output
const errorOutput = consoleErrorOutput.join('\n')
t.ok(errorOutput.includes('❌ Failed to initialize identity'), 'Should show error message')
} finally {
IdentityManager.generateIdentity = originalGenerateIdentity
restoreProcessExit()
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleInit - handles Ctrl+C during passphrase input', async (t) => {
t.plan(1)
await cleanupTestIdentity()
mockConsole()
mockProcessExit()
// Mock stdin for Ctrl+C
const originalStdin = process.stdin
process.stdin = {
setRawMode: () => {},
resume: () => {},
pause: () => {},
setEncoding: () => {},
on: (event, callback) => {
if (event === 'data') {
setTimeout(() => callback('\u0003'), 10) // Ctrl+C
}
},
removeListener: () => {}
}
try {
const parsed = { command: 'init', flags: ['--with-passphrase'], positional: [] }
try {
await handleInit(parsed)
t.fail('Should have thrown an error')
} catch (error) {
t.ok(error.message.includes('Process exit called with code 1'), 'Should exit with code 1 on Ctrl+C')
}
} finally {
process.stdin = originalStdin
restoreProcessExit()
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleInit - displays correct identity details', async (t) => {
t.plan(5)
await cleanupTestIdentity()
mockConsole()
try {
const parsed = { command: 'init', flags: [], positional: [] }
await handleInit(parsed)
const output = consoleOutput.join('\n')
// Check that all required details are displayed
t.ok(output.includes('Public Key:'), 'Should display public key')
t.ok(output.includes('Created:'), 'Should display creation date')
t.ok(output.includes('Algorithm: keet-identity-key'), 'Should display algorithm')
t.ok(output.includes('💾 Identity saved to: ~/.hyperident/'), 'Should display save location')
t.ok(output.includes('hyperident create-profile'), 'Should show next steps')
} catch (error) {
t.fail(`Should not throw error: ${error.message}`)
} finally {
restoreConsole()
await cleanupTestIdentity()
}
})
// Address Command Tests
test('handleAddress - generates address successfully', async (t) => {
t.plan(6)
await cleanupTestIdentity()
mockConsole()
try {
// Create identity and profile first
await IdentityManager.generateIdentity()
await IdentityManager.createProfile('work', 'Work profile')
const parsed = { command: 'address', flags: [], positional: ['work'] }
await handleAddress(parsed)
// Check console output
const output = consoleOutput.join('\n')
t.ok(output.includes('🔍 Getting address for profile \'work\''), 'Should show getting address message')
t.ok(output.includes('✅ Address generated successfully'), 'Should show success message')
t.ok(output.includes('📋 Profile Address'), 'Should show address section')
t.ok(output.includes('Profile: work'), 'Should show profile name')
t.ok(output.includes('Address:'), 'Should show address field')
t.ok(output.includes('🚀 Next steps'), 'Should show next steps')
} catch (error) {
t.fail(`Should not throw error: ${error.message}`)
} finally {
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleAddress - fails without identity', async (t) => {
t.plan(3)
await cleanupTestIdentity()
mockConsole()
mockProcessExit()
try {
const parsed = { command: 'address', flags: [], positional: ['work'] }
try {
await handleAddress(parsed)
t.fail('Should have thrown an error')
} catch (error) {
t.ok(error.message.includes('Process exit called with code 2'), 'Should exit with code 2')
}
// Check console output
const errorOutput = consoleErrorOutput.join('\n')
t.ok(errorOutput.includes('❌ Failed to get profile address'), 'Should show error message')
t.ok(errorOutput.includes('💡 Run "hyperident init" first'), 'Should show helpful guidance')
} finally {
restoreProcessExit()
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleAddress - fails with non-existent profile', async (t) => {
t.plan(3)
await cleanupTestIdentity()
mockConsole()
mockProcessExit()
try {
// Create identity but no profiles
await IdentityManager.generateIdentity()
const parsed = { command: 'address', flags: [], positional: ['nonexistent'] }
try {
await handleAddress(parsed)
t.fail('Should have thrown an error')
} catch (error) {
t.ok(error.message.includes('Process exit called with code 2'), 'Should exit with code 2')
}
// Check console output
const errorOutput = consoleErrorOutput.join('\n')
t.ok(errorOutput.includes('❌ Failed to get profile address'), 'Should show error message')
t.ok(errorOutput.includes('💡 Check available profiles'), 'Should show helpful guidance')
} finally {
restoreProcessExit()
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleAddress - handles encrypted identity with passphrase', async (t) => {
t.plan(6)
await cleanupTestIdentity()
mockConsole()
// Mock stdin for passphrase input
const originalStdin = process.stdin
process.stdin = {
setRawMode: () => {},
resume: () => {},
pause: () => {},
setEncoding: () => {},
on: (event, callback) => {
if (event === 'data') {
setTimeout(() => callback('testpass123\r'), 10)
}
},
removeListener: () => {}
}
try {
// Create encrypted identity and profile first
await IdentityManager.generateIdentity('testpass123')
await IdentityManager.createProfile('work', 'Work profile', 'testpass123')
const parsed = { command: 'address', flags: [], positional: ['work'] }
await handleAddress(parsed)
// Check console output
const output = consoleOutput.join('\n')
t.ok(output.includes('🔍 Getting address for profile \'work\''), 'Should show getting address message')
t.ok(output.includes('🔐 Identity is encrypted'), 'Should show encryption message')
t.ok(output.includes('✅ Address generated successfully'), 'Should show success message')
t.ok(output.includes('Profile: work'), 'Should show profile name')
t.ok(output.includes('Address:'), 'Should show address field')
t.ok(output.includes('🚀 Next steps'), 'Should show next steps')
} catch (error) {
t.fail(`Should not throw error: ${error.message}`)
} finally {
process.stdin = originalStdin
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleAddress - fails with invalid passphrase', async (t) => {
t.plan(3)
await cleanupTestIdentity()
mockConsole()
mockProcessExit()
// Mock stdin for wrong passphrase
const originalStdin = process.stdin
process.stdin = {
setRawMode: () => {},
resume: () => {},
pause: () => {},
setEncoding: () => {},
on: (event, callback) => {
if (event === 'data') {
setTimeout(() => callback('wrongpass\r'), 10)
}
},
removeListener: () => {}
}
try {
// Create encrypted identity and profile first
await IdentityManager.generateIdentity('testpass123')
await IdentityManager.createProfile('work', 'Work profile', 'testpass123')
const parsed = { command: 'address', flags: [], positional: ['work'] }
try {
await handleAddress(parsed)
t.fail('Should have thrown an error')
} catch (error) {
t.ok(error.message.includes('Process exit called with code 2'), 'Should exit with code 2')
}
// Check console output
const errorOutput = consoleErrorOutput.join('\n')
t.ok(errorOutput.includes('❌ Failed to get profile address'), 'Should show error message')
t.ok(errorOutput.includes('💡 Please check your passphrase'), 'Should show passphrase guidance')
} finally {
process.stdin = originalStdin
restoreProcessExit()
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleAddress - handles Ctrl+C during passphrase input', async (t) => {
t.plan(1)
await cleanupTestIdentity()
mockConsole()
mockProcessExit()
// Mock stdin for Ctrl+C
const originalStdin = process.stdin
process.stdin = {
setRawMode: () => {},
resume: () => {},
pause: () => {},
setEncoding: () => {},
on: (event, callback) => {
if (event === 'data') {
setTimeout(() => callback('\u0003'), 10) // Ctrl+C
}
},
removeListener: () => {}
}
try {
// Create encrypted identity and profile first
await IdentityManager.generateIdentity('testpass123')
await IdentityManager.createProfile('work', 'Work profile', 'testpass123')
const parsed = { command: 'address', flags: [], positional: ['work'] }
try {
await handleAddress(parsed)
t.fail('Should have thrown an error')
} catch (error) {
t.ok(error.message.includes('Process exit called with code 1'), 'Should exit with code 1 on Ctrl+C')
}
} finally {
process.stdin = originalStdin
restoreProcessExit()
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleAddress - displays correct address information', async (t) => {
t.plan(6)
await cleanupTestIdentity()
mockConsole()
try {
// Create identity and profile first
await IdentityManager.generateIdentity()
await IdentityManager.createProfile('test-profile', 'Test profile description')
const parsed = { command: 'address', flags: [], positional: ['test-profile'] }
await handleAddress(parsed)
const output = consoleOutput.join('\n')
// Check that all required information is displayed
t.ok(output.includes('🔗 Usage:'), 'Should display usage section')
t.ok(output.includes('This address can be used with hyperswarm applications'), 'Should show usage info')
t.ok(output.includes('The same profile will always generate the same address'), 'Should show deterministic info')
t.ok(output.includes('Use this address in hyperswarm applications'), 'Should show next steps')
t.ok(output.includes('Create more profiles: hyperident create-profile'), 'Should show profile creation suggestion')
t.ok(output.includes('List all profiles: hyperident list-profiles'), 'Should show list profiles suggestion')
} catch (error) {
t.fail(`Should not throw error: ${error.message}`)
} finally {
restoreConsole()
await cleanupTestIdentity()
}
})
test('handleAddress - generates consistent addresses', async (t) => {
t.plan(3)
await cleanupTestIdentity()
mockConsole()
try {
// Create identity and profile first
await IdentityManager.generateIdentity()
await IdentityManager.createProfile('consistent', 'Consistent profile')
// Get address first time
const parsed = { command: 'address', flags: [], positional: ['consistent'] }
await handleAddress(parsed)
const output1 = consoleOutput.join('\n')
const addressMatch1 = output1.match(/Address: ([a-f0-9]{64})/)
t.ok(addressMatch1, 'Should generate address first time')
// Clear console output
consoleOutput = []
// Get address second time
await handleAddress(parsed)
const output2 = consoleOutput.join('\n')
const addressMatch2 = output2.match(/Address: ([a-f0-9]{64})/)
t.ok(addressMatch2, 'Should generate address second time')
// Addresses should be the same
if (addressMatch1 && addressMatch2) {
t.is(addressMatch1[1], addressMatch2[1], 'Addresses should be consistent')
} else {
t.fail('Could not extract addresses for comparison')
}
} catch (error) {
t.fail(`Should not throw error: ${error.message}`)
} finally {
restoreConsole()
await cleanupTestIdentity()
}
})
// Address Command Integration Tests
test('Address command integration - full workflow', async (t) => {
t.plan(6)
await cleanupTestIdentity()
mockConsole()
try {
// Create identity first
await IdentityManager.generateIdentity()
// Create multiple profiles
await IdentityManager.createProfile('work', 'Work profile')
await IdentityManager.createProfile('personal', 'Personal profile')
// Get address for work profile
const workParsed = { command: 'address', flags: [], positional: ['work'] }
await handleAddress(workParsed)
let output = consoleOutput.join('\n')
t.ok(output.includes('✅ Address generated successfully'), 'Should generate work address successfully')
t.ok(output.includes('Profile: work'), 'Should show work profile')
const workAddressMatch = output.match(/Address: ([a-f0-9]{64})/)
t.ok(workAddressMatch, 'Should extract work address')
// Clear console output
consoleOutput = []
// Get address for personal profile
const personalParsed = { command: 'address', flags: [], positional: ['personal'] }
await handleAddress(personalParsed)
output = consoleOutput.join('\n')
t.ok(output.includes('✅ Address generated successfully'), 'Should generate personal address successfully')
t.ok(output.includes('Profile: personal'), 'Should show personal profile')
const personalAddressMatch = output.match(/Address: ([a-f0-9]{64})/)
t.ok(personalAddressMatch, 'Should extract personal address')
// Addresses should be different for different profiles
if (workAddressMatch && personalAddressMatch) {
t.not(workAddressMatch[1], personalAddressMatch[1], 'Different profiles should have different addresses')
} else {
t.fail('Could not extract addresses for comparison')
}
} catch (error) {
t.fail(`Should not throw error: ${error.message}`)
} finally {
restoreConsole()
await cleanupTestIdentity()
}
})