gongxiangp2/lib/__tests__/external-api-key.test.ts

99 lines
2.9 KiB
TypeScript

/** @jest-environment node */
jest.mock('@/lib/prisma', () => ({
prisma: {
apiKey: {
findUnique: jest.fn(),
update: jest.fn(),
create: jest.fn(),
},
},
}))
import { prisma } from '@/lib/prisma'
import {
createApiKey,
getApiKeyFromRequest,
getApiKeyPreview,
validateApiKey,
} from '@/lib/external-api'
const apiKeyDb = prisma.apiKey as unknown as {
findUnique: jest.Mock
update: jest.Mock
create: jest.Mock
}
function createRequest(url: string, headers?: Record<string, string>) {
return {
nextUrl: new URL(url),
headers: new Headers(headers),
} as any
}
describe('getApiKeyFromRequest', () => {
test('默认只读取 X-API-Key 请求头', () => {
const request = createRequest('https://example.com/api/external/demo?apiKey=query-key', {
'X-API-Key': 'header-key',
})
expect(getApiKeyFromRequest(request)).toBe('header-key')
})
test('默认不读取 URL 参数,避免所有外部接口无意开放 query key', () => {
const request = createRequest('https://example.com/api/external/demo?apiKey=query-key')
expect(getApiKeyFromRequest(request)).toBeNull()
})
test('显式允许时可读取钉钉固定 URL 参数 apiKey', () => {
const request = createRequest('https://example.com/api/external/demo?apiKey=query-key')
expect(getApiKeyFromRequest(request, { allowQuery: true })).toBe('query-key')
})
test('请求头优先于 URL 参数', () => {
const request = createRequest('https://example.com/api/external/demo?apiKey=query-key', {
'X-API-Key': 'header-key',
})
expect(getApiKeyFromRequest(request, { allowQuery: true })).toBe('header-key')
})
})
describe('API Key 哈希存储', () => {
beforeEach(() => jest.clearAllMocks())
test('新建 Key 只向数据库写入哈希,原始值仅返回一次', async () => {
apiKeyDb.create.mockImplementation(async ({ data }: any) => ({
id: 'key-1',
name: data.name,
key: data.key,
createdAt: new Date('2026-07-10T00:00:00Z'),
}))
const created = await createApiKey('产品平台', 'admin-1')
const storedKey = apiKeyDb.create.mock.calls[0][0].data.key
expect(created.key).toMatch(/^[a-f0-9]{32}$/)
expect(storedKey).not.toBe(created.key)
expect(storedKey).toMatch(/^v1:[a-f0-9]{8}:[a-f0-9]{64}$/)
})
test('已有明文 Key 首次验证成功后迁移成哈希', async () => {
apiKeyDb.findUnique
.mockResolvedValueOnce(null)
.mockResolvedValueOnce({ id: 'legacy-1', name: '旧 Key', isActive: true, createdBy: 'admin-1' })
apiKeyDb.update.mockResolvedValue({})
const result = await validateApiKey('legacy-api-key')
expect(result?.id).toBe('legacy-1')
expect(apiKeyDb.update.mock.calls[0][0].data.key).toMatch(/^v1:legacy-a:[a-f0-9]{64}$/)
})
test('后台只展示原始 Key 前缀', () => {
expect(getApiKeyPreview(`v1:12345678:${'a'.repeat(64)}`)).toBe('12345678...')
})
})