OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-note / test / unit / identity-registry.test.js

206 lines · javascript ref:
import test from 'brittle'
import { IdentityRegistry } from '../../lib/auth/identity-registry.js'
import { MockRegistry } from '../helpers/fixtures.js'

test('IdentityRegistry - create user with defaults', async (t) => {
  const db = new MockRegistry()
  const reg = new IdentityRegistry(db)

  const user = await reg.createUser()
  t.ok(user.userId, 'has userId')
  t.is(user.name, 'Anonymous', 'default name')
  t.alike(user.globalPermissions, ['read'], 'default read perms')
  t.is(user.notebooks.length, 1, 'has default notebook entry')
  t.is(user.notebooks[0].name, '*', 'wildcard notebook')
  t.alike(user.notebooks[0].permissions, ['read'], 'read perms on notebook')
  t.alike(user.identities, [], 'no identities yet')
})

test('IdentityRegistry - create user with custom data', async (t) => {
  const db = new MockRegistry()
  const reg = new IdentityRegistry(db)

  const user = await reg.createUser({
    name: 'Alice',
    email: 'alice@example.com',
    permissions: ['read', 'write', 'admin'],
    notebooks: [{ name: 'private', permissions: ['read', 'write'] }]
  })

  t.is(user.name, 'Alice')
  t.is(user.email, 'alice@example.com')
  t.alike(user.globalPermissions, ['read', 'write', 'admin'])
  t.is(user.notebooks[0].name, 'private')
})

test('IdentityRegistry - create and link identity', async (t) => {
  const db = new MockRegistry()
  const reg = new IdentityRegistry(db)

  const user = await reg.createUser({ name: 'Bob' })
  const identity = await reg.createIdentity({
    identityId: 'pk_abc123',
    method: 'web-identity',
    credentials: { publicKey: 'pk_abc123' }
  })

  t.ok(identity.identityId, 'identity created')
  t.is(identity.userId, null, 'not linked yet')

  const result = await reg.linkIdentity('pk_abc123', user.userId)
  t.is(result.identity.userId, user.userId, 'identity linked to user')
  t.ok(result.user.identities.includes('pk_abc123'), 'user has identity')
})

test('IdentityRegistry - autoRegister', async (t) => {
  const db = new MockRegistry()
  const reg = new IdentityRegistry(db)

  const { user, identity } = await reg.autoRegister({
    identityId: 'pk_auto123',
    method: 'web-identity',
    credentials: { publicKey: 'pk_auto123' }
  })

  t.ok(user.userId, 'user created')
  t.is(identity.userId, user.userId, 'identity linked')
  t.ok(user.identities.includes('pk_auto123'), 'identity in user list')
})

test('IdentityRegistry - getUserByIdentity', async (t) => {
  const db = new MockRegistry()
  const reg = new IdentityRegistry(db)

  const { user } = await reg.autoRegister({
    identityId: 'pk_lookup',
    method: 'web-identity',
    credentials: { publicKey: 'pk_lookup' }
  })

  const found = await reg.getUserByIdentity('pk_lookup')
  t.ok(found, 'found user')
  t.is(found.userId, user.userId, 'correct user')
})

test('IdentityRegistry - getIdentityByPublicKey', async (t) => {
  const db = new MockRegistry()
  const reg = new IdentityRegistry(db)

  await reg.autoRegister({
    identityId: 'pk_pubkey',
    method: 'web-identity',
    credentials: { publicKey: 'pk_pubkey' }
  })

  const identity = await reg.getIdentityByPublicKey('pk_pubkey')
  t.ok(identity, 'found identity')
  t.is(identity.identityId, 'pk_pubkey')
})

test('IdentityRegistry - upgrade notebook permissions', async (t) => {
  const db = new MockRegistry()
  const reg = new IdentityRegistry(db)

  const user = await reg.createUser({ name: 'Writer' })

  const updated = await reg.upgradePermissions(user.userId, 'my-notes', ['write'])
  const nb = updated.notebooks.find(r => r.name === 'my-notes')
  t.ok(nb, 'notebook permission entry created')
  t.ok(nb.permissions.includes('write'), 'write permission added')
})

test('IdentityRegistry - revoke and restore identity', async (t) => {
  const db = new MockRegistry()
  const reg = new IdentityRegistry(db)

  const { identity } = await reg.autoRegister({
    identityId: 'pk_revoke',
    method: 'web-identity',
    credentials: { publicKey: 'pk_revoke' }
  })

  // Revoke
  const revoked = await reg.revokeIdentity('pk_revoke', { reason: 'test' })
  t.ok(revoked.revoked, 'identity marked revoked')

  const isRevoked = await reg.isRevoked('pk_revoke')
  t.ok(isRevoked, 'isRevoked returns true')

  // Restore
  const restored = await reg.restoreIdentity('pk_revoke')
  t.absent(restored.revoked, 'revoked flag removed')

  const isStillRevoked = await reg.isRevoked('pk_revoke')
  t.absent(isStillRevoked, 'isRevoked returns false after restore')
})

test('IdentityRegistry - merge accounts transfers notebooks', async (t) => {
  const db = new MockRegistry()
  const reg = new IdentityRegistry(db)

  const primary = await reg.createUser({
    name: 'Primary',
    notebooks: [{ name: 'work', permissions: ['read', 'write'] }]
  })

  const secondary = await reg.createUser({
    name: 'Secondary',
    notebooks: [{ name: 'personal', permissions: ['read'] }]
  })

  await reg.createIdentity({
    identityId: 'pk_secondary',
    method: 'web-identity',
    userId: secondary.userId,
    credentials: { publicKey: 'pk_secondary' }
  })
  secondary.identities.push('pk_secondary')
  await db.put(`user:${secondary.userId}`, secondary)

  const result = await reg.mergeAccounts(primary.userId, secondary.userId)
  t.is(result.stats.identitiesTransferred, 1, 'identity transferred')
  t.ok(result.primary.notebooks.find(r => r.name === 'personal'), 'secondary notebook merged')
  t.alike(result.merged.notebooks, [], 'secondary notebooks cleared')
})

test('IdentityRegistry - deactivateUser is a soft-delete (preserves records, blocks auth)', async (t) => {
  const db = new MockRegistry()
  const reg = new IdentityRegistry(db)

  const { user } = await reg.autoRegister({
    identityId: 'pk_deact',
    method: 'web-identity',
    credentials: { publicKey: 'pk_deact' }
  })

  await reg.deactivateUser(user.userId, { by: 'admin-1', reason: 'spam' })

  // User record is PRESERVED — never hard-deleted
  const after = await reg.getUser(user.userId)
  t.ok(after, 'user record still exists (not hard-deleted)')
  t.is(after.deactivated, true, 'user flagged deactivated')
  t.is(after.deactivatedBy, 'admin-1', 'records who deactivated it')

  // Identity is revoked → providers will reject auth; identity row preserved
  t.ok(await reg.isRevoked('pk_deact'), 'linked identity is revoked (auth blocked)')
  t.ok(await reg.getIdentity('pk_deact'), 'identity record still exists')
})

test('IdentityRegistry - reactivateUser reverses a soft-delete', async (t) => {
  const db = new MockRegistry()
  const reg = new IdentityRegistry(db)

  const { user } = await reg.autoRegister({
    identityId: 'pk_react',
    method: 'web-identity',
    credentials: { publicKey: 'pk_react' }
  })

  await reg.deactivateUser(user.userId, { by: 'admin-1' })
  await reg.reactivateUser(user.userId)

  const after = await reg.getUser(user.userId)
  t.absent(after.deactivated, 'deactivated flag cleared')
  t.absent(await reg.isRevoked('pk_react'), 'identity restored (no longer revoked)')
})