#111: Session Testing with Elixir

Published February 28, 2020

Here we have an application that lists a some albums. Currently you don’t have to be signed into the site to view the albums - It’s accessible to everyone.

In this episode let’s update our application so that users needs to be signed in to view any of the album pages and let’s let tests drive our development. To start, let’s run our test suite to see how everything looks. We’ll go to the command line and run $ mix test and great - the tests that we currently have all pass.

$ mix test

Finished in 0.1 seconds
2 tests, 0 failures

Randomized with seed 68884

Let’s look at those tests. Here we can see our two existing tests. One checking that a 200 is returned with the content “Albums” when we access the album’s index page. And another testing that a 200 is returned along with the album title when we access that album’s show page.

We’ll update this to include tests for our new functionality. Starting with the “index/2” function we’ll add a test that checks that our user is redirected to the homepage if they’re not signed. Then let’s add that same test for our “show/2” function. For our existing tests, we’ll need to handle our session so they pass, but for now let’s comment both of them out.

test/teacher_web/controllers/album_controller_test.exs

defmodule TeacherWeb.AlbumControllerTest do
  use TeacherWeb.ConnCase

  alias Teacher.Recordings

  @create_attrs %{artist: "some artist", summary: "some summary", title: "some title", year: "some year"}

  def fixture(:album) do
    {:ok, album} = Recordings.create_album(@create_attrs)
    album
  end

  describe "index/2" do
    #test "lists all albums when signed in", %{conn: conn} do
    #  #init session
    #  conn = get(conn, Routes.album_path(conn, :index))
    #  assert html_response(conn, 200) =~ "Albums"
    #end

    test "redirects to homepage when not signed in", %{conn: conn} do
      conn = get(conn, Routes.album_path(conn, :index))
      assert html_response(conn, 302) =~ "redirected"
    end
  end

  describe "show/2" do
    setup [:create_album]

    #test "shows the requested album when signed in", %{conn: conn, album: album} do
    #  #init session
    #  conn = get(conn, Routes.album_path(conn, :show, album))
    #  assert html_response(conn, 200) =~ album.title
    #end

    test "redirects to homepage when not signed in", %{conn: conn, album: album} do
      conn = get(conn, Routes.album_path(conn, :show, album))
      assert html_response(conn, 302) =~ "redirected"
    end
  end

  defp create_album(_) do
    album = fixture(:album)
    {:ok, album: album}
  end
end

Then let’s go to the command line and run our tests - and we see that our new test are failing.

$ mix test
...
2 tests, 2 failures

Let’s fix these two test cases and to do that we need to update our project to redirect users to the homepage unless they are signed in. We’ll look in our “plugs” directory and we currently have a plug called SetAuth In it we see that it looks up the user from the session’s current_user_id if it exists. Let’s create another plug that checks if the user exists on the connection and if it does, allow them to access our album pages.

We’ll create a new file in our “plugs” directory named check_auth.ex. Inside it we’ll define the module and import Plug.Conn. We’ll define the init function, ignoring any arguments passed in. Then we can define the call function and let’s pattern match on the connection to see if the current_user is set in the assigns.

If the current user exists, let’s return the connection. Now we’ll define another call function that will be executed when our connection doesn’t have a current user set in it. Inside the function let’s add a flash message telling the user that they need to sign in and then redirect them to our homepage.

We’ll also call halt() to prevent any subsequent plugs from being called. Let’s include aliases for Phoenix.Controller so we can call the controller functions without the prefix and for our Routes. It looks like there’s a type in our second call function. Let’s fix that.

lib/teacher_web/plugs/check_auth.ex

defmodule TeacherWeb.Plugs.CheckAuth do
  import Plug.Conn

  alias TeacherWeb.Router.Helpers, as: Routes

  alias Phoenix.Controller

  def init(_args) do
  end

  def call(%Plug.Conn{assigns: %{current_user: _current_user}} = conn, _args) do
    conn
  end

  def call(conn, _args) do
    conn
    |> Controller.put_flash(:error, "Please sign in to view that page")
    |> Controller.redirect(to: Routes.page_path(conn, :index))
    |> halt()
  end

end

Then let’s open our album_controller.ex and add our new plug to the controller so it’s executed for both the “index” and “show” actions.

lib/teacher_web/controllers/album_controller.ex

defmodule TeacherWeb.AlbumController do
  use TeacherWeb, :controller

  alias Teacher.Recordings

  plug TeacherWeb.Plugs.CheckAuth

  ...
  
end

With that let’s go back to the command line and run our tests again - and great are two tests are now passing.

$ mix test
..

Finished in 0.09 seconds
2 tests, 0 failures

Now let’s go back to our album_controller_test.exs and uncomment our other two tests and let’s run our test suite again.

$ mix test
...
4 tests, 2 failures

We see that our two tests that should be returning a 200 if we’re signed in are failing. This is because we need to put a valid current_user_id on the session so the user can be looked up and then set on the plug. To fix this Plug gives us the Plug.Test.init_test_session function that allows us to initialize the session with specified content, in this case it will be our current_user_id.

Let’s go back to our tests and we’ll need to create a user and set their ID in the session so let’s create another fixture function to return a user. We’ll define the function and define some user attributes and then call Auth.create_user to create a test user.

Also we’ll add an alias for the Auth module so we can call it without the prefix. Then let’s create a function called create_user and in it we’ll call our new user fixture. We’ll return an OK tuple with our user so we can pattern match to get it from our test setup. Then we can update our test for the “show/2” function. Let’s update the setup to include “create_user” then we can pattern match to get our user in addition to the connection and album. And now that we have the user we can call Plug.Test.init_test_session passing in the connection and the current_user_id.

With that we can follow the same pattern for our “index/2” test and make those changes.

test/teacher_web/controllers/album_controller_test.exs

defmodule TeacherWeb.AlbumControllerTest do
  use TeacherWeb.ConnCase

  alias Teacher.{Auth, Recordings}

  @create_attrs %{artist: "some artist", summary: "some summary", title: "some title", year: "some year"}

  def fixture(:album) do
    {:ok, album} = Recordings.create_album(@create_attrs)
    album
  end

  def fixture(:user) do
    user_attrs = %{
      "email" => "hello@elixircasts.io", 
      "username" => "alekx", 
      "encrypted_password" => "foobar"
    }
    {:ok, user} = Auth.create_user(user_attrs)
    user
  end

  describe "index/2" do
    setup [:create_user]

    test "lists all albums when signed in", %{conn: conn, user: user} do
      conn = Plug.Test.init_test_session(conn, current_user_id: user.id)
      conn = get(conn, Routes.album_path(conn, :index))
      assert html_response(conn, 200) =~ "Albums"
    end

    test "redirects to homepage when not signed in", %{conn: conn} do
      conn = get(conn, Routes.album_path(conn, :index))
      assert html_response(conn, 302) =~ "redirected"
    end
  end

  describe "show/2" do
    setup [:create_album, :create_user]

    test "shows the requested album when signed in", %{conn: conn, album: album, user: user} do
      conn = Plug.Test.init_test_session(conn, current_user_id: user.id)
      conn = get(conn, Routes.album_path(conn, :show, album))
      assert html_response(conn, 200) =~ album.title
    end

    test "redirects to homepage when not signed in", %{conn: conn, album: album} do
      conn = get(conn, Routes.album_path(conn, :show, album))
      assert html_response(conn, 302) =~ "redirected"
    end
  end

  defp create_user(_) do
    user = fixture(:user)
    {:ok, user: user}
  end

  defp create_album(_) do
    album = fixture(:album)
    {:ok, album: album}
  end
end


Alright, now for the moment of truth. Let’s go back to the command line and run our tests again.

Perfect - all of our tests now pass. Users must now be signed in to our app in order to view the album pages.

$ mix test
....

Finished in 1.2 seconds
4 tests, 0 failures

© 2020 HEXMONSTER LLC