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/workspaces.js
2026-03-15 15:45:00 +00:00

199 lines
8.8 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');
});
// ── POST /git-credentials/generate — server-side keygen (v0.28.6) ──
var generatedKeyId = null;
await T.test('crud', 'workspaces', 'POST /git-credentials/generate', async function () {
var d = await T.apiPost('/git-credentials/generate', { name: 'ICD Test Key' });
T.assert(d.id, 'should return id');
T.assert(d.auth_type === 'ssh_key', 'auth_type should be ssh_key');
T.assert(d.public_key && d.public_key.length > 0, 'should return public_key');
T.assert(d.fingerprint && d.fingerprint.startsWith('SHA256:'), 'should return SHA256 fingerprint');
T.assert(!d.encrypted_data, 'must NOT expose encrypted_data');
T.assert(!d.nonce, 'must NOT expose nonce');
generatedKeyId = d.id;
T.registerCleanup(function () { if (generatedKeyId) return T.safeDelete('/git-credentials/' + generatedKeyId); });
});
if (generatedKeyId) {
await T.test('crud', 'workspaces', 'GET /git-credentials/:id/public-key', async function () {
var d = await T.apiGet('/git-credentials/' + generatedKeyId + '/public-key');
T.assertHasKey(d, 'public_key', 'public-key response');
T.assertHasKey(d, 'fingerprint', 'public-key response');
T.assert(d.public_key.length > 0, 'public_key should be non-empty');
});
await T.test('crud', 'workspaces', 'GET /git-credentials (list after generate)', async function () {
var d = await T.apiGet('/git-credentials');
var found = (d.data || []).find(function (c) { return c.id === generatedKeyId; });
T.assert(found, 'generated key should appear in list');
T.assert(found.public_key, 'list item should have public_key');
T.assert(found.fingerprint, 'list item should have fingerprint');
});
await T.test('crud', 'workspaces', 'DELETE /git-credentials/:id', async function () {
var d = await T.apiDelete('/git-credentials/' + generatedKeyId);
T.assert(d.deleted === true, 'should return deleted: true');
generatedKeyId = null;
});
}
await T.test('crud', 'workspaces', 'POST /git-credentials/generate (missing name → 400)', async function () {
var token = await T.getAuthToken();
var d = await T.authFetch(token, 'POST', '/git-credentials/generate', {});
T.assertStatus(d, 400, 'missing name');
});
// ── 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;
});
}
}
}
};
})();