13
votes

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
3
(To print conn, use IO.inspect conn.)Dogbert
@Dogbert thanks I know :) How to solve my issue?asiniy
Can you post your full authentication plug?TheAnh
@asiniy could you add the contents of your router.ex too? Could you also please put the actual output of IO.inspect conn in the question? The current output is extremely unreadable as it's all on one line. IO.inspect should give you a nicely wrapped output.Dogbert
@Dogbert thanks for IO.inspect hint. Added routerasiniy

3 Answers

3
votes

There now exists...

Plug Test init_test_session/2

conn = Plug.Test.init_test_session(conn, user_id: user.id)
1
votes

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.

1
votes

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!