Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use connection with session in phoenix?

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
like image 208
asiniy Avatar asked Jul 01 '16 04:07

asiniy


3 Answers

There now exists...

Plug Test init_test_session/2

conn = Plug.Test.init_test_session(conn, user_id: user.id)
like image 114
sweeneydavidj Avatar answered Nov 20 '22 02:11

sweeneydavidj


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.

like image 42
michalmuskala Avatar answered Nov 20 '22 01:11

michalmuskala


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!

like image 1
TheAnh Avatar answered Nov 20 '22 02:11

TheAnh