Files
memind/server/portal-wiki-routes.test.mjs

516 lines
9.7 KiB
JavaScript

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