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()
})