Files
memind/mindspace-publications.test.mjs
T
john 9b4a25799f Add smart ACK provider for WeChat MP replies
Replace fixed ackText with a rule-based AckProvider that picks
response templates by message type and intent (translate, summary,
rewrite, poster, ppt, mindmap, code, search, schedule). Pure sync,
zero I/O, auto-falls back to config.ackText on any error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 15:19:03 +08:00

198 lines
6.5 KiB
JavaScript

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 { publicationInternals } from './mindspace-publications.mjs';
test('normalizes safe long-page slugs', () => {
assert.equal(publicationInternals.normalizeSlug(' product-intro-2026 '), 'product-intro-2026');
assert.throws(
() => publicationInternals.normalizeSlug('../private'),
(error) => error.code === 'invalid_publish_input',
);
assert.throws(
() => publicationInternals.normalizeSlug('中文地址'),
(error) => error.code === 'invalid_publish_input',
);
});
test('accepts all documented access modes', () => {
for (const mode of [
'public',
'password',
'private_link',
'time_limited',
'login_required',
'owner_only',
]) {
assert.equal(publicationInternals.normalizeAccessMode(mode), mode);
}
assert.throws(
() => publicationInternals.normalizeAccessMode('team_only'),
(error) => error.code === 'invalid_publish_input',
);
});
test('hashes access passwords with a random salt', () => {
const first = publicationInternals.hashPassword('Publish-Password-2026');
const second = publicationInternals.hashPassword('Publish-Password-2026');
assert.notEqual(first, second);
assert.equal(publicationInternals.verifyPassword('Publish-Password-2026', first), true);
assert.equal(publicationInternals.verifyPassword('wrong-password', first), false);
assert.throws(
() => publicationInternals.normalizePassword('short', true),
(error) => error.code === 'invalid_publish_input',
);
});
test('requires future expiry timestamps for time-limited pages', () => {
const future = Date.now() + 60_000;
assert.equal(publicationInternals.normalizeExpiresAt(future, true), future);
assert.throws(
() => publicationInternals.normalizeExpiresAt(Date.now() - 1, true),
(error) => error.code === 'invalid_publish_input',
);
});
test('classifies devices and strips referrer paths', () => {
assert.equal(
publicationInternals.deviceType('Mozilla/5.0 (iPhone; CPU iPhone OS 18_0) Mobile'),
'mobile',
);
assert.equal(publicationInternals.deviceType('Googlebot/2.1'), 'bot');
assert.equal(publicationInternals.deviceType('Mozilla/5.0 (Macintosh)'), 'desktop');
assert.equal(
publicationInternals.referrerHost('https://example.com/private/path?token=secret'),
'example.com',
);
assert.equal(publicationInternals.referrerHost('not a url'), null);
});
test('warns for contact details and external links without silently hiding them', () => {
const result = publicationInternals.scanContent(
'联系 13812345678 或 hello@example.com,详情 https://example.com/path',
);
assert.equal(result.status, 'warned');
assert.equal(result.riskLevel, 'medium');
assert.equal(result.allowed, true);
assert.deepEqual(
result.findings.map((finding) => finding.type),
['phone', 'email', 'external_link'],
);
assert.doesNotMatch(result.findings[0].sampleMasked, /13812345678/);
});
test('deduplicates the same sensitive value repeated by summary and content', () => {
const result = publicationInternals.scanContent(
'摘要 qa@example.com\n正文 qa@example.com',
);
assert.equal(result.findings[0].occurrenceCount, 1);
});
test('blocks identity and bank-card numbers', () => {
const result = publicationInternals.scanContent(
'身份证 310101199001011234,银行卡 6222021234567890123',
);
assert.equal(result.status, 'blocked');
assert.equal(result.riskLevel, 'high');
assert.equal(result.allowed, false);
assert.ok(result.findings.every((finding) => finding.blocking));
});
test('publicHomepageResponse summarizes public cards and total views', () => {
const result = publicationInternals.publicHomepageResponse(
{
id: 'user-1',
slug: 'john',
username: 'john',
display_name: 'John',
},
[
{
id: 'pub-1',
page_id: 'page-1',
title: '第一篇',
summary: '摘要一',
template_id: 'report',
public_url: '/u/john/pages/one',
url_slug: 'one',
view_count: 7,
published_at: 100,
},
{
id: 'pub-2',
page_id: 'page-2',
title: '第二篇',
summary: '摘要二',
template_id: 'editorial',
public_url: '/u/john/pages/two',
url_slug: 'two',
view_count: 3,
published_at: 200,
},
],
);
assert.equal(result.owner.displayName, 'John');
assert.equal(result.totalViews, 10);
assert.equal(result.pageCount, 2);
assert.equal(result.pages[0].publicUrl, '/u/john/pages/one');
});
test('buildPublicationThumbnailFallback points private resources at the public page thumbnail', () => {
assert.equal(
publicationInternals.buildPublicationThumbnailFallback('john', 'page-037d497f'),
'/u/john/pages/page-037d497f.thumbnail.png',
);
assert.equal(
publicationInternals.buildPublicationThumbnailFallback('木子', 'demo-page'),
'/u/%E6%9C%A8%E5%AD%90/pages/demo-page.thumbnail.png',
);
});
test('prepareHtmlPublishContent inlines owned private image assets before scanning', async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-publish-'));
const storageKey = 'users/user-1/assets/asset-1/current.png';
await fs.mkdir(path.dirname(path.join(root, storageKey)), { recursive: true });
await fs.writeFile(path.join(root, storageKey), Buffer.from('png-bytes'));
const pool = {
async query(sql, params) {
assert.match(sql, /h5_assets/);
assert.equal(params[0], 'user-1');
assert.deepEqual(params[1], ['asset-1']);
return [
[
{
id: 'asset-1',
mime_type: 'image/png',
storage_key: storageKey,
},
],
];
},
};
try {
const html = '<!doctype html><img src="/api/mindspace/v1/assets/asset-1/download?inline=1&v=1">';
const prepared = await publicationInternals.prepareHtmlPublishContent({
pool,
userId: 'user-1',
html,
ownerSlug: 'john',
urlSlug: 'korean-daily-look',
absoluteStoragePath: (key) => path.join(root, key),
});
assert.match(prepared, /src="data:image\/png;base64,/);
assert.doesNotMatch(prepared, /\/api\/mindspace\/v1\/assets\//);
const scan = publicationInternals.scanContent(prepared, { format: 'html' });
assert.equal(
scan.findings.some((finding) => finding.type === 'private_resource_reference'),
false,
);
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});