gongxiangp2/app/api/admin/api-keys/route.test.ts

160 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** @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')
})
})