Changeset 0.28.7 (#193)
This commit is contained in:
194
packages/icd-test-runner/js/crud/projects.js
Normal file
194
packages/icd-test-runner/js/crud/projects.js
Normal file
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* 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 */ } }
|
||||
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user