OverviewFilesCommitsIssuesDocsReleasesPeers

hyperident / test / identity-manager.test.js

1121 lines · javascript ref:
import test from 'brittle'
import { IdentityManager } from '../lib/identity-manager.js'
import { FileManager } from '../lib/file-ops.js'
import IdentityKey from 'keet-identity-key'
import fs from 'fs'
import path from 'path'
import os from 'os'

// ISOLATION. This suite used to resolve the vault via os.homedir() and delete it — so `npm test`
// destroyed the real ~/.hyperident of whoever ran it, on the FIRST test, every time. It did exactly
// that on 2026-08-17 (hyper-social #47). Two defences, because an env var alone fails OPEN if someone
// forgets it:
//   1. redirect the vault into a temp dir for the whole run
//   2. a hard guard that REFUSES to delete the real path even if 1 is missing
const TEST_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'hyperident-test-'))
process.env.HYPERIDENT_HOME = TEST_HOME
FileManager.setBasePath(path.join(TEST_HOME, '.hyperident'))

const REAL_VAULT = path.resolve(path.join(os.homedir(), '.hyperident'))
function assertNotRealVault (p) {
  if (path.resolve(p) === REAL_VAULT) {
    throw new Error('REFUSING to delete the real vault at ' + p + ' — a test must redirect via FileManager.setBasePath()/HYPERIDENT_HOME first')
  }
}

// Test helper to clean up test files
async function cleanupTestFiles() {
  try {
    const hyperidentPath = FileManager.getHyperidentPath()
    assertNotRealVault(hyperidentPath)
    if (fs.existsSync(hyperidentPath)) {
      // Remove all files in the directory
      const files = fs.readdirSync(hyperidentPath)
      for (const file of files) {
        fs.unlinkSync(path.join(hyperidentPath, file))
      }
      fs.rmdirSync(hyperidentPath)
    }
  } catch (error) {
    // Ignore cleanup errors
  }
}

test('IdentityManager - generate identity without passphrase', async (t) => {
  t.plan(7)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    const identity = await IdentityManager.generateIdentity()
    
    t.ok(identity, 'Identity should be generated')
    t.ok(identity.mnemonic, 'Identity should have mnemonic')
    t.ok(identity.publicKey, 'Identity should have public key')
    t.is(identity.version, '1.0.0', 'Identity should have correct version')
    t.is(identity.algorithm, 'keet-identity-key', 'Identity should use keet-identity-key')
    t.ok(identity.created, 'Identity should have creation timestamp')
    
    // Verify mnemonic is valid by generating keys from it
    const identityKey = await IdentityKey.from({ mnemonic: identity.mnemonic })
    t.is(identity.publicKey, identityKey.identityKeyPair.publicKey.toString('hex'), 'Public key should match mnemonic-derived key')
    
  } catch (error) {
    t.fail(`Should not throw error: ${error.message}`)
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - generate identity with passphrase', async (t) => {
  t.plan(7)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    const passphrase = 'test-passphrase-123'
    const identity = await IdentityManager.generateIdentity(passphrase)
    
    t.ok(identity, 'Identity should be generated')
    t.ok(identity.mnemonic, 'Identity should have mnemonic')
    t.ok(identity.publicKey, 'Identity should have public key')
    t.is(identity.version, '1.0.0', 'Identity should have correct version')
    t.is(identity.algorithm, 'keet-identity-key', 'Identity should use keet-identity-key')
    t.ok(identity.created, 'Identity should have creation timestamp')
    
    // Verify mnemonic is valid by generating keys from it
    const identityKey = await IdentityKey.from({ mnemonic: identity.mnemonic })
    t.is(identity.publicKey, identityKey.identityKeyPair.publicKey.toString('hex'), 'Public key should match mnemonic-derived key')
    
  } catch (error) {
    t.fail(`Should not throw error: ${error.message}`)
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - save and load identity without passphrase', async (t) => {
  t.plan(7)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    const mnemonic = IdentityKey.generateMnemonic()
    
    // Save identity
    await IdentityManager.saveIdentity(mnemonic)
    
    // Load identity
    const identity = await IdentityManager.loadIdentity()
    
    t.ok(identity, 'Identity should be loaded')
    t.is(identity.mnemonic, mnemonic, 'Loaded mnemonic should match saved mnemonic')
    t.ok(identity.publicKey, 'Identity should have public key')
    t.is(identity.version, '1.0.0', 'Identity should have correct version')
    t.is(identity.algorithm, 'keet-identity-key', 'Identity should use keet-identity-key')
    t.ok(identity.created, 'Identity should have creation timestamp')
    t.is(identity.encrypted, false, 'Identity should not be encrypted')
    
  } catch (error) {
    t.fail(`Should not throw error: ${error.message}`)
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - save and load identity with passphrase', async (t) => {
  t.plan(7)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    const mnemonic = IdentityKey.generateMnemonic()
    const passphrase = 'test-passphrase-456'
    
    // Save identity with passphrase
    await IdentityManager.saveIdentity(mnemonic, passphrase)
    
    // Load identity with passphrase
    const identity = await IdentityManager.loadIdentity(passphrase)
    
    t.ok(identity, 'Identity should be loaded')
    t.is(identity.mnemonic, mnemonic, 'Loaded mnemonic should match saved mnemonic')
    t.ok(identity.publicKey, 'Identity should have public key')
    t.is(identity.version, '1.0.0', 'Identity should have correct version')
    t.is(identity.algorithm, 'keet-identity-key', 'Identity should use keet-identity-key')
    t.is(identity.encrypted, true, 'Identity should be encrypted')
    
    // Verify keys match mnemonic
    const identityKey = await IdentityKey.from({ mnemonic })
    t.is(identity.publicKey, identityKey.identityKeyPair.publicKey.toString('hex'), 'Public key should match mnemonic-derived key')
    
  } catch (error) {
    t.fail(`Should not throw error: ${error.message}`)
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - load encrypted identity with wrong passphrase', async (t) => {
  t.plan(1)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    const mnemonic = IdentityKey.generateMnemonic()
    const passphrase = 'correct-passphrase'
    const wrongPassphrase = 'wrong-passphrase'
    
    // Save identity with passphrase
    await IdentityManager.saveIdentity(mnemonic, passphrase)
    
    // Try to load with wrong passphrase
    await IdentityManager.loadIdentity(wrongPassphrase)
    t.fail('Should throw error for wrong passphrase')
    
  } catch (error) {
    t.is(error.message, 'Failed to load identity: Invalid passphrase', 'Should throw invalid passphrase error')
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - load encrypted identity without passphrase', async (t) => {
  t.plan(1)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    const mnemonic = IdentityKey.generateMnemonic()
    const passphrase = 'test-passphrase'
    
    // Save identity with passphrase
    await IdentityManager.saveIdentity(mnemonic, passphrase)
    
    // Try to load without passphrase
    await IdentityManager.loadIdentity()
    t.fail('Should throw error when no passphrase provided for encrypted identity')
    
  } catch (error) {
    t.is(error.message, 'Failed to load identity: Identity is encrypted but no passphrase provided', 'Should throw missing passphrase error')
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - load unencrypted identity with passphrase', async (t) => {
  t.plan(1)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    const mnemonic = IdentityKey.generateMnemonic()
    
    // Save identity without passphrase
    await IdentityManager.saveIdentity(mnemonic)
    
    // Try to load with passphrase
    await IdentityManager.loadIdentity('some-passphrase')
    t.fail('Should throw error when passphrase provided for unencrypted identity')
    
  } catch (error) {
    t.is(error.message, 'Failed to load identity: Identity is not encrypted but passphrase provided', 'Should throw unexpected passphrase error')
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - get public key', async (t) => {
  t.plan(2)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    const mnemonic = IdentityKey.generateMnemonic()
    const identityKey = await IdentityKey.from({ mnemonic })
    
    // Save identity
    await IdentityManager.saveIdentity(mnemonic)
    
    // Get public key
    const publicKey = await IdentityManager.getPublicKey()
    
    t.ok(publicKey, 'Public key should be returned')
    t.is(publicKey, identityKey.identityKeyPair.publicKey.toString('hex'), 'Public key should match expected value')
    
  } catch (error) {
    t.fail(`Should not throw error: ${error.message}`)
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - get public key when identity not found', async (t) => {
  t.plan(1)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    await IdentityManager.getPublicKey()
    t.fail('Should throw error when identity not found')
    
  } catch (error) {
    t.is(error.message, 'Identity not found. Run "hyperident init" to create one.', 'Should throw identity not found error')
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - identity exists', async (t) => {
  t.plan(2)
  
  // Clean up before test
  await cleanupTestFiles()
  
  // Check when identity doesn't exist
  t.is(IdentityManager.identityExists(), false, 'Should return false when identity does not exist')
  
  // Create identity
  const mnemonic = IdentityKey.generateMnemonic()
  await IdentityManager.saveIdentity(mnemonic)
  
  // Check when identity exists
  t.is(IdentityManager.identityExists(), true, 'Should return true when identity exists')
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - load identity when not found', async (t) => {
  t.plan(1)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    await IdentityManager.loadIdentity()
    t.fail('Should throw error when identity not found')
    
  } catch (error) {
    t.is(error.message, 'Failed to load identity: Identity not found. Run "hyperident init" to create one.', 'Should throw identity not found error')
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

// Test that profiles.json is created during identity save
test('IdentityManager - profiles.json created during save', async (t) => {
  t.plan(2)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    const mnemonic = IdentityKey.generateMnemonic()
    
    // Save identity
    await IdentityManager.saveIdentity(mnemonic)
    
    // Check that profiles.json exists and has correct structure
    const profiles = await FileManager.readProfiles()
    
    t.ok(profiles, 'Profiles file should be created')
    t.is(profiles.version, '1.0.0', 'Profiles should have correct version')
    
  } catch (error) {
    t.fail(`Should not throw error: ${error.message}`)
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

// Test mnemonic format validation
test('IdentityManager - mnemonic format validation', async (t) => {
  t.plan(2)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    const identity = await IdentityManager.generateIdentity()
    
    // Check mnemonic format (should be 24 words for keet-identity-key)
    const words = identity.mnemonic.split(' ')
    t.is(words.length, 24, 'Mnemonic should have 24 words')
    
    // Verify each word is non-empty
    const validWords = words.every(word => word.length > 0)
    t.ok(validWords, 'All mnemonic words should be non-empty')
    
  } catch (error) {
    t.fail(`Should not throw error: ${error.message}`)
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

// Test encryption round trip
test('IdentityManager - encryption round trip', async (t) => {
  t.plan(3)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    const mnemonic1 = IdentityKey.generateMnemonic()
    const mnemonic2 = IdentityKey.generateMnemonic()
    const passphrase = 'test-passphrase-789'
    
    // Save two different identities with same passphrase
    await IdentityManager.saveIdentity(mnemonic1, passphrase)
    
    // Load first identity
    const identity1 = await IdentityManager.loadIdentity(passphrase)
    t.is(identity1.mnemonic, mnemonic1, 'First identity should be loaded correctly')
    
    // Clean up and save second identity
    await cleanupTestFiles()
    await IdentityManager.saveIdentity(mnemonic2, passphrase)
    
    // Load second identity
    const identity2 = await IdentityManager.loadIdentity(passphrase)
    t.is(identity2.mnemonic, mnemonic2, 'Second identity should be loaded correctly')
    
    // Verify they are different
    t.not(identity1.mnemonic, identity2.mnemonic, 'Different mnemonics should produce different identities')
    
  } catch (error) {
    t.fail(`Should not throw error: ${error.message}`)
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

// Profile Management Tests (Step 5)

test('IdentityManager - validate profile name - valid names', async (t) => {
  t.plan(6)
  
  t.ok(IdentityManager.validateProfileName('work'), 'Simple name should be valid')
  t.ok(IdentityManager.validateProfileName('personal'), 'Longer name should be valid')
  t.ok(IdentityManager.validateProfileName('work-server'), 'Name with hyphen should be valid')
  t.ok(IdentityManager.validateProfileName('test123'), 'Name with numbers should be valid')
  t.ok(IdentityManager.validateProfileName('a'), 'Single character should be valid')
  t.ok(IdentityManager.validateProfileName('a'.repeat(32)), '32 character name should be valid')
})

test('IdentityManager - validate profile name - invalid names', async (t) => {
  t.plan(12)
  
  t.ok(!IdentityManager.validateProfileName(''), 'Empty name should be invalid')
  t.ok(!IdentityManager.validateProfileName(null), 'Null should be invalid')
  t.ok(!IdentityManager.validateProfileName(undefined), 'Undefined should be invalid')
  t.ok(!IdentityManager.validateProfileName(123), 'Number should be invalid')
  t.ok(!IdentityManager.validateProfileName('a'.repeat(33)), 'Name longer than 32 chars should be invalid')
  t.ok(!IdentityManager.validateProfileName('work space'), 'Name with space should be invalid')
  t.ok(!IdentityManager.validateProfileName('work@home'), 'Name with @ should be invalid')
  t.ok(!IdentityManager.validateProfileName('-work'), 'Name starting with hyphen should be invalid')
  t.ok(!IdentityManager.validateProfileName('work-'), 'Name ending with hyphen should be invalid')
  t.ok(!IdentityManager.validateProfileName('work--server'), 'Name with consecutive hyphens should be invalid')
  t.ok(!IdentityManager.validateProfileName('default'), 'Reserved name "default" should be invalid')
  t.ok(!IdentityManager.validateProfileName('CONFIG'), 'Reserved name "config" (case insensitive) should be invalid')
})

test('IdentityManager - create profile without identity', async (t) => {
  t.plan(1)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    await IdentityManager.createProfile('work')
    t.fail('Should throw error when identity does not exist')
  } catch (error) {
    t.is(error.message, 'Failed to create profile: Identity not found. Run "hyperident init" to create one.', 'Should throw identity not found error')
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - create profile with valid name', async (t) => {
  t.plan(4)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    // Create identity first
    const mnemonic = IdentityKey.generateMnemonic()
    await IdentityManager.saveIdentity(mnemonic)
    
    // Create profile
    await IdentityManager.createProfile('work', 'Work profile')
    
    // Verify profile was created
    const profiles = await FileManager.readProfiles()
    t.ok(profiles.profiles.work, 'Profile should be created')
    t.is(profiles.profiles.work.name, 'work', 'Profile name should match')
    t.is(profiles.profiles.work.description, 'Work profile', 'Profile description should match')
    t.ok(profiles.profiles.work.created, 'Profile should have creation timestamp')
    
  } catch (error) {
    t.fail(`Should not throw error: ${error.message}`)
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - create profile with invalid name', async (t) => {
  t.plan(1)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    // Create identity first
    const mnemonic = IdentityKey.generateMnemonic()
    await IdentityManager.saveIdentity(mnemonic)
    
    // Try to create profile with invalid name
    await IdentityManager.createProfile('invalid name')
    t.fail('Should throw error for invalid profile name')
    
  } catch (error) {
    t.is(error.message, 'Failed to create profile: Invalid profile name. Must be 1-32 characters, alphanumeric and hyphens only.', 'Should throw invalid name error')
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - create duplicate profile', async (t) => {
  t.plan(1)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    // Create identity first
    const mnemonic = IdentityKey.generateMnemonic()
    await IdentityManager.saveIdentity(mnemonic)
    
    // Create first profile
    await IdentityManager.createProfile('work')
    
    // Try to create duplicate profile
    await IdentityManager.createProfile('work')
    t.fail('Should throw error for duplicate profile')
    
  } catch (error) {
    t.is(error.message, "Failed to create profile: Profile 'work' already exists", 'Should throw duplicate profile error')
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - list profiles when empty', async (t) => {
  t.plan(2)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    // Create identity first
    const mnemonic = IdentityKey.generateMnemonic()
    await IdentityManager.saveIdentity(mnemonic)
    
    // List profiles
    const profiles = await IdentityManager.listProfiles()
    
    t.ok(profiles, 'Profiles object should be returned')
    t.is(Object.keys(profiles.profiles).length, 0, 'Should have no profiles')
    
  } catch (error) {
    t.fail(`Should not throw error: ${error.message}`)
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - list profiles with multiple profiles', async (t) => {
  t.plan(8)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    // Create identity first
    const mnemonic = IdentityKey.generateMnemonic()
    await IdentityManager.saveIdentity(mnemonic)
    
    // Create multiple profiles
    await IdentityManager.createProfile('work', 'Work profile')
    await IdentityManager.createProfile('personal', 'Personal profile')
    await IdentityManager.createProfile('server', 'Server profile')
    
    // List profiles
    const profiles = await IdentityManager.listProfiles()
    
    t.ok(profiles, 'Profiles object should be returned')
    t.is(Object.keys(profiles.profiles).length, 3, 'Should have 3 profiles')
    
    // Check alphabetical sorting
    const profileNames = Object.keys(profiles.profiles)
    const expectedOrder = ['personal', 'server', 'work']
    t.is(profileNames.length, expectedOrder.length, 'Profile count should match expected')
    for (let i = 0; i < expectedOrder.length; i++) {
      t.is(profileNames[i], expectedOrder[i], `Profile ${i} should be ${expectedOrder[i]}`)
    }
    
    // Check profile data
    t.is(profiles.profiles.work.description, 'Work profile', 'Work profile description should match')
    t.is(profiles.profiles.personal.description, 'Personal profile', 'Personal profile description should match')
    
  } catch (error) {
    t.fail(`Should not throw error: ${error.message}`)
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - list profiles without identity', async (t) => {
  t.plan(1)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    await IdentityManager.listProfiles()
    t.fail('Should throw error when identity does not exist')
  } catch (error) {
    t.is(error.message, 'Failed to list profiles: Identity not found. Run "hyperident init" to create one.', 'Should throw identity not found error')
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - get profile', async (t) => {
  t.plan(3)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    // Create identity first
    const mnemonic = IdentityKey.generateMnemonic()
    await IdentityManager.saveIdentity(mnemonic)
    
    // Create profile
    await IdentityManager.createProfile('work', 'Work profile')
    
    // Get profile
    const profile = await IdentityManager.getProfile('work')
    
    t.is(profile.name, 'work', 'Profile name should match')
    t.is(profile.description, 'Work profile', 'Profile description should match')
    t.ok(profile.created, 'Profile should have creation timestamp')
    
  } catch (error) {
    t.fail(`Should not throw error: ${error.message}`)
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - get non-existent profile', async (t) => {
  t.plan(1)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    // Create identity first
    const mnemonic = IdentityKey.generateMnemonic()
    await IdentityManager.saveIdentity(mnemonic)
    
    // Try to get non-existent profile
    await IdentityManager.getProfile('nonexistent')
    t.fail('Should throw error for non-existent profile')
    
  } catch (error) {
    t.is(error.message, "Failed to get profile: Profile 'nonexistent' not found", 'Should throw profile not found error')
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - profile exists', async (t) => {
  t.plan(2)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    // Create identity first
    const mnemonic = IdentityKey.generateMnemonic()
    await IdentityManager.saveIdentity(mnemonic)
    
    // Check non-existent profile
    const exists1 = await IdentityManager.profileExists('work')
    t.is(exists1, false, 'Non-existent profile should return false')
    
    // Create profile
    await IdentityManager.createProfile('work')
    
    // Check existing profile
    const exists2 = await IdentityManager.profileExists('work')
    t.is(exists2, true, 'Existing profile should return true')
    
  } catch (error) {
    t.fail(`Should not throw error: ${error.message}`)
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - delete profile', async (t) => {
  t.plan(2)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    // Create identity first
    const mnemonic = IdentityKey.generateMnemonic()
    await IdentityManager.saveIdentity(mnemonic)
    
    // Create profile
    await IdentityManager.createProfile('work')
    
    // Verify profile exists
    const exists1 = await IdentityManager.profileExists('work')
    t.is(exists1, true, 'Profile should exist before deletion')
    
    // Delete profile
    await IdentityManager.deleteProfile('work')
    
    // Verify profile is deleted
    const exists2 = await IdentityManager.profileExists('work')
    t.is(exists2, false, 'Profile should not exist after deletion')
    
  } catch (error) {
    t.fail(`Should not throw error: ${error.message}`)
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - delete non-existent profile', async (t) => {
  t.plan(1)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    // Create identity first
    const mnemonic = IdentityKey.generateMnemonic()
    await IdentityManager.saveIdentity(mnemonic)
    
    // Try to delete non-existent profile
    await IdentityManager.deleteProfile('nonexistent')
    t.fail('Should throw error for non-existent profile')
    
  } catch (error) {
    t.is(error.message, "Failed to delete profile: Profile 'nonexistent' not found", 'Should throw profile not found error')
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - delete profile without identity', async (t) => {
  t.plan(1)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    await IdentityManager.deleteProfile('work')
    t.fail('Should throw error when identity does not exist')
  } catch (error) {
    t.is(error.message, 'Failed to delete profile: Identity not found. Run "hyperident init" to create one.', 'Should throw identity not found error')
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - update profile', async (t) => {
  t.plan(3)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    // Create identity first
    const mnemonic = IdentityKey.generateMnemonic()
    await IdentityManager.saveIdentity(mnemonic)
    
    // Create profile
    await IdentityManager.createProfile('work', 'Original description')
    
    // Add a small delay to ensure different timestamps
    await new Promise(resolve => setTimeout(resolve, 10))
    
    // Update profile
    await IdentityManager.updateProfile('work', { description: 'Updated description' })
    
    // Get updated profile
    const profile = await IdentityManager.getProfile('work')
    
    t.is(profile.description, 'Updated description', 'Profile description should be updated')
    t.ok(profile.lastModified, 'Profile should have lastModified timestamp')
    t.not(profile.created, profile.lastModified, 'lastModified should be different from created')
    
  } catch (error) {
    t.fail(`Should not throw error: ${error.message}`)
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - update non-existent profile', async (t) => {
  t.plan(1)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    // Create identity first
    const mnemonic = IdentityKey.generateMnemonic()
    await IdentityManager.saveIdentity(mnemonic)
    
    // Try to update non-existent profile
    await IdentityManager.updateProfile('nonexistent', { description: 'New description' })
    t.fail('Should throw error for non-existent profile')
    
  } catch (error) {
    t.is(error.message, "Failed to update profile: Profile 'nonexistent' not found", 'Should throw profile not found error')
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

// Topic Derivation Tests (Step 6)

test('IdentityManager - derive topic from profile - deterministic', async (t) => {
  t.plan(3)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    // Create identity first
    const mnemonic = IdentityKey.generateMnemonic()
    await IdentityManager.saveIdentity(mnemonic)
    
    // Create profile
    await IdentityManager.createProfile('work', 'Work profile')
    
    // Derive topic twice
    const topic1 = await IdentityManager.deriveTopicFromProfile('work')
    const topic2 = await IdentityManager.deriveTopicFromProfile('work')
    
    t.ok(Buffer.isBuffer(topic1), 'Topic should be a Buffer')
    t.is(topic1.length, 32, 'Topic should be 32 bytes')
    t.ok(topic1.equals(topic2), 'Same inputs should produce same topic (deterministic)')
    
  } catch (error) {
    t.fail(`Should not throw error: ${error.message}`)
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - derive topic from profile - different profiles produce different topics', async (t) => {
  t.plan(3)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    // Create identity first
    const mnemonic = IdentityKey.generateMnemonic()
    await IdentityManager.saveIdentity(mnemonic)
    
    // Create multiple profiles
    await IdentityManager.createProfile('work', 'Work profile')
    await IdentityManager.createProfile('personal', 'Personal profile')
    
    // Derive topics
    const workTopic = await IdentityManager.deriveTopicFromProfile('work')
    const personalTopic = await IdentityManager.deriveTopicFromProfile('personal')
    
    t.is(workTopic.length, 32, 'Work topic should be 32 bytes')
    t.is(personalTopic.length, 32, 'Personal topic should be 32 bytes')
    t.ok(!workTopic.equals(personalTopic), 'Different profiles should produce different topics')
    
  } catch (error) {
    t.fail(`Should not throw error: ${error.message}`)
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - derive topic from profile - different identities produce different topics', async (t) => {
  t.plan(3)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    // Create first identity and profile
    const mnemonic1 = IdentityKey.generateMnemonic()
    await IdentityManager.saveIdentity(mnemonic1)
    await IdentityManager.createProfile('work', 'Work profile')
    const topic1 = await IdentityManager.deriveTopicFromProfile('work')
    
    // Clean up and create second identity
    await cleanupTestFiles()
    const mnemonic2 = IdentityKey.generateMnemonic()
    await IdentityManager.saveIdentity(mnemonic2)
    await IdentityManager.createProfile('work', 'Work profile')
    const topic2 = await IdentityManager.deriveTopicFromProfile('work')
    
    t.is(topic1.length, 32, 'First topic should be 32 bytes')
    t.is(topic2.length, 32, 'Second topic should be 32 bytes')
    t.ok(!topic1.equals(topic2), 'Same profile across different identities should produce different topics')
    
  } catch (error) {
    t.fail(`Should not throw error: ${error.message}`)
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - derive topic from profile - with encrypted identity', async (t) => {
  t.plan(2)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    // Create encrypted identity
    const mnemonic = IdentityKey.generateMnemonic()
    const passphrase = 'test-passphrase'
    await IdentityManager.saveIdentity(mnemonic, passphrase)
    
    // Create profile
    await IdentityManager.createProfile('work', 'Work profile')
    
    // Derive topic with passphrase
    const topic = await IdentityManager.deriveTopicFromProfile('work', passphrase)
    
    t.ok(Buffer.isBuffer(topic), 'Topic should be a Buffer')
    t.is(topic.length, 32, 'Topic should be 32 bytes')
    
  } catch (error) {
    t.fail(`Should not throw error: ${error.message}`)
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - derive topic from profile - missing identity', async (t) => {
  t.plan(1)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    await IdentityManager.deriveTopicFromProfile('work')
    t.fail('Should throw error when identity does not exist')
  } catch (error) {
    t.is(error.message, 'Failed to derive topic from profile: Identity not found. Run "hyperident init" to create one.', 'Should throw identity not found error')
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - derive topic from profile - missing profile', async (t) => {
  t.plan(1)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    // Create identity but no profile
    const mnemonic = IdentityKey.generateMnemonic()
    await IdentityManager.saveIdentity(mnemonic)
    
    await IdentityManager.deriveTopicFromProfile('nonexistent')
    t.fail('Should throw error for non-existent profile')
  } catch (error) {
    t.is(error.message, 'Failed to derive topic from profile: Profile \'nonexistent\' not found. Create it first with "hyperident create-profile nonexistent".', 'Should throw profile not found error')
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - derive topic from profile - encrypted identity without passphrase', async (t) => {
  t.plan(1)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    // Create encrypted identity
    const mnemonic = IdentityKey.generateMnemonic()
    const passphrase = 'test-passphrase'
    await IdentityManager.saveIdentity(mnemonic, passphrase)
    
    // Create profile
    await IdentityManager.createProfile('work', 'Work profile')
    
    // Try to derive topic without passphrase
    await IdentityManager.deriveTopicFromProfile('work')
    t.fail('Should throw error when no passphrase provided for encrypted identity')
  } catch (error) {
    t.is(error.message, 'Failed to derive topic from profile: Failed to load identity: Identity is encrypted but no passphrase provided', 'Should throw missing passphrase error')
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - derive topic from profile - invalid inputs', async (t) => {
  t.plan(3)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    // Create identity first
    const mnemonic = IdentityKey.generateMnemonic()
    await IdentityManager.saveIdentity(mnemonic)
    
    // Test empty profile name
    await IdentityManager.deriveTopicFromProfile('')
    t.fail('Should throw error for empty profile name')
  } catch (error) {
    t.is(error.message, 'Failed to derive topic from profile: Profile name is required and must be a string', 'Should throw error for empty profile name')
  }
  
  try {
    // Test null profile name
    await IdentityManager.deriveTopicFromProfile(null)
    t.fail('Should throw error for null profile name')
  } catch (error) {
    t.is(error.message, 'Failed to derive topic from profile: Profile name is required and must be a string', 'Should throw error for null profile name')
  }
  
  try {
    // Test non-string profile name
    await IdentityManager.deriveTopicFromProfile(123)
    t.fail('Should throw error for non-string profile name')
  } catch (error) {
    t.is(error.message, 'Failed to derive topic from profile: Profile name is required and must be a string', 'Should throw error for non-string profile name')
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - get profile address', async (t) => {
  t.plan(2)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    // Create identity first
    const mnemonic = IdentityKey.generateMnemonic()
    await IdentityManager.saveIdentity(mnemonic)
    
    // Create profile
    await IdentityManager.createProfile('work', 'Work profile')
    
    // Get profile address
    const address = await IdentityManager.getProfileAddress('work')
    
    t.ok(typeof address === 'string', 'Address should be a string')
    t.is(address.length, 64, 'Address should be 64 hex characters (32 bytes)')
    
  } catch (error) {
    t.fail(`Should not throw error: ${error.message}`)
  }
  
  // Clean up after test
  await cleanupTestFiles()
})

test('IdentityManager - validate profile', async (t) => {
  t.plan(2)
  
  // Clean up before test
  await cleanupTestFiles()
  
  try {
    // Create identity first
    const mnemonic = IdentityKey.generateMnemonic()
    await IdentityManager.saveIdentity(mnemonic)
    
    // Create profile
    await IdentityManager.createProfile('work', 'Work profile')
    
    // Validate existing profile (should not throw)
    await IdentityManager.validateProfile('work')
    t.pass('Should not throw error for existing profile')
    
    // Try to validate non-existent profile
    await IdentityManager.validateProfile('nonexistent')
    t.fail('Should throw error for non-existent profile')
    
  } catch (error) {
    t.is(error.message, 'Profile \'nonexistent\' not found. Create it first with "hyperident create-profile nonexistent".', 'Should throw profile not found error')
  }
  
  // Clean up after test
  await cleanupTestFiles()
})