flnxi-Wu/lib/__tests__/premium-module-service.test.ts
2026-06-03 18:41:52 +08:00

1845 lines
60 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 paidCoreArtifact = {
type: 'registry-image' as const,
image: 'registry.test/flnxi/premium/paid-core:1.0.0',
digest: null,
version: '1.0.0',
};
function mockPullCredential(overrides: Partial<Record<string, unknown>> = {}) {
return {
registryHost: 'registry.test',
repository: 'flnxi/premium/paid-core',
tag: '1.0.0',
digest: null,
username: 'flnxi-module',
pullToken: 'pull-token:paid-core',
expiresAt: new Date(Date.now() + 60_000).toISOString(),
...overrides,
};
}
function mockPaidCoreManifestModule(overrides: Record<string, unknown> = {}) {
return {
id: 'paid-core',
minLicenseType: 'STANDARD',
artifact: paidCoreArtifact,
sizeBytes: 18,
sha256: 'hash',
entitlementVersion: 1,
...overrides,
};
}
function encryptedConfig(value: unknown) {
return `encrypted:${JSON.stringify(value)}`;
}
function mockLicense(type: 'FREE' | 'STANDARD' | 'PRO') {
mockGetLocalLicense.mockResolvedValue({
instanceId: 'instance-1',
licenseKey: `FLNXI-${type}-TEST`,
licenseData: 'encrypted-license',
lastVerified: new Date().toISOString(),
cachedConfig: {
type,
limits: type === 'FREE' ? { maxUsers: 10, maxMaterials: 100 } : { maxUsers: -1, maxMaterials: -1 },
features: {
customTemplates: type !== 'FREE',
systemSettings: type !== 'FREE',
departmentIsolation: type !== 'FREE',
apiAccess: type !== 'FREE',
intranetPenetration: type === 'PRO',
},
expiresAt: null,
leaseExpiresAt: new Date(Date.now() + 60_000).toISOString(),
},
} as any);
mockIsLicenseConfigExpired.mockReturnValue(false);
}
function mockCachedPaidCore(params: {
withEntitlement?: boolean;
runtimeStatus?: 'missing' | 'pulling' | 'running' | 'unhealthy' | 'expired' | 'revoked';
pullCredential?: ReturnType<typeof mockPullCredential> | null;
}) {
(prisma.systemConfig.findUnique as jest.Mock).mockImplementation(({ where }) => {
if (where.key === 'premium_module_manifest') {
return Promise.resolve({
value: encryptedConfig({
manifest: {
version: 1,
licenseType: 'PRO',
instanceId: 'instance-1',
modules: [mockPaidCoreManifestModule()],
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' && params.withEntitlement) {
return Promise.resolve({
value: encryptedConfig({
entitlements: {
'paid-core': {
entitlementBase64: 'module-entitlement',
sha256: 'hash',
sizeBytes: 18,
expiresAt: new Date(Date.now() + 60_000).toISOString(),
artifact: paidCoreArtifact,
pullCredential: params.pullCredential === undefined ? mockPullCredential() : params.pullCredential,
syncedAt: new Date().toISOString(),
},
},
}),
});
}
if (where.key === 'premium_module_runtime_status' && params.runtimeStatus) {
return Promise.resolve({
value: encryptedConfig({
'paid-core': {
moduleId: 'paid-core',
image: paidCoreArtifact.image,
digest: null,
status: params.runtimeStatus,
lastSyncAt: new Date().toISOString(),
},
}),
});
}
return Promise.resolve(null);
});
}
describe('premium-module-service', () => {
beforeEach(() => {
jest.clearAllMocks();
mockFetch.mockReset();
mockVerifySignature.mockReturnValue(true);
(prisma.systemConfig.findUnique as jest.Mock).mockResolvedValue(null);
delete process.env.PAID_CORE_MODULE_URL;
});
afterEach(() => {
delete process.env.PAID_CORE_MODULE_URL;
});
it('免费版只同步空模块清单,不拉取 paid-core', async () => {
mockLicense('FREE');
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: [],
issuedAt: new Date().toISOString(),
expiresAt: manifestExpiresAt,
},
signature: 'valid-signature',
}),
});
const { syncPremiumModules } = await import('../license/premium-module-service');
const result = await syncPremiumModules();
expect(result).toEqual({ synced: true });
expect(mockFetch).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledWith(
'https://license.test/api/license/flnxi/premium-modules',
expect.objectContaining({ method: 'POST' })
);
expect(prisma.systemConfig.upsert).toHaveBeenCalledTimes(3);
});
it('付费版应拉取 paid-core 授权凭证和短期拉包凭证', async () => {
mockLicense('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: [mockPaidCoreManifestModule()],
issuedAt: new Date().toISOString(),
expiresAt: manifestExpiresAt,
},
signature: 'valid-signature',
}),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
success: true,
entitlement: {
moduleId: 'paid-core',
entitlementBase64: 'module-entitlement',
sha256: 'hash',
sizeBytes: 18,
expiresAt: new Date(Date.now() + 60_000).toISOString(),
artifact: paidCoreArtifact,
},
}),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
success: true,
pullCredential: mockPullCredential(),
}),
});
const { syncPremiumModules } = await import('../license/premium-module-service');
const result = await syncPremiumModules();
expect(result).toEqual({ synced: true });
expect(mockFetch).toHaveBeenNthCalledWith(
2,
'https://license.test/api/license/flnxi/premium-modules/paid-core',
expect.objectContaining({ method: 'POST' })
);
expect(mockFetch).toHaveBeenNthCalledWith(
3,
'https://license.test/api/license/flnxi/premium-modules/paid-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('没有有效授权凭证时不应视为已拥有 paid-core', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: false });
const { hasPremiumModule } = await import('../license/premium-module-service');
await expect(hasPremiumModule('paid-core')).resolves.toBe(false);
});
it('清单和本地授权凭证都有效时应视为已拥有 paid-core', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true });
const { hasPremiumModule } = await import('../license/premium-module-service');
await expect(hasPremiumModule('paid-core')).resolves.toBe(true);
});
it('缓存中的短期拉包凭证有效时才返回给 Agent', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, pullCredential: mockPullCredential() });
const { getCachedModulePullCredential } = await import('../license/premium-module-service');
await expect(getCachedModulePullCredential('paid-core')).resolves.toEqual(
expect.objectContaining({
username: 'flnxi-module',
repository: 'flnxi/premium/paid-core',
})
);
});
it('过期的短期拉包凭证不应返回给 Agent', async () => {
mockLicense('PRO');
mockCachedPaidCore({
withEntitlement: true,
pullCredential: mockPullCredential({ expiresAt: new Date(Date.now() - 1_000).toISOString() }),
});
const { getCachedModulePullCredential } = await import('../license/premium-module-service');
await expect(getCachedModulePullCredential('paid-core')).resolves.toBeNull();
});
it('paid-core 未处于运行态时不允许使用受控能力', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'missing' });
const { authorizePremiumModuleAction } = await import('../license/premium-module-service');
await expect(authorizePremiumModuleAction('paid-core', 'customTemplates:use')).resolves.toEqual(
expect.objectContaining({
available: false,
moduleId: 'paid-core',
action: 'customTemplates:use',
status: 'missing',
})
);
});
it('paid-core 运行且授权接口放行时允许使用受控能力', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', allowed: true }),
});
const { authorizePremiumModuleAction } = await import('../license/premium-module-service');
await expect(authorizePremiumModuleAction('paid-core', 'customTemplates:use')).resolves.toEqual(
expect.objectContaining({
available: true,
moduleId: 'paid-core',
action: 'customTemplates:use',
status: 'running',
})
);
expect(mockFetch).toHaveBeenLastCalledWith(
'http://paid-core.test/authorize',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('"action":"customTemplates:use"'),
})
);
});
it('paid-core 运行时应由模块执行自定义模板字段校验和格式化', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
moduleId: 'paid-core',
valid: true,
fields: [
{
id: null,
key: 'brandName',
label: '品牌',
type: 'text',
required: false,
orderIndex: 0,
layoutWidth: 'full',
validations: null,
optionsSource: null,
optionsStatic: null,
allowedExtensions: null,
visibleOnHomeFilter: false,
isSearchable: false,
isReadOnly: false,
addToCategory: false,
conditions: null,
},
],
}),
});
const { validateUploadTemplateFieldsWithPaidCore } = await import('../license/premium-module-service');
const result = await validateUploadTemplateFieldsWithPaidCore([
{ key: 'brandName', label: '品牌', type: 'text' },
]);
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.fields[0].key).toBe('brandName');
}
expect(mockFetch).toHaveBeenLastCalledWith(
'http://paid-core.test/templates/validate-fields',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('"brandName"'),
})
);
});
it('paid-core 运行时应由模块校验素材提交的自定义模板字段内容', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
moduleId: 'paid-core',
valid: true,
templateName: '自定义模板',
templateFields: {
customCode: 'CODE-001',
},
visibleFieldKeys: ['customCode'],
}),
});
const { validateSubmittedTemplateFieldsWithPaidCore } = await import('../license/premium-module-service');
const result = await validateSubmittedTemplateFieldsWithPaidCore({
templateId: 'template-1',
templateName: '自定义模板',
fields: [
{
key: 'customCode',
label: '自定义编码',
type: 'text',
required: true,
validations: JSON.stringify({ pattern: '^CODE-' }),
},
],
templateFields: {
customCode: 'CODE-001',
},
allowUnknownKeys: false,
});
expect(result).toEqual({
valid: true,
templateName: '自定义模板',
templateFields: {
customCode: 'CODE-001',
},
visibleFieldKeys: ['customCode'],
});
expect(mockFetch).toHaveBeenLastCalledWith(
'http://paid-core.test/materials/validate-template-fields',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('"templateId":"template-1"'),
})
);
});
it('paid-core 拒绝素材自定义字段内容时应透传错误码和详情', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: false,
status: 400,
json: async () => ({
moduleId: 'paid-core',
valid: false,
error: '模板必填字段未填写:自定义编码',
errorCode: 'TEMPLATE_REQUIRED_FIELDS_MISSING',
details: {
missingRequiredFields: [{ key: 'customCode', label: '自定义编码', type: 'text' }],
},
}),
});
const { validateSubmittedTemplateFieldsWithPaidCore } = await import('../license/premium-module-service');
const result = await validateSubmittedTemplateFieldsWithPaidCore({
templateId: 'template-1',
templateName: '自定义模板',
fields: [{ key: 'customCode', label: '自定义编码', type: 'text', required: true }],
templateFields: {},
allowUnknownKeys: false,
});
expect(result).toEqual({
valid: false,
error: '模板必填字段未填写:自定义编码',
errorCode: 'TEMPLATE_REQUIRED_FIELDS_MISSING',
details: {
missingRequiredFields: [{ key: 'customCode', label: '自定义编码', type: 'text' }],
},
});
});
it('paid-core 运行时应由模块准备自定义模板素材 metadata', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
moduleId: 'paid-core',
valid: true,
standardData: { productModel: 'MX-001' },
metadata: JSON.stringify({
templateId: 'template-1',
templateFields: { customCode: 'CODE-001' },
}),
metadataSize: { ok: true, size: 72, maxSize: 65535 },
}),
});
const { prepareMaterialMetadataWithPaidCore } = await import('../license/premium-module-service');
const result = await prepareMaterialMetadataWithPaidCore({
templateId: 'template-1',
templateFields: {
productModel: 'MX-001',
customCode: 'CODE-001',
},
previewUrls: ['s3://bucket/preview.webp'],
});
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.standardData.productModel).toBe('MX-001');
expect(result.metadata).toContain('customCode');
expect(result.metadataSize.ok).toBe(true);
}
expect(mockFetch).toHaveBeenLastCalledWith(
'http://paid-core.test/materials/prepare-metadata',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('"templateId":"template-1"'),
})
);
});
it('paid-core 拒绝超限 metadata 时应透传错误码和详情', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: false,
status: 400,
json: async () => ({
moduleId: 'paid-core',
valid: false,
error: '自定义字段数据过大',
errorCode: 'METADATA_TOO_LARGE',
details: { size: 70000, maxSize: 65535 },
}),
});
const { prepareMaterialMetadataWithPaidCore } = await import('../license/premium-module-service');
const result = await prepareMaterialMetadataWithPaidCore({
templateId: 'template-1',
templateFields: { customCode: 'x'.repeat(70_000) },
});
expect(result).toEqual({
valid: false,
error: '自定义字段数据过大',
errorCode: 'METADATA_TOO_LARGE',
details: { size: 70000, maxSize: 65535 },
});
});
it('paid-core 运行时应由模块准备模板迁移 metadata', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', allowed: true }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
moduleId: 'paid-core',
valid: true,
standardData: { productModel: 'MX-001' },
metadata: JSON.stringify({
templateId: 'template-target',
templateFields: { customCode: 'CODE-001' },
templateMigrationBackups: [
{
sourceTemplateId: 'template-source',
targetTemplateId: 'template-target',
droppedTemplateFields: { oldOnlyField: '旧值' },
},
],
}),
metadataSize: { ok: true, size: 180, maxSize: 65535 },
droppedTemplateFieldCount: 1,
droppedStandardFieldCount: 1,
droppedTemplateFields: { oldOnlyField: '旧值' },
droppedStandardFields: { categoryName: '旧分类' },
}),
});
const { prepareTemplateMigrationMetadataWithPaidCore } = await import('../license/premium-module-service');
const result = await prepareTemplateMigrationMetadataWithPaidCore({
sourceTemplateId: 'template-source',
sourceTemplateName: '旧模板',
targetTemplateId: 'template-target',
targetTemplateName: '新模板',
migratedAt: '2026-06-03T00:00:00.000Z',
targetFieldKeys: ['productModel', 'customCode'],
templateFields: {
productModel: 'MX-001',
customCode: 'CODE-001',
},
materialStandardFields: {
productModel: 'MX-001',
categoryName: '旧分类',
},
});
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.standardData.productModel).toBe('MX-001');
expect(result.droppedTemplateFieldCount).toBe(1);
expect(result.droppedStandardFields.categoryName).toBe('旧分类');
expect(result.metadata).toContain('templateMigrationBackups');
}
expect(mockFetch).toHaveBeenLastCalledWith(
'http://paid-core.test/templates/prepare-migration-metadata',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('"targetTemplateId":"template-target"'),
})
);
});
it('paid-core 运行时应由模块生成模板迁移预览和备份统计', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
moduleId: 'paid-core',
valid: true,
migrationBackup: {
materialsWithDroppedFields: 1,
droppedTemplateFieldCount: 1,
droppedStandardFieldCount: 1,
},
migrationPreview: {
totalMaterialCount: 5,
totalMaterials: 1,
previewLimited: true,
previewLimit: 1,
targetCurrentFields: [
{ key: 'productModel', label: '产品型号', type: 'text', required: false },
],
backupFields: {
templateFields: [
{
key: 'oldOnlyField',
label: '旧字段',
type: 'text',
source: 'template',
affectedCount: 1,
},
],
standardFields: [
{
key: 'categoryName',
label: '分类',
source: 'standard',
affectedCount: 1,
},
],
},
samples: [
{
materialId: 'mat-1',
productModel: 'MX-001',
droppedTemplateFields: [
{ key: 'oldOnlyField', label: '旧字段', type: 'text', source: 'template' },
],
droppedStandardFields: [
{ key: 'categoryName', label: '分类', source: 'standard' },
],
},
],
},
}),
});
const { buildTemplateMigrationPreviewWithPaidCore } = await import('../license/premium-module-service');
const result = await buildTemplateMigrationPreviewWithPaidCore({
totalMaterialCount: 5,
migratableCount: 1,
previewOnly: true,
previewLimit: 1,
targetFields: [{ key: 'productModel', label: '产品型号', type: 'text', required: false }],
sourceTemplateFields: [{ key: 'oldOnlyField', label: '旧字段', type: 'text', required: false }],
impacts: [
{
materialId: 'mat-1',
productModel: 'MX-001',
droppedTemplateFieldKeys: ['oldOnlyField'],
droppedStandardFieldKeys: ['categoryName'],
},
],
});
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.migrationBackup.materialsWithDroppedFields).toBe(1);
expect(result.migrationPreview.previewLimited).toBe(true);
expect(result.migrationPreview.backupFields.templateFields[0].key).toBe('oldOnlyField');
expect(result.migrationPreview.samples[0].materialId).toBe('mat-1');
}
expect(mockFetch).toHaveBeenLastCalledWith(
'http://paid-core.test/templates/build-migration-preview',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('"droppedTemplateFieldKeys":["oldOnlyField"]'),
})
);
});
it('paid-core 运行时应由模块规划模板迁移批处理', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: false,
status: 409,
json: async () => ({
moduleId: 'paid-core',
valid: true,
allowed: false,
statusCode: 409,
error: '该模板当前有 300 个素材,超过默认安全迁移数量 200。请先预览迁移影响,确认备份后再执行。',
errorCode: 'MIGRATION_CONFIRM_REQUIRED',
details: {
totalMaterialCount: 300,
safeLimit: 200,
migrationLimit: 300,
maxBatchLimit: 1000,
},
}),
});
const { planTemplateMigrationBatchWithPaidCore } = await import('../license/premium-module-service');
const result = await planTemplateMigrationBatchWithPaidCore({
sourceTemplateId: 'template-source',
sourceTemplateName: '旧模板',
targetTemplateId: 'template-target',
targetTemplateName: '新模板',
totalMaterialCount: 300,
request: {
migrationLimit: 300,
},
});
expect(result).toEqual({
valid: true,
allowed: false,
statusCode: 409,
error: '该模板当前有 300 个素材,超过默认安全迁移数量 200。请先预览迁移影响,确认备份后再执行。',
errorCode: 'MIGRATION_CONFIRM_REQUIRED',
response: undefined,
details: {
totalMaterialCount: 300,
safeLimit: 200,
migrationLimit: 300,
maxBatchLimit: 1000,
},
});
expect(mockFetch).toHaveBeenLastCalledWith(
'http://paid-core.test/templates/plan-migration-batch',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('"totalMaterialCount":300'),
})
);
});
it('paid-core 运行时应由模块生成公开模板列表和素材类型投影', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
moduleId: 'paid-core',
valid: true,
templates: [
{ id: 'builtin-other-material', name: '其他素材模板', isBuiltin: true },
{ id: 'custom-template', name: '客户模板素材模板', isBuiltin: false },
],
uploadTemplates: [
{ id: 'builtin-other-material', name: '其他素材模板', isBuiltin: true, materialCount: 3 },
{
id: 'custom-template',
name: '客户模板素材模板',
isBuiltin: false,
materialCount: 2,
fields: [{ key: 'customCode', label: '编码', isReadOnly: false }],
},
],
materialTypes: ['其他', 'template:custom-template:客户模板'],
}),
});
const { projectPublicTemplateOptionsWithPaidCore } = await import('../license/premium-module-service');
const result = await projectPublicTemplateOptionsWithPaidCore({
builtinTemplates: [{ id: 'builtin-other-material', name: '其他素材模板' }],
customTemplates: [
{
id: 'custom-template',
name: '客户模板素材模板',
status: 'active',
fields: [
{ key: 'customCode', label: '编码', isReadOnly: false },
{ key: 'materialType', label: '素材类型', isReadOnly: false },
],
},
],
materialCounts: {
'builtin-other-material': 3,
'custom-template': 2,
},
builtinMaterialTypes: ['其他'],
});
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.templates).toHaveLength(2);
expect(result.uploadTemplates[1]).toMatchObject({ id: 'custom-template', materialCount: 2 });
expect(result.materialTypes).toContain('template:custom-template:客户模板');
}
expect(mockFetch).toHaveBeenLastCalledWith(
'http://paid-core.test/templates/project-public-options',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('"builtinMaterialTypes":["其他"]'),
})
);
});
it('paid-core 运行时应由模块计算自定义模板字段填写率', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
moduleId: 'paid-core',
valid: true,
fieldStats: {
totalMaterials: 2,
fields: [
{
fieldKey: 'brandName',
fieldLabel: '品牌',
fieldType: 'text',
filledCount: 1,
totalCount: 2,
fillRate: 50,
isLowFillRate: false,
},
],
lowFillRateFields: [],
},
}),
});
const { calculateTemplateFieldStatsWithPaidCore } = await import('../license/premium-module-service');
const result = await calculateTemplateFieldStatsWithPaidCore({
fields: [{ key: 'brandName', label: '品牌', type: 'text' }],
materials: [
{ id: 'mat-1', metadata: JSON.stringify({ templateFields: { brandName: 'FLNXI' } }) },
{ id: 'mat-2', metadata: JSON.stringify({ templateFields: { brandName: '' } }) },
],
});
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.fieldStats?.totalMaterials).toBe(2);
expect(result.fieldStats?.fields[0]).toMatchObject({
fieldKey: 'brandName',
fillRate: 50,
});
}
expect(mockFetch).toHaveBeenLastCalledWith(
'http://paid-core.test/templates/calculate-field-stats',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('"fields":[{"key":"brandName"'),
})
);
});
it('paid-core 运行时应由模块规划素材列表自定义字段搜索', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
moduleId: 'paid-core',
valid: true,
search: 'CODE-001',
customFieldKeys: ['customCode'],
}),
});
const { planMaterialCustomSearchWithPaidCore } = await import('../license/premium-module-service');
const result = await planMaterialCustomSearchWithPaidCore({
search: 'CODE-001',
searchableFields: [
{ key: 'customCode' },
{ key: 'productModel' },
],
});
expect(result).toEqual({
valid: true,
search: 'CODE-001',
customFieldKeys: ['customCode'],
});
expect(mockFetch).toHaveBeenLastCalledWith(
'http://paid-core.test/materials/plan-custom-search',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('"searchableFields":[{"key":"customCode"},{"key":"productModel"}]'),
})
);
});
it('paid-core 运行时应由模块规划素材列表查询条件', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
moduleId: 'paid-core',
valid: true,
search: 'CODE-001',
standardSearchFields: ['productModel', 'brandName'],
customSearchFieldKeys: ['customCode'],
standardHomeFilters: [{ key: 'brandName', value: 'FLNXI' }],
customHomeFilters: [{ key: 'customCode', value: 'CODE-001' }],
}),
});
const { planMaterialListQueryWithPaidCore } = await import('../license/premium-module-service');
const result = await planMaterialListQueryWithPaidCore({
search: 'CODE-001',
searchableFields: [{ key: 'customCode' }, { key: 'productModel' }],
homeFieldFilters: [
{ key: 'brandName', value: 'FLNXI' },
{ key: 'customCode', value: 'CODE-001' },
],
enabledHomeFilterFields: [{ key: 'customCode' }],
});
expect(result).toEqual({
valid: true,
search: 'CODE-001',
standardSearchFields: ['productModel', 'brandName'],
customSearchFieldKeys: ['customCode'],
standardHomeFilters: [{ key: 'brandName', value: 'FLNXI' }],
customHomeFilters: [{ key: 'customCode', value: 'CODE-001' }],
moduleAvailable: true,
});
expect(mockFetch).toHaveBeenLastCalledWith(
'http://paid-core.test/materials/plan-list-query',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('"homeFieldFilters"'),
})
);
});
it('paid-core 运行时应由模块规划首页自定义筛选字段统计', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
moduleId: 'paid-core',
valid: true,
customFieldKeys: ['customCode'],
}),
});
const { planHomeFilterCustomFieldCountsWithPaidCore } = await import('../license/premium-module-service');
const result = await planHomeFilterCustomFieldCountsWithPaidCore({
fields: [
{ key: 'categoryName' },
{ key: 'customCode' },
],
});
expect(result).toEqual({
valid: true,
customFieldKeys: ['customCode'],
});
expect(mockFetch).toHaveBeenLastCalledWith(
'http://paid-core.test/materials/plan-home-filter-counts',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('"fields":[{"key":"categoryName"},{"key":"customCode"}]'),
})
);
});
it('paid-core 运行时应由模块规划首页筛选字段统计来源', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
moduleId: 'paid-core',
valid: true,
fieldPlans: [
{ key: 'categoryName', source: 'column' },
{ key: 'customCode', source: 'metadata' },
],
}),
});
const { planHomeFilterFieldCountsWithPaidCore } = await import('../license/premium-module-service');
const result = await planHomeFilterFieldCountsWithPaidCore({
fields: [
{ key: 'categoryName' },
{ key: 'customCode' },
],
});
expect(result).toEqual({
valid: true,
fieldPlans: [
{ key: 'categoryName', source: 'column' },
{ key: 'customCode', source: 'metadata' },
],
moduleAvailable: true,
});
expect(mockFetch).toHaveBeenLastCalledWith(
'http://paid-core.test/materials/plan-home-filter-fields',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('"customCode"'),
})
);
});
it('paid-core 运行时应由模块规划部门隔离管理范围', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
moduleId: 'paid-core',
valid: true,
scopedDepartments: ['设计部', '设计部-建模组'],
withinScope: true,
}),
});
const { planDepartmentScopeWithPaidCore } = await import('../license/premium-module-service');
const result = await planDepartmentScopeWithPaidCore({
scopeDepartment: '设计部',
targetDepartment: '设计部-建模组',
departments: [
{ name: '设计部' },
{ name: '设计部-建模组' },
{ name: '运营部' },
],
});
expect(result).toEqual({
valid: true,
scopedDepartments: ['设计部', '设计部-建模组'],
withinScope: true,
});
expect(mockFetch).toHaveBeenLastCalledWith(
'http://paid-core.test/departments/plan-scope',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('"scopeDepartment":"设计部"'),
})
);
});
it('paid-core 运行时应由模块规划部门结构变更', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
moduleId: 'paid-core',
valid: true,
allowed: true,
action: 'rename',
departmentOps: [
{ type: 'rename', id: 'dept-1', oldName: '设计部', newName: '产品部' },
],
userOps: [
{ type: 'update', id: 'user-1', data: { department: '产品部' } },
],
summary: {
renamedDepartmentCount: 1,
updatedUserCount: 1,
},
}),
});
const { planDepartmentStructureChangeWithPaidCore } = await import('../license/premium-module-service');
const result = await planDepartmentStructureChangeWithPaidCore({
action: 'rename',
target: { id: 'dept-1', name: '产品部' },
departments: [{ id: 'dept-1', name: '设计部' }],
users: [{ id: 'user-1', department: '设计部', managedDepartment: null }],
});
expect(result).toEqual({
valid: true,
allowed: true,
action: 'rename',
departmentOps: [
{ type: 'rename', id: 'dept-1', oldName: '设计部', newName: '产品部' },
],
userOps: [
{ type: 'update', id: 'user-1', data: { department: '产品部' } },
],
summary: {
renamedDepartmentCount: 1,
updatedUserCount: 1,
},
});
expect(mockFetch).toHaveBeenLastCalledWith(
'http://paid-core.test/departments/plan-structure-change',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('"action":"rename"'),
})
);
});
it('paid-core 拒绝模板迁移超限 metadata 时应透传错误码和详情', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', allowed: true }),
})
.mockResolvedValueOnce({
ok: false,
status: 400,
json: async () => ({
moduleId: 'paid-core',
valid: false,
error: '模板迁移后 metadata 数据过大',
errorCode: 'METADATA_TOO_LARGE',
details: { size: 70000, maxSize: 65535 },
}),
});
const { prepareTemplateMigrationMetadataWithPaidCore } = await import('../license/premium-module-service');
const result = await prepareTemplateMigrationMetadataWithPaidCore({
sourceTemplateId: 'template-source',
sourceTemplateName: '旧模板',
targetTemplateId: 'template-target',
targetTemplateName: '新模板',
migratedAt: '2026-06-03T00:00:00.000Z',
targetFieldKeys: ['productModel'],
templateFields: { productModel: 'MX-001' },
materialStandardFields: { productModel: 'MX-001' },
});
expect(result).toEqual({
valid: false,
error: '模板迁移后 metadata 数据过大',
errorCode: 'METADATA_TOO_LARGE',
details: { size: 70000, maxSize: 65535 },
});
});
it('paid-core 拒绝模板迁移动作授权时不应继续准备迁移 metadata', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: false,
status: 403,
json: async () => ({ moduleId: 'paid-core', allowed: false, error: '授权模块拒绝迁移' }),
});
const { prepareTemplateMigrationMetadataWithPaidCore } = await import('../license/premium-module-service');
const result = await prepareTemplateMigrationMetadataWithPaidCore({
sourceTemplateId: 'template-source',
sourceTemplateName: '旧模板',
targetTemplateId: 'template-target',
targetTemplateName: '新模板',
migratedAt: '2026-06-03T00:00:00.000Z',
targetFieldKeys: ['productModel'],
targetFields: [{ key: 'productModel', label: '产品型号', type: 'text', required: false }],
materialFieldValues: { productModel: 'MX-001' },
materialStandardFields: { productModel: 'MX-001' },
});
expect(result).toEqual(
expect.objectContaining({
valid: false,
errorCode: 'PAID_CORE_TEMPLATE_MIGRATION_UNAUTHORIZED',
})
);
expect(mockFetch).toHaveBeenCalledTimes(2);
expect(mockFetch).toHaveBeenLastCalledWith(
'http://paid-core.test/authorize',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('customTemplates:migrate'),
})
);
});
it('paid-core 未运行时不得准备模板迁移 metadata', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'missing' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
const { prepareTemplateMigrationMetadataWithPaidCore } = await import('../license/premium-module-service');
const result = await prepareTemplateMigrationMetadataWithPaidCore({
sourceTemplateId: 'template-source',
sourceTemplateName: '旧模板',
targetTemplateId: 'template-target',
targetTemplateName: '新模板',
migratedAt: '2026-06-03T00:00:00.000Z',
targetFieldKeys: ['productModel'],
templateFields: { productModel: 'MX-001' },
materialStandardFields: { productModel: 'MX-001' },
});
expect(result).toEqual(
expect.objectContaining({
valid: false,
errorCode: 'PAID_CORE_TEMPLATE_MIGRATION_UNAVAILABLE',
})
);
expect(mockFetch).not.toHaveBeenCalled();
});
it('paid-core 运行时应由模块执行自定义模板保存请求标准化', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
moduleId: 'paid-core',
valid: true,
mode: 'create',
fieldsProvided: true,
template: {
name: '成品图模板',
description: null,
status: 'draft',
materialType: '成品图',
materialKind: 'IMAGE',
fieldGroups: null,
displayConfig: null,
fields: [
{
id: null,
key: 'brandName',
label: '品牌',
type: 'text',
required: false,
orderIndex: 0,
layoutWidth: 'full',
validations: null,
optionsSource: null,
optionsStatic: null,
allowedExtensions: null,
visibleOnHomeFilter: false,
isSearchable: false,
isReadOnly: false,
addToCategory: false,
conditions: null,
},
],
},
}),
});
const { normalizeUploadTemplateSaveWithPaidCore } = await import('../license/premium-module-service');
const result = await normalizeUploadTemplateSaveWithPaidCore(
{
name: ' 成品图模板 ',
materialType: '成品图',
materialKind: 'image',
fields: [{ key: 'brandName', label: '品牌', type: 'text' }],
},
'create'
);
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.template).toEqual(
expect.objectContaining({
name: '成品图模板',
materialKind: 'IMAGE',
})
);
expect(result.fieldsProvided).toBe(true);
}
expect(mockFetch).toHaveBeenLastCalledWith(
'http://paid-core.test/templates/normalize-save',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('"mode":"create"'),
})
);
expect(mockFetch.mock.calls.at(-1)?.[1]?.body).toContain('"payload"');
});
it('paid-core 保存标准化支持模板局部更新', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
moduleId: 'paid-core',
valid: true,
mode: 'update',
fieldsProvided: false,
template: {
description: '只更新说明',
},
}),
});
const { normalizeUploadTemplateSaveWithPaidCore } = await import('../license/premium-module-service');
const result = await normalizeUploadTemplateSaveWithPaidCore({ description: '只更新说明' }, 'update');
expect(result).toEqual({
valid: true,
mode: 'update',
fieldsProvided: false,
template: {
description: '只更新说明',
},
});
expect(mockFetch).toHaveBeenLastCalledWith(
'http://paid-core.test/templates/normalize-save',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('"mode":"update"'),
})
);
});
it('paid-core 未运行时不得执行模板保存标准化', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'missing' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
const { normalizeUploadTemplateSaveWithPaidCore } = await import('../license/premium-module-service');
const result = await normalizeUploadTemplateSaveWithPaidCore({ description: '只更新说明' }, 'update');
expect(result).toEqual(
expect.objectContaining({
valid: false,
field: 'template',
})
);
if (!result.valid) {
expect(result.details).toEqual(expect.objectContaining({ moduleId: 'paid-core', status: 'missing' }));
}
expect(mockFetch).not.toHaveBeenCalled();
});
it('paid-core 运行时应由模块规划自定义模板删除', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
moduleId: 'paid-core',
valid: true,
allowed: false,
error: '无法删除模板"使用中的模板",因为有 5 个素材正在使用该模板。请先删除或迁移这些素材。',
errorCode: 'TEMPLATE_IN_USE',
details: {
templateName: '使用中的模板',
materialCount: 5,
},
}),
});
const { planUploadTemplateDeleteWithPaidCore } = await import('../license/premium-module-service');
const result = await planUploadTemplateDeleteWithPaidCore({
templateName: '使用中的模板',
materialCount: 5,
});
expect(result).toEqual({
valid: true,
allowed: false,
error: '无法删除模板"使用中的模板",因为有 5 个素材正在使用该模板。请先删除或迁移这些素材。',
errorCode: 'TEMPLATE_IN_USE',
details: {
templateName: '使用中的模板',
materialCount: 5,
},
});
expect(mockFetch).toHaveBeenLastCalledWith(
'http://paid-core.test/templates/plan-delete',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('"materialCount":5'),
})
);
});
it('paid-core 未运行时不得规划自定义模板删除', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'missing' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
const { planUploadTemplateDeleteWithPaidCore } = await import('../license/premium-module-service');
const result = await planUploadTemplateDeleteWithPaidCore({
templateName: '使用中的模板',
materialCount: 5,
});
expect(result).toEqual(
expect.objectContaining({
valid: false,
allowed: false,
errorCode: 'PAID_CORE_TEMPLATE_DELETE_PLAN_UNAVAILABLE',
})
);
expect(mockFetch).not.toHaveBeenCalled();
});
it('paid-core 运行时应由模块规划自定义模板更新', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
moduleId: 'paid-core',
valid: true,
updateData: {
description: '更新说明',
version: 2,
},
fieldsProvided: false,
fields: null,
nextVersion: 2,
}),
});
const { planUploadTemplateUpdateWithPaidCore } = await import('../license/premium-module-service');
const result = await planUploadTemplateUpdateWithPaidCore({
payload: { description: '更新说明', version: 1 },
existingTemplate: {
id: 'template-1',
version: 1,
name: '模板',
updatedAt: '2026-06-03T00:00:00.000Z',
materialType: '3DMAX',
materialKind: 'IMAGE',
materialCount: 0,
fields: [],
},
});
expect(result).toEqual({
valid: true,
updateData: {
description: '更新说明',
version: 2,
},
fieldsProvided: false,
fields: null,
nextVersion: 2,
});
expect(mockFetch).toHaveBeenLastCalledWith(
'http://paid-core.test/templates/plan-update',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('"existingTemplate"'),
})
);
});
it('paid-core 更新规划拒绝结构变更时应透传状态码和详情', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: false,
status: 409,
json: async () => ({
moduleId: 'paid-core',
valid: false,
error: '模板已有素材在使用',
errorCode: 'TEMPLATE_STRUCTURE_LOCKED',
field: 'template',
statusCode: 409,
details: {
templateName: '使用中的模板',
materialCount: 3,
dangerousChanges: [{ type: 'fieldDeleted', fieldId: 'field-1' }],
},
}),
});
const { planUploadTemplateUpdateWithPaidCore } = await import('../license/premium-module-service');
const result = await planUploadTemplateUpdateWithPaidCore({
payload: { version: 1, fields: [] },
existingTemplate: {
id: 'template-1',
version: 1,
name: '使用中的模板',
updatedAt: '2026-06-03T00:00:00.000Z',
materialType: '3DMAX',
materialKind: 'IMAGE',
materialCount: 3,
fields: [{
id: 'field-1',
key: 'productModel',
type: 'text',
label: '型号',
required: false,
addToCategory: false,
isSearchable: false,
isReadOnly: false,
validations: null,
optionsStatic: null,
conditions: null,
}],
},
});
expect(result).toEqual(
expect.objectContaining({
valid: false,
errorCode: 'TEMPLATE_STRUCTURE_LOCKED',
statusCode: 409,
details: expect.objectContaining({
materialCount: 3,
}),
})
);
});
it('paid-core 拒绝保存标准化时应透传错误字段和详情', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: false,
status: 400,
json: async () => ({
moduleId: 'paid-core',
valid: false,
error: '模板状态必须是 draft、active 或 archived',
field: 'status',
details: { allowedStatuses: ['draft', 'active', 'archived'] },
}),
});
const { normalizeUploadTemplateSaveWithPaidCore } = await import('../license/premium-module-service');
const result = await normalizeUploadTemplateSaveWithPaidCore({ status: 'invalid' }, 'update');
expect(result).toEqual({
valid: false,
error: '模板状态必须是 draft、active 或 archived',
field: 'status',
details: { allowedStatuses: ['draft', 'active', 'archived'] },
});
});
it('paid-core 运行时应由模块判断自定义模板结构变更风险', async () => {
mockLicense('PRO');
mockCachedPaidCore({ withEntitlement: true, runtimeStatus: 'running' });
process.env.PAID_CORE_MODULE_URL = 'http://paid-core.test';
mockFetch
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ moduleId: 'paid-core', status: 'healthy' }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
moduleId: 'paid-core',
valid: true,
changes: [
{
type: 'fieldDeleted',
fieldId: 'field-1',
fieldKey: 'brandName',
fieldLabel: '品牌',
},
],
}),
});
const { detectTemplateStructureChangesWithPaidCore } = await import('../license/premium-module-service');
const result = await detectTemplateStructureChangesWithPaidCore({
existingTemplate: {
materialType: 'image',
materialKind: 'IMAGE',
fields: [
{
id: 'field-1',
key: 'brandName',
label: '品牌',
type: 'text',
required: false,
addToCategory: false,
isSearchable: true,
isReadOnly: false,
validations: null,
optionsStatic: null,
conditions: null,
},
],
},
nextFields: [],
});
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.changes[0]).toEqual(
expect.objectContaining({
type: 'fieldDeleted',
fieldKey: 'brandName',
})
);
}
expect(mockFetch).toHaveBeenLastCalledWith(
'http://paid-core.test/templates/detect-structure-changes',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('"existingTemplate"'),
})
);
});
});