pow-auth / pow

Robust, modular, and extendable user authentication system
https://powauth.com
MIT License
1.59k stars 153 forks source link

Reset password - test fails issue #679

Open mateuszbabski opened 1 year ago

mateuszbabski commented 1 year ago

Hi I really appreciate your work on Pow Auth. It is really great and compact. For learning purposes I try to create my custom controllers for the most important cases. Just to know how it works under the hood, to test it easily in Postman (putting tokens in json and pasting them in another bodies) and how to write good tests in Elixir.

I've struggling for many hours with testing reset password controller, it raises errors every time and that forced me to write an issue cause my digging through web leads to nowhere :)

I start from beginning. Here is my forgot_password func. Pretty straight forward except token in json - easy to test with postman.

@spec forgot_password(Conn.t(), map()) :: Conn.t()
  def forgot_password(conn, email) do
    conn
    |> PowResetPassword.Plug.create_reset_token(email)
    |> case do
      {:ok, %{token: token, user: _user}, conn} ->
        #send email to be implemented
        conn
        |> put_status(200)
        |> json(%{data: %{status: 200, message: "Email has been sent", token: token}})

      {:error, _changeset, conn} ->
        conn
        |> put_status(200)
        |> json(%{data: %{status: 200, message: "Email has been sent"}})
    end
  end

And here is my reset_password controller:

@spec reset_password(Conn.t(), map()) :: Conn.t()
  def reset_password(conn, %{"id" => token, "user" => user_params}) do
    with {:ok, conn} <- PowResetPassword.Plug.load_user_by_token(conn, token),
         {:ok, _user, conn}  <- PowResetPassword.Plug.update_user_password(conn, user_params) do
          json(conn, %{status: "Password changed"})
    else
          {:error, _changeset, conn} ->
            json(conn, %{error: %{message: "Passwords are not the same"}})

          _ ->
            json(conn, %{error: %{message: "Expired Token"}})
    end
  end

Everything works fine during testing in Postman - I dont know yet how to use session/cache token in this application so I use what I learnt while learning C# with JWT tokes.

I put my forgot_password and reset_password tests in one file (the same like they both are in one controller file) Here is my forgot_password part:

setup do
    user =
      %User{}
        |> User.changeset(%{email: "test@example.com", password: @password, password_confirmation: @password})
        |> Repo.insert!()

    {:ok, user: user}
  end

  describe "forgot_password/2" do

    test "with email in database", %{conn: conn} do
      conn = post(conn, Routes.password_path(conn, :forgot_password, @valid_email))

      assert json = json_response(conn, 200)
      assert json["data"]["status"]
      assert json["data"]["message"]
      assert json["data"]["token"]
    end

    test "with invalid email", %{conn: conn} do
      conn = post(conn, Routes.password_path(conn, :forgot_password, @invalid_email))

      assert json = json_response(conn, 200)
      assert json["data"]["status"]
      assert json["data"]["message"]
    end
  end

It works well. Simple, compact, 5 minutes of work. Then problems started to appear - I've tried many configurations of reset password tests but nothing seems to work.

1st attemp:

@valid_params %{"id" => "token", "user" => %{"password" => @new_password, "password_confirmation" => @new_password}}

  describe "reset_password/2" do

    test "with valid token and passwords", %{conn: conn} do
      conn = post(conn, Routes.password_path(conn, :reset_password, @valid_params))

      assert json = json_response(conn, 200)
      assert json["status"]
    end
  end

Witth error:

Expected truthy, got nil
     code: assert json["status"]
     arguments:

         # 1
         %{"error" => %{"message" => "Expired Token"}}

         # 2
         "status"

I suppose it needs real token. So pretty simple that It crashed.

2nd attemp:

use PowApiTemplateWeb.ConnCase

  alias Plug.Conn
  alias PowApiTemplate.{Repo, Users.User}
  alias PowResetPassword.Plug
  alias Pow.Plug, as: PowPlug

setup do
    user =
      %User{}
        |> User.changeset(%{email: "test@example.com", password: @password, password_confirmation: @password})
        |> Repo.insert!()

    {:ok, user: user}
  end

  describe "reset_password/2" do
    setup %{conn: conn} do
      token = PowResetPassword.Plug.create_reset_token(conn, "test@example.com")

      {:ok, conn: conn, token: token}
    end

    test "with valid token and passwords", %{conn: conn} do
      conn = post(conn, Routes.password_path(conn, :reset_password, %{"id" => token, "user" => %{"password" => @new_password, "password_confirmation" => @new_password}}))

      assert json = json_response(conn, 200)
      assert json["status"]
    end
  end

At first I thought it has to be working. Token is invoked and I didnt use any variables to omit mistakes. Then I got error: (Pow.Config.ConfigError) Pow configuration not found in connection. Please use a Pow plug that puts the Pow configuration in the plug connection.

I have no idea what to do now. I spent many hours on elixir forum, with documentation and on github trying to find people with similar problem or repos with custom controllers but it leads to nowhere. I have in my head another solutions, but I know too little yet to do it all by myself without any tips. I dont want to go further without understanding this part and I want to have my own boilerplate for my projects.

I have also some questions:

  1. What comes to my mind is that maybe I need to mock some part of test. Dont know yet which.
  2. I dont know If that type of controllers are possible/secure to go in production. I know that whole Pow is about caching tokes and invoking next functions then. If I use them like I did - they are needed to be pasted into params by "hand" - are they still also passed under the hood in cache? For now I learn mostly API/Backend stuff and it's easy to test it in Postman in contrast to playing with cache/session tokes, but If it is terrible way I just need to know and learn it in other way (this is how 99% courses for C# are taught).

Greetings and thanks for your help :)

mateuszbabski commented 1 year ago

Update:

I think I made big step with this case. I digged into issues here and I found something that looks promising to me. After all it still shows an error but I think it's pretty easy to pass, but still I didnt found solution.

I added to test folder Ets Cache Mock:

defmodule PowApiTemplate.Test.EtsCacheMock do
  @moduledoc false
  @tab __MODULE__

  def init, do: :ets.new(@tab, [:ordered_set, :protected, :named_table])

  def get(config, key) do
    ets_key = ets_key(config, key)

    @tab
    |> :ets.lookup(ets_key)
    |> case do
      [{^ets_key, value} | _rest] -> value
      []                          -> :not_found
    end
  end

  def delete(config, key) do
    :ets.delete(@tab, ets_key(config, key))

    :ok
  end

  def put(config, record_or_records) do
    records     = List.wrap(record_or_records)
    ets_records = Enum.map(records, fn {key, value} ->
      {ets_key(config, key), value}
    end)

    send(self(), {:ets, :put, records, config})
    :ets.insert(@tab, ets_records)
  end

  def all(config, match) do
    ets_key_match = ets_key(config, match)

    @tab
    |> :ets.select([{{ets_key_match, :_}, [], [:"$_"]}])
    |> Enum.map(fn {[_namespace | keys], value} -> {keys, value} end)
  end

  defp ets_key(config, key) do
    [Keyword.get(config, :namespace, "cache")] ++ List.wrap(key)
  end
end

Updated test config with:

config :pow_api_template, :pow,
  cache_backend: [cache_store_backend: PowApiTemplate.Test.EtsCacheMock]

Error is the same if I change it to:

config :pow_api_template, :pow,
  cache_store_backend: PowApiTemplate.Test.EtsCacheMock

I updated conn_case.ex with:

  setup _tags do
    EtsCacheMock.init()

    {:ok, conn: Phoenix.ConnTest.build_conn(), ets: EtsCacheMock}
  end

Adding this line to test_helper.exs leads to fail all tests - even with registration/session controllers:

PowApiTemplate.Test.EtsCacheMock.init()

I also updated password_controller_test.exs:

 setup do
    user =
      %User{}
        |> User.changeset(%{email: "test@example.com", password: @password, password_confirmation: @password})
        |> Repo.insert!()

    {:ok, user: user}
  end

describe "reset_password/2" do

    test "with valid token and passwords", %{conn: conn} do
      PowApiTemplate.Test.EtsCacheMock.init()
      pow_config = [otp_app: :pow_api_template]

      {:ok, %{token: token, user: user}, conn} =
        conn
        |> Pow.Plug.put_config(Application.get_env(:pow_api_template, :pow))
        |> PowResetPassword.Plug.create_reset_token(%{"email" => "test@example.com"})

      valid_params = %{"id" => token, "user" => %{"password" => @new_password, "password_confirmation" => @new_password}}

      conn = post(conn, Routes.password_path(conn, :reset_password, valid_params))

      assert json = json_response(conn, 200)
    end
  end

Trying to start test leads to error:

1) test reset_password/2 with valid token and passwords (PowApiTemplateWeb.PasswordControllerTest)
     test/pow_api_template_web/controllers/password_controller_test.exs:45
     ** (ArgumentError) errors were found at the given arguments:

       * 2nd argument: invalid options

     code: PowApiTemplate.Test.EtsCacheMock.init()
     stacktrace:
       (stdlib 4.0.1) :ets.new(PowApiTemplate.Test.EtsCacheMock, [:set, :protected, :named_table])
       (pow_api_template 0.1.0) test/support/ets_cache_mock.ex:5: PowApiTemplate.Test.EtsCacheMock.init/0
       test/pow_api_template_web/controllers/password_controller_test.exs:46: (test)

I also changed :set to :ordered_set but nothing changes with error. Maybe in Pow 1.0.27 there are changes that I didnt implemented or I wrote something incorrectly. I'll check later PowApiTemplate.Test.EtsCacheMock.init() function and I will try to play with options.