import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { createHash } from 'crypto' import { Hono } from 'hono' // Mock the db module before importing authenticateBot const mockSelect = vi.fn() vi.mock('../db/index.js', () => ({ db: { select: (...args: unknown[]) => { const result = mockSelect(...args) return { from: () => ({ where: () => ({ limit: () => result, }), }), } }, }, schema: { bots: { id: 'id', name: 'name', secretHash: 'secretHash', webhookUrl: 'webhookUrl', }, }, })) // Import after mock setup const { authenticateBot } = await import('./bot-auth.js') const TEST_SECRET = 'my-bot-secret-key' const TEST_HASH = createHash('sha256').update(TEST_SECRET).digest('hex') const TEST_BOT = { id: 'bot-123', name: 'TestBot', secretHash: TEST_HASH, webhookUrl: 'https://example.com/webhook', } function createApp() { const app = new Hono() app.get('/test', async (c) => { const result = await authenticateBot(c) if (result instanceof Response) return result return c.json({ botId: result.botId, botName: result.botName }) }) return app } describe('authenticateBot', () => { beforeEach(() => { mockSelect.mockReset() }) it('authenticates via Authorization header', async () => { mockSelect.mockReturnValue([TEST_BOT]) const app = createApp() const res = await app.request('/test', { headers: { Authorization: `Bot bot-123:${TEST_SECRET}` }, }) expect(res.status).toBe(200) const body = await res.json() as { botId: string; botName: string } expect(body.botId).toBe('bot-123') expect(body.botName).toBe('TestBot') }) it('authenticates via query params', async () => { mockSelect.mockReturnValue([TEST_BOT]) const app = createApp() const res = await app.request(`/test?bot_id=bot-123&secret=${TEST_SECRET}`) expect(res.status).toBe(200) const body = await res.json() as { botId: string } expect(body.botId).toBe('bot-123') }) it('returns 401 when no credentials provided', async () => { const app = createApp() const res = await app.request('/test') expect(res.status).toBe(401) const body = await res.json() as { error: string } expect(body.error).toContain('Authentication required') }) it('returns 401 for invalid bot_id (not found)', async () => { mockSelect.mockReturnValue([]) const app = createApp() const res = await app.request('/test', { headers: { Authorization: `Bot unknown:${TEST_SECRET}` }, }) expect(res.status).toBe(401) const body = await res.json() as { error: string } expect(body.error).toContain('Invalid bot_id or secret') }) it('returns 401 for wrong secret', async () => { mockSelect.mockReturnValue([TEST_BOT]) const app = createApp() const res = await app.request('/test', { headers: { Authorization: 'Bot bot-123:wrong-secret' }, }) expect(res.status).toBe(401) const body = await res.json() as { error: string } expect(body.error).toContain('Invalid bot_id or secret') }) }) describe('constant-time comparison', () => { it('uses constant-time XOR loop (not early-exit)', async () => { // Verify the wrong-secret response time doesn't vary significantly // between a completely wrong secret and an almost-correct one mockSelect.mockReturnValue([TEST_BOT]) const app = createApp() // Completely wrong secret (first char differs) const res1 = await app.request('/test', { headers: { Authorization: 'Bot bot-123:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' }, }) expect(res1.status).toBe(401) // Almost-correct secret (only last char differs) const almostRight = TEST_SECRET.slice(0, -1) + 'X' const res2 = await app.request('/test', { headers: { Authorization: `Bot bot-123:${almostRight}` }, }) expect(res2.status).toBe(401) // Both return identical error messages (no info leakage) const body1 = await res1.json() as { error: string } const body2 = await res2.json() as { error: string } expect(body1.error).toBe(body2.error) expect(body1.error).toContain('Invalid bot_id or secret') }) it('response time variance is minimal across 100 requests', async () => { mockSelect.mockReturnValue([TEST_BOT]) const app = createApp() const times: number[] = [] for (let i = 0; i < 100; i++) { // Vary the secret to test different XOR paths const secret = `wrong-secret-${i.toString().padStart(4, '0')}` const start = performance.now() await app.request('/test', { headers: { Authorization: `Bot bot-123:${secret}` }, }) times.push(performance.now() - start) } const mean = times.reduce((a, b) => a + b, 0) / times.length const variance = times.reduce((a, b) => a + (b - mean) ** 2, 0) / times.length const stddev = Math.sqrt(variance) // Standard deviation should be small relative to mean // In practice, network/test overhead dominates, so we just check // that no request is dramatically slower (which would indicate timing leak) const maxTime = Math.max(...times) const minTime = Math.min(...times) // Max should not be more than 10x min (very lenient for CI) expect(maxTime).toBeLessThan(minTime * 10 + 1) }) })