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