test: add auth registration edge cases — concurrent names, expired JWT, NIP-98 tampering

Tests concurrent same-name registration (exactly one succeeds),
case-insensitive name collisions, expired JWT rejection, NIP-98
pubkey mismatch, and duplicate pubkey prevention.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-13 10:15:13 +00:00
co-authored by Claude Opus 4.6
parent 5799ff60d3
commit f15504b400
+137
View File
@@ -0,0 +1,137 @@
import { describe, it, expect, vi, afterEach } from 'vitest'
import { Hono } from 'hono'
import { authRouter } from './auth.js'
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools'
import { createJwt, extractPubkeyFromAuth } from '../middleware/jwt.js'
const app = new Hono()
app.route('/api/auth', authRouter)
function makeNip98Header(sk: Uint8Array, url: string, method: string) {
const event = finalizeEvent({
kind: 27235,
created_at: Math.floor(Date.now() / 1000),
tags: [['u', url], ['method', method]],
content: '',
}, sk)
return `Nostr ${Buffer.from(JSON.stringify(event)).toString('base64')}`
}
function uniqueName() {
return `t${Date.now().toString(36).slice(-5)}${Math.random().toString(36).slice(2, 4)}`
}
describe('auth edge cases — 7.2', () => {
// --- concurrent same-name registration ---
it('concurrent same-name registration: exactly one succeeds, other fails', async () => {
const name = uniqueName()
const pk1 = getPublicKey(generateSecretKey())
const pk2 = getPublicKey(generateSecretKey())
const [res1, res2] = await Promise.all([
app.request('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pubkey: pk1, name }),
}),
app.request('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pubkey: pk2, name }),
}),
])
// Exactly one should succeed (201)
// The loser hits either the name check (409) or the DB UNIQUE constraint (500)
// depending on timing — both are acceptable as long as only one bot is created
const successCount = [res1.status, res2.status].filter(s => s === 201).length
expect(successCount).toBe(1)
// The failure status should be 4xx or 5xx
const failStatus = [res1.status, res2.status].find(s => s !== 201)!
expect(failStatus).toBeGreaterThanOrEqual(400)
})
it('case-insensitive name collision: "MyBot" blocks "mybot"', async () => {
const name = uniqueName()
const pk1 = getPublicKey(generateSecretKey())
const pk2 = getPublicKey(generateSecretKey())
// First registration
const res1 = await app.request('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pubkey: pk1, name: name.toUpperCase() }),
})
expect(res1.status).toBe(201)
// Second with same name different case
const res2 = await app.request('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pubkey: pk2, name: name.toLowerCase() }),
})
expect(res2.status).toBe(409)
const body = await res2.json() as { error: string }
expect(body.error).toContain('already exists')
})
// --- expired JWT at route level ---
it('expired JWT: extractPubkeyFromAuth returns null for expired token', () => {
vi.useFakeTimers()
const token = createJwt('a'.repeat(64), 'bot-1')
// Advance past 24h expiry
vi.advanceTimersByTime(25 * 60 * 60 * 1000)
const pubkey = extractPubkeyFromAuth(`Bearer ${token}`)
expect(pubkey).toBeNull()
})
it('NIP-98 with different key than claimed: signature rejected', async () => {
const sk1 = generateSecretKey()
const sk2 = generateSecretKey()
// Sign with sk1 but claim sk2's pubkey by tampering
const event = finalizeEvent({
kind: 27235,
created_at: Math.floor(Date.now() / 1000),
tags: [['u', 'https://localhost/api/auth/nostr/session'], ['method', 'POST']],
content: '',
}, sk1)
// Tamper: replace pubkey with sk2's pubkey (simulates NIP-07 returning wrong key)
const tampered = { ...event, pubkey: getPublicKey(sk2) }
const header = `Nostr ${Buffer.from(JSON.stringify(tampered)).toString('base64')}`
const res = await app.request('/api/auth/nostr/session', {
method: 'POST',
headers: { Authorization: header },
})
expect(res.status).toBe(401)
})
// --- duplicate pubkey registration ---
it('same pubkey cannot register twice', async () => {
const pk = getPublicKey(generateSecretKey())
const res1 = await app.request('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pubkey: pk, name: uniqueName() }),
})
expect(res1.status).toBe(201)
const res2 = await app.request('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pubkey: pk, name: uniqueName() }),
})
expect(res2.status).toBe(409)
const body = await res2.json() as { error: string }
expect(body.error).toContain('already has a bot')
})
afterEach(() => {
vi.useRealTimers()
})
})