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