OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-note / test / shared-registry.test.js

229 lines · javascript ref:
import test from 'brittle'
import Corestore from 'corestore'
import os from 'bare-os'
import path from 'bare-path'
import { randomBytes } from 'hypercore-crypto'
import { SharedRegistry } from '../lib/multiwriter/shared-registry.js'

function createStore () {
  const tmp = path.join(os.tmpdir(), 'hypernote-test-' + randomBytes(16).toString('hex'))
  return new Corestore(tmp)
}

test('create and get user', async (t) => {
  const store = createStore()
  const reg = new SharedRegistry(store)
  await reg.ready()

  const { userId } = await reg.createUser({
    userId: 'u1',
    name: 'Alice',
    role: 'admin',
    identityKey: 'id-alice'
  })

  t.is(userId, 'u1')

  const user = await reg.getUser('u1')
  t.ok(user)
  t.is(user.name, 'Alice')
  t.is(user.role, 'admin')
  t.is(user.identityKey, 'id-alice')

  // Also test getUserByIdentity
  const user2 = await reg.getUserByIdentity('id-alice')
  t.ok(user2)
  t.is(user2.userId, 'u1')

  await reg.close()
  await store.close()
})

test('list users', async (t) => {
  const store = createStore()
  const reg = new SharedRegistry(store)
  await reg.ready()

  await reg.createUser({ userId: 'u1', name: 'Alice', role: 'admin' })
  await reg.createUser({ userId: 'u2', name: 'Bob', role: 'read' })

  const users = await reg.listUsers()
  t.is(users.length, 2)

  const names = users.map(u => u.name).sort()
  t.alike(names, ['Alice', 'Bob'])

  await reg.close()
  await store.close()
})

test('create and get notebook by slug', async (t) => {
  const store = createStore()
  const reg = new SharedRegistry(store)
  await reg.ready()

  const { notebookId, slug } = await reg.createNotebook({
    slug: 'my-notes',
    title: 'My Notes',
    owner: 'u1'
  })

  t.ok(notebookId)
  t.is(slug, 'my-notes')

  const nb = await reg.getNotebook(notebookId)
  t.ok(nb)
  t.is(nb.title, 'My Notes')
  t.is(nb.owner, 'u1')
  t.is(nb.shared, false)

  const nb2 = await reg.getNotebookBySlug('my-notes')
  t.ok(nb2)
  t.is(nb2.notebookId, notebookId)

  await reg.close()
  await store.close()
})

test('share notebook sets shared flag and autobaseKey', async (t) => {
  const store = createStore()
  const reg = new SharedRegistry(store)
  await reg.ready()

  const { notebookId } = await reg.createNotebook({
    slug: 'shared-nb',
    title: 'Shared',
    owner: 'u1'
  })

  await reg.shareNotebook(notebookId, 'autobase-key-abc')

  const nb = await reg.getNotebook(notebookId)
  t.is(nb.shared, true)
  t.is(nb.autobaseKey, 'autobase-key-abc')

  // Unshare
  await reg.unshareNotebook(notebookId)
  const nb2 = await reg.getNotebook(notebookId)
  t.is(nb2.shared, false)
  t.is(nb2.autobaseKey, null)

  await reg.close()
  await store.close()
})

test('set and get permission', async (t) => {
  const store = createStore()
  const reg = new SharedRegistry(store)
  await reg.ready()

  await reg.setPermission('u1', 'write', { scope: 'notebook', notebookId: 'nb-1' })

  const perm = await reg.getPermission('u1')
  t.ok(perm)
  t.is(perm.role, 'write')
  t.is(perm.scope, 'notebook')
  t.is(perm.notebookId, 'nb-1')

  await reg.close()
  await store.close()
})

test('add writer does not error', async (t) => {
  const store = createStore()
  const reg = new SharedRegistry(store)
  await reg.ready()

  // Adding the local writer key should not throw
  await reg.addWriter(reg.localWriterKey)
  t.pass('addWriter completed without error')

  await reg.close()
  await store.close()
})

test('bootstrapKey available after ready', async (t) => {
  const store = createStore()
  const reg = new SharedRegistry(store)
  await reg.ready()

  t.ok(reg.bootstrapKey)
  t.ok(Buffer.isBuffer(reg.bootstrapKey))
  t.is(reg.bootstrapKey.length, 32)

  await reg.close()
  await store.close()
})

test('list notebooks filters correctly', async (t) => {
  const store = createStore()
  const reg = new SharedRegistry(store)
  await reg.ready()

  await reg.createNotebook({ slug: 'nb-a', title: 'A', owner: 'u1' })
  await reg.createNotebook({ slug: 'nb-b', title: 'B', owner: 'u1' })

  const list = await reg.listNotebooks()
  t.is(list.length, 2)
  const titles = list.map(n => n.title).sort()
  t.alike(titles, ['A', 'B'])

  await reg.close()
  await store.close()
})

test('replicate between two instances', async (t) => {
  const store1 = createStore()
  const store2 = createStore()

  const reg1 = new SharedRegistry(store1)
  await reg1.ready()

  // Create user on instance 1
  await reg1.createUser({ userId: 'u1', name: 'Alice', role: 'admin' })

  // Instance 2 joins with the bootstrap key
  const reg2 = new SharedRegistry(store2, { bootstrapKey: reg1.bootstrapKey })
  await reg2.ready()

  // Replicate: create paired streams from each autobase
  // Autobase.replicate wraps store.replicate + wakeup protocol
  const s1 = reg1.base.replicate(true)
  const s2 = reg2.base.replicate(false)
  s1.pipe(s2).pipe(s1)

  // Autobase v7 needs the joiner to update first to discover the bootstrap,
  // then update again after replication delivers the data
  await reg2.update()

  // Give replication time to sync
  for (let i = 0; i < 15; i++) {
    await new Promise(resolve => setTimeout(resolve, 500))
    try {
      await reg2.update()
      const user = await reg2.view.get('user:u1')
      if (user) {
        t.is(user.value.name, 'Alice')
        t.pass('replicated user successfully')
        s1.destroy()
        s2.destroy()
        await reg1.close()
        await reg2.close()
        await store1.close()
        await store2.close()
        return
      }
    } catch (e) {
      // View may not be ready yet, keep trying
    }
  }

  t.fail('user did not replicate within timeout')
  s1.destroy()
  s2.destroy()
  await reg1.close()
  await reg2.close()
  await store1.close()
  await store2.close()
})