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:
southseact-3d
2026-02-18 16:43:10 +00:00
parent b635c80d51
commit 25ee088d6c
6 changed files with 2575 additions and 0 deletions

View File

@@ -0,0 +1,363 @@
const { describe, test, expect, beforeEach, afterEach } = require('bun:test')
const crypto = require('crypto')
const {
initEncryption,
encrypt,
decrypt,
hashValue,
verifyHash,
generateToken,
isEncryptionInitialized
} = require('./encryption')
describe('Encryption Utils', () => {
const testMasterKey = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
beforeEach(() => {
if (isEncryptionInitialized()) {
process.env.ENCRYPTION_KEY = testMasterKey
}
})
describe('initEncryption', () => {
test('initializes with valid 64-character hex key', () => {
initEncryption(testMasterKey)
expect(isEncryptionInitialized()).toBe(true)
})
test('throws error without key', () => {
expect(() => initEncryption()).toThrow('Master encryption key is required')
})
test('throws error with non-string key', () => {
expect(() => initEncryption(123)).toThrow('Master encryption key is required')
})
test('throws error with key shorter than 64 characters', () => {
expect(() => initEncryption('short')).toThrow('must be at least 64 hex characters')
})
test('throws error with null key', () => {
expect(() => initEncryption(null)).toThrow('Master encryption key is required')
})
test('throws error with empty string key', () => {
expect(() => initEncryption('')).toThrow('Master encryption key is required')
})
})
describe('encrypt', () => {
beforeEach(() => {
initEncryption(testMasterKey)
})
test('encrypts a simple string', () => {
const plaintext = 'Hello, World!'
const encrypted = encrypt(plaintext)
expect(encrypted).toBeDefined()
expect(encrypted).not.toBe(plaintext)
expect(encrypted.split(':').length).toBe(4)
})
test('returns empty string for null input', () => {
expect(encrypt(null)).toBe('')
})
test('returns empty string for undefined input', () => {
expect(encrypt(undefined)).toBe('')
})
test('returns empty string for empty string input', () => {
expect(encrypt('')).toBe('')
})
test('produces different ciphertext for same plaintext (random IV)', () => {
const plaintext = 'Same message'
const encrypted1 = encrypt(plaintext)
const encrypted2 = encrypt(plaintext)
expect(encrypted1).not.toBe(encrypted2)
})
test('encrypts special characters', () => {
const plaintext = '!@#$%^&*()_+-=[]{}|;:,.<>?'
const encrypted = encrypt(plaintext)
const decrypted = decrypt(encrypted)
expect(decrypted).toBe(plaintext)
})
test('encrypts unicode characters', () => {
const plaintext = '日本語 🎉 émojis'
const encrypted = encrypt(plaintext)
const decrypted = decrypt(encrypted)
expect(decrypted).toBe(plaintext)
})
test('encrypts long strings', () => {
const plaintext = 'a'.repeat(10000)
const encrypted = encrypt(plaintext)
const decrypted = decrypt(encrypted)
expect(decrypted).toBe(plaintext)
})
test('throws error when encryption not initialized', () => {
const original = require('./encryption')
original.initEncryption(testMasterKey)
const plaintext = 'test'
const result = encrypt(plaintext)
expect(result).toBeDefined()
})
test('produces correct format: salt:iv:tag:ciphertext', () => {
const encrypted = encrypt('test')
const parts = encrypted.split(':')
expect(parts.length).toBe(4)
expect(parts[0].length).toBe(64)
expect(parts[1].length).toBe(32)
expect(parts[2].length).toBe(32)
})
})
describe('decrypt', () => {
beforeEach(() => {
initEncryption(testMasterKey)
})
test('decrypts encrypted string correctly', () => {
const plaintext = 'Secret message'
const encrypted = encrypt(plaintext)
const decrypted = decrypt(encrypted)
expect(decrypted).toBe(plaintext)
})
test('returns empty string for null input', () => {
expect(decrypt(null)).toBe('')
})
test('returns empty string for undefined input', () => {
expect(decrypt(undefined)).toBe('')
})
test('returns empty string for empty string input', () => {
expect(decrypt('')).toBe('')
})
test('throws error for invalid format', () => {
expect(() => decrypt('invalid')).toThrow('Failed to decrypt data')
})
test('throws error for wrong number of parts', () => {
expect(() => decrypt('part1:part2:part3')).toThrow('Failed to decrypt data')
})
test('throws error for tampered ciphertext', () => {
const encrypted = encrypt('test')
const parts = encrypted.split(':')
parts[3] = '0'.repeat(parts[3].length)
expect(() => decrypt(parts.join(':'))).toThrow('Failed to decrypt data')
})
test('throws error for tampered auth tag', () => {
const encrypted = encrypt('test')
const parts = encrypted.split(':')
parts[2] = '0'.repeat(parts[2].length)
expect(() => decrypt(parts.join(':'))).toThrow('Failed to decrypt data')
})
test('fails authentication check with wrong key', () => {
const encrypted = encrypt('test')
initEncryption('fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210')
expect(() => decrypt(encrypted)).toThrow('Failed to decrypt data')
})
})
describe('hashValue', () => {
beforeEach(() => {
initEncryption(testMasterKey)
})
test('hashes a value and returns hash and salt', () => {
const result = hashValue('password123')
expect(result).toHaveProperty('hash')
expect(result).toHaveProperty('salt')
expect(result.hash.length).toBe(64)
expect(result.salt.length).toBe(64)
})
test('produces different hashes for same value (random salt)', () => {
const hash1 = hashValue('password123')
const hash2 = hashValue('password123')
expect(hash1.hash).not.toBe(hash2.hash)
expect(hash1.salt).not.toBe(hash2.salt)
})
test('produces same hash when given same salt', () => {
const salt = 'a'.repeat(64)
const hash1 = hashValue('password123', salt)
const hash2 = hashValue('password123', salt)
expect(hash1.hash).toBe(hash2.hash)
expect(hash1.salt).toBe(salt)
})
test('throws error for null value', () => {
expect(() => hashValue(null)).toThrow('Value is required for hashing')
})
test('throws error for undefined value', () => {
expect(() => hashValue(undefined)).toThrow('Value is required for hashing')
})
test('throws error for empty string value', () => {
expect(() => hashValue('')).toThrow('Value is required for hashing')
})
test('handles special characters', () => {
const result = hashValue('!@#$%^&*()')
expect(result.hash).toBeDefined()
expect(result.salt).toBeDefined()
})
test('handles unicode characters', () => {
const result = hashValue('日本語')
expect(result.hash).toBeDefined()
expect(result.salt).toBeDefined()
})
})
describe('verifyHash', () => {
beforeEach(() => {
initEncryption(testMasterKey)
})
test('verifies correct password', () => {
const { hash, salt } = hashValue('password123')
expect(verifyHash('password123', hash, salt)).toBe(true)
})
test('rejects incorrect password', () => {
const { hash, salt } = hashValue('password123')
expect(verifyHash('wrongpassword', hash, salt)).toBe(false)
})
test('returns false for null value', () => {
expect(verifyHash(null, 'hash', 'salt')).toBe(false)
})
test('returns false for null hash', () => {
expect(verifyHash('value', null, 'salt')).toBe(false)
})
test('returns false for null salt', () => {
expect(verifyHash('value', 'hash', null)).toBe(false)
})
test('returns false for invalid hash format', () => {
expect(verifyHash('value', 'invalid', 'invalid')).toBe(false)
})
test('is timing-safe (constant time comparison)', () => {
const { hash, salt } = hashValue('password123')
const start1 = Date.now()
verifyHash('password123', hash, salt)
const time1 = Date.now() - start1
const start2 = Date.now()
verifyHash('completelywrong', hash, salt)
const time2 = Date.now() - start2
expect(Math.abs(time1 - time2)).toBeLessThan(10)
})
})
describe('generateToken', () => {
test('generates token with default 32 bytes', () => {
const token = generateToken()
expect(token).toBeDefined()
expect(token.length).toBe(64)
})
test('generates token with specified bytes', () => {
const token = generateToken(16)
expect(token.length).toBe(32)
})
test('generates different tokens each time', () => {
const token1 = generateToken()
const token2 = generateToken()
expect(token1).not.toBe(token2)
})
test('generates hex string', () => {
const token = generateToken()
expect(/^[0-9a-f]+$/.test(token)).toBe(true)
})
test('generates token with 0 bytes', () => {
const token = generateToken(0)
expect(token.length).toBe(0)
})
test('generates large tokens', () => {
const token = generateToken(128)
expect(token.length).toBe(256)
})
})
describe('isEncryptionInitialized', () => {
test('returns true after initialization', () => {
initEncryption(testMasterKey)
expect(isEncryptionInitialized()).toBe(true)
})
})
describe('Round-trip encryption', () => {
beforeEach(() => {
initEncryption(testMasterKey)
})
test('encrypt/decrypt round trip for various strings', () => {
const testStrings = [
'simple',
'with spaces',
'with\nnewlines\nand\ttabs',
'emoji: 🎉🚀💻',
'japanese: 日本語テスト',
'special: !@#$%^&*()_+-={}[]|\\:";\'<>?,./`~',
'json: {"key": "value", "number": 123}',
'html: <div class="test">content</div>',
'sql: SELECT * FROM users WHERE id = 1',
'base64: SGVsbG8gV29ybGQ=',
]
testStrings.forEach(str => {
const encrypted = encrypt(str)
const decrypted = decrypt(encrypted)
expect(decrypted).toBe(str)
})
})
})
})

View File

@@ -0,0 +1,533 @@
const { describe, test, expect, beforeEach } = require('bun:test')
const {
initTokenManager,
generateDeviceFingerprint,
generateAccessToken,
verifyAccessToken,
generateRefreshToken,
verifyRefreshToken,
extractToken,
createSecureCookie,
parseCookies,
getTokenTTL,
isTokenManagerInitialized
} = require('./tokenManager')
describe('Token Manager', () => {
const testSecret = 'test-jwt-secret-key-for-testing-purposes-only'
beforeEach(() => {
initTokenManager(testSecret)
})
describe('initTokenManager', () => {
test('initializes with valid secret', () => {
initTokenManager(testSecret)
expect(isTokenManagerInitialized()).toBe(true)
})
test('throws error without secret', () => {
expect(() => initTokenManager()).toThrow('JWT secret is required')
})
test('throws error with non-string secret', () => {
expect(() => initTokenManager(123)).toThrow('JWT secret is required')
})
test('throws error with null secret', () => {
expect(() => initTokenManager(null)).toThrow('JWT secret is required')
})
test('throws error with empty string secret', () => {
expect(() => initTokenManager('')).toThrow('JWT secret is required')
})
})
describe('generateDeviceFingerprint', () => {
test('generates fingerprint from request headers', () => {
const req = {
headers: {
'user-agent': 'Mozilla/5.0',
'accept-language': 'en-US'
},
ip: '127.0.0.1',
connection: { remoteAddress: '127.0.0.1' }
}
const fingerprint = generateDeviceFingerprint(req)
expect(fingerprint).toBeDefined()
expect(fingerprint.length).toBe(32)
expect(/^[0-9a-f]+$/.test(fingerprint)).toBe(true)
})
test('produces same fingerprint for same request', () => {
const req = {
headers: {
'user-agent': 'Mozilla/5.0',
'accept-language': 'en-US'
},
ip: '127.0.0.1'
}
const fp1 = generateDeviceFingerprint(req)
const fp2 = generateDeviceFingerprint(req)
expect(fp1).toBe(fp2)
})
test('produces different fingerprint for different user agent', () => {
const req1 = {
headers: { 'user-agent': 'Mozilla/5.0' },
ip: '127.0.0.1'
}
const req2 = {
headers: { 'user-agent': 'Chrome/1.0' },
ip: '127.0.0.1'
}
const fp1 = generateDeviceFingerprint(req1)
const fp2 = generateDeviceFingerprint(req2)
expect(fp1).not.toBe(fp2)
})
test('handles missing headers gracefully', () => {
const req = { headers: {} }
const fingerprint = generateDeviceFingerprint(req)
expect(fingerprint).toBeDefined()
expect(fingerprint.length).toBe(32)
})
test('handles x-forwarded-for header', () => {
const req = {
headers: {
'user-agent': 'Mozilla/5.0',
'x-forwarded-for': '192.168.1.1'
}
}
const fingerprint = generateDeviceFingerprint(req)
expect(fingerprint).toBeDefined()
expect(fingerprint.length).toBe(32)
})
test('handles null/undefined values', () => {
const req = {
headers: {
'user-agent': null,
'accept-language': undefined
}
}
const fingerprint = generateDeviceFingerprint(req)
expect(fingerprint).toBeDefined()
})
})
describe('generateAccessToken', () => {
test('generates valid JWT token', () => {
const payload = {
userId: 'user-123',
email: 'test@example.com',
role: 'user',
plan: 'hobby'
}
const token = generateAccessToken(payload)
expect(token).toBeDefined()
expect(token.split('.').length).toBe(3)
})
test('includes all payload fields', () => {
const payload = {
userId: 'user-123',
email: 'test@example.com',
role: 'admin',
plan: 'enterprise'
}
const token = generateAccessToken(payload)
const decoded = verifyAccessToken(token)
expect(decoded.userId).toBe(payload.userId)
expect(decoded.email).toBe(payload.email)
expect(decoded.role).toBe(payload.role)
expect(decoded.plan).toBe(payload.plan)
})
test('includes jti (JWT ID)', () => {
const token = generateAccessToken({ userId: '123', email: 'test@test.com' })
const decoded = verifyAccessToken(token)
expect(decoded.jti).toBeDefined()
expect(decoded.jti).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
})
test('includes iat (issued at)', () => {
const beforeTime = Math.floor(Date.now() / 1000)
const token = generateAccessToken({ userId: '123', email: 'test@test.com' })
const afterTime = Math.floor(Date.now() / 1000)
const decoded = verifyAccessToken(token)
expect(decoded.iat).toBeGreaterThanOrEqual(beforeTime)
expect(decoded.iat).toBeLessThanOrEqual(afterTime)
})
test('includes exp (expiration)', () => {
const token = generateAccessToken({ userId: '123', email: 'test@test.com' })
const decoded = verifyAccessToken(token)
const { accessTokenTTL } = getTokenTTL()
expect(decoded.exp).toBeDefined()
expect(decoded.exp - decoded.iat).toBe(accessTokenTTL)
})
test('uses default values for missing role/plan', () => {
const token = generateAccessToken({ userId: '123', email: 'test@test.com' })
const decoded = verifyAccessToken(token)
expect(decoded.role).toBe('user')
expect(decoded.plan).toBe('hobby')
})
test('accepts custom TTL option', () => {
const customTTL = 3600
const token = generateAccessToken(
{ userId: '123', email: 'test@test.com' },
{ ttl: customTTL }
)
const decoded = verifyAccessToken(token)
expect(decoded.exp - decoded.iat).toBe(customTTL)
})
test('generates unique tokens', () => {
const payload = { userId: '123', email: 'test@test.com' }
const token1 = generateAccessToken(payload)
const token2 = generateAccessToken(payload)
expect(token1).not.toBe(token2)
})
})
describe('verifyAccessToken', () => {
test('verifies and decodes valid token', () => {
const payload = {
userId: 'user-123',
email: 'test@example.com',
role: 'admin'
}
const token = generateAccessToken(payload)
const decoded = verifyAccessToken(token)
expect(decoded.userId).toBe(payload.userId)
expect(decoded.email).toBe(payload.email)
expect(decoded.role).toBe(payload.role)
})
test('returns expired object for expired token', () => {
const token = generateAccessToken(
{ userId: '123', email: 'test@test.com' },
{ ttl: -1 }
)
const result = verifyAccessToken(token)
expect(result.expired).toBe(true)
expect(result.error).toBe('Token expired')
})
test('returns invalid object for malformed token', () => {
const result = verifyAccessToken('invalid.token.here')
expect(result.invalid).toBe(true)
expect(result.error).toBe('Invalid token')
})
test('returns null for completely invalid input', () => {
const result = verifyAccessToken('not-a-jwt')
expect(result.invalid).toBe(true)
})
test('rejects token signed with wrong secret', () => {
const jwt = require('jsonwebtoken')
const wrongSecretToken = jwt.sign(
{ userId: '123' },
'wrong-secret',
{ algorithm: 'HS256' }
)
const result = verifyAccessToken(wrongSecretToken)
expect(result.invalid).toBe(true)
})
test('rejects token with wrong algorithm', () => {
const jwt = require('jsonwebtoken')
const noneAlgToken = jwt.sign(
{ userId: '123' },
'',
{ algorithm: 'none' }
)
const result = verifyAccessToken(noneAlgToken)
expect(result.invalid).toBe(true)
})
})
describe('generateRefreshToken', () => {
test('generates refresh token', () => {
const result = generateRefreshToken()
expect(result.token).toBeDefined()
expect(result.tokenHash).toBeDefined()
})
test('token is 128 character hex string', () => {
const { token } = generateRefreshToken()
expect(token.length).toBe(128)
expect(/^[0-9a-f]+$/.test(token)).toBe(true)
})
test('tokenHash contains salt and hash', () => {
const { tokenHash } = generateRefreshToken()
const parts = tokenHash.split(':')
expect(parts.length).toBe(2)
expect(parts[0].length).toBe(64)
expect(parts[1].length).toBe(64)
})
test('generates unique tokens', () => {
const { token: token1 } = generateRefreshToken()
const { token: token2 } = generateRefreshToken()
expect(token1).not.toBe(token2)
})
})
describe('verifyRefreshToken', () => {
test('verifies correct refresh token', () => {
const { token, tokenHash } = generateRefreshToken()
expect(verifyRefreshToken(token, tokenHash)).toBe(true)
})
test('rejects incorrect refresh token', () => {
const { tokenHash } = generateRefreshToken()
expect(verifyRefreshToken('wrong-token', tokenHash)).toBe(false)
})
test('returns false for null token', () => {
expect(verifyRefreshToken(null, 'hash')).toBe(false)
})
test('returns false for null hash', () => {
expect(verifyRefreshToken('token', null)).toBe(false)
})
test('returns false for malformed hash', () => {
expect(verifyRefreshToken('token', 'malformed')).toBe(false)
})
test('returns false for hash with wrong format', () => {
expect(verifyRefreshToken('token', 'only-one-part')).toBe(false)
})
})
describe('extractToken', () => {
test('extracts from Authorization header (Bearer)', () => {
const req = {
headers: {
authorization: 'Bearer my-token-123'
}
}
expect(extractToken(req)).toBe('my-token-123')
})
test('extracts from cookie', () => {
const req = {
headers: {
cookie: 'access_token=cookie-token-456; other=value'
}
}
expect(extractToken(req)).toBe('cookie-token-456')
})
test('prefers Authorization header over cookie', () => {
const req = {
headers: {
authorization: 'Bearer header-token',
cookie: 'access_token=cookie-token'
}
}
expect(extractToken(req)).toBe('header-token')
})
test('extracts from custom cookie name', () => {
const req = {
headers: {
cookie: 'custom_token=custom-value'
}
}
expect(extractToken(req, 'custom_token')).toBe('custom-value')
})
test('returns null when no token found', () => {
const req = { headers: {} }
expect(extractToken(req)).toBe(null)
})
test('handles empty Authorization header', () => {
const req = {
headers: {
authorization: ''
}
}
expect(extractToken(req)).toBe(null)
})
test('handles non-Bearer Authorization', () => {
const req = {
headers: {
authorization: 'Basic dXNlcjpwYXNz'
}
}
expect(extractToken(req)).toBe(null)
})
})
describe('parseCookies', () => {
test('parses single cookie', () => {
const result = parseCookies('name=value')
expect(result.name).toBe('value')
})
test('parses multiple cookies', () => {
const result = parseCookies('name1=value1; name2=value2')
expect(result.name1).toBe('value1')
expect(result.name2).toBe('value2')
})
test('handles cookies with spaces', () => {
const result = parseCookies('name1=value1 ; name2 = value2')
expect(result.name1).toBe('value1')
expect(result.name2).toBe('value2')
})
test('handles cookies with equals in value', () => {
const result = parseCookies('name=value=with=equals')
expect(result.name).toBe('value=with=equals')
})
test('returns empty object for null input', () => {
expect(parseCookies(null)).toEqual({})
})
test('returns empty object for undefined input', () => {
expect(parseCookies(undefined)).toEqual({})
})
test('returns empty object for empty string', () => {
expect(parseCookies('')).toEqual({})
})
})
describe('createSecureCookie', () => {
test('creates basic cookie', () => {
const result = createSecureCookie('name', 'value')
expect(result).toContain('name=value')
expect(result).toContain('Path=/')
expect(result).toContain('HttpOnly')
expect(result).toContain('SameSite=Strict')
})
test('includes Max-Age when provided', () => {
const result = createSecureCookie('name', 'value', { maxAge: 3600 })
expect(result).toContain('Max-Age=3600')
})
test('includes custom Path when provided', () => {
const result = createSecureCookie('name', 'value', { path: '/api' })
expect(result).toContain('Path=/api')
})
test('includes Secure when provided', () => {
const result = createSecureCookie('name', 'value', { secure: true })
expect(result).toContain('Secure')
})
test('includes custom SameSite when provided', () => {
const result = createSecureCookie('name', 'value', { sameSite: 'Lax' })
expect(result).toContain('SameSite=Lax')
})
test('omits HttpOnly when httpOnly is false', () => {
const result = createSecureCookie('name', 'value', { httpOnly: false })
expect(result).not.toContain('HttpOnly')
})
test('creates cookie with all options', () => {
const result = createSecureCookie('name', 'value', {
maxAge: 3600,
path: '/api',
httpOnly: true,
secure: true,
sameSite: 'None'
})
expect(result).toContain('name=value')
expect(result).toContain('Max-Age=3600')
expect(result).toContain('Path=/api')
expect(result).toContain('HttpOnly')
expect(result).toContain('Secure')
expect(result).toContain('SameSite=None')
})
})
describe('getTokenTTL', () => {
test('returns TTL values', () => {
const ttl = getTokenTTL()
expect(ttl).toHaveProperty('accessTokenTTL')
expect(ttl).toHaveProperty('refreshTokenTTL')
expect(ttl.accessTokenTTL).toBe(15 * 60)
expect(ttl.refreshTokenTTL).toBe(7 * 24 * 60 * 60)
})
})
describe('isTokenManagerInitialized', () => {
test('returns true after initialization', () => {
initTokenManager(testSecret)
expect(isTokenManagerInitialized()).toBe(true)
})
})
})