Files
memind/server/portal-static-delivery-routes.test.mjs
T

435 lines
8.8 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import {
attachPortalStaticDeliveryRoutes,
} from './portal-static-delivery-routes.mjs';
function createResponse() {
return {
statusCode: 200,
body: undefined,
contentType: null,
headers: {},
redirectValue: null,
sentFile: null,
ended: false,
status(code) {
this.statusCode = code;
return this;
},
type(value) {
this.contentType = 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;
},
sendFile(filePath, callback) {
this.sentFile = filePath;
callback?.(null);
return this;
},
redirect(status, location) {
this.statusCode = status;
this.redirectValue = location;
return this;
},
end() {
this.ended = true;
return this;
},
};
}
function createSetup(overrides = {}) {
const routes = [];
const staticCalls = [];
const calls = [];
const app = {
get(routePath, ...handlers) {
routes.push({
method: 'get',
path: routePath,
handlers,
});
},
use(routePath, ...handlers) {
if (typeof routePath === 'function') {
routes.push({
method: 'use',
path: null,
handlers: [routePath, ...handlers],
});
return;
}
routes.push({
method: 'use',
path: routePath,
handlers,
});
},
};
attachPortalStaticDeliveryRoutes({
app,
h5Root: '/project',
host: '127.0.0.1',
port: 8081,
publishRootDir: 'MindSpace',
usersRoot: '/users',
resolveRequestOrigin: () =>
'https://portal.example',
staticMiddleware(directory, options) {
staticCalls.push([directory, options]);
return function staticHandler() {};
},
fetchFn: async (url, options) => {
calls.push(['fetch', url, options]);
return {
ok: true,
status: 200,
async text() {
return '<html>upstream</html>';
},
};
},
injectOgTagsFn(html, context) {
calls.push(['inject', html, context]);
return '<html>injected</html>';
},
extractSharePreviewMetaFn(html, context) {
calls.push(['extract', html, context]);
return { title: 'Preview' };
},
renderWechatSharePreviewHtmlFn(
preview,
options,
) {
calls.push(['render', preview, options]);
return '<html>preview</html>';
},
...overrides,
});
return {
routes,
staticCalls,
calls,
route(method, routePath) {
return routes.find(
(route) =>
route.method === method &&
route.path === routePath,
);
},
};
}
async function invoke(
setup,
method,
routePath,
request = {},
) {
const route = setup.route(method, routePath);
assert.ok(
route,
`${method.toUpperCase()} ${routePath}`,
);
const response = createResponse();
let nextCalled = false;
await route.handlers.at(-1)(
{
path: '/',
url: '/',
query: {},
get: () => '',
...request,
},
response,
() => {
nextCalled = true;
},
);
return { response, nextCalled };
}
test('registers static delivery routes and middleware in legacy order', () => {
const setup = createSetup();
assert.deepEqual(
setup.routes.map(({ method, path: routePath }) => [
method,
routePath instanceof RegExp
? routePath.toString()
: routePath,
]),
[
['use', '/temp'],
['use', '/user'],
['get', '/MindSpace/wiki/*'],
['get', '/temp/wiki/*'],
['use', '/plaza-covers'],
['use', '/brand'],
['use', '/assets'],
['get', '/dev/wechat-share-preview'],
['use', '/dev'],
[
'get',
'/^\\/MP_verify_[A-Za-z0-9]+\\.txt$/',
],
['use', '/auth'],
['use', '/admin-api'],
['use', null],
['get', '*'],
],
);
assert.deepEqual(setup.staticCalls, [
[
'/project/public/plaza-covers',
{ maxAge: '7d' },
],
[
'/project/public/brand',
{ maxAge: '7d' },
],
[
'/project/public/assets',
{ maxAge: '1d' },
],
['/project/public/dev', { maxAge: 0 }],
[
'/project/dist',
{ index: 'index.html' },
],
]);
});
test('preserves temp and wiki redirects plus Wiki SPA delivery', async () => {
const setup = createSetup();
const temp = await invoke(
setup,
'use',
'/temp',
{ url: '/wiki/page' },
);
assert.equal(temp.response.statusCode, 301);
assert.equal(
temp.response.redirectValue,
'/MindSpace/wiki/page',
);
const wiki = await invoke(
setup,
'get',
'/MindSpace/wiki/*',
);
assert.equal(
wiki.response.sentFile,
'/project/MindSpace/wiki/index.html',
);
const legacyWiki = await invoke(
setup,
'get',
'/temp/wiki/*',
{ url: '/temp/wiki/topic' },
);
assert.equal(
legacyWiki.response.redirectValue,
'/MindSpace/wiki/topic',
);
});
test('preserves public user file traversal and missing-user responses', async () => {
const setup = createSetup({
fsApi: {
existsSync() {
return false;
},
},
});
const empty = await invoke(
setup,
'use',
'/user',
{ path: '/' },
);
assert.equal(empty.nextCalled, true);
const missing = await invoke(
setup,
'use',
'/user',
{ path: '/john/page.html' },
);
assert.equal(missing.response.statusCode, 404);
assert.deepEqual(missing.response.body, {
message: '用户不存在',
});
const traversal = await invoke(
setup,
'use',
'/user',
{ path: '/../outside.txt' },
);
assert.equal(traversal.response.statusCode, 403);
assert.deepEqual(traversal.response.body, {
message: '禁止访问',
});
});
test('serves public user files and directory indexes', async (t) => {
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'portal-static-'),
);
t.after(() => fs.rmSync(root, {
recursive: true,
force: true,
}));
fs.mkdirSync(
path.join(root, 'john', 'guide'),
{ recursive: true },
);
fs.writeFileSync(
path.join(root, 'john', 'guide', 'index.html'),
'guide',
);
fs.writeFileSync(
path.join(root, 'john', 'note.txt'),
'note',
);
const setup = createSetup({
usersRoot: root,
});
const directory = await invoke(
setup,
'use',
'/user',
{ path: '/john/guide' },
);
assert.equal(
directory.response.sentFile,
path.join(
root,
'john',
'guide',
'index.html',
),
);
const file = await invoke(
setup,
'use',
'/user',
{ path: '/john/note.txt' },
);
assert.equal(
file.response.sentFile,
path.join(root, 'john', 'note.txt'),
);
});
test('preserves WeChat share preview validation and rendering', async () => {
const setup = createSetup();
const missing = await invoke(
setup,
'get',
'/dev/wechat-share-preview',
);
assert.equal(missing.response.statusCode, 400);
assert.match(missing.response.body, /缺少 url 参数/);
const rendered = await invoke(
setup,
'get',
'/dev/wechat-share-preview',
{
query: {
url: '/MindSpace/john/page.html',
},
get: () => 'test-agent',
},
);
assert.equal(
rendered.response.body,
'<html>preview</html>',
);
assert.equal(
rendered.response.headers['Cache-Control'],
'no-store',
);
assert.deepEqual(setup.calls[0], [
'fetch',
'https://portal.example/MindSpace/john/page.html',
{
headers: {
'user-agent': 'test-agent',
accept: 'text/html,*/*',
},
redirect: 'follow',
},
]);
});
test('preserves verification, API fallbacks, and SPA fallback', async () => {
const missingSetup = createSetup({
fsApi: {
existsSync() {
return false;
},
},
});
const verification = missingSetup.routes.find(
(route) =>
route.path instanceof RegExp,
);
const missingResponse = createResponse();
verification.handlers[0](
{ path: '/MP_verify_abc123.txt' },
missingResponse,
);
assert.equal(missingResponse.statusCode, 404);
assert.equal(missingResponse.ended, true);
for (const routePath of [
'/auth',
'/admin-api',
]) {
const fallback = await invoke(
missingSetup,
'use',
routePath,
);
assert.equal(
fallback.response.statusCode,
404,
);
assert.match(
fallback.response.body.message,
/接口不存在/,
);
}
const spa = await invoke(
missingSetup,
'get',
'*',
);
assert.equal(
spa.response.sentFile,
'/project/dist/index.html',
);
});