Files
memind/server/portal-mindspace-chat-share-routes.test.mjs
T

373 lines
12 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { attachPortalMindSpaceChatShareRoutes } from './portal-mindspace-chat-share-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,
status(code) {
this.statusCode = code;
return this;
},
json(body) {
this.body = body;
return this;
},
};
}
function createRequest(overrides = {}) {
return {
body: {},
query: {},
currentUser: { id: 'user-1', username: 'alice' },
...overrides,
};
}
function createBundle() {
return {
resolvedHtml: {
content: '<html>Page</html>',
filename: 'page.html',
},
};
}
function createDependencies(overrides = {}) {
const services = {
pages: { id: 'pages' },
publications: { id: 'publications' },
plazaPosts: { id: 'plaza-posts' },
};
const calls = {
data: [],
errors: [],
plazaErrors: [],
};
return {
calls,
services,
dependencies: {
h5Root: '/workspace',
env: {},
getMindSpacePages: () => services.pages,
getMindSpacePublications: () => services.publications,
getPlazaPosts: () => services.plazaPosts,
resolveChatSaveBundle: async () => createBundle(),
resolvePlazaPostUrl: (postId) => `/plaza/p/${postId}`,
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 } });
},
handlePlazaError(res, req, error) {
calls.plazaErrors.push({ res, req, error });
return res.status(409).json({ plaza: error.code });
},
handleMindSpaceError(res, req, error) {
calls.errors.push({ res, req, error });
return res.status(418).json({ mapped: error.message });
},
logger: {
log() {},
info() {},
error() {},
},
...overrides,
},
};
}
test('chat share module preserves route inventory and order', () => {
const api = createRouterRecorder();
attachPortalMindSpaceChatShareRoutes(api, createDependencies().dependencies);
assert.deepEqual([...api.routes.keys()], [
'POST /mindspace/v1/pages/quick-share-from-chat',
'POST /mindspace/v1/pages/quick-plaza-from-chat',
'GET /mindspace/v1/pages/quick-plaza-from-public-html/status',
'POST /mindspace/v1/pages/quick-plaza-from-public-html',
]);
});
test('quick share preserves localization, path, write, and 201 response', async () => {
const calls = [];
const api = createRouterRecorder();
const dependencies = createDependencies({
getAuthPool: () => ({ id: 'pool' }),
resolveMindSpaceRuntimeConfigFn: () => ({
storageRoot: '/storage',
}),
async inlinePrivateAssetsInHtmlFn(
pool,
storageRoot,
userId,
html,
) {
calls.push({ kind: 'inline', pool, storageRoot, userId, html });
return { html: '<html>Localized</html>' };
},
resolveMindSpaceUserPublishDirFn: () => '/workspace/user',
async mkdir(dir, options) {
calls.push({ kind: 'mkdir', dir, options });
},
rewriteWorkspacePublicAssetReferencesFn(html, relativePath) {
calls.push({ kind: 'rewrite', html, relativePath });
return '<html>Rewritten</html>';
},
async writeFile(filePath, content, encoding) {
calls.push({ kind: 'write', filePath, content, encoding });
},
randomUUID: () => '12345678-aaaa-bbbb-cccc-dddddddddddd',
buildMindSpacePublicUrlForUserFn(input) {
calls.push({ kind: 'url', input });
return 'https://example.com/MindSpace/user/public/shared/page-12345678.html';
},
});
attachPortalMindSpaceChatShareRoutes(api, dependencies.dependencies);
const res = createResponseRecorder();
await api.routes.get('POST /mindspace/v1/pages/quick-share-from-chat')(
createRequest({ body: { session_id: 'session-1' } }),
res,
);
assert.equal(res.statusCode, 201);
assert.deepEqual(res.body, {
data: {
publicUrl:
'https://example.com/MindSpace/user/public/shared/page-12345678.html',
filename: 'page-12345678.html',
},
});
assert.deepEqual(calls.find((call) => call.kind === 'inline'), {
kind: 'inline',
pool: { id: 'pool' },
storageRoot: '/storage',
userId: 'user-1',
html: '<html>Page</html>',
});
assert.deepEqual(calls.find((call) => call.kind === 'rewrite'), {
kind: 'rewrite',
html: '<html>Localized</html>',
relativePath: 'public/shared/page-12345678.html',
});
assert.deepEqual(calls.find((call) => call.kind === 'write'), {
kind: 'write',
filePath: '/workspace/user/public/shared/page-12345678.html',
content: '<html>Rewritten</html>',
encoding: 'utf8',
});
});
test('quick Plaza from chat preserves dependencies, timing, result, and errors', async () => {
const forwarded = [];
let clock = 1_000;
const api = createRouterRecorder();
const dependencies = createDependencies({
resolveMindSpaceUserPublishDirFn: () => '/workspace/user',
now: () => {
clock += 25;
return clock;
},
async quickPlazaFromChatFn(input) {
forwarded.push(input);
return {
pageId: 'page-1',
publicationId: 'publication-1',
post: { id: 'post-1' },
};
},
});
attachPortalMindSpaceChatShareRoutes(api, dependencies.dependencies);
const req = createRequest({ body: { title: 'Post title' } });
await api.routes.get('POST /mindspace/v1/pages/quick-plaza-from-chat')(
req,
createResponseRecorder(),
);
assert.equal(dependencies.calls.data[0].status, 201);
assert.deepEqual(forwarded[0], {
user: req.currentUser,
h5Root: '/workspace',
bundle: createBundle(),
body: { title: 'Post title' },
mindSpacePages: dependencies.services.pages,
mindSpacePublications: dependencies.services.publications,
plazaPosts: dependencies.services.plazaPosts,
publishDir: '/workspace/user',
});
const failureApi = createRouterRecorder();
const plazaFailure = Object.assign(new Error('conflict'), {
code: 'ALREADY_PUBLISHED',
});
const failureDependencies = createDependencies({
quickPlazaFromChatFn: async () => {
throw plazaFailure;
},
resolveMindSpaceUserPublishDirFn: () => '/workspace/user',
mapPlazaError: () => 409,
});
attachPortalMindSpaceChatShareRoutes(
failureApi,
failureDependencies.dependencies,
);
await failureApi.routes.get(
'POST /mindspace/v1/pages/quick-plaza-from-chat',
)(createRequest(), createResponseRecorder());
assert.equal(
failureDependencies.calls.plazaErrors[0].error,
plazaFailure,
);
});
test('quick Plaza status preserves relative path, viewer result, and URL', async () => {
const forwarded = [];
const api = createRouterRecorder();
const dependencies = createDependencies({
async getQuickPlazaFromPublicHtmlStatusFn(input) {
forwarded.push(input);
return {
published: true,
post: { id: 'post-1' },
};
},
resolvePlazaPostUrl(postId, req) {
assert.equal(req.query.relativePath, ' public/page.html ');
return `https://plaza.example.com/p/${postId}`;
},
});
attachPortalMindSpaceChatShareRoutes(api, dependencies.dependencies);
const req = createRequest({
query: { relativePath: ' public/page.html ' },
});
await api.routes.get(
'GET /mindspace/v1/pages/quick-plaza-from-public-html/status',
)(req, createResponseRecorder());
assert.equal(forwarded[0].relativePath, 'public/page.html');
assert.equal(forwarded[0].mindSpacePages, dependencies.services.pages);
assert.deepEqual(dependencies.calls.data[0].data, {
published: true,
post: { id: 'post-1' },
plaza_url: 'https://plaza.example.com/p/post-1',
});
});
test('quick Plaza public HTML preserves publish input, URL, and 201 response', async () => {
const forwarded = [];
const api = createRouterRecorder();
const dependencies = createDependencies({
resolveMindSpaceUserPublishDirFn: () => '/workspace/user',
async quickPlazaFromPublicHtmlFn(input) {
forwarded.push(input);
return {
pageId: 'page-1',
publicationId: 'publication-1',
post: { id: 'post-1' },
};
},
});
attachPortalMindSpaceChatShareRoutes(api, dependencies.dependencies);
await api.routes.get(
'POST /mindspace/v1/pages/quick-plaza-from-public-html',
)(
createRequest({
body: { relative_path: ' public/page.html ' },
}),
createResponseRecorder(),
);
assert.equal(forwarded[0].relativePath, 'public/page.html');
assert.equal(forwarded[0].publishDir, '/workspace/user');
assert.equal(dependencies.calls.data[0].status, 201);
assert.deepEqual(dependencies.calls.data[0].data, {
pageId: 'page-1',
publicationId: 'publication-1',
post: { id: 'post-1' },
plaza_url: '/plaza/p/post-1',
});
});
test('quick Plaza public HTML enriches already-published errors with URL', async () => {
const failure = Object.assign(new Error('already published'), {
code: 'ALREADY_PUBLISHED',
details: { post_id: 'post-existing' },
});
const api = createRouterRecorder();
const dependencies = createDependencies({
resolveMindSpaceUserPublishDirFn: () => '/workspace/user',
quickPlazaFromPublicHtmlFn: async () => {
throw failure;
},
mapPlazaError: () => 409,
});
attachPortalMindSpaceChatShareRoutes(api, dependencies.dependencies);
await api.routes.get(
'POST /mindspace/v1/pages/quick-plaza-from-public-html',
)(createRequest(), createResponseRecorder());
assert.equal(
failure.details.plaza_url,
'/plaza/p/post-existing',
);
assert.equal(dependencies.calls.plazaErrors[0].error, failure);
});
test('chat share routes preserve availability gates and MindSpace fallback errors', async () => {
const unavailableApi = createRouterRecorder();
const unavailable = createDependencies({
getMindSpacePages: () => null,
getMindSpacePublications: () => null,
getPlazaPosts: () => null,
});
attachPortalMindSpaceChatShareRoutes(
unavailableApi,
unavailable.dependencies,
);
const shareRes = createResponseRecorder();
await unavailableApi.routes.get(
'POST /mindspace/v1/pages/quick-share-from-chat',
)(createRequest(), shareRes);
assert.equal(shareRes.statusCode, 503);
for (const route of [
'POST /mindspace/v1/pages/quick-plaza-from-chat',
'GET /mindspace/v1/pages/quick-plaza-from-public-html/status',
'POST /mindspace/v1/pages/quick-plaza-from-public-html',
]) {
const res = createResponseRecorder();
await unavailableApi.routes.get(route)(createRequest(), res);
assert.equal(res.statusCode, 503, route);
assert.equal(res.body.error.code, 'plaza_unavailable');
}
const failureApi = createRouterRecorder();
const failure = new Error('share failed');
const dependencies = createDependencies({
resolveChatSaveBundle: async () => {
throw failure;
},
});
attachPortalMindSpaceChatShareRoutes(
failureApi,
dependencies.dependencies,
);
await failureApi.routes.get(
'POST /mindspace/v1/pages/quick-share-from-chat',
)(createRequest(), createResponseRecorder());
assert.equal(dependencies.calls.errors.at(-1).error, failure);
});