summaryrefslogtreecommitdiffstats
path: root/test/support/fake_server.ex
diff options
context:
space:
mode:
authorMurilo Ijanc <[email protected]>2026-07-26 12:19:34 -0300
committerMurilo Ijanc <[email protected]>2026-07-26 12:19:34 -0300
commit075683ff44a254e99adbad8a0372ef80ff06151a (patch)
tree648289c8485afc6f003a3f719fb937c21f149311 /test/support/fake_server.ex
downloadelixir-075683ff44a254e99adbad8a0372ef80ff06151a.tar.gz
Initial importv0.1.0
Diffstat (limited to 'test/support/fake_server.ex')
-rw-r--r--test/support/fake_server.ex154
1 files changed, 154 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