Test conversation package route boundary

This commit is contained in:
john
2026-07-02 21:39:28 +08:00
parent 85936fff05
commit 0e00ed52cd
4 changed files with 164 additions and 22 deletions
+46
View File
@@ -0,0 +1,46 @@
function defaultSendData(res, _req, data) {
return res.json({ data });
}
function defaultRouteError(res, _req, error) {
return res.status(500).json({ message: error?.message || 'MindSpace 服务异常' });
}
function resolveRegistry(input) {
return typeof input === 'function' ? input() : input;
}
export function createGetConversationPackageHandler({
registry,
getRegistry,
userAuth,
getUserAuth,
ensureMindSpaceEnabled = () => true,
sendData = defaultSendData,
mindSpaceError = defaultRouteError,
} = {}) {
return async function getConversationPackage(req, res) {
const activeRegistry = resolveRegistry(getRegistry ?? registry);
if (!activeRegistry || !ensureMindSpaceEnabled(res, req)) {
if (res.headersSent) return undefined;
return res.status(503).json({ message: 'MindSpace 对话包未启用' });
}
const sessionId = String(req.params?.sessionId ?? '').trim();
if (!sessionId) return res.status(400).json({ message: '缺少 sessionId' });
try {
const activeUserAuth = resolveRegistry(getUserAuth ?? userAuth);
if (!(await activeUserAuth.ownsSession(req.currentUser.id, sessionId))) {
return res.status(403).json({ message: '无权访问该会话' });
}
const manifest = await activeRegistry.readManifestForSession({
userId: req.currentUser.id,
sessionId,
});
if (!manifest) return res.status(404).json({ message: '对话包不存在' });
return sendData(res, req, manifest);
} catch (error) {
return mindSpaceError(res, req, error);
}
};
}
@@ -0,0 +1,109 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createGetConversationPackageHandler } from './mindspace-conversation-package-routes.mjs';
function createResponseRecorder() {
return {
statusCode: 200,
headersSent: false,
body: null,
status(code) {
this.statusCode = code;
return this;
},
json(payload) {
this.body = payload;
this.headersSent = true;
return this;
},
};
}
test('GET conversation package returns manifest for owned sessions', async () => {
const handler = createGetConversationPackageHandler({
registry: {
async readManifestForSession(input) {
assert.deepEqual(input, { userId: 'user-1', sessionId: 'session-1' });
return { packageId: 'cp_session-1', artifacts: [{ artifactId: 'ca_1', kind: 'public_html' }] };
},
},
userAuth: {
async ownsSession(userId, sessionId) {
assert.equal(userId, 'user-1');
assert.equal(sessionId, 'session-1');
return true;
},
},
});
const res = createResponseRecorder();
await handler({ currentUser: { id: 'user-1' }, params: { sessionId: 'session-1' } }, res);
assert.equal(res.statusCode, 200);
assert.deepEqual(res.body, {
data: { packageId: 'cp_session-1', artifacts: [{ artifactId: 'ca_1', kind: 'public_html' }] },
});
});
test('GET conversation package blocks sessions owned by another user', async () => {
const handler = createGetConversationPackageHandler({
registry: {
async readManifestForSession() {
assert.fail('registry should not be called for unauthorized sessions');
},
},
userAuth: {
async ownsSession() {
return false;
},
},
});
const res = createResponseRecorder();
await handler({ currentUser: { id: 'user-1' }, params: { sessionId: 'session-2' } }, res);
assert.equal(res.statusCode, 403);
assert.deepEqual(res.body, { message: '无权访问该会话' });
});
test('GET conversation package returns 404 when package has not been recorded', async () => {
const handler = createGetConversationPackageHandler({
registry: {
async readManifestForSession() {
return null;
},
},
userAuth: {
async ownsSession() {
return true;
},
},
});
const res = createResponseRecorder();
await handler({ currentUser: { id: 'user-1' }, params: { sessionId: 'session-empty' } }, res);
assert.equal(res.statusCode, 404);
assert.deepEqual(res.body, { message: '对话包不存在' });
});
test('GET conversation package respects MindSpace feature gates', async () => {
const handler = createGetConversationPackageHandler({
registry: {},
userAuth: {
async ownsSession() {
assert.fail('user ownership should not be checked when feature gate rejects');
},
},
ensureMindSpaceEnabled(res) {
res.status(403).json({ message: 'MindSpace 已关闭' });
return false;
},
});
const res = createResponseRecorder();
await handler({ currentUser: { id: 'user-1' }, params: { sessionId: 'session-1' } }, res);
assert.equal(res.statusCode, 403);
assert.deepEqual(res.body, { message: 'MindSpace 已关闭' });
});
+1 -1
View File
@@ -38,7 +38,7 @@
"build:portal-runtime": "node scripts/build-portal-runtime.mjs",
"check:mindspace-public-links": "node scripts/check-mindspace-public-links.mjs --downloads-only",
"check:mindspace-public-links:all": "node scripts/check-mindspace-public-links.mjs --all-links",
"test": "node --test auth.test.mjs asr-proxy.test.mjs billing.test.mjs billing-recharge.test.mjs wechat-pay.test.mjs wechat-oauth.test.mjs wechat-mp.test.mjs schedule-intent.test.mjs schedule-reminder-worker.test.mjs capabilities.test.mjs policies.test.mjs chat-skills.test.mjs chat-finish-sync.test.mjs conversation-display.test.mjs user-publish.test.mjs user-memory-profile.test.mjs skills-registry.test.mjs agent-run-gateway.test.mjs agent-run-routes.test.mjs tool-gateway.test.mjs mindspace.test.mjs mindspace-scan.test.mjs mindspace-assets.test.mjs mindspace-pages.test.mjs mindspace-html-download-links.test.mjs mindspace-page-purge.test.mjs mindspace-publications.test.mjs mindspace-public-links.test.mjs mindspace-chat-save.test.mjs mindspace-public-finish-sync.test.mjs mindspace-chat-context.test.mjs mindspace-canonical-url.test.mjs mindspace-conversation-package.test.mjs mindspace-conversation-package-registry.test.mjs mindspace-conversation-package-store.test.mjs mindspace-conversation-schema.test.mjs mindspace-service.test.mjs mindspace-storage-adapter.test.mjs mindspace-content-scan.test.mjs mindspace-html-localize.test.mjs mindspace-visual-editor.test.mjs mindspace-cleanup.test.mjs mindspace-thumbnails.test.mjs mindspace-workspace-thumbnails.test.mjs mindspace-workspace-sync.test.mjs mindspace-asset-preview.test.mjs mindspace-agent-jobs.test.mjs mindspace-agent-runner.test.mjs mindspace-sandbox-mcp.test.mjs message-stream.test.mjs plaza-posts.test.mjs plaza-interactions.test.mjs plaza-algorithm.test.mjs plaza-seo.test.mjs plaza-ops.test.mjs user-auth.test.mjs llm-providers.test.mjs admin-guard.test.mjs user-feedback.test.mjs",
"test": "node --test auth.test.mjs asr-proxy.test.mjs billing.test.mjs billing-recharge.test.mjs wechat-pay.test.mjs wechat-oauth.test.mjs wechat-mp.test.mjs schedule-intent.test.mjs schedule-reminder-worker.test.mjs capabilities.test.mjs policies.test.mjs chat-skills.test.mjs chat-finish-sync.test.mjs conversation-display.test.mjs user-publish.test.mjs user-memory-profile.test.mjs skills-registry.test.mjs agent-run-gateway.test.mjs agent-run-routes.test.mjs tool-gateway.test.mjs mindspace.test.mjs mindspace-scan.test.mjs mindspace-assets.test.mjs mindspace-pages.test.mjs mindspace-html-download-links.test.mjs mindspace-page-purge.test.mjs mindspace-publications.test.mjs mindspace-public-links.test.mjs mindspace-chat-save.test.mjs mindspace-public-finish-sync.test.mjs mindspace-chat-context.test.mjs mindspace-canonical-url.test.mjs mindspace-conversation-package.test.mjs mindspace-conversation-package-registry.test.mjs mindspace-conversation-package-routes.test.mjs mindspace-conversation-package-store.test.mjs mindspace-conversation-schema.test.mjs mindspace-service.test.mjs mindspace-storage-adapter.test.mjs mindspace-content-scan.test.mjs mindspace-html-localize.test.mjs mindspace-visual-editor.test.mjs mindspace-cleanup.test.mjs mindspace-thumbnails.test.mjs mindspace-workspace-thumbnails.test.mjs mindspace-workspace-sync.test.mjs mindspace-asset-preview.test.mjs mindspace-agent-jobs.test.mjs mindspace-agent-runner.test.mjs mindspace-sandbox-mcp.test.mjs message-stream.test.mjs plaza-posts.test.mjs plaza-interactions.test.mjs plaza-algorithm.test.mjs plaza-seo.test.mjs plaza-ops.test.mjs user-auth.test.mjs llm-providers.test.mjs admin-guard.test.mjs user-feedback.test.mjs",
"verify:chat-finish-sync": "node scripts/verify-chat-finish-sync.mjs",
"verify:public-finish-sync-runtime": "node scripts/verify-public-finish-sync-runtime.mjs",
"verify:mindspace-publish-guards": "node scripts/verify-mindspace-publish-guards.mjs",
+8 -21
View File
@@ -43,6 +43,7 @@ import { ensureMindSpaceConfig } from './mindspace-config.mjs';
import { createAssetService } from './mindspace-assets.mjs';
import { createPageService, pageInternals, inlinePrivateAssetsInHtml, normalizeWorkspaceRelativePath } from './mindspace-pages.mjs';
import { createConversationPackageRegistry } from './mindspace-conversation-package-registry.mjs';
import { createGetConversationPackageHandler } from './mindspace-conversation-package-routes.mjs';
import { createConversationPackageStore } from './mindspace-conversation-package-store.mjs';
import { createMindSpaceServiceFacade } from './mindspace-service.mjs';
import { createLocalMindSpaceStorageAdapter } from './mindspace-storage-adapter.mjs';
@@ -2487,27 +2488,13 @@ api.post('/internal/agent/jobs/:jobId/complete', async (req, res) => {
}
});
api.get('/mindspace/v1/conversation-packages/:sessionId', async (req, res) => {
if (!mindSpaceConversationPackageRegistry || !ensureMindSpaceEnabled(res, req)) {
if (res.headersSent) return;
return res.status(503).json({ message: 'MindSpace 对话包未启用' });
}
const sessionId = String(req.params.sessionId ?? '').trim();
if (!sessionId) return res.status(400).json({ message: '缺少 sessionId' });
if (!(await userAuth.ownsSession(req.currentUser.id, sessionId))) {
return res.status(403).json({ message: '无权访问该会话' });
}
try {
const manifest = await mindSpaceConversationPackageRegistry.readManifestForSession({
userId: req.currentUser.id,
sessionId,
});
if (!manifest) return res.status(404).json({ message: '对话包不存在' });
return sendData(res, req, manifest);
} catch (error) {
return mindSpaceError(res, req, error);
}
});
api.get('/mindspace/v1/conversation-packages/:sessionId', createGetConversationPackageHandler({
getRegistry: () => mindSpaceConversationPackageRegistry,
getUserAuth: () => userAuth,
ensureMindSpaceEnabled,
sendData,
mindSpaceError,
}));
api.post('/mindspace/v1/uploads', async (req, res) => {
if (!mindSpaceAssets || !ensureMindSpaceEnabled(res, req, { upload: true })) return;