From 34a83fb0fc345854362c44020505650456333d50 Mon Sep 17 00:00:00 2001 From: Dorian Date: Thu, 12 Mar 2026 23:43:57 +0000 Subject: [PATCH] test: add auth routes test suite with 13 cases Tests check-name validation, login pubkey validation, register name/pubkey validation, register-human validation, NIP-98 session (valid + expired). Co-Authored-By: Claude Opus 4.6 --- server/src/routes/auth.test.ts | 159 +++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 server/src/routes/auth.test.ts diff --git a/server/src/routes/auth.test.ts b/server/src/routes/auth.test.ts new file mode 100644 index 0000000..398dbd8 --- /dev/null +++ b/server/src/routes/auth.test.ts @@ -0,0 +1,159 @@ +import { describe, it, expect, vi } from 'vitest' +import { Hono } from 'hono' +import { authRouter } from './auth.js' +import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools' + +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')}` +} + +describe('auth routes', () => { + // --- check-name --- + it('check-name: rejects short name', async () => { + const res = await app.request('/api/auth/check-name/x') + const body = await res.json() as { available: boolean; error?: string } + expect(body.available).toBe(false) + expect(body.error).toContain('2-12') + }) + + it('check-name: rejects long name (>12)', async () => { + const res = await app.request('/api/auth/check-name/verylongbotnamehere') + const body = await res.json() as { available: boolean; error?: string } + expect(body.available).toBe(false) + }) + + it('check-name: available name returns true', async () => { + const name = `t${Date.now().toString(36).slice(-6)}` + const res = await app.request(`/api/auth/check-name/${name}`) + const body = await res.json() as { available: boolean } + expect(body.available).toBe(true) + }) + + // --- login --- + it('login: rejects invalid pubkey (too short)', async () => { + const res = await app.request('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ pubkey: 'tooshort' }), + }) + expect(res.status).toBe(400) + const body = await res.json() as { error: string } + expect(body.error).toContain('Invalid pubkey') + }) + + it('login: rejects missing pubkey', async () => { + const res = await app.request('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({}), + }) + expect(res.status).toBe(400) + }) + + it('login: returns exists=false for unknown pubkey', async () => { + const pk = '0'.repeat(64) + const res = await app.request('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ pubkey: pk }), + }) + expect(res.status).toBe(200) + const body = await res.json() as { exists: boolean; pubkey?: string } + expect(body.exists).toBe(false) + expect(body.pubkey).toBe(pk) + }) + + // --- register --- + it('register: rejects invalid pubkey', async () => { + const res = await app.request('/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ pubkey: 'bad', name: 'test-bot' }), + }) + expect(res.status).toBe(400) + const body = await res.json() as { error: string } + expect(body.error).toContain('Invalid pubkey') + }) + + it('register: rejects invalid name (special chars)', async () => { + const res = await app.request('/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ pubkey: 'a'.repeat(64), name: 'hello world!' }), + }) + expect(res.status).toBe(400) + const body = await res.json() as { error: string } + expect(body.error).toContain('alphanumeric') + }) + + it('register: rejects name too short', async () => { + const res = await app.request('/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ pubkey: 'b'.repeat(64), name: 'x' }), + }) + expect(res.status).toBe(400) + const body = await res.json() as { error: string } + expect(body.error).toContain('2-12') + }) + + // --- register-human --- + it('register-human: rejects invalid pubkey', async () => { + const res = await app.request('/api/auth/register-human', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ pubkey: 'short', name: 'human1' }), + }) + expect(res.status).toBe(400) + }) + + // --- nostr/session --- + it('nostr/session: rejects missing auth header', async () => { + const res = await app.request('/api/auth/nostr/session', { + method: 'POST', + }) + expect(res.status).toBe(401) + }) + + it('nostr/session: accepts valid NIP-98 token', async () => { + const sk = generateSecretKey() + const pk = getPublicKey(sk) + const header = makeNip98Header(sk, 'https://localhost/api/auth/nostr/session', 'POST') + + const res = await app.request('/api/auth/nostr/session', { + method: 'POST', + headers: { Authorization: header }, + }) + expect(res.status).toBe(200) + const body = await res.json() as { token: string; pubkey: string; exists: boolean } + expect(body.token).toBeDefined() + expect(body.pubkey).toBe(pk) + expect(body.exists).toBe(false) + }) + + it('nostr/session: rejects expired NIP-98 token', async () => { + const sk = generateSecretKey() + const event = finalizeEvent({ + kind: 27235, + created_at: Math.floor(Date.now() / 1000) - 300, + tags: [['u', 'https://localhost/api/auth/nostr/session'], ['method', 'POST']], + content: '', + }, sk) + const header = `Nostr ${Buffer.from(JSON.stringify(event)).toString('base64')}` + + const res = await app.request('/api/auth/nostr/session', { + method: 'POST', + headers: { Authorization: header }, + }) + expect(res.status).toBe(401) + }) +})