import assert from 'node:assert/strict'; import test from 'node:test'; import { attachPortalWikiRoutes, } from './portal-wiki-routes.mjs'; function createResponse() { return { statusCode: 200, headers: {}, body: undefined, status(code) { this.statusCode = code; return this; }, set(name, value) { this.headers[name] = value; return this; }, json(body) { this.body = body; return this; }, }; } function createSetup(overrides = {}) { const routes = []; const calls = []; const jsonBody = (_req, _res, next) => next(); const router = { use(...handlers) { routes.push({ method: 'use', path: null, handlers, }); }, get(path, ...handlers) { routes.push({ method: 'get', path, handlers, }); }, post(path, ...handlers) { routes.push({ method: 'post', path, handlers, }); }, delete(path, ...handlers) { routes.push({ method: 'delete', path, handlers, }); }, }; const wikiAuth = { COOKIE_NAME: 'wiki_session', register(username, password, displayName) { calls.push([ 'register', username, password, displayName, ]); return { ok: true, user: { username, displayName, }, }; }, login(username, password) { calls.push([ 'login', username, password, ]); return { ok: true, token: 'session-token', user: { username }, }; }, revoke(token) { calls.push(['revoke', token]); }, verify(token) { calls.push(['verify', token]); return token === 'session-token' ? { username: 'john' } : null; }, getUser(username) { calls.push(['get-user', username]); return { username, displayName: 'John', }; }, searchPages(username, query) { calls.push([ 'search-pages', username, query, ]); return [{ slug: 'matched' }]; }, listPages(username) { calls.push(['list-pages', username]); return [{ slug: 'home' }]; }, getPage(username, slug) { calls.push([ 'get-page', username, slug, ]); if (slug === 'missing') return null; return { slug, title: 'Page', }; }, savePage( username, slug, title, content, tags, ) { calls.push([ 'save-page', username, slug, title, content, tags, ]); return { slug, title, content, tags, }; }, deletePage(username, slug) { calls.push([ 'delete-page', username, slug, ]); }, }; const options = { router, jsonBody, wikiAuth, isSecureRequest: () => true, parseCookiesFn(cookieHeader) { calls.push([ 'parse-cookies', cookieHeader, ]); return cookieHeader === 'wiki_session=session-token' ? { wiki_session: 'session-token', } : {}; }, ...overrides, }; attachPortalWikiRoutes(options); return { routes, calls, jsonBody, wikiAuth, route(method, path) { return routes.find( (route) => route.method === method && route.path === path, ); }, }; } function createRequest(overrides = {}) { const headers = { cookie: '', ...overrides.headers, }; return { body: {}, query: {}, params: {}, get(name) { return ( headers[String(name).toLowerCase()] ?? '' ); }, ...overrides, }; } async function invoke( setup, method, path, req = createRequest(), { protectedRoute = false } = {}, ) { const route = setup.route(method, path); assert.ok( route, `${method.toUpperCase()} ${path}`, ); const res = createResponse(); if (protectedRoute) { let nextCalled = false; await route.handlers[0](req, res, () => { nextCalled = true; }); if (nextCalled) { await route.handlers.at(-1)(req, res); } } else { await route.handlers.at(-1)(req, res); } return res; } test('registers Wiki middleware and route inventory in order', () => { const setup = createSetup(); assert.equal( setup.routes[0].handlers[0], setup.jsonBody, ); assert.deepEqual( setup.routes .filter(({ method }) => method !== 'use') .map(({ method, path }) => [ method, path, ]), [ ['post', '/auth/register'], ['post', '/auth/login'], ['post', '/auth/logout'], ['get', '/auth/me'], ['get', '/pages'], ['get', '/pages/:slug'], ['post', '/pages/:slug'], ['delete', '/pages/:slug'], ], ); }); test('preserves Wiki registration validation, conflicts, and success', async () => { const setup = createSetup(); const missing = await invoke( setup, 'post', '/auth/register', ); assert.equal(missing.statusCode, 400); const success = await invoke( setup, 'post', '/auth/register', createRequest({ body: { username: 'john', password: 'secret', displayName: 'John', }, }), ); assert.deepEqual(success.body, { ok: true, user: { username: 'john', displayName: 'John', }, }); const conflict = createSetup(); conflict.wikiAuth.register = () => ({ ok: false, message: '用户已存在', }); const conflictRes = await invoke( conflict, 'post', '/auth/register', createRequest({ body: { username: 'john', password: 'secret', }, }), ); assert.equal(conflictRes.statusCode, 409); }); test('preserves Wiki login and secure session cookie', async () => { const setup = createSetup(); const res = await invoke( setup, 'post', '/auth/login', createRequest({ body: { username: 'john', password: 'secret', }, }), ); assert.equal(res.statusCode, 200); assert.match( res.headers['Set-Cookie'], /^wiki_session=session-token;/, ); assert.match( res.headers['Set-Cookie'], /Max-Age=604800/, ); assert.match(res.headers['Set-Cookie'], /Secure/); setup.wikiAuth.login = () => ({ ok: false, message: '密码错误', }); const denied = await invoke( setup, 'post', '/auth/login', createRequest({ body: { username: 'john', password: 'wrong', }, }), ); assert.equal(denied.statusCode, 401); }); test('preserves Wiki logout and current-user states', async () => { const setup = createSetup(); const authenticatedReq = createRequest({ headers: { cookie: 'wiki_session=session-token', }, }); const me = await invoke( setup, 'get', '/auth/me', authenticatedReq, ); assert.deepEqual(me.body, { authenticated: true, user: { username: 'john', displayName: 'John', }, }); const logout = await invoke( setup, 'post', '/auth/logout', authenticatedReq, ); assert.deepEqual(logout.body, { ok: true }); assert.match( logout.headers['Set-Cookie'], /Max-Age=0/, ); assert.ok( setup.calls.some( (call) => call[0] === 'revoke' && call[1] === 'session-token', ), ); const anonymous = await invoke( setup, 'get', '/auth/me', ); assert.deepEqual(anonymous.body, { authenticated: false, user: null, }); }); test('preserves Wiki authorization gate and page listing/search', async () => { const setup = createSetup(); const denied = await invoke( setup, 'get', '/pages', createRequest(), { protectedRoute: true }, ); assert.equal(denied.statusCode, 401); const authenticated = createRequest({ headers: { cookie: 'wiki_session=session-token', }, }); const listed = await invoke( setup, 'get', '/pages', authenticated, { protectedRoute: true }, ); assert.deepEqual(listed.body, [ { slug: 'home' }, ]); const searched = await invoke( setup, 'get', '/pages', createRequest({ headers: { cookie: 'wiki_session=session-token', }, query: { q: 'guide' }, }), { protectedRoute: true }, ); assert.deepEqual(searched.body, [ { slug: 'matched' }, ]); }); test('preserves Wiki page read, write, delete, and missing mapping', async () => { const setup = createSetup(); const authenticated = (overrides = {}) => createRequest({ headers: { cookie: 'wiki_session=session-token', }, ...overrides, }); const read = await invoke( setup, 'get', '/pages/:slug', authenticated({ params: { slug: 'home' }, }), { protectedRoute: true }, ); assert.deepEqual(read.body, { slug: 'home', title: 'Page', }); const missing = await invoke( setup, 'get', '/pages/:slug', authenticated({ params: { slug: 'missing' }, }), { protectedRoute: true }, ); assert.equal(missing.statusCode, 404); const saved = await invoke( setup, 'post', '/pages/:slug', authenticated({ params: { slug: 'guide' }, body: { title: 'Guide', content: '# Guide', tags: ['docs'], }, }), { protectedRoute: true }, ); assert.equal(saved.body.slug, 'guide'); const deleted = await invoke( setup, 'delete', '/pages/:slug', authenticated({ params: { slug: 'guide' }, }), { protectedRoute: true }, ); assert.deepEqual(deleted.body, { ok: true }); });