160 lines
5.8 KiB
TypeScript
160 lines
5.8 KiB
TypeScript
/** @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')
|
||
})
|
||
})
|