diff options
| author | Thijs Schreijer <thijs@thijsschreijer.nl> | 2026-08-31 22:08:11 +0200 |
|---|---|---|
| committer | Thijs Schreijer <thijs@thijsschreijer.nl> | 2026-09-01 07:57:09 +0200 |
| commit | c5f169ec303a70707bc48fbece9b52cc90a48de0 (patch) | |
| tree | 3e4bfc24b51bce648f99b072e47707e6399ed0a4 /test/httpfixture.lua | |
| parent | 0a4fa559e44ed3e77f723092feab977539b8aab0 (diff) | |
| download | luasocket-c5f169ec303a70707bc48fbece9b52cc90a48de0.tar.gz luasocket-c5f169ec303a70707bc48fbece9b52cc90a48de0.tar.bz2 luasocket-c5f169ec303a70707bc48fbece9b52cc90a48de0.zip | |
Add CI-runnable HTTP test fixture on the testsrvr/testclnt harness
Drives socket.http.request against fully scripted, controlled raw HTTP
responses over a real socket by reusing the existing testsrvr.lua
remote-execution server -- no Apache, no Docker, no third-party hosts.
Proves the fixture with a plain-GET smoke test, a chunked-transfer-
encoding smoke test, and a test asserting a response body delivered
across separately-timed writes (the incremental-delivery capability the
upcoming SSE tests need). Wires the new test into the CI regression step.
Claude-Session: https://claude.ai/code/session_01SW789SftppghLPVXYbWsGi
Diffstat (limited to 'test/httpfixture.lua')
| -rw-r--r-- | test/httpfixture.lua | 104 |
1 files changed, 104 insertions, 0 deletions
diff --git a/test/httpfixture.lua b/test/httpfixture.lua new file mode 100644 index 0000000..3ea0737 --- /dev/null +++ b/test/httpfixture.lua | |||
| @@ -0,0 +1,104 @@ | |||
| 1 | -- Test fixture for exercising socket.http.request against a fully | ||
| 2 | -- controlled, scripted HTTP response -- no Apache, no Docker, no | ||
| 3 | -- third-party hosts. Built on top of the existing testsrvr.lua remote- | ||
| 4 | -- execution server (see test/testclnt.lua for the `remote()` protocol this | ||
| 5 | -- reuses): a control connection sends Lua snippets for testsrvr.lua to | ||
| 6 | -- `load()` and run, which is how it's told to accept the connection that | ||
| 7 | -- socket.http.request makes and write raw response bytes back to it. | ||
| 8 | local socket = require("socket") | ||
| 9 | |||
| 10 | local M = {} | ||
| 11 | |||
| 12 | M.host = "localhost" | ||
| 13 | M.port = "8383" | ||
| 14 | |||
| 15 | -- Connects to a running testsrvr.lua as a control channel and returns a | ||
| 16 | -- `remote(fmt, ...)` function that scripts a Lua snippet to run on the | ||
| 17 | -- server. Mirrors test/testclnt.lua's own remote(): the snippet is | ||
| 18 | -- squashed onto a single line (embedded newlines become ';', runs of | ||
| 19 | -- whitespace collapse to one space) since the control channel is | ||
| 20 | -- line-oriented, and the ack sent back is only proof the server received | ||
| 21 | -- the command, not that it finished running it -- so a snippet that blocks | ||
| 22 | -- (e.g. server:accept()) doesn't stall the caller. | ||
| 23 | function M.connect(host, port) | ||
| 24 | local control = assert(socket.connect(host or M.host, port or M.port)) | ||
| 25 | control:setoption("tcp-nodelay", true) | ||
| 26 | local function remote(fmt, ...) | ||
| 27 | local s = string.format(fmt, ...) | ||
| 28 | s = string.gsub(s, "\n", ";") | ||
| 29 | s = string.gsub(s, "%s+", " ") | ||
| 30 | s = string.gsub(s, "^%s*", "") | ||
| 31 | assert(control:send(s .. "\n")) | ||
| 32 | control:receive() | ||
| 33 | end | ||
| 34 | return control, remote | ||
| 35 | end | ||
| 36 | |||
| 37 | -- Turns a raw Lua string into a single-line, double-quoted Lua string | ||
| 38 | -- literal safe to embed in a remote()-scripted snippet: escapes backslash, | ||
| 39 | -- quote, and every whitespace control character so no byte in the | ||
| 40 | -- *encoded* text is itself whitespace. remote() collapses whitespace runs | ||
| 41 | -- and turns newlines into ';', which would otherwise corrupt raw response | ||
| 42 | -- bytes (status lines/headers/chunked framing all rely on literal CRLF). | ||
| 43 | local function quote(s) | ||
| 44 | s = string.gsub(s, "\\", "\\\\") | ||
| 45 | s = string.gsub(s, "\"", "\\\"") | ||
| 46 | s = string.gsub(s, "\r", "\\r") | ||
| 47 | s = string.gsub(s, "\n", "\\n") | ||
| 48 | s = string.gsub(s, "\t", "\\t") | ||
| 49 | return "\"" .. s .. "\"" | ||
| 50 | end | ||
| 51 | |||
| 52 | -- Scripts the server to accept one connection (into the shared `data` | ||
| 53 | -- global, mirroring test/testclnt.lua's reconnect()), write a sequence of | ||
| 54 | -- raw byte chunks to it, then close it. `chunks` is an array of either | ||
| 55 | -- plain strings, or {body, delay = seconds} tables -- the delay (via | ||
| 56 | -- socket.sleep) is applied before sending that chunk, so callers can prove | ||
| 57 | -- incremental/partial delivery instead of one atomic send. | ||
| 58 | function M.accept_and_send(remote, chunks) | ||
| 59 | local parts = { | ||
| 60 | "if data then data:close() data = nil end", | ||
| 61 | "data = server:accept()", | ||
| 62 | "data:setoption(\"tcp-nodelay\", true)", | ||
| 63 | } | ||
| 64 | for _, chunk in ipairs(chunks) do | ||
| 65 | local body, delay | ||
| 66 | if type(chunk) == "table" then | ||
| 67 | body, delay = chunk[1], chunk.delay | ||
| 68 | else | ||
| 69 | body = chunk | ||
| 70 | end | ||
| 71 | if delay then | ||
| 72 | parts[#parts + 1] = string.format("socket.sleep(%f)", delay) | ||
| 73 | end | ||
| 74 | parts[#parts + 1] = "data:send(" .. quote(body) .. ")" | ||
| 75 | end | ||
| 76 | parts[#parts + 1] = "data:close() data = nil" | ||
| 77 | remote(table.concat(parts, "\n")) | ||
| 78 | end | ||
| 79 | |||
| 80 | -- Builds a raw HTTP/1.1 response (status line + headers + body) as a | ||
| 81 | -- single CRLF-framed string, ready to hand to accept_and_send. | ||
| 82 | function M.response(status, headers, body) | ||
| 83 | local lines = { "HTTP/1.1 " .. status } | ||
| 84 | for _, h in ipairs(headers or {}) do | ||
| 85 | lines[#lines + 1] = h | ||
| 86 | end | ||
| 87 | lines[#lines + 1] = "" | ||
| 88 | lines[#lines + 1] = body or "" | ||
| 89 | return table.concat(lines, "\r\n") | ||
| 90 | end | ||
| 91 | |||
| 92 | -- Encodes a list of body pieces as HTTP chunked-transfer-encoding, ending | ||
| 93 | -- with the terminating zero-size chunk. Saves callers from hand-rolling | ||
| 94 | -- the "<hex-size>\r\n<data>\r\n" framing for chunked test bodies. | ||
| 95 | function M.chunked(pieces) | ||
| 96 | local out = {} | ||
| 97 | for _, piece in ipairs(pieces) do | ||
| 98 | out[#out + 1] = string.format("%x\r\n%s\r\n", #piece, piece) | ||
| 99 | end | ||
| 100 | out[#out + 1] = "0\r\n\r\n" | ||
| 101 | return table.concat(out) | ||
| 102 | end | ||
| 103 | |||
| 104 | return M | ||
