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 'page'; }, 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, 'page'); 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: '', }, }), uploadRes, ); assert.deepEqual(received[1].input, { imageBase64: 'base64', mimeType: 'image/png', title: 'Title', summary: 'Summary', 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: '', 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 'localized'; }, async renderPreview() { return { html: 'preview', contentFormat: 'html', }; }, async renderDraftPreview(userId, pageId, input) { calls.push({ kind: 'draft', userId, pageId, input }); return { html: 'draft', 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: ' file ' } }), rewriteRes, ); assert.equal(calls[0].content, 'file'); assert.equal(rewriteRes.body.data.html, 'localized'); 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', ); });