how can i set session in setup when i test phoenix action which need user_id in session?
Asked Answered
S

3

18

I have a test which need to set user_id to session before testing, because this action need to know the current_user.

setup do
  %User{
    id: 123456,
    username: "lcp",
    email: "[email protected]",
    password: Comeonin.Bcrypt.hashpwsalt("password")
  } |> Repo.insert

  {:ok, user: Repo.get(User, 123456) }
end

test "POST /posts", context do
  # conn = conn()
  #      |> put_session(:user_id, context[:user].id)
  #      |> post("/posts", %{ post: %{ title: "title", body: "body" } })

  # assert get_flash(conn, :info) == "Post created successfully."

  # updated to => 
  conn = conn()
        |> Map.put(:secret_key_base, String.duplicate("abcdefgh", 8))
        |> Plug.Session.call(@session)
        |> Plug.Conn.fetch_session
        |> put_session(:user_id, context[:user].id)
        |> post("/posts", %{ post: %{ title: "title", body: "body" } })

  assert get_flash(conn, :info) == "Post created successfully."
end

I have tried this code, but it says that session not fetched, call fetch_session/2.


web/controllers/controller_helper.ex

defmodule SimpleBlog.ControllerHelpers do    
  alias Phoenix.Controller
  alias Plug.Conn
  alias SimpleBlog.Router.Helpers

  def authenticate(conn, _) do
    case Conn.get_session(conn, :user_id) do
      nil ->
        unauthorized(conn)
      user_id ->
        case SimpleBlog.Repo.get(SimpleBlog.User, user_id) do
          {:ok, user} ->
            Conn.assign(conn, :current_user, user)
          nil ->
            unauthorized(conn)
        end
    end
  end

  def unauthorized(conn) do
    conn
      |> Controller.put_flash(:error, "You must be logged in")
      |> Controller.redirect(to: Helpers.session_path(conn, :new))
      |> Conn.halt
  end
end

Updated

I get nil when i get user_id from session through Conn.get_session(conn, :user_id).

Here is post controller web/controllers/post_controller.ex

defmodule SimpleBlog.PostController do
  use SimpleBlog.Web, :controller
  import SimpleBlog.ControllerHelpers

  alias SimpleBlog.Post

  plug :authenticate when not action in [:new]

  def create(conn, %{ "post" => post_params }) do
    changeset = Post.changeset(%Post{}, post_params)

    case Repo.insert(changeset) do
      {:ok, _post} ->
        conn
          |> put_flash(:info, "Post created successfully.")
          |> redirect(to: post_path(conn, :new))
      {:error, changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end
end

It's my test file.

defmodule SimpleBlog.PostControllerTest do
  use SimpleBlog.ConnCase
  alias SimpleBlog.Repo
  alias SimpleBlog.User

  @session Plug.Session.init(
    store: :cookie,
    key: "_app",
    encryption_salt: "yadayada",
    signing_salt: "yadayada"
  )

  setup do
    %User{
      id: 123456,
      username: "lcp",
      email: "[email protected]",
      password: Comeonin.Bcrypt.hashpwsalt("password")
    } |> Repo.insert

    {:ok, user: Repo.get(User, 123456) }
  end


  @tag timeout: 900000
  test "POST /posts", context do
    conn = conn()
            |> Map.put(:secret_key_base, String.duplicate("abcdefgh", 8))
            |> Plug.Session.call(@session)
            |> Plug.Conn.fetch_session
            |> put_session(:user_id, context[:user].id)
            |> post("/posts", %{ post: %{ title: "title", body: "body" } })

    assert get_flash(conn, :info) == "Post created successfully."
  end
end


update ..


lib/simple_blog/plugs/authenticated.ex

I define a plug authenticated

defmodule SimpleBlog.Plugs.Authenticated do
  import Plug.Conn
  alias Phoenix.Controller
  alias SimpleBlog.Router.Helpers
  alias SimpleBlog.User

  def init(options) do
    options
  end

  def call(conn, _) do
    case conn |> current_user_id do
      nil ->
        conn
          |> Controller.put_flash(:error, "You must be logged in")
          |> Controller.redirect(to: Helpers.session_path(conn, :new))
          |> halt
      current_user_id ->
        conn |> assign(:current_user, SimpleBlog.Repo.get(User, current_user_id))
    end
  end

  defp current_user_id(conn) do
    case Mix.env do
      :test ->
        conn.private[:authenticated_current_user_id]
      _ ->
        conn |> fetch_session |> get_session(:current_user_id)
    end
  end
end

in my test

conn = conn()
        |> put_private(:authenticated_current_user_id, context[:user].id)
        |> post("/posts", %{ post: %{ title: "title", body: "body" } })

assert get_flash(conn, :info) == "Post created successfully."

now, the test is passed.

Scherman answered 13/8, 2015 at 8:23 Comment(0)
T
13

You actually can't do it like this due to the post action resetting the session. You have a couple of options.

First, you can do an integration test that visits your login path with valid credentials, and then make your request to create the post.

Secondly, you can create an authentication plug like this:

defmodule SimpleBlog.Plug.Authenticate do
  import Plug.Conn
  alias SimpleBlog.Router.Helpers, as: RouteHelpers
  import Phoenix.Controller

  alias SimpleBlog.Repo
  alias SimpleBlog.User

  def init(opts), do: opts

  def call(conn, _opts) do
    if user = get_user(conn) do
      assign(conn, :current_user, user)
    else
      auth_error!(conn)
    end
  end

  def get_user(conn) do
    case conn.assigns[:current_user] do
      nil      -> fetch_user(conn)
      user     -> user
    end
  end

  defp fetch_user(conn) do
    case get_session(conn, :current_user) |> find_user
      {:ok, user} -> user
      _           -> nil
    end
  end

  defp find_user(id) when do
    Repo.get(User, id)
  end

  defp auth_error!(conn) do
    conn
    |> put_flash(:error, "You need to be signed in to view this page")
    |> redirect(to: RouteHelpers.session_path(conn, :new))
    |> halt
  end
end

You can test this by doing something like the following taken from the Phoenix tests:

defmodule SimpleBlog.Plug.AuthenticationTest do
  use ExUnit.Case
  use Plug.Test

  alias Plug.Conn
  alias SimpleBlog.Plug.Authenticate
  alias SimpleBlog.Repo
  alias SimpleBlog.User
  import SimpleBlog.Router.Helpers

  @session Plug.Session.init(
    store: :cookie,
    key: "_app",
    encryption_salt: "yadayada",
    signing_salt: "yadayada"
  )

  setup do
    user = %User{
      id: 123456,
      username: "lcp",
      email: "[email protected]",
      password: Comeonin.Bcrypt.hashpwsalt("password")
    } |> Repo.insert!

    session_data = %{id: user.id}
    conn =
      conn(:get, "/")
      |> Map.put(:secret_key_base, String.duplicate("abcdefgh", 8))
      |> Plug.Session.call(@session)
      |> Conn.fetch_session()
    {:ok, conn: conn, user: user, session_data: session_data}
  end

  test "get_user returns the user if it is set in conn.assigns", %{conn: conn, user: user} do
    conn = Conn.assign(conn, :current_user, user)
    assert Authenticate.get_user(conn) == user
  end

  test "get_user returns the user if it is set in a session", %{conn: conn, user: user, session_data: session_data} do
    conn = Conn.put_session(conn, :current_user, session_data)
    assert Authenticate.get_user(conn) == user
  end

  test "get_user returns nil if the user is not in assigns or session", %{conn: conn} do
    assert Authenticate.get_user(conn) == nil
  end

  test "when there is not user stored", %{conn: conn} do
    conn =
      |> Phoenix.Controller.fetch_flash
      |> Authenticate.call([])
    assert Phoenix.Controller.get_flash(new_conn, :error) == "You need to be signed in to view this page"
    assert Phoenix.ConnTest.redirected_to(new_conn) == session_path(new_conn, :new)
  end
end

You can now test your controller by doing:

setup do
  %User{
    id: 123456,
    username: "lcp",
    email: "[email protected]",
    password: Comeonin.Bcrypt.hashpwsalt("password")
  } |> Repo.insert

  {:ok, user: Repo.get(User, 123456) }
end

test "POST /posts", %{user: user} do
   conn = conn()
        |> assign(:current_user, user)
        |> post("/posts", %{ post: %{ title: "title", body: "body" } })

   assert get_flash(conn, :info) == "Post created successfully."
end
Thoer answered 13/8, 2015 at 8:29 Comment(12)
I have tried, but can not get user_id from session.Scherman
Plug.Conn.get_session(conn, :user_id)Scherman
hexdocs.pm/plug/Plug.Conn.html#get_session/2 - your code is correct. Could you post more of your code to see what the issue is?Thoer
@Scherman Your controller code looks fine (you may wish to move it out into a separate module in future) - could you please post your updated test?Thoer
@Scherman There is problem with using fetch_session to set up the session and then making a request with that session value - making the request resets the session. Your options are either do a full integration test (call your sign_in route in the test first to set up the session) or refactor it out to fetch from conn.assigns or the sessionThoer
Can you give me more examples about it. I am new to phoenix.Scherman
I would avoid the second solution. I think the best way is for you to access the action that sets the session and test that behaviour (as you mentioned first). That's because I consider the session an implementation detail so it should be covered by integration tests. When you want the user to be injected for a test, simply store it in private and read it from private in your authentication plug.Stimson
@JoséValim Currently I have an OAuth only authentication setup for my app and I don't want to go through the OAuth process (also can't include the necessary creds in the tests). How would you recommend testing that we do redirect when not authed and test for proper actions while 'authed'? Regarding the store it in the auth plug I can't see the plug knowing which test we are in without too much extra state.Novelette
@JoséValim I have rewrote the code, is that right? Can you give me some more advice? THX!Scherman
assign(:current_user, user) did the trick; there was no need to bring in the session config for me.Frumpy
@carp I guess in your controller/authentication plug code you are lazily assigning the current user from the session to assigns? I guess that way, the session loading will be skipped during testing .. neatTut
@Tut Yes, kind of. My current_user in my authentication plug has a conn.assigns[:current_user] || get_session(conn, :current_user) line, really only to enable better testing. I don't know if that's good or bad, but in any event: come to think of it, (lazily) assigning it to assigns as you suggested seems cleaner.Frumpy
K
4

There's Phoenix.ConnTest.init_test_session/2 which might help in setting up a session in tests. If e.g. you need a :token to be present in the session you can set it up like this:

conn =
  conn
  |> init_test_session(conn, %{token: "some-token"})
  |> get("/some-request")
Kershaw answered 8/7, 2021 at 8:52 Comment(0)
T
3

Another simple way is to use assigns, and lazily load data from the session. During integration testing, the data from the session will be loaded, but during unit testing you can simply assign the data:

# controller code
def success(conn, _params) do
  conn
  |> assign(:username, conn.assigns[:username] || get_session(conn, :username))
  |> render("auth_success.html")
end

# test
test "it renders success page with username and token", %{conn: conn} do
  conn = conn
  |> assign(:username, "unpredictablestring")
  |> get("/portal/success")

  assert html_response(conn, 200) =~ "unpredictablestring"
end

Credit goes to @carp from the comments, I just thought this deserved to be posted as an answer.

Tut answered 27/9, 2016 at 4:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.