OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-note / test / unit / first-run.test.js

150 lines · javascript ref:
import test from 'brittle'
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import { IdentityManager } from '../../lib/identity/identity-manager.js'

function tmpDir() {
  const dir = path.join(os.tmpdir(), 'hn-firstrun-' + Date.now() + '-' + Math.random().toString(36).slice(2))
  fs.mkdirSync(dir, { recursive: true })
  return dir
}

function rmrf(dir) {
  try {
    for (const f of fs.readdirSync(dir)) {
      const fp = path.join(dir, f)
      const stat = fs.statSync(fp)
      if (stat.isDirectory()) rmrf(fp)
      else fs.unlinkSync(fp)
    }
    fs.rmdirSync(dir)
  } catch {}
}

test('first-run - ensureKeypair creates identity file', async (t) => {
  const dir = tmpDir()
  const mgr = new IdentityManager({
    storagePath: dir,
    identityFile: path.join(dir, 'device-identity.json')
  })

  const isNew = await mgr.ensureKeypair()
  t.ok(isNew, 'newly generated')

  // Read back
  const data = JSON.parse(fs.readFileSync(path.join(dir, 'device-identity.json'), 'utf-8'))
  t.ok(data.publicKey, 'has publicKey')
  t.ok(data.secretKey, 'has secretKey')
  t.is(data.publicKey.length, 64, 'publicKey is 32 bytes hex')

  // Second call is a no-op
  const isNew2 = await mgr.ensureKeypair()
  t.absent(isNew2, 'second call returns false')

  rmrf(dir)
})

test('first-run - setup creates vault + proof', async (t) => {
  const dir = tmpDir()
  const mgr = new IdentityManager({
    storagePath: dir,
    identityFile: path.join(dir, 'device-identity.json')
  })

  await mgr.ensureKeypair()

  const password = Buffer.from('test-password-123')
  const result = await mgr.setup(password)

  t.ok(result.mnemonic, 'returns mnemonic')
  t.ok(result.mnemonic.split(' ').length >= 12, 'mnemonic has 12+ words')
  t.ok(result.identityPublicKey, 'returns identity public key')

  // Check vault exists
  t.ok(mgr.vault.exists(), 'vault file exists')

  // Check proof stored
  const status = mgr.status()
  t.ok(status.hasKeypair, 'has keypair')
  t.ok(status.hasVault, 'has vault')
  t.ok(status.hasProof, 'has proof')
  t.ok(status.identityPublicKey, 'has identity public key')

  rmrf(dir)
})

test('first-run - verifyProof validates stored proof', async (t) => {
  const dir = tmpDir()
  const mgr = new IdentityManager({
    storagePath: dir,
    identityFile: path.join(dir, 'device-identity.json')
  })

  await mgr.ensureKeypair()
  await mgr.setup(Buffer.from('pass123'))

  const verification = mgr.verifyProof()
  t.ok(verification.valid, 'proof is valid')
  t.ok(verification.identityPublicKey, 'has identity public key')
  t.ok(verification.devicePublicKey, 'has device public key')

  rmrf(dir)
})

test('first-run - exportMnemonic requires correct password', async (t) => {
  const dir = tmpDir()
  const mgr = new IdentityManager({
    storagePath: dir,
    identityFile: path.join(dir, 'device-identity.json')
  })

  await mgr.ensureKeypair()
  const password = Buffer.from('export-test-pw')
  const result = await mgr.setup(password)

  // Export with correct password
  const exported = await mgr.exportMnemonic(password)
  t.is(exported, result.mnemonic, 'exported mnemonic matches')

  // Export with wrong password fails
  try {
    await mgr.exportMnemonic(Buffer.from('wrong'))
    t.fail('should have thrown')
  } catch (err) {
    t.pass('wrong password rejected')
  }

  rmrf(dir)
})

test('first-run - setupFromMnemonic restores identity on new device', async (t) => {
  // Simulate first device
  const dir1 = tmpDir()
  const mgr1 = new IdentityManager({
    storagePath: dir1,
    identityFile: path.join(dir1, 'device-identity.json')
  })
  await mgr1.ensureKeypair()
  const result1 = await mgr1.setup(Buffer.from('pw1'))

  // Simulate second device
  const dir2 = tmpDir()
  const mgr2 = new IdentityManager({
    storagePath: dir2,
    identityFile: path.join(dir2, 'device-identity.json')
  })
  await mgr2.ensureKeypair()
  const result2 = await mgr2.setupFromMnemonic(result1.mnemonic, Buffer.from('pw2'))

  // Same identity public key, different device keys
  t.is(result2.identityPublicKey, result1.identityPublicKey, 'same identity key')

  const status1 = mgr1.status()
  const status2 = mgr2.status()
  t.not(status1.devicePublicKey, status2.devicePublicKey, 'different device keys')

  rmrf(dir1)
  rmrf(dir2)
})