/** * ICD Test Runner — CRUD: Extensions * Extension install lifecycle (install → list → update → user list → * tier filter → manifest → tools → assets → settings → disable → * duplicate 409 → invalid tier 400 → not found 404 → disabled asset 404 → * delete), system extension disable rejection. */ (function () { 'use strict'; var T = window.ICD; if (!T) return; if (!T.crud) T.crud = {}; T.crud.extensions = async function (testTag) { // ── Extensions CRUD ── var extId = null; // UUID (DB id) var extExtId = null; // ext_id (manifest id) await T.test('crud', 'extensions', 'POST /admin/extensions (install)', async function () { extExtId = testTag + '-ext'; var d = await T.apiPost('/admin/extensions', { ext_id: extExtId, name: testTag + ' Test Extension', version: '1.0.0', tier: 'browser', description: 'ICD CRUD test extension', author: 'icd-runner', manifest: { _script: 'console.log("icd-test");', tools: [{ name: 'icd_tool', description: 'Test tool' }] }, is_enabled: true, is_system: false }); T.assertHasKey(d, 'data', 'install response'); T.assertShape(d.data, T.S.extension, 'installed extension'); T.assert(d.data.ext_id === extExtId, 'ext_id mismatch'); T.assert(d.data.tier === 'browser', 'tier should be browser'); T.assert(d.data.is_enabled === true, 'should be enabled'); T.assert(d.data.is_system === false, 'should not be system'); extId = d.data.id; T.registerCleanup(function () { if (extId) return T.safeDelete('/admin/extensions/' + extId); }); }); if (extId) { await T.test('crud', 'extensions', 'GET /admin/extensions (list)', async function () { var d = await T.apiGet('/admin/extensions'); T.assertHasKey(d, 'data', '/admin/extensions'); T.assert(Array.isArray(d.data), 'admin list should be array'); var found = d.data.some(function (e) { return e.id === extId; }); T.assert(found, 'installed extension should be in admin list'); }); await T.test('crud', 'extensions', 'PUT /admin/extensions/:id (update)', async function () { var d = await T.apiPut('/admin/extensions/' + extId, { name: testTag + ' Updated Extension', version: '2.0.0', description: 'Updated description' }); T.assertHasKey(d, 'data', 'update response'); T.assert(d.data.name === testTag + ' Updated Extension', 'name not updated'); T.assert(d.data.version === '2.0.0', 'version not updated'); }); await T.test('crud', 'extensions', 'GET /extensions (user list)', async function () { var d = await T.apiGet('/extensions'); T.assertHasKey(d, 'data', '/extensions'); T.assert(Array.isArray(d.data), 'user list should be array'); var found = d.data.some(function (e) { return e.id === extId; }); T.assert(found, 'enabled extension should appear in user list'); }); await T.test('crud', 'extensions', 'GET /extensions?tier=browser (filter)', async function () { var d = await T.apiGet('/extensions?tier=browser'); T.assertHasKey(d, 'data', '/extensions?tier=browser'); var found = d.data.some(function (e) { return e.id === extId; }); T.assert(found, 'browser extension should appear with tier filter'); }); await T.test('crud', 'extensions', 'GET /extensions?tier=starlark (filter excludes)', async function () { var d = await T.apiGet('/extensions?tier=starlark'); T.assertHasKey(d, 'data', '/extensions?tier=starlark'); var found = d.data.some(function (e) { return e.id === extId; }); T.assert(!found, 'browser extension should NOT appear with starlark filter'); }); await T.test('crud', 'extensions', 'GET /extensions/:id/manifest (by ext_id)', async function () { var d = await T.apiGet('/extensions/' + extExtId + '/manifest'); T.assertHasKey(d, 'data', 'manifest response'); T.assert(typeof d.data === 'object', 'manifest should be object'); }); await T.test('crud', 'extensions', 'GET /extensions/tools (tool schemas)', async function () { var d = await T.apiGet('/extensions/tools'); T.assertHasKey(d, 'data', '/extensions/tools'); T.assert(Array.isArray(d.data), 'tools should be array'); var found = d.data.some(function (t) { return t.name === 'icd_tool'; }); T.assert(found, 'icd_tool should appear in tool schemas'); }); await T.test('crud', 'extensions', 'GET /extensions/:id/assets (public, no auth)', async function () { var resp = await fetch(T.base + '/api/v1/extensions/' + extExtId + '/assets/main.js'); T.assert(resp.ok, 'asset fetch should succeed: ' + resp.status); var text = await resp.text(); T.assert(text.indexOf('icd-test') !== -1, 'script body should contain icd-test'); var ct = resp.headers.get('Content-Type') || ''; T.assert(ct.indexOf('javascript') !== -1, 'content-type should be javascript, got ' + ct); }); await T.test('crud', 'extensions', 'POST /extensions/:id/settings (save)', async function () { var d = await T.apiPost('/extensions/' + extId + '/settings', { settings: { theme: 'dark' }, is_enabled: true }); T.assertHasKey(d, 'ok', 'settings response'); T.assert(d.ok === true, 'ok should be true'); }); await T.test('crud', 'extensions', 'POST /extensions/:id/settings (disable non-system)', async function () { var d = await T.apiPost('/extensions/' + extId + '/settings', { is_enabled: false }); T.assert(d.ok === true, 'should succeed for non-system extension'); // Re-enable for subsequent tests await T.apiPost('/extensions/' + extId + '/settings', { is_enabled: true }); }); await T.test('crud', 'extensions', 'POST /admin/extensions (duplicate → 409)', async function () { var d = await T.authFetch(await T.getAuthToken(), 'POST', '/admin/extensions', { ext_id: extExtId, name: 'Duplicate' }); T.assert(d._status === 409, 'duplicate ext_id should return 409, got ' + d._status); }); await T.test('crud', 'extensions', 'POST /admin/extensions (invalid tier → 400)', async function () { var d = await T.authFetch(await T.getAuthToken(), 'POST', '/admin/extensions', { ext_id: 'bad-tier-ext', name: 'Bad Tier', tier: 'invalid' }); T.assert(d._status === 400, 'invalid tier should return 400, got ' + d._status); }); await T.test('crud', 'extensions', 'PUT /admin/extensions/:id (not found → 404)', async function () { var d = await T.authFetch(await T.getAuthToken(), 'PUT', '/admin/extensions/00000000-0000-0000-0000-000000000000', { name: 'ghost' }); T.assert(d._status === 404, 'nonexistent should return 404, got ' + d._status); }); await T.test('crud', 'extensions', 'GET /extensions/:id/assets (disabled → 404)', async function () { // Disable via admin await T.apiPut('/admin/extensions/' + extId, { is_enabled: false }); // Cache-bust: the earlier asset fetch is cached (max-age=3600) var resp = await fetch(T.base + '/api/v1/extensions/' + extExtId + '/assets/main.js?_cb=' + Date.now()); T.assert(resp.status === 404, 'disabled asset should 404, got ' + resp.status); // Re-enable await T.apiPut('/admin/extensions/' + extId, { is_enabled: true }); }); await T.test('crud', 'extensions', 'DELETE /admin/extensions/:id', async function () { await T.safeDelete('/admin/extensions/' + extId); // Verify gone from admin list var d = await T.apiGet('/admin/extensions'); var found = d.data.some(function (e) { return e.id === extId; }); T.assert(!found, 'deleted extension should not appear in list'); extId = null; }); } // ── System extension disable rejection ── var sysExtId = null; await T.test('crud', 'extensions', 'POST /admin/extensions (system ext)', async function () { var d = await T.apiPost('/admin/extensions', { ext_id: testTag + '-sys-ext', name: 'System Test', is_system: true, is_enabled: true }); T.assertHasKey(d, 'data', 'system install'); T.assert(d.data.is_system === true, 'should be system'); sysExtId = d.data.id; T.registerCleanup(function () { if (sysExtId) return T.safeDelete('/admin/extensions/' + sysExtId); }); }); if (sysExtId) { await T.test('crud', 'extensions', 'POST /extensions/:id/settings (system disable → 403)', async function () { var d = await T.authFetch(await T.getAuthToken(), 'POST', '/extensions/' + sysExtId + '/settings', { is_enabled: false }); T.assert(d._status === 403, 'disabling system ext should return 403, got ' + d._status); }); await T.test('crud', 'extensions', 'DELETE /admin/extensions/:id (system cleanup)', async function () { await T.safeDelete('/admin/extensions/' + sysExtId); sysExtId = null; }); } }; })();