From: Laurent Mazet Date: Wed, 22 Jul 2026 17:43:21 +0000 (+0200) Subject: add test for html and sse requests X-Git-Url: https://secure.softndesign.org/git/?a=commitdiff_plain;h=4627a3b39fefd8a38b6d37f39606b00dce720f43;p=trivabble.git add test for html and sse requests --- diff --git a/test/test-common.js b/test/test-common.js index 0fa23c0..5a4535b 100644 --- a/test/test-common.js +++ b/test/test-common.js @@ -107,6 +107,35 @@ function postRaw(body, headers = {}) { }); } +/* Sends one raw HTTP request to an arbitrary path and returns the full */ +/* response. */ +function requestRaw(method, requestPath, body = "", headers = {}) { + return new Promise((resolve, reject) => { + const req = http.request({ + host: HOST, + port: PORT, + path: requestPath, + method, + headers: { + ...headers, + ...(body ? {"Content-Length": Buffer.byteLength(body)} : {}) + } + }, (res) => { + let data = ""; + res.setEncoding("utf8"); + res.on("data", (chunk) => { + data += chunk; + }); + res.on("end", () => resolve({statusCode: res.statusCode, headers: res.headers, body: data})); + }); + req.on("error", reject); + if (body) { + req.write(body); + } + req.end(); + }); +} + /* Opens one server-sent-events stream using the server's URL-encoded */ /* payload format. */ function sseConnect(initialPayload) { @@ -126,6 +155,69 @@ function sseConnect(initialPayload) { }); } +/* Reads the first SSE chunk, then closes the request to avoid hanging tests. */ +function readFirstSseChunk(initialPayload) { + return new Promise((resolve, reject) => { + sseConnect(initialPayload).then(({req, res}) => { + let settled = false; + + function finish(value) { + if (!settled) { + settled = true; + try { + req.destroy(); + } catch (e) { + /* ignore socket shutdown errors */ + } + resolve({ + statusCode: res.statusCode, + headers: res.headers, + chunk: value + }); + } + } + + res.once("data", (chunk) => finish(chunk)); + res.once("error", reject); + + setTimeout(() => finish(""), 1000); + }, reject); + }); +} + +/* Collects SSE chunks for a short time window, then closes the request. */ +function collectSseChunks(initialPayload, durationMs = 500) { + return new Promise((resolve, reject) => { + sseConnect(initialPayload).then(({req, res}) => { + let data = ""; + let settled = false; + + function finish() { + if (!settled) { + settled = true; + try { + req.destroy(); + } catch (e) { + /* ignore socket shutdown errors */ + } + resolve({ + statusCode: res.statusCode, + headers: res.headers, + body: data + }); + } + } + + res.on("data", (chunk) => { + data += chunk; + }); + res.once("error", reject); + + setTimeout(finish, durationMs); + }, reject); + }); +} + /* Client websocket frames must be masked, so tests build frames manually. */ function createMaskedWebSocketFrame(data) { const payload = Buffer.from(data, "utf8"); @@ -359,6 +451,9 @@ module.exports = { log, postJson, postRaw, + requestRaw, + readFirstSseChunk, + collectSseChunks, randomPlayer, runTestSection, sseConnect, diff --git a/test/test-http.js b/test/test-http.js index 6dc3c3f..0fe9659 100644 --- a/test/test-http.js +++ b/test/test-http.js @@ -5,7 +5,7 @@ * ----------------------------------------------------------------------------- */ -const {assert, postJson, postRaw, randomPlayer, runTestSection} = require("./test-common"); +const {assert, postJson, postRaw, randomPlayer, readFirstSseChunk, requestRaw, runTestSection} = require("./test-common"); /* Basic health check for the main JSON command endpoint. */ async function testHello() { @@ -34,6 +34,28 @@ async function testUnknownCommand() { assert(body[0].reason === "Unknown command", "unknown command should report the correct reason"); } +/* SSE GET requests should switch to event-stream mode and emit the initial */ +/* state. */ +async function testSseEndpointReturnsEventStream() { + const playerName = randomPlayer("sse-http"); + const gameNumber = `sse-http-${Date.now()}-${Math.floor(Math.random() * 1e6)}`; + + const res = await readFirstSseChunk({playerName, gameNumber}); + + assert(res.statusCode === 200, "SSE endpoint should return HTTP 200"); + assert((res.headers["content-type"] || "").includes("text/event-stream"), "SSE endpoint should return event-stream content type"); + assert(res.chunk.includes("data:"), "SSE endpoint should emit an initial data frame"); + assert(res.chunk.includes(`"playerName":"${playerName}"`), "SSE initial payload should include the player name"); +} + +/* Invalid JSON delivered through the SSE URL should still be rejected */ +/* cleanly. */ +async function testSseEndpointRejectsInvalidJson() { + const res = await requestRaw("GET", "/sse/%7Bnot-json"); + assert(res.statusCode === 400, "invalid SSE JSON should return 400"); + assert(res.body.includes("Unable to parse your JSON data"), "invalid SSE JSON should explain the parse error"); +} + /* Oversized request bodies should be rejected by closing the connection. */ async function testOversizedPostClosesConnection() { const body = "x".repeat(1000001); @@ -52,5 +74,7 @@ runTestSection("HTTP", [ testHello, testInvalidJson, testUnknownCommand, + testSseEndpointReturnsEventStream, + testSseEndpointRejectsInvalidJson, testOversizedPostClosesConnection ]); diff --git a/test/test-realtime.js b/test/test-realtime.js index 0d645ae..71bfbec 100644 --- a/test/test-realtime.js +++ b/test/test-realtime.js @@ -10,10 +10,11 @@ const { createMaskedWebSocketFrame, crypto, net, + collectSseChunks, postJson, randomPlayer, + readFirstSseChunk, runTestSection, - sseConnect, wait, wsConnect } = require("./test-common"); @@ -24,19 +25,19 @@ async function testSseReceivesEvents() { const listener = randomPlayer("listener"); const actor = randomPlayer("actor"); - const connection = await sseConnect({playerName: listener, gameNumber}); - let stream = ""; - connection.res.on("data", (chunk) => { - stream += chunk; - }); + const initial = await readFirstSseChunk({playerName: listener, gameNumber}); + assert(initial.statusCode === 200, "SSE stream should return HTTP 200"); + assert(initial.chunk.includes("data:"), "SSE stream should emit an initial data frame"); + assert(initial.chunk.includes(`"playerName":"${listener}"`), "SSE initial frame should include the listener name"); + + const streamPromise = collectSseChunks({playerName: listener, gameNumber}, 700); await wait(200); await postJson({playerName: actor, gameNumber, cmds: [{cmd: "hello"}]}); - await wait(300); - connection.req.destroy(); - assert(stream.includes("data:"), "SSE stream should contain data frames"); - assert(stream.includes("players"), "SSE stream should include player event data"); + const stream = await streamPromise; + assert(stream.body.includes("data:"), "SSE stream should contain data frames"); + assert(stream.body.includes("players"), "SSE stream should include player event data"); } /* WebSocket clients should be able to send a command and receive a reply. */