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:
363
chat/src/utils/encryption.test.js
Normal file
363
chat/src/utils/encryption.test.js
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
533
chat/src/utils/tokenManager.test.js
Normal file
533
chat/src/utils/tokenManager.test.js
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user