1defmodule Plug.RouterTest do 2 defmodule Forward do 3 use Plug.Router 4 use Plug.ErrorHandler 5 6 plug :match 7 plug :dispatch 8 9 def call(conn, opts) do 10 super(conn, opts) 11 after 12 Process.put(:plug_forward_call, true) 13 end 14 15 get "/" do 16 resp(conn, 200, "forwarded") 17 end 18 19 get "/script_name" do 20 resp(conn, 200, Enum.join(conn.script_name, ",")) 21 end 22 23 match "/params" do 24 resp(conn, 200, conn.params["param"]) 25 end 26 27 match "/throw", via: [:get, :post] do 28 _ = conn 29 throw :oops 30 end 31 32 match "/raise" do 33 _ = conn 34 raise Plug.Parsers.RequestTooLargeError 35 end 36 37 match "/send_and_exit" do 38 send_resp(conn, 200, "ok") 39 exit(:oops) 40 end 41 42 match "/fancy_id/:id" do 43 send_resp(conn, 200, id <> "--" <> List.last(conn.path_info)) 44 end 45 46 def handle_errors(conn, assigns) do 47 # Custom call is always invoked before 48 true = Process.get(:plug_forward_call) 49 50 Process.put(:plug_handle_errors, Map.put(assigns, :status, conn.status)) 51 super(conn, assigns) 52 end 53 end 54 55 defmodule Reforward do 56 use Plug.Router 57 use Plug.ErrorHandler 58 59 plug :match 60 plug :dispatch 61 62 forward "/step2", to: Forward 63 end 64 65 defmodule SamplePlug do 66 import Plug.Conn 67 68 def init(:hello), do: :world 69 def init(options), do: options 70 71 def call(conn, options) do 72 send_resp(conn, 200, "#{inspect options}") 73 end 74 end 75 76 defmodule Sample do 77 use Plug.Router 78 use Plug.ErrorHandler 79 80 plug :match 81 plug :verify_router_options 82 plug :dispatch 83 84 get "/", host: "foo.bar", do: resp(conn, 200, "foo.bar root") 85 get "/", host: "foo.", do: resp(conn, 200, "foo.* root") 86 forward "/", to: Forward, host: "foo." 87 88 get "/" do 89 resp(conn, 200, "root") 90 end 91 92 get "/1/bar" do 93 resp(conn, 200, "ok") 94 end 95 96 get "/2/:bar" do 97 resp(conn, 200, inspect(bar)) 98 end 99 100 get "/3/bar-:bar" do 101 resp(conn, 200, inspect(bar)) 102 end 103 104 get "/4/*bar" do 105 resp(conn, 200, inspect(bar)) 106 end 107 108 get "/5/bar-*bar" do 109 resp(conn, 200, inspect(bar)) 110 end 111 112 match "/6/bar" do 113 resp(conn, 200, "ok") 114 end 115 116 get "/7/:bar" when byte_size(bar) <= 3, 117 some_option: :hello, 118 do: resp(conn, 200, inspect(bar)) 119 120 get "/8/bar baz/:bat" do 121 resp(conn, 200, bat) 122 end 123 124 plug = SamplePlug 125 opts = :hello 126 get "/plug/match", to: SamplePlug 127 get "/plug/match/options", to: plug, init_opts: opts 128 129 forward "/step1", to: Reforward 130 forward "/forward", to: Forward 131 forward "/nested/forward", to: Forward 132 133 match "/params/get/:param" do 134 resp(conn, 200, conn.params["param"]) 135 end 136 137 forward "/params/forward/:param", to: Forward 138 139 get "/options/map", private: %{an_option: :a_value} do 140 resp(conn, 200, inspect(conn.private)) 141 end 142 143 get "/options/assigns", assigns: %{an_option: :a_value} do 144 resp(conn, 200, inspect(conn.assigns)) 145 end 146 147 forward "/options/forward", to: Forward, private: %{an_option: :a_value}, 148 assigns: %{another_option: :another_value} 149 150 plug = SamplePlug 151 opts = :hello 152 forward "/plug/forward", to: SamplePlug 153 forward "/plug/init_opts", to: plug, init_opts: opts, private: %{baz: :qux} 154 155 match _ do 156 resp(conn, 404, "oops") 157 end 158 159 defp verify_router_options(conn, _opts) do 160 if conn.path_info == ["options", "map"] and is_nil(conn.private[:an_option]) do 161 raise "should be able to read option after match" 162 end 163 conn 164 end 165 end 166 167 use ExUnit.Case, async: true 168 use Plug.Test 169 170 test "dispatch root" do 171 conn = call(Sample, conn(:get, "/")) 172 assert conn.resp_body == "root" 173 end 174 175 test "dispatch literal segment" do 176 conn = call(Sample, conn(:get, "/1/bar")) 177 assert conn.resp_body == "ok" 178 end 179 180 test "dispatch dynamic segment" do 181 conn = call(Sample, conn(:get, "/2/value")) 182 assert conn.resp_body == ~s("value") 183 end 184 185 test "dispatch dynamic segment with prefix" do 186 conn = call(Sample, conn(:get, "/3/bar-value")) 187 assert conn.resp_body == ~s("value") 188 end 189 190 test "dispatch glob segment" do 191 conn = call(Sample, conn(:get, "/4/value")) 192 assert conn.resp_body == ~s(["value"]) 193 194 conn = call(Sample, conn(:get, "/4/value/extra")) 195 assert conn.resp_body == ~s(["value", "extra"]) 196 end 197 198 test "dispatch glob segment with prefix" do 199 conn = call(Sample, conn(:get, "/5/bar-value/extra")) 200 assert conn.resp_body == ~s(["bar-value", "extra"]) 201 end 202 203 test "dispatch custom route" do 204 conn = call(Sample, conn(:get, "/6/bar")) 205 assert conn.resp_body == "ok" 206 end 207 208 test "dispatch with guards" do 209 conn = call(Sample, conn(:get, "/7/a")) 210 assert conn.resp_body == ~s("a") 211 212 conn = call(Sample, conn(:get, "/7/ab")) 213 assert conn.resp_body == ~s("ab") 214 215 conn = call(Sample, conn(:get, "/7/abc")) 216 assert conn.resp_body == ~s("abc") 217 218 conn = call(Sample, conn(:get, "/7/abcd")) 219 assert conn.resp_body == "oops" 220 end 221 222 test "dispatch after decoding guards" do 223 conn = call(Sample, conn(:get, "/8/bar baz/bat")) 224 assert conn.resp_body == "bat" 225 226 conn = call(Sample, conn(:get, "/8/bar%20baz/bat bag")) 227 assert conn.resp_body == "bat bag" 228 229 conn = call(Sample, conn(:get, "/8/bar%20baz/bat%20bag")) 230 assert conn.resp_body == "bat bag" 231 end 232 233 test "dispatch wrong verb" do 234 conn = call(Sample, conn(:post, "/1/bar")) 235 assert conn.resp_body == "oops" 236 end 237 238 test "dispatch to plug" do 239 conn = call(Sample, conn(:get, "/plug/match")) 240 assert conn.resp_body == "[]" 241 end 242 243 test "dispatch to plug with options" do 244 conn = call(Sample, conn(:get, "/plug/match/options")) 245 assert conn.resp_body == ":world" 246 end 247 248 test "dispatch with forwarding" do 249 conn = call(Sample, conn(:get, "/forward")) 250 assert conn.resp_body == "forwarded" 251 assert conn.path_info == ["forward"] 252 end 253 254 test "dispatch with forwarding with custom call" do 255 call(Sample, conn(:get, "/forward")) 256 assert Process.get(:plug_forward_call, true) 257 end 258 259 test "dispatch with forwarding including slashes" do 260 conn = call(Sample, conn(:get, "/nested/forward")) 261 assert conn.resp_body == "forwarded" 262 assert conn.path_info == ["nested", "forward"] 263 end 264 265 test "dispatch with forwarding handles urlencoded path segments" do 266 conn = call(Sample, conn(:get, "/nested/forward/fancy_id/%2BANcgj1jZc%2F9O%2B")) 267 assert conn.resp_body == "+ANcgj1jZc/9O+--%2BANcgj1jZc%2F9O%2B" 268 end 269 270 test "dispatch with forwarding handles un-urlencoded path segments" do 271 conn = call(Sample, conn(:get, "/nested/forward/fancy_id/+ANcgj1jZc9O+")) 272 assert conn.resp_body == "+ANcgj1jZc9O+--+ANcgj1jZc9O+" 273 end 274 275 test "dispatch with forwarding modifies script_name" do 276 conn = call(Sample, conn(:get, "/nested/forward/script_name")) 277 assert conn.resp_body == "nested,forward" 278 279 conn = call(Sample, conn(:get, "/step1/step2/script_name")) 280 assert conn.resp_body == "step1,step2" 281 end 282 283 test "dispatch any verb" do 284 conn = call(Sample, conn(:get, "/6/bar")) 285 assert conn.resp_body == "ok" 286 287 conn = call(Sample, conn(:post, "/6/bar")) 288 assert conn.resp_body == "ok" 289 290 conn = call(Sample, conn(:put, "/6/bar")) 291 assert conn.resp_body == "ok" 292 293 conn = call(Sample, conn(:patch, "/6/bar")) 294 assert conn.resp_body == "ok" 295 296 conn = call(Sample, conn(:delete, "/6/bar")) 297 assert conn.resp_body == "ok" 298 299 conn = call(Sample, conn(:options, "/6/bar")) 300 assert conn.resp_body == "ok" 301 302 conn = call(Sample, conn(:unknown, "/6/bar")) 303 assert conn.resp_body == "ok" 304 end 305 306 test "dispatches based on host" do 307 conn = call(Sample, conn(:get, "http://foo.bar/")) 308 assert conn.resp_body == "foo.bar root" 309 310 conn = call(Sample, conn(:get, "http://foo.other/")) 311 assert conn.resp_body == "foo.* root" 312 313 conn = call(Sample, conn(:get, "http://foo.other/script_name")) 314 assert conn.resp_body == "" 315 end 316 317 test "dispatch not found" do 318 conn = call(Sample, conn(:get, "/unknown")) 319 assert conn.status == 404 320 assert conn.resp_body == "oops" 321 end 322 323 @already_sent {:plug_conn, :sent} 324 325 test "handle errors" do 326 try do 327 call(Sample, conn(:get, "/forward/throw")) 328 flunk "oops" 329 catch 330 :throw, :oops -> 331 assert_received @already_sent 332 assigns = Process.get(:plug_handle_errors) 333 assert assigns.status == 500 334 assert assigns.kind == :throw 335 assert assigns.reason == :oops 336 assert is_list assigns.stack 337 end 338 end 339 340 test "handle errors translates exceptions to status code" do 341 try do 342 call(Sample, conn(:get, "/forward/raise")) 343 flunk "oops" 344 rescue 345 Plug.Parsers.RequestTooLargeError -> 346 assert_received @already_sent 347 assigns = Process.get(:plug_handle_errors) 348 assert assigns.status == 413 349 assert assigns.kind == :error 350 assert assigns.reason.__struct__ == Plug.Parsers.RequestTooLargeError 351 assert is_list assigns.stack 352 end 353 end 354 355 test "handle errors when response was sent" do 356 try do 357 call(Sample, conn(:get, "/forward/send_and_exit")) 358 flunk "oops" 359 catch 360 :exit, :oops -> 361 assert_received @already_sent 362 assert is_nil Process.get(:plug_handle_errors) 363 end 364 end 365 366 test "assigns path params to conn params and path_params" do 367 conn = call(Sample, conn(:get, "/params/get/a_value")) 368 assert conn.params["param"] == "a_value" 369 assert conn.path_params["param"] == "a_value" 370 assert conn.resp_body == "a_value" 371 end 372 373 test "assigns path params to conn params and path_params on forward" do 374 conn = call(Sample, conn(:get, "/params/forward/a_value/params")) 375 assert conn.params["param"] == "a_value" 376 assert conn.path_params["param"] == "a_value" 377 assert conn.resp_body == "a_value" 378 end 379 380 test "path params have priority over body and query params" do 381 conn = conn(:post, "/params/get/p_value", "param=b_value") 382 |> put_req_header("content-type", "application/x-www-form-urlencoded") 383 |> Plug.Parsers.call(Plug.Parsers.init(parsers: [:urlencoded])) 384 385 conn = call(Sample, conn) 386 assert conn.resp_body == "p_value" 387 end 388 389 test "assigns route options to private conn map" do 390 conn = call(Sample, conn(:get, "/options/map")) 391 assert conn.private[:an_option] == :a_value 392 assert conn.resp_body =~ ~s(an_option: :a_value) 393 end 394 395 test "assigns route options to assigns conn map" do 396 conn = call(Sample, conn(:get, "/options/assigns")) 397 assert conn.assigns[:an_option] == :a_value 398 assert conn.resp_body =~ ~s(an_option: :a_value) 399 end 400 401 test "assigns options on forward" do 402 conn = call(Sample, conn(:get, "/options/forward")) 403 assert conn.private[:an_option] == :a_value 404 assert conn.assigns[:another_option] == :another_value 405 assert conn.resp_body == "forwarded" 406 end 407 408 test "forwards to a plug" do 409 conn = call(Sample, conn(:get, "/plug/forward")) 410 assert conn.resp_body == "[]" 411 end 412 413 test "forwards to a plug with init options" do 414 conn = call(Sample, conn(:get, "/plug/init_opts")) 415 assert conn.private[:baz] == :qux 416 assert conn.resp_body == ":world" 417 end 418 419 defp call(mod, conn) do 420 mod.call(conn, []) 421 end 422end 423