1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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
|