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']); });