175 lines
6.9 KiB
JavaScript
175 lines
6.9 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
import { fileURLToPath } from 'node:url';
|
|
import vm from 'node:vm';
|
|
import ts from 'typescript';
|
|
|
|
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
const fromProjectRoot = (...segments) => path.join(projectRoot, ...segments);
|
|
|
|
function loadResources() {
|
|
const compiled = ts.transpileModule(fs.readFileSync(fromProjectRoot('src/api/resources.ts'), 'utf8'), {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
|
}).outputText;
|
|
const resourceModule = { exports: {} };
|
|
vm.runInNewContext(compiled, { module: resourceModule, exports: resourceModule.exports });
|
|
return resourceModule.exports.resources;
|
|
}
|
|
|
|
function loadResourceForm() {
|
|
const source = fs.readFileSync(fromProjectRoot('src/api/resource-form.ts'), 'utf8');
|
|
const compiled = ts.transpileModule(source, {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
|
}).outputText;
|
|
const resourceModule = { exports: {} };
|
|
vm.runInNewContext(compiled, { module: resourceModule, exports: resourceModule.exports });
|
|
return resourceModule.exports;
|
|
}
|
|
|
|
test('资源字段声明保留数字、布尔、时间与 JSON 类型', () => {
|
|
const resources = loadResources();
|
|
const field = (resource, key) => resources.find((item) => item.name === resource).fields.find((item) => item.key === key);
|
|
assert.equal(field('ec_product', 'price_amount').type, 'number');
|
|
assert.equal(field('ec_product_image', 'is_cover').type, 'boolean');
|
|
assert.equal(field('delivery_track', 'started_at').type, 'datetime');
|
|
assert.equal(field('dev_telemetry', 'payload').type, 'json');
|
|
});
|
|
|
|
test('表单载荷构造器省略空的可选关系并转换字段类型', async () => {
|
|
const resourceFormPath = fromProjectRoot('src/api/resource-form.ts');
|
|
assert.ok(fs.existsSync(resourceFormPath), 'resource-form.ts should define the payload boundary');
|
|
const resourceForm = loadResourceForm();
|
|
const fields = [
|
|
{ key: 'gas_basic_identity', label: '气站', type: 'identity' },
|
|
{ key: 'quantity', label: '数量', type: 'number' },
|
|
{ key: 'selected', label: '选中', type: 'boolean' },
|
|
];
|
|
assert.deepEqual(
|
|
JSON.parse(JSON.stringify(resourceForm.buildResourcePayload(fields, {
|
|
gas_basic_identity: '',
|
|
quantity: '2',
|
|
selected: false,
|
|
}))),
|
|
{ quantity: 2, selected: false },
|
|
);
|
|
});
|
|
|
|
test('安全规则与订单明细将 JSON 字段作为有效 JSON 字符串提交', () => {
|
|
const resources = loadResources();
|
|
const { buildResourcePayload } = loadResourceForm();
|
|
const fields = (name) => resources.find((item) => item.name === name).fields;
|
|
|
|
assert.deepEqual(
|
|
JSON.parse(JSON.stringify(buildResourcePayload(fields('saf_rule'), {
|
|
rule_code: 'pressure-limit',
|
|
threshold: '{"max":10}',
|
|
action: 'close-valve',
|
|
gray_scope: '["north"]',
|
|
}))),
|
|
{
|
|
rule_code: 'pressure-limit',
|
|
threshold: '{"max":10}',
|
|
action: 'close-valve',
|
|
gray_scope: '["north"]',
|
|
},
|
|
);
|
|
assert.deepEqual(
|
|
JSON.parse(JSON.stringify(buildResourcePayload(fields('ec_order_item'), {
|
|
ec_order_identity: 'order-a',
|
|
ec_product_identity: 'product-a',
|
|
product_snapshot: '{"name":"液化气"}',
|
|
quantity: 2,
|
|
sale_amount: 500,
|
|
}))),
|
|
{
|
|
ec_order_identity: 'order-a',
|
|
ec_product_identity: 'product-a',
|
|
product_snapshot: '{"name":"液化气"}',
|
|
quantity: 2,
|
|
sale_amount: 500,
|
|
},
|
|
);
|
|
});
|
|
|
|
test('日期按 RFC3339 提交,密码只在创建时必填并提交', () => {
|
|
const { buildResourcePayload, isResourceFieldRequired } = loadResourceForm();
|
|
const fields = [
|
|
{ key: 'bill_date', label: '账单日期', type: 'date', required: true },
|
|
{ key: 'started_at', label: '开始时间', type: 'datetime' },
|
|
{ key: 'username', label: '用户名', type: 'text', required: true },
|
|
{ key: 'password', label: '密码', type: 'password', required: true },
|
|
];
|
|
|
|
assert.equal(isResourceFieldRequired(fields[3], 'create'), true);
|
|
assert.equal(isResourceFieldRequired(fields[3], 'edit'), false);
|
|
assert.deepEqual(
|
|
JSON.parse(JSON.stringify(buildResourcePayload(fields, {
|
|
bill_date: '2026-07-27',
|
|
started_at: '2026-07-27T10:30:00Z',
|
|
username: 'operator',
|
|
password: 'secret',
|
|
}, 'create'))),
|
|
{
|
|
bill_date: '2026-07-27T00:00:00.000Z',
|
|
started_at: '2026-07-27T10:30:00.000Z',
|
|
username: 'operator',
|
|
password: 'secret',
|
|
},
|
|
);
|
|
assert.deepEqual(
|
|
JSON.parse(JSON.stringify(buildResourcePayload(fields, {
|
|
bill_date: '2026-07-27',
|
|
username: 'operator',
|
|
password: 'replacement-must-not-be-sent',
|
|
}, 'edit'))),
|
|
{
|
|
bill_date: '2026-07-27T00:00:00.000Z',
|
|
username: 'operator',
|
|
},
|
|
);
|
|
});
|
|
|
|
test('审批只读页提供同意和驳回操作', () => {
|
|
const source = fs.readFileSync(fromProjectRoot('src/views/shared/ReadOnlyListPage.vue'), 'utf8');
|
|
const resources = fs.readFileSync(fromProjectRoot('src/api/resources.ts'), 'utf8');
|
|
assert.match(resources, /aud_approval[\s\S]*\/audit\/aud_approval\/:identity\/approve/);
|
|
assert.match(source, /submitDetailAction/);
|
|
});
|
|
|
|
test('树页面通过资源归档接口归档节点', () => {
|
|
const source = fs.readFileSync(fromProjectRoot('src/views/shared/TreePage.vue'), 'utf8');
|
|
assert.match(source, /resourceApi\.archive/);
|
|
assert.match(source, /Modal\.(warning|confirm)/);
|
|
});
|
|
|
|
test('route access uses authenticated role and assigned menu codes', () => {
|
|
const routeDir = fromProjectRoot('src/router/routes/modules');
|
|
const routeSource = fs.readdirSync(routeDir)
|
|
.filter((name) => name.endsWith('.ts'))
|
|
.map((name) => fs.readFileSync(path.join(routeDir, name), 'utf8'))
|
|
.join('\n');
|
|
const userStore = fs.readFileSync(fromProjectRoot('src/store/modules/user/index.ts'), 'utf8');
|
|
const permission = fs.readFileSync(fromProjectRoot('src/hooks/permission.ts'), 'utf8');
|
|
|
|
assert.doesNotMatch(routeSource, /roles:\s*\[\s*['"]\*['"]\s*\]/);
|
|
assert.match(routeSource, /menuCode:\s*['"]finance['"]/);
|
|
assert.match(userStore, /profile\.role_code/);
|
|
assert.match(userStore, /menuCodes/);
|
|
assert.match(permission, /menuCode/);
|
|
});
|
|
|
|
test('platform role UI replaces assigned menu identities through the singular contract URL', () => {
|
|
const resources = fs.readFileSync(fromProjectRoot('src/api/resources.ts'), 'utf8');
|
|
const crudPage = fs.readFileSync(fromProjectRoot('src/views/shared/CrudListPage.vue'), 'utf8');
|
|
const platformApi = fs.readFileSync(fromProjectRoot('src/api/platform.ts'), 'utf8');
|
|
|
|
assert.match(resources, /platform_role[\s\S]*\/platform\/platform_role\/:identity\/menu/);
|
|
assert.match(resources, /menu-identities/);
|
|
assert.match(crudPage, /multiple/);
|
|
assert.match(crudPage, /menu_identities/);
|
|
assert.match(platformApi, /\/platform\/platform_role\/\$\{identity\}\/menu/);
|
|
assert.match(platformApi, /method:\s*['"]PUT['"]/);
|
|
});
|