@steedos-labs/plugin-workflow 3.0.103 → 3.0.105
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/designer/dist/amis-renderer/amis-renderer.js +1 -1
- package/designer/dist/assets/{index-C7za9VGq.js → index-CTmOPK1Y.js} +131 -131
- package/designer/dist/index.html +1 -1
- package/main/default/client/flow2_render.client.js +1 -1
- package/main/default/objects/instances/buttons/instance_delete.button.yml +1 -1
- package/main/default/objects/instances/buttons/instance_retrieve.button.yml +31 -4
- package/main/default/objects/instances/buttons/instance_save.button.yml +1 -1
- package/main/default/objects/instances/buttons/instance_submit.button.yml +1 -1
- package/main/default/routes/api_workflow_remove.router.js +6 -1
- package/main/default/routes/api_workflow_retrieve.router.js +9 -2
- package/main/default/routes/api_workflow_v2_remove.router.js +6 -1
- package/main/default/test/test_remove_guards.js +231 -0
- package/main/default/test/test_retrieve_guards.js +308 -0
- package/package.json +3 -1
- package/public/amis-renderer/amis-renderer.js +1 -1
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 回归测试:取回(/api/workflow/retrieve)的状态与资格校验。
|
|
3
|
+
*
|
|
4
|
+
* 背景(线上现象):
|
|
5
|
+
* 1. 已结束(state=completed)的申请单,最后一步处理人调用取回接口,
|
|
6
|
+
* 实例会被重新打开(state 重置为 pending)——实例状态校验被注释掉了,
|
|
7
|
+
* 且结束 trace 没有 approves,已读校验形同虚设。
|
|
8
|
+
* 2. retrieve_type 判定为空(当前用户不是上一步唯一处理人、也没有可取回的
|
|
9
|
+
* 传阅记录)时,路由静默返回 {status: 0},前端误报「取回成功」并刷新页面。
|
|
10
|
+
*
|
|
11
|
+
* 期望(修复后):
|
|
12
|
+
* - 非 pending 状态的申请单一律拒绝取回(含传阅取回),返回明确错误。
|
|
13
|
+
* - retrieve_type 为空时返回明确错误,不再静默假成功。
|
|
14
|
+
* - 正常取回、滑步取回、退回到开始步骤的已读豁免等既有行为不变。
|
|
15
|
+
*
|
|
16
|
+
* 用法:
|
|
17
|
+
* node main/default/test/test_retrieve_guards.js
|
|
18
|
+
* 或 npm run test:retrieve-guards
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const assert = require('assert');
|
|
22
|
+
const Module = require('module');
|
|
23
|
+
|
|
24
|
+
// ---- 伪造无法在独立环境 resolve 的外部依赖(express / @steedos/*) ----
|
|
25
|
+
const capturedRoutes = {};
|
|
26
|
+
const objectqlUpdates = [];
|
|
27
|
+
const fakeModules = {
|
|
28
|
+
'express': {
|
|
29
|
+
Router: function () {
|
|
30
|
+
return {
|
|
31
|
+
post: function (path) {
|
|
32
|
+
capturedRoutes[path] = arguments[arguments.length - 1];
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
'@steedos/auth': {
|
|
38
|
+
requireAuthentication: function (req, res, next) { next(); },
|
|
39
|
+
},
|
|
40
|
+
'@steedos/objectql': {
|
|
41
|
+
getObject: function (name) {
|
|
42
|
+
return {
|
|
43
|
+
update: async function (id, doc) { objectqlUpdates.push({ name, id, doc }); return doc; },
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
const origResolve = Module._resolveFilename;
|
|
49
|
+
Module._resolveFilename = function (request) {
|
|
50
|
+
if (fakeModules[request]) return 'fake:' + request;
|
|
51
|
+
return origResolve.apply(this, arguments);
|
|
52
|
+
};
|
|
53
|
+
Object.keys(fakeModules).forEach(function (name) {
|
|
54
|
+
require.cache['fake:' + name] = {
|
|
55
|
+
id: 'fake:' + name,
|
|
56
|
+
filename: 'fake:' + name,
|
|
57
|
+
loaded: true,
|
|
58
|
+
exports: fakeModules[name],
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ---- 伪造插件内部依赖 ----
|
|
63
|
+
let instanceUpdateCalls = [];
|
|
64
|
+
const dummyInstancesCollection = {
|
|
65
|
+
updateOne: async function (selector, modifier) {
|
|
66
|
+
instanceUpdateCalls.push({ selector: selector, modifier: modifier });
|
|
67
|
+
return { modifiedCount: 1 };
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
const dummyUsersCollection = {
|
|
71
|
+
findOne: async function (selector) { return { _id: selector._id, name: 'name_of_' + selector._id }; },
|
|
72
|
+
};
|
|
73
|
+
const dummyOrganizationsCollection = {
|
|
74
|
+
findOne: async function () { return { name: 'org', fullname: 'org' }; },
|
|
75
|
+
};
|
|
76
|
+
function seedCache(relPath, exportsObj) {
|
|
77
|
+
const p = require.resolve(relPath);
|
|
78
|
+
require.cache[p] = { id: p, filename: p, loaded: true, exports: exportsObj };
|
|
79
|
+
}
|
|
80
|
+
seedCache('../utils/collection.js', {
|
|
81
|
+
getCollection: async function (name) {
|
|
82
|
+
if (name === 'instances') return dummyInstancesCollection;
|
|
83
|
+
if (name === 'organizations') return dummyOrganizationsCollection;
|
|
84
|
+
return dummyUsersCollection;
|
|
85
|
+
},
|
|
86
|
+
_makeNewID: (function () { let n = 0; return function () { return 'new_id_' + (++n); }; })(),
|
|
87
|
+
});
|
|
88
|
+
seedCache('../manager/push_manager.js', {
|
|
89
|
+
send_message_current_user: async function () { },
|
|
90
|
+
send_message_to_specifyUser: async function () { },
|
|
91
|
+
triggerWebhook: async function () { },
|
|
92
|
+
});
|
|
93
|
+
seedCache('../manager/instance_tasks_manager.js', {
|
|
94
|
+
insert_instance_tasks: async function () { },
|
|
95
|
+
update_instance_tasks: async function () { },
|
|
96
|
+
update_many_instance_tasks: async function () { },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
let fakeInstance = null;
|
|
100
|
+
let stepTypes = {};
|
|
101
|
+
seedCache('../manager/uuflow_manager.js', {
|
|
102
|
+
getInstance: async function () { return fakeInstance; },
|
|
103
|
+
getFlow: async function () { return { _id: 'flow1' }; },
|
|
104
|
+
getStep: function (ins, flow, stepId) {
|
|
105
|
+
return { _id: stepId, name: 'name_' + stepId, step_type: stepTypes[stepId] || 'sign' };
|
|
106
|
+
},
|
|
107
|
+
getDueDate: async function () { return null; },
|
|
108
|
+
getSpaceUser: async function () { return { organization: 'org1' }; },
|
|
109
|
+
getSpaceUserOrgInfo: async function () {
|
|
110
|
+
return { organization: 'org1', organization_name: 'org', organization_fullname: 'org' };
|
|
111
|
+
},
|
|
112
|
+
setRemindInfo: async function () { },
|
|
113
|
+
getCurrentStepAutoSubmit: async function () { return false; },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
require('../routes/api_workflow_retrieve.router.js');
|
|
117
|
+
const handler = capturedRoutes['/api/workflow/retrieve'];
|
|
118
|
+
assert.ok(handler, '未捕获到取回路由 handler');
|
|
119
|
+
|
|
120
|
+
// ---- 实例构造 ----
|
|
121
|
+
// 默认结构:填写申请(t1, user_a) → 审批(t2, user_b 已审) → 当前步骤(t3, user_c 待审)
|
|
122
|
+
function buildPendingInstance(opts) {
|
|
123
|
+
opts = opts || {};
|
|
124
|
+
const traces = [
|
|
125
|
+
{
|
|
126
|
+
_id: 't1', step: 'step_start', name: '填写申请单', is_finished: true, judge: 'submitted',
|
|
127
|
+
approves: [{ _id: 'a1', user: 'user_a', handler: 'user_a', type: 'draft', judge: 'submitted' }],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
_id: 't2', step: 'step_real', name: '实际审批步骤', is_finished: true, judge: 'approved',
|
|
131
|
+
previous_trace_ids: ['t1'],
|
|
132
|
+
approves: [{ _id: 'a2', user: 'user_b', handler: 'user_b', judge: 'approved' }],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
_id: 't3', step: 'step_current', name: '当前步骤', is_finished: false,
|
|
136
|
+
previous_trace_ids: ['t2'],
|
|
137
|
+
approves: [{ _id: 'a3', user: 'user_c', handler: 'user_c', is_finished: false, is_read: !!opts.nextStepRead }],
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
return {
|
|
141
|
+
_id: 'ins1', space: 'space1', state: 'pending', applicant: 'user_a', submitter: 'user_a',
|
|
142
|
+
flow: 'flow1', form: 'form1', form_version: 'fv1',
|
|
143
|
+
inbox_users: ['user_c'], outbox_users: ['user_a', 'user_b'], cc_users: [],
|
|
144
|
+
values: {}, current_step_name: '当前步骤',
|
|
145
|
+
traces: traces,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 已结束实例:填写申请(t1, user_a) → 审批(t2, user_b 已审) → 结束(t_end, 无 approves)
|
|
150
|
+
function buildCompletedInstance(opts) {
|
|
151
|
+
opts = opts || {};
|
|
152
|
+
const t2Approves = [{ _id: 'a2', user: 'user_b', handler: 'user_b', judge: 'approved' }];
|
|
153
|
+
if (opts.withCC) {
|
|
154
|
+
t2Approves.push({ _id: 'a_cc', user: 'user_cc', handler: 'user_cc', type: 'cc', is_finished: true, judge: 'approved' });
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
_id: 'ins2', space: 'space1', state: 'completed', applicant: 'user_a', submitter: 'user_a',
|
|
158
|
+
flow: 'flow1', form: 'form1', form_version: 'fv1',
|
|
159
|
+
inbox_users: [], outbox_users: ['user_a', 'user_b', 'user_cc'], cc_users: [],
|
|
160
|
+
values: {}, current_step_name: '结束', final_decision: 'approved',
|
|
161
|
+
traces: [
|
|
162
|
+
{
|
|
163
|
+
_id: 't1', step: 'step_start', name: '填写申请单', is_finished: true, judge: 'submitted',
|
|
164
|
+
approves: [{ _id: 'a1', user: 'user_a', handler: 'user_a', type: 'draft', judge: 'submitted' }],
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
_id: 't2', step: 'step_real', name: '实际审批步骤', is_finished: true, judge: 'approved',
|
|
168
|
+
previous_trace_ids: ['t1'],
|
|
169
|
+
approves: t2Approves,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
_id: 't_end', step: 'step_end', name: '结束', is_finished: true,
|
|
173
|
+
previous_trace_ids: ['t2'],
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function callRetrieve(userId) {
|
|
180
|
+
instanceUpdateCalls = [];
|
|
181
|
+
objectqlUpdates.length = 0;
|
|
182
|
+
const req = {
|
|
183
|
+
user: { userId: userId, spaceId: 'space1', name: 'name_of_' + userId },
|
|
184
|
+
body: { _id: fakeInstance._id, retrieve_comment: '取回测试' },
|
|
185
|
+
};
|
|
186
|
+
const res = {
|
|
187
|
+
statusCode: null, body: null,
|
|
188
|
+
status: function (c) { this.statusCode = c; return this; },
|
|
189
|
+
send: function (b) { this.body = b; return this; },
|
|
190
|
+
};
|
|
191
|
+
await handler(req, res);
|
|
192
|
+
return res;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let passed = 0;
|
|
196
|
+
let failed = 0;
|
|
197
|
+
async function run(name, fn) {
|
|
198
|
+
try {
|
|
199
|
+
await fn();
|
|
200
|
+
console.log(` ✓ ${name}`);
|
|
201
|
+
passed++;
|
|
202
|
+
} catch (e) {
|
|
203
|
+
console.log(` ✗ ${name}`);
|
|
204
|
+
console.log(` ${e.message}`);
|
|
205
|
+
failed++;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
(async () => {
|
|
210
|
+
console.log('取回接口状态与资格校验(retrieve guards)');
|
|
211
|
+
|
|
212
|
+
await run('正常取回:上一步唯一处理人取回,行为不变', async () => {
|
|
213
|
+
stepTypes = {};
|
|
214
|
+
fakeInstance = buildPendingInstance();
|
|
215
|
+
const res = await callRetrieve('user_b');
|
|
216
|
+
assert.strictEqual(res.body.status, 0, '应返回 status 0,实际:' + JSON.stringify(res.body));
|
|
217
|
+
const setCall = instanceUpdateCalls.find(c => c.modifier.$set && c.modifier.$set.traces);
|
|
218
|
+
assert.ok(setCall, '未更新实例 traces');
|
|
219
|
+
const newTraces = setCall.modifier.$set.traces;
|
|
220
|
+
const newTrace = newTraces[newTraces.length - 1];
|
|
221
|
+
assert.strictEqual(newTrace.step, 'step_real', '新 trace 应落在上一步');
|
|
222
|
+
assert.deepStrictEqual(setCall.modifier.$set.inbox_users, ['user_b']);
|
|
223
|
+
assert.strictEqual(setCall.modifier.$set.state, 'pending');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
await run('下一步已读 → 报「下一步处理人已读,不能取回」', async () => {
|
|
227
|
+
stepTypes = {};
|
|
228
|
+
fakeInstance = buildPendingInstance({ nextStepRead: true });
|
|
229
|
+
const res = await callRetrieve('user_b');
|
|
230
|
+
assert.strictEqual(res.body.status, -1, '应返回 status -1,实际:' + JSON.stringify(res.body));
|
|
231
|
+
assert.ok(/已读/.test(res.body.msg), '应报已读错误,实际:' + res.body.msg);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await run('退回到开始步骤不受已读限制:申请人取回第一步已读的单子仍成功', async () => {
|
|
235
|
+
stepTypes = { step_start: 'start' };
|
|
236
|
+
fakeInstance = buildPendingInstance({ nextStepRead: true });
|
|
237
|
+
// 只保留 填写申请(t1) → 当前步骤(t2'),当前步骤已读,申请人 user_a 取回
|
|
238
|
+
fakeInstance.traces = [
|
|
239
|
+
fakeInstance.traces[0],
|
|
240
|
+
{
|
|
241
|
+
_id: 't2p', step: 'step_current', name: '当前步骤', is_finished: false,
|
|
242
|
+
previous_trace_ids: ['t1'],
|
|
243
|
+
approves: [{ _id: 'a9', user: 'user_c', handler: 'user_c', is_finished: false, is_read: true }],
|
|
244
|
+
},
|
|
245
|
+
];
|
|
246
|
+
const res = await callRetrieve('user_a');
|
|
247
|
+
assert.strictEqual(res.body.status, 0, '应返回 status 0,实际:' + JSON.stringify(res.body));
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await run('滑步后取回:回溯到实际处理过的步骤,中间 skipped trace 一并标记 retrieved', async () => {
|
|
251
|
+
stepTypes = {};
|
|
252
|
+
fakeInstance = buildPendingInstance();
|
|
253
|
+
// 在 t2 和 t3 之间插入被滑步跳过的 t_skip
|
|
254
|
+
fakeInstance.traces.splice(2, 0, {
|
|
255
|
+
_id: 't_skip', step: 'step_skipped', name: '被滑步跳过的步骤', is_finished: true, judge: 'skipped',
|
|
256
|
+
previous_trace_ids: ['t2'],
|
|
257
|
+
approves: [{ _id: 'a_skip', user: 'user_x', handler: 'user_x', judge: 'skipped' }],
|
|
258
|
+
});
|
|
259
|
+
fakeInstance.traces[3].previous_trace_ids = ['t_skip'];
|
|
260
|
+
const res = await callRetrieve('user_b');
|
|
261
|
+
assert.strictEqual(res.body.status, 0, '应返回 status 0,实际:' + JSON.stringify(res.body));
|
|
262
|
+
const setCall = instanceUpdateCalls.find(c => c.modifier.$set && c.modifier.$set.traces);
|
|
263
|
+
const newTraces = setCall.modifier.$set.traces;
|
|
264
|
+
assert.strictEqual(newTraces[newTraces.length - 1].step, 'step_real', '新 trace 应落在实际处理过的步骤');
|
|
265
|
+
const skipTrace = newTraces.find(t => t._id === 't_skip');
|
|
266
|
+
assert.strictEqual(skipTrace.judge, 'retrieved', '滑步跳过的 trace 应标记为 retrieved');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
await run('当前用户不是上一步唯一处理人 → 明确报错,不再静默假成功', async () => {
|
|
270
|
+
stepTypes = {};
|
|
271
|
+
fakeInstance = buildPendingInstance();
|
|
272
|
+
// user_a 是提交人且在 outbox_users 中,但上一步(t2)处理人是 user_b
|
|
273
|
+
const res = await callRetrieve('user_a');
|
|
274
|
+
assert.strictEqual(res.body.status, -1, '应返回 status -1,实际:' + JSON.stringify(res.body));
|
|
275
|
+
assert.ok(res.body.msg, '应返回错误信息');
|
|
276
|
+
assert.strictEqual(instanceUpdateCalls.length, 0, '不应更新实例');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
await run('已结束实例 → 最后一步处理人取回被拒绝,实例不被重新打开', async () => {
|
|
280
|
+
stepTypes = {};
|
|
281
|
+
fakeInstance = buildCompletedInstance();
|
|
282
|
+
const res = await callRetrieve('user_b');
|
|
283
|
+
assert.strictEqual(res.body.status, -1, '应返回 status -1,实际:' + JSON.stringify(res.body));
|
|
284
|
+
assert.ok(/已结束/.test(res.body.msg), '应报申请单已结束,实际:' + res.body.msg);
|
|
285
|
+
assert.strictEqual(instanceUpdateCalls.length, 0, '不应更新实例(不能重新打开已结束的申请单)');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await run('已结束实例 → 传阅取回同样被拒绝', async () => {
|
|
289
|
+
stepTypes = {};
|
|
290
|
+
fakeInstance = buildCompletedInstance({ withCC: true });
|
|
291
|
+
const res = await callRetrieve('user_cc');
|
|
292
|
+
assert.strictEqual(res.body.status, -1, '应返回 status -1,实际:' + JSON.stringify(res.body));
|
|
293
|
+
assert.ok(/已结束/.test(res.body.msg), '应报申请单已结束,实际:' + res.body.msg);
|
|
294
|
+
assert.strictEqual(instanceUpdateCalls.length, 0, '不应更新实例');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
await run('草稿状态 → 拒绝取回', async () => {
|
|
298
|
+
stepTypes = {};
|
|
299
|
+
fakeInstance = buildPendingInstance();
|
|
300
|
+
fakeInstance.state = 'draft';
|
|
301
|
+
const res = await callRetrieve('user_a');
|
|
302
|
+
assert.strictEqual(res.body.status, -1, '应返回 status -1,实际:' + JSON.stringify(res.body));
|
|
303
|
+
assert.ok(/草稿/.test(res.body.msg), '应报草稿不能取回,实际:' + res.body.msg);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
307
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
308
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steedos-labs/plugin-workflow",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.105",
|
|
4
4
|
"main": "package.service.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
"test:approve-values": "node main/default/test/test_getApproveValues.js",
|
|
28
28
|
"test:skip-handler-error": "node main/default/test/test_handleSkipProcessed_handlerError.js",
|
|
29
29
|
"test:return-after-skip": "node main/default/test/test_return_after_skip.js",
|
|
30
|
+
"test:retrieve-guards": "node main/default/test/test_retrieve_guards.js",
|
|
31
|
+
"test:remove-guards": "node main/default/test/test_remove_guards.js",
|
|
30
32
|
"test:next-step-users-cc": "node main/default/test/test_next_step_users_value_cc.js",
|
|
31
33
|
"test:flow-validator": "node main/default/test/test_flow_validator.js",
|
|
32
34
|
"test:ajax-error-message": "node main/default/test/test_ajax_error_message.js",
|