OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-social / bridge-vault / test / vault.test.js

215 lines · javascript ref:
import test from 'brittle'
import { Vault, generateKey, deriveKey } from '../index.js'
import b4a from 'b4a'
import { tmpdir } from 'os'
import { join } from 'path'
import { randomBytes } from 'crypto'

function tmpStorage () {
  return join(tmpdir(), 'bridge-vault-test-' + randomBytes(8).toString('hex'))
}

test('Vault - put and get credential', async (t) => {
  const vault = new Vault(tmpStorage())
  await vault.ready()

  await vault.put('irc', 'libera', {
    server: 'irc.libera.chat',
    port: 6697,
    nick: 'testbot',
    password: 'secret123'
  })

  const cred = await vault.get('irc', 'libera')
  t.is(cred.server, 'irc.libera.chat')
  t.is(cred.port, 6697)
  t.is(cred.nick, 'testbot')
  t.is(cred.password, 'secret123')

  await vault.close()
})

test('Vault - get nonexistent returns null', async (t) => {
  const vault = new Vault(tmpStorage())
  await vault.ready()

  const cred = await vault.get('irc', 'nonexistent')
  t.is(cred, null)

  await vault.close()
})

test('Vault - delete credential', async (t) => {
  const vault = new Vault(tmpStorage())
  await vault.ready()

  await vault.put('discord', 'mybot', { token: 'abc123' })
  t.ok(await vault.get('discord', 'mybot'))

  await vault.del('discord', 'mybot')
  t.is(await vault.get('discord', 'mybot'), null)

  await vault.close()
})

test('Vault - update credential', async (t) => {
  const vault = new Vault(tmpStorage())
  await vault.ready()

  await vault.put('irc', 'server1', { nick: 'old' })
  await vault.put('irc', 'server1', { nick: 'new' })

  const cred = await vault.get('irc', 'server1')
  t.is(cred.nick, 'new')

  await vault.close()
})

test('Vault - list credentials for platform', async (t) => {
  const vault = new Vault(tmpStorage())
  await vault.ready()

  await vault.put('irc', 'libera', { nick: 'bot1' })
  await vault.put('irc', 'oftc', { nick: 'bot2' })
  await vault.put('discord', 'server1', { token: 'xyz' })

  const ircCreds = await vault.list('irc')
  t.is(ircCreds.length, 2)
  t.ok(ircCreds.find(c => c.name === 'libera'))
  t.ok(ircCreds.find(c => c.name === 'oftc'))

  const discordCreds = await vault.list('discord')
  t.is(discordCreds.length, 1)
  t.is(discordCreds[0].name, 'server1')

  await vault.close()
})

test('Vault - listPlatforms', async (t) => {
  const vault = new Vault(tmpStorage())
  await vault.ready()

  await vault.put('irc', 'a', { x: 1 })
  await vault.put('discord', 'b', { x: 2 })
  await vault.put('matrix', 'c', { x: 3 })

  const platforms = await vault.listPlatforms()
  t.is(platforms.length, 3)
  t.ok(platforms.includes('irc'))
  t.ok(platforms.includes('discord'))
  t.ok(platforms.includes('matrix'))

  await vault.close()
})

test('Vault - encryption works (wrong key fails)', async (t) => {
  const storage = tmpStorage()
  const key1 = generateKey()
  const key2 = generateKey()

  const vault1 = new Vault(storage, { key: key1 })
  await vault1.ready()
  await vault1.put('irc', 'test', { secret: 'hidden' })
  await vault1.close()

  const vault2 = new Vault(storage, { key: key2 })
  await vault2.ready()

  try {
    await vault2.get('irc', 'test')
    t.fail('should have thrown')
  } catch (err) {
    t.ok(err.message.includes('Decryption failed'), 'wrong key throws')
  }

  await vault2.close()
})

test('Vault - same key reopens vault', async (t) => {
  const storage = tmpStorage()
  const key = generateKey()

  const vault1 = new Vault(storage, { key })
  await vault1.ready()
  await vault1.put('irc', 'test', { value: 42 })
  await vault1.close()

  const vault2 = new Vault(storage, { key })
  await vault2.ready()
  const cred = await vault2.get('irc', 'test')
  t.is(cred.value, 42)
  await vault2.close()
})

test('Vault - complex credential values', async (t) => {
  const vault = new Vault(tmpStorage())
  await vault.ready()

  await vault.put('mastodon', 'instance1', {
    baseUrl: 'https://mastodon.social',
    accessToken: 'token123',
    scopes: ['read', 'write', 'push'],
    metadata: {
      createdAt: '2024-01-01',
      expiresAt: null,
      refreshToken: 'refresh456'
    }
  })

  const cred = await vault.get('mastodon', 'instance1')
  t.is(cred.baseUrl, 'https://mastodon.social')
  t.alike(cred.scopes, ['read', 'write', 'push'])
  t.is(cred.metadata.refreshToken, 'refresh456')
  t.is(cred.metadata.expiresAt, null)

  await vault.close()
})

test('generateKey - returns buffer of correct size', (t) => {
  const key = generateKey()
  t.ok(b4a.isBuffer(key))
  t.is(key.length, 32)
})

test('deriveKey - derives from password', (t) => {
  const { key, salt } = deriveKey('my-password')
  t.ok(b4a.isBuffer(key))
  t.is(key.length, 32)
  t.ok(b4a.isBuffer(salt))
})

test('deriveKey - same password + salt gives same key', (t) => {
  const { key: key1, salt } = deriveKey('password')
  const { key: key2 } = deriveKey('password', salt)
  t.ok(b4a.equals(key1, key2))
})

test('deriveKey - different passwords give different keys', (t) => {
  const { key: key1, salt } = deriveKey('password1')
  const { key: key2 } = deriveKey('password2', salt)
  t.absent(b4a.equals(key1, key2))
})

test('Vault - empty list returns empty array', async (t) => {
  const vault = new Vault(tmpStorage())
  await vault.ready()

  const creds = await vault.list('nonexistent')
  t.alike(creds, [])

  await vault.close()
})

test('Vault - ready is idempotent', async (t) => {
  const vault = new Vault(tmpStorage())
  await vault.ready()
  await vault.ready()
  await vault.ready()

  await vault.put('irc', 'test', { ok: true })
  const cred = await vault.get('irc', 'test')
  t.is(cred.ok, true)

  await vault.close()
})