import test from 'brittle'
import { MockRegistry } from '../helpers/fixtures.js'
import { BearerTokenProvider } from '../../lib/auth/providers/bearer-token.js'
const SHARE_PREFIX = 'share:'
// BearerTokenProvider expects IdentityRegistry which wraps Hyperbee as .db
function wrapRegistry (mockRegistry) {
return { db: mockRegistry, isRevoked: async () => false }
}
// Helper — create a share record (mimics what the route does)
async function createShare (registry, bearerProvider, slug, options = {}) {
const { permissions = ['read'], label = '', username = 'admin' } = options
const tokenResult = await bearerProvider.createToken({
username: `share:${username}`,
description: `Share: ${slug} (${label || permissions.join(', ')})`,
permissions,
notebooks: [slug],
expiresAt: null
})
const shareKey = `${SHARE_PREFIX}${slug}:${tokenResult.tokenHash}`
await registry.put(shareKey, {
notebook: slug,
tokenHash: tokenResult.tokenHash,
permissions,
sharedBy: username,
sharedAt: Date.now(),
label
})
return tokenResult
}
// Helper — list shares for a notebook
async function listShares (registry, slug) {
const prefix = `${SHARE_PREFIX}${slug}:`
const shares = []
for await (const entry of registry.createReadStream({
gt: prefix,
lt: prefix + '\xff'
})) {
shares.push(entry.value)
}
return shares
}
// Helper — revoke a share
async function revokeShare (registry, bearerProvider, slug, tokenHash) {
const shareKey = `${SHARE_PREFIX}${slug}:${tokenHash}`
await bearerProvider.revokeToken(tokenHash)
await registry.del(shareKey)
}
// ==================== SHARE CREATION ====================
test('create share — returns token and stores record', async (t) => {
const registry = new MockRegistry()
const bearer = new BearerTokenProvider(wrapRegistry(registry))
const result = await createShare(registry, bearer, 'my-notebook', {
permissions: ['read'],
label: 'For Alice'
})
t.ok(result.token, 'returns token')
t.ok(result.tokenHash, 'returns tokenHash')
t.is(result.token.length, 64, 'token is 64 hex chars (32 bytes)')
// Share record exists
const shares = await listShares(registry, 'my-notebook')
t.is(shares.length, 1)
t.is(shares[0].notebook, 'my-notebook')
t.is(shares[0].label, 'For Alice')
t.alike(shares[0].permissions, ['read'])
t.is(shares[0].sharedBy, 'admin')
t.ok(shares[0].sharedAt)
})
test('create share — with write permissions', async (t) => {
const registry = new MockRegistry()
const bearer = new BearerTokenProvider(wrapRegistry(registry))
const result = await createShare(registry, bearer, 'collab-notebook', {
permissions: ['read', 'write'],
label: 'Collaborator'
})
// Token has write permission
const tokenData = await bearer.validateToken(result.token)
t.ok(tokenData)
t.alike(tokenData.permissions, ['read', 'write'])
t.alike(tokenData.notebooks, ['collab-notebook'])
})
test('create share — token scoped to specific notebook', async (t) => {
const registry = new MockRegistry()
const bearer = new BearerTokenProvider(wrapRegistry(registry))
const result = await createShare(registry, bearer, 'scoped-notebook', {
permissions: ['read']
})
const tokenData = await bearer.validateToken(result.token)
t.alike(tokenData.notebooks, ['scoped-notebook'], 'token scoped to notebook')
})
// ==================== LISTING SHARES ====================
test('list shares — returns all shares for a notebook', async (t) => {
const registry = new MockRegistry()
const bearer = new BearerTokenProvider(wrapRegistry(registry))
await createShare(registry, bearer, 'shared-nb', { label: 'Alice', permissions: ['read'] })
await createShare(registry, bearer, 'shared-nb', { label: 'Bob', permissions: ['read', 'write'] })
await createShare(registry, bearer, 'other-nb', { label: 'Eve', permissions: ['read'] })
const shares = await listShares(registry, 'shared-nb')
t.is(shares.length, 2, 'only shares for this notebook')
const labels = shares.map(s => s.label).sort()
t.alike(labels, ['Alice', 'Bob'])
})
test('list shares — empty for unshared notebook', async (t) => {
const registry = new MockRegistry()
const shares = await listShares(registry, 'no-shares')
t.is(shares.length, 0)
})
// ==================== REVOKING SHARES ====================
test('revoke share — removes record and revokes token', async (t) => {
const registry = new MockRegistry()
const bearer = new BearerTokenProvider(wrapRegistry(registry))
const result = await createShare(registry, bearer, 'revoke-test', { label: 'Temporary' })
// Token works before revocation
const valid = await bearer.validateToken(result.token)
t.ok(valid, 'token valid before revocation')
// Revoke
await revokeShare(registry, bearer, 'revoke-test', result.tokenHash)
// Share record gone
const shares = await listShares(registry, 'revoke-test')
t.is(shares.length, 0, 'share record removed')
// Token no longer valid
const invalid = await bearer.validateToken(result.token)
t.is(invalid, null, 'token invalid after revocation')
})
test('revoke share — does not affect other shares', async (t) => {
const registry = new MockRegistry()
const bearer = new BearerTokenProvider(wrapRegistry(registry))
const share1 = await createShare(registry, bearer, 'multi-share', { label: 'Keep' })
const share2 = await createShare(registry, bearer, 'multi-share', { label: 'Revoke' })
await revokeShare(registry, bearer, 'multi-share', share2.tokenHash)
// share1 still works
const valid = await bearer.validateToken(share1.token)
t.ok(valid, 'other share still valid')
const shares = await listShares(registry, 'multi-share')
t.is(shares.length, 1)
t.is(shares[0].label, 'Keep')
})
// ==================== TOKEN AUTHENTICATION ====================
test('share token — authenticates with correct permissions', async (t) => {
const registry = new MockRegistry()
const bearer = new BearerTokenProvider(wrapRegistry(registry))
const result = await createShare(registry, bearer, 'auth-test', {
permissions: ['read'],
username: 'owner'
})
// Simulate authentication
const authResult = await bearer.authenticate(result.token)
t.ok(authResult.verified)
t.is(authResult.method, 'bearer-token')
t.alike(authResult.permissions, ['read'])
t.alike(authResult.notebooks, ['auth-test'])
t.is(authResult.credentials.username, 'share:owner')
})
test('share token — read-only cannot write', async (t) => {
const registry = new MockRegistry()
const bearer = new BearerTokenProvider(wrapRegistry(registry))
const result = await createShare(registry, bearer, 'readonly-test', {
permissions: ['read']
})
const authResult = await bearer.authenticate(result.token)
t.alike(authResult.permissions, ['read'])
t.not(authResult.permissions.includes('write'), 'no write permission')
})
test('share token — write token has write permission', async (t) => {
const registry = new MockRegistry()
const bearer = new BearerTokenProvider(wrapRegistry(registry))
const result = await createShare(registry, bearer, 'write-test', {
permissions: ['read', 'write']
})
const authResult = await bearer.authenticate(result.token)
t.ok(authResult.permissions.includes('read'))
t.ok(authResult.permissions.includes('write'))
})