From 21bb46c1b038891f615f3fb7d051edc3bc1f4919 Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 13 Mar 2026 09:29:08 +0000 Subject: [PATCH] test: verify admin endpoints require creator pubkey on all routes Test all 7 admin endpoints (stats, bots, deactivate, activate, reset-elo, fights, backup) reject non-creator pubkeys and missing pubkeys with 403. Verifies global middleware guard works correctly. Co-Authored-By: Claude Opus 4.6 --- server/src/routes/admin.test.ts | 82 +++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 server/src/routes/admin.test.ts diff --git a/server/src/routes/admin.test.ts b/server/src/routes/admin.test.ts new file mode 100644 index 0000000..025d705 --- /dev/null +++ b/server/src/routes/admin.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect, vi } from 'vitest' +import { Hono } from 'hono' + +// Mock DB +vi.mock('../db/index.js', () => ({ + db: { + select: vi.fn().mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + limit: vi.fn().mockResolvedValue([]), + orderBy: vi.fn().mockResolvedValue([]), + }), + orderBy: vi.fn().mockReturnValue({ + limit: vi.fn().mockResolvedValue([]), + }), + }), + }), + update: vi.fn().mockReturnValue({ + set: vi.fn().mockReturnValue({ + where: vi.fn().mockResolvedValue(undefined), + }), + }), + }, + schema: { + bots: { id: 'id', name: 'name', eloRating: 'eloRating', wins: 'wins', losses: 'losses', tier: 'tier', isActive: 'isActive', archetype: 'archetype', botType: 'botType', consecutiveErrors: 'consecutiveErrors', lastFightAt: 'lastFightAt', createdAt: 'createdAt' }, + fights: { id: 'id', botAId: 'botAId', botBId: 'botBId', arena: 'arena', status: 'status', winnerId: 'winnerId', botAHp: 'botAHp', botBHp: 'botBHp', totalRounds: 'totalRounds', mode: 'mode', potSats: 'potSats', createdAt: 'createdAt' }, + }, + sqlite: { prepare: vi.fn().mockReturnValue({ get: vi.fn().mockReturnValue({ size: 1000 }) }) }, +})) + +vi.mock('./fights.js', () => ({ getActiveSSECount: () => 0 })) +vi.mock('../engine/backup.js', () => ({ createBackup: () => '/tmp/backup.db' })) + +const { adminRouter } = await import('./admin.js') + +function makeApp() { + const app = new Hono() + app.route('/api/admin', adminRouter) + return app +} + +const CREATOR_PK = 'da5e0c1b646bdb13c2300f805b0ca3e5afe5b052c594ce78bac8978d21c3fa39' +const FAKE_PK = 'a'.repeat(64) + +describe('admin routes — creator pubkey check', () => { + const endpoints = [ + { method: 'GET', path: '/api/admin/stats' }, + { method: 'GET', path: '/api/admin/bots' }, + { method: 'POST', path: '/api/admin/bots/test-id/deactivate' }, + { method: 'POST', path: '/api/admin/bots/test-id/activate' }, + { method: 'POST', path: '/api/admin/bots/test-id/reset-elo' }, + { method: 'GET', path: '/api/admin/fights' }, + { method: 'GET', path: '/api/admin/backup' }, + ] + + for (const { method, path } of endpoints) { + it(`${method} ${path} returns 403 for non-creator`, async () => { + const app = makeApp() + const res = await app.request(path, { + method, + headers: { 'x-pubkey': FAKE_PK }, + }) + expect(res.status).toBe(403) + const body = await res.json() as { error: string } + expect(body.error).toBe('Forbidden') + }) + + it(`${method} ${path} returns 403 with no pubkey`, async () => { + const app = makeApp() + const res = await app.request(path, { method }) + expect(res.status).toBe(403) + }) + } + + it('GET /stats returns 200 for creator pubkey', async () => { + const app = makeApp() + const res = await app.request('/api/admin/stats', { + headers: { 'x-pubkey': CREATOR_PK }, + }) + expect(res.status).toBe(200) + }) +})