Authentication
https://www.tddphoenix.com/authentication/** (UndefinedFunctionError) function ChatterWeb.SessionView.render/2 is
undefined (module ChatterWeb.SessionView is not available)
Let's fix that by adding a view module:
defmodule ChatterWeb.SessionView do use ChatterWeb, :view
end
Run our test again:
$ mix test test/chatter_web/features/user_creates_new_chat_room_test.exs
Compiling 2 files (.ex)
Generated chatter app 05:27:14.210 [error] Server: localhost:4002 (http)
Request: GET /sign_in
** (exit) an exception was raised: ** (Phoenix.Template.UndefinedError) Could not render "new.html" for ChatterWeb.SessionView, please define a matching clause for render/2 or define a template at "lib/chatter_web/templates/session". No templates were compiled for this module.
Assigns: %{conn: %Plug.Conn{adapter: {Plug.Cowboy.Conn, :...}, assigns: %{layout: {ChatterWeb.LayoutView, "app.html"}}, before_send: [ Assigned keys: [:conn, :view_module, :view_template] (phoenix 1.5.4) lib/phoenix/template.ex:337: Phoenix.Template.raise_template_not_found/3 (phoenix 1.5.4) lib/phoenix/view.ex:310: Phoenix.View.render_within/3 (phoenix 1.5.4) lib/phoenix/view.ex:472: Phoenix.View.render_to_iodata/3 (phoenix 1.5.4) lib/phoenix/controller.ex:776: Phoenix.Controller.render_and_send/4 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:1: ChatterWeb.SessionController.action/2 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:1: ChatterWeb.SessionController.phoenix_controller_pipeline/2 (phoenix 1.5.4) lib/phoenix/router.ex:352: Phoenix.Router.__call__/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.plug_builder_call/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.call/2 (phoenix 1.5.4) lib/phoenix/endpoint/cowboy2_handler.ex:65: Phoenix.Endpoint.Cowboy2Handler.init/4 1) test user creates a new chat room successfully (ChatterWeb.UserCreatesNewChatRoomTest) test/chatter_web/features/user_creates_new_chat_room_test.exs:4 ** (Wallaby.QueryError) Expected to find 1, visible text input or textarea 'Email' but 0, visible text inputs or textareas were found. code: |> sign_in(as: user) stacktrace: (wallaby) lib/wallaby/browser.ex:475: Wallaby.Browser.find/2 (wallaby) lib/wallaby/browser.ex:456: Wallaby.Browser.find/3 test/chatter_web/features/user_creates_new_chat_room_test.exs:17: ChatterWeb.UserCreatesNewChatRoomTest.sign_in/2 test/chatter_web/features/user_creates_new_chat_room_test.exs:9: (test) Finished in 3.6 seconds
1 test, 1 failure
Now we see that there is no "new.html"
template:
** (Phoenix.Template.UndefinedError) Could not render "new.html" for
ChatterWeb.SessionView, please define a matching clause for render/2 or define a
template at "lib/chatter_web/templates/session". No templates were compiled for
this module.
You might have expected that error after our last one. Let's just create an
empty template there:
$ mkdir lib/chatter_web/templates/session
$ touch lib/chatter_web/templates/session/new.html.eex
Rerun the test:
$ mix test test/chatter_web/features/user_creates_new_chat_room_test.exs
Compiling 1 file (.ex) 1) test user creates a new chat room successfully (ChatterWeb.UserCreatesNewChatRoomTest) test/chatter_web/features/user_creates_new_chat_room_test.exs:4 ** (Wallaby.QueryError) Expected to find 1, visible text input or textarea 'Email' but 0, visible text inputs or textareas were found. code: |> sign_in(as: user) stacktrace: (wallaby 0.26.2) lib/wallaby/browser.ex:716: Wallaby.Browser.find/2 (wallaby 0.26.2) lib/wallaby/browser.ex:700: Wallaby.Browser.find/3 test/chatter_web/features/user_creates_new_chat_room_test.exs:17: ChatterWeb.UserCreatesNewChatRoomTest.sign_in/2 test/chatter_web/features/user_creates_new_chat_room_test.exs:9: (test) Finished in 3.6 seconds
1 test, 1 failure
Good! We got rid of all Elixir compilation errors and Phoenix errors. We can now
focus on Wallaby's error — expecting to find forum inputs:
** (Wallaby.QueryError) Expected to find 1, visible text input or textarea
'Email' but 0, visible text inputs or textareas were found.
Let's add a form for our users to sign in:
<%= form_for @conn, Routes.session_path(@conn, :create), fn f -> %> <label> Email: <%= text_input f, :email %> </label> <label> Password: <%= password_input f, :password %> </label> <%= submit "Sign in" %>
<% end %>
Now rerun our test:
$ mix test test/chatter_web/features/user_creates_new_chat_room_test.exs
Compiling 1 file (.ex)
05:42:41.871 [error] Server: localhost:4002 (http)
Request: GET /sign_in
** (exit) an exception was raised: ** (ArgumentError) no action :create for ChatterWeb.Router.Helpers.session_path/2. The following actions/clauses are supported: session_path(conn_or_endpoint, :new, params \\ []) (phoenix 1.5.4) lib/phoenix/router/helpers.ex:374: Phoenix.Router.Helpers.invalid_route_error/3 (chatter 0.1.0) lib/chatter_web/templates/session/new.html.eex:1: ChatterWeb.SessionView."new.html"/1 (phoenix 1.5.4) lib/phoenix/view.ex:310: Phoenix.View.render_within/3 (phoenix 1.5.4) lib/phoenix/view.ex:472: Phoenix.View.render_to_iodata/3 (phoenix 1.5.4) lib/phoenix/controller.ex:776: Phoenix.Controller.render_and_send/4 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:1: ChatterWeb.SessionController.action/2 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:1: ChatterWeb.SessionController.phoenix_controller_pipeline/2 (phoenix 1.5.4) lib/phoenix/router.ex:352: Phoenix.Router.__call__/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.plug_builder_call/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.call/2 (phoenix 1.5.4) lib/phoenix/endpoint/cowboy2_handler.ex:65: Phoenix.Endpoint.Cowboy2Handler.init/4 (stdlib 3.13.1) proc_lib.erl:226: :proc_lib.init_p_do_apply/3 1) test user creates a new chat room successfully (ChatterWeb.UserCreatesNewChatRoomTest) test/chatter_web/features/user_creates_new_chat_room_test.exs:4 ** (Wallaby.QueryError) Expected to find 1, visible text input or textarea 'Email' but 0, visible text inputs or textareas were found. code: |> sign_in(as: user) stacktrace: (wallaby 0.26.2) lib/wallaby/browser.ex:716: Wallaby.Browser.find/2 (wallaby 0.26.2) lib/wallaby/browser.ex:700: Wallaby.Browser.find/3 test/chatter_web/features/user_creates_new_chat_room_test.exs:17: ChatterWeb.UserCreatesNewChatRoomTest.sign_in/2 test/chatter_web/features/user_creates_new_chat_room_test.exs:9: (test) Finished in 3.5 seconds
1 test, 1 failure
The test fails because we do not have a create Routes.session_path
. Let's add
that route:
scope "/", ChatterWeb do pipe_through :browser get "/sign_in", SessionController, :new
resources "/sessions", SessionController, only: [:create] end
Rerun our test:
$ mix test test/chatter_web/features/user_creates_new_chat_room_test.exs
Compiling 2 files (.ex)
05:44:22.757 [error] Server: localhost:4002 (http)
Request: POST /sessions
** (exit) an exception was raised: ** (UndefinedFunctionError) function ChatterWeb.SessionController.create/2 is undefined or private (chatter 0.1.0) ChatterWeb.SessionController.create(%Plug.Conn{adapter: {Plug.Cowboy.Conn, :...}, assigns: %{}, before_send: [ (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:1: ChatterWeb.SessionController.action/2 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:1: ChatterWeb.SessionController.phoenix_controller_pipeline/2 (phoenix 1.5.4) lib/phoenix/router.ex:352: Phoenix.Router.__call__/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.plug_builder_call/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.call/2 (phoenix 1.5.4) lib/phoenix/endpoint/cowboy2_handler.ex:65: Phoenix.Endpoint.Cowboy2Handler.init/4 (stdlib 3.13.1) proc_lib.erl:226: :proc_lib.init_p_do_apply/3 1) test user creates a new chat room successfully (ChatterWeb.UserCreatesNewChatRoomTest) test/chatter_web/features/user_creates_new_chat_room_test.exs:4 ** (Wallaby.QueryError) Expected to find 1, visible link 'New chat room' but 0, visible links were found. code: |> click(new_chat_link()) stacktrace: (wallaby 0.26.2) lib/wallaby/browser.ex:716: Wallaby.Browser.find/2 (wallaby 0.26.2) lib/wallaby/browser.ex:700: Wallaby.Browser.find/3 test/chatter_web/features/user_creates_new_chat_room_test.exs:10: (test) Finished in 4.0 seconds
1 test, 1 failure
Our test now fails because our controller's create action is undefined: **
.
(UndefinedFunctionError) function ChatterWeb.SessionController.create/2 is
undefined or private
Let's define that action with a modified version of Doorman's example in its
documentation for creating the session:
def new(conn, _) do render(conn, "new.html") end def create(conn, %{"email" => email, "password" => password}) do user = Doorman.authenticate(email, password) conn |> Doorman.Login.Session.login(user) |> redirect(to: "/") endend
Rerun the test:
$ mix test test/chatter_web/features/user_creates_new_chat_room_test.exs
Compiling 2 files (.ex)
05:45:45.327 [error] Server: localhost:4002 (http)
Request: POST /sessions
** (exit) an exception was raised: ** (RuntimeError) You must add `user_module` to `doorman` in your config Here is an example configuration: config :doorman, repo: MyApp.Repo, secure_with: Doorman.Auth.Bcrypt, user_module: MyApp.User (doorman 0.6.2) lib/doorman.ex:81: Doorman.get_module/1 (doorman 0.6.2) lib/doorman.ex:26: Doorman.authenticate/3 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:9: ChatterWeb.SessionController.create/2 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:1: ChatterWeb.SessionController.action/2 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:1: ChatterWeb.SessionController.phoenix_controller_pipeline/2 (phoenix 1.5.4) lib/phoenix/router.ex:352: Phoenix.Router.__call__/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.plug_builder_call/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.call/2 (phoenix 1.5.4) lib/phoenix/endpoint/cowboy2_handler.ex:65: Phoenix.Endpoint.Cowboy2Handler.init/4 (stdlib 3.13.1) proc_lib.erl:226: :proc_lib.init_p_do_apply/3 1) test user creates a new chat room successfully (ChatterWeb.UserCreatesNewChatRoomTest) test/chatter_web/features/user_creates_new_chat_room_test.exs:4 ** (Wallaby.QueryError) Expected to find 1, visible link 'New chat room' but 0, visible links were found. code: |> click(new_chat_link()) stacktrace: (wallaby 0.26.2) lib/wallaby/browser.ex:716: Wallaby.Browser.find/2 (wallaby 0.26.2) lib/wallaby/browser.ex:700: Wallaby.Browser.find/3 test/chatter_web/features/user_creates_new_chat_room_test.exs:10: (test) Finished in 3.9 seconds
1 test, 1 failure
Whoops! This time the test caught an oversight on our part. When adding Doorman,
we did not configure it properly. Thankfully we got a helpful error:
** (exit) an exception was raised: ** (RuntimeError) You must add `user_module` to `doorman` in your config
Here is an example configuration:
config :doorman,
repo: MyApp.Repo,
secure_with: Doorman.Auth.Bcrypt,
user_module: MyApp.User
Let's set that configuration in our config.exs
file:
config :phoenix, :json_library, Jason config :doorman, repo: Chatter.Repo, secure_with: Doorman.Auth.Bcrypt, user_module: Chatter.User
Now rerun our test:
$ mix test test/chatter_web/features/user_creates_new_chat_room_test.exs
Compiling 26 files (.ex)
Generated chatter app
05:47:38.978 [error] Server: localhost:4002 (http)
Request: POST /sessions
** (exit) an exception was raised: ** (ArgumentError) Wrong type. The password and hash need to be strings. (comeonin 2.6.0) lib/comeonin/bcrypt.ex:122: Comeonin.Bcrypt.checkpw/2 (doorman 0.6.2) lib/doorman.ex:29: Doorman.authenticate/3 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:9: ChatterWeb.SessionController.create/2 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:1: ChatterWeb.SessionController.action/2 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:1: ChatterWeb.SessionController.phoenix_controller_pipeline/2 (phoenix 1.5.4) lib/phoenix/router.ex:352: Phoenix.Router.__call__/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.plug_builder_call/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.call/2 (phoenix 1.5.4) lib/phoenix/endpoint/cowboy2_handler.ex:65: Phoenix.Endpoint.Cowboy2Handler.init/4 (stdlib 3.13.1) proc_lib.erl:226: :proc_lib.init_p_do_apply/3 1) test user creates a new chat room successfully (ChatterWeb.UserCreatesNewChatRoomTest) test/chatter_web/features/user_creates_new_chat_room_test.exs:4 ** (Wallaby.QueryError) Expected to find 1, visible link 'New chat room' but 0, visible links were found. code: |> click(new_chat_link()) stacktrace: (wallaby 0.26.2) lib/wallaby/browser.ex:716: Wallaby.Browser.find/2 (wallaby 0.26.2) lib/wallaby/browser.ex:700: Wallaby.Browser.find/3 test/chatter_web/features/user_creates_new_chat_room_test.exs:10: (test) Finished in 3.9 seconds
1 test, 1 failure
Whoa! That is an unexpected error. The stack trace goes out of our application
and into Doorman's modules and even into Comeonin, a library Doorman uses:
** (ArgumentError) Wrong type. The password and hash need to be strings. (comeonin 2.6.0) lib/comeonin/bcrypt.ex:122: Comeonin.Bcrypt.checkpw/2 (doorman 0.6.2) lib/doorman.ex:29: Doorman.authenticate/3 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:9: ChatterWeb.SessionController.create/2
The error may seem daunting at first. But focusing on the error message, we see
that the password
and hash
need to be strings. We set the password in our
user factory, so it is a string. But the hashed_password
is missing. Doorman
and Comeonin must be trying to compare our password to the missing hashed
version, and that's why we get an error.
We would expect Doorman to set the hashed_password
for us. And indeed, looking
at the User
module in the documentation's example, we see it import ahash_password/1
function from Doorman.Auth.Bcrypt
. Let's use that function
in our factory.
At this point, I do not know if all of our test users will need ahashed_password
. And since hashing could be slow, I'll create a new functionset_password/2
to do the hashing. Let's change the insert(:user)
in our test
to build a user, set and hash the password, and then insert it:
# test/chatter_web/features/user_creates_new_chat_room_test.exs test "user creates a new chat room successfully", %{session: session} do
- user = insert(:user)
+ user = build(:user) |> set_password("superpass") |> insert()
session |> visit("/")
Now let's add the set_password/2
function to our factory:
def set_password(user, password) do user |> Ecto.Changeset.change(%{password: password}) |> Doorman.Auth.Bcrypt.hash_password() |> Ecto.Changeset.apply_changes() endend
In set_password/2
, we take a User
struct and a password
. We then turn that
data into a changeset, since the Doorman.Auth.Bcrypt.hash_password/1
function
requires a changeset. Finally, we apply the changes to get a User
struct ready
to be inserted into the database.
Now rerun our test:
$ mix test test/chatter_web/features/user_creates_new_chat_room_test.exs 1) test user creates a new chat room successfully (ChatterWeb.UserCreatesNewChatRoomTest) test/chatter_web/features/user_creates_new_chat_room_test.exs:4 ** (Wallaby.QueryError) Expected to find 1, visible link 'New chat room' but 0, visible links were found. code: |> click(new_chat_link()) stacktrace: (wallaby 0.26.2) lib/wallaby/browser.ex:716: Wallaby.Browser.find/2 (wallaby 0.26.2) lib/wallaby/browser.ex:700: Wallaby.Browser.find/3 test/chatter_web/features/user_creates_new_chat_room_test.exs:10: (test) Finished in 4.5 seconds
1 test, 1 failure
Alright! We're past the strange error Doorman was throwing. And now, our users
can sign in! But why can't Wallaby find the "New chat room" link?
Our application loses all knowledge of a user's authentication when it redirects
to a new page — we need to use a session.
Setting the current user
Doorman.logged_in?/1
checks for a current_user
in the conn.assigns
. But we need another plug to
set the current_user
in the first place. Doorman's documentation recommends
adding the Doorman.Login.Session
plug in our :browser
pipeline to do just
that. So let's do it:
plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers
plug Doorman.Login.Session end
Now, rerun the test:
$ mix test test/chatter_web/features/user_creates_new_chat_room_test.exs
Compiling 2 files (.ex)
18:54:14.932 [error] Server: localhost:4002 (http)
Request: GET /
** (exit) an exception was raised: ** (ArgumentError) nil given for :session_secret. Comparison with nil is forbidden as it is unsafe. Instead write a query with is_nil/1, for example: is_nil(s.session_secret) (ecto 3.4.6) lib/ecto/query/builder/filter.ex:135: Ecto.Query.Builder.Filter.kw!/7 (ecto 3.4.6) lib/ecto/query/builder/filter.ex:128: Ecto.Query.Builder.Filter.kw!/3 (ecto 3.4.6) lib/ecto/query/builder/filter.ex:110: Ecto.Query.Builder.Filter.filter!/6 (ecto 3.4.6) lib/ecto/query/builder/filter.ex:122: Ecto.Query.Builder.Filter.filter!/7 (ecto 3.4.6) lib/ecto/repo/queryable.ex:70: Ecto.Repo.Queryable.get_by/4 (doorman 0.6.2) lib/login/session.ex:2: Doorman.Login.Session.call/2 (chatter 0.1.0) ChatterWeb.Router.browser/2 (chatter 0.1.0) lib/chatter_web/router.ex:1: ChatterWeb.Router.__pipe_through1__/1 (phoenix 1.5.4) lib/phoenix/router.ex:347: Phoenix.Router.__call__/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.plug_builder_call/2 (chatter 0.1.0) lib/chatter_web/endpoint.ex:1: ChatterWeb.Endpoint.call/2 (phoenix 1.5.4) lib/phoenix/endpoint/cowboy2_handler.ex:65: Phoenix.Endpoint.Cowboy2Handler.init/4 (stdlib 3.13.1) proc_lib.erl:226: :proc_lib.init_p_do_apply/3 1) test user creates a new chat room successfully (ChatterWeb.UserCreatesNewChatRoomTest) test/chatter_web/features/user_creates_new_chat_room_test.exs:4 ** (Wallaby.QueryError) Expected to find 1, visible link 'New chat room' but 0, visible links were found. code: |> click(new_chat_link()) stacktrace: (wallaby 0.26.2) lib/wallaby/browser.ex:716: Wallaby.Browser.find/2 (wallaby 0.26.2) lib/wallaby/browser.ex:700: Wallaby.Browser.find/3 test/chatter_web/features/user_creates_new_chat_room_test.exs:10: (test) Finished in 4.7 seconds
1 test, 1 failure
The output is large, but the main error is this one:
** (ArgumentError) nil given for :session_secret. Comparison with nil is
forbidden as it is unsafe. Instead write a query with is_nil/1, for example:
is_nil(s.session_secret)
Our :session_secret
is nil
for some reason. Looking at the stack trace, we
can see it comes from Doorman:
(doorman 0.6.2) lib/login/session.ex:2: Doorman.Login.Session.call/2
Why do we get that error?
If you recall, we defined a session_secret
in our users
table, but we never
set it in our tests. According to Doorman's documentation, it needs to be set
during user creation with Doorman.Auth.Secret.put_session_secret/1
. Let's add
that step to our set_password/2
function to set the session secret when we set
the password:
def set_password(user, password) do user |> Ecto.Changeset.change(%{password: password}) |> Doorman.Auth.Bcrypt.hash_password()
|> Doorman.Auth.Secret.put_session_secret() |> Ecto.Changeset.apply_changes() end
Now run our test once more!
$ mix test test/chatter_web/features/user_creates_new_chat_room_test.exs . Finished in 1.4 seconds
1 test, 0 failures
Great! We did it. Our users can now sign in before they start chatting.
Testing invalid authentication credentials
Before we move further, there's one thing we glossed over in our quest for a
passing feature spec. When I copied over Doorman's example of creating a
session, I called it a modified version because we didn't include any error
handling. Let's do that now.
Read Next page