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