I have an authentication plug and I want to test my controllers. The problem is that the line in this plug has
user_id = get_session(conn, :user_id)
And it's always nil when I'm using this method (I used dirty hack before, but I no longer want to do it):
@session Plug.Session.init([
store: :cookie,
key: "_app",
encryption_salt: "secret",
signing_salt: "secret",
encrypt: false
])
user = MyApp.Factory.create(:user)
conn()
|> put_req_header("accept", "application/vnd.api+json")
|> put_req_header("content-type", "application/vnd.api+json")
|> Map.put(:secret_key_base, String.duplicate("abcdefgh", 8))
|> Plug.Session.call(@session)
|> fetch_session
|> put_session(:user_id, user.id)
I'm sending a patch request using this conn, and its session user_id is nil. Results of IO.puts conn
in my plug:
%Plug.Conn{adapter: {Plug.Adapters.Test.Conn, :...}, assigns: %{},
before_send: [#Function<0.111117999/1 in Plug.Session.before_send/2>,
#Function<0.110103833/1 in JaSerializer.ContentTypeNegotiation.set_content_type/2>,
#Function<1.55011211/1 in Plug.Logger.call/2>,
#Function<0.111117999/1 in Plug.Session.before_send/2>], body_params: %{},
cookies: %{}, halted: false, host: "www.example.com", method: "PATCH",
owner: #PID<0.349.0>,
params: %{"data" => %{"attributes" => %{"action" => "start"}}, "id" => "245"},
path_info: ["api", "tasks", "245"], peer: {{127, 0, 0, 1}, 111317}, port: 80,
private: %{MyApp.Router => {[], %{}}, :phoenix_endpoint => MyApp.Endpoint,
:phoenix_format => "json-api", :phoenix_pipelines => [:api],
:phoenix_recycled => true,
:phoenix_route => #Function<4.15522358/1 in MyApp.Router.match_route/4>,
:phoenix_router => MyApp.Router, :plug_session => %{},
:plug_session_fetch => :done, :plug_session_info => :write,
:plug_skip_csrf_protection => true}, query_params: %{}, query_string: "",
remote_ip: {127, 0, 0, 1}, req_cookies: %{},
req_headers: [{"accept", "application/vnd.api+json"},
{"content-type", "application/vnd.api+json"}], request_path: "/api/tasks/245",
resp_body: nil, resp_cookies: %{},
resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"},
{"x-request-id", "d00tun3s9d7fo2ah2klnhafvt3ks4pbj"}], scheme: :http,
script_name: [],
secret_key_base: "npvJ1fWodIYzJ2eNnJmC5b1LecCTsveK4/mj7akuBaLdeAr2KGH4gwohwHsz8Ony",
state: :unset, status: nil}
What do I need to do to solve this issue and test authentication well?
UPDATE Authentication plug
defmodule MyApp.Plug.Authenticate do
import Plug.Conn
import Phoenix.Controller
def init(default), do: default
def call(conn, _) do
IO.puts inspect get_session(conn, :user_id)
IO.puts conn
user_id = get_session(conn, :user_id)
if user_id do
current_user = MyApp.Repo.get(MyApp.Task, user_id)
assign(conn, :current_user, current_user)
else
conn
|> put_status(401)
|> json(%{})
|> halt
end
end
end
router (I cutted some parts from here):
defmodule MyApp.Router do
use MyApp.Web, :router
pipeline :api do
plug :accepts, ["json-api"] # this line and 3 below are under JaSerializer package responsibility
plug JaSerializer.ContentTypeNegotiation
plug JaSerializer.Deserializer
plug :fetch_session
plug MyApp.Plug.Authenticate # this one
end
scope "/api", MyApp do
pipe_through :api
# tasks
resources "/tasks", TaskController, only: [:show, :update]
end
end
There now exists...
Plug Test init_test_session/2
conn = Plug.Test.init_test_session(conn, user_id: user.id)
It can be solved much easier by bypassing sessions altogether in tests. The idea is to assign current_user
directly to the conn in tests and in the authentication plug - skip fetching user from session when the current_user
assign is set.
This obviously leaves the authentication plug itself untested, but testing there should be much easier, than going through the whole stack.
# in the authentication plug
def call(%{assigns: %{current_user: user}} = conn, opts) when user != nil do
conn
end
def call(conn, opts) do
# handle fetching user from session
end
This allows you to just do assign(conn, :current_user, user)
in tests to authenticate the connection.
Since you call your session before fetch_session/2
so in your authentication plug get_session/2
will return nil
Let's change your authentication plug to make a test:
defmodule MyApp.Plug.Authenticate do
import Plug.Conn
import Phoenix.Controller
alias MyApp.{Repo, User}
def init(opts), do: opts
def call(conn, _opts) do
if user = get_user(conn) do
assign(conn, :current_user, user)
else
conn
|> put_status(401)
|> put_flash(:error, "You must be logged in!")
|> halt
end
end
def get_user(conn) do
case conn.assigns[:current_user] do
nil ->
case get_session(conn, :user_id) do
id -> fetch_user(id)
nil -> nil
end
user -> user
end
end
defp fetch_user(id), do: Repo.get!(User, id)
end
Now you can test your plug like this:
defmodule MyApp.Plug.AuthenticateTest do
use ExUnit.Case, async: true
use Plug.Test
import Phoenix.ConnTest
alias MyApp.Plug.Authenticate
@endpoint MyApp.Endpoint
@session Plug.Session.init([
store: :cookie,
key: "_app",
encryption_salt: "secret",
signing_salt: "secret",
encrypt: false
])
setup do
user = MyApp.Factory.create(:user)
conn = build_conn()
|> put_req_header("accept", "application/vnd.api+json")
|> put_req_header("content-type", "application/vnd.api+json")
|> Map.put(:secret_key_base, String.duplicate("abcdefgh", 8))
|> Plug.Session.call(@session)
|> fetch_session
|> put_session(:user_id, user.id)
{:ok, conn: conn, user: user}
end
test "get_user returns where it is set in session", %{conn: conn, user: user} do
assert Authenticate.get_user(conn) == user
end
end
And finally you can test your controller like:
setup do
user = MyApp.Factory.create(:user)
{:ok, user: user}
end
test "GET /login", %{user: user} do
conn = build_conn()
|> assign(:current_user, user)
|> get("/login")
assert html_response(conn, 200) =~ "Successfull login"
end
There's a similar question like this:
how can i set session in setup when i test phoenix action which need user_id in session?
And have a better way when you want the user inject for a test is store it in conn.private
and read it from private in your authentication plug.
You should take a look to see the change.
Hope that help you!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With