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()
})