/** @jest-environment node */ import { getServerSession } from 'next-auth' import { isSuperAdmin } from '@/lib/department-permission' import { ApiKeyNotRecoverableError, createApiKey, getApiKeys, getApiKeyPreview, revealApiKey, } from '@/lib/external-api' import { prisma } from '@/lib/prisma' import { GET as getApiKeysRoute, POST as createApiKeyRoute } from '@/app/api/admin/api-keys/route' import { PUT as updateApiKeyRoute, DELETE as deleteApiKeyRoute } from '@/app/api/admin/api-keys/[id]/route' import { POST as revealApiKeyRoute } from '@/app/api/admin/api-keys/[id]/reveal/route' import { enableApiKey, disableApiKey } from '@/lib/external-api' jest.mock('next-auth', () => ({ getServerSession: jest.fn(), })) jest.mock('@/lib/auth', () => ({ authOptions: {}, })) jest.mock('@/lib/department-permission', () => ({ isSuperAdmin: jest.fn(), })) jest.mock('@/lib/external-api', () => ({ ApiKeyEncryptionConfigError: class ApiKeyEncryptionConfigError extends Error {}, ApiKeyNotRecoverableError: class ApiKeyNotRecoverableError extends Error {}, getApiKeys: jest.fn(), createApiKey: jest.fn(), getApiKeyPreview: jest.fn((key: string) => `${String(key).slice(0, 8)}...`), enableApiKey: jest.fn(), disableApiKey: jest.fn(), revealApiKey: jest.fn(), })) jest.mock('@/lib/prisma', () => ({ prisma: { apiKey: { findUnique: jest.fn(), delete: jest.fn(), }, }, })) describe('admin api-keys routes', () => { beforeEach(() => { jest.clearAllMocks() ;(getServerSession as jest.Mock).mockResolvedValue({ user: { id: 'admin-1', role: 'SYSTEM_ADMIN', isSuperAdmin: false, }, }) ;(isSuperAdmin as jest.Mock).mockResolvedValue(true) }) test('GET 会继续返回 masked key 预览', async () => { ;(getApiKeys as jest.Mock).mockResolvedValue([ { id: 'key-1', key: 'abcdefgh12345678', name: '任务系统', isActive: true, canReveal: true }, ]) const response = await getApiKeysRoute(new Request('http://localhost/api/admin/api-keys') as any) const body = await response.json() expect(response.status).toBe(200) expect(body.data[0].key).toBe('abcdefgh...') expect(body.data[0].canReveal).toBe(true) expect(getApiKeyPreview).toHaveBeenCalledWith('abcdefgh12345678') }) test('非真正超管时 GET/POST/PUT/DELETE/reveal 都返回 403', async () => { ;(isSuperAdmin as jest.Mock).mockResolvedValue(false) const getResponse = await getApiKeysRoute(new Request('http://localhost/api/admin/api-keys') as any) const postResponse = await createApiKeyRoute({ json: jest.fn().mockResolvedValue({ name: '任务系统' }) } as any) const putResponse = await updateApiKeyRoute( { json: jest.fn().mockResolvedValue({ isActive: true }) } as any, { params: { id: 'key-1' } } as any, ) const deleteResponse = await deleteApiKeyRoute( new Request('http://localhost/api/admin/api-keys/key-1') as any, { params: { id: 'key-1' } } as any, ) const revealResponse = await revealApiKeyRoute( new Request('http://localhost/api/admin/api-keys/key-1/reveal', { method: 'POST' }) as any, { params: { id: 'key-1' } } as any, ) expect(getResponse.status).toBe(403) expect(postResponse.status).toBe(403) expect(putResponse.status).toBe(403) expect(deleteResponse.status).toBe(403) expect(revealResponse.status).toBe(403) expect(createApiKey).not.toHaveBeenCalled() expect(enableApiKey).not.toHaveBeenCalled() expect(disableApiKey).not.toHaveBeenCalled() expect(prisma.apiKey.delete).not.toHaveBeenCalled() expect(revealApiKey).not.toHaveBeenCalled() }) test('超管可创建并管理 API Key', async () => { ;(createApiKey as jest.Mock).mockResolvedValue({ id: 'key-1', key: 'plain-key' }) ;(prisma.apiKey.findUnique as jest.Mock).mockResolvedValue({ id: 'key-1' }) ;(prisma.apiKey.delete as jest.Mock).mockResolvedValue({ id: 'key-1' }) const postResponse = await createApiKeyRoute({ json: jest.fn().mockResolvedValue({ name: '任务系统' }) } as any) const putResponse = await updateApiKeyRoute( { json: jest.fn().mockResolvedValue({ isActive: false }) } as any, { params: { id: 'key-1' } } as any, ) const deleteResponse = await deleteApiKeyRoute( new Request('http://localhost/api/admin/api-keys/key-1') as any, { params: { id: 'key-1' } } as any, ) expect(postResponse.status).toBe(200) expect((await postResponse.json()).data.key).toBe('plain-key') expect(postResponse.headers.get('cache-control')).toContain('no-store') expect(putResponse.status).toBe(200) expect(disableApiKey).toHaveBeenCalledWith('key-1') expect(deleteResponse.status).toBe(200) expect(prisma.apiKey.delete).toHaveBeenCalledWith({ where: { id: 'key-1' } }) }) test('超管可按需查看完整 Key,响应禁止缓存', async () => { ;(revealApiKey as jest.Mock).mockResolvedValue('plain-key') const response = await revealApiKeyRoute( new Request('http://localhost/api/admin/api-keys/key-1/reveal', { method: 'POST' }) as any, { params: { id: 'key-1' } } as any, ) const body = await response.json() expect(response.status).toBe(200) expect(body.data.key).toBe('plain-key') expect(response.headers.get('cache-control')).toContain('no-store') expect(revealApiKey).toHaveBeenCalledWith('key-1', 'admin-1') }) test('历史纯哈希 Key 返回可操作的 409 提示', async () => { ;(revealApiKey as jest.Mock).mockRejectedValue(new ApiKeyNotRecoverableError()) const response = await revealApiKeyRoute( new Request('http://localhost/api/admin/api-keys/key-old/reveal', { method: 'POST' }) as any, { params: { id: 'key-old' } } as any, ) const body = await response.json() expect(response.status).toBe(409) expect(body.code).toBe('API_KEY_NOT_RECOVERABLE') }) })