OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet-desktop / test / auth.cjs

164 lines · javascript ref:
// test/auth.cjs - Tests for lib/auth.cjs

const test = require('brittle')
const path = require('bare-path')
const fs = require('bare-fs')
const os = require('bare-os')
const crypto = require('hypercore-crypto')
const b4a = require('b4a')

// Note: lib/auth.cjs uses hardcoded paths (~/.hyper-wallet/.api-token)
// These tests verify the module works, but can't use mocking
// Real file operations happen in user's home directory during tests

// Helper to generate test tokens
function generateTestToken(bytes = 32) {
  return b4a.toString(crypto.randomBytes(bytes), 'hex')
}

test('generateToken produces 64-char hex', function (t) {
  const { generateToken } = require('../lib/auth.cjs')
  const token = generateToken()

  t.ok(token, 'token exists')
  t.is(typeof token, 'string', 'token is string')
  t.is(token.length, 64, 'token is 64 characters')
  t.ok(/^[0-9a-f]{64}$/.test(token), 'token is hex')
})

test('generateToken produces unique tokens', function (t) {
  const { generateToken } = require('../lib/auth.cjs')

  const token1 = generateToken()
  const token2 = generateToken()

  t.not(token1, token2, 'tokens are unique')
})

test('verifyToken accepts valid Bearer token', function (t) {
  const { generateToken, saveToken, verifyToken } = require('../lib/auth.cjs')

  const token = generateToken()
  saveToken(token)

  t.teardown(() => {
    const { deleteToken } = require('../lib/auth.cjs')
    deleteToken()
  })

  const req = {
    headers: {
      authorization: `Bearer ${token}`
    }
  }

  const result = verifyToken(req)
  t.ok(result, 'valid token accepted')
})

test('verifyToken rejects missing authorization header', function (t) {
  const { verifyToken } = require('../lib/auth.cjs')

  const req = { headers: {} }
  const result = verifyToken(req)

  t.absent(result, 'rejects missing header')
})

test('verifyToken rejects malformed Bearer header', function (t) {
  const { generateToken, saveToken, verifyToken } = require('../lib/auth.cjs')

  saveToken(generateToken())

  t.teardown(() => {
    const { deleteToken } = require('../lib/auth.cjs')
    deleteToken()
  })

  const testCases = [
    { headers: { authorization: 'InvalidFormat' } },
    { headers: { authorization: 'Bearer' } },
    { headers: { authorization: 'Bearer ' } },
    { headers: { authorization: 'Basic dXNlcjpwYXNz' } }
  ]

  for (const req of testCases) {
    const result = verifyToken(req)
    t.absent(result, `rejects: ${req.headers.authorization}`)
  }
})

test('verifyToken rejects wrong token', function (t) {
  const { generateToken, saveToken, verifyToken } = require('../lib/auth.cjs')

  const correctToken = generateToken()
  const wrongToken = generateToken()

  saveToken(correctToken)

  t.teardown(() => {
    const { deleteToken } = require('../lib/auth.cjs')
    deleteToken()
  })

  const req = {
    headers: {
      authorization: `Bearer ${wrongToken}`
    }
  }

  const result = verifyToken(req)
  t.absent(result, 'rejects wrong token')
})

test('tokenExists detects token file', function (t) {
  const { generateToken, saveToken, tokenExists, deleteToken } = require('../lib/auth.cjs')

  // Clean up first
  deleteToken()

  t.absent(tokenExists(), 'no token initially')

  saveToken(generateToken())
  t.teardown(() => deleteToken())

  t.ok(tokenExists(), 'token exists after save')
})

test('saveToken creates file with 0600 permissions', function (t) {
  const { generateToken, saveToken, deleteToken } = require('../lib/auth.cjs')

  const token = generateToken()
  saveToken(token)

  t.teardown(() => deleteToken())

  const tokenPath = path.join(os.homedir(), '.hyper-wallet', '.api-token')
  t.ok(fs.existsSync(tokenPath), 'token file created')

  const stat = fs.statSync(tokenPath)
  t.is(stat.mode & 0o777, 0o600, 'token file has 0o600 permissions')
})

test('loadToken returns saved token', function (t) {
  const { generateToken, saveToken, loadToken, deleteToken } = require('../lib/auth.cjs')

  const testToken = generateToken()
  saveToken(testToken)

  t.teardown(() => deleteToken())

  const loaded = loadToken()
  t.is(loaded, testToken, 'loaded token matches saved')
})

test('loadToken returns null if file missing', function (t) {
  const { loadToken, deleteToken } = require('../lib/auth.cjs')

  // Ensure no token file
  deleteToken()

  const loaded = loadToken()
  t.is(loaded, null, 'returns null for missing file')
})