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