diff options
Diffstat (limited to 'test/ssehttptest.lua')
| -rw-r--r-- | test/ssehttptest.lua | 151 |
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. | ||
| 7 | local socket = require("socket") | ||
| 8 | local http = require("socket.http") | ||
| 9 | local ltn12 = require("ltn12") | ||
| 10 | local sse = require("socket.sse") | ||
| 11 | local fixture = require("httpfixture") | ||
| 12 | |||
| 13 | dofile("testsupport.lua") | ||
| 14 | |||
| 15 | local url = "http://" .. fixture.host .. ":" .. fixture.port .. "/" | ||
| 16 | |||
| 17 | local control, remote = fixture.connect() | ||
| 18 | |||
| 19 | io.write("testing a live SSE stream dispatches Messages incrementally, not batched until close: ") | ||
| 20 | do | ||
| 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") | ||
| 72 | end | ||
| 73 | print("ok") | ||
| 74 | |||
| 75 | io.write("testing headers_callback is not invoked for a redirected (3xx) response: ") | ||
| 76 | do | ||
| 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])) | ||
| 106 | end | ||
| 107 | print("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. | ||
| 113 | local 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") | ||
| 128 | end | ||
| 129 | |||
| 130 | io.write("testing the headers_callback sink-swap offer is inert when a 204 response skips the body: ") | ||
| 131 | assertsinkswapskipped(nil, fixture.response("204 No Content", { "Connection: close" }, ""), 204) | ||
| 132 | print("ok") | ||
| 133 | |||
| 134 | io.write("testing the headers_callback sink-swap offer is inert when a 304 response skips the body: ") | ||
| 135 | assertsinkswapskipped(nil, fixture.response("304 Not Modified", { "Connection: close" }, ""), 304) | ||
| 136 | print("ok") | ||
| 137 | |||
| 138 | io.write("testing the headers_callback sink-swap offer is inert when a HEAD request skips the body: ") | ||
| 139 | do | ||
| 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) | ||
| 146 | end | ||
| 147 | print("ok") | ||
| 148 | |||
| 149 | remote("os.exit()") | ||
| 150 | |||
| 151 | print("the library passed all tests") | ||
