aboutsummaryrefslogtreecommitdiff
path: root/test/ssetest.lua
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/ssetest.lua
parentc5f169ec303a70707bc48fbece9b52cc90a48de0 (diff)
downloadluasocket-cd6b41a13af403d32e08ff416e60982b5cb718dc.tar.gz
luasocket-cd6b41a13af403d32e08ff416e60982b5cb718dc.tar.bz2
luasocket-cd6b41a13af403d32e08ff416e60982b5cb718dc.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 'test/ssetest.lua')
-rw-r--r--test/ssetest.lua370
1 files changed, 370 insertions, 0 deletions
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")