OverviewFilesCommitsIssuesDocsReleasesPeers

keet-daemon / test / identity-bootstrap.test.js

181 lines · javascript ref:
// Standalone identity-bootstrap probe — plain Bare script, no brittle.
// Spins up keet-daemon against /tmp/keet-daemon-test-<rand>, drives the
// vault.setup RPC sequence, asserts the identity got materialized.
//
// Touches NO user storage. Cleanup unconditional.
//
// Run:
//   cd hyper-fleeky/packages/keet-daemon && bare test/identity-bootstrap.test.js
//   (or copy into any pgh-installed bare-runtime tree alongside its node_modules)
//
// Exit code 0 = success, 1 = any failure (with diagnostics printed).

import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import Pipe from 'bare-pipe'
import FramedStream from 'framed-stream'
import { startDaemon } from '../index.js'

let exitCode = 0
const failures = []

function ok (cond, msg) {
  if (cond) console.log('  ok   ' + msg)
  else { console.log('  FAIL ' + msg); exitCode = 1; failures.push(msg) }
}

function rmrf (dir) {
  try { fs.rmSync(dir, { recursive: true, force: true }) } catch {}
}

function mktemp (prefix) {
  const dir = path.join(os.tmpdir(), 'keet-daemon-test-' + prefix + '-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8))
  fs.mkdirSync(dir, { recursive: true })
  return dir
}

class TestClient {
  constructor (socketPath) {
    this._socketPath = socketPath
    this._pipe = null
    this._framed = null
    this._nextId = 1
    this._pending = new Map()
  }
  connect () {
    return new Promise((resolve, reject) => {
      const pipe = new Pipe(this._socketPath)
      const onErr = (err) => reject(err)
      pipe.once('error', onErr)
      pipe.once('connect', () => {
        pipe.removeListener('error', onErr)
        this._pipe = pipe
        const FS = FramedStream.default || FramedStream
        this._framed = new FS(pipe)
        this._framed.on('data', (buf) => {
          let msg
          try { msg = JSON.parse(buf.toString('utf-8')) } catch { return }
          const p = this._pending.get(msg.id)
          if (!p) return
          this._pending.delete(msg.id)
          if (msg.ok === false || msg.error) p.reject(new Error(msg.error || 'RPC failed'))
          else p.resolve(msg.data !== undefined ? msg.data : msg)
        })
        resolve()
      })
    })
  }
  call (method, params = {}, timeoutMs = 60000) {
    const id = this._nextId++
    const payload = JSON.stringify({ id, method, params })
    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        this._pending.delete(id)
        reject(new Error('RPC timeout: ' + method + ' (' + timeoutMs + 'ms)'))
      }, timeoutMs)
      this._pending.set(id, {
        resolve: (v) => { clearTimeout(timer); resolve(v) },
        reject: (e) => { clearTimeout(timer); reject(e) }
      })
      this._framed.write(Buffer.from(payload, 'utf-8'))
    })
  }
  close () {
    try { this._framed && this._framed.destroy() } catch {}
    try { this._pipe && this._pipe.destroy() } catch {}
  }
}

async function main () {
  const baseDir = mktemp('bootstrap')
  const socketPath = path.join(baseDir, 'daemon.sock')
  console.log('[test] baseDir = ' + baseDir)
  console.log('[test] socket  = ' + socketPath)

  let daemon
  try {
    daemon = await startDaemon({ storage: baseDir, socketPath })
    ok(true, 'startDaemon resolved')
  } catch (err) {
    ok(false, 'startDaemon threw: ' + err.message)
    rmrf(baseDir)
    return
  }

  const client = new TestClient(socketPath)
  try {
    await client.connect()
    ok(true, 'UDS client connected')
  } catch (err) {
    ok(false, 'UDS connect failed: ' + err.message)
    try { await daemon.core.close() } catch {}
    rmrf(baseDir)
    return
  }

  // 1. List identities — should show identity-0 (created by migration)
  try {
    const list = await client.call('identity-manager.list', {}, 10000)
    ok(list && Array.isArray(list.identities), 'list.identities is an array')
    ok(list.activeId === 'identity-0', 'activeId === identity-0; got ' + list.activeId)
    ok(list.identities.length === 1, 'one identity present; got ' + list.identities.length)
  } catch (err) {
    ok(false, 'identity-manager.list threw: ' + err.message)
  }

  // 2. vault.setup — the actual bootstrap step
  const password = 'test-pw-' + Math.random().toString(36).slice(2, 10)
  console.log('[test] calling vault.setup with password=' + password)
  const t0 = Date.now()
  try {
    const setupResult = await client.call('vault.setup', { password }, 90000)
    const elapsed = Date.now() - t0
    ok(true, 'vault.setup OK in ' + elapsed + 'ms')
    ok(setupResult && setupResult.ok === true, 'vault.setup returned {ok:true}; got ' + JSON.stringify(setupResult))
  } catch (err) {
    const elapsed = Date.now() - t0
    ok(false, 'vault.setup failed in ' + elapsed + 'ms: ' + err.message)
  }

  // 3. identity RPC should now return a publicKey
  try {
    const id = await client.call('identity', {}, 10000)
    ok(id && id.publicKey, 'identity.publicKey present after setup: ' + (id && id.publicKey ? id.publicKey.slice(0, 16) + '…' : '<missing>'))
  } catch (err) {
    ok(false, 'identity RPC failed: ' + err.message)
  }

  // 4. Disk evidence: identity-0/0/db should be populated
  const idDir = path.join(baseDir, 'identity-0')
  ok(fs.existsSync(idDir), 'identity-0 dir exists on disk')
  const dbDir = path.join(idDir, '0', 'db')
  if (fs.existsSync(dbDir)) {
    const entries = fs.readdirSync(dbDir)
    ok(entries.length > 0, 'identity-0/0/db has ' + entries.length + ' files')
  } else {
    ok(false, 'identity-0/0/db does not exist')
  }

  // Cleanup
  console.log('[test] cleaning up…')
  client.close()
  try { await daemon.core.close() } catch {}
  try { await daemon.uds.stop() } catch {}
  rmrf(baseDir)

  console.log('')
  console.log('=== RESULT ===')
  if (exitCode === 0) console.log('ALL PASSED')
  else {
    console.log(failures.length + ' FAILED:')
    for (const f of failures) console.log('  - ' + f)
  }
  Bare.exit(exitCode)
}

main().catch(err => {
  console.error('uncaught:', err)
  Bare.exit(1)
})