diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 12:42:08 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 12:42:08 -0300 |
| commit | 7fd555fcaf4304cb0dff3e8f5661565c3618299e (patch) | |
| tree | 61199793c3046d73b97e9822895ede2a33034d41 /test | |
| download | lua-71232179245c1f52db6822ef5a77cc913ba6a2a4.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'test')
| -rw-r--r-- | test/harness.lua | 205 | ||||
| -rw-r--r-- | test/run.lua | 506 | ||||
| -rw-r--r-- | test/server.lua | 182 |
3 files changed, 893 insertions, 0 deletions
diff --git a/test/harness.lua b/test/harness.lua new file mode 100644 index 0000000..b6b3a65 --- /dev/null +++ b/test/harness.lua @@ -0,0 +1,205 @@ +--- Assertions, the test registry and control of the fake server. +-- +-- No rock beyond the ones the library already needs: registering a test is +-- `test(name, fn)`, a failure is an `error()` out of an assertion, and the +-- runner exits non-zero if any of them fires. + +local http = require("socket.http") +local ltn12 = require("ltn12") +local socket = require("socket") +local json = require("dkjson") + +local M = {} + +M.tests = {} + +--- Register a test. Order of registration is order of execution. +function M.test(name, fn) + M.tests[#M.tests + 1] = { name = name, fn = fn } +end + +local function fail(message, level) + error(message, (level or 2) + 1) +end + +local function show(value) + if type(value) == "string" then + return string.format("%q", value) + end + return tostring(value) +end + +function M.ok(value, message) + if not value then + fail(message or ("expected a truthy value, got " .. show(value))) + end +end + +function M.falsy(value, message) + if value then + fail(message or ("expected a falsy value, got " .. show(value))) + end +end + +function M.equal(got, want, message) + if got ~= want then + fail( + (message and (message .. ": ") or "") + .. "expected " + .. show(want) + .. ", got " + .. show(got) + ) + end +end + +function M.near(got, want, message) + if type(got) ~= "number" or math.abs(got - want) > 1e-6 then + fail((message and (message .. ": ") or "") .. "expected ~" .. tostring(want) .. ", got " .. show(got)) + end +end + +function M.is_nil(value, message) + if value ~= nil then + fail(message or ("expected nil, got " .. show(value))) + end +end + +function M.is_false(value, message) + if value ~= false then + fail(message or ("expected false, got " .. show(value))) + end +end + +--- Assert a call failed with a given error kind, and return the error. +function M.error_kind(value, err, kind) + if value ~= nil then + fail("expected a failure, got " .. show(value)) + end + if type(err) ~= "table" or err.kind == nil then + fail("expected a cepis error, got " .. show(err)) + end + if err.kind ~= kind then + fail("expected kind " .. show(kind) .. ", got " .. show(err.kind) .. " (" .. tostring(err.message) .. ")") + end + return err +end + +--- Assert a call raised through `error()`, for programmer mistakes. +function M.raises(fn, ...) + local ok = pcall(fn, ...) + if ok then + fail("expected error() to be raised") + end +end + +local function request(url, body) + local chunks = {} + local headers = nil + local source = nil + if body then + source = ltn12.source.string(body) + headers = { + ["content-length"] = tostring(#body), + ["content-type"] = "application/json", + } + end + local ok, code = http.request({ + url = url, + method = body and "POST" or "GET", + source = source, + headers = headers, + sink = ltn12.sink.table(chunks), + }) + if not ok then + error("control request to " .. url .. " failed: " .. tostring(code)) + end + return table.concat(chunks) +end + +local Server = {} +Server.__index = Server + +--- Replace the routes and clear the call log. +function Server:program(routes) + request(self.url .. "/__program", json.encode(routes)) +end + +--- Every request target the server has seen since the last `program`. +function Server:calls() + return json.decode(request(self.url .. "/__calls")) or {} +end + +--- How many of them began with `prefix`. +function Server:count(prefix) + local total = 0 + for _, target in ipairs(self:calls()) do + if target:sub(1, #prefix) == prefix then + total = total + 1 + end + end + return total +end + +function Server:stop() + pcall(request, self.url .. "/__stop") +end + +--- Spawn the fake server and wait for it to report its port. +function M.start_server() + local directory = (arg[0] or "test/run.lua"):match("^(.*)[/\\]") or "." + local portfile = os.tmpname() + os.remove(portfile) + + local interpreter = arg[-1] or "lua" + os.execute( + string.format( + "%s %s/server.lua %s >/dev/null 2>&1 &", + interpreter, + directory, + portfile + ) + ) + + local port + local deadline = socket.gettime() + 10 + while socket.gettime() < deadline do + local handle = io.open(portfile, "r") + if handle then + local content = handle:read("*a") + handle:close() + port = tonumber(content) + if port then + break + end + end + socket.sleep(0.02) + end + + os.remove(portfile) + if not port then + error("the fake server did not start") + end + + return setmetatable({ url = "http://127.0.0.1:" .. port, port = port }, Server) +end + +--- Run every registered test, printing one line each. +function M.run() + local failures = 0 + for _, entry in ipairs(M.tests) do + local ok, err = pcall(entry.fn) + if ok then + print("ok - " .. entry.name) + else + failures = failures + 1 + print("FAIL - " .. entry.name) + print(" " .. tostring(err)) + end + end + + print(string.format("\n%d test(s), %d failure(s)", #M.tests, failures)) + return failures +end + +return M diff --git a/test/run.lua b/test/run.lua new file mode 100644 index 0000000..6220615 --- /dev/null +++ b/test/run.lua @@ -0,0 +1,506 @@ +--- Test suite. +-- +-- Every test drives a real HTTP server over a real socket, pointed at through +-- `base_url`. No test touches the network and none stubs LuaSocket: the code +-- path exercised is the one that runs in production, request construction and +-- error body reading included. +-- +-- It doubles as the test that `base_url` works — if it did not, none of this +-- would run. +-- +-- The fixture field names stay Portuguese because they mirror the API's JSON. + +package.path = "./?.lua;./?/init.lua;" .. package.path + +local json = require("dkjson") +local harness = require("test.harness") +local cepis = require("cepis") +local csv = require("cepis.csv") + +local test = harness.test +local equal = harness.equal +local ok = harness.ok +local near = harness.near + +local SE_ADDRESS = { + cep = "01001000", + logradouro = "Praça da Sé", + complemento = "lado ímpar", + unidade = "", + bairro = "Sé", + localidade = "São Paulo", + uf = "SP", + estado = "São Paulo", + regiao = "Sudeste", + ibge = "3550308", + gia = "1004", + ddd = "11", + siafi = "7107", +} + +local CSV_HEADER = + "cep,logradouro,complemento,unidade,bairro,localidade,uf,estado,regiao,ibge,gia,ddd,siafi" + +local function address_json(extra) + local body = {} + for key, value in pairs(SE_ADDRESS) do + body[key] = value + end + for key, value in pairs(extra or {}) do + body[key] = value + end + return json.encode(body) +end + +local function csv_body(rows) + return CSV_HEADER .. "\n" .. table.concat(rows, "\n") .. "\n" +end + +local function once(status, body, headers) + return { responses = { { status = status, body = body, headers = headers } } } +end + +local server = harness.start_server() + +local function client(opts) + opts = opts or {} + opts.base_url = opts.base_url or server.url + if opts.timeout == nil then + opts.timeout = 5 + end + if opts.backoff_base == nil then + opts.backoff_base = 0.001 + end + if opts.backoff_max == nil then + opts.backoff_max = 0.01 + end + return cepis.new(opts) +end + +test("normalize_cep accepts the mask and stray separators", function() + for _, value in ipairs({ "01001000", "01001-000", " 01001-000 ", "01001.000" }) do + equal(cepis.normalize_cep(value), "01001000", value) + end +end) + +test("normalize_cep rejects anything that is not 8 digits", function() + for _, value in ipairs({ "", "123", "0100100a", "010010000", "abcdefgh" }) do + local got, err = cepis.normalize_cep(value) + harness.error_kind(got, err, cepis.INVALID_CEP) + end +end) + +test("normalize_cep rejects a non-string", function() + local got, err = cepis.normalize_cep(1001000) + harness.error_kind(got, err, cepis.INVALID_CEP) +end) + +test("a successful lookup reads the address", function() + server:program({ ["/01001000"] = once(200, address_json()) }) + local address, err = client():lookup("01001-000") + ok(address, tostring(err)) + equal(address.localidade, "São Paulo") + equal(address.uf, "SP") + equal(address.logradouro, "Praça da Sé") + equal(address.cep, "01001000") + equal(address:formatted_cep(), "01001-000") + harness.is_nil(address.geo) +end) + +test("a coordinate is read when present", function() + server:program({ + ["/01310100"] = once( + 200, + address_json({ + geo = { lat = -23.5, lon = -46.6, raio_m = 120, precisao = "logradouro" }, + }) + ), + }) + local address, err = client():lookup("01310100") + ok(address, tostring(err)) + ok(address.geo) + near(address.geo.lat, -23.5) + near(address.geo.lon, -46.6) + equal(address.geo.raio_m, 120) + equal(address.geo:is_precise(), true) +end) + +test("a municipality coordinate is not precise", function() + server:program({ + ["/01001000"] = once( + 200, + address_json({ + geo = { lat = -23.5, lon = -46.6, raio_m = 22000, precisao = "localidade" }, + }) + ), + }) + local address = client():lookup("01001000") + ok(address.geo) + equal(address.geo:is_precise(), false) + equal(address.geo.raio_m, 22000) +end) + +test("a malformed geo block does not lose the address", function() + server:program({ + ["/01001000"] = once(200, address_json({ geo = { lat = "not a number" } })), + }) + local address = client():lookup("01001000") + harness.is_nil(address.geo) + equal(address.localidade, "São Paulo") +end) + +test("a field the API grows is kept in raw", function() + server:program({ + ["/01001000"] = once(200, address_json({ campo_do_futuro = "value" })), + }) + local address = client():lookup("01001000") + equal(address.raw.campo_do_futuro, "value") + equal(address.localidade, "São Paulo") +end) + +test("a missing CEP is not_found", function() + server:program({ + ["/99999999"] = once(404, '{"erro":"cep_nao_encontrado","mensagem":"gone"}'), + }) + local address, err = client():lookup("99999999") + local failure = harness.error_kind(address, err, cepis.NOT_FOUND) + equal(failure.cep, "99999999") + equal(failure:is_retriable(), false) + ok(cepis.is_error(failure)) +end) + +test("404 is not retried", function() + server:program({ ["/99999999"] = once(404, '{"erro":"cep_nao_encontrado"}') }) + local address, err = client():lookup("99999999") + harness.error_kind(address, err, cepis.NOT_FOUND) + equal(server:count("/99999999"), 1) +end) + +test("an invalid CEP never leaves the machine", function() + server:program({}) + local address, err = client():lookup("123") + harness.error_kind(address, err, cepis.INVALID_CEP) + equal(#server:calls(), 0) +end) + +test("a 400 from the API becomes invalid_cep with its message", function() + server:program({ + ["/00000000"] = once(400, '{"erro":"cep_invalido","mensagem":"rejected by the API"}'), + }) + local address, err = client():lookup("00000000") + local failure = harness.error_kind(address, err, cepis.INVALID_CEP) + equal(failure.message, "rejected by the API") + equal(server:count("/00000000"), 1) +end) + +test("a body that is not JSON is invalid_response", function() + server:program({ ["/01001000"] = once(200, "<html>captive portal</html>") }) + local address, err = client():lookup("01001000") + local failure = harness.error_kind(address, err, cepis.INVALID_RESPONSE) + equal(failure:is_retriable(), false) +end) + +test("a JSON array is invalid_response, not a blank address", function() + server:program({ ["/01001000"] = once(200, "[1,2,3]") }) + local address, err = client():lookup("01001000") + harness.error_kind(address, err, cepis.INVALID_RESPONSE) +end) + +test("a 500 is retried and the next attempt succeeds", function() + server:program({ + ["/01001000"] = { + responses = { + { status = 500, body = '{"erro":"interno"}' }, + { status = 200, body = address_json() }, + }, + }, + }) + local address, err = client():lookup("01001000") + ok(address, tostring(err)) + equal(address.localidade, "São Paulo") + equal(server:count("/01001000"), 2) +end) + +test("attempts are exhausted and then it fails", function() + server:program({ ["/01001000"] = once(503, '{"erro":"interno"}') }) + local address, err = client():lookup("01001000") + local failure = harness.error_kind(address, err, cepis.SERVER_ERROR) + equal(failure.status, 503) + equal(failure:is_retriable(), true) + equal(server:count("/01001000"), 3) +end) + +test("max_attempts = 1 disables retrying", function() + server:program({ ["/01001000"] = once(500, "{}") }) + local address, err = client({ max_attempts = 1 }):lookup("01001000") + harness.error_kind(address, err, cepis.SERVER_ERROR) + equal(server:count("/01001000"), 1) +end) + +test("429 becomes rate_limited and exposes Retry-After", function() + server:program({ ["/01001000"] = once(429, "{}", { ["Retry-After"] = "7" }) }) + local address, err = client():lookup("01001000") + local failure = harness.error_kind(address, err, cepis.RATE_LIMITED) + near(failure.retry_after, 7) + equal(failure:is_retriable(), true) +end) + +test("an absurd Retry-After is capped by backoff_max", function() + server:program({ ["/01001000"] = once(429, "{}", { ["Retry-After"] = "3600" }) }) + local started = os.time() + local address, err = client():lookup("01001000") + harness.error_kind(address, err, cepis.RATE_LIMITED) + ok(os.difftime(os.time(), started) < 5, "the call parked for too long") +end) + +test("Retry-After is read in its HTTP-date form too", function() + local future = os.date("!%a, %d %b %Y %H:%M:%S GMT", os.time() + 120) + local seconds = require("cepis.client").retry_after_seconds(future) + ok(seconds, "an HTTP-date Retry-After was not parsed") + ok(seconds > 60 and seconds < 180, "expected roughly 120s, got " .. tostring(seconds)) + harness.is_nil(require("cepis.client").retry_after_seconds("not a date")) +end) + +test("nothing listening is a network_error", function() + local offline = cepis.new({ + base_url = "http://127.0.0.1:1", + max_attempts = 2, + backoff_base = 0.001, + backoff_max = 0.01, + timeout = 1, + }) + local address, err = offline:lookup("01001000") + local failure = harness.error_kind(address, err, cepis.NETWORK_ERROR) + equal(failure:is_retriable(), true) +end) + +test("total_timeout bounds the whole call", function() + server:program({ ["/01001000"] = once(500, "{}") }) + local bounded = client({ + max_attempts = 50, + backoff_base = 0.05, + backoff_max = 0.05, + total_timeout = 0.3, + }) + local started = os.time() + local address, err = bounded:lookup("01001000") + harness.error_kind(address, err, cepis.SERVER_ERROR) + ok(os.difftime(os.time(), started) < 5, "total_timeout did not cut the call short") + ok(server:count("/01001000") < 50, "every attempt ran despite total_timeout") +end) + +test("a batch keeps order and marks the missing entry", function() + server:program({ + ["/csv"] = once( + 200, + csv_body({ + "01001000,Praça da Sé,lado ímpar,,Sé,São Paulo,SP,São Paulo,Sudeste,3550308,1004,11,7107", + "99999999,,,,,,,,,,,,", + "20040020,Avenida Rio Branco,,,Centro,Rio de Janeiro,RJ,Rio de Janeiro,Sudeste,3304557,,21,6001", + }) + ), + }) + local result, err = client():lookup_many({ "01001000", "99999999", "20040020" }) + ok(result, tostring(err)) + equal(#result, 3) + ok(result[1]) + equal(result[1].localidade, "São Paulo") + harness.is_false(result[2]) + ok(result[3]) + equal(result[3].uf, "RJ") +end) + +test("a missing CEP is false, not a hole that truncates the list", function() + server:program({ + ["/csv"] = once( + 200, + csv_body({ + "99999999,,,,,,,,,,,,", + "01001000,Praça da Sé,,,Sé,São Paulo,SP,São Paulo,Sudeste,3550308,1004,11,7107", + "20040020,Avenida Rio Branco,,,Centro,Rio de Janeiro,RJ,Rio de Janeiro,Sudeste,3304557,,21,6001", + }) + ), + }) + local result = client():lookup_many({ "99999999", "01001000", "20040020" }) + + equal(#result, 3, "a nil at index 1 would have truncated the sequence") + + local seen = 0 + for _ in ipairs(result) do + seen = seen + 1 + end + equal(seen, 3, "ipairs stopped early, so the list had a hole") + + harness.is_false(result[1]) + ok(result[2]) + equal(result[2].localidade, "São Paulo") + ok(result[3]) +end) + +test("a batch carries no coordinate", function() + server:program({ + ["/csv"] = once( + 200, + csv_body({ + "01001000,Praça da Sé,,,Sé,São Paulo,SP,São Paulo,Sudeste,3550308,1004,11,7107", + }) + ), + }) + local result = client():lookup_many({ "01001000" }) + ok(result[1]) + harness.is_nil(result[1].geo) +end) + +test("a quoted field containing a comma is parsed", function() + server:program({ + ["/csv"] = once( + 200, + csv_body({ '01001000,"Rua Um, Dois",,,Centro,São Paulo,SP,São Paulo,Sudeste,1,,11,1' }) + ), + }) + local result = client():lookup_many({ "01001000" }) + ok(result[1]) + equal(result[1].logradouro, "Rua Um, Dois") +end) + +test("an empty batch makes no request", function() + server:program({}) + local result = client():lookup_many({}) + equal(#result, 0) + equal(#server:calls(), 0) +end) + +test("a batch above 500 is chunked", function() + server:program({ ["/csv"] = { csv_from_query = true, status = 200 } }) + + local ceps = {} + for index = 0, 500 do + ceps[#ceps + 1] = string.format("%08d", index) + end + + local result, err = client():lookup_many(ceps) + ok(result, tostring(err)) + equal(#result, 501) + equal(server:count("/csv"), 2) + ok(result[1]) + ok(result[501]) +end) + +test("a truncated batch response is padded, never shifted", function() + server:program({ + ["/csv"] = once( + 200, + csv_body({ "01001000,,,,,São Paulo,SP,São Paulo,Sudeste,1,,11,1" }) + ), + }) + local result = client():lookup_many({ "01001000", "20040020" }) + equal(#result, 2) + ok(result[1]) + harness.is_false(result[2]) +end) + +test("a batch response longer than the request is truncated", function() + server:program({ + ["/csv"] = once( + 200, + csv_body({ + "01001000,,,,,São Paulo,SP,São Paulo,Sudeste,1,,11,1", + "20040020,,,,,Rio de Janeiro,RJ,Rio de Janeiro,Sudeste,1,,21,1", + }) + ), + }) + local result = client():lookup_many({ "01001000" }) + equal(#result, 1) +end) + +test("health reads the live database identifier", function() + server:program({ + ["/health"] = once( + 200, + json.encode({ + status = "ok", + dne = "26071", + ceps = 1596168, + geo = 894489, + base_gerada_em = "2026-07-24T03:48:28Z", + }) + ), + }) + local health, err = client():health() + ok(health, tostring(err)) + equal(health:ok(), true) + equal(health.dne, "26071") + equal(health.ceps, 1596168) + equal(health.geo, 894489) + equal(health.base_gerada_em, "2026-07-24T03:48:28Z") +end) + +test("a degraded API is a diagnosis, not a failure", function() + server:program({ + ["/health"] = once( + 503, + json.encode({ status = "degradado", dne = "", ceps = 0, geo = 0 }) + ), + }) + local health, err = client():health() + ok(health, tostring(err)) + equal(health:ok(), false) + equal(health.status, "degradado") + equal(server:count("/health"), 1) +end) + +test("a trailing slash on base_url is not duplicated", function() + equal(cepis.new({ base_url = "http://x/" }).base_url, "http://x") + equal(cepis.new({ base_url = "http://x///" }).base_url, "http://x") +end) + +test("a bad option is a programmer mistake and raises", function() + harness.raises(cepis.new, { max_attempts = 0 }) + harness.raises(cepis.new, { timeout = 0 }) + harness.raises(cepis.new, { timeout = -1 }) + harness.raises(cepis.new, { base_url = "" }) + harness.raises(cepis.new, { total_timeout = 0 }) + harness.raises(function() + return cepis.new():lookup_many("not a table") + end) +end) + +test("the default User-Agent identifies the library", function() + local agent = cepis.new().user_agent + ok(agent:find("cepis-lua/", 1, true), agent) + ok(agent:find(cepis._VERSION, 1, true), agent) + equal(cepis.new({ user_agent = "myapp/1.0" }).user_agent, "myapp/1.0") +end) + +test("the defaults are the documented ones", function() + local c = cepis.new() + equal(c.base_url, "https://cep.is") + equal(c.max_attempts, 3) + equal(c.backoff_base, 0.5) + equal(c.backoff_max, 8.0) + harness.is_nil(c.total_timeout) + equal(cepis.MAX_BATCH, 500) + equal(cepis._VERSION, "0.1.0") +end) + +test("the CSV reader handles quotes, escapes and CRLF", function() + local rows = csv.parse('a,b\r\n"x,1","he said ""hi""" \r\n,,\n') + equal(#rows, 3) + equal(rows[1][1], "a") + equal(rows[1][2], "b") + equal(rows[2][1], "x,1") + equal(rows[2][2], 'he said "hi" ') + equal(#rows[3], 3) + equal(rows[3][1], "") +end) + +test("the CSV reader ignores a UTF-8 BOM and blank lines", function() + local rows = csv.parse("\239\187\191a,b\n\nc,d\n") + equal(#rows, 2) + equal(rows[1][1], "a") + equal(rows[2][1], "c") +end) + +local failures = harness.run() +server:stop() +os.exit(failures == 0 and 0 or 1) 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() |