Files
memind/mindspace-public-delivery.test.mjs
john ac520d8ae5 fix(mindspace-public): 公开页区分作者与访客视图 + 图片加载自动重试
工作区直链 /MindSpace/<userId>/public/*.html 此前对所有人(含匿名访客、
转发链接收到的其他登录用户)展示完全相同的悬浮操作按钮,导致非作者也能
看到"发布 Plaza"入口——而 Plaza 发布会把内容归属到操作者自己的账号下,
必须只对作者开放。

- server.mjs: serveUserPublishFile 用现有 session/cookie 鉴权判断访问者是
  否等于 URL 中的 ownerKey,结果透传给 sendPublishFile;该响应内容因人
  而异,显式加 Cache-Control: private, no-store(原来未设置任何缓存头)
- mindspace-public-share-widget.mjs: injectPublicFileShareButton 新增
  isOwner 参数(默认 true 保持兼容),非作者时隐藏"发布 Plaza"按钮与确认
  弹窗,"保存长图"/"公开分享"对所有访客保留
- mindspace-public-delivery.mjs: 透传 isOwner,并注入图片重试脚本
- mindspace-public-image-retry.mjs(新增): 页面级 onerror 之前用捕获阶段
  监听拦截 mindspace 资产图片的加载失败,带退避自动重试 3 次,避免刚生成
  页面时的资产物化竞态被"永久占位符"放大成显示故障

本地起服务用 owner / 非 owner 已登录用户 / 匿名访客三种身份实测验证。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 10:56:26 +08:00

175 lines
6.0 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
collectInlineScriptHashes,
decorateMindSpacePublishedHtml,
handleMindSpaceLongImageDownload,
} from './mindspace-public-delivery.mjs';
test('collectInlineScriptHashes returns sha256 hashes for inline scripts only', () => {
const html = `<html><body>
<script>console.log('page')</script>
<script src="https://cdn.example/app.js"></script>
<script>console.log('share')</script>
</body></html>`;
const hashes = collectInlineScriptHashes(html);
assert.equal(hashes.length, 2);
assert.match(hashes[0], /^[A-Za-z0-9+/]+=*$/);
assert.notEqual(hashes[0], hashes[1]);
});
test('handleMindSpaceLongImageDownload renders and downloads when requested', async () => {
const calls = [];
const res = {
headersSent: false,
set(key, value) {
calls.push(['set', key, value]);
return this;
},
download(filePath, filename, cb) {
calls.push(['download', filePath, filename]);
cb?.();
},
status(code) {
calls.push(['status', code]);
return this;
},
type(value) {
calls.push(['type', value]);
return this;
},
send(value) {
calls.push(['send', value]);
return this;
},
json(value) {
calls.push(['json', value]);
return this;
},
};
const handled = await handleMindSpaceLongImageDownload({
query: { download: 'long-image' },
filePath: '/tmp/report.html',
pageUrl: 'https://m.tkmind.cn/MindSpace/u/public/report.html',
res,
isLongImageDownloadRequest: () => true,
longImagePathForHtml: () => '/tmp/report.long.png',
renderLongImage: async (...args) => {
calls.push(['render', ...args]);
},
});
assert.equal(handled, true);
assert.deepEqual(calls[0], ['render', {
url: 'https://m.tkmind.cn/MindSpace/u/public/report.html',
outputPath: '/tmp/report.long.png',
}]);
assert.deepEqual(calls[1], ['set', 'Cache-Control', 'no-store']);
assert.deepEqual(calls[2], ['download', '/tmp/report.long.png', 'report.long.png']);
});
test('handleMindSpaceLongImageDownload falls back to local html path without page url', async () => {
const calls = [];
const res = {
headersSent: false,
set() { return this; },
download() {},
status() { return this; },
type() { return this; },
send() { return this; },
};
await handleMindSpaceLongImageDownload({
query: { download: 'long-image' },
filePath: '/tmp/report.html',
res,
isLongImageDownloadRequest: () => true,
longImagePathForHtml: () => '/tmp/report.long.png',
renderLongImage: async (...args) => {
calls.push(['render', ...args]);
},
});
assert.deepEqual(calls[0], ['render', { htmlPath: '/tmp/report.html', outputPath: '/tmp/report.long.png' }]);
});
test('decorateMindSpacePublishedHtml returns decorated html and csp', () => {
const pageScript = "document.body.dataset.ready='1'";
const shareScript = "document.body.dataset.share='1'";
let sharedIsOwner;
const result = decorateMindSpacePublishedHtml({
html: `<html><body><script>${pageScript}</script>demo</body></html>`,
embed: false,
context: {
origin: 'https://m.tkmind.cn',
pageUrl: 'https://m.tkmind.cn/MindSpace/u/public/report.html',
pageDirUrl: 'https://m.tkmind.cn/MindSpace/u/public/',
fallbackImageUrl: 'https://m.tkmind.cn/MindSpace/u/public/report.thumbnail.png',
},
userAgent: 'MicroMessenger',
preparePublicationHtmlForEmbed: (value) => value,
injectOgTags: (value, meta) => `${value}<!--og:${meta.pageUrl}-->`,
injectWechatShareBridge: (value, meta) => `${value}<!--wechat:${meta.pageUrl}-->`,
injectPublicFileShareButton: (value, options) => {
sharedIsOwner = options?.isOwner;
return {
html: value.replace('</body>', `<script>${shareScript}</script></body>`),
scriptHashes: ['ignored'],
};
},
publishedPageCsp: (_value, options) => JSON.stringify(options),
isWechatUserAgent: () => true,
});
assert.match(result.html, /og:/);
assert.match(result.html, /wechat:/);
assert.match(result.html, /dataset\.share/);
assert.match(result.html, /data-mindspace-image-retry="1"/);
assert.equal(result.allowEmbedFrame, false);
assert.equal(sharedIsOwner, true);
const options = JSON.parse(result.csp);
assert.equal(options.embed, false);
assert.equal(options.wechatShare, true);
assert.deepEqual(options.scriptHashes, collectInlineScriptHashes(result.html));
assert.equal(options.scriptHashes.length, 3);
});
test('decorateMindSpacePublishedHtml forwards isOwner=false to the share button injector', () => {
let sharedIsOwner;
decorateMindSpacePublishedHtml({
html: '<html><body>demo</body></html>',
embed: false,
isOwner: false,
context: { origin: '', pageUrl: '', pageDirUrl: '', fallbackImageUrl: '' },
preparePublicationHtmlForEmbed: (value) => value,
injectOgTags: (value) => value,
injectWechatShareBridge: (value) => value,
injectPublicFileShareButton: (value, options) => {
sharedIsOwner = options?.isOwner;
return { html: value, scriptHashes: [] };
},
publishedPageCsp: (_value, options) => JSON.stringify(options),
isWechatUserAgent: () => false,
});
assert.equal(sharedIsOwner, false);
});
test('decorateMindSpacePublishedHtml supports embed mode without share injection', () => {
const result = decorateMindSpacePublishedHtml({
html: '<html></html>',
embed: true,
context: { origin: '', pageUrl: '', pageDirUrl: '', fallbackImageUrl: '' },
preparePublicationHtmlForEmbed: (value) => `${value}<!--embed-->`,
injectOgTags: (value) => value,
injectWechatShareBridge: (value) => value,
injectPublicFileShareButton: (value) => ({ html: value, scriptHashes: [] }),
publishedPageCsp: (_value, options) => JSON.stringify(options),
isWechatUserAgent: () => false,
});
assert.match(result.html, /embed/);
assert.equal(result.allowEmbedFrame, true);
assert.equal(result.csp, JSON.stringify({ embed: true, wechatShare: false, scriptHashes: [] }));
});