diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 12:19:34 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 12:19:34 -0300 |
| commit | 075683ff44a254e99adbad8a0372ef80ff06151a (patch) | |
| tree | 648289c8485afc6f003a3f719fb937c21f149311 /test/cepis | |
| download | elixir-075683ff44a254e99adbad8a0372ef80ff06151a.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'test/cepis')
| -rw-r--r-- | test/cepis/batch_test.exs | 98 | ||||
| -rw-r--r-- | test/cepis/csv_test.exs | 49 | ||||
| -rw-r--r-- | test/cepis/health_test.exs | 45 | ||||
| -rw-r--r-- | test/cepis/lookup_test.exs | 152 | ||||
| -rw-r--r-- | test/cepis/retry_test.exs | 90 |
5 files changed, 434 insertions, 0 deletions
diff --git a/test/cepis/batch_test.exs b/test/cepis/batch_test.exs new file mode 100644 index 0000000..dd8fe83 --- /dev/null +++ b/test/cepis/batch_test.exs @@ -0,0 +1,98 @@ +defmodule Cepis.BatchTest do + use Cepis.ServerCase + + test "order is preserved and a missing CEP becomes nil", %{server: server, client: client} do + body = + csv([ + "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" + ]) + + FakeServer.on_request(server, fn _path, _n -> {200, body, []} end) + + assert {:ok, [first, nil, third]} = + Cepis.lookup_many(["01001000", "99999999", "20040020"], client) + + assert first.localidade == "São Paulo" + assert third.uf == "RJ" + + assert [path] = FakeServer.calls(server) + assert path == "/csv?ceps=01001000%2C99999999%2C20040020" + end + + test "batches never carry a coordinate", %{server: server, client: client} do + body = csv(["01001000,Praça da Sé,,,Sé,São Paulo,SP,São Paulo,Sudeste,3550308,1004,11,7107"]) + FakeServer.on_request(server, fn _path, _n -> {200, body, []} end) + + assert {:ok, [address]} = Cepis.lookup_many(["01001000"], client) + assert address.geo == nil + end + + test "a quoted field containing a comma is parsed, not split", %{server: server, client: client} do + body = csv([~s(01001000,"Rua Um, Dois",,,Centro,São Paulo,SP,São Paulo,Sudeste,1,,11,1)]) + FakeServer.on_request(server, fn _path, _n -> {200, body, []} end) + + assert {:ok, [address]} = Cepis.lookup_many(["01001000"], client) + assert address.logradouro == "Rua Um, Dois" + end + + test "an escaped quote inside a quoted field is unescaped", %{server: server, client: client} do + body = csv([~s(01001000,"Rua ""A""",,,Centro,São Paulo,SP,São Paulo,Sudeste,1,,11,1)]) + FakeServer.on_request(server, fn _path, _n -> {200, body, []} end) + + assert {:ok, [address]} = Cepis.lookup_many(["01001000"], client) + assert address.logradouro == ~s(Rua "A") + end + + test "an empty list makes no request", %{server: server, client: client} do + assert Cepis.lookup_many([], client) == {:ok, []} + assert FakeServer.calls(server) == [] + end + + test "more than 500 CEPs become more than one request", %{server: server, client: client} do + ceps = Enum.map(0..500, &String.pad_leading(to_string(&1), 8, "0")) + {first, second} = Enum.split(ceps, 500) + + rows = fn chunk -> + csv(Enum.map(chunk, &"#{&1},,,,,X,SP,São Paulo,Sudeste,1,,11,1")) + end + + bodies = %{1 => rows.(first), 2 => rows.(second)} + FakeServer.on_request(server, fn _path, n -> {200, Map.fetch!(bodies, n), []} end) + + assert {:ok, result} = Cepis.lookup_many(ceps, client) + assert length(result) == 501 + assert Enum.all?(result, &(&1.localidade == "X")) + assert length(FakeServer.calls(server)) == 2 + end + + test "a truncated response pads rather than shifting", %{server: server, client: client} do + body = csv(["01001000,,,,,São Paulo,SP,São Paulo,Sudeste,1,,11,1"]) + FakeServer.on_request(server, fn _path, _n -> {200, body, []} end) + + assert {:ok, [first, nil]} = Cepis.lookup_many(["01001000", "20040020"], client) + assert first.localidade == "São Paulo" + end + + test "an empty body yields a full list of nils", %{server: server, client: client} do + FakeServer.on_request(server, fn _path, _n -> {200, "", []} end) + + assert {:ok, [nil, nil]} = Cepis.lookup_many(["01001000", "20040020"], client) + end + + test "a transport failure fails the whole batch", %{server: server, client: client} do + FakeServer.on_request(server, fn _path, _n -> {503, "", []} end) + + assert {:error, %Cepis.ServerError{status: 503}} = Cepis.lookup_many(["01001000"], client) + end + + test "the bang variant returns the list", %{server: server, client: client} do + body = csv(["01001000,,,,,São Paulo,SP,São Paulo,Sudeste,1,,11,1"]) + FakeServer.on_request(server, fn _path, _n -> {200, body, []} end) + + assert [%Cepis.Address{}] = Cepis.lookup_many!(["01001000"], client) + end +end diff --git a/test/cepis/csv_test.exs b/test/cepis/csv_test.exs new file mode 100644 index 0000000..719de66 --- /dev/null +++ b/test/cepis/csv_test.exs @@ -0,0 +1,49 @@ +defmodule Cepis.CSVTest do + use ExUnit.Case, async: true + + alias Cepis.CSV + + test "parses plain rows" do + assert CSV.parse("a,b\n1,2\n") == [["a", "b"], ["1", "2"]] + end + + test "tolerates a missing final newline" do + assert CSV.parse("a,b\n1,2") == [["a", "b"], ["1", "2"]] + end + + test "tolerates CRLF" do + assert CSV.parse("a,b\r\n1,2\r\n") == [["a", "b"], ["1", "2"]] + end + + test "keeps empty fields" do + assert CSV.parse("a,b,c\n1,,3\n") == [["a", "b", "c"], ["1", "", "3"]] + end + + test "unquotes fields containing commas, quotes and newlines" do + text = ~s(a,b\n"x, y","he said ""hi"""\n) + assert CSV.parse(text) == [["a", "b"], ["x, y", ~s(he said "hi")]] + + assert CSV.parse(~s(a\n"one\ntwo"\n)) == [["a"], ["one\ntwo"]] + end + + test "strips a byte order mark" do + assert CSV.parse("a,b\n1,2\n") == [["a", "b"], ["1", "2"]] + end + + test "keeps multi-byte characters intact" do + assert CSV.parse("a\nSão Paulo\n") == [["a"], ["São Paulo"]] + end + + test "an empty document has no rows" do + assert CSV.parse("") == [] + assert CSV.parse("\n") == [] + end + + test "pairs values with the header, padding short rows" do + assert CSV.parse_with_header("a,b,c\n1,2\n") == [%{"a" => "1", "b" => "2", "c" => ""}] + end + + test "a header alone yields no rows" do + assert CSV.parse_with_header("a,b,c\n") == [] + end +end diff --git a/test/cepis/health_test.exs b/test/cepis/health_test.exs new file mode 100644 index 0000000..47a49c2 --- /dev/null +++ b/test/cepis/health_test.exs @@ -0,0 +1,45 @@ +defmodule Cepis.HealthTest do + use Cepis.ServerCase + + test "reads the live database identifier", %{server: server, client: client} do + body = + json(%{ + "status" => "ok", + "dne" => "26071", + "ceps" => 1_596_168, + "geo" => 894_489, + "base_gerada_em" => "2026-07-24T03:48:28Z" + }) + + FakeServer.respond(server, "/health", {200, body, []}) + + assert {:ok, health} = Cepis.health(client) + assert Cepis.Health.ok?(health) + assert health.dne == "26071" + assert health.ceps == 1_596_168 + assert health.geo == 894_489 + assert health.base_gerada_em == "2026-07-24T03:48:28Z" + end + + test "a degraded API is a diagnosis, not a failure", %{server: server, client: client} do + body = json(%{"status" => "degradado", "dne" => "", "ceps" => 0, "geo" => 0}) + FakeServer.respond(server, "/health", {503, body, []}) + + assert {:ok, health} = Cepis.health(client) + refute Cepis.Health.ok?(health) + assert health.status == "degradado" + end + + test "a 503 on /health is not retried", %{server: server, client: client} do + FakeServer.respond(server, "/health", {503, json(%{"status" => "degradado"}), []}) + + assert {:ok, _health} = Cepis.health(client) + assert FakeServer.count(server, "/health") == 1 + end + + test "the bang variant returns the struct", %{server: server, client: client} do + FakeServer.respond(server, "/health", {200, json(%{"status" => "ok"}), []}) + + assert %Cepis.Health{status: "ok"} = Cepis.health!(client) + end +end diff --git a/test/cepis/lookup_test.exs b/test/cepis/lookup_test.exs new file mode 100644 index 0000000..b68ecbe --- /dev/null +++ b/test/cepis/lookup_test.exs @@ -0,0 +1,152 @@ +defmodule Cepis.LookupTest do + use Cepis.ServerCase + + test "resolves a CEP", %{server: server, client: client} do + FakeServer.respond(server, "/01001000", {200, json(address_fixture()), []}) + + assert {:ok, address} = Cepis.lookup("01001-000", client) + assert address.logradouro == "Praça da Sé" + assert address.localidade == "São Paulo" + assert address.uf == "SP" + assert Cepis.Address.formatted_cep(address) == "01001-000" + assert address.geo == nil + end + + test "reads the coordinate when the API sends one", %{server: server, client: client} do + body = + address_fixture(%{ + "geo" => %{"lat" => -23.5, "lon" => -46.6, "raio_m" => 120, "precisao" => "logradouro"} + }) + + FakeServer.respond(server, "/01310100", {200, json(body), []}) + + assert {:ok, address} = Cepis.lookup("01310100", client) + assert address.geo.lat == -23.5 + assert address.geo.lon == -46.6 + assert address.geo.raio_m == 120 + assert Cepis.Geo.precise?(address.geo) + end + + test "a municipality centroid is not precise", %{server: server, client: client} do + body = + address_fixture(%{ + "geo" => %{"lat" => -23.5, "lon" => -46.6, "raio_m" => 22_000, "precisao" => "localidade"} + }) + + FakeServer.respond(server, "/01001000", {200, json(body), []}) + + assert {:ok, address} = Cepis.lookup("01001000", client) + refute Cepis.Geo.precise?(address.geo) + end + + test "a malformed geo block does not lose the address", %{server: server, client: client} do + body = + address_fixture(%{ + "geo" => %{ + "lat" => "not a number", + "lon" => "-46.6", + "raio_m" => "120", + "precisao" => "logradouro" + } + }) + + FakeServer.respond(server, "/01001000", {200, json(body), []}) + + assert {:ok, address} = Cepis.lookup("01001000", client) + assert address.geo == nil + assert address.localidade == "São Paulo" + end + + test "a truncated geo block does not lose the address", %{server: server, client: client} do + FakeServer.respond(server, "/01001000", {200, json(address_fixture(%{"geo" => %{}})), []}) + + assert {:ok, address} = Cepis.lookup("01001000", client) + assert address.geo == nil + assert address.uf == "SP" + end + + test "a field the API grows is kept in raw", %{server: server, client: client} do + body = address_fixture(%{"campo_do_futuro" => "value"}) + FakeServer.respond(server, "/01001000", {200, json(body), []}) + + assert {:ok, address} = Cepis.lookup("01001000", client) + assert address.raw["campo_do_futuro"] == "value" + assert address.localidade == "São Paulo" + end + + test "a missing CEP is a not-found error", %{server: server, client: client} do + FakeServer.respond( + server, + "/99999999", + {404, json(%{"erro" => "cep_nao_encontrado", "mensagem" => "gone"}), []} + ) + + assert {:error, %Cepis.NotFoundError{cep: "99999999"} = error} = + Cepis.lookup("99999999", client) + + assert Exception.message(error) =~ "99999999" + end + + test "a 404 is never retried", %{server: server, client: client} do + FakeServer.respond(server, "/99999999", {404, json(%{"erro" => "cep_nao_encontrado"}), []}) + + assert {:error, %Cepis.NotFoundError{}} = Cepis.lookup("99999999", client) + assert FakeServer.count(server, "/99999999") == 1 + end + + test "a malformed CEP never leaves the machine", %{server: server, client: client} do + assert {:error, %Cepis.InvalidCEPError{}} = Cepis.lookup("123", client) + assert FakeServer.calls(server) == [] + end + + test "a 400 carries the API's own message", %{server: server, client: client} do + FakeServer.respond( + server, + "/00000000", + {400, json(%{"erro" => "cep_invalido", "mensagem" => "rejected by the API"}), []} + ) + + assert {:error, %Cepis.InvalidCEPError{} = error} = Cepis.lookup("00000000", client) + assert Exception.message(error) =~ "rejected by the API" + end + + test "a 400 is never retried", %{server: server, client: client} do + FakeServer.respond(server, "/00000000", {400, json(%{"erro" => "cep_invalido"}), []}) + + assert {:error, %Cepis.InvalidCEPError{}} = Cepis.lookup("00000000", client) + assert FakeServer.count(server, "/00000000") == 1 + end + + test "a captive portal answering instead of the API is diagnosed", %{ + server: server, + client: client + } do + FakeServer.respond(server, "/01001000", {200, "<html>captive portal</html>", []}) + + assert {:error, %Cepis.InvalidResponseError{} = error} = Cepis.lookup("01001000", client) + assert Exception.message(error) =~ "captive portal" + end + + test "a JSON body that is not an object is rejected", %{server: server, client: client} do + FakeServer.respond(server, "/01001000", {200, "[1,2,3]", []}) + + assert {:error, %Cepis.InvalidResponseError{}} = Cepis.lookup("01001000", client) + end + + test "the bang variant returns the address and raises on failure", %{ + server: server, + client: client + } do + FakeServer.respond(server, "/01001000", {200, json(address_fixture()), []}) + assert %Cepis.Address{uf: "SP"} = Cepis.lookup!("01001000", client) + + assert_raise Cepis.NotFoundError, fn -> Cepis.lookup!("99999999", client) end + end + + test "keyword options work in place of a client struct", %{server: server} do + FakeServer.respond(server, "/01001000", {200, json(address_fixture()), []}) + + assert {:ok, address} = Cepis.lookup("01001000", base_url: server.url, timeout: 5_000) + assert address.localidade == "São Paulo" + end +end diff --git a/test/cepis/retry_test.exs b/test/cepis/retry_test.exs new file mode 100644 index 0000000..3308ca4 --- /dev/null +++ b/test/cepis/retry_test.exs @@ -0,0 +1,90 @@ +defmodule Cepis.RetryTest do + use Cepis.ServerCase + + test "a 500 is retried and the second attempt wins", %{server: server, client: client} do + FakeServer.respond(server, "/01001000", [ + {500, json(%{"erro" => "interno"}), []}, + {200, json(address_fixture()), []} + ]) + + assert {:ok, address} = Cepis.lookup("01001000", client) + assert address.localidade == "São Paulo" + assert FakeServer.count(server, "/01001000") == 2 + end + + test "attempts are exhausted and then it gives up", %{server: server, client: client} do + FakeServer.respond(server, "/01001000", {503, json(%{"erro" => "interno"}), []}) + + assert {:error, %Cepis.ServerError{status: 503}} = Cepis.lookup("01001000", client) + assert FakeServer.count(server, "/01001000") == 3 + end + + test "max_attempts: 1 disables retrying", %{server: server} do + client = Cepis.Client.new(base_url: server.url, max_attempts: 1, timeout: 5_000) + FakeServer.respond(server, "/01001000", {500, "{}", []}) + + assert {:error, %Cepis.ServerError{status: 500}} = Cepis.lookup("01001000", client) + assert FakeServer.count(server, "/01001000") == 1 + end + + test "a 429 exposes Retry-After in milliseconds", %{server: server, client: client} do + FakeServer.respond(server, "/01001000", {429, "{}", [{"retry-after", "7"}]}) + + assert {:error, %Cepis.RateLimitError{retry_after: 7_000}} = Cepis.lookup("01001000", client) + end + + test "a Retry-After HTTP date is understood", %{server: server, client: client} do + date = + DateTime.utc_now() + |> DateTime.add(30, :second) + |> Calendar.strftime("%a, %d %b %Y %H:%M:%S GMT") + + FakeServer.respond(server, "/01001000", {429, "{}", [{"retry-after", date}]}) + + assert {:error, %Cepis.RateLimitError{retry_after: retry_after}} = + Cepis.lookup("01001000", client) + + assert retry_after > 20_000 + assert retry_after <= 30_000 + end + + test "an absurd Retry-After cannot park the caller", %{server: server, client: client} do + FakeServer.respond(server, "/01001000", {429, "{}", [{"retry-after", "3600"}]}) + + {elapsed, result} = :timer.tc(fn -> Cepis.lookup("01001000", client) end, :millisecond) + + assert {:error, %Cepis.RateLimitError{}} = result + assert elapsed < 2_000 + end + + test "nothing listening is a network error" do + client = + Cepis.Client.new( + base_url: "http://127.0.0.1:1", + max_attempts: 2, + backoff_base: 1, + timeout: 1_000 + ) + + assert {:error, %Cepis.NetworkError{}} = Cepis.lookup("01001000", client) + end + + test "total_timeout bounds the whole call", %{server: server} do + client = + Cepis.Client.new( + base_url: server.url, + max_attempts: 50, + backoff_base: 50, + backoff_max: 50, + total_timeout: 300 + ) + + FakeServer.respond(server, "/01001000", {500, "{}", []}) + + {elapsed, result} = :timer.tc(fn -> Cepis.lookup("01001000", client) end, :millisecond) + + assert {:error, %Cepis.ServerError{}} = result + assert elapsed < 3_000 + assert FakeServer.count(server, "/01001000") < 50 + end +end |