Files
memind/server/portal-api-auth-middleware.test.mjs
2026-07-25 09:44:26 +08:00

360 lines
10 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { isLegacyPageDataApiPath } from '../page-data-routes.mjs';
import { isPageDataPublicPath } from '../page-data-public-service.mjs';
import { createPortalApiAuthMiddleware } from './portal-api-auth-middleware.mjs';
import {
PORTAL_ACCESS_POLICY_MODE,
resolvePortalAccessEnforcementConfig,
} from './portal-access-policy.mjs';
function createResponseRecorder() {
return {
statusCode: 200,
body: null,
status(code) {
this.statusCode = code;
return this;
},
json(body) {
this.body = body;
return this;
},
};
}
async function invoke(middleware, overrides = {}) {
const req = {
path: '/mindspace/v1/pages',
method: 'GET',
requestId: 'req-test',
userToken: 'user-token',
get: () => '',
...overrides,
};
const res = createResponseRecorder();
let nextCalls = 0;
await middleware(req, res, () => {
nextCalls += 1;
});
return { req, res, nextCalls };
}
function createLogger() {
const entries = [];
return {
entries,
info(...args) {
entries.push(['info', ...args]);
},
error(...args) {
entries.push(['error', ...args]);
},
};
}
function createMiddleware(overrides = {}) {
return createPortalApiAuthMiddleware({
isPageDataPublicPath,
isLegacyPageDataApiPath,
isProductAnalyticsPublicPath: (path, method) =>
method === 'GET' && path === '/analytics/context',
logger: createLogger(),
...overrides,
});
}
test('direct global-auth bypass routes preserve unauthenticated access', async () => {
const middleware = createMiddleware();
for (const [path, method] of [
['/status', 'GET'],
['/runtime/status', 'GET'],
['/config/blocked-words', 'GET'],
['/internal/agent/jobs/job-1/claim', 'POST'],
['/internal/deep-search/llm', 'POST'],
['/mindspace/v1/assets/asset-1/download', 'GET'],
]) {
const result = await invoke(middleware, { path, method });
assert.equal(result.nextCalls, 1, `${method} ${path}`);
assert.equal(result.res.body, null);
}
});
test('no-auth mode keeps private and adjacent routes protected', async () => {
const middleware = createMiddleware();
for (const [path, method] of [
['/mindspace/v1/pages', 'GET'],
['/public/pages/page-1/data/signups/rows', 'GET'],
['/public/pages/page-1/data/signups', 'POST'],
['/page-dataevil/signups', 'GET'],
['/plaza/v1/posts/post-1/reports', 'POST'],
]) {
const result = await invoke(middleware, { path, method });
assert.equal(result.nextCalls, 0, `${method} ${path}`);
assert.equal(result.res.statusCode, 401);
}
});
test('legacy mode accepts only a verified legacy session', async () => {
const verifiedTokens = [];
const legacyAuth = {
verify(token) {
verifiedTokens.push(token);
return token === 'legacy-ok';
},
};
const middleware = createMiddleware({
getLegacyAuth: () => legacyAuth,
getLegacySessionToken: (req) => req.legacyToken,
});
const denied = await invoke(middleware, { legacyToken: 'legacy-bad' });
assert.equal(denied.res.statusCode, 401);
const allowed = await invoke(middleware, { legacyToken: 'legacy-ok' });
assert.equal(allowed.nextCalls, 1);
assert.deepEqual(verifiedTokens, ['legacy-bad', 'legacy-ok']);
});
test('multi-user public routes preserve optional user hydration', async () => {
let getMeCalls = 0;
const user = { id: 'user-1' };
const middleware = createMiddleware({
getUserAuth: () => ({
async getMe(token) {
getMeCalls += 1;
assert.equal(token, 'user-token');
return user;
},
}),
getTkmindProxy: () => ({}),
});
const anonymous = await invoke(middleware, {
path: '/public/pages/page-1/data/signups',
userSession: null,
});
assert.equal(anonymous.nextCalls, 1);
assert.equal(anonymous.req.currentUser, undefined);
const authenticated = await invoke(middleware, {
path: '/public/pages/page-1/data/signups',
userSession: { id: 'session-1' },
});
assert.equal(authenticated.nextCalls, 1);
assert.equal(authenticated.req.currentUser, user);
assert.equal(getMeCalls, 1);
});
test('product analytics context preserves optional user hydration', async () => {
let getMeCalls = 0;
const user = { id: 'analytics-user' };
const middleware = createMiddleware({
getUserAuth: () => ({
async getMe(token) {
getMeCalls += 1;
assert.equal(token, 'user-token');
return user;
},
}),
getTkmindProxy: () => ({}),
});
const anonymous = await invoke(middleware, {
path: '/analytics/context',
userSession: null,
});
assert.equal(anonymous.nextCalls, 1);
assert.equal(anonymous.req.currentUser, undefined);
const authenticated = await invoke(middleware, {
path: '/analytics/context',
userSession: { id: 'session-analytics' },
});
assert.equal(authenticated.nextCalls, 1);
assert.equal(authenticated.req.currentUser, user);
assert.equal(getMeCalls, 1);
});
test('multi-user private routes preserve login, expiry, and double-check behavior', async () => {
let currentUser = { id: 'user-1' };
let getMeCalls = 0;
const middleware = createMiddleware({
getUserAuth: () => ({
async getMe() {
getMeCalls += 1;
return currentUser;
},
}),
getTkmindProxy: () => ({}),
});
const anonymous = await invoke(middleware, { userSession: null });
assert.equal(anonymous.res.statusCode, 401);
assert.equal(anonymous.res.body.message, '未授权,请重新登录');
const authenticated = await invoke(middleware, {
userSession: { id: 'session-1' },
});
assert.equal(authenticated.nextCalls, 1);
assert.equal(authenticated.req.currentUser, currentUser);
assert.equal(getMeCalls, 2);
currentUser = null;
getMeCalls = 0;
const expired = await invoke(middleware, {
userSession: { id: 'session-expired' },
});
assert.equal(expired.res.statusCode, 401);
assert.equal(expired.res.body.message, '登录已过期');
assert.equal(getMeCalls, 2);
});
test('multi-user session service errors fail closed only for private routes', async () => {
const middleware = createMiddleware({
getUserAuth: () => ({ getMe: async () => ({ id: 'user-1' }) }),
getTkmindProxy: () => ({}),
});
const privateResult = await invoke(middleware, {
userSessionError: new Error('session unavailable'),
});
assert.equal(privateResult.res.statusCode, 503);
const publicResult = await invoke(middleware, {
path: '/public/pages/page-1/data/signups',
userSessionError: new Error('session unavailable'),
});
assert.equal(publicResult.nextCalls, 1);
});
test('multi-user getMe failures preserve the 503 response and error log', async () => {
const logger = createLogger();
const middleware = createMiddleware({
getUserAuth: () => ({
async getMe() {
throw new Error('database timeout');
},
}),
getTkmindProxy: () => ({}),
logger,
});
const result = await invoke(middleware, {
userSession: { id: 'session-1' },
});
assert.equal(result.res.statusCode, 503);
assert.equal(result.res.body.message, '用户认证服务不可用,请稍后重试');
assert.deepEqual(logger.entries[0], [
'error',
'[Auth] API auth failed:',
'database timeout',
]);
});
test('shadow mode records differences without changing the response', async () => {
const events = [];
const middleware = createMiddleware({
accessPolicyMode: PORTAL_ACCESS_POLICY_MODE.SHADOW,
accessShadowReporter: {
record(event) {
events.push(event);
},
},
});
const result = await invoke(middleware, {
path: '/plaza/v1/categories',
});
assert.equal(result.res.statusCode, 401);
assert.equal(events.length, 1);
assert.equal(events[0].policyGroup, 'plaza-optional-user');
assert.equal(events[0].mismatch, true);
});
test('single-group enforcement overrides only the selected no-auth boundary', async () => {
const logger = createLogger();
const middleware = createMiddleware({
accessEnforcementConfig: resolvePortalAccessEnforcementConfig({
MEMIND_PORTAL_ACCESS_POLICY_ENFORCEMENT_ENABLED: '1',
MEMIND_PORTAL_ACCESS_POLICY_ENFORCE_GROUPS: 'page-data-public',
}),
logger,
});
const selected = await invoke(middleware, {
path: '/public/pages/page-1/data/signups',
});
assert.equal(selected.nextCalls, 1);
assert.equal(logger.entries.length, 1);
assert.match(logger.entries[0][2], /"behaviorChanged":true/);
const legacy = await invoke(middleware, {
path: '/page-data/signups',
});
assert.equal(legacy.res.statusCode, 401);
const plaza = await invoke(middleware, {
path: '/plaza/v1/categories',
});
assert.equal(plaza.res.statusCode, 401);
});
test('enforcement does not skip multi-user optional identity hydration', async () => {
let getMeCalls = 0;
const middleware = createMiddleware({
getUserAuth: () => ({
async getMe() {
getMeCalls += 1;
return { id: 'user-1' };
},
}),
getTkmindProxy: () => ({}),
accessEnforcementConfig: resolvePortalAccessEnforcementConfig({
MEMIND_PORTAL_ACCESS_POLICY_ENFORCEMENT_ENABLED: '1',
MEMIND_PORTAL_ACCESS_POLICY_ENFORCE_GROUPS: 'page-data-public',
}),
});
const result = await invoke(middleware, {
path: '/public/pages/page-1/data/signups',
userSession: { id: 'session-1' },
});
assert.equal(result.nextCalls, 1);
assert.equal(result.req.currentUser.id, 'user-1');
assert.equal(getMeCalls, 1);
});
test('kill switch restores the legacy global-auth result', async () => {
const middleware = createMiddleware({
accessEnforcementConfig: resolvePortalAccessEnforcementConfig({
MEMIND_PORTAL_ACCESS_POLICY_ENFORCEMENT_ENABLED: '1',
MEMIND_PORTAL_ACCESS_POLICY_ENFORCE_GROUPS: 'page-data-public',
MEMIND_PORTAL_ACCESS_POLICY_KILL_SWITCH: '1',
}),
});
const result = await invoke(middleware, {
path: '/public/pages/page-1/data/signups',
});
assert.equal(result.res.statusCode, 401);
assert.equal(result.nextCalls, 0);
});
test('middleware waits for auth bootstrap before reading service getters', async () => {
const calls = [];
const middleware = createMiddleware({
waitForUserAuthReady: async () => {
calls.push('ready');
},
getUserAuth: () => {
calls.push('user-auth');
return null;
},
getTkmindProxy: () => {
calls.push('proxy');
return null;
},
getLegacyAuth: () => {
calls.push('legacy');
return null;
},
});
await invoke(middleware);
assert.deepEqual(calls, ['ready', 'user-auth', 'proxy', 'legacy']);
});