Files
demo/test/goods-business-input.test.cjs

71 lines
3.7 KiB
JavaScript

'use strict';
require('reflect-metadata');
const assert = require('node:assert/strict');
const test = require('node:test');
const fs = require('node:fs');
const path = require('node:path');
const ts = require('typescript');
const { SaasBusinessReject } = require('@cool-midway/module-runtime');
const sourceRoot = path.resolve(__dirname, '../src');
const previous = require.extensions['.ts'];
let GoodsService;
try {
require.extensions['.ts'] = (module, filename) => {
assert.ok(filename.startsWith(sourceRoot + path.sep));
const compiled = ts.transpileModule(fs.readFileSync(filename, 'utf8'), {
fileName: filename,
compilerOptions: { target: ts.ScriptTarget.ES2018, module: ts.ModuleKind.CommonJS,
experimentalDecorators: true, emitDecoratorMetadata: true, esModuleInterop: true },
});
module._compile(compiled.outputText, filename);
};
({ GoodsService } = require('../src/service/goods.ts'));
} finally {
if (previous) require.extensions['.ts'] = previous;
else delete require.extensions['.ts'];
}
function fixture(context = {}) {
const service = new GoodsService();
const calls = [];
service.saasActionContext = context;
service.inventory = { assertNoActiveReservations: async id => { calls.push(id); } };
return { service, calls };
}
const rejects = code => error => error instanceof SaasBusinessReject && error.code === code;
test('new catalog records accept an explicit initial stock and nonnegative decimal price', async () => {
const { service, calls } = fixture();
await service.modifyBefore({ operation: 'add', input: { title: '验收商品', price: '12.50', stock: 20, status: 1 } });
await service.modifyBefore({ operation: 'add', input: { title: '赠品', price: '0.00', stock: 0, status: 0 } });
assert.deepEqual(calls, []);
});
test('invalid catalog values are business rejections before persistence', async t => {
const inputs = [{ title: ' ' }, { price: '-0.01' }, { stock: -1 }, { stock: 1.5 }, { stock: 2147483648 }, { status: 2 }];
for (const input of inputs) await t.test(JSON.stringify(input), async () => {
const { service } = fixture();
await assert.rejects(service.modifyBefore({ operation: 'add', input: { title: '商品', price: '1.00', ...input } }), rejects('GOODS_INPUT_INVALID'));
});
});
test('catalog edits keep managed inventory protected even with valid stock values', async () => {
const { service } = fixture();
await service.modifyBefore({ operation: 'update', input: { id: '1', title: '更新名称', price: '0.00', status: 0 } });
await assert.rejects(service.modifyBefore({ operation: 'update', input: { id: '1', stock: 2 } }), rejects('STOCK_COMMAND_REQUIRED'));
await assert.rejects(service.modifyBefore({ operation: 'update', input: { id: '1', price: '-1.00' } }), rejects('GOODS_INPUT_INVALID'));
});
test('managed stock updates retain the same domain validation', async () => {
const { service } = fixture({ managedCrudCaller: { kind: 'business', actionName: 'goods.reserveStock' }, workflow: { coordinatorModule: 'order' } });
await service.modifyBefore({ operation: 'update', input: { id: '1', stock: 0 } });
await assert.rejects(service.modifyBefore({ operation: 'update', input: { id: '1', stock: -1 } }), rejects('GOODS_INPUT_INVALID'));
});
test('deletion still requires the inventory reservation lock and rejects an active reservation', async () => {
const { service, calls } = fixture();
await service.modifyBefore({ operation: 'delete', input: { id: '1' } });
assert.deepEqual(calls, ['1']);
service.inventory.assertNoActiveReservations = async () => { throw new SaasBusinessReject('STOCK_RESERVATION_CONFLICT', '仍有库存预留'); };
await assert.rejects(service.modifyBefore({ operation: 'delete', input: { id: '1' } }), rejects('STOCK_RESERVATION_CONFLICT'));
});