Authentication

Authentication

https://www.tddphoenix.com/authentication/

Create a test file for the session controller
test/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 assuming
Doorman.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 in
test/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 the
sign_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 include
ChatterWeb.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 from
user_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

Report Page