Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
518 lines
24 KiB
JavaScript
518 lines
24 KiB
JavaScript
/**
|
|
* ICD Test Runner — CRUD: Channels
|
|
* Channel lifecycle, DMs, folders, participants, model roster,
|
|
* KB linking, files, messages, typing, mark-read, user search.
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
var T = window.ICD;
|
|
if (!T) return;
|
|
if (!T.crud) T.crud = {};
|
|
|
|
T.crud.channels = async function (testTag) {
|
|
|
|
// ── Channels CRUD ──
|
|
var channelId = null;
|
|
await T.test('crud', 'channels', 'POST /channels (create)', async function () {
|
|
var d = await T.apiPost('/channels', {
|
|
title: testTag + '-channel',
|
|
type: 'direct',
|
|
description: 'ICD integration test channel',
|
|
tags: ['icd-test']
|
|
});
|
|
T.assertShape(d, T.S.channelFull, 'created channel');
|
|
T.assert(d.type === 'direct', 'type should be direct');
|
|
T.assert(d.is_archived === false, 'should not be archived');
|
|
channelId = d.id;
|
|
T.registerCleanup(function () { if (channelId) return T.safeDelete('/channels/' + channelId); });
|
|
});
|
|
|
|
if (channelId) {
|
|
await T.test('crud', 'channels', 'GET /channels/:id (read)', async function () {
|
|
var d = await T.apiGet('/channels/' + channelId);
|
|
T.assertShape(d, T.S.channelFull, 'channel');
|
|
T.assert(d.id === channelId, 'id mismatch');
|
|
T.assert(d.title.indexOf(testTag) !== -1, 'title mismatch');
|
|
});
|
|
|
|
await T.test('crud', 'channels', 'PUT /channels/:id (update title+topic+ai_mode)', async function () {
|
|
var d = await T.apiPut('/channels/' + channelId, {
|
|
title: testTag + '-updated',
|
|
topic: 'ICD test topic',
|
|
ai_mode: 'mention_only'
|
|
});
|
|
T.assert(d.title === testTag + '-updated', 'title not updated');
|
|
T.assert(d.topic === 'ICD test topic', 'topic not set');
|
|
T.assert(d.ai_mode === 'mention_only', 'ai_mode not set');
|
|
});
|
|
|
|
await T.test('crud', 'channels', 'PUT /channels/:id (restore ai_mode)', async function () {
|
|
var d = await T.apiPut('/channels/' + channelId, { ai_mode: 'auto' });
|
|
T.assert(d.ai_mode === 'auto', 'ai_mode not restored');
|
|
});
|
|
|
|
// ── Channel List Query Filters ──
|
|
await T.test('crud', 'channels', 'GET /channels?type=direct', async function () {
|
|
var d = await T.apiGet('/channels?type=direct&per_page=5');
|
|
var arr = d.data || [];
|
|
T.assert(Array.isArray(arr), 'expected data array');
|
|
arr.forEach(function (ch) { T.assert(ch.type === 'direct', 'type filter leaked: ' + ch.type); });
|
|
});
|
|
|
|
await T.test('crud', 'channels', 'GET /channels?search=...', async function () {
|
|
var d = await T.apiGet('/channels?search=' + encodeURIComponent(testTag) + '&per_page=5');
|
|
var arr = d.data || [];
|
|
T.assert(Array.isArray(arr), 'expected data array');
|
|
T.assert(arr.length >= 1, 'search should find our channel');
|
|
});
|
|
|
|
await T.test('crud', 'channels', 'GET /channels?archived=true (empty)', async function () {
|
|
var d = await T.apiGet('/channels?archived=true&per_page=5');
|
|
var arr = d.data || [];
|
|
T.assert(Array.isArray(arr), 'expected data array');
|
|
// Our channel is not archived, so it shouldn't appear here
|
|
});
|
|
|
|
// ── Message Tree ──
|
|
await T.test('crud', 'channels', 'GET /channels/:id/path (empty)', async function () {
|
|
var d = await T.apiGet('/channels/' + channelId + '/path');
|
|
T.assertHasKey(d, 'messages', '/path');
|
|
T.assert(Array.isArray(d.messages), 'messages should be array');
|
|
});
|
|
|
|
// ── Participants ──
|
|
await T.test('crud', 'channels', 'GET /channels/:id/participants (auto owner)', async function () {
|
|
var d = await T.apiGet('/channels/' + channelId + '/participants');
|
|
T.assertHasKey(d, 'participants', '/participants');
|
|
T.assert(Array.isArray(d.participants), 'participants should be array');
|
|
T.assert(d.participants.length >= 1, 'should have at least owner participant');
|
|
if (d.participants.length > 0) {
|
|
T.assertShape(d.participants[0], T.S.participant, 'participant[0]');
|
|
T.assert(d.participants[0].role === 'owner', 'first participant should be owner');
|
|
}
|
|
});
|
|
|
|
// ── Participant CRUD (add fixture user, update role, remove) ──
|
|
var addedParticipantId = null;
|
|
var fixtureUser = T.getFixtureUser ? T.getFixtureUser('-user') : null;
|
|
if (fixtureUser && fixtureUser.id) {
|
|
await T.test('crud', 'channels', 'POST /channels/:id/participants (add user)', async function () {
|
|
var d = await T.apiPost('/channels/' + channelId + '/participants', {
|
|
participant_type: 'user',
|
|
participant_id: fixtureUser.id,
|
|
role: 'member'
|
|
});
|
|
// Response varies — may be participant object or list
|
|
var p = d.participant || d;
|
|
T.assert(p.id || p.participants, 'expected participant id or list');
|
|
if (p.id) addedParticipantId = p.id;
|
|
// Verify by re-listing
|
|
var list = await T.apiGet('/channels/' + channelId + '/participants');
|
|
var found = list.participants.find(function (pp) { return pp.participant_id === fixtureUser.id; });
|
|
T.assert(found, 'added user not found in participant list');
|
|
if (!addedParticipantId && found) addedParticipantId = found.id;
|
|
});
|
|
|
|
if (addedParticipantId) {
|
|
await T.test('crud', 'channels', 'PATCH /channels/:id/participants/:id (update role)', async function () {
|
|
var d = await T.apiPatch('/channels/' + channelId + '/participants/' + addedParticipantId, {
|
|
role: 'observer'
|
|
});
|
|
T.assert(typeof d === 'object', 'expected response');
|
|
// Verify
|
|
var list = await T.apiGet('/channels/' + channelId + '/participants');
|
|
var found = list.participants.find(function (pp) { return pp.id === addedParticipantId; });
|
|
T.assert(found && found.role === 'observer', 'role not updated to observer');
|
|
});
|
|
|
|
// ── Multi-user access: participant can GET channel (CS1 fix) ──
|
|
await T.test('crud', 'channels', 'GET /channels/:id (as participant, CS1 fix)', async function () {
|
|
var d = await T.authFetch(fixtureUser.token, 'GET', '/channels/' + channelId);
|
|
T.assert(d._status === 200, 'participant should see channel, got ' + d._status);
|
|
T.assert(d.id === channelId, 'id mismatch');
|
|
});
|
|
|
|
await T.test('crud', 'channels', 'GET /channels/:id/path (as participant)', async function () {
|
|
var d = await T.authFetch(fixtureUser.token, 'GET', '/channels/' + channelId + '/path');
|
|
T.assert(d._status === 200, 'participant should see path, got ' + d._status);
|
|
T.assertHasKey(d, 'messages', 'participant path');
|
|
});
|
|
|
|
await T.test('crud', 'channels', 'DELETE /channels/:id/participants/:id (remove)', async function () {
|
|
await T.apiDelete('/channels/' + channelId + '/participants/' + addedParticipantId);
|
|
var list = await T.apiGet('/channels/' + channelId + '/participants');
|
|
var found = list.participants.find(function (pp) { return pp.id === addedParticipantId; });
|
|
T.assert(!found, 'participant should be removed');
|
|
addedParticipantId = null;
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── Model Roster CRUD ──
|
|
await T.test('crud', 'channels', 'GET /channels/:id/models (roster)', async function () {
|
|
var d = await T.apiGet('/channels/' + channelId + '/models');
|
|
T.assertHasKey(d, 'models', '/channel-models');
|
|
T.assert(Array.isArray(d.models), 'models should be array');
|
|
});
|
|
|
|
var rosterModelId = null;
|
|
await T.test('crud', 'channels', 'POST /channels/:id/models (add)', async function () {
|
|
var d = await T.apiPost('/channels/' + channelId + '/models', {
|
|
model_id: 'icd-test-model',
|
|
display_name: 'ICD Test Model',
|
|
provider_config_id: null
|
|
});
|
|
T.assertHasKey(d, 'models', '/models add');
|
|
T.assert(Array.isArray(d.models), 'models should be array');
|
|
var added = d.models.find(function (m) { return m.model_id === 'icd-test-model'; });
|
|
T.assert(added, 'added model not found in roster');
|
|
rosterModelId = added ? added.id : null;
|
|
});
|
|
|
|
if (rosterModelId) {
|
|
await T.test('crud', 'channels', 'PATCH /channels/:id/models/:id (update)', async function () {
|
|
var d = await T.apiPatch('/channels/' + channelId + '/models/' + rosterModelId, {
|
|
display_name: 'ICD Updated Model'
|
|
});
|
|
T.assertHasKey(d, 'models', '/models update');
|
|
var updated = d.models.find(function (m) { return m.id === rosterModelId; });
|
|
T.assert(updated && updated.display_name === 'ICD Updated Model', 'display_name not updated');
|
|
});
|
|
|
|
await T.test('crud', 'channels', 'DELETE /channels/:id/models/:id (remove)', async function () {
|
|
var d = await T.apiDelete('/channels/' + channelId + '/models/' + rosterModelId);
|
|
T.assertHasKey(d, 'models', '/models delete');
|
|
var models = d.models || [];
|
|
var found = models.find(function (m) { return m.id === rosterModelId; });
|
|
T.assert(!found, 'deleted model still in roster');
|
|
rosterModelId = null;
|
|
});
|
|
}
|
|
|
|
// ── KB linking ──
|
|
await T.test('crud', 'channels', 'GET /channels/:id/knowledge-bases', async function () {
|
|
var d = await T.apiGet('/channels/' + channelId + '/knowledge-bases');
|
|
T.assertHasKey(d, 'data', '/channel-kbs');
|
|
});
|
|
|
|
// ── Files ──
|
|
await T.test('crud', 'channels', 'GET /channels/:id/files', async function () {
|
|
var d = await T.apiGet('/channels/' + channelId + '/files');
|
|
T.assertHasKey(d, 'files', '/channel-files');
|
|
T.assert(Array.isArray(d.files), 'files should be array');
|
|
});
|
|
|
|
// ── File Upload/Download Lifecycle (conditional on storage) ──
|
|
var storageOk = false;
|
|
if (T.user.role === 'admin') {
|
|
try {
|
|
var ss = await T.apiGet('/admin/storage/status');
|
|
storageOk = ss && ss.configured === true;
|
|
} catch (e) { /* not admin or storage disabled */ }
|
|
}
|
|
|
|
var uploadedFileId = null;
|
|
if (storageOk) {
|
|
await T.test('crud', 'channels', 'POST /channels/:id/files (upload)', async function () {
|
|
var blob = new Blob(['ICD test file content'], { type: 'text/plain' });
|
|
var d = await T.apiUpload('/channels/' + channelId + '/files', blob, 'icd-test.txt');
|
|
T.assert(d.id, 'expected file id');
|
|
T.assert(d.filename === 'icd-test.txt', 'filename mismatch');
|
|
T.assert(d.origin === 'user_upload', 'origin should be user_upload');
|
|
T.assert(d.content_type === 'text/plain', 'content_type mismatch');
|
|
uploadedFileId = d.id;
|
|
T.registerCleanup(function () { if (uploadedFileId) return T.safeDelete('/files/' + uploadedFileId); });
|
|
});
|
|
|
|
if (uploadedFileId) {
|
|
await T.test('crud', 'channels', 'GET /files/:id (metadata)', async function () {
|
|
var d = await T.apiGet('/files/' + uploadedFileId);
|
|
T.assertShape(d, T.S.file, 'file metadata');
|
|
T.assert(d.id === uploadedFileId, 'id mismatch');
|
|
T.assert(d.channel_id === channelId, 'channel_id mismatch');
|
|
});
|
|
|
|
await T.test('crud', 'channels', 'GET /files/:id/download', async function () {
|
|
var token = await T.getAuthToken();
|
|
var resp = await fetch(T.base + '/api/v1/files/' + uploadedFileId + '/download', {
|
|
headers: { 'Authorization': 'Bearer ' + token },
|
|
credentials: 'same-origin'
|
|
});
|
|
T.assert(resp.ok, 'download should succeed, got ' + resp.status);
|
|
var text = await resp.text();
|
|
T.assert(text === 'ICD test file content', 'download content mismatch');
|
|
});
|
|
|
|
await T.test('crud', 'channels', 'DELETE /files/:id', async function () {
|
|
var delId = uploadedFileId;
|
|
await T.safeDelete('/files/' + delId);
|
|
uploadedFileId = null;
|
|
// Verify gone
|
|
try {
|
|
await T.apiGet('/files/' + delId);
|
|
T.assert(false, 'file should be deleted');
|
|
} catch (e) {
|
|
T.assert(e.message.indexOf('404') !== -1 || e.message.indexOf('not found') !== -1, 'expected 404');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── Message Create + Edit + Cursor + Siblings ──
|
|
var msgId = null;
|
|
await T.test('crud', 'channels', 'POST /channels/:id/messages (create)', async function () {
|
|
var d = await T.apiPost('/channels/' + channelId + '/messages', {
|
|
role: 'user',
|
|
content: 'ICD integration test message'
|
|
});
|
|
T.assertShape(d, T.S.message, 'message');
|
|
msgId = d.id;
|
|
});
|
|
|
|
if (msgId) {
|
|
await T.test('crud', 'channels', 'GET /channels/:id/messages (all)', async function () {
|
|
var d = await T.apiGet('/channels/' + channelId + '/messages');
|
|
var arr = d.messages || d.data;
|
|
T.assert(Array.isArray(arr), 'expected array');
|
|
T.assert(arr.length >= 1, 'expected at least 1 message');
|
|
});
|
|
|
|
await T.test('crud', 'channels', 'GET /channels/:id/messages/:id/siblings', async function () {
|
|
var d = await T.apiGet('/channels/' + channelId + '/messages/' + msgId + '/siblings');
|
|
T.assertHasKey(d, 'siblings', '/siblings');
|
|
T.assert(Array.isArray(d.siblings), 'siblings should be array');
|
|
});
|
|
|
|
// Edit creates sibling
|
|
var editedMsgId = null;
|
|
await T.test('crud', 'channels', 'POST /channels/:id/messages/:id/edit', async function () {
|
|
var d = await T.apiPost('/channels/' + channelId + '/messages/' + msgId + '/edit', {
|
|
content: 'ICD edited message content'
|
|
});
|
|
T.assertShape(d, T.S.message, 'edited message');
|
|
T.assert(d.id !== msgId, 'edit should create new message id');
|
|
T.assert(d.content === 'ICD edited message content', 'content mismatch');
|
|
editedMsgId = d.id;
|
|
});
|
|
|
|
if (editedMsgId) {
|
|
await T.test('crud', 'channels', 'GET siblings (after edit, expect 2)', async function () {
|
|
var d = await T.apiGet('/channels/' + channelId + '/messages/' + msgId + '/siblings');
|
|
T.assert(d.siblings.length >= 2, 'edit should create sibling, got ' + d.siblings.length);
|
|
});
|
|
|
|
await T.test('crud', 'channels', 'PUT /channels/:id/cursor (switch branch)', async function () {
|
|
var d = await T.apiPut('/channels/' + channelId + '/cursor', {
|
|
active_leaf_id: msgId
|
|
});
|
|
T.assertHasKey(d, 'messages', '/cursor');
|
|
T.assert(Array.isArray(d.messages), 'cursor response should have messages array');
|
|
});
|
|
}
|
|
|
|
await T.test('crud', 'channels', 'GET /messages/:id/files', async function () {
|
|
var d = await T.apiGet('/messages/' + msgId + '/files');
|
|
T.assertHasKey(d, 'files', '/message-files');
|
|
T.assert(Array.isArray(d.files), 'files should be array');
|
|
});
|
|
|
|
await T.test('crud', 'channels', 'GET /channels/:id/path (after messages)', async function () {
|
|
var d = await T.apiGet('/channels/' + channelId + '/path');
|
|
T.assertHasKey(d, 'messages', '/path');
|
|
T.assert(d.messages.length >= 1, 'path should have messages');
|
|
});
|
|
|
|
await T.test('crud', 'channels', 'POST /channels/:id/generate-title', async function () {
|
|
try {
|
|
var d = await T.apiPost('/channels/' + channelId + '/generate-title', {});
|
|
T.assert(typeof d === 'object', 'expected object');
|
|
} catch (e) {
|
|
if (e.message && (e.message.indexOf('400') !== -1 || e.message.indexOf('502') !== -1 || e.message.indexOf('model') !== -1))
|
|
return;
|
|
throw e;
|
|
}
|
|
});
|
|
}
|
|
|
|
// ── Typing indicator ──
|
|
await T.test('crud', 'channels', 'POST /channels/:id/typing', async function () {
|
|
var d = await T.apiPost('/channels/' + channelId + '/typing', {});
|
|
T.assert(d.ok === true, 'expected { ok: true }');
|
|
});
|
|
|
|
// ── Mark Read ──
|
|
await T.test('crud', 'channels', 'POST /channels/:id/mark-read', async function () {
|
|
var d = await T.apiPost('/channels/' + channelId + '/mark-read', {});
|
|
T.assert(typeof d === 'object', 'expected object');
|
|
});
|
|
|
|
// ── Delete ──
|
|
await T.test('crud', 'channels', 'DELETE /channels/:id', async function () {
|
|
await T.safeDelete('/channels/' + channelId);
|
|
channelId = null;
|
|
});
|
|
}
|
|
|
|
// ── DM Creation + Dedup ──
|
|
var dmChannelId = null;
|
|
var dmUser = T.getFixtureUser ? T.getFixtureUser('-user') : null;
|
|
if (dmUser && dmUser.id) {
|
|
await T.test('crud', 'channels', 'POST /channels (type=dm, create)', async function () {
|
|
var d = await T.apiPost('/channels', {
|
|
title: testTag + '-dm',
|
|
type: 'dm',
|
|
participants: [dmUser.id]
|
|
});
|
|
T.assert(d.id, 'DM channel should have id');
|
|
T.assert(d.type === 'dm', 'type should be dm');
|
|
dmChannelId = d.id;
|
|
T.registerCleanup(function () { if (dmChannelId) return T.safeDelete('/channels/' + dmChannelId); });
|
|
});
|
|
|
|
if (dmChannelId) {
|
|
await T.test('crud', 'channels', 'POST /channels (dm dedup → 200)', async function () {
|
|
var d = await T.apiPost('/channels', {
|
|
title: testTag + '-dm-dup',
|
|
type: 'dm',
|
|
participants: [dmUser.id]
|
|
});
|
|
T.assert(d.id === dmChannelId, 'dedup should return same channel id');
|
|
});
|
|
|
|
await T.test('crud', 'channels', 'GET /channels?types=dm (type filter)', async function () {
|
|
var d = await T.apiGet('/channels?types=dm&per_page=10');
|
|
var arr = d.data || [];
|
|
T.assert(Array.isArray(arr), 'expected data array');
|
|
var found = arr.find(function (ch) { return ch.id === dmChannelId; });
|
|
T.assert(found, 'DM channel should appear in dm type filter');
|
|
});
|
|
|
|
await T.test('crud', 'channels', 'DELETE /channels/:id (dm cleanup)', async function () {
|
|
await T.safeDelete('/channels/' + dmChannelId);
|
|
dmChannelId = null;
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── Channel Types (group, channel) ──
|
|
var groupChId = null;
|
|
await T.test('crud', 'channels', 'POST /channels (type=group)', async function () {
|
|
var d = await T.apiPost('/channels', {
|
|
title: testTag + '-group',
|
|
type: 'group',
|
|
description: 'ICD group test'
|
|
});
|
|
T.assertShape(d, T.S.channelFull, 'group channel');
|
|
T.assert(d.type === 'group', 'type should be group, got: ' + d.type);
|
|
groupChId = d.id;
|
|
T.registerCleanup(function () { if (groupChId) return T.safeDelete('/channels/' + groupChId); });
|
|
});
|
|
|
|
var teamChId = null;
|
|
await T.test('crud', 'channels', 'POST /channels (type=channel)', async function () {
|
|
var d = await T.apiPost('/channels', {
|
|
title: testTag + '-team-channel',
|
|
type: 'channel',
|
|
description: 'ICD channel test'
|
|
});
|
|
T.assertShape(d, T.S.channelFull, 'team channel');
|
|
T.assert(d.type === 'channel', 'type should be channel, got: ' + d.type);
|
|
teamChId = d.id;
|
|
T.registerCleanup(function () { if (teamChId) return T.safeDelete('/channels/' + teamChId); });
|
|
});
|
|
|
|
// ── Multi-type filter ──
|
|
await T.test('crud', 'channels', 'GET /channels?types=group,channel (multi)', async function () {
|
|
var d = await T.apiGet('/channels?types=group,channel&per_page=50');
|
|
var arr = d.data || [];
|
|
T.assert(Array.isArray(arr), 'expected data array');
|
|
arr.forEach(function (ch) {
|
|
T.assert(ch.type === 'group' || ch.type === 'channel',
|
|
'multi-type filter leaked: ' + ch.type);
|
|
});
|
|
});
|
|
|
|
await T.test('crud', 'channels', 'GET /channels?types=direct,dm,group,channel (all)', async function () {
|
|
var d = await T.apiGet('/channels?types=direct,dm,group,channel&per_page=50');
|
|
var arr = d.data || [];
|
|
T.assert(Array.isArray(arr), 'expected data array');
|
|
var types = new Set(arr.map(function (ch) { return ch.type; }));
|
|
T.assert(types.size <= 4, 'should only contain known types');
|
|
});
|
|
|
|
// Cleanup channel types
|
|
if (groupChId) {
|
|
await T.test('crud', 'channels', 'DELETE /channels/:id (group cleanup)', async function () {
|
|
await T.safeDelete('/channels/' + groupChId);
|
|
groupChId = null;
|
|
});
|
|
}
|
|
if (teamChId) {
|
|
await T.test('crud', 'channels', 'DELETE /channels/:id (channel cleanup)', async function () {
|
|
await T.safeDelete('/channels/' + teamChId);
|
|
teamChId = null;
|
|
});
|
|
}
|
|
|
|
// ── Folders CRUD + Channel Assignment ──
|
|
var folderId = null;
|
|
await T.test('crud', 'channels', 'POST /folders (create)', async function () {
|
|
var d = await T.apiPost('/folders', { name: testTag + '-folder', sort_order: 0 });
|
|
var folder = d.folder || d.data || d;
|
|
T.assert(folder.id || folder.ID, 'folder response missing id, got keys: ' + Object.keys(folder).join(', '));
|
|
folderId = folder.id || folder.ID;
|
|
T.registerCleanup(function () { if (folderId) return T.safeDelete('/folders/' + folderId); });
|
|
});
|
|
|
|
if (folderId) {
|
|
await T.test('crud', 'channels', 'PUT /folders/:id (update)', async function () {
|
|
var d = await T.apiPut('/folders/' + folderId, { name: testTag + '-folder-updated', sort_order: 1 });
|
|
T.assert(typeof d === 'object', 'expected object');
|
|
});
|
|
|
|
// Create a channel, assign to folder, verify filter works
|
|
var folderCh = null;
|
|
await T.test('crud', 'channels', 'PUT /channels/:id { folder_id } (assign)', async function () {
|
|
var ch = await T.apiPost('/channels', { title: testTag + '-folder-test', type: 'direct' });
|
|
folderCh = ch.id;
|
|
T.registerCleanup(function () { if (folderCh) return T.safeDelete('/channels/' + folderCh); });
|
|
var d = await T.apiPut('/channels/' + folderCh, { folder_id: folderId });
|
|
T.assert(d.folder_id === folderId, 'folder_id should be set, got: ' + d.folder_id);
|
|
});
|
|
|
|
if (folderCh) {
|
|
await T.test('crud', 'channels', 'GET /channels?folder_id=... (filter)', async function () {
|
|
var d = await T.apiGet('/channels?folder_id=' + folderId + '&per_page=10');
|
|
var arr = d.data || [];
|
|
T.assert(Array.isArray(arr), 'expected data array');
|
|
var found = arr.find(function (ch) { return ch.id === folderCh; });
|
|
T.assert(found, 'channel should appear in folder_id filter');
|
|
});
|
|
|
|
await T.test('crud', 'channels', 'PUT /channels/:id { folder_id: "" } (unbind)', async function () {
|
|
var d = await T.apiPut('/channels/' + folderCh, { folder_id: '' });
|
|
T.assert(!d.folder_id, 'folder_id should be cleared');
|
|
});
|
|
|
|
await T.test('crud', 'channels', 'DELETE /channels/:id (folder test cleanup)', async function () {
|
|
await T.safeDelete('/channels/' + folderCh);
|
|
folderCh = null;
|
|
});
|
|
}
|
|
|
|
await T.test('crud', 'channels', 'DELETE /folders/:id', async function () {
|
|
await T.safeDelete('/folders/' + folderId);
|
|
folderId = null;
|
|
});
|
|
}
|
|
|
|
// ── User Search ──
|
|
await T.test('crud', 'channels', 'GET /users/search', async function () {
|
|
var d = await T.apiGet('/users/search?q=' + encodeURIComponent(T.user.username.substring(0, 3)));
|
|
T.assertHasKey(d, 'users', '/users/search');
|
|
T.assert(Array.isArray(d.users), 'users should be array');
|
|
});
|
|
|
|
};
|
|
})();
|