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