590 lines
16 KiB
JavaScript
590 lines
16 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { attachPortalMindSpacePagePublishRoutes } from './portal-mindspace-page-publish-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);
|
|
},
|
|
};
|
|
}
|
|
|
|
function createResponseRecorder() {
|
|
return {
|
|
statusCode: 200,
|
|
body: undefined,
|
|
headers: {},
|
|
status(code) {
|
|
this.statusCode = code;
|
|
return this;
|
|
},
|
|
json(body) {
|
|
this.body = body;
|
|
return this;
|
|
},
|
|
set(name, value) {
|
|
this.headers[name] = value;
|
|
return this;
|
|
},
|
|
setHeader(name, value) {
|
|
this.headers[name] = value;
|
|
return this;
|
|
},
|
|
send(body) {
|
|
this.body = body;
|
|
return this;
|
|
},
|
|
};
|
|
}
|
|
|
|
function createRequest(overrides = {}) {
|
|
return {
|
|
body: {},
|
|
params: {
|
|
pageId: 'page-1',
|
|
publicationId: 'publication-1',
|
|
},
|
|
currentUser: {
|
|
id: 'user-1',
|
|
workspaceRoot: '/workspace/user-1',
|
|
},
|
|
requestId: 'request-1',
|
|
ip: '127.0.0.1',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createDependencies(overrides = {}) {
|
|
const calls = {
|
|
data: [],
|
|
routeErrors: [],
|
|
};
|
|
return {
|
|
calls,
|
|
dependencies: {
|
|
env: {},
|
|
getMindSpacePages: () => ({}),
|
|
getMindSpacePublications: () => ({}),
|
|
getMindSpaceAudit: () => null,
|
|
getAuthPool: () => ({ id: 'pool' }),
|
|
sendData(res, req, data, status = 200) {
|
|
calls.data.push({ res, req, data, status });
|
|
return res.status(status).json({ data });
|
|
},
|
|
handleMindSpaceError(res, req, error) {
|
|
calls.routeErrors.push({ res, req, error });
|
|
return res.status(418).json({ mapped: error.message });
|
|
},
|
|
...overrides,
|
|
},
|
|
};
|
|
}
|
|
|
|
test('page publish module preserves route inventory and order', () => {
|
|
const api = createRouterRecorder();
|
|
attachPortalMindSpacePagePublishRoutes(
|
|
api,
|
|
createDependencies().dependencies,
|
|
);
|
|
assert.deepEqual([...api.routes.keys()], [
|
|
'GET /mindspace/v1/pages/:pageId/thumbnail',
|
|
'POST /mindspace/v1/pages/:pageId/thumbnail/upload',
|
|
'POST /mindspace/v1/pages/:pageId/thumbnail/regenerate',
|
|
'POST /mindspace/v1/pages/:pageId/rewrite-download-links',
|
|
'GET /mindspace/v1/pages/:pageId/preview',
|
|
'POST /mindspace/v1/pages/:pageId/preview-draft',
|
|
'POST /mindspace/v1/pages/:pageId/publish-check',
|
|
'POST /mindspace/v1/pages/:pageId/redact',
|
|
'POST /mindspace/v1/pages/:pageId/publish-fix',
|
|
'POST /mindspace/v1/pages/:pageId/redacted-copy',
|
|
'POST /mindspace/v1/pages/:pageId/publish',
|
|
'POST /mindspace/v1/publications/:publicationId/update-status',
|
|
'POST /mindspace/v1/publications/:publicationId/offline',
|
|
'GET /mindspace/v1/publications/:publicationId/stats',
|
|
]);
|
|
});
|
|
|
|
test('thumbnail read and upload preserve headers and payload mapping', async () => {
|
|
const received = [];
|
|
const api = createRouterRecorder();
|
|
const setup = createDependencies({
|
|
getMindSpacePages: () => ({
|
|
async renderThumbnail(userId, pageId) {
|
|
received.push({ kind: 'render', userId, pageId });
|
|
return '<svg>page</svg>';
|
|
},
|
|
async uploadThumbnail(userId, pageId, input) {
|
|
received.push({ kind: 'upload', userId, pageId, input });
|
|
return { uploaded: true };
|
|
},
|
|
}),
|
|
});
|
|
attachPortalMindSpacePagePublishRoutes(api, setup.dependencies);
|
|
|
|
const thumbnailRes = createResponseRecorder();
|
|
await api.routes.get(
|
|
'GET /mindspace/v1/pages/:pageId/thumbnail',
|
|
)(createRequest(), thumbnailRes);
|
|
assert.equal(thumbnailRes.body, '<svg>page</svg>');
|
|
assert.equal(
|
|
thumbnailRes.headers['Content-Type'],
|
|
'image/svg+xml; charset=utf-8',
|
|
);
|
|
assert.equal(
|
|
thumbnailRes.headers['Cache-Control'],
|
|
'private, max-age=300',
|
|
);
|
|
assert.equal(
|
|
thumbnailRes.headers['X-Request-Id'],
|
|
'request-1',
|
|
);
|
|
|
|
const uploadRes = createResponseRecorder();
|
|
await api.routes.get(
|
|
'POST /mindspace/v1/pages/:pageId/thumbnail/upload',
|
|
)(
|
|
createRequest({
|
|
body: {
|
|
image_base64: 'base64',
|
|
mime_type: 'image/png',
|
|
title: 'Title',
|
|
summary: 'Summary',
|
|
html: '<html></html>',
|
|
},
|
|
}),
|
|
uploadRes,
|
|
);
|
|
assert.deepEqual(received[1].input, {
|
|
imageBase64: 'base64',
|
|
mimeType: 'image/png',
|
|
title: 'Title',
|
|
summary: 'Summary',
|
|
html: '<html></html>',
|
|
});
|
|
assert.equal(uploadRes.body.data.uploaded, true);
|
|
});
|
|
|
|
test('thumbnail regeneration preserves AI callback and encryption key', async () => {
|
|
const calls = [];
|
|
const api = createRouterRecorder();
|
|
const setup = createDependencies({
|
|
env: { H5_SETTINGS_ENCRYPTION_KEY: 'encryption-key' },
|
|
getAuthPool: () => ({ id: 'pool-1' }),
|
|
getMindSpacePages: () => ({
|
|
async regenerateThumbnail(userId, pageId, input, options) {
|
|
calls.push({ userId, pageId, input });
|
|
const suggested = await options.suggestCoverMeta({
|
|
title: 'Page',
|
|
});
|
|
return { suggested };
|
|
},
|
|
}),
|
|
async suggestCoverMetaWithAiFn(pool, input) {
|
|
calls.push({ pool, input });
|
|
return { cover: 'ai' };
|
|
},
|
|
});
|
|
attachPortalMindSpacePagePublishRoutes(api, setup.dependencies);
|
|
const res = createResponseRecorder();
|
|
await api.routes.get(
|
|
'POST /mindspace/v1/pages/:pageId/thumbnail/regenerate',
|
|
)(
|
|
createRequest({
|
|
body: {
|
|
html: '<html></html>',
|
|
title: 'Title',
|
|
summary: 'Summary',
|
|
use_ai: true,
|
|
instruction: 'bright',
|
|
},
|
|
}),
|
|
res,
|
|
);
|
|
assert.equal(calls[0].input.useAi, true);
|
|
assert.deepEqual(calls[1], {
|
|
pool: { id: 'pool-1' },
|
|
input: {
|
|
title: 'Page',
|
|
encryptionKey: 'encryption-key',
|
|
},
|
|
});
|
|
assert.equal(res.body.data.suggested.cover, 'ai');
|
|
|
|
const missingPoolApi = createRouterRecorder();
|
|
const missingPool = createDependencies({
|
|
getAuthPool: () => null,
|
|
getMindSpacePages: () => ({
|
|
async regenerateThumbnail(_userId, _pageId, _input, options) {
|
|
return options.suggestCoverMeta({});
|
|
},
|
|
}),
|
|
});
|
|
attachPortalMindSpacePagePublishRoutes(
|
|
missingPoolApi,
|
|
missingPool.dependencies,
|
|
);
|
|
const missingPoolRes = createResponseRecorder();
|
|
await missingPoolApi.routes.get(
|
|
'POST /mindspace/v1/pages/:pageId/thumbnail/regenerate',
|
|
)(
|
|
createRequest({ body: { use_ai: true } }),
|
|
missingPoolRes,
|
|
);
|
|
assert.equal(missingPoolRes.statusCode, 418);
|
|
assert.equal(
|
|
missingPool.calls.routeErrors[0].error.code,
|
|
'llm_not_configured',
|
|
);
|
|
});
|
|
|
|
test('download-link rewrite and previews preserve validation, CSP, and draft input', async () => {
|
|
const calls = [];
|
|
const api = createRouterRecorder();
|
|
const setup = createDependencies({
|
|
getMindSpacePages: () => ({
|
|
async rewriteHtmlDownloadLinksForPage(
|
|
userId,
|
|
pageId,
|
|
content,
|
|
) {
|
|
calls.push({ kind: 'rewrite', userId, pageId, content });
|
|
return '<a>localized</a>';
|
|
},
|
|
async renderPreview() {
|
|
return {
|
|
html: '<html>preview</html>',
|
|
contentFormat: 'html',
|
|
};
|
|
},
|
|
async renderDraftPreview(userId, pageId, input) {
|
|
calls.push({ kind: 'draft', userId, pageId, input });
|
|
return {
|
|
html: '<html>draft</html>',
|
|
contentFormat: 'markdown',
|
|
};
|
|
},
|
|
}),
|
|
previewContentSecurityPolicy(format) {
|
|
calls.push({ kind: 'csp', format });
|
|
return `policy-${format}`;
|
|
},
|
|
});
|
|
attachPortalMindSpacePagePublishRoutes(api, setup.dependencies);
|
|
|
|
const rewriteRes = createResponseRecorder();
|
|
await api.routes.get(
|
|
'POST /mindspace/v1/pages/:pageId/rewrite-download-links',
|
|
)(
|
|
createRequest({ body: { content: ' <a>file</a> ' } }),
|
|
rewriteRes,
|
|
);
|
|
assert.equal(calls[0].content, '<a>file</a>');
|
|
assert.equal(rewriteRes.body.data.html, '<a>localized</a>');
|
|
|
|
const previewRes = createResponseRecorder();
|
|
await api.routes.get(
|
|
'GET /mindspace/v1/pages/:pageId/preview',
|
|
)(createRequest(), previewRes);
|
|
assert.equal(
|
|
previewRes.headers['Content-Security-Policy'],
|
|
'policy-html',
|
|
);
|
|
assert.equal(previewRes.headers['Cache-Control'], 'private, no-store');
|
|
assert.equal(previewRes.headers['X-Content-Type-Options'], 'nosniff');
|
|
|
|
const draftRes = createResponseRecorder();
|
|
await api.routes.get(
|
|
'POST /mindspace/v1/pages/:pageId/preview-draft',
|
|
)(
|
|
createRequest({
|
|
body: {
|
|
title: 'Draft',
|
|
summary: 'Summary',
|
|
content: 'Body',
|
|
template_id: 'template-1',
|
|
},
|
|
}),
|
|
draftRes,
|
|
);
|
|
assert.deepEqual(
|
|
calls.find((call) => call.kind === 'draft').input,
|
|
{
|
|
title: 'Draft',
|
|
summary: 'Summary',
|
|
content: 'Body',
|
|
templateId: 'template-1',
|
|
},
|
|
);
|
|
assert.equal(
|
|
draftRes.headers['Content-Security-Policy'],
|
|
'policy-markdown',
|
|
);
|
|
|
|
const invalidRes = createResponseRecorder();
|
|
await api.routes.get(
|
|
'POST /mindspace/v1/pages/:pageId/rewrite-download-links',
|
|
)(createRequest({ body: { content: ' ' } }), invalidRes);
|
|
assert.equal(invalidRes.statusCode, 418);
|
|
assert.equal(
|
|
setup.calls.routeErrors.at(-1).error.code,
|
|
'invalid_page_input',
|
|
);
|
|
});
|
|
|
|
test('publish check and page transforms preserve inputs and audits', async () => {
|
|
const calls = [];
|
|
const audit = [];
|
|
const api = createRouterRecorder();
|
|
const transformResult = {
|
|
page: { id: 'page-1' },
|
|
originalScan: { riskLevel: 'medium' },
|
|
};
|
|
const setup = createDependencies({
|
|
getMindSpacePublications: () => ({
|
|
async check(userId, pageId, input) {
|
|
calls.push({ kind: 'check', userId, pageId, input });
|
|
return { allowed: true };
|
|
},
|
|
}),
|
|
getMindSpacePages: () => ({
|
|
async redactPage(userId, pageId, input) {
|
|
calls.push({ kind: 'redact', userId, pageId, input });
|
|
return transformResult;
|
|
},
|
|
async localizePrivateResources(userId, pageId, input) {
|
|
calls.push({ kind: 'fix', userId, pageId, input });
|
|
return transformResult;
|
|
},
|
|
}),
|
|
getMindSpaceAudit: () => ({
|
|
async write(entry) {
|
|
audit.push(entry);
|
|
},
|
|
}),
|
|
});
|
|
attachPortalMindSpacePagePublishRoutes(api, setup.dependencies);
|
|
const body = {
|
|
page_version_id: 'version-1',
|
|
expected_version: 2,
|
|
access_mode: 'public',
|
|
url_slug: 'page',
|
|
password: 'secret',
|
|
expires_at: '2030-01-01',
|
|
title: 'Title',
|
|
summary: 'Summary',
|
|
content: 'Body',
|
|
};
|
|
|
|
await api.routes.get(
|
|
'POST /mindspace/v1/pages/:pageId/publish-check',
|
|
)(createRequest({ body }), createResponseRecorder());
|
|
assert.deepEqual(calls[0].input, {
|
|
pageVersionId: 'version-1',
|
|
accessMode: 'public',
|
|
urlSlug: 'page',
|
|
password: 'secret',
|
|
expiresAt: '2030-01-01',
|
|
});
|
|
|
|
for (const route of [
|
|
'POST /mindspace/v1/pages/:pageId/redact',
|
|
'POST /mindspace/v1/pages/:pageId/publish-fix',
|
|
'POST /mindspace/v1/pages/:pageId/redacted-copy',
|
|
]) {
|
|
await api.routes.get(route)(
|
|
createRequest({ body }),
|
|
createResponseRecorder(),
|
|
);
|
|
}
|
|
assert.deepEqual(
|
|
calls.slice(1).map((call) => call.kind),
|
|
['redact', 'fix', 'redact'],
|
|
);
|
|
assert.deepEqual(
|
|
audit.map((entry) => entry.action),
|
|
['page.redact', 'page.publish_fix', 'page.redact'],
|
|
);
|
|
assert.equal(audit[0].riskLevel, 'medium');
|
|
});
|
|
|
|
test('publish preserves publication input, Page Data policy sync, audit, and 201', async () => {
|
|
const calls = [];
|
|
const api = createRouterRecorder();
|
|
const setup = createDependencies({
|
|
getMindSpacePublications: () => ({
|
|
async publish(userId, pageId, input) {
|
|
calls.push({ kind: 'publish', userId, pageId, input });
|
|
return {
|
|
id: 'publication-1',
|
|
accessMode: 'public',
|
|
};
|
|
},
|
|
}),
|
|
getMindSpaceAudit: () => ({
|
|
async write(entry) {
|
|
calls.push({ kind: 'audit', entry });
|
|
},
|
|
}),
|
|
syncPageDataPolicyAccessModeFn(
|
|
workspaceRoot,
|
|
pageId,
|
|
accessMode,
|
|
userId,
|
|
) {
|
|
calls.push({
|
|
kind: 'sync-policy',
|
|
workspaceRoot,
|
|
pageId,
|
|
accessMode,
|
|
userId,
|
|
});
|
|
return { pageId, accessMode };
|
|
},
|
|
async upsertPageDataPolicyIndexFn(pool, policy) {
|
|
calls.push({ kind: 'upsert-policy', pool, policy });
|
|
},
|
|
});
|
|
attachPortalMindSpacePagePublishRoutes(api, setup.dependencies);
|
|
const res = createResponseRecorder();
|
|
await api.routes.get('POST /mindspace/v1/pages/:pageId/publish')(
|
|
createRequest({
|
|
body: {
|
|
page_version_id: 'version-1',
|
|
access_mode: 'public',
|
|
url_slug: 'page',
|
|
password: 'secret',
|
|
expires_at: '2030-01-01',
|
|
acknowledged_finding_ids: ['finding-1'],
|
|
},
|
|
}),
|
|
res,
|
|
);
|
|
assert.deepEqual(calls[0].input, {
|
|
pageVersionId: 'version-1',
|
|
accessMode: 'public',
|
|
urlSlug: 'page',
|
|
password: 'secret',
|
|
expiresAt: '2030-01-01',
|
|
acknowledgedFindingIds: ['finding-1'],
|
|
});
|
|
assert.deepEqual(calls.map((call) => call.kind), [
|
|
'publish',
|
|
'sync-policy',
|
|
'upsert-policy',
|
|
'audit',
|
|
]);
|
|
assert.equal(res.statusCode, 201);
|
|
});
|
|
|
|
test('publication status, offline, and stats preserve forwarding and audits', async () => {
|
|
const calls = [];
|
|
const api = createRouterRecorder();
|
|
const setup = createDependencies({
|
|
getMindSpacePublications: () => ({
|
|
async updatePublicationStatus(userId, publicationId, input) {
|
|
calls.push({
|
|
kind: 'status',
|
|
userId,
|
|
publicationId,
|
|
input,
|
|
});
|
|
return { id: publicationId, accessMode: input.accessMode };
|
|
},
|
|
async offline(userId, publicationId) {
|
|
calls.push({ kind: 'offline', userId, publicationId });
|
|
return { id: publicationId, status: 'offline' };
|
|
},
|
|
async getStats(userId, publicationId) {
|
|
calls.push({ kind: 'stats', userId, publicationId });
|
|
return { views: 8 };
|
|
},
|
|
}),
|
|
getMindSpaceAudit: () => ({
|
|
async write(entry) {
|
|
calls.push({ kind: 'audit', entry });
|
|
},
|
|
}),
|
|
});
|
|
attachPortalMindSpacePagePublishRoutes(api, setup.dependencies);
|
|
|
|
await api.routes.get(
|
|
'POST /mindspace/v1/publications/:publicationId/update-status',
|
|
)(
|
|
createRequest({
|
|
body: {
|
|
access_mode: 'password',
|
|
expires_at: '2030-01-01',
|
|
},
|
|
}),
|
|
createResponseRecorder(),
|
|
);
|
|
assert.deepEqual(calls[0].input, {
|
|
accessMode: 'password',
|
|
expiresAt: '2030-01-01',
|
|
});
|
|
assert.equal(calls[1].entry.action, 'publication.status_updated');
|
|
|
|
await api.routes.get(
|
|
'POST /mindspace/v1/publications/:publicationId/offline',
|
|
)(createRequest(), createResponseRecorder());
|
|
assert.equal(calls[2].kind, 'offline');
|
|
assert.equal(calls[3].entry.action, 'page.offline');
|
|
|
|
const statsRes = createResponseRecorder();
|
|
await api.routes.get(
|
|
'GET /mindspace/v1/publications/:publicationId/stats',
|
|
)(createRequest(), statsRes);
|
|
assert.equal(calls[4].kind, 'stats');
|
|
assert.equal(statsRes.body.data.views, 8);
|
|
});
|
|
|
|
test('page publish routes preserve unavailable and mapped errors', async () => {
|
|
const unavailableApi = createRouterRecorder();
|
|
const unavailable = createDependencies({
|
|
getMindSpacePages: () => null,
|
|
getMindSpacePublications: () => null,
|
|
});
|
|
attachPortalMindSpacePagePublishRoutes(
|
|
unavailableApi,
|
|
unavailable.dependencies,
|
|
);
|
|
const thumbnailRes = createResponseRecorder();
|
|
await unavailableApi.routes.get(
|
|
'GET /mindspace/v1/pages/:pageId/thumbnail',
|
|
)(createRequest(), thumbnailRes);
|
|
assert.equal(thumbnailRes.statusCode, 503);
|
|
const statsRes = createResponseRecorder();
|
|
await unavailableApi.routes.get(
|
|
'GET /mindspace/v1/publications/:publicationId/stats',
|
|
)(createRequest(), statsRes);
|
|
assert.equal(statsRes.statusCode, 503);
|
|
|
|
const failureApi = createRouterRecorder();
|
|
const failure = createDependencies({
|
|
getMindSpacePages: () => ({
|
|
async renderThumbnail() {
|
|
throw new Error('thumbnail failed');
|
|
},
|
|
}),
|
|
});
|
|
attachPortalMindSpacePagePublishRoutes(
|
|
failureApi,
|
|
failure.dependencies,
|
|
);
|
|
const failureRes = createResponseRecorder();
|
|
await failureApi.routes.get(
|
|
'GET /mindspace/v1/pages/:pageId/thumbnail',
|
|
)(createRequest(), failureRes);
|
|
assert.equal(failureRes.statusCode, 418);
|
|
assert.equal(
|
|
failure.calls.routeErrors[0].error.message,
|
|
'thumbnail failed',
|
|
);
|
|
});
|