// 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')
})