344 lines
7.6 KiB
JavaScript
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/,
|
|
);
|
|
});
|