446 lines
12 KiB
JavaScript
446 lines
12 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { attachPortalMindSpacePageCoreRoutes } from './portal-mindspace-page-core-routes.mjs';
|
|
|
|
function createRouterRecorder() {
|
|
const routes = new Map();
|
|
return {
|
|
routes,
|
|
get(path, handler) {
|
|
routes.set(`GET ${path}`, handler);
|
|
},
|
|
post(path, handler) {
|
|
routes.set(`POST ${path}`, handler);
|
|
},
|
|
put(path, handler) {
|
|
routes.set(`PUT ${path}`, handler);
|
|
},
|
|
delete(path, handler) {
|
|
routes.set(`DELETE ${path}`, handler);
|
|
},
|
|
};
|
|
}
|
|
|
|
function createResponseRecorder() {
|
|
return {
|
|
statusCode: 200,
|
|
body: undefined,
|
|
status(code) {
|
|
this.statusCode = code;
|
|
return this;
|
|
},
|
|
json(body) {
|
|
this.body = body;
|
|
return this;
|
|
},
|
|
};
|
|
}
|
|
|
|
function createRequest(overrides = {}) {
|
|
return {
|
|
body: {},
|
|
query: {},
|
|
params: { pageId: 'page-1' },
|
|
currentUser: { id: 'user-1' },
|
|
ip: '127.0.0.1',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createDependencies(overrides = {}) {
|
|
const calls = {
|
|
data: [],
|
|
errors: [],
|
|
routeErrors: [],
|
|
sync: [],
|
|
enabled: [],
|
|
};
|
|
return {
|
|
calls,
|
|
dependencies: {
|
|
getMindSpacePages: () => ({}),
|
|
getMindSpacePublications: () => null,
|
|
getMindSpace: () => ({ getQuota: async () => ({ used: 1 }) }),
|
|
getMindSpaceAudit: () => null,
|
|
getMindSpacePageLiveEdit: () => ({}),
|
|
getMindSpacePageEditSession: () => ({}),
|
|
getUserAuth: () => ({ canUseChat: async () => ({ ok: true }) }),
|
|
async syncUserGeneratedPages(userId) {
|
|
calls.sync.push(userId);
|
|
},
|
|
ownsAgentSession: async () => true,
|
|
ensureMindSpaceEnabled(res, req) {
|
|
calls.enabled.push({ res, req });
|
|
return true;
|
|
},
|
|
sendData(res, req, data, status = 200) {
|
|
calls.data.push({ res, req, data, status });
|
|
return res.status(status).json({ data });
|
|
},
|
|
sendError(res, req, status, code, message) {
|
|
calls.errors.push({ res, req, status, code, message });
|
|
return res.status(status).json({ error: { code, message } });
|
|
},
|
|
handleMindSpaceError(res, req, error) {
|
|
calls.routeErrors.push({ res, req, error });
|
|
return res.status(418).json({ mapped: error.message });
|
|
},
|
|
...overrides,
|
|
},
|
|
};
|
|
}
|
|
|
|
test('page core module preserves route inventory and order', () => {
|
|
const api = createRouterRecorder();
|
|
attachPortalMindSpacePageCoreRoutes(
|
|
api,
|
|
createDependencies().dependencies,
|
|
);
|
|
assert.deepEqual([...api.routes.keys()], [
|
|
'POST /mindspace/v1/pages',
|
|
'GET /mindspace/v1/pages',
|
|
'GET /mindspace/v1/pages/:pageId',
|
|
'DELETE /mindspace/v1/pages/:pageId',
|
|
'GET /mindspace/v1/pages/:pageId/delete-preview',
|
|
'PUT /mindspace/v1/pages/:pageId',
|
|
'POST /mindspace/v1/pages/:pageId/live-edit/bind',
|
|
'POST /mindspace/v1/pages/:pageId/live-edit/fork-session',
|
|
'POST /mindspace/v1/pages/:pageId/live-edit/close-session',
|
|
'GET /mindspace/v1/pages/:pageId/live-edit/revision',
|
|
]);
|
|
});
|
|
|
|
test('create and list preserve payload mapping, sync, and pagination envelope', async () => {
|
|
const received = [];
|
|
const api = createRouterRecorder();
|
|
const setup = createDependencies({
|
|
getMindSpacePages: () => ({
|
|
async createPage(userId, input) {
|
|
received.push({ kind: 'create', userId, input });
|
|
return { id: 'page-1' };
|
|
},
|
|
async listPages(userId, input) {
|
|
received.push({ kind: 'list', userId, input });
|
|
return {
|
|
items: [{ id: 'page-1' }],
|
|
total: 1,
|
|
limit: 20,
|
|
offset: 4,
|
|
hasMore: false,
|
|
};
|
|
},
|
|
}),
|
|
});
|
|
attachPortalMindSpacePageCoreRoutes(api, setup.dependencies);
|
|
|
|
const createRes = createResponseRecorder();
|
|
await api.routes.get('POST /mindspace/v1/pages')(
|
|
createRequest({
|
|
body: {
|
|
title: 'Title',
|
|
summary: 'Summary',
|
|
content: '<h1>Page</h1>',
|
|
content_format: 'html',
|
|
template_id: 'template-1',
|
|
page_type: 'html',
|
|
category_code: 'draft',
|
|
},
|
|
}),
|
|
createRes,
|
|
);
|
|
assert.equal(createRes.statusCode, 201);
|
|
assert.equal(received[0].input.contentFormat, 'html');
|
|
|
|
const listRes = createResponseRecorder();
|
|
await api.routes.get('GET /mindspace/v1/pages')(
|
|
createRequest({
|
|
query: {
|
|
status: 'active',
|
|
category_code: 'draft',
|
|
limit: '20',
|
|
offset: '4',
|
|
},
|
|
}),
|
|
listRes,
|
|
);
|
|
assert.deepEqual(setup.calls.sync, ['user-1']);
|
|
assert.deepEqual(received[1].input, {
|
|
status: 'active',
|
|
categoryCode: 'draft',
|
|
limit: 20,
|
|
offset: 4,
|
|
});
|
|
assert.deepEqual(listRes.body.page, {
|
|
total: 1,
|
|
limit: 20,
|
|
offset: 4,
|
|
has_more: false,
|
|
next_cursor: null,
|
|
});
|
|
});
|
|
|
|
test('get and update preserve parallel lookup and version mapping', async () => {
|
|
const api = createRouterRecorder();
|
|
const setup = createDependencies({
|
|
getMindSpacePages: () => ({
|
|
getPage: async () => ({ id: 'page-1', title: 'Page' }),
|
|
listVersions: async () => [{ id: 'version-1' }],
|
|
async updatePage(userId, pageId, input) {
|
|
assert.equal(userId, 'user-1');
|
|
assert.equal(pageId, 'page-1');
|
|
assert.deepEqual(input, {
|
|
expectedVersion: 2,
|
|
title: 'Updated',
|
|
summary: undefined,
|
|
content: 'Body',
|
|
templateId: undefined,
|
|
pageType: undefined,
|
|
changeNote: 'edit',
|
|
});
|
|
return { id: pageId, versionNo: 3 };
|
|
},
|
|
}),
|
|
getMindSpacePublications: () => ({
|
|
getCurrent: async () => ({ id: 'publication-1' }),
|
|
}),
|
|
});
|
|
attachPortalMindSpacePageCoreRoutes(api, setup.dependencies);
|
|
|
|
const getRes = createResponseRecorder();
|
|
await api.routes.get('GET /mindspace/v1/pages/:pageId')(
|
|
createRequest(),
|
|
getRes,
|
|
);
|
|
assert.equal(getRes.body.data.publication.id, 'publication-1');
|
|
assert.equal(getRes.body.data.versions[0].id, 'version-1');
|
|
|
|
const updateRes = createResponseRecorder();
|
|
await api.routes.get('PUT /mindspace/v1/pages/:pageId')(
|
|
createRequest({
|
|
body: {
|
|
expected_version: 2,
|
|
title: 'Updated',
|
|
content: 'Body',
|
|
change_note: 'edit',
|
|
},
|
|
}),
|
|
updateRes,
|
|
);
|
|
assert.equal(updateRes.body.data.versionNo, 3);
|
|
});
|
|
|
|
test('delete and delete-preview preserve cleanup, quota, audit, and feature gate', async () => {
|
|
const audit = [];
|
|
const api = createRouterRecorder();
|
|
const setup = createDependencies({
|
|
getMindSpacePages: () => ({
|
|
async deletePage(userId, pageId, options) {
|
|
assert.equal(userId, 'user-1');
|
|
assert.equal(pageId, 'page-1');
|
|
assert.deepEqual(options, { removeFromPlaza: true });
|
|
return {
|
|
offlinedPublicationCount: 1,
|
|
hiddenPlazaPostCount: 2,
|
|
deletedAssetCount: 3,
|
|
freedBytes: 4,
|
|
};
|
|
},
|
|
async getDeletePreview() {
|
|
return { pageId: 'page-1', assetCount: 3 };
|
|
},
|
|
}),
|
|
getMindSpaceAudit: () => ({
|
|
async write(entry) {
|
|
audit.push(entry);
|
|
},
|
|
}),
|
|
});
|
|
attachPortalMindSpacePageCoreRoutes(api, setup.dependencies);
|
|
|
|
const deleteRes = createResponseRecorder();
|
|
await api.routes.get('DELETE /mindspace/v1/pages/:pageId')(
|
|
createRequest({ query: { remove_from_plaza: '1' } }),
|
|
deleteRes,
|
|
);
|
|
assert.equal(deleteRes.body.data.quota.used, 1);
|
|
assert.equal(audit[0].action, 'page.delete');
|
|
assert.deepEqual(audit[0].detail, {
|
|
offlinedPublicationCount: 1,
|
|
hiddenPlazaPostCount: 2,
|
|
deletedAssetCount: 3,
|
|
freedBytes: 4,
|
|
});
|
|
|
|
const previewRes = createResponseRecorder();
|
|
await api.routes.get(
|
|
'GET /mindspace/v1/pages/:pageId/delete-preview',
|
|
)(createRequest(), previewRes);
|
|
assert.equal(previewRes.body.data.assetCount, 3);
|
|
assert.equal(setup.calls.enabled.length, 2);
|
|
});
|
|
|
|
test('live edit bind preserves validation, ownership, page check, and binding', async () => {
|
|
const calls = [];
|
|
const api = createRouterRecorder();
|
|
const setup = createDependencies({
|
|
getMindSpacePages: () => ({
|
|
async getPage(userId, pageId) {
|
|
calls.push({ kind: 'page', userId, pageId });
|
|
},
|
|
}),
|
|
ownsAgentSession: async (userId, sessionId) => {
|
|
calls.push({ kind: 'owns', userId, sessionId });
|
|
return true;
|
|
},
|
|
getMindSpacePageLiveEdit: () => ({
|
|
bindSession(input) {
|
|
calls.push({ kind: 'bind', input });
|
|
return { revision: 1 };
|
|
},
|
|
}),
|
|
});
|
|
attachPortalMindSpacePageCoreRoutes(api, setup.dependencies);
|
|
const res = createResponseRecorder();
|
|
await api.routes.get(
|
|
'POST /mindspace/v1/pages/:pageId/live-edit/bind',
|
|
)(
|
|
createRequest({
|
|
body: {
|
|
session_id: 'session-1',
|
|
parent_session_id: 'parent-1',
|
|
},
|
|
}),
|
|
res,
|
|
);
|
|
assert.deepEqual(calls.map((call) => call.kind), [
|
|
'owns',
|
|
'page',
|
|
'bind',
|
|
]);
|
|
assert.deepEqual(calls[2].input, {
|
|
userId: 'user-1',
|
|
sessionId: 'session-1',
|
|
pageId: 'page-1',
|
|
parentSessionId: 'parent-1',
|
|
});
|
|
|
|
const deniedApi = createRouterRecorder();
|
|
const denied = createDependencies({
|
|
getMindSpacePageLiveEdit: () => ({}),
|
|
ownsAgentSession: async () => false,
|
|
});
|
|
attachPortalMindSpacePageCoreRoutes(
|
|
deniedApi,
|
|
denied.dependencies,
|
|
);
|
|
const deniedRes = createResponseRecorder();
|
|
await deniedApi.routes.get(
|
|
'POST /mindspace/v1/pages/:pageId/live-edit/bind',
|
|
)(
|
|
createRequest({ body: { session_id: 'session-2' } }),
|
|
deniedRes,
|
|
);
|
|
assert.equal(deniedRes.statusCode, 403);
|
|
assert.equal(denied.calls.errors[0].code, 'forbidden');
|
|
});
|
|
|
|
test('live edit fork, close, and revision preserve service contracts', async () => {
|
|
const calls = [];
|
|
const api = createRouterRecorder();
|
|
const setup = createDependencies({
|
|
getUserAuth: () => ({
|
|
async canUseChat(userId) {
|
|
calls.push({ kind: 'gate', userId });
|
|
return { ok: true };
|
|
},
|
|
}),
|
|
getMindSpacePageEditSession: () => ({
|
|
async forkSession(input) {
|
|
calls.push({ kind: 'fork', input });
|
|
return { sessionId: 'fork-1' };
|
|
},
|
|
async closeSession(input) {
|
|
calls.push({ kind: 'close', input });
|
|
return { closed: true };
|
|
},
|
|
}),
|
|
getMindSpacePageLiveEdit: () => ({
|
|
async getRevisionSnapshot(userId, pageId) {
|
|
calls.push({ kind: 'revision', userId, pageId });
|
|
return { revision: 7 };
|
|
},
|
|
}),
|
|
});
|
|
attachPortalMindSpacePageCoreRoutes(api, setup.dependencies);
|
|
|
|
await api.routes.get(
|
|
'POST /mindspace/v1/pages/:pageId/live-edit/fork-session',
|
|
)(
|
|
createRequest({
|
|
body: {
|
|
parent_session_id: 'parent-1',
|
|
h5_api_base: ' https://api.example ',
|
|
},
|
|
}),
|
|
createResponseRecorder(),
|
|
);
|
|
assert.equal(calls[1].input.h5ApiBase, 'https://api.example');
|
|
|
|
await api.routes.get(
|
|
'POST /mindspace/v1/pages/:pageId/live-edit/close-session',
|
|
)(
|
|
createRequest({
|
|
body: {
|
|
session_id: 'session-1',
|
|
parent_session_id: 'parent-1',
|
|
summary: 'done',
|
|
},
|
|
}),
|
|
createResponseRecorder(),
|
|
);
|
|
assert.equal(calls[2].input.summary, 'done');
|
|
|
|
const revisionRes = createResponseRecorder();
|
|
await api.routes.get(
|
|
'GET /mindspace/v1/pages/:pageId/live-edit/revision',
|
|
)(createRequest(), revisionRes);
|
|
assert.equal(revisionRes.body.data.revision, 7);
|
|
});
|
|
|
|
test('page core routes preserve unavailable and mapped error responses', async () => {
|
|
const unavailableApi = createRouterRecorder();
|
|
const unavailable = createDependencies({
|
|
getMindSpacePages: () => null,
|
|
});
|
|
attachPortalMindSpacePageCoreRoutes(
|
|
unavailableApi,
|
|
unavailable.dependencies,
|
|
);
|
|
const unavailableRes = createResponseRecorder();
|
|
await unavailableApi.routes.get('GET /mindspace/v1/pages')(
|
|
createRequest(),
|
|
unavailableRes,
|
|
);
|
|
assert.equal(unavailableRes.statusCode, 503);
|
|
|
|
const failureApi = createRouterRecorder();
|
|
const failure = createDependencies({
|
|
getMindSpacePages: () => ({
|
|
async listPages() {
|
|
throw new Error('list failed');
|
|
},
|
|
}),
|
|
});
|
|
attachPortalMindSpacePageCoreRoutes(
|
|
failureApi,
|
|
failure.dependencies,
|
|
);
|
|
const failureRes = createResponseRecorder();
|
|
await failureApi.routes.get('GET /mindspace/v1/pages')(
|
|
createRequest(),
|
|
failureRes,
|
|
);
|
|
assert.equal(failureRes.statusCode, 418);
|
|
assert.equal(failure.calls.routeErrors[0].error.message, 'list failed');
|
|
});
|