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