import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; import { createPageService, pageInternals, normalizeWorkspaceRelativePath } from './mindspace-pages.mjs'; import { PUBLISH_ROOT_DIR } from './user-publish.mjs'; test('parseJsonColumn accepts mysql json objects and strings', () => { const objectValue = { relative_path: 'public/demo.html', auto_synced: true }; assert.deepEqual(pageInternals.parseJsonColumn(objectValue), objectValue); assert.deepEqual( pageInternals.parseJsonColumn(JSON.stringify(objectValue)), objectValue, ); assert.deepEqual(pageInternals.parseJsonColumn(null, { ok: true }), { ok: true }); }); test('normalizePageInput trims values and constrains templates', () => { const result = pageInternals.normalizePageInput({ title: ' 页面标题 ', content: ' 页面正文 ', templateId: 'unknown-template', }); assert.equal(result.title, '页面标题'); assert.equal(result.content, '页面正文'); assert.equal(result.templateId, 'editorial'); assert.equal(result.contentBytes, Buffer.byteLength('页面正文')); }); test('normalizePageInput rejects empty and oversized page content', () => { assert.throws( () => pageInternals.normalizePageInput({ title: '', content: '正文' }), { code: 'invalid_page_input' }, ); assert.throws( () => pageInternals.normalizePageInput({ title: '页面', content: '文'.repeat(400_000), }), { code: 'page_content_too_large' }, ); }); test('normalizePageInput maps html content to static-html template', () => { const result = pageInternals.normalizePageInput({ title: '页面', content: 'Hi', contentFormat: 'html', }); assert.equal(result.contentFormat, 'html'); assert.equal(result.templateId, 'static-html'); assert.equal(result.pageType, 'html'); }); test('renderHtmlPreview injects restrictive CSP', () => { const html = pageInternals.renderHtmlPreview( '

Hi

', ); assert.match(html, /Content-Security-Policy/); assert.match(html, /script-src 'none'/); }); test('renderPublicationHtml serves html pages without escaping markup', () => { const html = pageInternals.renderPublicationHtml({ page_type: 'html', title: '麻婆豆腐', summary: '川菜', template_id: 'static-html', version_no: 1, content: '麻婆豆腐

麻婆豆腐

', }); assert.match(html, /

麻婆豆腐<\/h1>/); assert.doesNotMatch(html, /<h1>/); assert.doesNotMatch(html, /MINDSPACE/); }); test('normalizeListPageFilters clamps limit and offset', () => { assert.deepEqual(pageInternals.normalizeListPageFilters({ limit: 999, offset: -5 }), { limit: 100, offset: 0, }); assert.deepEqual(pageInternals.normalizeListPageFilters({ limit: 15, offset: 30 }), { limit: 15, offset: 30, }); }); test('pageResponse includes publication metrics when publish row is present', () => { const response = pageInternals.pageResponse( { id: 'page-1', user_id: 'user-1', category_id: 'cat-1', category_code: 'public', source_session_id: null, source_message_id: null, source_asset_id: null, title: '成果页面', summary: '摘要', page_type: 'html', template_id: 'static-html', status: 'published', visibility: 'public', current_version_id: 'ver-1', version_no: 3, pub_access_mode: 'public', pub_public_url: 'https://m.tkmind.cn/u/john/pages/demo', pub_id: 'pub-1', pub_status: 'online', pub_view_count: 128, pub_published_at: 1_700_000_000_000, created_at: 1_699_000_000_000, updated_at: 1_700_100_000_000, }, { includeContent: false }, ); assert.equal(response.publicationId, 'pub-1'); assert.equal(response.publicationStatus, 'online'); assert.equal(response.viewCount, 128); assert.equal(response.clickCount, null); assert.equal(response.statsSource, 'publication'); assert.equal(response.publishedAt, 1_700_000_000_000); assert.equal(response.publicationUrl, 'https://m.tkmind.cn/u/john/pages/demo'); }); test('preview rendering escapes active HTML and emits a restrictive CSP', () => { const html = pageInternals.renderPreviewHtml({ title: '', summary: '', content: '# 标题\n\n', templateId: 'report', versionNo: 2, }); assert.match(html, /Content-Security-Policy/); assert.match(html, /default-src 'none'/); assert.doesNotMatch(html, /