From e824f4ca7fc5f8b262868c0e9f97163a0500b65b Mon Sep 17 00:00:00 2001 From: Dorian Date: Thu, 30 Jul 2026 22:03:18 -0400 Subject: [PATCH] test(09-02): add failing test for GET /api/auth/me (BOT-01) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers missing / malformed / forged / blacklisted / unregistered / valid JWT cases for the JWT-only identity route that replaces the bare-pubkey auto-restore path. Route does not exist yet — 6/7 fail as expected. Co-Authored-By: Claude Fable 5 --- server/src/routes/auth-me.test.ts | 116 ++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 server/src/routes/auth-me.test.ts diff --git a/server/src/routes/auth-me.test.ts b/server/src/routes/auth-me.test.ts new file mode 100644 index 0000000..07c6997 --- /dev/null +++ b/server/src/routes/auth-me.test.ts @@ -0,0 +1,116 @@ +import { describe, it, expect } from 'vitest' +import { Hono } from 'hono' +import { authRouter } from './auth.js' +import { generateSecretKey, getPublicKey } from 'nostr-tools' +import { createJwt, blacklistJwt } from '../middleware/jwt.js' +import { db, schema } from '../db/index.js' +import { createHash, randomBytes } from 'crypto' +import { nanoid } from 'nanoid' + +const app = new Hono() +app.route('/api/auth', authRouter) + +async function seedBot(pubkey: string, overrides: Partial = {}) { + const id = overrides.id || nanoid(12) + const secret = randomBytes(32).toString('hex') + await db.insert(schema.bots).values({ + id, + name: overrides.name || `t${Date.now().toString(36).slice(-8)}`, + webhookUrl: overrides.webhookUrl || 'http://poll.local/', + avatarSeed: overrides.avatarSeed || 'seed', + archetype: overrides.archetype || 'standard', + secretHash: createHash('sha256').update(secret).digest('hex'), + publicKey: pubkey, + profilePicUrl: overrides.profilePicUrl ?? null, + customization: overrides.customization ?? null, + createdAt: overrides.createdAt || new Date().toISOString(), + }) + return id +} + +describe('GET /api/auth/me', () => { + it('rejects when no Authorization header is present', async () => { + const res = await app.request('/api/auth/me') + expect(res.status).toBe(401) + const body = await res.json() as { error: string } + expect(body.error).toBeDefined() + }) + + it('rejects a malformed / garbage Bearer value', async () => { + const res = await app.request('/api/auth/me', { + headers: { Authorization: 'Bearer not-a-real-jwt' }, + }) + expect(res.status).toBe(401) + const body = await res.json() as { error: string } + expect(body.error).toBeDefined() + }) + + it('rejects a token whose signature does not verify', async () => { + const pk = getPublicKey(generateSecretKey()) + const token = createJwt(pk) + const parts = token.split('.') + // tamper one character of the signature segment + const tamperedSig = (parts[2][0] === 'a' ? 'b' : 'a') + parts[2].slice(1) + const tampered = `${parts[0]}.${parts[1]}.${tamperedSig}` + + const res = await app.request('/api/auth/me', { + headers: { Authorization: `Bearer ${tampered}` }, + }) + expect(res.status).toBe(401) + }) + + it('rejects a blacklisted token', async () => { + const pk = getPublicKey(generateSecretKey()) + const token = createJwt(pk) + blacklistJwt(token) + + const res = await app.request('/api/auth/me', { + headers: { Authorization: `Bearer ${token}` }, + }) + expect(res.status).toBe(401) + }) + + it('returns exists:false for a valid token with no bot row', async () => { + const pk = getPublicKey(generateSecretKey()) + const token = createJwt(pk) + + const res = await app.request('/api/auth/me', { + headers: { Authorization: `Bearer ${token}` }, + }) + expect(res.status).toBe(200) + const body = await res.json() as { exists: boolean } + expect(body.exists).toBe(false) + }) + + it('returns exists:true with the bot for a valid token owning a bot row', async () => { + const pk = getPublicKey(generateSecretKey()) + const name = `me${Date.now().toString(36).slice(-8)}` + const botId = await seedBot(pk, { name }) + const token = createJwt(pk, botId) + + const res = await app.request('/api/auth/me', { + headers: { Authorization: `Bearer ${token}` }, + }) + expect(res.status).toBe(200) + const body = await res.json() as { exists: boolean; bot: { id: string; name: string; isHuman: boolean; hasWallet: boolean } } + expect(body.exists).toBe(true) + expect(body.bot.id).toBe(botId) + expect(body.bot.name).toBe(name) + // Same key set as POST /login's 200 body + expect(body.bot.isHuman).toBe(false) + expect(body.bot.hasWallet).toBe(false) + }) + + it('performs no writes: querying /me for an unregistered creator pubkey does not create a row', async () => { + // A non-creator pubkey with a valid token and no bot row must stay exists:false + // with zero side effects (GET /me never inserts/updates). + const pk = getPublicKey(generateSecretKey()) + const token = createJwt(pk) + + const before = await db.select({ id: schema.bots.id }).from(schema.bots) + await app.request('/api/auth/me', { headers: { Authorization: `Bearer ${token}` } }) + const after = await db.select({ id: schema.bots.id }).from(schema.bots) + + expect(after.length).toBe(before.length) + }) +})