Files
memind/server/portal-mindspace-page-core-routes.test.mjs

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');
});