fix: harden platform form and response boundaries

This commit is contained in:
2026-07-27 12:19:34 +08:00
parent 34e8b092b3
commit cd58e1f6b6
7 changed files with 294 additions and 67 deletions

View File

@@ -18,6 +18,16 @@ function loadResources() {
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);
@@ -30,19 +40,14 @@ test('资源字段声明保留数字、布尔、时间与 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 source = fs.readFileSync(resourceFormPath, '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 });
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(resourceModule.exports.buildResourcePayload(fields, {
JSON.parse(JSON.stringify(resourceForm.buildResourcePayload(fields, {
gas_basic_identity: '',
quantity: '2',
selected: false,
@@ -51,6 +56,81 @@ test('表单载荷构造器省略空的可选关系并转换字段类型', async
);
});
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');