import assert from 'node:assert/strict'; import test from 'node:test'; import { attachPortalPublicationRoutes, renderPublicationPasswordGate, renderPublicHomepage, } from './portal-publication-routes.mjs'; function createResponse() { return { statusCode: 200, body: undefined, headers: {}, headersSent: false, redirectValue: null, sentFile: null, status(code) { this.statusCode = code; return this; }, type(value) { this.headers['Content-Type'] = value; return this; }, set(name, value) { this.headers[name] = value; return this; }, send(body) { this.body = body; return this; }, json(body) { this.body = body; return this; }, redirect(status, location) { this.statusCode = status; this.redirectValue = location; return this; }, sendFile(filePath, callback) { this.sentFile = filePath; callback?.(null); return this; }, }; } function createRequest(overrides = {}) { return { path: '/', originalUrl: '/', params: {}, query: {}, body: {}, userSession: null, userToken: null, get(name) { return { 'user-agent': 'test-agent', referer: 'https://referrer.example', }[name]; }, ...overrides, }; } function createSetup(overrides = {}) { const routes = []; const calls = []; const urlencodedBody = () => {}; let authPool = { async query(sql, params) { calls.push(['query', sql, params]); if (sql.includes('pr.user_id')) { return [[{ user_id: 'user-1', page_id: 'page-1', }]]; } return [[{ id: 'user-1' }]]; }, }; let mindSpacePages = { async renderThumbnail(userId, pageId) { calls.push([ 'thumbnail', userId, pageId, ]); return 'thumbnail'; }, }; let publicResult = { ownerId: 'user-1', workspaceRelativePath: 'public/page.html', workspacePublicUrl: '/MindSpace/user-1/public/page.html', publication: { accessMode: 'public', }, }; let mindSpacePublications = { async resolvePublic(...args) { calls.push(['resolve-public', ...args]); return publicResult; }, async getPublicHomepage(ownerSlug) { calls.push(['homepage', ownerSlug]); return { owner: { displayName: 'Alice', slug: 'alice', }, pages: [], pageCount: 0, totalViews: 0, }; }, async resolvePrivateLink(...args) { calls.push(['resolve-private', ...args]); return { publication: { accessMode: 'private_link', }, }; }, }; let userAuth = { async getMe(token) { calls.push(['get-me', token]); return { id: 'viewer-1' }; }, }; const app = { use(routePath, ...handlers) { if (typeof routePath === 'function') { routes.push({ method: 'use', path: null, handlers: [ routePath, ...handlers, ], }); return; } routes.push({ method: 'use', path: routePath, handlers, }); }, get(routePath, ...handlers) { routes.push({ method: 'get', path: routePath, handlers, }); }, post(routePath, ...handlers) { routes.push({ method: 'post', path: routePath, handlers, }); }, }; const defaults = { app, urlencodedBody, userAuthReady: Promise.resolve(), h5Root: '/project', getAuthPool: () => authPool, getMindSpacePages: () => mindSpacePages, getMindSpacePublications: () => mindSpacePublications, getWorkspacePublicationDelivery: () => ({ async readOwnerPublicAsset({ ownerSlug, requestPath, }) { calls.push([ 'read-owner-public-asset', ownerSlug, requestPath, ]); return { action: 'deliver', kind: 'binary', servedName: 'file.txt', bodyBase64: Buffer.from( 'asset-body', ).toString('base64'), }; }, }), getUserAuth: () => userAuth, async sendPublishedPage( req, res, result, options, ) { calls.push([ 'send-published', result, options, ]); return res.send('published-page'); }, isEmbedRequest(query) { return query.embed === '1'; }, isPublicWorkspaceHtml(value) { return value?.startsWith('public/'); }, rasterizeThumbnail(svg) { calls.push(['rasterize', svg]); return Buffer.from('png'); }, ...overrides, }; attachPortalPublicationRoutes(defaults); return { routes, calls, urlencodedBody, setAuthPool(value) { authPool = value; }, setMindSpacePages(value) { mindSpacePages = value; }, setMindSpacePublications(value) { mindSpacePublications = value; }, setUserAuth(value) { userAuth = value; }, setPublicResult(value) { publicResult = value; }, route(method, routePath) { return routes.find( (route) => route.method === method && route.path === routePath, ); }, }; } async function invoke( setup, method, routePath, request = createRequest(), ) { const route = setup.route(method, routePath); assert.ok( route, `${method.toUpperCase()} ${routePath}`, ); const response = createResponse(); let nextCalled = false; await route.handlers.at(-1)( request, response, () => { nextCalled = true; }, ); return { response, nextCalled }; } test('registers publication route inventory and password middleware in order', () => { const setup = createSetup(); assert.deepEqual( setup.routes.map(({ method, path }) => [ method, path, ]), [ ['use', null], [ 'get', '/u/:ownerSlug/pages/:urlSlug', ], ['use', '/u/:ownerSlug/public'], ['get', '/u/:ownerSlug'], [ 'post', '/u/:ownerSlug/pages/:urlSlug', ], ['get', '/s/:token'], ], ); assert.equal( setup.routes[4].handlers[0], setup.urlencodedBody, ); }); test('public page redirects canonical workspace HTML and preserves viewer metadata', async () => { const setup = createSetup(); const { response } = await invoke( setup, 'get', '/u/:ownerSlug/pages/:urlSlug', createRequest({ params: { ownerSlug: 'alice', urlSlug: 'page', }, userSession: { id: 'session' }, userToken: 'viewer-token', }), ); assert.equal(response.statusCode, 301); assert.equal( response.redirectValue, '/MindSpace/user-1/public/page.html', ); assert.deepEqual( setup.calls.find( (call) => call[0] === 'resolve-public', ), [ 'resolve-public', 'alice', 'page', 'viewer-1', null, { userAgent: 'test-agent', referrer: 'https://referrer.example', }, ], ); }); test('raw, embed, and password access preserve direct page delivery', async () => { const setup = createSetup(); const raw = await invoke( setup, 'get', '/u/:ownerSlug/pages/:urlSlug', createRequest({ params: { ownerSlug: 'alice', urlSlug: 'page', }, query: { view: 'RAW' }, }), ); assert.equal(raw.response.body, 'published-page'); assert.deepEqual( setup.calls.at(-1), [ 'send-published', { ownerId: 'user-1', workspaceRelativePath: 'public/page.html', workspacePublicUrl: '/MindSpace/user-1/public/page.html', publication: { accessMode: 'public', }, }, { embed: false, raw: true, ownerSlug: 'alice', }, ], ); const posted = await invoke( setup, 'post', '/u/:ownerSlug/pages/:urlSlug', createRequest({ params: { ownerSlug: 'alice', urlSlug: 'page', }, query: { embed: '1' }, body: { password: 'password-123', }, }), ); assert.equal( posted.response.body, 'published-page', ); const resolveCall = setup.calls .filter( (call) => call[0] === 'resolve-public', ) .at(-1); assert.equal(resolveCall[4], 'password-123'); }); test('public page preserves password, login, not-found, and generic errors', async () => { for (const [ code, password, status, text, ] of [ [ 'publication_password_required', null, 200, '此页面受密码保护', ], [ 'publication_password_required', 'wrong-password', 403, '此页面受密码保护', ], [ 'publication_login_required', null, 401, '请先登录 TKMind', ], [ 'publication_not_found', null, 404, '页面不存在或已下线', ], [ 'unexpected', null, 500, '页面加载失败', ], ]) { const setup = createSetup(); setup.setMindSpacePublications({ async resolvePublic() { throw Object.assign( new Error(code), { code }, ); }, }); const method = password ? 'post' : 'get'; const result = await invoke( setup, method, '/u/:ownerSlug/pages/:urlSlug', createRequest({ originalUrl: '/u/alice/pages/page', params: { ownerSlug: 'alice', urlSlug: 'page', }, body: { password }, }), ); assert.equal( result.response.statusCode, status, ); assert.match( String(result.response.body), new RegExp(text), ); } }); test('thumbnail middleware preserves pass-through, lookup, headers, and errors', async () => { const setup = createSetup(); const middleware = setup.routes[0]; const response = createResponse(); let nextCalled = false; await middleware.handlers[0]( createRequest({ path: '/other' }), response, () => { nextCalled = true; }, ); assert.equal(nextCalled, true); const thumbnailResponse = createResponse(); await middleware.handlers[0]( createRequest({ path: '/u/alice/pages/page.thumbnail.png', }), thumbnailResponse, () => {}, ); assert.equal( thumbnailResponse.statusCode, 200, ); assert.equal( thumbnailResponse.headers['Content-Type'], 'image/png', ); assert.equal( thumbnailResponse.headers['Cache-Control'], 'public, max-age=300', ); assert.equal( thumbnailResponse.body.toString(), 'png', ); setup.setAuthPool(null); const disabledResponse = createResponse(); await middleware.handlers[0]( createRequest({ path: '/u/alice/pages/page.thumbnail.png', }), disabledResponse, () => {}, ); assert.equal( disabledResponse.statusCode, 503, ); }); test('public asset route proxies logical MindSpace delivery results', async () => { const setup = createSetup(); const success = await invoke( setup, 'use', '/u/:ownerSlug/public', createRequest({ path: '/assets/file.txt', params: { ownerSlug: 'ALICE' }, }), ); assert.equal( success.response.body.toString(), 'asset-body', ); assert.deepEqual( setup.calls.find( (call) => call[0] === 'read-owner-public-asset', ), [ 'read-owner-public-asset', 'alice', '/assets/file.txt', ], ); const missingSetup = createSetup({ getWorkspacePublicationDelivery: () => ({ async readOwnerPublicAsset() { return { action: 'not_found', reason: 'missing_owner', }; }, }), }); const missing = await invoke( missingSetup, 'use', '/u/:ownerSlug/public', createRequest({ path: '/file.txt', params: { ownerSlug: 'alice' }, }), ); assert.equal(missing.response.statusCode, 404); assert.deepEqual(missing.response.body, { message: '用户不存在', }); }); test('homepage and private link preserve headers, viewer, and delivery options', async () => { const setup = createSetup(); const homepage = await invoke( setup, 'get', '/u/:ownerSlug', createRequest({ params: { ownerSlug: 'alice' }, }), ); assert.equal( homepage.response.statusCode, 200, ); assert.match( homepage.response.body, /Alice · MindSpace/, ); assert.equal( homepage.response.headers['Cache-Control'], 'public, max-age=60', ); const privateLink = await invoke( setup, 'get', '/s/:token', createRequest({ params: { token: 'private-token' }, query: { embed: '1', view: 'raw', }, userSession: { id: 'session' }, userToken: 'viewer-token', }), ); assert.equal( privateLink.response.body, 'published-page', ); assert.deepEqual( setup.calls.find( (call) => call[0] === 'resolve-private', ), [ 'resolve-private', 'private-token', 'viewer-1', { userAgent: 'test-agent', referrer: 'https://referrer.example', }, ], ); assert.deepEqual( setup.calls.at(-1)[2], { embed: true, raw: true, }, ); }); test('publication HTML helpers preserve escaping and password form action', () => { const gate = renderPublicationPasswordGate( '/u/alice/pages/private', ); assert.match( gate, /action="\/u\/alice\/pages\/private"/, ); const homepage = renderPublicHomepage({ owner: { displayName: '', slug: 'alice', }, pages: [{ publicUrl: 'https://example.test/?a=1&b=2', templateId: '