diff options
Diffstat (limited to 'test/support')
| -rw-r--r-- | test/support/fake_server.ex | 154 | ||||
| -rw-r--r-- | test/support/server_case.ex | 52 |
2 files changed, 206 insertions, 0 deletions
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 |