/** * ICD Test Runner — CRUD: Workspaces * Workspace lifecycle — create, read, update, file ops, stats, isolation, delete. * Git credentials list envelope. */ (function () { 'use strict'; var T = window.ICD; if (!T) return; if (!T.crud) T.crud = {}; T.crud.workspaces = async function (testTag) { // ── GET /workspaces — list envelope ── await T.test('crud', 'workspaces', 'GET /workspaces (list envelope)', async function () { var d = await T.apiGet('/workspaces'); T.assertHasKey(d, 'data', 'GET /workspaces'); T.assert(Array.isArray(d.data), 'data must be array'); }); // ── GET /git-credentials — list envelope ── await T.test('crud', 'workspaces', 'GET /git-credentials (list envelope)', async function () { var d = await T.apiGet('/git-credentials'); T.assertHasKey(d, 'data', 'GET /git-credentials'); T.assert(Array.isArray(d.data), 'data must be array'); }); // ── Workspace CRUD ── var wsId = null; await T.test('crud', 'workspaces', 'POST /workspaces (create)', async function () { var d = await T.apiPost('/workspaces', { name: testTag + '-ws', owner_type: 'user', owner_id: T.user.id }); T.assertShape(d, T.S.workspace, 'workspace'); T.assert(d.owner_id === T.user.id, 'owner_id mismatch'); T.assert(d.status === 'active', 'status should be active'); wsId = d.id; T.registerCleanup(function () { if (wsId) return T.safeDelete('/workspaces/' + wsId); }); }); if (wsId) { await T.test('crud', 'workspaces', 'GET /workspaces/:id (read)', async function () { var d = await T.apiGet('/workspaces/' + wsId); T.assertShape(d, T.S.workspace, 'workspace'); T.assert(d.id === wsId, 'id mismatch'); T.assert(d.name === testTag + '-ws', 'name mismatch'); }); // ── PATCH /workspaces/:id ── await T.test('crud', 'workspaces', 'PATCH /workspaces/:id (update name)', async function () { var d = await T.apiPatch('/workspaces/' + wsId, { name: testTag + '-ws-updated' }); T.assertShape(d, T.S.workspace, 'workspace-patched'); T.assert(d.name === testTag + '-ws-updated', 'name not updated'); }); // ── root_path must never be exposed ── await T.test('crud', 'workspaces', 'GET /workspaces/:id (no root_path)', async function () { var d = await T.apiGet('/workspaces/' + wsId); T.assert(d.root_path === undefined, 'root_path must not be exposed in API'); }); // ── File operations ── await T.test('crud', 'workspaces', 'GET /workspaces/:id/files (list root)', async function () { var d = await T.apiGet('/workspaces/' + wsId + '/files?path=/'); T.assertHasKey(d, 'data', 'ListFiles envelope'); T.assert(Array.isArray(d.data), 'files should be array'); }); // mkdir await T.test('crud', 'workspaces', 'POST /workspaces/:id/files/mkdir', async function () { var d = await T.apiPost('/workspaces/' + wsId + '/files/mkdir?path=/testdir', {}); T.assert(d.ok === true, 'mkdir should return ok:true'); T.assert(d.path === '/testdir', 'path mismatch'); }); // Write a file then read it back await T.test('crud', 'workspaces', 'PUT /workspaces/:id/files/write', async function () { var d = await T.apiPut('/workspaces/' + wsId + '/files/write?path=' + encodeURIComponent('/test.txt'), { content: 'Hello from ICD test runner' }); T.assert(typeof d === 'object', 'expected object'); }); await T.test('crud', 'workspaces', 'GET /workspaces/:id/files/read', async function () { var d = await T.apiGet('/workspaces/' + wsId + '/files/read?path=/test.txt'); T.assertHasKey(d, 'content', '/ws-file-read'); T.assert(d.content === 'Hello from ICD test runner', 'content mismatch'); }); await T.test('crud', 'workspaces', 'GET /workspaces/:id/stats', async function () { var d = await T.apiGet('/workspaces/' + wsId + '/stats'); T.assert(typeof d === 'object', 'expected object'); T.assertHasKey(d, 'file_count', 'stats.file_count'); T.assertHasKey(d, 'total_bytes', 'stats.total_bytes'); }); await T.test('crud', 'workspaces', 'GET /workspaces/:id/index-status', async function () { var d = await T.apiGet('/workspaces/' + wsId + '/index-status'); T.assertHasKey(d, 'workspace_id', 'index-status.workspace_id'); T.assertHasKey(d, 'status_counts', 'index-status.status_counts'); T.assertHasKey(d, 'total_chunks', 'index-status.total_chunks'); }); // ── File cleanup ── await T.test('crud', 'workspaces', 'DELETE /workspaces/:id/files (file)', async function () { try { await T.safeDelete('/workspaces/' + wsId + '/files/delete?path=' + encodeURIComponent('/test.txt')); } catch (e) { if (e.message && e.message.indexOf('404') === -1) throw e; } }); await T.test('crud', 'workspaces', 'DELETE /workspaces/:id', async function () { await T.safeDelete('/workspaces/' + wsId); wsId = null; }); } // ── Workspace isolation: other user cannot access ── // Create workspace as running admin user, try to access from fixture regular user if (T.fixtures.ready) { var fixtureUser = T.getFixtureUser('-user'); if (fixtureUser && fixtureUser.token) { var isolWsId = null; await T.test('crud', 'workspaces', 'isolation: create admin workspace', async function () { var d = await T.apiPost('/workspaces', { name: testTag + '-iso-ws', owner_type: 'user', owner_id: T.user.id }); T.assertShape(d, T.S.workspace, 'workspace-iso'); isolWsId = d.id; T.registerCleanup(function () { if (isolWsId) return T.safeDelete('/workspaces/' + isolWsId); }); }); if (isolWsId) { await T.test('crud', 'workspaces', 'isolation: user cannot GET other workspace', async function () { var d = await T.authFetch(fixtureUser.token, 'GET', '/workspaces/' + isolWsId); T.assert(d._status === 403, 'expected 403, got ' + d._status); }); // cleanup await T.test('crud', 'workspaces', 'isolation: cleanup admin workspace', async function () { await T.safeDelete('/workspaces/' + isolWsId); isolWsId = null; }); } } } }; })();