From 075683ff44a254e99adbad8a0372ef80ff06151a Mon Sep 17 00:00:00 2001 From: Murilo Ijanc Date: Sun, 26 Jul 2026 12:19:34 -0300 Subject: Initial import --- test/cepis/batch_test.exs | 98 ++++++++++++++++++++++++++++ test/cepis/csv_test.exs | 49 ++++++++++++++ test/cepis/health_test.exs | 45 +++++++++++++ test/cepis/lookup_test.exs | 152 +++++++++++++++++++++++++++++++++++++++++++ test/cepis/retry_test.exs | 90 ++++++++++++++++++++++++++ test/cepis_test.exs | 79 +++++++++++++++++++++++ test/support/fake_server.ex | 154 ++++++++++++++++++++++++++++++++++++++++++++ test/support/server_case.ex | 52 +++++++++++++++ test/test_helper.exs | 1 + 9 files changed, 720 insertions(+) create mode 100644 test/cepis/batch_test.exs create mode 100644 test/cepis/csv_test.exs create mode 100644 test/cepis/health_test.exs create mode 100644 test/cepis/lookup_test.exs create mode 100644 test/cepis/retry_test.exs create mode 100644 test/cepis_test.exs create mode 100644 test/support/fake_server.ex create mode 100644 test/support/server_case.ex create mode 100644 test/test_helper.exs (limited to 'test') 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, "captive portal", []}) + + 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 diff --git a/test/cepis_test.exs b/test/cepis_test.exs new file mode 100644 index 0000000..a6bedb1 --- /dev/null +++ b/test/cepis_test.exs @@ -0,0 +1,79 @@ +defmodule CepisTest do + use ExUnit.Case, async: true + + doctest Cepis + doctest Cepis.Client + + describe "normalize_cep/1" do + test "accepts the mask and the bare digits" do + for value <- ["01001000", "01001-000", " 01001-000 ", "01001.000"] do + assert Cepis.normalize_cep(value) == {:ok, "01001000"} + end + end + + test "rejects anything that is not 8 digits" do + for value <- ["", "123", "0100100a", "010010000", "abcdefgh"] do + assert {:error, %Cepis.InvalidCEPError{}} = Cepis.normalize_cep(value) + end + end + + test "rejects a value that is not a string" do + assert {:error, %Cepis.InvalidCEPError{}} = Cepis.normalize_cep(1_001_000) + end + + test "the bang variant raises" do + assert_raise Cepis.InvalidCEPError, fn -> Cepis.normalize_cep!("123") end + end + end + + describe "Cepis.Client.new/1" do + test "strips a trailing slash so separators cannot double up" do + assert Cepis.Client.new(base_url: "http://x/").base_url == "http://x" + assert Cepis.Client.new(base_url: "http://x//").base_url == "http://x" + end + + test "rejects an impossible configuration" do + assert_raise ArgumentError, fn -> Cepis.Client.new(max_attempts: 0) end + assert_raise ArgumentError, fn -> Cepis.Client.new(timeout: 0) end + end + + test "rejects an unknown option" do + assert_raise KeyError, fn -> Cepis.Client.new(retries: 3) end + end + + test "the default user agent identifies the library" do + assert Cepis.Client.new([]).user_agent =~ "cepis-elixir/" + end + + test "an already built client passes through untouched" do + client = Cepis.Client.new(base_url: "http://x") + assert Cepis.Client.new(client) == client + end + end + + describe "Cepis.Error" do + test "splits retriable transport failures from definitive answers" do + assert Cepis.Error.retriable?(%Cepis.ServerError{status: 500}) + assert Cepis.Error.retriable?(%Cepis.RateLimitError{}) + assert Cepis.Error.retriable?(%Cepis.NetworkError{}) + refute Cepis.Error.retriable?(%Cepis.NotFoundError{}) + refute Cepis.Error.retriable?(%Cepis.InvalidCEPError{}) + end + + test "recognises its own errors" do + assert Cepis.Error.error?(%Cepis.NotFoundError{}) + refute Cepis.Error.error?(%RuntimeError{}) + refute Cepis.Error.error?(:oops) + end + end + + describe "Cepis.Address.formatted_cep/1" do + test "adds the mask" do + assert Cepis.Address.formatted_cep(%Cepis.Address{cep: "01001000"}) == "01001-000" + end + + test "leaves an unexpected value alone" do + assert Cepis.Address.formatted_cep(%Cepis.Address{cep: ""}) == "" + end + end +end diff --git a/test/support/fake_server.ex b/test/support/fake_server.ex new file mode 100644 index 0000000..bd1fc5a --- /dev/null +++ b/test/support/fake_server.ex @@ -0,0 +1,154 @@ +defmodule Cepis.FakeServer do + @moduledoc false + + defstruct [:pid, :agent, :port, :url] + + @type t :: %__MODULE__{pid: pid(), agent: pid(), port: :inet.port_number(), url: String.t()} + + @listen_options [ + :binary, + ip: {127, 0, 0, 1}, + packet: :http_bin, + packet_size: 1_048_576, + active: false, + reuseaddr: true, + backlog: 128 + ] + + @not_found {404, ~s({"erro":"cep_nao_encontrado","mensagem":"CEP não existe na base do DNE"}), + []} + + def start do + {:ok, agent} = Agent.start_link(fn -> %{routes: %{}, calls: [], handler: nil} end) + parent = self() + + pid = + spawn_link(fn -> + {:ok, listen} = :gen_tcp.listen(0, @listen_options) + {:ok, port} = :inet.port(listen) + send(parent, {:listening, self(), port}) + accept_loop(listen, agent) + end) + + port = + receive do + {:listening, ^pid, port} -> port + after + 5_000 -> raise "fake server did not start" + end + + %__MODULE__{pid: pid, agent: agent, port: port, url: "http://127.0.0.1:#{port}"} + end + + def stop(%__MODULE__{} = server) do + Process.exit(server.pid, :kill) + Process.exit(server.agent, :kill) + :ok + end + + def respond(%__MODULE__{agent: agent}, path, response) do + Agent.update(agent, fn state -> put_in(state.routes[path], response) end) + end + + def on_request(%__MODULE__{agent: agent}, fun) when is_function(fun, 2) do + Agent.update(agent, fn state -> %{state | handler: fun} end) + end + + def calls(%__MODULE__{agent: agent}) do + Agent.get(agent, & &1.calls) + end + + def count(%__MODULE__{} = server, prefix) do + server |> calls() |> Enum.count(&String.starts_with?(&1, prefix)) + end + + defp accept_loop(listen, agent) do + case :gen_tcp.accept(listen) do + {:ok, socket} -> + worker = spawn(fn -> serve(socket, agent) end) + :gen_tcp.controlling_process(socket, worker) + send(worker, :ready) + accept_loop(listen, agent) + + {:error, _reason} -> + :ok + end + end + + defp serve(socket, agent) do + receive do + :ready -> :ok + after + 5_000 -> :ok + end + + with {:ok, path} <- read_request(socket) do + {status, body, headers} = dispatch(agent, path) + :gen_tcp.send(socket, encode(status, body, headers)) + end + + :gen_tcp.close(socket) + end + + defp read_request(socket) do + case :gen_tcp.recv(socket, 0, 5_000) do + {:ok, {:http_request, _method, {:abs_path, path}, _version}} -> + with :ok <- drain_headers(socket), do: {:ok, path} + + other -> + other + end + end + + defp drain_headers(socket) do + case :gen_tcp.recv(socket, 0, 5_000) do + {:ok, :http_eoh} -> :ok + {:ok, {:http_header, _length, _field, _reserved, _value}} -> drain_headers(socket) + other -> other + end + end + + defp dispatch(agent, path) do + Agent.get_and_update(agent, fn state -> + state = %{state | calls: state.calls ++ [path]} + select(state, path) + end) + end + + defp select(%{handler: handler} = state, path) when is_function(handler, 2) do + {handler.(path, length(state.calls)), state} + end + + defp select(state, path) do + case Map.get(state.routes, path) do + nil -> {@not_found, state} + [single] -> {single, state} + [next | rest] -> {next, put_in(state.routes[path], rest)} + response -> {response, state} + end + end + + defp encode(status, body, headers) do + payload = IO.iodata_to_binary(body) + + head = [ + "HTTP/1.1 #{status} #{reason(status)}\r\n", + Enum.map(headers, fn {key, value} -> "#{key}: #{value}\r\n" end), + "content-type: text/plain; charset=utf-8\r\n", + "content-length: #{byte_size(payload)}\r\n", + "connection: close\r\n\r\n" + ] + + IO.iodata_to_binary([head, payload]) + end + + defp reason(200), do: "OK" + defp reason(400), do: "Bad Request" + defp reason(404), do: "Not Found" + defp reason(429), do: "Too Many Requests" + defp reason(500), do: "Internal Server Error" + defp reason(502), do: "Bad Gateway" + defp reason(503), do: "Service Unavailable" + defp reason(504), do: "Gateway Timeout" + defp reason(_status), do: "Status" +end diff --git a/test/support/server_case.ex b/test/support/server_case.ex new file mode 100644 index 0000000..a64bb05 --- /dev/null +++ b/test/support/server_case.ex @@ -0,0 +1,52 @@ +defmodule Cepis.ServerCase do + @moduledoc false + + use ExUnit.CaseTemplate + + using do + quote do + import Cepis.ServerCase + + alias Cepis.FakeServer + end + end + + setup do + server = Cepis.FakeServer.start() + on_exit(fn -> Cepis.FakeServer.stop(server) end) + + client = + Cepis.Client.new( + base_url: server.url, + timeout: 5_000, + backoff_base: 1, + backoff_max: 10 + ) + + %{server: server, client: client} + end + + @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" + } + + @csv_header "cep,logradouro,complemento,unidade,bairro,localidade,uf,estado,regiao,ibge,gia,ddd,siafi" + + def address_fixture(extra \\ %{}), do: Map.merge(@address, extra) + + def json(term), do: IO.iodata_to_binary(:json.encode(term)) + + def csv(rows), do: Enum.join([@csv_header | rows], "\n") <> "\n" +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() -- cgit v1.3