test: add poll-responses, NIP-98, and bot-auth test suites

- poll-responses.test.ts: 10 tests covering lifecycle, timeout, duplicate rejection
- nip98.test.ts: 7 tests covering valid token, expiry, method, signature, tags
- bot-auth.test.ts: 5 tests covering header auth, query params, invalid credentials

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-12 23:40:24 +00:00
co-authored by Claude Opus 4.6
parent a49cc124fe
commit 21f9650585
3 changed files with 341 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
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')
})
})
+123
View File
@@ -0,0 +1,123 @@
import { describe, it, expect, vi, afterEach } from 'vitest'
import { verifyNip98Token } from './nip98.js'
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools'
function createNip98Event(
sk: Uint8Array,
url: string,
method: string,
overrides: Record<string, unknown> = {},
) {
const event = {
kind: 27235,
created_at: Math.floor(Date.now() / 1000),
tags: [
['u', url],
['method', method],
],
content: '',
...overrides,
}
return finalizeEvent(event, sk)
}
function toAuthHeader(event: object): string {
return `Nostr ${Buffer.from(JSON.stringify(event)).toString('base64')}`
}
describe('verifyNip98Token', () => {
afterEach(() => {
vi.useRealTimers()
})
it('accepts a valid NIP-98 token', () => {
const sk = generateSecretKey()
const pk = getPublicKey(sk)
const event = createNip98Event(sk, 'https://example.com/api/auth/nostr/session', 'POST')
const header = toAuthHeader(event)
const result = verifyNip98Token(header, '/api/auth/nostr/session', 'POST')
expect(result.valid).toBe(true)
expect(result.pubkey).toBe(pk)
})
it('rejects expired token (>120s)', () => {
const sk = generateSecretKey()
const event = createNip98Event(sk, 'https://example.com/api/auth', 'POST', {
created_at: Math.floor(Date.now() / 1000) - 200,
})
// Need to re-finalize with the old created_at
// Actually createNip98Event's overrides merge before finalize, but created_at gets set by finalizeEvent
// Let me build manually
const rawEvent = {
kind: 27235,
created_at: Math.floor(Date.now() / 1000) - 200,
tags: [['u', 'https://example.com/api/auth'], ['method', 'POST']],
content: '',
}
const signed = finalizeEvent(rawEvent, sk)
const header = toAuthHeader(signed)
const result = verifyNip98Token(header, '/api/auth', 'POST')
expect(result.valid).toBe(false)
expect(result.error).toContain('expired')
})
it('rejects wrong method', () => {
const sk = generateSecretKey()
const event = createNip98Event(sk, 'https://example.com/api/auth', 'POST')
const header = toAuthHeader(event)
const result = verifyNip98Token(header, '/api/auth', 'GET')
expect(result.valid).toBe(false)
expect(result.error).toContain('Method mismatch')
})
it('rejects invalid signature (tampered event)', () => {
const sk = generateSecretKey()
const event = createNip98Event(sk, 'https://example.com/api/auth', 'POST')
// Tamper with pubkey
const tampered = { ...event, pubkey: '0'.repeat(64) }
const header = toAuthHeader(tampered)
const result = verifyNip98Token(header, '/api/auth', 'POST')
expect(result.valid).toBe(false)
expect(result.error).toContain('Invalid signature')
})
it('rejects missing URL tag', () => {
const sk = generateSecretKey()
const event = finalizeEvent({
kind: 27235,
created_at: Math.floor(Date.now() / 1000),
tags: [['method', 'POST']],
content: '',
}, sk)
const header = toAuthHeader(event)
const result = verifyNip98Token(header, '/api/auth', 'POST')
expect(result.valid).toBe(false)
expect(result.error).toContain('Missing URL tag')
})
it('rejects invalid auth header format', () => {
const result = verifyNip98Token('Bearer xyz123', '/api/auth', 'POST')
expect(result.valid).toBe(false)
expect(result.error).toContain('Invalid auth header format')
})
it('rejects wrong event kind', () => {
const sk = generateSecretKey()
const event = finalizeEvent({
kind: 1, // Regular note, not 27235
created_at: Math.floor(Date.now() / 1000),
tags: [['u', 'https://example.com/api/auth'], ['method', 'POST']],
content: '',
}, sk)
const header = toAuthHeader(event)
const result = verifyNip98Token(header, '/api/auth', 'POST')
expect(result.valid).toBe(false)
expect(result.error).toContain('Wrong event kind')
})
})