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

638 lines
17 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { attachPortalPlazaRoutes } from './portal-plaza-routes.mjs';
function createRouterRecorder() {
const routes = new Map();
const register = (method) => (path, handler) => {
routes.set(`${method} ${path}`, handler);
};
return {
routes,
get: register('GET'),
post: register('POST'),
patch: register('PATCH'),
delete: register('DELETE'),
};
}
function createResponseRecorder() {
return {
statusCode: 200,
body: undefined,
headers: {},
appended: [],
status(code) {
this.statusCode = code;
return this;
},
json(body) {
this.body = body;
return this;
},
append(name, value) {
this.appended.push([name, value]);
return this;
},
};
}
function createRequest(overrides = {}) {
const headers = overrides.headers ?? {};
return {
body: {},
query: {},
params: {},
currentUser: null,
ip: '127.0.0.1',
headers,
get(name) {
return headers[String(name).toLowerCase()];
},
...overrides,
headers,
};
}
function createDependencies(overrides = {}) {
const calls = {
data: [],
errors: [],
routeErrors: [],
};
return {
calls,
dependencies: {
sendData(res, req, data, status = 200) {
calls.data.push({ res, req, data, status });
return res.status(status).json({ data });
},
sendError(res, req, status, code, message, details) {
calls.errors.push({ res, req, status, code, message, details });
return res.status(status).json({ error: { code, message, details } });
},
handleRouteError(res, req, error) {
calls.routeErrors.push({ res, req, error });
return res.status(418).json({ mapped: error.message });
},
...overrides,
},
};
}
test('Plaza module preserves all remaining route registrations and order', () => {
const api = createRouterRecorder();
attachPortalPlazaRoutes(api);
assert.deepEqual([...api.routes.keys()], [
'POST /plaza/v1/attribution/events',
'POST /plaza/v1/posts/:id/reports',
'POST /plaza/v1/comments/:id/reports',
'GET /plaza/v1/feed',
'POST /plaza/v1/events',
'POST /plaza/v1/posts/:id/reactions',
'DELETE /plaza/v1/posts/:id/reactions/:type',
'GET /plaza/v1/posts/:id/comments',
'POST /plaza/v1/posts/:id/comments',
'DELETE /plaza/v1/comments/:id',
'POST /plaza/v1/comments/:id/reactions',
'GET /plaza/v1/users/:slug',
'GET /plaza/v1/users/:slug/posts',
'POST /plaza/v1/users/:slug/follow',
'DELETE /plaza/v1/users/:slug/follow',
'GET /plaza/v1/posts/:id',
'POST /plaza/v1/posts',
'PATCH /plaza/v1/posts/:id',
'DELETE /plaza/v1/posts/:id',
]);
});
test('attribution and reports preserve IP, target mapping, auth, and 201 status', async () => {
const calls = [];
const api = createRouterRecorder();
const deps = createDependencies({
getPlazaSeo: () => ({
async recordAttribution(body, ip) {
calls.push({ kind: 'attribution', body, ip });
return { recorded: true };
},
}),
getPlazaOps: () => ({
async createReport(userId, input) {
calls.push({ kind: 'report', userId, input });
return { id: `report-${input.target_type}` };
},
}),
resolveClientIp: () => '203.0.113.10',
});
attachPortalPlazaRoutes(api, deps.dependencies);
await api.routes.get('POST /plaza/v1/attribution/events')(
createRequest({ body: { source: 'search' } }),
createResponseRecorder(),
);
const deniedRes = createResponseRecorder();
await api.routes.get('POST /plaza/v1/posts/:id/reports')(
createRequest({ params: { id: 'post-1' } }),
deniedRes,
);
assert.equal(deniedRes.statusCode, 401);
await api.routes.get('POST /plaza/v1/posts/:id/reports')(
createRequest({
currentUser: { id: 'user-1' },
params: { id: 'post-1' },
body: { reason: 'spam', detail: 'details' },
}),
createResponseRecorder(),
);
await api.routes.get('POST /plaza/v1/comments/:id/reports')(
createRequest({
currentUser: { id: 'user-1' },
params: { id: 'comment-1' },
body: { reason: 'abuse' },
}),
createResponseRecorder(),
);
assert.deepEqual(calls, [
{
kind: 'attribution',
body: { source: 'search' },
ip: '203.0.113.10',
},
{
kind: 'report',
userId: 'user-1',
input: {
target_type: 'post',
target_id: 'post-1',
reason: 'spam',
detail: 'details',
},
},
{
kind: 'report',
userId: 'user-1',
input: {
target_type: 'comment',
target_id: 'comment-1',
reason: 'abuse',
detail: undefined,
},
},
]);
assert.deepEqual(
deps.calls.data.map((item) => item.status),
[201, 201, 201],
);
});
test('feed and events preserve optional user, session cookie, query, and explicit session id', async () => {
const calls = [];
const api = createRouterRecorder();
const deps = createDependencies({
getPlazaPosts: () => ({
async listFeed(input) {
calls.push({ kind: 'feed', input });
return { items: [] };
},
}),
getPlazaEvents: () => ({
async recordEvents(input) {
calls.push({ kind: 'events', input });
return { accepted: input.events.length };
},
}),
randomUUID: () => 'generated-session',
});
attachPortalPlazaRoutes(api, deps.dependencies);
const feedRes = createResponseRecorder();
await api.routes.get('GET /plaza/v1/feed')(
createRequest({
currentUser: { id: 'user-1' },
query: {
sort: 'hot',
category: 'tech',
cursor: 'cursor-1',
limit: '20',
},
headers: {},
}),
feedRes,
);
assert.deepEqual(feedRes.appended, [
[
'Set-Cookie',
'plaza_sid=generated-session; Path=/; Max-Age=31536000; SameSite=Lax; HttpOnly',
],
]);
const explicitRes = createResponseRecorder();
await api.routes.get('POST /plaza/v1/events')(
createRequest({
body: {
session_id: 'explicit-session',
events: [{ event_type: 'view' }],
},
}),
explicitRes,
);
assert.deepEqual(explicitRes.appended, []);
assert.deepEqual(calls, [
{
kind: 'feed',
input: {
sort: 'hot',
categorySlug: 'tech',
cursor: 'cursor-1',
limit: '20',
viewerId: 'user-1',
sessionId: 'generated-session',
},
},
{
kind: 'events',
input: {
userId: null,
sessionId: 'explicit-session',
events: [{ event_type: 'view' }],
},
},
]);
assert.deepEqual(deps.calls.data[1].data, {
accepted: 1,
session_id: 'explicit-session',
});
assert.equal(deps.calls.data[1].status, 201);
});
test('post reactions and comments preserve auth, payloads, and async event recording', async () => {
const calls = [];
const interactions = {
async addReaction(...args) {
calls.push({ kind: 'addReaction', args });
return { type: 'like', active: true };
},
async removeReaction(...args) {
calls.push({ kind: 'removeReaction', args });
return { active: false };
},
async listComments(...args) {
calls.push({ kind: 'listComments', args });
return { items: [] };
},
async createComment(...args) {
calls.push({ kind: 'createComment', args });
return { id: 'comment-1' };
},
};
const api = createRouterRecorder();
const deps = createDependencies({
getPlazaInteractions: () => interactions,
getPlazaEvents: () => ({
async recordEvents(input) {
calls.push({ kind: 'recordEvents', input });
return {};
},
}),
randomUUID: () => 'session-1',
});
attachPortalPlazaRoutes(api, deps.dependencies);
const deniedRes = createResponseRecorder();
await api.routes.get('POST /plaza/v1/posts/:id/reactions')(
createRequest({ params: { id: 'post-1' } }),
deniedRes,
);
assert.equal(deniedRes.statusCode, 401);
const userReq = {
currentUser: { id: 'user-1' },
headers: { cookie: 'plaza_sid=cookie-session' },
};
await api.routes.get('POST /plaza/v1/posts/:id/reactions')(
createRequest({
...userReq,
params: { id: 'post-1' },
body: { type: 'like' },
}),
createResponseRecorder(),
);
await api.routes.get('DELETE /plaza/v1/posts/:id/reactions/:type')(
createRequest({
...userReq,
params: { id: 'post-1', type: 'like' },
}),
createResponseRecorder(),
);
await api.routes.get('GET /plaza/v1/posts/:id/comments')(
createRequest({
...userReq,
params: { id: 'post-1' },
query: { cursor: 'c1', limit: '5', parent_id: 'parent-1' },
}),
createResponseRecorder(),
);
await api.routes.get('POST /plaza/v1/posts/:id/comments')(
createRequest({
...userReq,
params: { id: 'post-1' },
body: { content: 'hello' },
}),
createResponseRecorder(),
);
await Promise.resolve();
assert.deepEqual(calls, [
{
kind: 'addReaction',
args: ['user-1', 'post-1', 'like'],
},
{
kind: 'recordEvents',
input: {
userId: 'user-1',
sessionId: 'cookie-session',
events: [{ event_type: 'like', post_id: 'post-1' }],
},
},
{
kind: 'removeReaction',
args: ['user-1', 'post-1', 'like'],
},
{
kind: 'listComments',
args: [
'post-1',
{
cursor: 'c1',
limit: '5',
parentId: 'parent-1',
viewerId: 'user-1',
},
],
},
{
kind: 'createComment',
args: ['user-1', 'post-1', { content: 'hello' }],
},
{
kind: 'recordEvents',
input: {
userId: 'user-1',
sessionId: 'cookie-session',
events: [{ event_type: 'comment', post_id: 'post-1' }],
},
},
]);
});
test('comment mutation routes preserve delete and liked default mapping', async () => {
const calls = [];
const api = createRouterRecorder();
const deps = createDependencies({
getPlazaInteractions: () => ({
async deleteComment(...args) {
calls.push({ kind: 'delete', args });
return { id: 'comment-1', deleted: true };
},
async toggleCommentLike(...args) {
calls.push({ kind: 'like', args });
return { liked: args[2] };
},
}),
});
attachPortalPlazaRoutes(api, deps.dependencies);
const base = {
currentUser: { id: 'user-1' },
params: { id: 'comment-1' },
};
await api.routes.get('DELETE /plaza/v1/comments/:id')(
createRequest(base),
createResponseRecorder(),
);
await api.routes.get('POST /plaza/v1/comments/:id/reactions')(
createRequest({ ...base, body: {} }),
createResponseRecorder(),
);
await api.routes.get('POST /plaza/v1/comments/:id/reactions')(
createRequest({ ...base, body: { liked: false } }),
createResponseRecorder(),
);
assert.deepEqual(calls, [
{ kind: 'delete', args: ['user-1', 'comment-1'] },
{ kind: 'like', args: ['user-1', 'comment-1', true] },
{ kind: 'like', args: ['user-1', 'comment-1', false] },
]);
});
test('profile, user posts, follow, and unfollow preserve optional viewer mapping', async () => {
const calls = [];
const api = createRouterRecorder();
const deps = createDependencies({
getPlazaInteractions: () => ({
async getUserProfile(...args) {
calls.push({ kind: 'profile', args });
return { slug: args[0] };
},
async listUserPosts(...args) {
calls.push({ kind: 'posts', args });
return { items: [] };
},
async followUser(...args) {
calls.push({ kind: 'follow', args });
return { following: true };
},
async unfollowUser(...args) {
calls.push({ kind: 'unfollow', args });
return { following: false };
},
}),
});
attachPortalPlazaRoutes(api, deps.dependencies);
await api.routes.get('GET /plaza/v1/users/:slug')(
createRequest({ params: { slug: 'alice' } }),
createResponseRecorder(),
);
await api.routes.get('GET /plaza/v1/users/:slug/posts')(
createRequest({
currentUser: { id: 'viewer-1' },
params: { slug: 'alice' },
query: { cursor: 'c1', limit: '9' },
}),
createResponseRecorder(),
);
await api.routes.get('POST /plaza/v1/users/:slug/follow')(
createRequest({
currentUser: { id: 'viewer-1' },
params: { slug: 'alice' },
}),
createResponseRecorder(),
);
await api.routes.get('DELETE /plaza/v1/users/:slug/follow')(
createRequest({
currentUser: { id: 'viewer-1' },
params: { slug: 'alice' },
}),
createResponseRecorder(),
);
assert.deepEqual(calls, [
{ kind: 'profile', args: ['alice', null] },
{
kind: 'posts',
args: [
'alice',
{ cursor: 'c1', limit: '9', viewerId: 'viewer-1' },
],
},
{ kind: 'follow', args: ['viewer-1', 'alice'] },
{ kind: 'unfollow', args: ['viewer-1', 'alice'] },
]);
});
test('post detail and CRUD preserve viewer, view/event side effects, auth, and statuses', async () => {
const calls = [];
const api = createRouterRecorder();
const deps = createDependencies({
getPlazaPosts: () => ({
async getPostById(...args) {
calls.push({ kind: 'get', args });
return { id: args[0] };
},
async createPost(...args) {
calls.push({ kind: 'create', args });
return { id: 'post-new' };
},
async updatePost(...args) {
calls.push({ kind: 'update', args });
return { id: args[1] };
},
async hidePost(...args) {
calls.push({ kind: 'hide', args });
return { id: args[1], hidden: true };
},
}),
getPlazaRedis: () => ({
async recordView(...args) {
calls.push({ kind: 'view', args });
},
}),
getPlazaEvents: () => ({
async recordEvents(input) {
calls.push({ kind: 'events', input });
return {};
},
}),
resolveClientIp: () => '198.51.100.8',
randomUUID: () => 'session-view',
});
attachPortalPlazaRoutes(api, deps.dependencies);
await api.routes.get('GET /plaza/v1/posts/:id')(
createRequest({
currentUser: { id: 'viewer-1' },
params: { id: 'post-1' },
}),
createResponseRecorder(),
);
await Promise.resolve();
const deniedRes = createResponseRecorder();
await api.routes.get('POST /plaza/v1/posts')(
createRequest({ body: { title: 'Denied' } }),
deniedRes,
);
assert.equal(deniedRes.statusCode, 401);
await api.routes.get('POST /plaza/v1/posts')(
createRequest({
currentUser: { id: 'author-1' },
body: { title: 'New' },
}),
createResponseRecorder(),
);
await api.routes.get('PATCH /plaza/v1/posts/:id')(
createRequest({
currentUser: { id: 'author-1' },
params: { id: 'post-1' },
body: { title: 'Updated' },
}),
createResponseRecorder(),
);
await api.routes.get('DELETE /plaza/v1/posts/:id')(
createRequest({
currentUser: { id: 'author-1' },
params: { id: 'post-1' },
}),
createResponseRecorder(),
);
assert.deepEqual(calls, [
{
kind: 'get',
args: ['post-1', { viewerId: 'viewer-1' }],
},
{
kind: 'view',
args: ['post-1', '198.51.100.8'],
},
{
kind: 'events',
input: {
userId: 'viewer-1',
sessionId: 'session-view',
events: [{ event_type: 'view', post_id: 'post-1' }],
},
},
{
kind: 'create',
args: ['author-1', { title: 'New' }],
},
{
kind: 'update',
args: ['author-1', 'post-1', { title: 'Updated' }],
},
{
kind: 'hide',
args: ['author-1', 'post-1'],
},
]);
assert.equal(deps.calls.data[1].status, 201);
});
test('service availability and route errors preserve existing error envelopes', async () => {
const unavailableApi = createRouterRecorder();
const unavailable = createDependencies();
attachPortalPlazaRoutes(unavailableApi, unavailable.dependencies);
const unavailableRes = createResponseRecorder();
await unavailableApi.routes.get('GET /plaza/v1/feed')(
createRequest(),
unavailableRes,
);
assert.equal(unavailableRes.statusCode, 503);
assert.equal(unavailable.calls.errors[0].code, 'plaza_unavailable');
const failureApi = createRouterRecorder();
const error = new Error('feed failed');
const failure = createDependencies({
getPlazaPosts: () => ({
async listFeed() {
throw error;
},
}),
randomUUID: () => 'session-1',
});
attachPortalPlazaRoutes(failureApi, failure.dependencies);
const failureReq = createRequest();
const failureRes = createResponseRecorder();
await failureApi.routes.get('GET /plaza/v1/feed')(
failureReq,
failureRes,
);
assert.deepEqual(failure.calls.routeErrors[0], {
res: failureRes,
req: failureReq,
error,
});
});