244 lines
7.0 KiB
Elixir
244 lines
7.0 KiB
Elixir
defmodule Plug.Adapters.Test.Conn do
|
|
@behaviour Plug.Conn.Adapter
|
|
@already_sent Plug.Conn.Adapter.already_sent()
|
|
@moduledoc false
|
|
|
|
## Test helpers
|
|
|
|
def conn(%Plug.Conn{} = conn, method, uri, body_or_params) do
|
|
maybe_flush()
|
|
uri = URI.parse(uri)
|
|
|
|
if is_binary(uri.path) and not String.starts_with?(uri.path, "/") do
|
|
# TODO: Convert to an error
|
|
IO.warn("the URI path used in plug tests must start with \"/\", got: #{inspect(uri.path)}")
|
|
end
|
|
|
|
method = method |> to_string |> String.upcase()
|
|
query = uri.query || ""
|
|
owner = self()
|
|
|
|
{params, {body, body_params}, {query, query_params}, req_headers} =
|
|
body_or_params(body_or_params, query, conn.req_headers, method)
|
|
|
|
state = %{
|
|
method: method,
|
|
params: params,
|
|
req_body: body,
|
|
chunks: nil,
|
|
ref: make_ref(),
|
|
owner: owner,
|
|
http_protocol: get_from_adapter(conn, :get_http_protocol, :"HTTP/1.1"),
|
|
peer_data:
|
|
get_from_adapter(conn, :get_peer_data, %{
|
|
address: {127, 0, 0, 1},
|
|
port: 111_317,
|
|
ssl_cert: nil
|
|
}),
|
|
sock_data:
|
|
get_from_adapter(conn, :get_sock_data, %{
|
|
address: {127, 0, 0, 1},
|
|
port: 111_318
|
|
}),
|
|
ssl_data: get_from_adapter(conn, :get_ssl_data, nil)
|
|
}
|
|
|
|
conn_port = if conn.port != 0, do: conn.port, else: 80
|
|
|
|
%{
|
|
conn
|
|
| adapter: {__MODULE__, state},
|
|
host: uri.host || conn.host || "www.example.com",
|
|
method: method,
|
|
path_info: split_path(uri.path),
|
|
port: uri.port || conn_port,
|
|
remote_ip: conn.remote_ip || {127, 0, 0, 1},
|
|
req_headers: req_headers,
|
|
request_path: uri.path,
|
|
query_string: query,
|
|
query_params: query_params || %Plug.Conn.Unfetched{aspect: :query_params},
|
|
body_params: body_params || %Plug.Conn.Unfetched{aspect: :body_params},
|
|
params: params || %Plug.Conn.Unfetched{aspect: :params},
|
|
scheme: (uri.scheme || "http") |> String.downcase() |> String.to_atom()
|
|
}
|
|
end
|
|
|
|
## Connection adapter
|
|
|
|
def send_resp(%{method: "HEAD"} = state, status, headers, _body) do
|
|
do_send(state, status, headers, "")
|
|
end
|
|
|
|
def send_resp(state, status, headers, body) do
|
|
do_send(state, status, headers, IO.iodata_to_binary(body))
|
|
end
|
|
|
|
def send_file(%{method: "HEAD"} = state, status, headers, _path, _offset, _length) do
|
|
do_send(state, status, headers, "")
|
|
end
|
|
|
|
def send_file(state, status, headers, path, offset, length) do
|
|
%File.Stat{type: :regular, size: size} = File.stat!(path)
|
|
|
|
length =
|
|
cond do
|
|
length == :all -> size
|
|
is_integer(length) -> length
|
|
end
|
|
|
|
{:ok, data} =
|
|
File.open!(path, [:read, :binary], fn device ->
|
|
:file.pread(device, offset, length)
|
|
end)
|
|
|
|
do_send(state, status, headers, data)
|
|
end
|
|
|
|
def send_chunked(%{owner: owner} = state, _status, _headers) do
|
|
send(owner, @already_sent)
|
|
{:ok, "", %{state | chunks: ""}}
|
|
end
|
|
|
|
def chunk(%{method: "HEAD"} = state, _body), do: {:ok, "", state}
|
|
|
|
def chunk(%{chunks: chunks} = state, body) do
|
|
body = chunks <> IO.iodata_to_binary(body)
|
|
{:ok, body, %{state | chunks: body}}
|
|
end
|
|
|
|
defp do_send(%{owner: owner, ref: ref} = state, status, headers, body) do
|
|
send(owner, @already_sent)
|
|
send(owner, {ref, {status, headers, body}})
|
|
{:ok, body, state}
|
|
end
|
|
|
|
def read_req_body(%{req_body: body} = state, opts \\ []) do
|
|
size = min(byte_size(body), Keyword.get(opts, :length, 8_000_000))
|
|
data = :binary.part(body, 0, size)
|
|
rest = :binary.part(body, size, byte_size(body) - size)
|
|
|
|
tag =
|
|
case rest do
|
|
"" -> :ok
|
|
_ -> :more
|
|
end
|
|
|
|
{tag, data, %{state | req_body: rest}}
|
|
end
|
|
|
|
def inform(%{owner: owner, ref: ref}, status, headers) do
|
|
send(owner, {ref, :inform, {status, headers}})
|
|
:ok
|
|
end
|
|
|
|
def upgrade(%{owner: owner, ref: ref}, :not_supported = protocol, opts) do
|
|
send(owner, {ref, :upgrade, {protocol, opts}})
|
|
{:error, :not_supported}
|
|
end
|
|
|
|
def upgrade(%{owner: owner, ref: ref} = state, protocol, opts) do
|
|
send(owner, {ref, :upgrade, {protocol, opts}})
|
|
{:ok, state}
|
|
end
|
|
|
|
def push(%{owner: owner, ref: ref}, path, headers) do
|
|
send(owner, {ref, :push, {path, headers}})
|
|
:ok
|
|
end
|
|
|
|
def get_peer_data(payload) do
|
|
Map.fetch!(payload, :peer_data)
|
|
end
|
|
|
|
def get_sock_data(payload) do
|
|
Map.fetch!(payload, :sock_data)
|
|
end
|
|
|
|
def get_ssl_data(payload) do
|
|
Map.fetch!(payload, :ssl_data)
|
|
end
|
|
|
|
def get_http_protocol(payload) do
|
|
Map.fetch!(payload, :http_protocol)
|
|
end
|
|
|
|
## Private helpers
|
|
|
|
defp get_from_adapter(conn, op, default) do
|
|
case conn.adapter do
|
|
{Plug.MissingAdapter, _} -> default
|
|
{adapter, payload} -> apply(adapter, op, [payload])
|
|
end
|
|
end
|
|
|
|
defp body_or_params(nil, query, headers, _method), do: {nil, {"", nil}, {query, nil}, headers}
|
|
|
|
defp body_or_params(body, query, headers, _method) when is_binary(body) do
|
|
{nil, {body, nil}, {query, nil}, headers}
|
|
end
|
|
|
|
defp body_or_params(params, query, headers, method) when is_list(params) do
|
|
body_or_params(Enum.into(params, %{}), query, headers, method)
|
|
end
|
|
|
|
defp body_or_params(params, query, headers, method)
|
|
when is_map(params) and method in ["GET", "HEAD"] do
|
|
params = stringify_params(params, &to_string/1)
|
|
|
|
from_query = Plug.Conn.Query.decode(query)
|
|
params = Map.merge(from_query, params)
|
|
|
|
query =
|
|
params
|
|
|> Map.merge(from_query)
|
|
|> Plug.Conn.Query.encode()
|
|
|
|
{params, {"", nil}, {query, params}, headers}
|
|
end
|
|
|
|
defp body_or_params(params, query, headers, _method) when is_map(params) do
|
|
content_type_header = {"content-type", "multipart/mixed; boundary=plug_conn_test"}
|
|
content_type = List.keyfind(headers, "content-type", 0, content_type_header)
|
|
headers = List.keystore(headers, "content-type", 0, content_type)
|
|
|
|
body_params = stringify_params(params, & &1)
|
|
query_params = Plug.Conn.Query.decode(query)
|
|
params = Map.merge(query_params, body_params)
|
|
|
|
{params, {"--plug_conn_test--", body_params}, {query, query_params}, headers}
|
|
end
|
|
|
|
defp stringify_params([{_, _} | _] = params, value_fun),
|
|
do: Enum.into(params, %{}, &stringify_kv(&1, value_fun))
|
|
|
|
defp stringify_params([_ | _] = params, value_fun),
|
|
do: Enum.map(params, &stringify_params(&1, value_fun))
|
|
|
|
defp stringify_params(%{__struct__: mod} = struct, _value_fun) when is_atom(mod), do: struct
|
|
defp stringify_params(fun, _value_fun) when is_function(fun), do: fun
|
|
|
|
defp stringify_params(%{} = params, value_fun),
|
|
do: Enum.into(params, %{}, &stringify_kv(&1, value_fun))
|
|
|
|
defp stringify_params(other, value_fun), do: value_fun.(other)
|
|
|
|
defp stringify_kv({k, v}, value_fun), do: {to_string(k), stringify_params(v, value_fun)}
|
|
|
|
defp split_path(nil), do: []
|
|
|
|
defp split_path(path) do
|
|
segments = :binary.split(path, "/", [:global])
|
|
for segment <- segments, segment != "", do: segment
|
|
end
|
|
|
|
@already_sent {:plug_conn, :sent}
|
|
|
|
defp maybe_flush() do
|
|
receive do
|
|
@already_sent -> :ok
|
|
after
|
|
0 -> :ok
|
|
end
|
|
end
|
|
end
|