/** @jest-environment node */ import { EventEmitter } from 'node:events' import { lookup } from 'node:dns/promises' import { request as httpsRequest } from 'node:https' import { getServerSession } from 'next-auth' import { prisma } from '@/lib/prisma' import { isSuperAdmin } from '@/lib/department-permission' import { AiModelError, assertAiModelAdmin } from '@/lib/ai-model-access' import { decryptAiModelKey, encryptAiModelKey, isAiModelEncryptionConfigured } from '@/lib/ai-model-crypto' import { normalizeAiModelBaseUrl, requestAiModel, requestAiModelCapabilityCatalog, resolveAiModelPublicAddresses } from '@/lib/ai-model-http' import { GET } from '@/app/api/admin/ai-models/route' import { POST } from '@/app/api/admin/ai-models/providers/route' test('inactive account with an active status string cannot manage or test models', async () => { ;(isSuperAdmin as jest.Mock).mockResolvedValue(true) ;(prisma.user.findUnique as jest.Mock).mockResolvedValue({ isSuperAdmin: true, status: 'active', isActive: false, archivedAt: null }) await expect(assertAiModelAdmin('admin')).rejects.toMatchObject({ status: 403, code: 'account_inactive' }) }) jest.mock('node:dns/promises', () => ({ lookup: jest.fn() })) jest.mock('node:https', () => ({ request: jest.fn() })) jest.mock('next-auth', () => ({ getServerSession: jest.fn() })) jest.mock('@/lib/auth', () => ({ authOptions: {} })) jest.mock('@/lib/department-permission', () => ({ isSuperAdmin: jest.fn() })) jest.mock('@/lib/prisma', () => ({ prisma: { user: { findUnique: jest.fn() } } })) jest.mock('@/lib/ai-model-service', () => ({ getAiModelAdminState: jest.fn().mockResolvedValue({ providers: [] }), createAiModelProvider: jest.fn() })) const originalKey = process.env.API_KEY_ENCRYPTION_KEY beforeEach(() => { jest.clearAllMocks() process.env.API_KEY_ENCRYPTION_KEY = 'synthetic-ai-encryption-key-32-bytes-minimum' ;(lookup as jest.Mock).mockResolvedValue([{ address: '93.184.216.34', family: 4 }]) ;(getServerSession as jest.Mock).mockResolvedValue({ user: { id: 'admin', role: 'USER', isSuperAdmin: true } }) ;(isSuperAdmin as jest.Mock).mockResolvedValue(true) ;(prisma.user.findUnique as jest.Mock).mockResolvedValue({ isSuperAdmin: true, status: 'active', isActive: true, archivedAt: null }) }) afterAll(() => { if (originalKey === undefined) delete process.env.API_KEY_ENCRYPTION_KEY; else process.env.API_KEY_ENCRYPTION_KEY = originalKey }) test.each([ 'http://example.com/v1', 'https://user:password@example.com/v1', 'https://example.com:8443/v1', 'https://example.com/v1?token=hidden', 'https://example.com/v1#hidden', 'https://localhost/v1', 'https://127.0.0.1/v1', 'https://2130706433/v1', 'https://[::1]/v1', 'https://[::ffff:127.0.0.1]/v1', 'https://example.local/v1', 'https://example.internal/v1', ])('rejects unsafe provider URL %s', value => { expect(() => normalizeAiModelBaseUrl(value)).toThrow(AiModelError) }) test.each(['10.0.0.1', '169.254.169.254', '192.168.58.20', '100.64.0.1', '::ffff:7f00:1', 'fe80::1', '2001:db8::1'])('rejects mixed DNS including %s before any socket', async address => { const family = address.includes(':') ? 6 : 4 ;(lookup as jest.Mock).mockResolvedValue([{ address: '93.184.216.34', family: 4 }, { address, family }]) await expect(requestAiModel('https://models.example.com/v1', 'synthetic-provider-key', '/models')).rejects.toMatchObject({ code: 'unsafe_provider_url' }) expect(httpsRequest).not.toHaveBeenCalled() }) test('DNS is resolved once and the HTTPS lookup uses only validated addresses', async () => { let requestOptions: any ;(httpsRequest as jest.Mock).mockImplementation((_target, options, callback) => { requestOptions = options const request = new EventEmitter() as any request.end = () => queueMicrotask(() => { const response = new EventEmitter() as any; response.statusCode = 200; response.headers = {}; response.destroy = jest.fn() callback(response); response.emit('data', Buffer.from('{"data":[]}')); response.emit('end') }) request.destroy = jest.fn() return request }) const response = await requestAiModel('https://models.example.com', 'synthetic-provider-key', '/models') expect(await response.json()).toEqual({ data: [] }) expect(lookup).toHaveBeenCalledTimes(1) const resolved = jest.fn(); requestOptions.lookup('models.example.com', { all: false }, resolved) expect(resolved).toHaveBeenCalledWith(null, '93.184.216.34', 4) expect(requestOptions.agent).toBe(false) }) test('revocation during DNS wait runs the fresh guard and creates zero sockets', async () => { ;(lookup as jest.Mock).mockImplementation(async () => { ;(isSuperAdmin as jest.Mock).mockResolvedValue(false); return [{ address: '93.184.216.34', family: 4 }] }) await expect(requestAiModel('https://models.example.com', 'synthetic-provider-key', '/models', undefined, undefined, () => assertAiModelAdmin('admin').then(() => undefined))).rejects.toMatchObject({ status: 403 }) expect(httpsRequest).not.toHaveBeenCalled() }) test.each([204, 302, 200])('handles empty response / redirect / excessive body safely: HTTP %s', async status => { ;(httpsRequest as jest.Mock).mockImplementation((_target, _options, callback) => { const request = new EventEmitter() as any; request.destroy = jest.fn() request.end = () => queueMicrotask(() => { const response = new EventEmitter() as any; response.statusCode = status; response.headers = {}; response.destroy = jest.fn() callback(response) if (status === 200) response.emit('data', Buffer.alloc(2 * 1024 * 1024 + 1)) response.emit('end') }) return request }) const promise = requestAiModel('https://models.example.com/v1', 'synthetic-provider-key', '/models') if (status === 204) expect((await promise).status).toBe(204) else await expect(promise).rejects.toThrow(status === 302 ? '重定向' : 'exceeds limit') }) test('credential encryption is randomized, authenticated and requires a dedicated master key', () => { const first = encryptAiModelKey('synthetic-upstream-secret'); const second = encryptAiModelKey('synthetic-upstream-secret') expect(first.secretCiphertext).not.toBe(second.secretCiphertext) expect(JSON.stringify(first)).not.toContain('synthetic-upstream-secret') expect(decryptAiModelKey(first)).toBe('synthetic-upstream-secret') const tampered = { ...first, secretAuthTag: Buffer.alloc(16).toString('base64') } expect(() => decryptAiModelKey(tampered)).toThrow('无法解密') delete process.env.API_KEY_ENCRYPTION_KEY expect(isAiModelEncryptionConfigured()).toBe(false) expect(() => encryptAiModelKey('test')).toThrow('API_KEY_ENCRYPTION_KEY') }) test('the final user-state lookup rejects revocation even if the earlier role read allowed it', async () => { ;(prisma.user.findUnique as jest.Mock).mockResolvedValue({ isSuperAdmin: false, status: 'active', isActive: true, archivedAt: null }) await expect(assertAiModelAdmin('admin')).rejects.toMatchObject({ status: 403, code: 'forbidden' }) }) test.each([ { isSuperAdmin: true, status: 'suspended', archivedAt: null }, { isSuperAdmin: true, status: 'active', archivedAt: new Date() }, { isSuperAdmin: true, status: 'pending', archivedAt: null }, ])('rejects inactive super administrators before reading configuration: %o', async state => { ;(prisma.user.findUnique as jest.Mock).mockResolvedValue(state) expect((await GET()).status).toBe(403) expect(httpsRequest).not.toHaveBeenCalled() }) test('unauthenticated and ordinary-admin requests fail before parsing bodies or network access', async () => { ;(getServerSession as jest.Mock).mockResolvedValue(null) expect((await GET()).status).toBe(401) ;(getServerSession as jest.Mock).mockResolvedValue({ user: { id: 'admin', role: 'ADMIN', isSuperAdmin: true } }) ;(isSuperAdmin as jest.Mock).mockResolvedValue(false) const request = new Request('https://platform.test/api/admin/ai-models/providers', { method: 'POST', body: 'not-json' }) expect((await POST(request)).status).toBe(403) expect(httpsRequest).not.toHaveBeenCalled() }) test('public DNS resolver refuses an empty result', async () => { await expect(resolveAiModelPublicAddresses('https://models.example.com', async () => [])).rejects.toMatchObject({ code: 'unsafe_provider_url' }) }) test('public capability discovery has a fixed endpoint and carries no private Authorization header', async () => { let targetUrl = '', sentHeaders: any ;(httpsRequest as jest.Mock).mockImplementation((target, options, callback) => { targetUrl = String(target); sentHeaders = options.headers const request = new EventEmitter() as any; request.destroy = jest.fn() request.end = () => queueMicrotask(() => { const response = new EventEmitter() as any; response.statusCode = 200; response.headers = {}; response.destroy = jest.fn(); callback(response); response.emit('data', Buffer.from('{}')); response.emit('end') }) return request }) await requestAiModelCapabilityCatalog('https://api.vip98k.com/v1') expect(targetUrl).toBe('https://api.vip98k.com/api/v2/catalog/models') expect(sentHeaders).toEqual({}) await expect(requestAiModelCapabilityCatalog('https://other.example.com/v1')).rejects.toMatchObject({ code: 'capability_catalog_unavailable' }) expect(httpsRequest).toHaveBeenCalledTimes(1) }) test('public capability discovery rejects protected DNS and excessive response bodies', async () => { ;(lookup as jest.Mock).mockResolvedValueOnce([{ address: '192.168.1.1', family: 4 }]) await expect(requestAiModelCapabilityCatalog('https://api.vip98k.com/v1')).rejects.toMatchObject({ code: 'unsafe_provider_url' }) expect(httpsRequest).not.toHaveBeenCalled() ;(httpsRequest as jest.Mock).mockImplementation((_target, _options, callback) => { const request = new EventEmitter() as any; request.destroy = jest.fn() request.end = () => queueMicrotask(() => { const response = new EventEmitter() as any; response.statusCode = 200; response.headers = { 'content-length': String(8 * 1024 * 1024 + 1) }; response.destroy = jest.fn(); callback(response) }) return request }) await expect(requestAiModelCapabilityCatalog('https://api.vip98k.com/v1')).rejects.toThrow('exceeds limit') })