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/surfaces/icd-test-runner/js/crud/projects.js
2026-03-14 19:36:33 +00:00

195 lines
9.5 KiB
JavaScript

/**
* ICD Test Runner — CRUD: Projects
* Full project lifecycle — CRUD, channel/KB/note associations, files,
* isolation, and admin list.
*/
(function () {
'use strict';
var T = window.ICD;
if (!T) return;
if (!T.crud) T.crud = {};
T.crud.projects = async function (testTag) {
// ── Create ──
var projectId = null;
await T.test('crud', 'projects', 'POST /projects (create, 201 + shape)', async function () {
var token = await T.getAuthToken();
var d = await T.authFetch(token, 'POST', '/projects', {
name: testTag + '-project',
description: 'ICD integration test project',
color: '#3b82f6',
icon: 'folder'
});
T.assertStatus(d, 201, 'POST /projects');
T.assertShape(d, T.S.project, 'project');
T.assert(d.scope === 'personal', 'scope forced to personal, got ' + d.scope);
T.assert(typeof d.owner_id === 'string' && d.owner_id.length > 0, 'owner_id must be set');
projectId = d.id;
T.registerCleanup(function () { if (projectId) return T.safeDelete('/projects/' + projectId); });
});
if (!projectId) return;
// ── Read ──
await T.test('crud', 'projects', 'GET /projects/:id (read, shape)', async function () {
var d = await T.apiGet('/projects/' + projectId);
T.assertShape(d, T.S.project, 'project');
T.assert(d.id === projectId, 'id mismatch');
T.assert(d.name === testTag + '-project', 'name mismatch');
T.assert(d.description === 'ICD integration test project', 'description mismatch');
});
// ── Update ──
await T.test('crud', 'projects', 'PUT /projects/:id (update, returns refreshed)', async function () {
var d = await T.apiPut('/projects/' + projectId, { name: testTag + '-proj-updated', is_archived: false });
T.assertShape(d, T.S.project, 'project');
T.assert(d.name === testTag + '-proj-updated', 'name not updated');
T.assert(d.id === projectId, 'id changed unexpectedly');
});
// ── List envelope ──
await T.test('crud', 'projects', 'GET /projects (list envelope)', async function () {
var d = await T.apiGet('/projects');
T.assertHasKey(d, 'data', '/projects');
T.assert(Array.isArray(d.data), 'data must be array');
var found = d.data.some(function (p) { return p.id === projectId; });
T.assert(found, 'created project not in list');
});
// ── Channel association ──
var channelId = null;
await T.test('crud', 'projects', 'POST /projects/:id/channels (add channel)', async function () {
// Create a scratch channel for association testing
var ch = await T.apiPost('/channels', { title: testTag + '-proj-ch', type: 'direct' });
channelId = ch.id;
T.registerCleanup(function () { if (channelId) return T.safeDelete('/channels/' + channelId); });
var d = await T.apiPost('/projects/' + projectId + '/channels', { channel_id: channelId, position: 0 });
T.assert(typeof d === 'object', 'expected object response');
});
await T.test('crud', 'projects', 'GET /projects/:id/channels (list, has added)', async function () {
var d = await T.apiGet('/projects/' + projectId + '/channels');
T.assertHasKey(d, 'data', '/project-channels');
T.assert(Array.isArray(d.data), 'data must be array');
T.assert(d.data.length >= 1, 'expected at least 1 channel association');
var entry = d.data[0];
T.assert(typeof entry.channel_id === 'string', 'missing channel_id');
T.assert(typeof entry.project_id === 'string', 'missing project_id');
T.assert(typeof entry.added_at === 'string', 'missing added_at');
});
await T.test('crud', 'projects', 'DELETE /projects/:id/channels/:channelId (remove)', async function () {
await T.apiDelete('/projects/' + projectId + '/channels/' + channelId);
var d = await T.apiGet('/projects/' + projectId + '/channels');
T.assert(d.data.length === 0, 'channel should be removed');
});
// ── KB association ──
var kbId = null;
await T.test('crud', 'projects', 'POST /projects/:id/knowledge-bases (add KB)', async function () {
// Create a scratch KB
var kb = await T.apiPost('/knowledge-bases', { name: testTag + '-proj-kb', scope: 'personal' });
kbId = kb.id;
T.registerCleanup(function () { if (kbId) return T.safeDelete('/knowledge-bases/' + kbId); });
var d = await T.apiPost('/projects/' + projectId + '/knowledge-bases', { kb_id: kbId, auto_search: true });
T.assert(typeof d === 'object', 'expected object response');
});
await T.test('crud', 'projects', 'GET /projects/:id/knowledge-bases (list, has added)', async function () {
var d = await T.apiGet('/projects/' + projectId + '/knowledge-bases');
T.assertHasKey(d, 'data', '/project-kbs');
T.assert(d.data.length >= 1, 'expected at least 1 KB association');
var entry = d.data[0];
T.assert(typeof entry.kb_id === 'string', 'missing kb_id');
T.assert(typeof entry.project_id === 'string', 'missing project_id');
T.assert(typeof entry.added_at === 'string', 'missing added_at');
});
await T.test('crud', 'projects', 'DELETE /projects/:id/knowledge-bases/:kbId (remove)', async function () {
await T.apiDelete('/projects/' + projectId + '/knowledge-bases/' + kbId);
var d = await T.apiGet('/projects/' + projectId + '/knowledge-bases');
T.assert(d.data.length === 0, 'KB should be removed');
});
// ── Note association ──
var noteId = null;
await T.test('crud', 'projects', 'POST /projects/:id/notes (add note)', async function () {
// Create a scratch note
var n = await T.apiPost('/notes', { title: testTag + '-proj-note', content: 'test' });
noteId = n.id;
T.registerCleanup(function () { if (noteId) return T.safeDelete('/notes/' + noteId); });
var d = await T.apiPost('/projects/' + projectId + '/notes', { note_id: noteId });
T.assert(typeof d === 'object', 'expected object response');
});
await T.test('crud', 'projects', 'GET /projects/:id/notes (list, has added)', async function () {
var d = await T.apiGet('/projects/' + projectId + '/notes');
T.assertHasKey(d, 'data', '/project-notes');
T.assert(d.data.length >= 1, 'expected at least 1 note association');
var entry = d.data[0];
T.assert(typeof entry.note_id === 'string', 'missing note_id');
T.assert(typeof entry.project_id === 'string', 'missing project_id');
T.assert(typeof entry.added_at === 'string', 'missing added_at');
});
await T.test('crud', 'projects', 'DELETE /projects/:id/notes/:noteId (remove)', async function () {
await T.apiDelete('/projects/' + projectId + '/notes/' + noteId);
var d = await T.apiGet('/projects/' + projectId + '/notes');
T.assert(d.data.length === 0, 'note should be removed');
});
// ── Files (shape only — no upload in runner) ──
await T.test('crud', 'projects', 'GET /projects/:id/files (files key + count)', async function () {
var d = await T.apiGet('/projects/' + projectId + '/files');
T.assertHasKey(d, 'files', '/project-files');
T.assertHasKey(d, 'count', '/project-files');
T.assert(Array.isArray(d.files), 'files must be array');
T.assert(typeof d.count === 'number', 'count must be number');
});
// ── Isolation ──
await T.test('crud', 'projects', 'isolation: other user cannot GET personal project', async function () {
var other = T.getFixtureUser('-user');
if (!other || !other.token) { T.assert(true, 'skip — no fixture user'); return; }
var d = await T.authFetch(other.token, 'GET', '/projects/' + projectId);
T.assert(d._status === 404, 'expected 404 for other user, got ' + d._status);
});
await T.test('crud', 'projects', 'isolation: other user cannot DELETE personal project', async function () {
var other = T.getFixtureUser('-user');
if (!other || !other.token) { T.assert(true, 'skip — no fixture user'); return; }
var d = await T.authFetch(other.token, 'DELETE', '/projects/' + projectId);
T.assert(d._status === 404, 'expected 404 for other user DELETE, got ' + d._status);
});
// ── Admin list ──
await T.test('crud', 'projects', 'GET /admin/projects (envelope + owner_name)', async function () {
var d = await T.apiGet('/admin/projects');
T.assertHasKey(d, 'data', '/admin/projects');
T.assert(Array.isArray(d.data), 'data must be array');
var found = d.data.find(function (p) { return p.id === projectId; });
T.assert(found, 'created project not in admin list');
T.assert(typeof found.owner_name === 'string', 'admin list must include owner_name');
T.assert(typeof found.channel_count === 'number', 'admin list must include channel_count');
T.assert(typeof found.kb_count === 'number', 'admin list must include kb_count');
T.assert(typeof found.note_count === 'number', 'admin list must include note_count');
});
// ── Delete ──
await T.test('crud', 'projects', 'DELETE /projects/:id', async function () {
await T.safeDelete('/projects/' + projectId);
projectId = null;
});
// Cleanup scratch resources (cleanups run in reverse but let's be explicit)
if (channelId) { try { await T.safeDelete('/channels/' + channelId); channelId = null; } catch (e) { /* ok */ } }
if (kbId) { try { await T.safeDelete('/knowledge-bases/' + kbId); kbId = null; } catch (e) { /* ok */ } }
if (noteId) { try { await T.safeDelete('/notes/' + noteId); noteId = null; } catch (e) { /* ok */ } }
};
})();