summaryrefslogtreecommitdiffstats
path: root/test/server.lua
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:42:08 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:42:08 -0300
commit7fd555fcaf4304cb0dff3e8f5661565c3618299e (patch)
tree61199793c3046d73b97e9822895ede2a33034d41 /test/server.lua
downloadlua-7fd555fcaf4304cb0dff3e8f5661565c3618299e.tar.gz
Initial importv0.1.0
Diffstat (limited to 'test/server.lua')
-rw-r--r--test/server.lua182
1 files changed, 182 insertions, 0 deletions
diff --git a/test/server.lua b/test/server.lua
new file mode 100644
index 0000000..a51b8bf
--- /dev/null
+++ b/test/server.lua
@@ -0,0 +1,182 @@
+--- Programmable HTTP server used by the suite, run as a separate process.
+--
+-- Lua has no threads, and the client blocks on its socket, so the server
+-- cannot share this interpreter with the code under test. It is spawned once
+-- per run, binds a free loopback port and writes it to the file named in
+-- `arg[1]`, then answers until it is told to stop or nothing has connected for
+-- a while.
+--
+-- Three control routes drive it from the test process:
+--
+-- POST /__program replace the routes and clear the call log
+-- GET /__calls the request targets seen so far, as a JSON array
+-- GET /__stop shut down
+--
+-- A route maps a path (query string excluded) to `{ responses = { ... } }`.
+-- Each request consumes the next response; the last one repeats once the list
+-- runs out, which is how retrying is tested. A route may instead set
+-- `csv_from_query`, and the server then generates one CSV row per CEP in the
+-- query — that keeps the 501-CEP chunking test from shipping a body through
+-- the control channel.
+
+local socket = require("socket")
+local json = require("dkjson")
+
+local IDLE_TIMEOUT = 60
+
+local REASON = {
+ [200] = "OK",
+ [400] = "Bad Request",
+ [404] = "Not Found",
+ [429] = "Too Many Requests",
+ [500] = "Internal Server Error",
+ [502] = "Bad Gateway",
+ [503] = "Service Unavailable",
+}
+
+local CSV_HEADER =
+ "cep,logradouro,complemento,unidade,bairro,localidade,uf,estado,regiao,ibge,gia,ddd,siafi"
+
+local routes = {}
+local cursor = {}
+local calls = {}
+local running = true
+
+local function unescape(value)
+ return (value:gsub("%%(%x%x)", function(hex)
+ return string.char(tonumber(hex, 16))
+ end))
+end
+
+local function read_request(client)
+ local line = client:receive("*l")
+ if not line then
+ return nil
+ end
+
+ local method, target = line:match("^(%u+)%s+(%S+)")
+ if not method then
+ return nil
+ end
+
+ local headers = {}
+ while true do
+ local header = client:receive("*l")
+ if not header or header == "" then
+ break
+ end
+ local name, value = header:match("^([^:]+):%s*(.*)$")
+ if name then
+ headers[name:lower()] = value
+ end
+ end
+
+ local body = ""
+ local length = tonumber(headers["content-length"] or "0") or 0
+ if length > 0 then
+ body = client:receive(length) or ""
+ end
+
+ return method, target, body
+end
+
+local function send(client, status, body, headers)
+ local out = {
+ string.format("HTTP/1.1 %d %s\r\n", status, REASON[status] or "Status"),
+ }
+ for name, value in pairs(headers or {}) do
+ out[#out + 1] = name .. ": " .. tostring(value) .. "\r\n"
+ end
+ out[#out + 1] = "Content-Length: " .. #body .. "\r\n"
+ out[#out + 1] = "Connection: close\r\n\r\n"
+ out[#out + 1] = body
+ client:send(table.concat(out))
+end
+
+local function csv_for(target)
+ local raw = target:match("[?&]ceps=([^&]*)") or ""
+ local rows = { CSV_HEADER }
+ for cep in unescape(raw):gmatch("[^,]+") do
+ rows[#rows + 1] = cep .. ",,,,,X,SP,São Paulo,Sudeste,1,,11,1"
+ end
+ return table.concat(rows, "\n") .. "\n"
+end
+
+local function next_response(path)
+ local route = routes[path]
+ if not route then
+ return nil
+ end
+
+ local responses = route.responses
+ if not responses or #responses == 0 then
+ return route
+ end
+
+ local index = cursor[path] or 1
+ if index < #responses then
+ cursor[path] = index + 1
+ end
+ return responses[index]
+end
+
+local function serve(client, target, body)
+ local path = target:match("^([^?]*)")
+
+ if path == "/__program" then
+ routes = json.decode(body) or {}
+ cursor = {}
+ calls = {}
+ send(client, 200, "{}")
+ return
+ end
+
+ if path == "/__calls" then
+ send(client, 200, json.encode(calls))
+ return
+ end
+
+ if path == "/__stop" then
+ running = false
+ send(client, 200, "{}")
+ return
+ end
+
+ calls[#calls + 1] = target
+
+ local response = next_response(path)
+ if not response then
+ send(client, 404, '{"erro":"cep_nao_encontrado","mensagem":"CEP nao existe"}')
+ return
+ end
+
+ if response.csv_from_query then
+ send(client, response.status or 200, csv_for(target), response.headers)
+ return
+ end
+
+ send(client, response.status or 200, response.body or "", response.headers)
+end
+
+local server = assert(socket.bind("127.0.0.1", 0))
+local _, port = server:getsockname()
+server:settimeout(IDLE_TIMEOUT)
+
+local handle = assert(io.open(arg[1], "w"))
+handle:write(tostring(port))
+handle:close()
+
+while running do
+ local client = server:accept()
+ if not client then
+ break
+ end
+ client:settimeout(10)
+ local _, target, body = read_request(client)
+ if target then
+ serve(client, target, body)
+ end
+ client:close()
+end
+
+server:close()