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/register-free'), 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 signedFreeLicense = { data: { projectCode: 'flnxi-sucai', licenseKey: 'FLNXI-FREE-TEST-1234-5678-ABCD', instanceId: 'instance-1', fingerprintHash: 'a'.repeat(64), type: 'FREE', limits: { maxUsers: 10, maxMaterials: 100 }, features: { customTemplates: false, systemSettings: false, departmentIsolation: false, apiAccess: false, intranetPenetration: false, }, expiresAt: null, issuedAt: '2026-05-24T00:00:00.000Z', }, signature: 'signed-free-license', issuedAt: '2026-05-24T00:00:00.000Z', }; function createMockRequest(options: { url?: string; headers?: Record; body?: Record; } = {}): NextRequest { const url = options.url || 'http://localhost:3000/api/license/activate-free'; const headers = new Headers(options.headers || {}); return new NextRequest(url, { method: 'POST', headers, body: JSON.stringify({ customerName: '测试公司', contactInfo: '13800000000', ...options.body, }), }); } function mockRegisterFreeSuccess(overrides: Record = {}) { (global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true, json: async () => ({ success: true, license: signedFreeLicense, data: { activationCode: 'FLNXI-FREE-TEST-1234-5678-ABCD', isExisting: false, }, ...overrides, }), }); } describe('POST /api/license/activate-free', () => { 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); mockRequireSuperAdminAfterInitialSetup.mockResolvedValue({ allowed: true }); mockSyncPremiumModules.mockResolvedValue({ synced: true }); mockInitializeSystem.mockResolvedValue({ admin: { created: false, }, } as any); }); it('保存许可证服务端返回的签名免费许可证,不再写空签名', async () => { mockRegisterFreeSuccess(); const response = await POST(createMockRequest()); const data = await response.json(); expect(response.status).toBe(200); expect(data.success).toBe(true); expect(mockSaveLicenseLocally).toHaveBeenCalledWith( 'FLNXI-FREE-TEST-1234-5678-ABCD', signedFreeLicense ); expect(mockSaveLicenseLocally.mock.calls[0][1].signature).toBe('signed-free-license'); expect(mockVerifyLicenseSignature).toHaveBeenCalledWith( signedFreeLicense.data, signedFreeLicense.signature ); expect(mockSyncPremiumModules).toHaveBeenCalled(); expect(mockStartHeartbeat).toHaveBeenCalled(); 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.customerName).toBe('测试公司'); expect(fetchBody.contactInfo).toBe('13800000000'); }); it('免费版授权模块同步失败不应阻断首次激活', async () => { const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); mockRegisterFreeSuccess(); 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).toHaveBeenCalled(); expect(mockInitializeSystem).toHaveBeenCalled(); expect(mockStartHeartbeat).toHaveBeenCalled(); expect(consoleWarnSpy).toHaveBeenCalledWith('[License] 授权模块清单同步失败:', 'network down'); consoleWarnSpy.mockRestore(); }); it('已有最高权限账号后,未授权请求不能改写免费许可证', 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(mockSyncPremiumModules).not.toHaveBeenCalled(); expect(mockInitializeSystem).not.toHaveBeenCalled(); expect(mockStartHeartbeat).not.toHaveBeenCalled(); }); it('许可证服务没有返回签名许可证时拒绝激活,避免保存不可读取的本地许可证', async () => { mockRegisterFreeSuccess({ license: undefined }); const response = await POST(createMockRequest()); const data = await response.json(); expect(response.status).toBe(502); expect(data.success).toBe(false); expect(data.error).toContain('许可证服务返回数据异常'); expect(mockSaveLicenseLocally).not.toHaveBeenCalled(); expect(mockSyncPremiumModules).not.toHaveBeenCalled(); expect(mockInitializeSystem).not.toHaveBeenCalled(); expect(mockStartHeartbeat).not.toHaveBeenCalled(); }); it('许可证签名验证失败时拒绝激活', async () => { mockRegisterFreeSuccess(); mockVerifyLicenseSignature.mockReturnValue(false); const response = await POST(createMockRequest()); const data = await response.json(); expect(response.status).toBe(400); expect(data.success).toBe(false); expect(data.error).toBe('许可证签名验证失败'); expect(mockSaveLicenseLocally).not.toHaveBeenCalled(); expect(mockSyncPremiumModules).not.toHaveBeenCalled(); expect(mockInitializeSystem).not.toHaveBeenCalled(); expect(mockStartHeartbeat).not.toHaveBeenCalled(); }); it('许可证绑定信息不匹配时拒绝激活', async () => { mockRegisterFreeSuccess({ license: { ...signedFreeLicense, data: { ...signedFreeLicense.data, licenseKey: 'FLNXI-FREE-OTHER-1234-5678-ABCD', }, }, }); 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(); expect(mockInitializeSystem).not.toHaveBeenCalled(); expect(mockStartHeartbeat).not.toHaveBeenCalled(); }); it('应该设置 license_activated cookie', async () => { mockRegisterFreeSuccess(); const response = await POST(createMockRequest()); const setCookieHeader = response.headers.get('set-cookie'); expect(setCookieHeader).toContain('license_activated=true'); expect(setCookieHeader).toContain('HttpOnly'); expect(setCookieHeader).toContain('Path=/'); expect(setCookieHeader).toContain('Max-Age=31536000'); expect(setCookieHeader?.toLowerCase()).toContain('samesite=lax'); }); it('初始管理员密码来自环境变量时不应在激活响应中回显明文', async () => { mockRegisterFreeSuccess(); 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', }); }); it('系统生成的一次性初始管理员密码仍只在首次激活响应中返回', async () => { mockRegisterFreeSuccess(); 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'); }); it('免费版激活日志不应输出完整激活码', async () => { const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined); mockRegisterFreeSuccess(); const response = await POST(createMockRequest()); const data = await response.json(); const logOutput = JSON.stringify(consoleLogSpy.mock.calls); expect(response.status).toBe(200); expect(data.success).toBe(true); expect(logOutput).toContain('FLNX****ABCD'); expect(logOutput).not.toContain('FLNXI-FREE-TEST-1234-5678-ABCD'); consoleLogSpy.mockRestore(); }); it('HTTP 请求时 cookie 不带 Secure,HTTPS 或代理 HTTPS 时带 Secure', async () => { mockRegisterFreeSuccess(); const httpResponse = await POST(createMockRequest({ url: 'http://localhost:3000/api/license/activate-free', })); expect(httpResponse.headers.get('set-cookie')).not.toContain('Secure'); mockRegisterFreeSuccess(); const httpsResponse = await POST(createMockRequest({ url: 'https://example.com/api/license/activate-free', })); expect(httpsResponse.headers.get('set-cookie')).toContain('Secure'); mockRegisterFreeSuccess(); const forwardedHttpsResponse = await POST(createMockRequest({ url: 'http://localhost:3000/api/license/activate-free', headers: { 'x-forwarded-proto': 'https' }, })); expect(forwardedHttpsResponse.headers.get('set-cookie')).toContain('Secure'); }); it('许可证服务器返回失败时应该返回 400', async () => { (global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true, json: async () => ({ success: false, error: '试用已过期', }), }); const response = await POST(createMockRequest()); const data = await response.json(); expect(response.status).toBe(400); expect(data.success).toBe(false); expect(data.error).toBe('试用已过期'); expect(mockSaveLicenseLocally).not.toHaveBeenCalled(); }); it('许可证服务器网络失败时应该返回 400', async () => { (global.fetch as jest.Mock).mockResolvedValueOnce({ ok: false, status: 503, statusText: 'Service Unavailable', json: async () => ({ error: '许可证服务不可用' }), }); const response = await POST(createMockRequest()); const data = await response.json(); expect(response.status).toBe(400); expect(data.success).toBe(false); expect(data.error).toBe('许可证服务不可用'); expect(mockSaveLicenseLocally).not.toHaveBeenCalled(); }); it('缺少客户名称或联系方式时返回 400', async () => { const missingNameResponse = await POST(createMockRequest({ body: { customerName: '' } })); expect(missingNameResponse.status).toBe(400); const missingContactResponse = await POST(createMockRequest({ body: { contactInfo: '' } })); expect(missingContactResponse.status).toBe(400); expect(global.fetch).not.toHaveBeenCalled(); expect(mockSaveLicenseLocally).not.toHaveBeenCalled(); }); });