aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThijs Schreijer <thijs@thijsschreijer.nl>2026-08-31 22:08:11 +0200
committerThijs Schreijer <thijs@thijsschreijer.nl>2026-09-01 07:57:09 +0200
commitc5f169ec303a70707bc48fbece9b52cc90a48de0 (patch)
tree3e4bfc24b51bce648f99b072e47707e6399ed0a4
parent0a4fa559e44ed3e77f723092feab977539b8aab0 (diff)
downloadluasocket-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
-rw-r--r--.github/workflows/build.yml2
-rw-r--r--test/httpfixture.lua104
-rw-r--r--test/httpfixturetest.lua68
3 files changed, 174 insertions, 0 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 96564fb..a81649a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -41,6 +41,8 @@ jobs:
41 lua hello.lua 41 lua hello.lua
42 lua testsrvr.lua > /dev/null & 42 lua testsrvr.lua > /dev/null &
43 lua testclnt.lua 43 lua testclnt.lua
44 lua testsrvr.lua > /dev/null &
45 lua httpfixturetest.lua
44 lua stufftest.lua 46 lua stufftest.lua
45 lua excepttest.lua 47 lua excepttest.lua
46 lua test_bind.lua 48 lua test_bind.lua
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.
8local socket = require("socket")
9
10local M = {}
11
12M.host = "localhost"
13M.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.
23function 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
35end
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).
43local 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 .. "\""
50end
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.
58function 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"))
78end
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.
82function 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")
90end
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.
95function 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)
102end
103
104return M
diff --git a/test/httpfixturetest.lua b/test/httpfixturetest.lua
new file mode 100644
index 0000000..89dfd90
--- /dev/null
+++ b/test/httpfixturetest.lua
@@ -0,0 +1,68 @@
1-- Smoke tests proving out the httpfixture.lua harness: drive
2-- socket.http.request against fully scripted, controlled raw HTTP
3-- responses over a real socket via the testsrvr.lua remote-execution
4-- server -- no Apache, no Docker, no third-party hosts. Later SSE tests
5-- build on this same fixture for real-socket, incremental-delivery
6-- coverage.
7local http = require("socket.http")
8local fixture = require("httpfixture")
9
10dofile("testsupport.lua")
11
12local url = "http://" .. fixture.host .. ":" .. fixture.port .. "/"
13
14local control, remote = fixture.connect()
15
16io.write("testing plain GET with a known body: ")
17do
18 local body = "hello, luasocket"
19 local response = fixture.response("200 OK", {
20 "Content-Length: " .. #body,
21 "Connection: close",
22 }, body)
23 fixture.accept_and_send(remote, { response })
24
25 local respbody, code = assert(http.request(url .. "get"))
26 assert(respbody == body, "body mismatch: " .. tostring(respbody))
27 assert(code == 200, "status code mismatch: " .. tostring(code))
28end
29print("ok")
30
31io.write("testing chunked-transfer-encoded body: ")
32do
33 local pieces = { "Hello, ", "chunked ", "world!" }
34 local response = fixture.response("200 OK", {
35 "Transfer-Encoding: chunked",
36 "Connection: close",
37 }, fixture.chunked(pieces))
38 fixture.accept_and_send(remote, { response })
39
40 local respbody, code = assert(http.request(url .. "chunked"))
41 assert(respbody == table.concat(pieces), "body mismatch: " .. tostring(respbody))
42 assert(code == 200, "status code mismatch: " .. tostring(code))
43end
44print("ok")
45
46io.write("testing a body delivered across separately-timed writes: ")
47do
48 local part1, part2 = "partial-", "delivery"
49 local body = part1 .. part2
50 local headersblock = fixture.response("200 OK", {
51 "Content-Length: " .. #body,
52 "Connection: close",
53 }, "")
54 fixture.accept_and_send(remote, {
55 headersblock,
56 { part1, delay = 0.2 },
57 part2,
58 })
59
60 local respbody, code = assert(http.request(url .. "slow"))
61 assert(respbody == body, "body mismatch: " .. tostring(respbody))
62 assert(code == 200, "status code mismatch: " .. tostring(code))
63end
64print("ok")
65
66remote("os.exit()")
67
68print("the library passed all tests")