Changeset 0.29.3 (#198)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit is contained in:
@@ -399,5 +399,205 @@
|
||||
}
|
||||
})();
|
||||
|
||||
// ── Form Submission Lifecycle (v0.29.3) ──────────────────
|
||||
// Tests typed form_template with stage_mode, form validation,
|
||||
// and the /w/:id/form-submit endpoint.
|
||||
if (T.user.role === 'admin') {
|
||||
await (async function () {
|
||||
var fwfId = null;
|
||||
var fwfSlug = testTag + '-form-wf';
|
||||
var fChannelId = null;
|
||||
|
||||
await T.test('crud', 'workflows', 'form: create form_only workflow', async function () {
|
||||
var wf = await T.apiPost('/workflows', {
|
||||
name: testTag + ' Form WF',
|
||||
slug: fwfSlug,
|
||||
entry_mode: 'public_link'
|
||||
});
|
||||
T.assertShape(wf, T.S.workflow, 'form workflow');
|
||||
fwfId = wf.id;
|
||||
T.registerCleanup(function () { if (fwfId) return T.safeDelete('/workflows/' + fwfId); });
|
||||
|
||||
await T.apiPost('/workflows/' + fwfId + '/stages', {
|
||||
name: 'Contact Info',
|
||||
ordinal: 0,
|
||||
history_mode: 'full',
|
||||
stage_mode: 'form_only',
|
||||
form_template: {
|
||||
fields: [
|
||||
{ key: 'name', type: 'text', label: 'Full Name', required: true, validation: { min_length: 2 } },
|
||||
{ key: 'email', type: 'email', label: 'Email', required: true },
|
||||
{ key: 'dept', type: 'select', label: 'Department', options: [
|
||||
{ value: 'eng', label: 'Engineering' },
|
||||
{ value: 'sales', label: 'Sales' }
|
||||
]}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
await T.apiPost('/workflows/' + fwfId + '/stages', {
|
||||
name: 'Done', ordinal: 1, history_mode: 'full', stage_mode: 'chat_only'
|
||||
});
|
||||
|
||||
await T.apiPatch('/workflows/' + fwfId, { is_active: true });
|
||||
await T.apiPost('/workflows/' + fwfId + '/publish', {});
|
||||
});
|
||||
|
||||
if (fwfId) {
|
||||
await T.test('crud', 'workflows', 'form: start instance', async function () {
|
||||
var inst = await T.apiPost('/workflows/' + fwfId + '/start', {});
|
||||
fChannelId = inst.channel_id || inst.id;
|
||||
T.assert(fChannelId, 'no channel_id in form start');
|
||||
T.registerCleanup(function () { if (fChannelId) return T.safeDelete('/channels/' + fChannelId); });
|
||||
});
|
||||
|
||||
if (fChannelId) {
|
||||
await T.test('crud', 'workflows', 'form: GET /w/:id/form (template)', async function () {
|
||||
var d = await T.sessionGet('/w/' + fChannelId + '/form');
|
||||
T.assert(d._status === 200, 'expected 200, got ' + d._status);
|
||||
T.assertHasKey(d, 'stage_mode', 'form response');
|
||||
T.assert(d.stage_mode === 'form_only', 'expected form_only, got ' + d.stage_mode);
|
||||
T.assertHasKey(d, 'form_template', 'form response');
|
||||
T.assert(d.form_template.fields && d.form_template.fields.length === 3,
|
||||
'expected 3 fields, got ' + (d.form_template.fields ? d.form_template.fields.length : 0));
|
||||
});
|
||||
|
||||
await T.test('crud', 'workflows', 'form: submit missing required → 400', async function () {
|
||||
var d = await T.sessionPost('/w/' + fChannelId + '/form-submit', {
|
||||
dept: 'eng'
|
||||
});
|
||||
T.assert(d._status === 400, 'expected 400, got ' + d._status);
|
||||
T.assertHasKey(d, 'errors', 'validation response');
|
||||
T.assert(Array.isArray(d.errors) && d.errors.length > 0, 'expected field errors');
|
||||
});
|
||||
|
||||
await T.test('crud', 'workflows', 'form: submit invalid email → 400', async function () {
|
||||
var d = await T.sessionPost('/w/' + fChannelId + '/form-submit', {
|
||||
name: 'Test User',
|
||||
email: 'not-an-email'
|
||||
});
|
||||
T.assert(d._status === 400, 'expected 400, got ' + d._status);
|
||||
T.assertHasKey(d, 'errors', 'validation response');
|
||||
var emailErr = d.errors.find(function (e) { return e.key === 'email'; });
|
||||
T.assert(emailErr, 'expected email validation error');
|
||||
});
|
||||
|
||||
await T.test('crud', 'workflows', 'form: submit valid → 200', async function () {
|
||||
var d = await T.sessionPost('/w/' + fChannelId + '/form-submit', {
|
||||
name: 'ICD Test User',
|
||||
email: 'test@icd.local',
|
||||
dept: 'eng'
|
||||
});
|
||||
T.assert(d._status === 200, 'expected 200, got ' + d._status);
|
||||
});
|
||||
|
||||
await T.test('crud', 'workflows', 'form: verify stage_data after submit', async function () {
|
||||
var d = await T.apiGet('/channels/' + fChannelId + '/workflow/status');
|
||||
T.assertShape(d, T.S.workflowStatus, 'status');
|
||||
// stage_data should contain submitted form values
|
||||
if (d.stage_data) {
|
||||
var sd = typeof d.stage_data === 'string' ? JSON.parse(d.stage_data) : d.stage_data;
|
||||
T.assert(sd.name === 'ICD Test User' || sd.form_name === 'ICD Test User',
|
||||
'stage_data should contain form values');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await T.test('crud', 'workflows', 'form: cleanup', async function () {
|
||||
if (fChannelId) { await T.safeDelete('/channels/' + fChannelId); fChannelId = null; }
|
||||
if (fwfId) { await T.safeDelete('/workflows/' + fwfId); fwfId = null; }
|
||||
});
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
// ── Cross-Visitor Isolation (v0.29.3) ─────────────────────
|
||||
// Two instances of the same workflow should have isolated stage_data
|
||||
// and one visitor cannot access the other's form endpoint.
|
||||
if (T.user.role === 'admin') {
|
||||
await (async function () {
|
||||
var xwfId = null;
|
||||
var xwfSlug = testTag + '-xvisitor';
|
||||
var chA = null, chB = null;
|
||||
|
||||
await T.test('crud', 'workflows', 'xvisitor: setup form_only workflow', async function () {
|
||||
var wf = await T.apiPost('/workflows', {
|
||||
name: testTag + ' XVisitor',
|
||||
slug: xwfSlug,
|
||||
entry_mode: 'public_link'
|
||||
});
|
||||
xwfId = wf.id;
|
||||
T.registerCleanup(function () { if (xwfId) return T.safeDelete('/workflows/' + xwfId); });
|
||||
|
||||
await T.apiPost('/workflows/' + xwfId + '/stages', {
|
||||
name: 'Form', ordinal: 0, stage_mode: 'form_only', history_mode: 'full',
|
||||
form_template: {
|
||||
fields: [{ key: 'name', type: 'text', label: 'Name', required: true }]
|
||||
}
|
||||
});
|
||||
await T.apiPost('/workflows/' + xwfId + '/stages', {
|
||||
name: 'Done', ordinal: 1, history_mode: 'full'
|
||||
});
|
||||
await T.apiPatch('/workflows/' + xwfId, { is_active: true });
|
||||
await T.apiPost('/workflows/' + xwfId + '/publish', {});
|
||||
});
|
||||
|
||||
if (xwfId) {
|
||||
await T.test('crud', 'workflows', 'xvisitor: start two instances', async function () {
|
||||
var a = await T.apiPost('/workflows/' + xwfId + '/start', {});
|
||||
chA = a.channel_id || a.id;
|
||||
T.assert(chA, 'no channel A');
|
||||
T.registerCleanup(function () { if (chA) return T.safeDelete('/channels/' + chA); });
|
||||
|
||||
var b = await T.apiPost('/workflows/' + xwfId + '/start', {});
|
||||
chB = b.channel_id || b.id;
|
||||
T.assert(chB, 'no channel B');
|
||||
T.registerCleanup(function () { if (chB) return T.safeDelete('/channels/' + chB); });
|
||||
|
||||
T.assert(chA !== chB, 'channels should be different');
|
||||
});
|
||||
|
||||
if (chA && chB) {
|
||||
await T.test('crud', 'workflows', 'xvisitor: submit different data to each', async function () {
|
||||
// Submit to A (admin token)
|
||||
var dA = await T.apiPost('/channels/' + chA + '/workflow/advance', {
|
||||
data: { name: 'Alice' }
|
||||
});
|
||||
T.assert(typeof dA === 'object', 'advance A');
|
||||
|
||||
// Submit to B
|
||||
var dB = await T.apiPost('/channels/' + chB + '/workflow/advance', {
|
||||
data: { name: 'Bob' }
|
||||
});
|
||||
T.assert(typeof dB === 'object', 'advance B');
|
||||
});
|
||||
|
||||
await T.test('crud', 'workflows', 'xvisitor: verify stage_data isolated', async function () {
|
||||
var stA = await T.apiGet('/channels/' + chA + '/workflow/status');
|
||||
var stB = await T.apiGet('/channels/' + chB + '/workflow/status');
|
||||
|
||||
// Parse stage_data
|
||||
var sdA = stA.stage_data;
|
||||
if (typeof sdA === 'string') sdA = JSON.parse(sdA);
|
||||
var sdB = stB.stage_data;
|
||||
if (typeof sdB === 'string') sdB = JSON.parse(sdB);
|
||||
|
||||
// A should have Alice, B should have Bob
|
||||
var aName = sdA && (sdA.name || sdA.form_name);
|
||||
var bName = sdB && (sdB.name || sdB.form_name);
|
||||
T.assert(aName === 'Alice', 'channel A stage_data should have Alice, got ' + aName);
|
||||
T.assert(bName === 'Bob', 'channel B stage_data should have Bob, got ' + bName);
|
||||
});
|
||||
}
|
||||
|
||||
await T.test('crud', 'workflows', 'xvisitor: cleanup', async function () {
|
||||
if (chA) { await T.safeDelete('/channels/' + chA); chA = null; }
|
||||
if (chB) { await T.safeDelete('/channels/' + chB); chB = null; }
|
||||
if (xwfId) { await T.safeDelete('/workflows/' + xwfId); xwfId = null; }
|
||||
});
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user