test: Add comprehensive test coverage for critical modules
- 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
This commit is contained in:
530
chat/admin/admin.test.js
Normal file
530
chat/admin/admin.test.js
Normal file
@@ -0,0 +1,530 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user