- Add tests for chat/encryption.js: encryption/decryption, hashing, token generation - Add tests for chat/tokenManager.js: JWT tokens, device fingerprints, cookie handling - Add tests for chat/prompt-sanitizer.js: security patterns, attack detection, obfuscation - Add tests for admin panel: session management, rate limiting, user/token management - Add tests for OpenCode write tool: file creation, overwrites, nested directories - Add tests for OpenCode todo tools: todo CRUD operations - Add tests for Console billing/account/provider: schemas, validation, price utilities These tests cover previously untested critical paths including: - Authentication and security - Payment processing validation - Admin functionality - Model routing and management - Account management
531 lines
16 KiB
JavaScript
531 lines
16 KiB
JavaScript
const { describe, test, expect, beforeEach, afterEach, mock } = require('bun:test')
|
|
|
|
describe('Admin Panel Functionality', () => {
|
|
let adminSessions
|
|
let adminLoginAttempts
|
|
|
|
const ADMIN_COOKIE_NAME = 'admin_session'
|
|
const ADMIN_SESSION_TTL_MS = 24 * 60 * 60 * 1000
|
|
const ADMIN_LOGIN_RATE_LIMIT = 5
|
|
|
|
beforeEach(() => {
|
|
adminSessions = new Map()
|
|
adminLoginAttempts = new Map()
|
|
})
|
|
|
|
afterEach(() => {
|
|
adminSessions.clear()
|
|
adminLoginAttempts.clear()
|
|
})
|
|
|
|
describe('Admin Session Management', () => {
|
|
function readAdminSessionToken(req) {
|
|
try {
|
|
const cookieHeader = req?.headers?.cookie || ''
|
|
if (!cookieHeader) return ''
|
|
const parts = cookieHeader.split(';').map((p) => p.trim())
|
|
const match = parts.find((p) => p.startsWith(`${ADMIN_COOKIE_NAME}=`))
|
|
if (!match) return ''
|
|
return decodeURIComponent(match.split('=').slice(1).join('=') || '')
|
|
} catch (_) {
|
|
return ''
|
|
}
|
|
}
|
|
|
|
function getAdminSession(req) {
|
|
const token = readAdminSessionToken(req)
|
|
if (!token) return null
|
|
const session = adminSessions.get(token)
|
|
if (!session) return null
|
|
if (session.expiresAt && session.expiresAt < Date.now()) {
|
|
adminSessions.delete(token)
|
|
return null
|
|
}
|
|
return { token, expiresAt: session.expiresAt }
|
|
}
|
|
|
|
function startAdminSession() {
|
|
const crypto = require('crypto')
|
|
const token = crypto.randomUUID()
|
|
const expiresAt = Date.now() + ADMIN_SESSION_TTL_MS
|
|
adminSessions.set(token, { expiresAt })
|
|
return token
|
|
}
|
|
|
|
function clearAdminSession(token) {
|
|
if (token) adminSessions.delete(token)
|
|
}
|
|
|
|
function requireAdminAuth(req) {
|
|
const session = getAdminSession(req)
|
|
if (!session) return null
|
|
return session
|
|
}
|
|
|
|
test('starts a new admin session', () => {
|
|
const token = startAdminSession()
|
|
|
|
expect(token).toBeDefined()
|
|
expect(adminSessions.has(token)).toBe(true)
|
|
expect(adminSessions.get(token).expiresAt).toBeGreaterThan(Date.now())
|
|
})
|
|
|
|
test('reads admin session token from cookie', () => {
|
|
const req = {
|
|
headers: {
|
|
cookie: `${ADMIN_COOKIE_NAME}=test-token-123; other=value`
|
|
}
|
|
}
|
|
|
|
const token = readAdminSessionToken(req)
|
|
expect(token).toBe('test-token-123')
|
|
})
|
|
|
|
test('returns null for missing cookie', () => {
|
|
const req = { headers: {} }
|
|
const token = readAdminSessionToken(req)
|
|
expect(token).toBe('')
|
|
})
|
|
|
|
test('validates active session', () => {
|
|
const token = startAdminSession()
|
|
const req = {
|
|
headers: { cookie: `${ADMIN_COOKIE_NAME}=${token}` }
|
|
}
|
|
|
|
const session = getAdminSession(req)
|
|
expect(session).not.toBeNull()
|
|
expect(session.token).toBe(token)
|
|
})
|
|
|
|
test('rejects expired session', () => {
|
|
const crypto = require('crypto')
|
|
const token = crypto.randomUUID()
|
|
adminSessions.set(token, { expiresAt: Date.now() - 1000 })
|
|
|
|
const req = {
|
|
headers: { cookie: `${ADMIN_COOKIE_NAME}=${token}` }
|
|
}
|
|
|
|
const session = getAdminSession(req)
|
|
expect(session).toBeNull()
|
|
expect(adminSessions.has(token)).toBe(false)
|
|
})
|
|
|
|
test('rejects invalid token', () => {
|
|
const req = {
|
|
headers: { cookie: `${ADMIN_COOKIE_NAME}=invalid-token` }
|
|
}
|
|
|
|
const session = getAdminSession(req)
|
|
expect(session).toBeNull()
|
|
})
|
|
|
|
test('clears admin session', () => {
|
|
const token = startAdminSession()
|
|
clearAdminSession(token)
|
|
|
|
expect(adminSessions.has(token)).toBe(false)
|
|
})
|
|
|
|
test('requireAdminAuth returns session for valid auth', () => {
|
|
const token = startAdminSession()
|
|
const req = {
|
|
headers: { cookie: `${ADMIN_COOKIE_NAME}=${token}` }
|
|
}
|
|
|
|
const session = requireAdminAuth(req)
|
|
expect(session).not.toBeNull()
|
|
})
|
|
|
|
test('requireAdminAuth returns null for invalid auth', () => {
|
|
const req = { headers: {} }
|
|
const session = requireAdminAuth(req)
|
|
expect(session).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('Admin Login Rate Limiting', () => {
|
|
const LOGIN_LOCKOUT_MS = 900000
|
|
const RATE_LIMIT_WINDOW_MS = 60000
|
|
|
|
function checkLoginRateLimit(ip, limit, attempts) {
|
|
const now = Date.now()
|
|
const record = attempts.get(ip)
|
|
|
|
if (!record) {
|
|
attempts.set(ip, { count: 1, windowStart: now, lockedUntil: null })
|
|
return { blocked: false, count: 1 }
|
|
}
|
|
|
|
if (record.lockedUntil && record.lockedUntil > now) {
|
|
return {
|
|
blocked: true,
|
|
retryAfter: Math.ceil((record.lockedUntil - now) / 1000)
|
|
}
|
|
}
|
|
|
|
if (now - record.windowStart > RATE_LIMIT_WINDOW_MS) {
|
|
attempts.set(ip, { count: 1, windowStart: now, lockedUntil: null })
|
|
return { blocked: false, count: 1 }
|
|
}
|
|
|
|
record.count++
|
|
|
|
if (record.count >= limit) {
|
|
record.lockedUntil = now + LOGIN_LOCKOUT_MS
|
|
return { blocked: true, retryAfter: Math.ceil(LOGIN_LOCKOUT_MS / 1000) }
|
|
}
|
|
|
|
return { blocked: false, count: record.count }
|
|
}
|
|
|
|
test('allows first login attempt', () => {
|
|
const result = checkLoginRateLimit('192.168.1.1', ADMIN_LOGIN_RATE_LIMIT, adminLoginAttempts)
|
|
|
|
expect(result.blocked).toBe(false)
|
|
expect(result.count).toBe(1)
|
|
})
|
|
|
|
test('counts attempts within window', () => {
|
|
const ip = '192.168.1.1'
|
|
checkLoginRateLimit(ip, ADMIN_LOGIN_RATE_LIMIT, adminLoginAttempts)
|
|
checkLoginRateLimit(ip, ADMIN_LOGIN_RATE_LIMIT, adminLoginAttempts)
|
|
const result = checkLoginRateLimit(ip, ADMIN_LOGIN_RATE_LIMIT, adminLoginAttempts)
|
|
|
|
expect(result.blocked).toBe(false)
|
|
expect(result.count).toBe(3)
|
|
})
|
|
|
|
test('blocks after exceeding limit', () => {
|
|
const ip = '192.168.1.1'
|
|
for (let i = 0; i < ADMIN_LOGIN_RATE_LIMIT; i++) {
|
|
checkLoginRateLimit(ip, ADMIN_LOGIN_RATE_LIMIT, adminLoginAttempts)
|
|
}
|
|
const result = checkLoginRateLimit(ip, ADMIN_LOGIN_RATE_LIMIT, adminLoginAttempts)
|
|
|
|
expect(result.blocked).toBe(true)
|
|
expect(result.retryAfter).toBeGreaterThan(0)
|
|
})
|
|
|
|
test('resets after lockout period', () => {
|
|
const ip = '192.168.1.1'
|
|
for (let i = 0; i < ADMIN_LOGIN_RATE_LIMIT; i++) {
|
|
checkLoginRateLimit(ip, ADMIN_LOGIN_RATE_LIMIT, adminLoginAttempts)
|
|
}
|
|
|
|
const record = adminLoginAttempts.get(ip)
|
|
record.lockedUntil = Date.now() - 1000
|
|
|
|
const result = checkLoginRateLimit(ip, ADMIN_LOGIN_RATE_LIMIT, adminLoginAttempts)
|
|
expect(result.blocked).toBe(false)
|
|
})
|
|
|
|
test('tracks different IPs separately', () => {
|
|
checkLoginRateLimit('192.168.1.1', ADMIN_LOGIN_RATE_LIMIT, adminLoginAttempts)
|
|
checkLoginRateLimit('192.168.1.1', ADMIN_LOGIN_RATE_LIMIT, adminLoginAttempts)
|
|
checkLoginRateLimit('192.168.1.2', ADMIN_LOGIN_RATE_LIMIT, adminLoginAttempts)
|
|
|
|
expect(adminLoginAttempts.get('192.168.1.1').count).toBe(2)
|
|
expect(adminLoginAttempts.get('192.168.1.2').count).toBe(1)
|
|
})
|
|
})
|
|
|
|
describe('Admin User Management', () => {
|
|
let usersDb
|
|
|
|
beforeEach(() => {
|
|
usersDb = [
|
|
{ id: 'user-1', email: 'user1@test.com', plan: 'hobby', billingStatus: 'active' },
|
|
{ id: 'user-2', email: 'user2@test.com', plan: 'starter', billingStatus: 'active' },
|
|
{ id: 'user-3', email: 'user3@test.com', plan: 'professional', billingStatus: 'active' }
|
|
]
|
|
})
|
|
|
|
function findUserById(userId) {
|
|
return usersDb.find(u => u.id === userId) || null
|
|
}
|
|
|
|
function normalizePlanSelection(plan) {
|
|
const validPlans = ['hobby', 'starter', 'professional', 'enterprise']
|
|
return validPlans.includes(plan) ? plan : 'hobby'
|
|
}
|
|
|
|
function isPaidPlan(plan) {
|
|
return ['starter', 'professional', 'enterprise'].includes(plan)
|
|
}
|
|
|
|
test('finds user by ID', () => {
|
|
const user = findUserById('user-1')
|
|
expect(user).toBeDefined()
|
|
expect(user.email).toBe('user1@test.com')
|
|
})
|
|
|
|
test('returns null for non-existent user', () => {
|
|
const user = findUserById('non-existent')
|
|
expect(user).toBeNull()
|
|
})
|
|
|
|
test('normalizes valid plan selections', () => {
|
|
expect(normalizePlanSelection('hobby')).toBe('hobby')
|
|
expect(normalizePlanSelection('starter')).toBe('starter')
|
|
expect(normalizePlanSelection('professional')).toBe('professional')
|
|
expect(normalizePlanSelection('enterprise')).toBe('enterprise')
|
|
})
|
|
|
|
test('defaults invalid plan to hobby', () => {
|
|
expect(normalizePlanSelection('invalid')).toBe('hobby')
|
|
expect(normalizePlanSelection('')).toBe('hobby')
|
|
expect(normalizePlanSelection(null)).toBe('hobby')
|
|
})
|
|
|
|
test('identifies paid plans', () => {
|
|
expect(isPaidPlan('hobby')).toBe(false)
|
|
expect(isPaidPlan('starter')).toBe(true)
|
|
expect(isPaidPlan('professional')).toBe(true)
|
|
expect(isPaidPlan('enterprise')).toBe(true)
|
|
})
|
|
|
|
test('updates user plan', () => {
|
|
const user = findUserById('user-1')
|
|
user.plan = 'enterprise'
|
|
|
|
expect(user.plan).toBe('enterprise')
|
|
})
|
|
|
|
test('lists all users', () => {
|
|
expect(usersDb.length).toBe(3)
|
|
})
|
|
|
|
test('deletes user', () => {
|
|
const initialCount = usersDb.length
|
|
const index = usersDb.findIndex(u => u.id === 'user-1')
|
|
usersDb.splice(index, 1)
|
|
|
|
expect(usersDb.length).toBe(initialCount - 1)
|
|
expect(findUserById('user-1')).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('Admin Token Management', () => {
|
|
let tokenUsage
|
|
let usersDb
|
|
|
|
beforeEach(() => {
|
|
tokenUsage = {}
|
|
usersDb = [
|
|
{ id: 'user-1', plan: 'hobby' },
|
|
{ id: 'user-2', plan: 'professional' }
|
|
]
|
|
})
|
|
|
|
function ensureTokenUsageBucket(userId) {
|
|
if (!tokenUsage[userId]) {
|
|
tokenUsage[userId] = { usage: 0, tokenOverride: null }
|
|
}
|
|
return tokenUsage[userId]
|
|
}
|
|
|
|
function getPlanTokenLimits(plan, userId) {
|
|
const limits = {
|
|
hobby: 10000,
|
|
starter: 50000,
|
|
professional: 200000,
|
|
enterprise: 1000000
|
|
}
|
|
const bucket = tokenUsage[userId]
|
|
if (bucket?.tokenOverride !== null && bucket?.tokenOverride !== undefined) {
|
|
return bucket.tokenOverride
|
|
}
|
|
return limits[plan] || limits.hobby
|
|
}
|
|
|
|
test('creates token bucket for new user', () => {
|
|
const bucket = ensureTokenUsageBucket('new-user')
|
|
|
|
expect(bucket).toBeDefined()
|
|
expect(bucket.usage).toBe(0)
|
|
expect(bucket.tokenOverride).toBeNull()
|
|
})
|
|
|
|
test('returns existing bucket', () => {
|
|
tokenUsage['existing-user'] = { usage: 5000, tokenOverride: 20000 }
|
|
const bucket = ensureTokenUsageBucket('existing-user')
|
|
|
|
expect(bucket.usage).toBe(5000)
|
|
expect(bucket.tokenOverride).toBe(20000)
|
|
})
|
|
|
|
test('sets token limit override', () => {
|
|
const userId = 'user-1'
|
|
const bucket = ensureTokenUsageBucket(userId)
|
|
bucket.tokenOverride = 50000
|
|
|
|
expect(bucket.tokenOverride).toBe(50000)
|
|
})
|
|
|
|
test('clears override when set to 0', () => {
|
|
const userId = 'user-1'
|
|
const bucket = ensureTokenUsageBucket(userId)
|
|
bucket.tokenOverride = 50000
|
|
bucket.tokenOverride = 0 ? null : bucket.tokenOverride
|
|
|
|
bucket.tokenOverride = null
|
|
|
|
expect(bucket.tokenOverride).toBeNull()
|
|
})
|
|
|
|
test('sets usage directly', () => {
|
|
const userId = 'user-1'
|
|
const bucket = ensureTokenUsageBucket(userId)
|
|
bucket.usage = 25000
|
|
|
|
expect(bucket.usage).toBe(25000)
|
|
})
|
|
|
|
test('calculates remaining tokens', () => {
|
|
const userId = 'user-1'
|
|
const bucket = ensureTokenUsageBucket(userId)
|
|
const limit = getPlanTokenLimits('hobby', userId)
|
|
bucket.usage = 5000
|
|
const remaining = limit - bucket.usage
|
|
|
|
expect(remaining).toBe(5000)
|
|
})
|
|
|
|
test('uses override limit when set', () => {
|
|
const userId = 'user-1'
|
|
const bucket = ensureTokenUsageBucket(userId)
|
|
bucket.tokenOverride = 50000
|
|
|
|
const limit = getPlanTokenLimits('hobby', userId)
|
|
expect(limit).toBe(50000)
|
|
})
|
|
})
|
|
|
|
describe('Admin Model Management', () => {
|
|
let opencodeModels
|
|
let publicModels
|
|
|
|
beforeEach(() => {
|
|
opencodeModels = [
|
|
{ id: 'model-1', name: 'gpt-4', label: 'GPT-4', tier: 'premium' },
|
|
{ id: 'model-2', name: 'claude-3', label: 'Claude 3', tier: 'premium' }
|
|
]
|
|
publicModels = [
|
|
{ id: 'model-3', name: 'gpt-3.5', label: 'GPT-3.5', tier: 'standard' }
|
|
]
|
|
})
|
|
|
|
function normalizeTier(tier) {
|
|
const validTiers = ['free', 'standard', 'premium', 'enterprise']
|
|
return validTiers.includes(tier) ? tier : 'standard'
|
|
}
|
|
|
|
function getTierMultiplier(tier) {
|
|
const multipliers = {
|
|
free: 0.5,
|
|
standard: 1,
|
|
premium: 2,
|
|
enterprise: 5
|
|
}
|
|
return multipliers[tier] || 1
|
|
}
|
|
|
|
test('lists opencode models', () => {
|
|
expect(opencodeModels.length).toBe(2)
|
|
expect(opencodeModels[0].name).toBe('gpt-4')
|
|
})
|
|
|
|
test('lists public models', () => {
|
|
expect(publicModels.length).toBe(1)
|
|
expect(publicModels[0].name).toBe('gpt-3.5')
|
|
})
|
|
|
|
test('normalizes tier values', () => {
|
|
expect(normalizeTier('free')).toBe('free')
|
|
expect(normalizeTier('premium')).toBe('premium')
|
|
expect(normalizeTier('invalid')).toBe('standard')
|
|
})
|
|
|
|
test('gets tier multipliers', () => {
|
|
expect(getTierMultiplier('free')).toBe(0.5)
|
|
expect(getTierMultiplier('standard')).toBe(1)
|
|
expect(getTierMultiplier('premium')).toBe(2)
|
|
expect(getTierMultiplier('enterprise')).toBe(5)
|
|
})
|
|
|
|
test('adds new model to opencode', () => {
|
|
const newModel = {
|
|
id: 'model-4',
|
|
name: 'llama-3',
|
|
label: 'LLaMA 3',
|
|
tier: 'standard'
|
|
}
|
|
opencodeModels.push(newModel)
|
|
|
|
expect(opencodeModels.length).toBe(3)
|
|
expect(opencodeModels.find(m => m.id === 'model-4')).toBeDefined()
|
|
})
|
|
|
|
test('updates existing model', () => {
|
|
const model = opencodeModels.find(m => m.id === 'model-1')
|
|
model.label = 'GPT-4 Turbo'
|
|
|
|
expect(model.label).toBe('GPT-4 Turbo')
|
|
})
|
|
|
|
test('deletes model', () => {
|
|
const index = opencodeModels.findIndex(m => m.id === 'model-1')
|
|
opencodeModels.splice(index, 1)
|
|
|
|
expect(opencodeModels.length).toBe(1)
|
|
expect(opencodeModels.find(m => m.id === 'model-1')).toBeUndefined()
|
|
})
|
|
|
|
test('reorders models', () => {
|
|
const newOrder = [
|
|
{ id: 'model-2' },
|
|
{ id: 'model-1' }
|
|
]
|
|
|
|
const reordered = []
|
|
newOrder.forEach(m => {
|
|
const model = opencodeModels.find(pm => pm.id === m.id)
|
|
if (model) reordered.push(model)
|
|
})
|
|
|
|
opencodeModels = reordered
|
|
|
|
expect(opencodeModels[0].id).toBe('model-2')
|
|
expect(opencodeModels[1].id).toBe('model-1')
|
|
})
|
|
})
|
|
|
|
describe('Admin Authentication Validation', () => {
|
|
function checkHoneypot(body) {
|
|
return !!(body && body.website)
|
|
}
|
|
|
|
function isValidEmail(email) {
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
return emailRegex.test(email)
|
|
}
|
|
|
|
test('detects honeypot field', () => {
|
|
expect(checkHoneypot({ website: 'spam' })).toBe(true)
|
|
expect(checkHoneypot({ website: '' })).toBe(true)
|
|
expect(checkHoneypot({ other: 'value' })).toBe(false)
|
|
expect(checkHoneypot({})).toBe(false)
|
|
})
|
|
|
|
test('validates email format', () => {
|
|
expect(isValidEmail('test@example.com')).toBe(true)
|
|
expect(isValidEmail('user.name@domain.co.uk')).toBe(true)
|
|
expect(isValidEmail('invalid')).toBe(false)
|
|
expect(isValidEmail('no@domain')).toBe(false)
|
|
expect(isValidEmail('@nodomain.com')).toBe(false)
|
|
})
|
|
})
|
|
})
|