conn.ex (6623B)
1 defmodule Plug.Adapters.Test.Conn do 2 @behaviour Plug.Conn.Adapter 3 @moduledoc false 4 5 ## Test helpers 6 7 def conn(conn, method, uri, body_or_params) do 8 maybe_flush() 9 uri = URI.parse(uri) 10 11 if is_binary(uri.path) and not String.starts_with?(uri.path, "/") do 12 # TODO: Convert to an error 13 IO.warn("the URI path used in plug tests must start with \"/\", got: #{inspect(uri.path)}") 14 end 15 16 method = method |> to_string |> String.upcase() 17 query = uri.query || "" 18 owner = self() 19 20 {params, {body, body_params}, {query, query_params}, req_headers} = 21 body_or_params(body_or_params, query, conn.req_headers, method) 22 23 state = %{ 24 method: method, 25 params: params, 26 req_body: body, 27 chunks: nil, 28 ref: make_ref(), 29 owner: owner, 30 http_protocol: get_from_adapter(conn, :get_http_protocol, :"HTTP/1.1"), 31 peer_data: 32 get_from_adapter(conn, :get_peer_data, %{ 33 address: {127, 0, 0, 1}, 34 port: 111_317, 35 ssl_cert: nil 36 }) 37 } 38 39 %Plug.Conn{ 40 conn 41 | adapter: {__MODULE__, state}, 42 host: uri.host || conn.host || "www.example.com", 43 method: method, 44 owner: owner, 45 path_info: split_path(uri.path), 46 port: uri.port || 80, 47 remote_ip: conn.remote_ip || {127, 0, 0, 1}, 48 req_headers: req_headers, 49 request_path: uri.path, 50 query_string: query, 51 query_params: query_params || %Plug.Conn.Unfetched{aspect: :query_params}, 52 body_params: body_params || %Plug.Conn.Unfetched{aspect: :body_params}, 53 params: params || %Plug.Conn.Unfetched{aspect: :params}, 54 scheme: (uri.scheme || "http") |> String.downcase() |> String.to_atom() 55 } 56 end 57 58 ## Connection adapter 59 60 def send_resp(%{method: "HEAD"} = state, status, headers, _body) do 61 do_send(state, status, headers, "") 62 end 63 64 def send_resp(state, status, headers, body) do 65 do_send(state, status, headers, IO.iodata_to_binary(body)) 66 end 67 68 def send_file(%{method: "HEAD"} = state, status, headers, _path, _offset, _length) do 69 do_send(state, status, headers, "") 70 end 71 72 def send_file(state, status, headers, path, offset, length) do 73 %File.Stat{type: :regular, size: size} = File.stat!(path) 74 75 length = 76 cond do 77 length == :all -> size 78 is_integer(length) -> length 79 end 80 81 {:ok, data} = 82 File.open!(path, [:read, :binary], fn device -> 83 :file.pread(device, offset, length) 84 end) 85 86 do_send(state, status, headers, data) 87 end 88 89 def send_chunked(state, _status, _headers), do: {:ok, "", %{state | chunks: ""}} 90 91 def chunk(%{method: "HEAD"} = state, _body), do: {:ok, "", state} 92 93 def chunk(%{chunks: chunks} = state, body) do 94 body = chunks <> IO.iodata_to_binary(body) 95 {:ok, body, %{state | chunks: body}} 96 end 97 98 defp do_send(%{owner: owner, ref: ref} = state, status, headers, body) do 99 send(owner, {ref, {status, headers, body}}) 100 {:ok, body, state} 101 end 102 103 def read_req_body(%{req_body: body} = state, opts \\ []) do 104 size = min(byte_size(body), Keyword.get(opts, :length, 8_000_000)) 105 data = :binary.part(body, 0, size) 106 rest = :binary.part(body, size, byte_size(body) - size) 107 108 tag = 109 case rest do 110 "" -> :ok 111 _ -> :more 112 end 113 114 {tag, data, %{state | req_body: rest}} 115 end 116 117 def inform(%{owner: owner, ref: ref}, status, headers) do 118 send(owner, {ref, :inform, {status, headers}}) 119 :ok 120 end 121 122 def upgrade(%{owner: owner, ref: ref}, :not_supported = protocol, opts) do 123 send(owner, {ref, :upgrade, {protocol, opts}}) 124 {:error, :not_supported} 125 end 126 127 def upgrade(%{owner: owner, ref: ref} = state, protocol, opts) do 128 send(owner, {ref, :upgrade, {protocol, opts}}) 129 {:ok, state} 130 end 131 132 def push(%{owner: owner, ref: ref}, path, headers) do 133 send(owner, {ref, :push, {path, headers}}) 134 :ok 135 end 136 137 def get_peer_data(payload) do 138 Map.fetch!(payload, :peer_data) 139 end 140 141 def get_http_protocol(payload) do 142 Map.fetch!(payload, :http_protocol) 143 end 144 145 ## Private helpers 146 147 defp get_from_adapter(conn, op, default) do 148 case conn.adapter do 149 {Plug.MissingAdapter, _} -> default 150 {adapter, payload} -> apply(adapter, op, [payload]) 151 end 152 end 153 154 defp body_or_params(nil, query, headers, _method), do: {nil, {"", nil}, {query, nil}, headers} 155 156 defp body_or_params(body, query, headers, _method) when is_binary(body) do 157 {nil, {body, nil}, {query, nil}, headers} 158 end 159 160 defp body_or_params(params, query, headers, method) when is_list(params) do 161 body_or_params(Enum.into(params, %{}), query, headers, method) 162 end 163 164 defp body_or_params(params, query, headers, method) 165 when is_map(params) and method in ["GET", "HEAD"] do 166 params = stringify_params(params, &to_string/1) 167 168 from_query = Plug.Conn.Query.decode(query) 169 params = Map.merge(from_query, params) 170 171 query = 172 params 173 |> Map.merge(from_query) 174 |> Plug.Conn.Query.encode() 175 176 {params, {"", nil}, {query, params}, headers} 177 end 178 179 defp body_or_params(params, query, headers, _method) when is_map(params) do 180 content_type_header = {"content-type", "multipart/mixed; boundary=plug_conn_test"} 181 content_type = List.keyfind(headers, "content-type", 0, content_type_header) 182 headers = List.keystore(headers, "content-type", 0, content_type) 183 184 body_params = stringify_params(params, & &1) 185 query_params = Plug.Conn.Query.decode(query) 186 params = Map.merge(query_params, body_params) 187 188 {params, {"--plug_conn_test--", body_params}, {query, query_params}, headers} 189 end 190 191 defp stringify_params([{_, _} | _] = params, value_fun), 192 do: Enum.into(params, %{}, &stringify_kv(&1, value_fun)) 193 194 defp stringify_params([_ | _] = params, value_fun), 195 do: Enum.map(params, &stringify_params(&1, value_fun)) 196 197 defp stringify_params(%{__struct__: mod} = struct, _value_fun) when is_atom(mod), do: struct 198 defp stringify_params(fun, _value_fun) when is_function(fun), do: fun 199 200 defp stringify_params(%{} = params, value_fun), 201 do: Enum.into(params, %{}, &stringify_kv(&1, value_fun)) 202 203 defp stringify_params(other, value_fun), do: value_fun.(other) 204 205 defp stringify_kv({k, v}, value_fun), do: {to_string(k), stringify_params(v, value_fun)} 206 207 defp split_path(nil), do: [] 208 209 defp split_path(path) do 210 segments = :binary.split(path, "/", [:global]) 211 for segment <- segments, segment != "", do: segment 212 end 213 214 @already_sent {:plug_conn, :sent} 215 216 defp maybe_flush() do 217 receive do 218 @already_sent -> :ok 219 after 220 0 -> :ok 221 end 222 end 223 end