Files
memind/server/portal-workspace-publication-delivery.test.mjs

344 lines
7.6 KiB
JavaScript

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 '<!doctype html><html><head><title>Demo</title></head><body><main>Demo</main></body></html>';
},
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/,
);
});