import { describe, test, expect, beforeEach, afterAll, mock, spyOn, } from 'bun:test' // Mock config before importing modules that depend on it const mockConfig = { port: 3000, host: '0.0.0.0', apiKeys: ['test-key-1', 'test-key-2'], baseUrl: '', pollTimeout: 8, heartbeatInterval: 20, jwtExpiresIn: 3600, disconnectTimeout: 300, webCorsOrigins: [], wsIdleTimeout: 30, wsKeepaliveInterval: 20, } mock.module('../config', () => ({ config: mockConfig, getBaseUrl: () => 'http://localhost:3000', })) import { validateApiKey, hashApiKey } from '../auth/api-key' import { generateWorkerJwt, verifyWorkerJwt } from '../auth/jwt' import { issueToken, resolveToken } from '../auth/token' import { storeReset, storeCreateUser } from '../store' // ---------- api-key ---------- describe('validateApiKey', () => { test('validates a configured API key', () => { expect(validateApiKey('test-key-1')).toBe(true) expect(validateApiKey('test-key-2')).toBe(true) }) test('rejects unknown key', () => { expect(validateApiKey('unknown-key')).toBe(false) }) test('rejects undefined', () => { expect(validateApiKey(undefined)).toBe(false) }) test('rejects empty string', () => { expect(validateApiKey('')).toBe(false) }) }) describe('hashApiKey', () => { test('produces consistent SHA-256 hex', () => { const hash = hashApiKey('my-key') expect(hash).toMatch(/^[0-9a-f]{64}$/) expect(hashApiKey('my-key')).toBe(hash) }) test('different keys produce different hashes', () => { expect(hashApiKey('key-a')).not.toBe(hashApiKey('key-b')) }) }) // ---------- jwt ---------- describe('JWT', () => { // JWT reads process.env.RCS_API_KEYS directly (not via config) const originalKeys = process.env.RCS_API_KEYS beforeEach(() => { process.env.RCS_API_KEYS = 'jwt-test-secret' }) afterAll(() => { process.env.RCS_API_KEYS = originalKeys }) describe('generateWorkerJwt', () => { test('produces a three-part base64url token', () => { const token = generateWorkerJwt('ses_123', 3600) const parts = token.split('.') expect(parts).toHaveLength(3) for (const part of parts) { expect(part).toMatch(/^[A-Za-z0-9_-]+$/) } }) test('contains correct header', () => { const token = generateWorkerJwt('ses_123', 3600) const header = JSON.parse( atob(token.split('.')[0].replace(/-/g, '+').replace(/_/g, '/')), ) expect(header.alg).toBe('HS256') expect(header.typ).toBe('JWT') }) test('throws when no API key configured', () => { delete process.env.RCS_API_KEYS expect(() => generateWorkerJwt('ses_123', 3600)).toThrow( 'No API key configured', ) process.env.RCS_API_KEYS = 'jwt-test-secret' }) }) describe('verifyWorkerJwt', () => { test('verifies a valid token', () => { const token = generateWorkerJwt('ses_abc', 3600) const payload = verifyWorkerJwt(token) expect(payload).not.toBeNull() expect(payload!.session_id).toBe('ses_abc') expect(payload!.role).toBe('worker') expect(payload!.iat).toBeGreaterThan(0) expect(payload!.exp).toBeGreaterThan(payload!.iat) }) test('returns null for expired token', () => { const token = generateWorkerJwt('ses_old', -10) expect(verifyWorkerJwt(token)).toBeNull() }) test('returns null for malformed token (not 3 parts)', () => { expect(verifyWorkerJwt('a.b')).toBeNull() expect(verifyWorkerJwt('just-a-string')).toBeNull() }) test('returns null for tampered signature', () => { const token = generateWorkerJwt('ses_123', 3600) const parts = token.split('.') const tampered = `${parts[0]}.${parts[1]}.${parts[2].slice(0, -4)}xxxx` expect(verifyWorkerJwt(tampered)).toBeNull() }) test('returns null for wrong signing key', () => { const token = generateWorkerJwt('ses_123', 3600) process.env.RCS_API_KEYS = 'wrong-key' expect(verifyWorkerJwt(token)).toBeNull() process.env.RCS_API_KEYS = 'jwt-test-secret' }) }) }) // ---------- token ---------- describe('issueToken / resolveToken', () => { beforeEach(() => { storeReset() }) test('issues and resolves a token', () => { storeCreateUser('alice') const { token, expires_in } = issueToken('alice') expect(token).toMatch(/^rct_\d+_[0-9a-f]+$/) expect(expires_in).toBe(86400) expect(resolveToken(token)).toBe('alice') }) test('returns null for unknown token', () => { expect(resolveToken('nonexistent')).toBeNull() }) test('returns null for undefined token', () => { expect(resolveToken(undefined)).toBeNull() }) test('tokens are unique', () => { storeCreateUser('alice') const t1 = issueToken('alice').token const t2 = issueToken('alice').token expect(t1).not.toBe(t2) }) })