import assert from 'node:assert/strict';
import test from 'node:test';
import { attachPortalMindSpaceAssetRoutes } from './portal-mindspace-asset-routes.mjs';
function createRouterRecorder() {
const routes = new Map();
return {
routes,
get(path, handler) {
routes.set(`GET ${path}`, handler);
},
post(path, handler) {
routes.set(`POST ${path}`, handler);
},
delete(path, handler) {
routes.set(`DELETE ${path}`, handler);
},
};
}
function createResponseRecorder() {
return {
statusCode: 200,
body: undefined,
headers: {},
filePath: undefined,
mimeType: undefined,
status(code) {
this.statusCode = code;
return this;
},
json(body) {
this.body = body;
return this;
},
set(name, value) {
this.headers[name] = value;
return this;
},
setHeader(name, value) {
this.headers[name] = value;
return this;
},
type(value) {
this.mimeType = value;
return this;
},
send(body) {
this.body = body;
return this;
},
sendFile(filePath) {
this.filePath = filePath;
return this;
},
};
}
function createRequest(overrides = {}) {
const headers = Object.fromEntries(
Object.entries(overrides.headers ?? {}).map(([key, value]) => [
key.toLowerCase(),
value,
]),
);
return {
body: {},
query: {},
params: {},
currentUser: { id: 'user-1' },
userSession: null,
ip: '127.0.0.1',
requestId: 'request-1',
headers,
get(name) {
return headers[String(name).toLowerCase()];
},
...overrides,
headers,
};
}
function createDependencies(overrides = {}) {
const calls = {
data: [],
mapped: [],
};
return {
calls,
dependencies: {
ensureMindSpaceEnabled: () => true,
sendData(res, req, data, status = 200) {
calls.data.push({ res, req, data, status });
return res.status(status).json({ data });
},
handleMindSpaceError(res, req, error) {
calls.mapped.push({ res, req, error });
return res.status(418).json({ mapped: error.message });
},
...overrides,
},
};
}
test('MindSpace asset module preserves route inventory and order', () => {
const api = createRouterRecorder();
attachPortalMindSpaceAssetRoutes(api);
assert.deepEqual([...api.routes.keys()], [
'GET /mindspace/v1/authorize-image',
'GET /mindspace/v1/assets/:assetId/download',
'GET /mindspace/v1/assets/:assetId/thumbnail',
'GET /mindspace/v1/assets/:assetId/preview',
'POST /mindspace/v1/pages/from-asset',
'DELETE /mindspace/v1/assets/:assetId',
]);
});
test('authorize image preserves gates, query, cache policy, and expiry', async () => {
const rows = [];
const api = createRouterRecorder();
const dependencies = createDependencies({
getMindSpacePublications: () => ({}),
getDatabasePool: () => ({
async query(sql, params) {
assert.match(sql, /h5_publication_asset_refs/);
assert.deepEqual(params, ['asset-1']);
return [rows];
},
}),
now: () => 1_000,
});
attachPortalMindSpaceAssetRoutes(api, dependencies.dependencies);
const handler = api.routes.get('GET /mindspace/v1/authorize-image');
const missingRes = createResponseRecorder();
await handler(createRequest({ query: {} }), missingRes);
assert.equal(missingRes.statusCode, 400);
rows.splice(0, rows.length, { access_mode: 'public', expires_at: null });
const publicRes = createResponseRecorder();
await handler(createRequest({ query: { asset_id: 'asset-1' } }), publicRes);
assert.equal(publicRes.statusCode, 200);
assert.equal(
publicRes.headers['Cache-Control'],
'public, max-age=31536000, immutable',
);
rows.splice(0, rows.length, {
access_mode: 'time_limited',
expires_at: 2_000,
});
const timedRes = createResponseRecorder();
await handler(createRequest({ query: { asset_id: 'asset-1' } }), timedRes);
assert.equal(timedRes.statusCode, 200);
assert.equal(timedRes.headers['Cache-Control'], 'public, max-age=60');
rows.splice(0, rows.length, {
access_mode: 'time_limited',
expires_at: 999,
});
const expiredRes = createResponseRecorder();
await handler(createRequest({ query: { asset_id: 'asset-1' } }), expiredRes);
assert.equal(expiredRes.statusCode, 403);
rows.splice(0);
const missingRefRes = createResponseRecorder();
await handler(
createRequest({ query: { asset_id: 'asset-1' } }),
missingRefRes,
);
assert.equal(missingRefRes.statusCode, 403);
const unavailableApi = createRouterRecorder();
attachPortalMindSpaceAssetRoutes(unavailableApi);
const unavailableRes = createResponseRecorder();
await unavailableApi.routes.get('GET /mindspace/v1/authorize-image')(
createRequest({ query: { asset_id: 'asset-1' } }),
unavailableRes,
);
assert.equal(unavailableRes.statusCode, 503);
});
test('authorize image preserves internal error response and logging', async () => {
const logs = [];
const api = createRouterRecorder();
attachPortalMindSpaceAssetRoutes(api, {
getMindSpacePublications: () => ({}),
getDatabasePool: () => ({
async query() {
throw new Error('query failed');
},
}),
logger: {
error(...args) {
logs.push(args);
},
},
});
const res = createResponseRecorder();
await api.routes.get('GET /mindspace/v1/authorize-image')(
createRequest({ query: { asset_id: 'asset-1' } }),
res,
);
assert.equal(res.statusCode, 500);
assert.deepEqual(res.body, { error: 'Internal server error' });
assert.deepEqual(logs[0], ['[authorize-image]', 'query failed']);
});
test('authenticated download preserves audit, headers, and file response', async () => {
const audit = [];
const api = createRouterRecorder();
const dependencies = createDependencies({
getMindSpaceAssets: () => ({
async readAsset(userId, assetId) {
assert.equal(userId, 'user-1');
assert.equal(assetId, 'asset-1');
return {
asset: {
filename: '报告 1.pdf',
mimeType: 'application/pdf',
riskLevel: 'low',
},
path: '/tmp/report.pdf',
};
},
}),
getMindSpaceAudit: () => ({
async write(entry) {
audit.push(entry);
},
}),
});
attachPortalMindSpaceAssetRoutes(api, dependencies.dependencies);
const res = createResponseRecorder();
await api.routes.get('GET /mindspace/v1/assets/:assetId/download')(
createRequest({
params: { assetId: 'asset-1' },
query: {},
ip: '10.0.0.1',
}),
res,
);
assert.equal(res.mimeType, 'application/pdf');
assert.equal(res.filePath, '/tmp/report.pdf');
assert.equal(
res.headers['Content-Disposition'],
"attachment; filename*=UTF-8''%E6%8A%A5%E5%91%8A%201.pdf",
);
assert.equal(res.headers['X-Request-Id'], 'request-1');
assert.deepEqual(audit[0], {
userId: 'user-1',
action: 'asset.download',
objectType: 'asset',
objectId: 'asset-1',
ip: '10.0.0.1',
riskLevel: 'low',
detail: undefined,
});
});
test('image download preserves inline viewer response', async () => {
const viewerCalls = [];
const api = createRouterRecorder();
attachPortalMindSpaceAssetRoutes(api, {
ensureMindSpaceEnabled: () => true,
getMindSpaceAssets: () => ({
async readAsset() {
return {
asset: {
filename: 'cover.png',
displayName: 'Cover',
mimeType: 'image/png',
riskLevel: 'none',
},
path: '/tmp/cover.png',
};
},
}),
wantsImageViewer(req) {
viewerCalls.push(req);
return true;
},
renderImageViewer(input) {
viewerCalls.push(input);
return 'viewer';
},
});
const req = createRequest({
params: { assetId: 'asset/image' },
query: { inline: '1' },
});
const res = createResponseRecorder();
await api.routes.get('GET /mindspace/v1/assets/:assetId/download')(req, res);
assert.equal(res.body, 'viewer');
assert.equal(res.headers['Content-Type'], 'text/html; charset=utf-8');
assert.equal(res.headers['Cache-Control'], 'private, no-store');
assert.deepEqual(viewerCalls[1], {
asset: {
filename: 'cover.png',
displayName: 'Cover',
mimeType: 'image/png',
riskLevel: 'none',
},
downloadUrl:
'/api/mindspace/v1/assets/asset%2Fimage/download?inline=1',
});
});
test('anonymous download preserves publication, signed-token, referrer, and denial rules', async () => {
const audit = [];
const rows = [];
const api = createRouterRecorder();
const dependencies = createDependencies({
getMindSpaceAssets: () => ({
async readPublicAsset(assetId) {
return {
asset: {
filename: `${assetId}.png`,
mimeType: 'image/png',
riskLevel: 'none',
},
path: `/tmp/${assetId}.png`,
};
},
}),
getDatabasePool: () => ({
async query() {
return [rows];
},
}),
getInternalAgentSecret: () => 'secret-1',
verifyAssetToken(assetId, token, secret) {
return assetId === 'asset-1' &&
token === 'valid-token' &&
secret === 'secret-1';
},
resolveRequestOrigin: () => 'https://portal.example.com',
getMindSpaceAudit: () => ({
async write(entry) {
audit.push(entry);
},
}),
});
attachPortalMindSpaceAssetRoutes(api, dependencies.dependencies);
const handler = api.routes.get('GET /mindspace/v1/assets/:assetId/download');
rows.splice(0, rows.length, { access_mode: 'public' });
await handler(
createRequest({
currentUser: null,
params: { assetId: 'asset-public' },
query: {},
}),
createResponseRecorder(),
);
assert.deepEqual(audit.at(-1).detail, { accessMode: 'public' });
rows.splice(0);
await handler(
createRequest({
currentUser: null,
params: { assetId: 'asset-1' },
query: { public_token: 'valid-token' },
}),
createResponseRecorder(),
);
assert.deepEqual(audit.at(-1).detail, {
accessMode: 'signed-public-token',
});
await handler(
createRequest({
currentUser: null,
params: { assetId: 'asset-referrer' },
query: {},
headers: {
referer: 'https://portal.example.com/MindSpace/user/public/page.html',
},
}),
createResponseRecorder(),
);
assert.deepEqual(audit.at(-1).detail, {
accessMode: 'public-page-referrer',
});
const deniedRes = createResponseRecorder();
await handler(
createRequest({
currentUser: null,
params: { assetId: 'asset-denied' },
query: {},
}),
deniedRes,
);
assert.equal(deniedRes.statusCode, 401);
assert.deepEqual(deniedRes.body, { message: '未授权,请重新登录' });
});
test('thumbnail, preview, and delete preserve service calls and response metadata', async () => {
const calls = [];
const audit = [];
const assets = {
async renderAssetThumbnail(userId, assetId) {
calls.push({ kind: 'thumbnail', userId, assetId });
return '';
},
async renderAssetPreview(userId, assetId) {
calls.push({ kind: 'preview', userId, assetId });
return 'preview';
},
async deleteAsset(userId, assetId) {
calls.push({ kind: 'delete', userId, assetId });
return { id: assetId, deleted: true };
},
};
const api = createRouterRecorder();
const dependencies = createDependencies({
getMindSpaceAssets: () => assets,
getMindSpaceAudit: () => ({
async write(entry) {
audit.push(entry);
},
}),
});
attachPortalMindSpaceAssetRoutes(api, dependencies.dependencies);
const req = createRequest({
params: { assetId: 'asset-1' },
ip: '10.0.0.2',
});
const thumbnailRes = createResponseRecorder();
await api.routes.get('GET /mindspace/v1/assets/:assetId/thumbnail')(
req,
thumbnailRes,
);
assert.equal(thumbnailRes.body, '');
assert.equal(
thumbnailRes.headers['Content-Type'],
'image/svg+xml; charset=utf-8',
);
const previewRes = createResponseRecorder();
await api.routes.get('GET /mindspace/v1/assets/:assetId/preview')(
req,
previewRes,
);
assert.equal(previewRes.body, 'preview');
assert.equal(previewRes.headers['Cache-Control'], 'private, no-store');
await api.routes.get('DELETE /mindspace/v1/assets/:assetId')(
req,
createResponseRecorder(),
);
assert.deepEqual(calls, [
{ kind: 'thumbnail', userId: 'user-1', assetId: 'asset-1' },
{ kind: 'preview', userId: 'user-1', assetId: 'asset-1' },
{ kind: 'delete', userId: 'user-1', assetId: 'asset-1' },
]);
assert.deepEqual(audit, [{
userId: 'user-1',
action: 'asset.delete',
objectType: 'asset',
objectId: 'asset-1',
ip: '10.0.0.2',
}]);
assert.deepEqual(dependencies.calls.data[0].data, {
id: 'asset-1',
deleted: true,
});
});
test('from-asset preserves existing-page reuse and HTML page creation', async () => {
const existingApi = createRouterRecorder();
const existingDependencies = createDependencies({
getMindSpaceAssets: () => ({}),
getMindSpacePages: () => ({
async findPageBySourceAsset(userId, assetId) {
assert.equal(userId, 'user-1');
assert.equal(assetId, 'asset-1');
return { id: 'page-existing', categoryCode: null };
},
}),
});
attachPortalMindSpaceAssetRoutes(
existingApi,
existingDependencies.dependencies,
);
await existingApi.routes.get('POST /mindspace/v1/pages/from-asset')(
createRequest({ body: { asset_id: 'asset-1' } }),
createResponseRecorder(),
);
assert.deepEqual(existingDependencies.calls.data[0].data, {
kind: 'page',
categoryCode: 'draft',
page: { id: 'page-existing', categoryCode: null },
});
const calls = [];
const createApi = createRouterRecorder();
const createDependenciesResult = createDependencies({
getMindSpaceAssets: () => ({
async readAsset(userId, assetId) {
calls.push({ kind: 'read-asset', userId, assetId });
return {
asset: {
id: 'asset-2',
mimeType: 'text/html',
originalFilename: 'landing.html',
displayName: 'Landing',
categoryCode: 'document',
},
path: '/tmp/landing.html',
};
},
}),
getMindSpacePages: () => ({
async findPageBySourceAsset() {
return null;
},
async findPageByRelativePath(userId, relativePath) {
calls.push({ kind: 'find-path', userId, relativePath });
return null;
},
async createFromChat(userId, input, metadata) {
calls.push({ kind: 'create', userId, input, metadata });
return { id: 'page-2', title: input.title };
},
}),
normalizeWorkspacePath(value) {
calls.push({ kind: 'normalize', value });
return value;
},
async readFile(path, encoding) {
calls.push({ kind: 'read-file', path, encoding });
return 'Landing';
},
});
attachPortalMindSpaceAssetRoutes(
createApi,
createDependenciesResult.dependencies,
);
const res = createResponseRecorder();
await createApi.routes.get('POST /mindspace/v1/pages/from-asset')(
createRequest({
body: {
asset_id: 'asset-2',
title: 'Custom title',
summary: 'Summary',
template_id: 'template-1',
},
}),
res,
);
assert.equal(res.statusCode, 201);
assert.deepEqual(res.body, {
data: {
kind: 'page',
categoryCode: 'draft',
page: { id: 'page-2', title: 'Custom title' },
},
});
assert.deepEqual(calls.find((call) => call.kind === 'create'), {
kind: 'create',
userId: 'user-1',
input: {
title: 'Custom title',
summary: 'Summary',
content: 'Landing',
contentFormat: 'html',
templateId: 'template-1',
categoryCode: 'draft',
},
metadata: {
assetId: 'asset-2',
snapshot: {
source_asset_id: 'asset-2',
source_category: 'document',
content_mode: 'html',
relative_path: 'public/landing.html',
},
},
});
});
test('asset route errors preserve denied audit and shared mapping', async () => {
const failure = Object.assign(new Error('blocked'), {
code: 'security_risk_blocked',
});
const audit = [];
const api = createRouterRecorder();
const dependencies = createDependencies({
getMindSpaceAssets: () => ({
async readAsset() {
throw failure;
},
async renderAssetThumbnail() {
throw failure;
},
async renderAssetPreview() {
throw failure;
},
async deleteAsset() {
throw failure;
},
}),
getMindSpacePages: () => ({
async findPageBySourceAsset() {
throw failure;
},
}),
getMindSpaceAudit: () => ({
async write(entry) {
audit.push(entry);
},
}),
});
attachPortalMindSpaceAssetRoutes(api, dependencies.dependencies);
for (const route of [
'GET /mindspace/v1/assets/:assetId/download',
'GET /mindspace/v1/assets/:assetId/thumbnail',
'GET /mindspace/v1/assets/:assetId/preview',
'POST /mindspace/v1/pages/from-asset',
'DELETE /mindspace/v1/assets/:assetId',
]) {
await api.routes.get(route)(
createRequest({
params: { assetId: 'asset-1' },
body: { asset_id: 'asset-1' },
}),
createResponseRecorder(),
);
}
assert.equal(dependencies.calls.mapped.length, 5);
assert.deepEqual(audit[0], {
userId: 'user-1',
action: 'asset.download',
objectType: 'asset',
objectId: 'asset-1',
ip: '127.0.0.1',
result: 'denied',
riskLevel: 'high',
});
});