99 lines
2.9 KiB
TypeScript
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...')
|
|
})
|
|
})
|