jest.mock('@/lib/prisma', () => ({ prisma: { systemConfig: { findUnique: jest.fn(), upsert: jest.fn(), }, }, })); jest.mock('../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('../license/license-service', () => ({ getLocalLicense: jest.fn(), isLicenseConfigExpired: jest.fn(), })); jest.mock('../license/crypto-service', () => ({ encryptLocal: jest.fn((data) => `encrypted:${data}`), decryptLocal: jest.fn((data) => data.replace('encrypted:', '')), hashData: jest.fn((data) => (data === 'module-package' ? 'hash' : 'unknown-hash')), verifySignature: jest.fn(), })); jest.mock('../license/request-signature', () => ({ buildSignedLicenseRequest: jest.fn((action, payload) => ({ ...payload, timestamp: 1779753600000, nonce: 'nonce', signature: `signature:${action}`, })), })); jest.mock('../license/server-config', () => ({ buildLicenseServerLicenseApiUrl: jest.fn((action) => `https://license.test/api/license/flnxi/${action}`), getLicenseServerUrl: jest.fn(() => 'https://license.test'), getLicenseProjectCode: jest.fn(() => 'flnxi'), })); import { prisma } from '@/lib/prisma'; import { verifySignature } from '../license/crypto-service'; import { getLocalLicense, isLicenseConfigExpired } from '../license/license-service'; const mockFetch = jest.fn(); global.fetch = mockFetch; const mockVerifySignature = verifySignature as jest.MockedFunction; const mockGetLocalLicense = getLocalLicense as jest.MockedFunction; const mockIsLicenseConfigExpired = isLicenseConfigExpired as jest.MockedFunction; function mockPaidLicense(type: 'STANDARD' | 'PRO' = 'PRO') { mockGetLocalLicense.mockResolvedValue({ instanceId: 'instance-1', licenseKey: `FLNXI-${type}-TEST`, licenseData: 'encrypted-license', lastVerified: new Date().toISOString(), cachedConfig: { type, limits: { maxUsers: -1, maxMaterials: -1 }, features: { customTemplates: true, systemSettings: true, departmentIsolation: true, apiAccess: true, intranetPenetration: type === 'PRO', }, expiresAt: null, leaseExpiresAt: new Date(Date.now() + 60_000).toISOString(), }, } as any); mockIsLicenseConfigExpired.mockReturnValue(false); } describe('premium-module-service', () => { beforeEach(() => { jest.clearAllMocks(); mockFetch.mockReset(); mockVerifySignature.mockReturnValue(true); }); it('免费版同步付费模块时应跳过,不访问验证服务器', async () => { mockGetLocalLicense.mockResolvedValue({ cachedConfig: { type: 'FREE', limits: { maxUsers: 10, maxMaterials: 100 }, features: { customTemplates: false, systemSettings: false, departmentIsolation: false, apiAccess: false, intranetPenetration: false, }, expiresAt: null, leaseExpiresAt: new Date(Date.now() + 60_000).toISOString(), }, } as any); mockIsLicenseConfigExpired.mockReturnValue(false); const { syncPremiumModules } = await import('../license/premium-module-service'); const result = await syncPremiumModules(); expect(result).toMatchObject({ synced: false, skipped: true }); expect(mockFetch).not.toHaveBeenCalled(); }); it('付费版应拉取、验签并加密缓存付费模块清单', async () => { mockPaidLicense('PRO'); mockFetch .mockResolvedValueOnce({ ok: true, json: async () => ({ success: true, manifest: { version: 1, licenseType: 'PRO', modules: [{ id: 'intranet-penetration-core', minLicenseType: 'PRO', sizeBytes: 14, sha256: 'hash' }], issuedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 60_000).toISOString(), }, signature: 'valid-signature', }), }) .mockResolvedValueOnce({ ok: true, json: async () => ({ success: true, package: { moduleId: 'intranet-penetration-core', contentBase64: 'module-package', sha256: 'hash', sizeBytes: 14, expiresAt: new Date(Date.now() + 60_000).toISOString(), }, }), }); const { syncPremiumModules } = await import('../license/premium-module-service'); const result = await syncPremiumModules(); expect(result).toEqual({ synced: true }); expect(mockFetch).toHaveBeenNthCalledWith( 1, 'https://license.test/api/license/flnxi/premium-modules', expect.objectContaining({ method: 'POST' }) ); expect(mockFetch).toHaveBeenNthCalledWith( 2, 'https://license.test/api/license/flnxi/premium-modules/intranet-penetration-core', expect.objectContaining({ method: 'POST' }) ); expect(prisma.systemConfig.upsert).toHaveBeenCalledTimes(2); }); it('付费模块清单同步网络异常时应返回失败结果而不是抛出', async () => { mockPaidLicense('PRO'); mockFetch.mockRejectedValue(new Error('network down')); const { syncPremiumModules } = await import('../license/premium-module-service'); const result = await syncPremiumModules(); expect(result).toEqual({ synced: false, error: 'network down' }); expect(prisma.systemConfig.upsert).not.toHaveBeenCalled(); }); it('缓存清单签名无效时不应视为可用模块', async () => { mockPaidLicense('PRO'); mockVerifySignature.mockReturnValue(false); (prisma.systemConfig.findUnique as jest.Mock).mockResolvedValue({ value: `encrypted:${JSON.stringify({ manifest: { version: 1, licenseType: 'PRO', modules: [{ id: 'intranet-penetration-core', minLicenseType: 'PRO', sizeBytes: 4096, sha256: 'hash' }], issuedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 60_000).toISOString(), }, signature: 'bad-signature', syncedAt: new Date().toISOString(), })}`, }); const { hasPremiumModule } = await import('../license/premium-module-service'); await expect(hasPremiumModule('intranet-penetration-core')).resolves.toBe(false); }); it('只有清单但缺少本地模块包时不应视为可用模块', async () => { mockPaidLicense('PRO'); (prisma.systemConfig.findUnique as jest.Mock).mockImplementation(({ where }) => { if (where.key === 'premium_module_manifest') { return Promise.resolve({ value: `encrypted:${JSON.stringify({ manifest: { version: 1, licenseType: 'PRO', modules: [{ id: 'intranet-penetration-core', minLicenseType: 'PRO', sizeBytes: 14, sha256: 'hash' }], issuedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 60_000).toISOString(), }, signature: 'valid-signature', syncedAt: new Date().toISOString(), })}`, }); } return Promise.resolve(null); }); const { hasPremiumModule } = await import('../license/premium-module-service'); await expect(hasPremiumModule('intranet-penetration-core')).resolves.toBe(false); }); it('清单和本地模块包都有效时应视为可用模块', async () => { mockPaidLicense('PRO'); (prisma.systemConfig.findUnique as jest.Mock).mockImplementation(({ where }) => { if (where.key === 'premium_module_manifest') { return Promise.resolve({ value: `encrypted:${JSON.stringify({ manifest: { version: 1, licenseType: 'PRO', modules: [{ id: 'intranet-penetration-core', minLicenseType: 'PRO', sizeBytes: 14, sha256: 'hash' }], issuedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 60_000).toISOString(), }, signature: 'valid-signature', syncedAt: new Date().toISOString(), })}`, }); } if (where.key === 'premium_module_packages') { return Promise.resolve({ value: `encrypted:${JSON.stringify({ packages: { 'intranet-penetration-core': { contentBase64: 'module-package', sha256: 'hash', sizeBytes: 14, expiresAt: new Date(Date.now() + 60_000).toISOString(), syncedAt: new Date().toISOString(), }, }, })}`, }); } return Promise.resolve(null); }); const { hasPremiumModule } = await import('../license/premium-module-service'); await expect(hasPremiumModule('intranet-penetration-core')).resolves.toBe(true); }); it('缓存清单许可证类型和当前许可证不一致时不应视为可用模块', async () => { mockPaidLicense('STANDARD'); (prisma.systemConfig.findUnique as jest.Mock).mockResolvedValue({ value: `encrypted:${JSON.stringify({ manifest: { version: 1, licenseType: 'PRO', modules: [{ id: 'intranet-penetration-core', minLicenseType: 'PRO', sizeBytes: 4096, sha256: 'hash' }], issuedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 60_000).toISOString(), }, signature: 'valid-signature', syncedAt: new Date().toISOString(), })}`, }); const { hasPremiumModule } = await import('../license/premium-module-service'); await expect(hasPremiumModule('intranet-penetration-core')).resolves.toBe(false); }); });