Problem with testing controllers that need authenticated user

HI,

I suppose solution is pretty easy, but for many hours I cant find it and I am struggling now with controller tests that need authenticated user to move on. I tried few solutions from forum and actually I still get failed tests.

Long story short: I have separated user authentication with user profile/data. I dont know if there is better solution but I let auth user create only one user profile with his data (first name, country etc.). Its only allowed to authenticated and confirmed user.

Here are my functions:

Profile_controller.ex

def show_me(conn, _params) do
    user = conn.assigns.current_user
    profile = Accounts.get_profile_by_user_id(user.id)
    render(conn, "show_me.json", profile: profile)
  end

  def create(conn, profile_params) do
    user = conn.assigns.current_user

    if Accounts.get_profile_by_user_id(user.id) do
      conn
      |> put_status(:forbidden)
      |> json(%{message: "You are allowed to have only one profile"})
    else
      with {:ok, %Profile{} = profile} <- Accounts.create_profile(user, profile_params) do
        conn
        |> put_status(:created)
        |> json(%{username: profile.username, message: "Profile created"})
      end
    end
  end

Profile_controller_test.exs

setup %{conn: conn} do
    {:ok, conn: put_req_header(conn, "accept", "application/json")}
  end

  describe "show_me profile" do
    setup do
      %{user: AccountsFixtures.confirmed_user_fixture()}
    end

    test "renders profile when data is valid", %{conn: conn, user: user} do
      token = Bookshare.Auth.generate_user_session_token(user)
      conn = conn |> Plug.Conn.put_req_header("authorization", "Token #{token}")

      profile = AccountsFixtures.profile_fixture(user)

      conn = get(conn, Routes.profile_path(conn, :show_me))
      assert json_response(conn, 200)
    end

    test "renders errors when data is invalid", %{conn: conn, user: user} do
      profile = AccountsFixtures.profile_fixture(user)
      conn = get(conn, Routes.profile_path(conn, :show_me))

      assert json_response(conn, 401)
    end
  end

Accounts_fixtures.exs

  def confirmed_user_fixture(attrs \\ %{}) do
    {:ok, user} =
      attrs
      |> valid_user_attributes()
      |> Bookshare.Auth.register_user()

    user = Map.put(user, :is_confirmed, true)
    user
  end


  def profile_fixture(user, _attrs \\ %{}) do
    attrs = %{
        username: unique_profile_username()
        }

    {:ok, profile} = Bookshare.Accounts.create_profile(user, attrs)

    profile
  end

Basis on show_me test I would do easily create/update tests, but everytime I get 401 Unauthorized error

 1) test show_me profile renders profile when data is valid (BookshareWeb.ProfileControllerTest)
     test/bookshare_web/controllers/profile_controller_test.exs:98
     ** (RuntimeError) expected response with status 200, got: 401, with body:
     "{\"errors\":{\"detail\":\"Unauthorized\"}}"
     code: assert json_response(conn, 200)
     stacktrace:
       (phoenix 1.6.14) lib/phoenix/test/conn_test.ex:369: Phoenix.ConnTest.response/2
       (phoenix 1.6.14) lib/phoenix/test/conn_test.ex:415: Phoenix.ConnTest.json_response/2
       test/bookshare_web/controllers/profile_controller_test.exs:105: (test)

Hi, @mbski!
How do you authorize user? Seems like it happens somewhere in plug before conn hits the controller action and for tests it just gets halted as unauthorized.

BTW, I noticed you put req header authorization: "Token #{token}". Usually, if it’s, bearer token conventionally we prepend the value with Bearer instead of Token, your case might be different though, so, just saying.

1 Like

Hi, thanks for response!

Actually I tried test with "Bearer " or even without any prefix and its still the same. Whats more, while using POSTMAN to manual test in Header tab I just choose Authorization and paste token without any prefix and everything works perfect.

I show whole way of authentication during login:

Login

  def login(conn, %{"email" => email, "password" => password}) do
    if user = Auth.get_user_by_email_and_password(email, password) do
      if user.is_confirmed do
        token = get_token(user)
        conn
        |> put_status(:ok)
        |> render("login.json", user: user, token: token)
      else
        {:error, :unauthorized, "Account not confirmed"}
      end
    else
      {:error, :bad_request, "Invalid email or password"}
    end
  end

get_token(user)

  def get_token(user) do
    Auth.generate_user_session_token(user)
  end

Auth.generate_user_session_token(user)

 def generate_user_session_token(user) do
    {token, user_token} = UserToken.build_session_token(user)
    Repo.insert!(user_token)
    token
  end

UserToken.build_session_token(user)

 def build_session_token(user) do
    token = Base.url_encode64(:crypto.strong_rand_bytes(@rand_size), padding: false)
    {token, %Bookshare.Accounts.UserToken{token: token, context: "session", user_id: user.id}}
  end

I created custom auth basis on few I found on github. I would add that I use Elixir only for API, and for manual test its easier for me using copy/paste tokens :slight_smile:

In addition I found in auth_controller_test.exs test that works well with case similar to those I am struggling with now.

  describe "register view" do
    setup [:create_user, :create_register_params]

    test "allows only unauthenticated user", %{conn: conn, user: user} do
      token = Auth.generate_user_session_token(user)
      conn = conn |> put_req_header("authorization", "Token #{token}")
      conn = post(conn, Routes.auth_path(conn, :register))
      assert json_response(conn, 401)
    end
end

Ok I found something really really strange. Probably my accounts_fixtures are broken in some place…

I imported user_factory:

defmodule Bookshare.Factory do
  use ExMachina.Ecto, repo: Bookshare.Repo

  def user_factory(attrs) do
    password = Map.get(attrs, :password, "password")

    user = %Bookshare.Accounts.User{
      email: sequence(:email, &"email-#{&1}@example.com"),
      hash_password: Bcrypt.hash_pwd_salt(password),
      is_confirmed: true
    }

    merge_attributes(user, attrs)
  end
end

and Setup user creation like in auth_controller_test and now SHOW_ME test works… I do not understand for now what is going on.

Here is my accounts_fixture module

defmodule Bookshare.AccountsFixtures do
  @moduledoc """
  This module defines test helpers for creating
  entities via the `Hello.Accounts` context.
  """

  def unique_user_email, do: "user#{System.unique_integer()}@example.com"
  def valid_user_password, do: "hello world!"

  def valid_user_attributes(attrs \\ %{}) do
    Enum.into(attrs, %{
      email: unique_user_email(),
      password: valid_user_password()
    })
  end

  def user_fixture(attrs \\ %{}) do
    {:ok, user} =
      attrs
      |> valid_user_attributes()
      |> Bookshare.Auth.register_user()

    user = Map.put(user, :is_confirmed, false)
    user
  end

  def confirmed_user_fixture(attrs \\ %{}) do
    {:ok, user} =
      attrs
      |> valid_user_attributes()
      |> Bookshare.Auth.register_user()

    user = Map.put(user, :is_confirmed, true)
    user
  end

    @doc """
  Generate a unique profile username.
  """
  def unique_profile_username, do: "username#{System.unique_integer([:positive])}"

  @doc """
  Generate a profile.
  """
  def profile_fixture(user, _attrs \\ %{}) do
    attrs = %{
        username: unique_profile_username()
        }

    {:ok, profile} = Bookshare.Accounts.create_profile(user, attrs)

    profile
  end
end

If someone see something terrible that makes my test failed in authentication I would hear what I did wrong

@mbski, seems like you are confused about how authentication actually happens in your app.
Most probably router.ex file can help to figure that out. There might be a pipeline with a set of plugs declared before your /profile routes. And probably, one of those plugs in the pipeline extracts the token from the authorization header and validates it. And your tests just don’t really satisfy the plugs needs, so the plug halts the connection and doesn’t let it get to the controller action.

Try to understand the whole flow of the request and response in the application. Read the Phoenix guide Request Lifecycle especially the section “from endpoint to views”.

1 Like

Thank you for your help.

After changing create_user process for tests, everything works perfect. Maybe there is better solution, more clean, but I leave it for a moment when I have better knowledge and then I refacator my code.

Of course I have many lacks and maybe I try to push too fast. I still dont know how to change this "Token " to "Bearer ". I couldn’t find in code anything that let this happen :DDD

Thank you for links, I’ll read it. Sometimes its great to get back to basics and read again about something that you thought you know well.

I suspect this last part is the problem - updating user with Map.put isn’t going to update the database, so the code in the controller will fetch the unconfirmed user and get redirected by the auth machinery.

The ExMachina example doesn’t have that issue because the library inserts what the factory function returns.

1 Like

You can be right! I tried to find a way to put confirmation in user_fixture, but only Map.put came in my mind, that didnt update db itself.

Thanks for response! :slight_smile: