test.ex (8498B)
1 defmodule Plug.Test do 2 @moduledoc """ 3 Conveniences for testing plugs. 4 5 This module can be used in your test cases, like this: 6 7 use ExUnit.Case, async: true 8 use Plug.Test 9 10 Using this module will: 11 12 * import all the functions from this module 13 * import all the functions from the `Plug.Conn` module 14 15 By default, Plug tests checks for invalid header keys, e.g. header keys which 16 include uppercase letters, and raises a `Plug.Conn.InvalidHeaderError` when 17 it finds one. To disable it, set `:validate_header_keys_during_test` to 18 false on the app config. 19 20 config :plug, :validate_header_keys_during_test, false 21 22 """ 23 24 @doc false 25 defmacro __using__(_) do 26 quote do 27 import Plug.Test 28 import Plug.Conn 29 end 30 end 31 32 alias Plug.Conn 33 @typep params :: binary | list | map | nil 34 35 @doc """ 36 Creates a test connection. 37 38 The request `method` and `path` are required arguments. `method` may be any 39 value that implements `to_string/1` and it will be properly converted and 40 normalized (e.g., `:get` or `"post"`). 41 42 The `path` is commonly the request path with optional query string but it may 43 also be a complete URI. When a URI is given, the host and schema will be used 44 as part of the request too. 45 46 The `params_or_body` field must be one of: 47 48 * `nil` - meaning there is no body; 49 * a binary - containing a request body. For such cases, `:headers` 50 must be given as option with a content-type; 51 * a map or list - containing the parameters which will automatically 52 set the content-type to multipart. The map or list may contain 53 other lists or maps and all entries will be normalized to string 54 keys; 55 56 ## Examples 57 58 conn(:get, "/foo?bar=10") 59 conn(:get, "/foo", %{bar: 10}) 60 conn(:post, "/") 61 conn("patch", "/", "") |> put_req_header("content-type", "application/json") 62 63 """ 64 @spec conn(String.Chars.t(), binary, params) :: Conn.t() 65 def conn(method, path, params_or_body \\ nil) do 66 Plug.Adapters.Test.Conn.conn(%Plug.Conn{}, method, path, params_or_body) 67 end 68 69 @doc """ 70 Returns the sent response. 71 72 This function is useful when the code being invoked crashes and 73 there is a need to verify a particular response was sent, even with 74 the crash. It returns a tuple with `{status, headers, body}`. 75 """ 76 def sent_resp(%Conn{adapter: {Plug.Adapters.Test.Conn, %{ref: ref}}}) do 77 case receive_resp(ref) do 78 :no_resp -> 79 raise "no sent response available for the given connection. " <> 80 "Maybe the application did not send anything?" 81 82 response -> 83 case receive_resp(ref) do 84 :no_resp -> 85 send(self(), {ref, response}) 86 response 87 88 _otherwise -> 89 raise "a response for the given connection has been sent more than once" 90 end 91 end 92 end 93 94 defp receive_resp(ref) do 95 receive do 96 {^ref, response} -> response 97 after 98 0 -> :no_resp 99 end 100 end 101 102 @doc """ 103 Returns the informational requests that have been sent. 104 105 This function depends on gathering the messages sent by the test adapter when 106 informational messages, such as an early hint, are sent. Calling this 107 function will clear the informational request messages from the inbox for the 108 process. To assert on multiple informs, the result of the function should be 109 stored in a variable. 110 111 ## Examples 112 113 conn = conn(:get, "/foo", "bar=10") 114 informs = Plug.Test.sent_informs(conn) 115 assert {"/static/application.css", [{"accept", "text/css"}]} in informs 116 assert {"/static/application.js", [{"accept", "application/javascript"}]} in informs 117 118 """ 119 def sent_informs(%Conn{adapter: {Plug.Adapters.Test.Conn, %{ref: ref}}}) do 120 Enum.reverse(receive_informs(ref, [])) 121 end 122 123 defp receive_informs(ref, informs) do 124 receive do 125 {^ref, :inform, response} -> 126 receive_informs(ref, [response | informs]) 127 after 128 0 -> informs 129 end 130 end 131 132 @doc """ 133 Returns the upgrade requests that have been sent. 134 135 This function depends on gathering the messages sent by the test adapter when 136 upgrade requests are sent. Calling this function will clear the upgrade request messages from the inbox for the 137 process. 138 139 ## Examples 140 141 conn = conn(:get, "/foo", "bar=10") 142 upgrades = Plug.Test.send_upgrades(conn) 143 assert {:websocket, [opt: :value]} in upgrades 144 145 """ 146 def sent_upgrades(%Conn{adapter: {Plug.Adapters.Test.Conn, %{ref: ref}}}) do 147 Enum.reverse(receive_upgrades(ref, [])) 148 end 149 150 defp receive_upgrades(ref, upgrades) do 151 receive do 152 {^ref, :upgrade, response} -> 153 receive_upgrades(ref, [response | upgrades]) 154 after 155 0 -> upgrades 156 end 157 end 158 159 @doc """ 160 Returns the assets that have been pushed. 161 162 This function depends on gathering the messages sent by the test adapter 163 when assets are pushed. Calling this function will clear the pushed message 164 from the inbox for the process. To assert on multiple pushes, the result 165 of the function should be stored in a variable. 166 167 ## Examples 168 169 conn = conn(:get, "/foo?bar=10") 170 pushes = Plug.Test.sent_pushes(conn) 171 assert {"/static/application.css", [{"accept", "text/css"}]} in pushes 172 assert {"/static/application.js", [{"accept", "application/javascript"}]} in pushes 173 174 """ 175 @deprecated "Most browsers and clients have removed push support" 176 def sent_pushes(%Conn{adapter: {Plug.Adapters.Test.Conn, %{ref: ref}}}) do 177 Enum.reverse(receive_pushes(ref, [])) 178 end 179 180 defp receive_pushes(ref, pushes) do 181 receive do 182 {^ref, :push, response} -> 183 receive_pushes(ref, [response | pushes]) 184 after 185 0 -> pushes 186 end 187 end 188 189 @doc """ 190 Puts the HTTP protocol. 191 """ 192 def put_http_protocol(conn, http_protocol) do 193 update_in(conn.adapter, fn {adapter, payload} -> 194 {adapter, Map.put(payload, :http_protocol, http_protocol)} 195 end) 196 end 197 198 @doc """ 199 Puts the peer data. 200 """ 201 def put_peer_data(conn, peer_data) do 202 update_in(conn.adapter, fn {adapter, payload} -> 203 {adapter, Map.put(payload, :peer_data, peer_data)} 204 end) 205 end 206 207 @doc """ 208 Puts a request cookie. 209 """ 210 @spec put_req_cookie(Conn.t(), binary, binary) :: Conn.t() 211 def put_req_cookie(conn, key, value) when is_binary(key) and is_binary(value) do 212 conn = delete_req_cookie(conn, key) 213 %{conn | req_headers: [{"cookie", "#{key}=#{value}"} | conn.req_headers]} 214 end 215 216 @doc """ 217 Deletes a request cookie. 218 """ 219 @spec delete_req_cookie(Conn.t(), binary) :: Conn.t() 220 def delete_req_cookie(%Conn{req_cookies: %Plug.Conn.Unfetched{}} = conn, key) 221 when is_binary(key) do 222 key = "#{key}=" 223 size = byte_size(key) 224 fun = &match?({"cookie", value} when binary_part(value, 0, size) == key, &1) 225 %{conn | req_headers: Enum.reject(conn.req_headers, fun)} 226 end 227 228 def delete_req_cookie(_conn, key) when is_binary(key) do 229 raise ArgumentError, message: "cannot put/delete request cookies after cookies were fetched" 230 end 231 232 @doc """ 233 Moves cookies from a connection into a new connection for subsequent requests. 234 235 This function copies the cookie information in `old_conn` into `new_conn`, 236 emulating multiple requests done by clients where cookies are always passed 237 forward, and returns the new version of `new_conn`. 238 """ 239 @spec recycle_cookies(Conn.t(), Conn.t()) :: Conn.t() 240 def recycle_cookies(new_conn, old_conn) do 241 req_cookies = Plug.Conn.fetch_cookies(old_conn).req_cookies 242 243 resp_cookies = 244 Enum.reduce(old_conn.resp_cookies, req_cookies, fn {key, opts}, acc -> 245 if value = Map.get(opts, :value) do 246 Map.put(acc, key, value) 247 else 248 Map.delete(acc, key) 249 end 250 end) 251 252 Enum.reduce(resp_cookies, new_conn, fn {key, value}, acc -> 253 put_req_cookie(acc, key, value) 254 end) 255 end 256 257 @doc """ 258 Initializes the session with the given contents. 259 260 If the session has already been initialized, the new contents will be merged 261 with the previous ones. 262 """ 263 @spec init_test_session(Conn.t(), %{optional(String.t() | atom) => any}) :: Conn.t() 264 def init_test_session(conn, session) do 265 conn = 266 if conn.private[:plug_session_fetch] do 267 Conn.fetch_session(conn) 268 else 269 conn 270 |> Conn.put_private(:plug_session, %{}) 271 |> Conn.put_private(:plug_session_fetch, :done) 272 end 273 274 Enum.reduce(session, conn, fn {key, value}, conn -> 275 Conn.put_session(conn, key, value) 276 end) 277 end 278 end