import assert from 'node:assert/strict'; 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'); });