1defmodule Phoenix.Controller.PipelineTest do
2  use ExUnit.Case, async: true
3  use RouterHelper
4
5  import Phoenix.Controller
6
7  defmodule MyController do
8    use Phoenix.Controller
9
10    plug :prepend, :before1 when action in [:show, :create, :secret]
11    plug :prepend, :before2
12    plug :do_halt when action in [:secret]
13
14    def show(conn, _) do
15      prepend(conn, :action)
16    end
17
18    def no_fallback(_conn, _) do
19      :not_a_conn
20    end
21
22    def create(conn, _) do
23      prepend(conn, :action)
24    end
25
26    def secret(conn, _) do
27      prepend(conn, :secret_action)
28    end
29
30    def no_match(_conn, %{"no" => "match"}) do
31      raise "Shouldn't have matched"
32    end
33
34    def non_top_level_function_clause_error(conn, params) do
35      send_resp(conn, :ok, trigger_func_clause_error(params))
36    end
37
38    defp trigger_func_clause_error(%{"no" => "match"}), do: :nomatch
39
40    defp do_halt(conn, _), do: halt(conn)
41
42    defp prepend(conn, val) do
43      update_in conn.private.stack, &[val|&1]
44    end
45  end
46
47  defmodule FallbackFunctionController do
48    use Phoenix.Controller
49
50    action_fallback :function_plug
51
52    plug :put_assign
53
54    def fallback(_conn, _), do: :not_a_conn
55
56    def bad_fallback(_conn, _), do: :bad_fallback
57
58    defp function_plug(%Plug.Conn{} = conn, :not_a_conn) do
59      Plug.Conn.send_resp(conn, 200, "function fallback")
60    end
61    defp function_plug(%Plug.Conn{}, :bad_fallback), do: :bad_function_fallback
62
63    defp put_assign(conn, _), do: assign(conn, :value_before_action, :a_value)
64  end
65
66  defmodule ActionController do
67    use Phoenix.Controller
68
69    action_fallback Phoenix.Controller.PipelineTest
70
71    plug :put_assign
72
73    def action(conn, _) do
74      apply(__MODULE__, conn.private.phoenix_action, [conn, conn.body_params,
75                                                      conn.query_params])
76    end
77
78    def show(conn, _, _), do: text(conn, "show")
79
80    def no_match(_conn, _, %{"no" => "match"}) do
81      raise "Shouldn't have matched"
82    end
83
84    def fallback(_conn, _, _) do
85      :not_a_conn
86    end
87
88    def bad_fallback(_conn, _, _) do
89      :bad_fallback
90    end
91
92    defp put_assign(conn, _), do: assign(conn, :value_before_action, :a_value)
93  end
94  def init(opts), do: opts
95  def call(conn, :not_a_conn), do: Plug.Conn.send_resp(conn, 200, "fallback")
96  def call(_conn, :bad_fallback), do: :bad_fallback
97
98  setup do
99    Logger.disable(self())
100    :ok
101  end
102
103  test "invokes the plug stack" do
104    conn = stack_conn()
105           |> MyController.call(:show)
106    assert conn.private.stack == [:action, :before2, :before1]
107  end
108
109  test "invokes the plug stack with guards" do
110    conn = stack_conn()
111           |> MyController.call(:create)
112    assert conn.private.stack == [:action, :before2, :before1]
113  end
114
115  test "halts prevent action from running" do
116    conn = stack_conn()
117           |> MyController.call(:secret)
118    assert conn.private.stack == [:before2, :before1]
119  end
120
121  test "does not override previous views/layouts" do
122    conn = stack_conn()
123           |> put_view(Hello)
124           |> put_layout(false)
125           |> MyController.call(:create)
126    assert view_module(conn) == Hello
127    assert layout(conn) == false
128  end
129
130  test "transforms top-level function clause errors into Phoenix.ActionClauseError" do
131    assert_raise Phoenix.ActionClauseError, fn ->
132      MyController.call(stack_conn(), :no_match)
133    end
134  end
135
136  test "wraps function clause errors lower in action stack in Plug.Conn.WrapperError" do
137    assert_raise Plug.Conn.WrapperError, fn ->
138      MyController.call(stack_conn(), :non_top_level_function_clause_error)
139    end
140  end
141
142  test "action/2 is overridable and still wraps function clause transforms" do
143    conn = ActionController.call(stack_conn(), :show)
144    assert conn.status == 200
145    assert conn.resp_body == "show"
146
147    assert_raise Phoenix.ActionClauseError, fn ->
148      ActionController.call(stack_conn(), :no_match)
149    end
150  end
151
152  describe "action_fallback" do
153    test "module fallback delegates to plug for bad return values when not configured" do
154      assert_raise RuntimeError, ~r/expected action\/2 to return a Plug.Conn/, fn ->
155        MyController.call(stack_conn(), :no_fallback)
156      end
157    end
158
159    test "module fallback invokes module plug when configured" do
160      conn = ActionController.call(stack_conn(), :fallback)
161      assert conn.status == 200
162      assert conn.assigns.value_before_action == :a_value
163      assert conn.resp_body == "fallback"
164    end
165
166    test "module fallback with bad return delegates to plug" do
167      assert_raise RuntimeError, ~r/expected action\/2 to return a Plug.Conn/, fn ->
168        ActionController.call(stack_conn(), :bad_fallback)
169      end
170    end
171
172    test "function fallback invokes module plug when configured" do
173      conn = FallbackFunctionController.call(stack_conn(), :fallback)
174      assert conn.status == 200
175      assert conn.assigns.value_before_action == :a_value
176      assert conn.resp_body == "function fallback"
177    end
178
179    test "function fallback with bad return delegates to plug" do
180      assert_raise RuntimeError, ~r/expected action\/2 to return a Plug.Conn/, fn ->
181        FallbackFunctionController.call(stack_conn(), :bad_fallback)
182      end
183    end
184
185    test "raises when calling from import instead of use", config do
186      assert_raise RuntimeError, ~r/can only be called when using Phoenix.Controller/, fn ->
187        defmodule config.test do
188          import Phoenix.Controller
189          action_fallback Boom
190        end
191      end
192    end
193
194    test "raises when calling more than once", config do
195      assert_raise RuntimeError, ~r/can only be called a single time/, fn ->
196        defmodule config.test do
197          use Phoenix.Controller
198          action_fallback Ok
199          action_fallback Boom
200        end
201      end
202    end
203  end
204
205  defp stack_conn() do
206    conn(:get, "/")
207    |> fetch_query_params()
208    |> put_private(:stack, [])
209  end
210end
211