aboutsummaryrefslogtreecommitdiff
path: root/test/ssehttptest.lua
diff options
context:
space:
mode:
Diffstat (limited to 'test/ssehttptest.lua')
-rw-r--r--test/ssehttptest.lua151
1 files changed, 151 insertions, 0 deletions
diff --git a/test/ssehttptest.lua b/test/ssehttptest.lua
new file mode 100644
index 0000000..b4ba6bc
--- /dev/null
+++ b/test/ssehttptest.lua
@@ -0,0 +1,151 @@
1-- End-to-end coverage proving socket.http.request, the headers_callback
2-- hook, and socket.sse cooperate correctly over a real socket -- using the
3-- httpfixture.lua harness (see httpfixturetest.lua) instead of Apache or a
4-- third-party host. Also covers the two headers_callback interactions that
5-- only matter once other request-handling logic (redirects,
6-- shouldreceivebody) is in the path.
7local socket = require("socket")
8local http = require("socket.http")
9local ltn12 = require("ltn12")
10local sse = require("socket.sse")
11local fixture = require("httpfixture")
12
13dofile("testsupport.lua")
14
15local url = "http://" .. fixture.host .. ":" .. fixture.port .. "/"
16
17local control, remote = fixture.connect()
18
19io.write("testing a live SSE stream dispatches Messages incrementally, not batched until close: ")
20do
21 local received, elapsed = {}, {}
22 local t0
23 local factory = sse.responseheaders(function(message)
24 table.insert(received, message)
25 table.insert(elapsed, socket.gettime() - t0)
26 return true
27 end)
28
29 -- Framed as HTTP chunked-transfer-encoding, one SSE Message per HTTP
30 -- chunk: the "until-closed"/"default" body source reads in fixed
31 -- socket.BLOCKSIZE (2048-byte) gulps, which would silently buffer both
32 -- of these small Messages into one read and defeat this test; the
33 -- chunked source instead reads exactly each chunk's declared size, so
34 -- Message 1 surfaces as soon as its chunk arrives, independent of
35 -- Message 2's delayed chunk.
36 local function httpchunk(piece)
37 return string.format("%x\r\n%s\r\n", #piece, piece)
38 end
39
40 -- generous relative to the artificial delay so a loaded CI runner's
41 -- scheduling jitter can't turn correct incremental behavior into a
42 -- flaky failure; the delay is deliberately long enough that "arrived
43 -- before it" and "arrived after it" stay unambiguous even with slack
44 local delay = 1.0
45 local headersblock = fixture.response("200 OK", {
46 "Content-Type: text/event-stream",
47 "Transfer-Encoding: chunked",
48 "Connection: close",
49 }, "")
50 fixture.accept_and_send(remote, {
51 headersblock,
52 httpchunk("data: first\n\n"),
53 { httpchunk("data: second\n\n"), delay = delay },
54 "0\r\n\r\n",
55 })
56
57 t0 = socket.gettime()
58 local ok, code = assert(http.request{ url = url .. "sse", headers_callback = factory })
59 assert(ok, "request failed")
60 assert(code == 200, "status code mismatch: " .. tostring(code))
61 assert(#received == 2, "expected two messages, got " .. #received)
62 assert(received[1].data == "first", "wrong data for message 1: " .. tostring(received[1].data))
63 assert(received[2].data == "second", "wrong data for message 2: " .. tostring(received[2].data))
64 -- message 1 must have been dispatched well before the server even sent
65 -- (let alone finished delaying) message 2 -- if delivery were batched
66 -- until the connection closed instead, both messages would only appear
67 -- once the full delay had elapsed
68 assert(elapsed[1] < delay / 2,
69 "message 1 dispatched too late (" .. elapsed[1] .. "s) to have arrived before message 2's delayed send")
70 assert(elapsed[2] >= delay / 2,
71 "message 2 dispatched suspiciously early (" .. elapsed[2] .. "s); the server's artificial delay may not have been exercised")
72end
73print("ok")
74
75io.write("testing headers_callback is not invoked for a redirected (3xx) response: ")
76do
77 local calls = {}
78 local function responseheaders(code)
79 table.insert(calls, code)
80 return true
81 end
82
83 local redirect = fixture.response("302 Found", {
84 "Location: /final",
85 "Content-Length: 0",
86 "Connection: close",
87 }, "")
88 local finalbody = "landed"
89 local final = fixture.response("200 OK", {
90 "Content-Length: " .. #finalbody,
91 "Connection: close",
92 }, finalbody)
93 fixture.accept_and_send_sequence(remote, { { redirect }, { final } })
94
95 local target = {}
96 local ok, code = assert(http.request{
97 url = url .. "redirect-me",
98 sink = ltn12.sink.table(target),
99 headers_callback = responseheaders,
100 })
101 assert(ok, "request failed")
102 assert(table.concat(target) == finalbody, "expected the redirected response's body")
103 assert(code == 200, "expected the final response's status code, got " .. tostring(code))
104 assert(#calls == 1, "expected headers_callback to be invoked exactly once, got " .. #calls)
105 assert(calls[1] == 200, "headers_callback must only ever see the final response, got " .. tostring(calls[1]))
106end
107print("ok")
108
109-- Shared by the three shouldreceivebody variants below (204, 304, HEAD):
110-- scripts `response`, issues a request built from `reqtextra`, and asserts
111-- headers_callback still fires with `expectedcode` while the sink it offers
112-- is never actually invoked, since shouldreceivebody skips the body.
113local function assertsinkswapskipped(reqtextra, response, expectedcode)
114 local sinkcalls, invokedwith = 0, nil
115 local function responseheaders(code)
116 invokedwith = code
117 return true, function() sinkcalls = sinkcalls + 1; return 1 end
118 end
119 fixture.accept_and_send(remote, { response })
120
121 local reqt = { url = url .. "skip-body", headers_callback = responseheaders }
122 for k, v in pairs(reqtextra or {}) do reqt[k] = v end
123 local ok, code = assert(http.request(reqt))
124 assert(ok, "request failed")
125 assert(code == expectedcode, "status code mismatch: " .. tostring(code))
126 assert(invokedwith == expectedcode, "expected headers_callback to still be invoked, got " .. tostring(invokedwith))
127 assert(sinkcalls == 0, "the offered sink must never run when shouldreceivebody skips the body")
128end
129
130io.write("testing the headers_callback sink-swap offer is inert when a 204 response skips the body: ")
131assertsinkswapskipped(nil, fixture.response("204 No Content", { "Connection: close" }, ""), 204)
132print("ok")
133
134io.write("testing the headers_callback sink-swap offer is inert when a 304 response skips the body: ")
135assertsinkswapskipped(nil, fixture.response("304 Not Modified", { "Connection: close" }, ""), 304)
136print("ok")
137
138io.write("testing the headers_callback sink-swap offer is inert when a HEAD request skips the body: ")
139do
140 local body = "should never be read"
141 local response = fixture.response("200 OK", {
142 "Content-Length: " .. #body,
143 "Connection: close",
144 }, body)
145 assertsinkswapskipped({ method = "HEAD" }, response, 200)
146end
147print("ok")
148
149remote("os.exit()")
150
151print("the library passed all tests")