638 lines
17 KiB
JavaScript
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,
|
|
});
|
|
});
|