aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorThijs Schreijer <thijs@thijsschreijer.nl>2026-08-31 22:27:42 +0200
committerThijs Schreijer <thijs@thijsschreijer.nl>2026-09-01 07:59:54 +0200
commitcd6b41a13af403d32e08ff416e60982b5cb718dc (patch)
tree3865d91753fd636a866d4711f50ab27613e58634 /test
parentc5f169ec303a70707bc48fbece9b52cc90a48de0 (diff)
downloadluasocket-feat/sse.tar.gz
luasocket-feat/sse.tar.bz2
luasocket-feat/sse.zip
Add SSE sink (socket.sse module)feat/sse
Implements SSE by using the replacement body sink. Replaces the sink with an SSE parser if the response is a text stream. Example application included
Diffstat (limited to '')
-rw-r--r--test/httpfixture.lua47
-rw-r--r--test/ssehttptest.lua151
-rw-r--r--test/ssetest.lua370
3 files changed, 556 insertions, 12 deletions
diff --git a/test/httpfixture.lua b/test/httpfixture.lua
index 3ea0737..14c7548 100644
--- a/test/httpfixture.lua
+++ b/test/httpfixture.lua
@@ -49,18 +49,12 @@ local function quote(s)
49 return "\"" .. s .. "\"" 49 return "\"" .. s .. "\""
50end 50end
51 51
52-- Scripts the server to accept one connection (into the shared `data` 52-- Appends the script parts for one accept-send-close cycle (see
53-- global, mirroring test/testclnt.lua's reconnect()), write a sequence of 53-- accept_and_send_sequence) onto `parts`.
54-- raw byte chunks to it, then close it. `chunks` is an array of either 54local function append_connection(parts, chunks)
55-- plain strings, or {body, delay = seconds} tables -- the delay (via 55 parts[#parts + 1] = "if data then data:close() data = nil end"
56-- socket.sleep) is applied before sending that chunk, so callers can prove 56 parts[#parts + 1] = "data = server:accept()"
57-- incremental/partial delivery instead of one atomic send. 57 parts[#parts + 1] = "data:setoption(\"tcp-nodelay\", true)"
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 58 for _, chunk in ipairs(chunks) do
65 local body, delay 59 local body, delay
66 if type(chunk) == "table" then 60 if type(chunk) == "table" then
@@ -74,6 +68,35 @@ function M.accept_and_send(remote, chunks)
74 parts[#parts + 1] = "data:send(" .. quote(body) .. ")" 68 parts[#parts + 1] = "data:send(" .. quote(body) .. ")"
75 end 69 end
76 parts[#parts + 1] = "data:close() data = nil" 70 parts[#parts + 1] = "data:close() data = nil"
71end
72
73-- Scripts the server to accept one connection (into the shared `data`
74-- global, mirroring test/testclnt.lua's reconnect()), write a sequence of
75-- raw byte chunks to it, then close it. `chunks` is an array of either
76-- plain strings, or {body, delay = seconds} tables -- the delay (via
77-- socket.sleep) is applied before sending that chunk, so callers can prove
78-- incremental/partial delivery instead of one atomic send.
79function M.accept_and_send(remote, chunks)
80 local parts = {}
81 append_connection(parts, chunks)
82 remote(table.concat(parts, "\n"))
83end
84
85-- Like accept_and_send, but scripts several accept-send-close cycles as one
86-- server-side script sent over a single remote() round trip. Needed for a
87-- client call that opens more than one connection in sequence within a
88-- single call of its own (e.g. socket.http.request following a redirect):
89-- queuing a second accept_and_send for that connection ahead of time would
90-- deadlock, since its remote() call can't get an ack until the server
91-- finishes the first accept_and_send's blocking accept() -- which itself
92-- can't complete until the client makes the very call that's stuck waiting
93-- on that ack. `connections` is an array of `chunks` arrays, one per
94-- connection, handled in order.
95function M.accept_and_send_sequence(remote, connections)
96 local parts = {}
97 for _, chunks in ipairs(connections) do
98 append_connection(parts, chunks)
99 end
77 remote(table.concat(parts, "\n")) 100 remote(table.concat(parts, "\n"))
78end 101end
79 102
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")
diff --git a/test/ssetest.lua b/test/ssetest.lua
new file mode 100644
index 0000000..03206ba
--- /dev/null
+++ b/test/ssetest.lua
@@ -0,0 +1,370 @@
1local socket = require("socket")
2local sse = require("socket.sse")
3
4dofile("testsupport.lua")
5
6-- collects everything a parser sink dispatches to it (Messages and, when
7-- enabled, Comments) into an ordered list; the "collect" name signals it
8-- passes through unchanged rather than transforming
9local function collect()
10 local list = {}
11 local snk = function(item, err)
12 if err then return nil, err end
13 table.insert(list, item)
14 return 1
15 end
16 return snk, list
17end
18
19-- feeds a parser sink the given raw chunks, then signals end of stream
20local function feed(parser, ...)
21 for _, chunk in ipairs({...}) do
22 local ok, err = parser(chunk)
23 if not ok then return nil, err end
24 end
25 return parser(nil)
26end
27
28--------------------------------
29io.write("testing event/data/id parsing on a single Message, retry line consumed but not attached: ")
30do
31 local snk, list = collect()
32 local parser = sse.parser(snk)
33 local ok = feed(parser,
34 "event: greeting\r\n" ..
35 "data: hello\r\n" ..
36 "id: 1\r\n" ..
37 "retry: 3000\r\n" ..
38 "\r\n")
39 assert(ok, "parser returned error")
40 assert(#list == 1, "expected exactly one message")
41 assert(list[1].event == "greeting", "wrong event")
42 assert(list[1].data == "hello", "wrong data")
43 assert(list[1].id == "1", "wrong id")
44 assert(list[1].retry == nil, "Message must not carry a retry field; retry only lives on the context table")
45 print("ok")
46end
47
48--------------------------------
49io.write("testing multiple data: lines are joined with \\n: ")
50do
51 local snk, list = collect()
52 local parser = sse.parser(snk)
53 local ok = feed(parser, "data: line one\ndata: line two\ndata: line three\n\n")
54 assert(ok, "parser returned error")
55 assert(#list == 1, "expected exactly one message")
56 assert(list[1].data == "line one\nline two\nline three", "data not joined correctly: " .. list[1].data)
57 print("ok")
58end
59
60--------------------------------
61io.write("testing event defaults to 'message' when omitted: ")
62do
63 local snk, list = collect()
64 local parser = sse.parser(snk)
65 local ok = feed(parser, "data: no event here\n\n")
66 assert(ok, "parser returned error")
67 assert(#list == 1, "expected exactly one message")
68 assert(list[1].event == "message", "expected default event type, got " .. tostring(list[1].event))
69 print("ok")
70end
71
72--------------------------------
73io.write("testing id persists forward onto later Messages: ")
74do
75 local snk, list = collect()
76 local parser = sse.parser(snk)
77 local ok = feed(parser,
78 "id: abc\ndata: first\n\n" ..
79 "data: second\n\n" ..
80 "id: def\ndata: third\n\n" ..
81 "data: fourth\n\n")
82 assert(ok, "parser returned error")
83 assert(#list == 4, "expected four messages, got " .. #list)
84 assert(list[1].id == "abc", "message 1 id")
85 assert(list[2].id == "abc", "message 2 id should persist from message 1")
86 assert(list[3].id == "def", "message 3 id")
87 assert(list[4].id == "def", "message 4 id should persist from message 3")
88 print("ok")
89end
90
91--------------------------------
92io.write("testing comment lines are silently absorbed when comments is off: ")
93do
94 local snk, list = collect()
95 local parser = sse.parser(snk)
96 local ok = feed(parser, ": this is a comment\ndata: real message\n\n")
97 assert(ok, "parser returned error")
98 assert(#list == 1, "expected only the message, comment should be absorbed")
99 assert(list[1].data == "real message", "wrong data")
100 print("ok")
101end
102
103--------------------------------
104io.write("testing comment lines are dispatched distinctly when comments is on: ")
105do
106 local snk, list = collect()
107 local parser = sse.parser(snk, { comments = true })
108 local ok = feed(parser, ": keep-alive\ndata: real message\n\n")
109 assert(ok, "parser returned error")
110 assert(#list == 2, "expected comment and message, got " .. #list)
111 assert(list[1].comment == "keep-alive", "wrong comment text: " .. tostring(list[1].comment))
112 assert(list[1].event == nil, "comment must not look like a message")
113 assert(list[2].data == "real message", "wrong data")
114 print("ok")
115end
116
117--------------------------------
118io.write("testing context table gets last_event_id/retry written and retains them after parsing ends: ")
119do
120 local snk = collect()
121 local context = {}
122 local parser = sse.parser(snk, { context = context })
123 local ok = feed(parser, "id: xyz\nretry: 5000\ndata: hi\n\n")
124 assert(ok, "parser returned error")
125 assert(context.last_event_id == "xyz", "context.last_event_id not written")
126 assert(context.retry == 5000, "context.retry not written")
127 print("ok")
128end
129
130--------------------------------
131io.write("testing context.retry persists across later events that don't repeat it, and no Message ever carries a retry field: ")
132do
133 local snk, list = collect()
134 local context = {}
135 local parser = sse.parser(snk, { context = context })
136 local ok = feed(parser,
137 "retry: 2000\ndata: first\n\n" ..
138 "data: second\n\n")
139 assert(ok, "parser returned error")
140 assert(#list == 2, "expected two messages, got " .. #list)
141 assert(list[1].retry == nil, "message 1 must not carry a retry field")
142 assert(list[2].retry == nil, "message 2 must not carry a retry field")
143 assert(context.retry == 2000, "context.retry should still reflect the most recent retry hint after a later event that didn't repeat it")
144 print("ok")
145end
146
147--------------------------------
148io.write("testing an oversized line produces a distinct error and aborts: ")
149do
150 local snk, list = collect()
151 local parser = sse.parser(snk)
152 local huge = string.rep("x", sse.MAXLINESIZE + 1)
153 local ok, err = feed(parser, "data: " .. huge .. "\n\n")
154 assert(not ok, "expected parser to fail on oversized line")
155 assert(err == "oversized", "expected 'oversized' error, got " .. tostring(err))
156 assert(#list == 0, "no message should have been dispatched")
157 print("ok")
158end
159
160--------------------------------
161io.write("testing an oversized event (many lines under MAXEVENTSIZE total) produces a distinct error: ")
162do
163 local snk, list = collect()
164 local parser = sse.parser(snk)
165 local line = "data: " .. string.rep("y", 100) .. "\n"
166 local lines = string.rep(line, math.ceil(sse.MAXEVENTSIZE / #line) + 1)
167 local ok, err = feed(parser, lines)
168 assert(not ok, "expected parser to fail on oversized event")
169 assert(err == "oversized", "expected 'oversized' error, got " .. tostring(err))
170 assert(#list == 0, "no message should have been dispatched")
171 print("ok")
172end
173
174--------------------------------
175io.write("testing a field split across two raw-byte chunks is parsed correctly: ")
176do
177 local snk, list = collect()
178 local parser = sse.parser(snk)
179 -- split mid field-name, mid value, and mid line-terminator
180 local ok = feed(parser, "eve", "nt: greet", "ing\ndata: hel", "lo\r", "\n\r\n")
181 assert(ok, "parser returned error")
182 assert(#list == 1, "expected exactly one message, got " .. #list)
183 assert(list[1].event == "greeting", "wrong event: " .. tostring(list[1].event))
184 assert(list[1].data == "hello", "wrong data: " .. tostring(list[1].data))
185 print("ok")
186end
187
188--------------------------------
189io.write("testing a Message-sink error aborts the parser sink chain: ")
190do
191 local calls = 0
192 local snk = function(message)
193 calls = calls + 1
194 if calls == 1 then return 1 end
195 return nil, "sink refused message"
196 end
197 local parser = sse.parser(snk)
198 local ok, err = feed(parser, "data: first\n\ndata: second\n\n")
199 assert(not ok, "expected parser to propagate message sink error")
200 assert(err == "sink refused message", "unexpected error: " .. tostring(err))
201 assert(calls == 2, "expected the sink to be invoked twice")
202 print("ok")
203end
204
205--------------------------------
206io.write("testing a Message with no data: line is not dispatched: ")
207do
208 local snk, list = collect()
209 local parser = sse.parser(snk)
210 local ok = feed(parser, "event: ping\nid: 1\n\ndata: real\n\n")
211 assert(ok, "parser returned error")
212 assert(#list == 1, "the dataless block should not have produced a message")
213 assert(list[1].data == "real", "wrong data")
214 print("ok")
215end
216
217--------------------------------
218io.write("testing callbacksink lets a plain callback act as a message sink: ")
219do
220 local received = {}
221 local callback = function(message) table.insert(received, message); return true end
222 local snk = sse.callbacksink(callback)
223 local parser = sse.parser(snk)
224 local ok = feed(parser, "data: first\n\ndata: second\n\n")
225 assert(ok, "parser returned error")
226 assert(#received == 2, "expected two messages, got " .. #received)
227 assert(received[1].data == "first", "wrong data")
228 assert(received[2].data == "second", "wrong data")
229 print("ok")
230end
231
232--------------------------------
233io.write("testing callbacksink propagates an error returned by the callback: ")
234do
235 local calls = 0
236 local callback = function(message)
237 calls = calls + 1
238 if message.data == "bad" then return nil, "callback refused" end
239 return true
240 end
241 local snk = sse.callbacksink(callback)
242 local parser = sse.parser(snk)
243 local ok, err = feed(parser, "data: good\n\ndata: bad\n\ndata: unreachable\n\n")
244 assert(not ok, "expected callback error to abort the chain")
245 assert(err == "callback refused", "unexpected error: " .. tostring(err))
246 assert(calls == 2, "expected the callback to stop being invoked after it errors, got " .. calls)
247 print("ok")
248end
249
250--------------------------------
251io.write("testing callbacksink does not swallow a sink that fails with a falsy ok and no error message: ")
252do
253 local calls = 0
254 local rawsink = function(message)
255 calls = calls + 1
256 if message.data == "bad" then return false end
257 return 1
258 end
259 local snk = sse.callbacksink(rawsink)
260 local parser = sse.parser(snk)
261 local ok, err = feed(parser, "data: good\n\ndata: bad\n\ndata: unreachable\n\n")
262 assert(not ok, "expected the falsy ok to abort the chain even without an error message")
263 assert(err == nil, "no error message was given, so none should be invented")
264 assert(calls == 2, "expected the sink to stop being invoked once it fails, got " .. calls)
265 print("ok")
266end
267
268--------------------------------
269io.write("testing callbacksink treats a callback that returns nothing as an error, not a default success: ")
270do
271 local snk = sse.callbacksink(function(message) end)
272 local parser = sse.parser(snk)
273 local ok = feed(parser, "data: hi\n\n")
274 assert(not ok, "a callback must explicitly return truthy to signal success, matching a raw sink's contract")
275 print("ok")
276end
277
278--------------------------------
279io.write("testing responseheaders forwards its config (context/comments) to the parser: ")
280do
281 local received = {}
282 local context = {}
283 local factory = sse.responseheaders(
284 function(message) table.insert(received, message); return true end,
285 { comments = true, context = context })
286 local ok, sink = factory(200, { ["content-type"] = "text/event-stream" }, "HTTP/1.1 200 OK")
287 assert(ok, "expected success")
288 local fed = feed(sink, ": keep-alive\nid: xyz\ndata: hello\n\n")
289 assert(fed, "sink chain returned an error")
290 assert(#received == 2, "expected the comment and the message, got " .. #received)
291 assert(received[1].comment == "keep-alive", "comments config was not forwarded to the parser")
292 assert(context.last_event_id == "xyz", "context table was not forwarded to the parser")
293 print("ok")
294end
295
296--------------------------------
297io.write("testing responseheaders installs the sink chain on a matching Content-Type: ")
298do
299 local received = {}
300 local factory = sse.responseheaders(function(message) table.insert(received, message); return true end)
301 local reqt = {}
302 reqt.headers_callback = factory
303 local ok, sink = reqt.headers_callback(200, { ["content-type"] = "text/event-stream" }, "HTTP/1.1 200 OK")
304 assert(ok, "expected success on a matching content-type")
305 assert(type(sink) == "function", "expected a sink function on a matching content-type")
306 reqt.sink = sink
307 local fed = feed(reqt.sink, "data: hello\n\n")
308 assert(fed, "sink chain returned an error")
309 assert(#received == 1, "expected one message dispatched through the installed sink")
310 assert(received[1].data == "hello", "wrong data")
311 print("ok")
312end
313
314--------------------------------
315io.write("testing responseheaders also accepts a raw ltn12-style message sink directly: ")
316do
317 local received = {}
318 -- a raw sink following full ltn12 protocol: truthy on success, (nil, err) on failure
319 local rawsink = function(message)
320 table.insert(received, message)
321 return 1
322 end
323 local factory = sse.responseheaders(rawsink)
324 local ok, sink = factory(200, { ["content-type"] = "text/event-stream" }, "HTTP/1.1 200 OK")
325 assert(ok, "expected success")
326 local fed = feed(sink, "data: raw\n\n")
327 assert(fed, "sink chain returned an error")
328 assert(#received == 1, "expected one message")
329 assert(received[1].data == "raw", "wrong data")
330 print("ok")
331end
332
333--------------------------------
334io.write("testing responseheaders matches a Content-Type with trailing parameters: ")
335do
336 local factory = sse.responseheaders(function() end)
337 local ok, sink = factory(200, { ["content-type"] = "text/event-stream; charset=utf-8" }, "HTTP/1.1 200 OK")
338 assert(ok, "expected success")
339 assert(type(sink) == "function", "expected a sink for a parameterized but matching content-type")
340 print("ok")
341end
342
343--------------------------------
344io.write("testing responseheaders declines cleanly on a non-matching Content-Type: ")
345do
346 local calledback = false
347 local factory = sse.responseheaders(function() calledback = true end)
348 local reqt = { sink = "original sink placeholder" }
349 local ok, sink = factory(200, { ["content-type"] = "text/html" }, "HTTP/1.1 200 OK")
350 assert(ok, "a non-matching content-type must still succeed (just decline the sink swap)")
351 assert(sink == nil, "a non-matching content-type must not offer a sink swap")
352 -- mirror what http.lua does with the factory's return values, to prove
353 -- the caller's existing sink survives untouched
354 if sink then reqt.sink = sink end
355 assert(reqt.sink == "original sink placeholder", "existing sink must be left untouched")
356 assert(not calledback, "message callback must not fire for a non-matching content-type")
357 print("ok")
358end
359
360--------------------------------
361io.write("testing responseheaders declines cleanly when Content-Type is missing: ")
362do
363 local factory = sse.responseheaders(function() end)
364 local ok, sink = factory(200, {}, "HTTP/1.1 200 OK")
365 assert(ok, "missing content-type must still succeed (just decline the sink swap)")
366 assert(sink == nil, "missing content-type must not offer a sink swap")
367 print("ok")
368end
369
370print("the library passed all tests")