Authentication
https://www.tddphoenix.com/authentication/Create a test file for the session controllertest/chatter_web/controllers/session_controller_test.exs
, and add the
following test:
defmodule ChatterWeb.SessionControllerTest do use ChatterWeb.ConnCase, async: true describe "create/2" do test "renders error when email/password combination is invalid", %{conn: conn} do user = build(:user) |> set_password("superpass") |> insert() response = conn |> post(Routes.session_path(conn, :create), %{ "email" => user.email, "password" => "invalid password" }) |> html_response(200) assert response =~ "Invalid email or password" end end
end
Let's run the test:
$ mix test test/chatter_web/controllers/session_controller_test.exs
Compiling 2 files (.ex) 1) test create/2 renders error when email/password combination is invalid (ChatterWeb.SessionControllerTest) test/chatter_web/controllers/session_controller_test.exs:5 ** (UndefinedFunctionError) function nil.id/0 is undefined. If you are using the dot syntax, such as map.field or module.function(), make sure the left side of the dot is an atom or a map code: |> post(Routes.session_path(conn, :create), %{ stacktrace: nil.id() (doorman 0.6.2) lib/login/session.ex:12: Doorman.Login.Session.login/2 (chatter 0.1.0) lib/chatter_web/controllers/session_controller.ex:12: 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/test/conn_test.ex:225: Phoenix.ConnTest.dispatch/5 test/chatter_web/controllers/session_controller_test.exs:11: (test) Finished in 0.7 seconds
1 test, 1 failure
We see the nil.id/0
error because we're always assumingDoorman.authenticate/3
will return a user
. But when the email/password
combination is invalid, the function returns nil
. So we're accidentally
passing nil
to Doorman.Login.Session.login/2
instead of a user
.
Let's go ahead and make it a case statement to handle the failure case:
# lib/chatter_web/controllers/session_controller.ex def create(conn, %{"email" => email, "password" => password}) do
- user = Doorman.authenticate(email, password)
+ case Doorman.authenticate(email, password) do
+ nil ->
+ conn
+ |> put_flash(:error, "Invalid email or password")
+ |> render("new.html")
- conn
- |> Doorman.Login.Session.login(user)
- |> redirect(to: "/")
+ user ->
+ conn
+ |> Doorman.Login.Session.login(user)
+ |> redirect(to: "/")
+ end
end
Now rerun our test:
$ mix test test/chatter_web/controllers/session_controller_test.exs
Compiling 2 files (.ex)
. Finished in 0.7 seconds
1 test, 0 failures
Excellent. We've now covered that failure case.
Updating Broken Tests
With authentication in place, I would expect the rest of our feature tests to
fail, since users in those tests aren't signed in. Let's run all of our feature
tests to see if that's true:
$ mix test test/chatter_web/features
Compiling 1 file (.ex) 1) test user can visit homepage (ChatterWeb.UserVisitsHomepageTest) test/chatter_web/features/user_visits_homepage_test.exs:4 ** (Wallaby.ExpectationNotMetError) Expected to find 1, visible element that matched the css '.title' but 0, visible elements were found. code: |> assert_has(Query.css(".title", text: "Welcome to Chatter!")) stacktrace: test/chatter_web/features/user_visits_homepage_test.exs:7: (test) 2) test user visits rooms page to see a list of rooms (ChatterWeb.UserVisitsRoomsPageTest) test/chatter_web/features/user_visits_rooms_page_test.exs:4 ** (Wallaby.ExpectationNotMetError) Expected to find 1, visible element with the attribute 'data-role' with value 'room' but 0, visible elements with the attribute were found. code: |> assert_has(room_name(room1)) stacktrace: test/chatter_web/features/user_visits_rooms_page_test.exs:9: (test) 3) test user can chat with others successfully (ChatterWeb.UserCanChatTest) test/chatter_web/features/user_can_chat_test.exs:4 ** (Wallaby.QueryError) Expected to find 1, visible link 'chat room 0' but 0, visible links were found. code: |> join_room(room.name) 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_can_chat_test.exs:11: (test) Finished in 4.7 seconds
4 tests, 3 failures
All three tests fail because Wallaby is unable to find some element that was
there before adding authentication. Though not evident from the errors, that
happens because the RequireLogin
plug redirects unauthenticated users to the
sign-in page.
To fix those errors, we simply need to add authentication to our tests. Let's
start with /user_visits_rooms_page_test.exs
. Copy the sign_in/2
function
from our previous feature test, and create a user to sign in:
test "user visits rooms page to see a list of rooms", %{session: session} do [room1, room2] = insert_pair(:chat_room)
user = build(:user) |> set_password("password") |> insert() session |> visit(rooms_index())
|> sign_in(as: user) |> assert_has(room_name(room1)) |> assert_has(room_name(room2)) end defp sign_in(session, as: user) do session |> fill_in(Query.text_field("Email"), with: user.email) |> fill_in(Query.text_field("Password"), with: user.password) |> click(Query.button("Sign in")) end defp rooms_index, do: Routes.chat_room_path(@endpoint, :index)
Now run that test:
$ mix test test/chatter_web/features/user_visits_rooms_page_test.exs . Finished in 1.3 seconds
1 test, 0 failures
Good!
Before moving forward with the other two failing tests, let's pause to do a
refactoring: extract the sign_in/2
function into a module that we can reuse
across tests. That way, we can avoid having to copy the sign_in/2
logic over
and over again.
Create a ChatterWeb.FeatureHelpers
module intest/support/feature_helpers.ex
, and move the sign_in/2
function there.
Since we're moving it to make it reusable, change it from a private function to
a public one:
defmodule ChatterWeb.FeatureHelpers do def sign_in(session, as: user) do session |> fill_in(Query.text_field("Email"), with: user.email) |> fill_in(Query.text_field("Password"), with: user.password) |> click(Query.button("Sign in")) end
end
Our sign_in/2
function uses Wallaby's DSL, so add that to the file:
defmodule ChatterWeb.FeatureHelpers do
use Wallaby.DSL def sign_in(session, as: user) do
Finally, import our new ChatterWeb.FeatureHelpers
in our test, and remove thesign_in/2
private function to avoid conflicts:
# test/chatter_web/features/user_visits_room_page_test.exs defmodule ChatterWeb.UserVisitsRoomsPageTest do use ChatterWeb.FeatureCase, async: true
+ import ChatterWeb.FeatureHelpers
+
test "user visits rooms page to see a list of rooms", %{session: session} do [room1, room2] = insert_pair(:chat_room) user = build(:user) |> set_password("password") |> insert()
session |> visit(rooms_index()) |> sign_in(as: user) |> assert_has(room_name(room1)) |> assert_has(room_name(room2)) end
- defp sign_in(session, as: user) do
- session
- |> fill_in(Query.text_field("Email"), with: user.email)
- |> fill_in(Query.text_field("Password"), with: user.password)
- |> click(Query.button("Sign in"))
- end
-
defp rooms_index, do: Routes.chat_room_path(@endpoint, :index)
Now rerun that test:
$ mix test test/chatter_web/features/user_visits_rooms_page_test.exs
Compiling 1 file (.ex)
Generated chatter app . Finished in 1.3 seconds
1 test, 0 failures
Great! The extraction worked, but we're not done. Let's includeChatterWeb.FeatureHelpers
in all feature tests. The easiest way to do that is
to import the module in our ChatterWeb.FeatureCase
:
quote do use Wallaby.DSL import Chatter.Factory
import ChatterWeb.FeatureHelpers alias ChatterWeb.Router.Helpers, as: Routes @endpoint ChatterWeb.Endpoint end
And now we can remove the import ChatterWeb.FeatureHelpers
from our test,
since it'll be imported via use ChatterWeb.FeatureCase
:
# test/chatter_web/features/user_visits_room_page_test.exs defmodule ChatterWeb.UserVisitsRoomsPageTest do use ChatterWeb.FeatureCase, async: true
- import ChatterWeb.FeatureHelpers
-
test "user visits rooms page to see a list of rooms", %{session: session} do
Rerun the test. It should still pass:
$ mix test test/chatter_web/features/user_visits_rooms_page_test.exs
Compiling 1 file (.ex) . Finished in 1.3 seconds
1 test, 0 failures
Good!
Now that we are including a sign_in/2
function across all feature tests, we
need to remove the original sign_in/2
private function fromuser_creates_new_chat_room_test.exs
. Do that:
# test/chatter_web/features/user_creates_new_chat_room_test.exs |> assert_has(room_title("elixir")) end
- defp sign_in(session, as: user) do
- session
- |> fill_in(Query.text_field("Email"), with: user.email)
- |> fill_in(Query.text_field("Password"), with: user.password)
- |> click(Query.button("Sign in"))
- end
-
defp new_chat_link, do: Query.link("New chat room")
And run that test:
$ mix test test/chatter_web/features/user_creates_new_chat_room_test.exs . Finished in 1.3 seconds
1 test, 0 failures
Excellent! Now we can easily add authentication to the rest of the feature
tests.
Updating the rest of the broken feature tests
Let's update user_visits_homepage_text.exs
next. Create a user and sign in:
defmodule ChatterWeb.UserVisitsHomepageTest do use ChatterWeb.FeatureCase, async: true test "user can visit homepage", %{session: session} do
user = build(:user) |> set_password("password") |> insert() session |> visit("/")
|> sign_in(as: user) |> assert_has(Query.css(".title", text: "Welcome to Chatter!")) end
end
Run the test:
$ mix test test/chatter_web/features/user_visits_homepage_test.exs . Finished in 1.1 seconds
1 test, 0 failures
Great.
The last test to update is user_can_chat_test.exs
. Since two users
chat in that test, we need to sign in twice. Unlike the other tests, however,
this test already has the concept of a "user":
user = metadata |> new_user() |> visit(rooms_index()) |> join_room(room.name)
A single look at new_user/1
shows that the function is misleading — It
doesn't create a user; it creates a session. We'll correct the misleading name
in a minute. But first, let's add authentication so our test passes:
test "user can chat with others successfully", %{metadata: metadata} do room = insert(:chat_room)
user1 = build(:user) |> set_password("password") |> insert() user2 = build(:user) |> set_password("password") |> insert() user = metadata |> new_user() |> visit(rooms_index())
|> sign_in(as: user1) |> join_room(room.name) other_user = metadata |> new_user() |> visit(rooms_index())
|> sign_in(as: user2) |> join_room(room.name)
We used user1
and user2
to avoid conflicts with user
and other_user
(which are truly sessions). Run the test:
$ mix test test/chatter_web/features/user_can_chat_test.exs . Finished in 2.4 seconds
1 test, 0 failures
Great! Now that the test is passing, we can refactor it. Let's rename the
session-related code to use "session" terminology rather than "user"
terminology:
# test/chatter_web/features/user_can_chat_test.exs test "user can chat with others successfully", %{metadata: metadata} do room = insert(:chat_room) user1 = build(:user) |> set_password("password") |> insert() user2 = build(:user) |> set_password("password") |> insert()
- user =
+ session1 =
metadata
- |> new_user()
+ |> new_session()
|> visit(rooms_index()) |> sign_in(as: user1) |> join_room(room.name)
- other_user =
+ session2 =
metadata
- |> new_user()
+ |> new_session()
|> visit(rooms_index()) |> sign_in(as: user2) |> join_room(room.name)
- user
+ session1
|> add_message("Hi everyone")
- other_user
+ session2
|> assert_has(message("Hi everyone")) |> add_message("Hi, welcome to #{room.name}")
- user
+ session1
|> assert_has(message("Hi, welcome to #{room.name}")) end
- defp new_user(metadata) do
- {:ok, user} = Wallaby.start_session(metadata: metadata)
- user
+ defp new_session(metadata) do
+ {:ok, session} = Wallaby.start_session(metadata: metadata)
+ session
end
defp rooms_index, do: Routes.chat_room_path(@endpoint, :index)
Now rerun the test:
$ mix test test/chatter_web/features/user_can_chat_test.exs . Finished in 2.4 seconds
1 test, 0 failures
Excellent!
Checking for regressions
Now that we've worked on all of our feature tests, let's run them all to confirm
they pass:
$ mix test test/chatter_web/features
.... Finished in 2.7 seconds
4 tests, 0 failures
Perfect. Let's now run our full test suite to see if we have any other failures:
$ mix test
.... 1) test create/2 renders new page with errors when data is invalid (ChatterWeb.ChatRoomControllerTest) ** (RuntimeError) expected response with status 200, got: 302, with body: <html><body>You are being <a href="/sign_in">redirected</a>.</body></html> code: |> html_response(200) stacktrace: (phoenix 1.5.4) lib/phoenix/test/conn_test.ex:369: Phoenix.ConnTest.response/2 (phoenix 1.5.4) lib/phoenix/test/conn_test.ex:383: Phoenix.ConnTest.html_response/2 test/chatter_web/controllers/chat_room_controller_test.exs:12: (test) .......... Finished in 2.7 seconds
16 tests, 1 failure
Read Next page