import { test } from 'node:test' import { deepStrictEqual, equal, match, rejects } from 'node:assert/strict' import http from 'node:http' import { createUserinputBoardProxyHandler } from '../../src/server/userinput-board-proxy.mjs' const board = { spaceDid: 'did:plc:reams-test-board', spaceRkey: 'board-key' } async function withServers(upstreamHandler, run) { const upstream = http.createServer(upstreamHandler) await new Promise(resolve => upstream.listen(0, '127.0.0.1', resolve)) const origin = `http://127.0.0.1:${upstream.address().port}` const handler = createUserinputBoardProxyHandler({ ...board, apiBaseUrl: origin, allowLoopbackTest: true }) const app = http.createServer(async (req, res) => { if (!await handler(req, res)) { res.writeHead(404); res.end() } }) await new Promise(resolve => app.listen(0, '127.0.0.1', resolve)) try { await run(`http://127.0.0.1:${app.address().port}`, origin) } finally { await Promise.all([upstream, app].map(server => new Promise(resolve => server.close(resolve)))) } } test('proxies only the configured public board without forwarding caller credentials', async () => { await withServers((req, res) => { equal(req.url, `/api/board/${board.spaceDid}/${board.spaceRkey}`) equal(req.method, 'GET') equal(req.headers.authorization, undefined) equal(req.headers.cookie, undefined) equal(req.headers.accept, 'application/json') res.writeHead(200, { 'content-type': 'application/json' }) res.end(JSON.stringify({ posts: [{ title: 'Public' }] })) }, async origin => { const response = await fetch(`${origin}/api/feedback/board?url=http://127.0.0.1/secret`, { headers: { authorization: 'Bearer caller-secret', cookie: 'session=caller-secret' }, }) equal(response.status, 200) match(response.headers.get('content-type'), /^application\/json; charset=utf-8$/) equal(response.headers.get('cache-control'), 'no-store') deepStrictEqual(await response.json(), { posts: [{ title: 'Public' }], spaceUrl: 'https://userinput.app/s/did:plc:reams-test-board/board-key' }) }) }) test('maps upstream 503 to actionable service-unavailable JSON', async () => { await withServers((_req, res) => { res.writeHead(503); res.end('edge unavailable') }, async origin => { const response = await fetch(`${origin}/api/feedback/board`) equal(response.status, 503) match((await response.json()).message, /temporarily unavailable.*Try again/) }) }) test('fails closed on invalid configuration and disallows arbitrary non-HTTPS origins', async () => { for (const options of [ { ...board, spaceDid: 'not-a-did' }, { ...board, spaceRkey: '../secret' }, { ...board, apiBaseUrl: 'http://127.0.0.1:1234' }, { ...board, apiBaseUrl: 'https://user:pass@userinput.app' }, ]) rejects(Promise.resolve().then(() => createUserinputBoardProxyHandler(options))) }) test('does not match unrelated paths and rejects non-GET methods', async () => { const handler = createUserinputBoardProxyHandler({ ...board }) const response = { writeHead(status, headers) { this.status = status; this.headers = headers }, end(body) { this.body = body } } equal(await handler({ method: 'GET', url: '/elsewhere' }, response), false) equal(await handler({ method: 'POST', url: '/api/feedback/board' }, response), true) equal(response.status, 405) equal(response.headers['cache-control'], 'no-store') deepStrictEqual(JSON.parse(response.body), { error: 'method_not_allowed', message: 'Use GET to read the public feedback board.' }) }) test('bounds upstream bodies', async () => { const originServer = http.createServer((_req, res) => { res.writeHead(200, { 'content-type': 'application/json' }) res.end('"' + 'x'.repeat(128) + '"') }) await new Promise(resolve => originServer.listen(0, '127.0.0.1', resolve)) const handler = createUserinputBoardProxyHandler({ ...board, apiBaseUrl: `http://127.0.0.1:${originServer.address().port}`, allowLoopbackTest: true, maxResponseBytes: 32 }) const response = { writeHead(status) { this.status = status }, end(body) { this.body = body } } try { await handler({ method: 'GET', url: '/api/feedback/board' }, response) equal(response.status, 502) match(JSON.parse(response.body).message, /configuration/) } finally { await new Promise(resolve => originServer.close(resolve)) } }) test('times out a slow upstream response', async () => { const upstream = http.createServer(() => {}) await new Promise(resolve => upstream.listen(0, '127.0.0.1', resolve)) const handler = createUserinputBoardProxyHandler({ ...board, apiBaseUrl: `http://127.0.0.1:${upstream.address().port}`, allowLoopbackTest: true, timeoutMs: 20 }) const response = { writeHead(status) { this.status = status }, end(body) { this.body = body } } try { await handler({ method: 'GET', url: '/api/feedback/board' }, response) equal(response.status, 504) match(JSON.parse(response.body).message, /too long to respond/) } finally { upstream.closeAllConnections() await new Promise(resolve => upstream.close(resolve)) } })