import assert from 'node:assert/strict'; import test from 'node:test'; import { createPortalWorkspacePublicationDelivery, } from './portal-workspace-publication-delivery.mjs'; function createResponse() { return { statusCode: 200, body: undefined, headers: {}, headersSent: false, sentFile: null, redirectValue: 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; }, json(body) { this.body = body; return this; }, send(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: '/user/public/page.html', originalUrl: '/MindSpace/user/public/page.html', url: '/user/public/page.html', query: {}, headers: { host: 'portal.example', }, protocol: 'https', userSession: null, userToken: null, get(name) { return name === 'user-agent' ? 'test-agent' : ''; }, ...overrides, }; } function createSetup(overrides = {}) { let publicResult = { action: 'serve', filePath: '/workspace/file.txt', ownerKey: 'user-1', }; let authPool = null; let userAuth = null; const calls = []; const fsApi = { readFileSync() { return 'Demo
Demo
'; }, existsSync() { return false; }, }; const service = createPortalWorkspacePublicationDelivery({ h5Root: '/project', internalAgentSecret: 'test-secret', analyticsConfig: { enabled: false, }, getAuthPool: () => authPool, getUserAuth: () => userAuth, getMindSpacePages: () => null, resolveRequestOrigin: () => 'https://portal.example', removeQueryParam(url, key) { calls.push([ 'remove-query', url, key, ]); return url; }, fsApi, async resolvePublicRequest(options) { calls.push([ 'resolve-public-request', options, ]); return publicResult; }, resolveUserPublishDir( _h5Root, user, ) { return `/project/MindSpace/${user.id}`; }, async getDeliveryContract() { return null; }, ensureThumbnail(svgPath) { calls.push(['thumbnail', svgPath]); return svgPath.replace(/\.svg$/, '.png'); }, ...overrides, }); return { service, calls, fsApi, setPublicResult(value) { publicResult = value; }, setAuthPool(value) { authPool = value; }, setUserAuth(value) { userAuth = value; }, }; } async function serve(setup, request) { const response = createResponse(); await setup.service.serveUserPublishFile( request ?? createRequest(), response, () => {}, ); return response; } test('maps redirects, forbidden results, and all not-found reasons', async () => { const setup = createSetup(); setup.setPublicResult({ action: 'redirect', status: 302, location: '/canonical', }); const redirect = await serve(setup); assert.equal(redirect.statusCode, 302); assert.equal( redirect.redirectValue, '/canonical', ); setup.setPublicResult({ action: 'forbidden', }); const forbidden = await serve(setup); assert.equal(forbidden.statusCode, 403); assert.deepEqual(forbidden.body, { message: '禁止访问', }); for (const [reason, message] of [ ['missing_owner_dir', '用户不存在'], [ 'missing_directory_index', '目录中没有 index.html', ], ['missing_file', '文件不存在'], ]) { setup.setPublicResult({ action: 'not_found', reason, }); const response = await serve(setup); assert.equal(response.statusCode, 404); assert.deepEqual(response.body, { message, }); } }); test('preserves username database resolution passed to public request resolver', async () => { let resolvedUserId = null; const authPool = { async query(sql, params) { assert.match(sql, /h5_users/); assert.deepEqual(params, ['alice']); return [[{ id: 'user-alice' }]]; }, }; const setup = createSetup({ getAuthPool: () => authPool, async resolvePublicRequest(options) { resolvedUserId = await options.resolveUsernameToUserId( 'alice', ); return { action: 'redirect', location: '/canonical', }; }, }); await serve(setup); assert.equal( resolvedUserId, 'user-alice', ); }); test('blocks HTML delivery while its delivery contract is not ready', async () => { const authPool = { async query() { return [[]]; }, }; const setup = createSetup({ getAuthPool: () => authPool, async resolvePublicRequest() { return { action: 'serve', filePath: '/project/MindSpace/user-1/public/page.html', ownerKey: 'user-1', }; }, async getDeliveryContract(options) { assert.equal(options.pool, authPool); assert.equal( options.userId, 'user-1', ); assert.equal( options.relativePath, 'public/page.html', ); return { status: 'validating' }; }, }); const response = await serve(setup); assert.equal(response.statusCode, 409); assert.match( response.body, /正在完成发布验证/, ); }); test('materializes thumbnail PNG sidecars before normal file delivery', async () => { const setup = createSetup({ fsApi: { readFileSync() { throw new Error('not used'); }, existsSync() { return true; }, }, async resolvePublicRequest() { return { action: 'serve', filePath: '/workspace/page.thumbnail.png', ownerKey: 'user-1', }; }, ensureThumbnail(svgPath) { assert.equal( svgPath, '/workspace/page.thumbnail.svg', ); return '/workspace/page.thumbnail.png'; }, }); const response = await serve(setup); assert.equal( response.sentFile, '/workspace/page.thumbnail.png', ); assert.equal( response.headers['Cache-Control'], 'public, max-age=300', ); }); test('serves non-HTML files unchanged and decorates HTML as private no-store', async () => { const setup = createSetup(); const fileResponse = createResponse(); await setup.service.sendPublishFile( createRequest(), fileResponse, '/workspace/file.txt', ); assert.equal( fileResponse.sentFile, '/workspace/file.txt', ); const htmlResponse = createResponse(); await setup.service.sendPublishFile( createRequest({ path: '/00000000-0000-0000-0000-000000000001/public/page.html', originalUrl: '/MindSpace/00000000-0000-0000-0000-000000000001/public/page.html', }), htmlResponse, '/project/MindSpace/00000000-0000-0000-0000-000000000001/public/page.html', { isOwner: false }, ); assert.equal( htmlResponse.statusCode, 200, ); assert.equal( htmlResponse.headers['Cache-Control'], 'private, no-store', ); assert.equal( htmlResponse.headers['Content-Type'], 'text/html; charset=utf-8', ); assert.match( String(htmlResponse.body), /Demo/, ); });