OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-identity / test / create-user-no-fabrication.test.js

90 lines · javascript ref:
// createUser must never report success for a write that did not land.
//
// Current behaviour (identity-registry.js:133-138):
//     await this._base.append(encodeOp(op))
//     await this._base.update()
//     const user = await this.getUser(userId)
//     return user || op          // <-- fabricates a user from the in-memory op
//
// `op` carries a userId minted locally at :111, so the caller receives an
// object indistinguishable from a persisted user. In production this turned a
// peer that could not linearize into a silent data-loss bug: every request
// re-ran TOFU, got a fresh fabricated userId, and the profile page rendered
// "Not authenticated yet" forever — with no error anywhere.
//
// The rule this restores is the one every durable autobase app follows
// (autobase skill, admission-patterns.md): "if admission can't be reached,
// THROW — never report success on a write that won't commit."

import test from 'brittle'
import Corestore from 'corestore'
import Hyperbee from 'hyperbee'
import path from '#node:path'
import os from '#node:os'
import crypto from 'hypercore-crypto'
import { IdentityRegistry } from '../lib/auth/identity-registry.js'

async function createRegistry () {
  const dir = path.join(os.tmpdir(), 'hyper-identity-nofab-' + crypto.randomBytes(8).toString('hex'))
  const store = new Corestore(dir)
  await store.ready()
  const db = new Hyperbee(store.get({ name: 'test-db' }), {
    keyEncoding: 'utf-8', valueEncoding: 'json'
  })
  await db.ready()
  const registry = new IdentityRegistry(store, { db })
  await registry.ready()
  return {
    registry,
    cleanup: async () => { await registry.close(); await db.close(); await store.close() }
  }
}

test('createUser returns the persisted user on the happy path', async (t) => {
  const { registry, cleanup } = await createRegistry()

  const user = await registry.createUser({ name: 'Alice' })
  t.ok(user && user.userId, 'returns a user with an id')
  t.is(user.name, 'Alice')

  const readBack = await registry.getUser(user.userId)
  t.ok(readBack, 'the user is genuinely readable from the view')
  t.is(readBack.userId, user.userId, 'the returned id matches what persisted')

  await cleanup()
})

test('createUser THROWS when the write does not land — no fabricated user', async (t) => {
  const { registry, cleanup } = await createRegistry()

  // Simulate a peer whose appends cannot linearize (stortron's condition:
  // admitted and "writable", but the linearizer has no indexers so nothing
  // ever confirms). The append resolves; the record never appears.
  registry.base.append = async () => {}

  await t.exception(
    () => registry.createUser({ name: 'ghost' }),
    /did not land|not persist/i,
    'createUser throws instead of returning a user that was never written'
  )

  await cleanup()
})

test('a failed createUser leaves nothing behind and is not silently retried into existence', async (t) => {
  const { registry, cleanup } = await createRegistry()

  registry.base.append = async () => {}

  let firstErr = null
  try { await registry.createUser({ name: 'ghost-1' }) } catch (err) { firstErr = err }
  t.ok(firstErr, 'first attempt threw')

  // The listing must not contain a phantom. Restore append so listUsers works
  // against a functioning view.
  const users = await registry.listUsers()
  t.absent(users.some(u => u.name === 'ghost-1'), 'no phantom user was persisted')

  await cleanup()
})