flnxi-Wu/lib/__tests__/premium-module-service.test.ts
2026-05-26 17:25:48 +08:00

284 lines
9.6 KiB
TypeScript

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<typeof verifySignature>;
const mockGetLocalLicense = getLocalLicense as jest.MockedFunction<typeof getLocalLicense>;
const mockIsLicenseConfigExpired = isLicenseConfigExpired as jest.MockedFunction<typeof isLicenseConfigExpired>;
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);
});
});