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:
co-authored by
Claude Opus 4.6
parent
5799ff60d3
commit
f15504b400
@@ -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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user