import { NextRequest } from 'next/server' import { POST } from '../route' import { verifyLicenseSignature } from '@/lib/license/crypto-service' import { saveLicenseLocally, initializeSystem, startHeartbeat } from '@/lib/license/license-service' import { syncPremiumModules } from '@/lib/license/premium-module-service' import { requireSuperAdminAfterInitialSetup } from '@/lib/license/license-route-guards' jest.mock('@/lib/license/fingerprint-service', () => ({ generateFingerprint: jest.fn().mockResolvedValue({ machineId: 'machine-1', hostname: 'nas-1', macAddress: 'AA:BB:CC:DD:EE:FF', instanceId: 'instance-1', }), })) jest.mock('@/lib/license/server-config', () => ({ buildLicenseServerLicenseApiUrl: jest.fn(() => 'https://license.example.com/api/license/flnxi/activate'), getLicenseProjectCode: jest.fn(() => 'flnxi-sucai'), })) jest.mock('@/lib/license/crypto-service', () => ({ verifyLicenseSignature: jest.fn(), })) jest.mock('@/lib/license/license-service', () => ({ saveLicenseLocally: jest.fn(), initializeSystem: jest.fn(), startHeartbeat: jest.fn(), })) jest.mock('@/lib/license/premium-module-service', () => ({ syncPremiumModules: jest.fn(), })) jest.mock('@/lib/license/license-route-guards', () => ({ requireSuperAdminAfterInitialSetup: jest.fn(), })) const mockVerifyLicenseSignature = verifyLicenseSignature as jest.MockedFunction const mockSaveLicenseLocally = saveLicenseLocally as jest.MockedFunction const mockInitializeSystem = initializeSystem as jest.MockedFunction const mockStartHeartbeat = startHeartbeat as jest.MockedFunction const mockSyncPremiumModules = syncPremiumModules as jest.MockedFunction const mockRequireSuperAdminAfterInitialSetup = requireSuperAdminAfterInitialSetup as jest.MockedFunction const signedLicense = { data: { projectCode: 'flnxi-sucai', licenseKey: 'ABCD-1234-EFGH-5678', instanceId: 'instance-1', fingerprintHash: 'a'.repeat(64), type: 'PRO', limits: { maxUsers: 100, maxMaterials: 10000 }, features: { customTemplates: true, systemSettings: true, departmentIsolation: true, apiAccess: true, intranetPenetration: true, }, expiresAt: null, issuedAt: '2026-05-24T00:00:00.000Z', }, signature: 'signed-license', issuedAt: '2026-05-24T00:00:00.000Z', } function createMockRequest(options: { url?: string headers?: Record body?: Record } = {}) { const url = options.url || 'http://localhost:3000/api/license/activate' const headers = new Headers(options.headers || {}) return new NextRequest(url, { method: 'POST', headers, body: JSON.stringify({ licenseKey: 'ABCD-1234-EFGH-5678', ...options.body, }), }) } function mockActivateSuccess(overrides: Record = {}) { ;(global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true, json: async () => ({ success: true, license: signedLicense, ...overrides, }), }) } describe('POST /api/license/activate', () => { beforeEach(() => { jest.clearAllMocks() global.fetch = jest.fn() process.env.LICENSE_PROJECT_CODE = 'flnxi-sucai' process.env.LICENSE_REQUEST_SIGNING_SECRET = 'a'.repeat(32) mockVerifyLicenseSignature.mockReturnValue(true) mockSyncPremiumModules.mockResolvedValue({ synced: true }) mockRequireSuperAdminAfterInitialSetup.mockResolvedValue({ allowed: true }) mockInitializeSystem.mockResolvedValue({ admin: { created: false, }, } as any) }) test('缺少激活码时应返回 400', async () => { const response = await POST(createMockRequest({ body: { licenseKey: '' } })) const data = await response.json() expect(response.status).toBe(400) expect(data.success).toBe(false) expect(data.error).toBe('请输入激活码') expect(global.fetch).not.toHaveBeenCalled() }) test('激活成功时保存签名许可证并设置 cookie', async () => { mockActivateSuccess() const response = await POST(createMockRequest()) const data = await response.json() const setCookieHeader = response.headers.get('set-cookie') expect(response.status).toBe(200) expect(data.success).toBe(true) expect(mockSaveLicenseLocally).toHaveBeenCalledWith('ABCD-1234-EFGH-5678', signedLicense) expect(mockVerifyLicenseSignature).toHaveBeenCalledWith(signedLicense.data, signedLicense.signature) expect(mockSyncPremiumModules).toHaveBeenCalled() expect(mockStartHeartbeat).toHaveBeenCalled() expect(setCookieHeader).toContain('license_activated=true') expect(setCookieHeader).toContain('HttpOnly') expect(setCookieHeader).toContain('Max-Age=31536000') const fetchBody = JSON.parse((global.fetch as jest.Mock).mock.calls[0][1].body) expect(fetchBody.timestamp).toEqual(expect.any(Number)) expect(fetchBody.nonce).toMatch(/^[0-9a-f]{32}$/) expect(fetchBody.signature).toMatch(/^[0-9a-f]{64}$/) expect(fetchBody.licenseKey).toBe('ABCD-1234-EFGH-5678') }) test('已有最高权限账号后,未授权请求不能改写本地许可证', async () => { mockRequireSuperAdminAfterInitialSetup.mockResolvedValueOnce({ allowed: false, status: 403, error: '系统已初始化,许可证变更需要最高权限账号登录', }) const response = await POST(createMockRequest()) const data = await response.json() expect(response.status).toBe(403) expect(data.success).toBe(false) expect(data.error).toBe('系统已初始化,许可证变更需要最高权限账号登录') expect(global.fetch).not.toHaveBeenCalled() expect(mockSaveLicenseLocally).not.toHaveBeenCalled() expect(mockInitializeSystem).not.toHaveBeenCalled() expect(mockStartHeartbeat).not.toHaveBeenCalled() }) test('环境变量来源的初始管理员密码不应在激活响应中回显', async () => { mockActivateSuccess() mockInitializeSystem.mockResolvedValueOnce({ admin: { created: true, email: 'admin@local.flnxi', username: 'admin', initialPassword: 'Env-Password-Should-Not-Return', mustChangePassword: true, passwordSource: 'environment', }, } as any) const response = await POST(createMockRequest()) const data = await response.json() expect(response.status).toBe(200) expect(data.initialAdmin).toEqual({ email: 'admin@local.flnxi', username: 'admin', password: null, mustChangePassword: true, passwordSource: 'environment', }) }) test('系统生成的一次性初始管理员密码仍只在首次激活响应中返回', async () => { mockActivateSuccess() mockInitializeSystem.mockResolvedValueOnce({ admin: { created: true, email: 'admin@local.flnxi', username: 'admin', initialPassword: 'Generated-Password-123', mustChangePassword: true, passwordSource: 'generated', }, } as any) const response = await POST(createMockRequest()) const data = await response.json() expect(response.status).toBe(200) expect(data.initialAdmin.password).toBe('Generated-Password-123') expect(data.initialAdmin.passwordSource).toBe('generated') }) test('许可证服务器拒绝或签名验证失败时不保存本地许可证', async () => { mockActivateSuccess({ success: false, error: '许可证无效', license: undefined, }) const rejectedResponse = await POST(createMockRequest()) const rejectedData = await rejectedResponse.json() expect(rejectedResponse.status).toBe(400) expect(rejectedData.error).toBe('许可证无效') expect(mockSaveLicenseLocally).not.toHaveBeenCalled() expect(mockSyncPremiumModules).not.toHaveBeenCalled() mockActivateSuccess() mockVerifyLicenseSignature.mockReturnValueOnce(false) const invalidSignatureResponse = await POST(createMockRequest()) const invalidSignatureData = await invalidSignatureResponse.json() expect(invalidSignatureResponse.status).toBe(400) expect(invalidSignatureData.error).toBe('许可证签名验证失败') expect(mockSaveLicenseLocally).not.toHaveBeenCalled() expect(mockSyncPremiumModules).not.toHaveBeenCalled() }) test('许可证绑定信息不匹配时不保存本地许可证', async () => { mockActivateSuccess({ license: { ...signedLicense, data: { ...signedLicense.data, instanceId: 'other-instance-id', }, }, }) const response = await POST(createMockRequest()) const data = await response.json() expect(response.status).toBe(400) expect(data.error).toContain('不属于当前设备') expect(mockSaveLicenseLocally).not.toHaveBeenCalled() expect(mockSyncPremiumModules).not.toHaveBeenCalled() }) test('授权模块清单同步失败不应阻断许可证激活', async () => { const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined) mockActivateSuccess() mockSyncPremiumModules.mockResolvedValueOnce({ synced: false, error: 'network down' }) const response = await POST(createMockRequest()) const data = await response.json() expect(response.status).toBe(200) expect(data.success).toBe(true) expect(mockSaveLicenseLocally).toHaveBeenCalledWith('ABCD-1234-EFGH-5678', signedLicense) expect(consoleWarnSpy).toHaveBeenCalledWith('[License] 授权模块清单同步失败:', 'network down') consoleWarnSpy.mockRestore() }) test('HTTPS 或代理 HTTPS 时 cookie 带 Secure', async () => { mockActivateSuccess() const httpsResponse = await POST(createMockRequest({ url: 'https://example.com/api/license/activate', })) expect(httpsResponse.headers.get('set-cookie')).toContain('Secure') mockActivateSuccess() const forwardedHttpsResponse = await POST(createMockRequest({ url: 'http://localhost:3000/api/license/activate', headers: { 'x-forwarded-proto': 'https' }, })) expect(forwardedHttpsResponse.headers.get('set-cookie')).toContain('Secure') }) })