This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/packages/icd-test-runner/js/crud/team-workflows.js
2026-03-19 13:29:27 +00:00

180 lines
7.0 KiB
JavaScript

/**
* ICD Test Runner — CRUD: Team Workflows (v0.31.2)
* Team-scoped workflow lifecycle via /teams/:teamId/workflows routes.
* Tests ownership isolation: workflows created via team routes are
* accessible only through team routes, and cross-team access is denied.
*/
(function () {
'use strict';
var T = window.ICD;
if (!T) return;
if (!T.crud) T.crud = {};
T.crud.teamWorkflows = async function (testTag) {
// Requires admin user (who is also team admin for fixtures.team)
if (T.user.role !== 'admin') return;
var team = T.fixtures && T.fixtures.team;
if (!team) {
await T.test('crud', 'team-workflows', 'SKIP — no fixture team', async function () {
T.assert(false, 'fixtures.team not provisioned — run fixtures first');
});
return;
}
var teamId = team.id;
var wfId = null;
var wfSlug = testTag.toLowerCase().replace(/[^a-z0-9-]/g, '-') + '-twf';
// ── Create via team route ──
await T.test('crud', 'team-workflows', 'POST /teams/:teamId/workflows (create)', async function () {
var d = await T.apiPost('/teams/' + teamId + '/workflows', {
name: testTag + '-team-workflow',
slug: wfSlug,
description: 'Team workflow CRUD test',
entry_mode: 'team_only'
});
T.assertShape(d, T.S.workflow, 'team workflow');
T.assert(d.slug === wfSlug, 'slug mismatch: expected ' + wfSlug + ', got ' + d.slug);
T.assert(d.team_id === teamId, 'team_id should be ' + teamId + ', got ' + d.team_id);
wfId = d.id;
T.registerCleanup(function () { if (wfId) return T.safeDelete('/workflows/' + wfId); });
});
if (!wfId) return;
// ── Read via team route ──
await T.test('crud', 'team-workflows', 'GET /teams/:teamId/workflows/:id (read)', async function () {
var d = await T.apiGet('/teams/' + teamId + '/workflows/' + wfId);
T.assertShape(d, T.S.workflow, 'team workflow');
T.assert(d.id === wfId, 'id mismatch');
T.assert(d.team_id === teamId, 'team_id mismatch');
});
// ── List via team route ──
await T.test('crud', 'team-workflows', 'GET /teams/:teamId/workflows (list)', async function () {
var d = await T.apiGet('/teams/' + teamId + '/workflows');
T.assertHasKey(d, 'data', '/workflows');
T.assert(Array.isArray(d.data), 'data should be array');
var found = d.data.find(function (w) { return w.id === wfId; });
T.assert(found, 'team workflow should appear in team list');
});
// ── Update via team route ──
await T.test('crud', 'team-workflows', 'PATCH /teams/:teamId/workflows/:id (update)', async function () {
var d = await T.apiPatch('/teams/' + teamId + '/workflows/' + wfId, {
description: 'Updated by team admin',
entry_mode: 'public_link'
});
T.assert(typeof d === 'object', 'expected object');
});
// ── Stage CRUD via team routes ──
var stageIds = [];
await T.test('crud', 'team-workflows', 'POST .../stages (create stage #1)', async function () {
var d = await T.apiPost('/teams/' + teamId + '/workflows/' + wfId + '/stages', {
name: 'Team Intake',
ordinal: 0,
history_mode: 'full',
stage_mode: 'chat_only'
});
T.assertShape(d, T.S.workflowStage, 'stage');
T.assert(d.name === 'Team Intake', 'name mismatch');
stageIds.push(d.id);
});
await T.test('crud', 'team-workflows', 'POST .../stages (create stage #2)', async function () {
var d = await T.apiPost('/teams/' + teamId + '/workflows/' + wfId + '/stages', {
name: 'Team Review',
ordinal: 1,
history_mode: 'summary',
stage_mode: 'review'
});
T.assertShape(d, T.S.workflowStage, 'stage');
stageIds.push(d.id);
});
await T.test('crud', 'team-workflows', 'GET .../stages (list)', async function () {
var d = await T.apiGet('/teams/' + teamId + '/workflows/' + wfId + '/stages');
T.assertHasKey(d, 'data', '/stages');
T.assert(d.data.length >= 2, 'expected at least 2 stages, got ' + d.data.length);
});
await T.test('crud', 'team-workflows', 'PUT .../stages/:sid (update)', async function () {
var d = await T.apiPut('/teams/' + teamId + '/workflows/' + wfId + '/stages/' + stageIds[0], {
name: 'Team Intake (updated)',
ordinal: 0,
history_mode: 'full',
stage_mode: 'form_chat',
form_template: { fields: [{ key: 'name', type: 'text', label: 'Name', required: true }] }
});
T.assert(typeof d === 'object', 'expected object');
});
if (stageIds.length >= 2) {
await T.test('crud', 'team-workflows', 'PATCH .../stages/reorder', async function () {
var reversed = stageIds.slice().reverse();
var d = await T.apiPatch('/teams/' + teamId + '/workflows/' + wfId + '/stages/reorder', {
ordered_ids: reversed
});
T.assert(typeof d === 'object', 'expected object');
});
// Restore order
await T.apiPatch('/teams/' + teamId + '/workflows/' + wfId + '/stages/reorder', {
ordered_ids: stageIds
});
}
// ── Activate + Publish via team route ──
var versionNum = null;
await T.test('crud', 'team-workflows', 'PATCH activate + POST publish', async function () {
await T.apiPatch('/teams/' + teamId + '/workflows/' + wfId, { is_active: true });
var v = await T.apiPost('/teams/' + teamId + '/workflows/' + wfId + '/publish', {});
T.assertShape(v, T.S.workflowVersion, 'version');
T.assertHasKey(v, 'snapshot', 'version');
versionNum = v.version_number;
});
await T.test('crud', 'team-workflows', 'GET .../versions/:v (read)', async function () {
T.assert(versionNum !== null, 'no version number from publish');
var d = await T.apiGet('/teams/' + teamId + '/workflows/' + wfId + '/versions/' + versionNum);
T.assertShape(d, T.S.workflowVersion, 'version');
T.assert(d.version_number === versionNum, 'version should be ' + versionNum);
});
// ── Ownership isolation: cross-team access denied ──
await T.test('crud', 'team-workflows', 'GET /teams/fake-id/workflows/:id → 403', async function () {
var d = await T.authFetch(await T.getAuthToken(), 'GET',
'/teams/00000000-0000-0000-0000-000000000099/workflows/' + wfId, null);
// Should be 403 (not a team member) or 404
T.assert(d._status === 403 || d._status === 404,
'cross-team access should be denied, got ' + d._status);
});
// ── Stage delete via team route ──
if (stageIds.length >= 2) {
await T.test('crud', 'team-workflows', 'DELETE .../stages/:sid', async function () {
await T.apiDelete('/teams/' + teamId + '/workflows/' + wfId + '/stages/' + stageIds.pop());
});
}
// ── Workflow delete via team route ──
await T.test('crud', 'team-workflows', 'DELETE /teams/:teamId/workflows/:id', async function () {
await T.apiDelete('/teams/' + teamId + '/workflows/' + wfId);
wfId = null;
});
};
})();