516 lines
9.7 KiB
JavaScript
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 });
|
|
});
|