594 lines
19 KiB
TypeScript
594 lines
19 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-entitlement' ? '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>;
|
|
|
|
const moduleArtifact = {
|
|
type: 'registry-image' as const,
|
|
image: 'registry.test/flnxi/premium/intranet-penetration-core:1.0.0',
|
|
digest: null,
|
|
version: '1.0.0',
|
|
};
|
|
|
|
const limitsModuleArtifact = {
|
|
type: 'registry-image' as const,
|
|
image: 'registry.test/flnxi/premium/license-limits-core:1.0.0',
|
|
digest: null,
|
|
version: '1.0.0',
|
|
};
|
|
|
|
function mockPullCredential(moduleId = 'intranet-penetration-core') {
|
|
return {
|
|
registryHost: 'registry.test',
|
|
repository: `flnxi/premium/${moduleId}`,
|
|
tag: '1.0.0',
|
|
digest: null,
|
|
username: 'flnxi-module',
|
|
pullToken: `pull-token:${moduleId}`,
|
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
};
|
|
}
|
|
|
|
function mockManifestModule(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
id: 'intranet-penetration-core',
|
|
minLicenseType: 'PRO',
|
|
artifact: moduleArtifact,
|
|
sizeBytes: 18,
|
|
sha256: 'hash',
|
|
entitlementVersion: 1,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function mockLimitsManifestModule(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
id: 'license-limits-core',
|
|
minLicenseType: 'FREE',
|
|
artifact: limitsModuleArtifact,
|
|
sizeBytes: 18,
|
|
sha256: 'hash',
|
|
entitlementVersion: 1,
|
|
limits: { maxUsers: 10, maxMaterials: 100 },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function mockFreeLicense() {
|
|
mockGetLocalLicense.mockResolvedValue({
|
|
instanceId: 'instance-1',
|
|
licenseKey: 'FLNXI-FREE-TEST',
|
|
licenseData: 'encrypted-license',
|
|
lastVerified: new Date().toISOString(),
|
|
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);
|
|
}
|
|
|
|
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 () => {
|
|
mockFreeLicense();
|
|
const manifestExpiresAt = new Date(Date.now() + 60_000).toISOString();
|
|
mockFetch
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
success: true,
|
|
manifest: {
|
|
version: 1,
|
|
licenseType: 'FREE',
|
|
instanceId: 'instance-1',
|
|
modules: [mockLimitsManifestModule()],
|
|
issuedAt: new Date().toISOString(),
|
|
expiresAt: manifestExpiresAt,
|
|
},
|
|
signature: 'valid-signature',
|
|
}),
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
success: true,
|
|
entitlement: {
|
|
moduleId: 'license-limits-core',
|
|
entitlementBase64: 'module-entitlement',
|
|
sha256: 'hash',
|
|
sizeBytes: 18,
|
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
artifact: limitsModuleArtifact,
|
|
},
|
|
}),
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
success: true,
|
|
pullCredential: mockPullCredential('license-limits-core'),
|
|
}),
|
|
});
|
|
|
|
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/license-limits-core',
|
|
expect.objectContaining({ method: 'POST' })
|
|
);
|
|
expect(mockFetch).toHaveBeenNthCalledWith(
|
|
3,
|
|
'https://license.test/api/license/flnxi/premium-modules/license-limits-core/pull-token',
|
|
expect.objectContaining({ method: 'POST' })
|
|
);
|
|
expect(JSON.parse(mockFetch.mock.calls[1][1].body).manifestExpiresAt).toBe(manifestExpiresAt);
|
|
expect(prisma.systemConfig.upsert).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it('付费版应拉取、验签并加密缓存授权模块清单', async () => {
|
|
mockPaidLicense('PRO');
|
|
const manifestExpiresAt = new Date(Date.now() + 60_000).toISOString();
|
|
mockFetch
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
success: true,
|
|
manifest: {
|
|
version: 1,
|
|
licenseType: 'PRO',
|
|
instanceId: 'instance-1',
|
|
modules: [mockManifestModule()],
|
|
issuedAt: new Date().toISOString(),
|
|
expiresAt: manifestExpiresAt,
|
|
},
|
|
signature: 'valid-signature',
|
|
}),
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
success: true,
|
|
entitlement: {
|
|
moduleId: 'intranet-penetration-core',
|
|
entitlementBase64: 'module-entitlement',
|
|
sha256: 'hash',
|
|
sizeBytes: 18,
|
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
artifact: moduleArtifact,
|
|
},
|
|
}),
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: async () => ({
|
|
success: true,
|
|
pullCredential: mockPullCredential('intranet-penetration-core'),
|
|
}),
|
|
});
|
|
|
|
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(mockFetch).toHaveBeenNthCalledWith(
|
|
3,
|
|
'https://license.test/api/license/flnxi/premium-modules/intranet-penetration-core/pull-token',
|
|
expect.objectContaining({ method: 'POST' })
|
|
);
|
|
expect(JSON.parse(mockFetch.mock.calls[1][1].body).manifestExpiresAt).toBe(manifestExpiresAt);
|
|
expect(prisma.systemConfig.upsert).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
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',
|
|
instanceId: 'instance-1',
|
|
modules: [mockManifestModule({ sizeBytes: 4096 })],
|
|
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',
|
|
instanceId: 'instance-1',
|
|
modules: [mockManifestModule()],
|
|
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',
|
|
instanceId: 'instance-1',
|
|
modules: [mockManifestModule()],
|
|
issuedAt: new Date().toISOString(),
|
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
},
|
|
signature: 'valid-signature',
|
|
syncedAt: new Date().toISOString(),
|
|
})}`,
|
|
});
|
|
}
|
|
|
|
if (where.key === 'premium_module_entitlements') {
|
|
return Promise.resolve({
|
|
value: `encrypted:${JSON.stringify({
|
|
entitlements: {
|
|
'intranet-penetration-core': {
|
|
entitlementBase64: 'module-entitlement',
|
|
sha256: 'hash',
|
|
sizeBytes: 18,
|
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
artifact: moduleArtifact,
|
|
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',
|
|
instanceId: 'instance-1',
|
|
modules: [mockManifestModule({ sizeBytes: 4096 })],
|
|
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);
|
|
});
|
|
|
|
it('限额模块清单和授权凭证都有效时应返回签名限额', async () => {
|
|
mockFreeLicense();
|
|
(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: 'FREE',
|
|
instanceId: 'instance-1',
|
|
modules: [mockLimitsManifestModule()],
|
|
issuedAt: new Date().toISOString(),
|
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
},
|
|
signature: 'valid-signature',
|
|
syncedAt: new Date().toISOString(),
|
|
})}`,
|
|
});
|
|
}
|
|
|
|
if (where.key === 'premium_module_entitlements') {
|
|
return Promise.resolve({
|
|
value: `encrypted:${JSON.stringify({
|
|
entitlements: {
|
|
'license-limits-core': {
|
|
entitlementBase64: 'module-entitlement',
|
|
sha256: 'hash',
|
|
sizeBytes: 18,
|
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
artifact: limitsModuleArtifact,
|
|
syncedAt: new Date().toISOString(),
|
|
},
|
|
},
|
|
})}`,
|
|
});
|
|
}
|
|
|
|
return Promise.resolve(null);
|
|
});
|
|
|
|
const { getAuthorizedUsageLimits } = await import('../license/premium-module-service');
|
|
|
|
await expect(getAuthorizedUsageLimits()).resolves.toEqual({
|
|
maxUsers: 10,
|
|
maxMaterials: 100,
|
|
});
|
|
});
|
|
|
|
it('限额模块缺少有效授权凭证时不应返回限额', async () => {
|
|
mockFreeLicense();
|
|
(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: 'FREE',
|
|
instanceId: 'instance-1',
|
|
modules: [mockLimitsManifestModule()],
|
|
issuedAt: new Date().toISOString(),
|
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
},
|
|
signature: 'valid-signature',
|
|
syncedAt: new Date().toISOString(),
|
|
})}`,
|
|
});
|
|
}
|
|
|
|
return Promise.resolve(null);
|
|
});
|
|
|
|
const { getAuthorizedUsageLimits } = await import('../license/premium-module-service');
|
|
|
|
await expect(getAuthorizedUsageLimits()).resolves.toBeNull();
|
|
});
|
|
|
|
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',
|
|
instanceId: 'instance-1',
|
|
modules: [mockManifestModule()],
|
|
issuedAt: new Date().toISOString(),
|
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
},
|
|
signature: 'valid-signature',
|
|
syncedAt: new Date().toISOString(),
|
|
})}`,
|
|
});
|
|
}
|
|
|
|
if (where.key === 'premium_module_entitlements') {
|
|
return Promise.resolve({
|
|
value: `encrypted:${JSON.stringify({
|
|
entitlements: {
|
|
'intranet-penetration-core': {
|
|
entitlementBase64: 'module-entitlement',
|
|
sha256: 'hash',
|
|
sizeBytes: 18,
|
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
artifact: moduleArtifact,
|
|
pullCredential: mockPullCredential('intranet-penetration-core'),
|
|
syncedAt: new Date().toISOString(),
|
|
},
|
|
},
|
|
})}`,
|
|
});
|
|
}
|
|
|
|
return Promise.resolve(null);
|
|
});
|
|
|
|
const { getCachedModulePullCredential } = await import('../license/premium-module-service');
|
|
|
|
await expect(getCachedModulePullCredential('intranet-penetration-core')).resolves.toEqual(
|
|
expect.objectContaining({
|
|
username: 'flnxi-module',
|
|
repository: 'flnxi/premium/intranet-penetration-core',
|
|
})
|
|
);
|
|
});
|
|
|
|
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',
|
|
instanceId: 'instance-1',
|
|
modules: [mockManifestModule()],
|
|
issuedAt: new Date().toISOString(),
|
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
},
|
|
signature: 'valid-signature',
|
|
syncedAt: new Date().toISOString(),
|
|
})}`,
|
|
});
|
|
}
|
|
|
|
if (where.key === 'premium_module_entitlements') {
|
|
return Promise.resolve({
|
|
value: `encrypted:${JSON.stringify({
|
|
entitlements: {
|
|
'intranet-penetration-core': {
|
|
entitlementBase64: 'module-entitlement',
|
|
sha256: 'hash',
|
|
sizeBytes: 18,
|
|
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
artifact: moduleArtifact,
|
|
pullCredential: {
|
|
...mockPullCredential('intranet-penetration-core'),
|
|
expiresAt: new Date(Date.now() - 1_000).toISOString(),
|
|
},
|
|
syncedAt: new Date().toISOString(),
|
|
},
|
|
},
|
|
})}`,
|
|
});
|
|
}
|
|
|
|
return Promise.resolve(null);
|
|
});
|
|
|
|
const { getCachedModulePullCredential } = await import('../license/premium-module-service');
|
|
|
|
await expect(getCachedModulePullCredential('intranet-penetration-core')).resolves.toBeNull();
|
|
});
|
|
});
|