156 lines
6.5 KiB
JavaScript
156 lines
6.5 KiB
JavaScript
/**
|
|
* 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;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
};
|
|
})();
|