diff --git a/features/config.exs b/features/config.exs index a75437b245684d42990238f4d28456c080a2d360..927c961a01a523080e9588bd45bebb3d8f18b6ab 100644 --- a/features/config.exs +++ b/features/config.exs @@ -9,6 +9,10 @@ defmodule WhiteBreadConfig do context: UserLoginContext, feature_paths: ["features/user_login.feature"] + suite name: "User Logout Features", + context: UserLogoutContext, + feature_paths: ["features/user_logout.feature"] + suite name: "Password Change Features", context: PasswordChangeContext, feature_paths: ["features/password_change.feature"] diff --git a/features/contexts/user_logout_context.exs b/features/contexts/user_logout_context.exs new file mode 100644 index 0000000000000000000000000000000000000000..408ca719fb6790fac6ebd3e7afb46af754882dc8 --- /dev/null +++ b/features/contexts/user_logout_context.exs @@ -0,0 +1,79 @@ +defmodule UserLogoutContext do + use WhiteBread.Context + use Hound.Helpers + alias PropTrackr.Accounts + alias PropTrackr.Repo + alias PropTrackr.Accounts.User + + scenario_starting_state fn _state -> + Ecto.Adapters.SQL.Sandbox.checkout(PropTrackr.Repo) + Ecto.Adapters.SQL.Sandbox.mode(PropTrackr.Repo, {:shared, self()}) + Hound.start_session() + %{} + end + + scenario_finalize fn _status, _state -> + Ecto.Adapters.SQL.Sandbox.checkin(PropTrackr.Repo) + Hound.end_session() + end + + given_ ~r/^there exists following accounts$/, fn state, %{table_data: table} -> + table + |> Enum.map(fn user_details -> User.changeset(%User{}, user_details) end) + |> Enum.each(fn changeset -> Repo.insert!(changeset) end) + + existing_user = List.first(table) + { + :ok, + state + |> Map.put(:email, existing_user[:email]) + |> Map.put(:password, existing_user[:password]) + } + end + + given_ ~r/^I am logged in$/, fn state -> + navigate_to("/login") + + fill_field({:id, "email"}, state[:email]) + fill_field({:id, "password"}, state[:password]) + + click({:id, "login_button"}) + + assert visible_in_page? ~r"Successfully logged in!" + assert current_path() == "/" + + navigate_to("/users") + + {:ok, state} + end + + when_ ~r/^I click the logout button$/, fn state -> + click({:id, "logout_button"}) + {:ok, state} + end + + then_ ~r/^I should be logged out$/, fn state -> + assert visible_in_page? ~r"You are successfully logged out" + {:ok, state} + end + + and_ ~r/^I should be redirected to the home page$/, fn state -> + assert current_path() == "/" + {:ok, state} + end + + given_ ~r/^I am not logged in$/, fn state -> + navigate_to("/") + {:ok, state} + end + + when_ ~r/^I try to access the logout functionality$/, fn state -> + navigate_to("/logout") + {:ok, state} + end + + then_ ~r/^I should see an error message$/, fn state -> + assert visible_in_page? ~r"You are not logged in" + {:ok, state} + end +end diff --git a/features/user_logout.feature b/features/user_logout.feature new file mode 100644 index 0000000000000000000000000000000000000000..3796f8f391065233b50a01d91f7e105d97852f9a --- /dev/null +++ b/features/user_logout.feature @@ -0,0 +1,16 @@ +Feature: User Logout + Scenario: Authenticated user can logout successfully (AC1, AC2) + Given there exists following accounts + | name | surname | birth_date | phone_number | email | password | confirm_password | + | Existing | Account | 2000-01-01 | 000 | existing.account@gmail.com | password | password | + And I am logged in + When I click the logout button + Then I should be logged out + And I should be redirected to the home page + + + Scenario: Unauthenticated user attempts to logout + Given I am not logged in + When I try to access the logout functionality + Then I should see an error message + And I should be redirected to the home page \ No newline at end of file diff --git a/lib/proptrackr_web/components/layouts/app.html.heex b/lib/proptrackr_web/components/layouts/app.html.heex index b8f00879033e0c16d7e5b62d67004fe9e9b51389..a25cfce2ce2a903988874c76b07ace2f2f996508 100644 --- a/lib/proptrackr_web/components/layouts/app.html.heex +++ b/lib/proptrackr_web/components/layouts/app.html.heex @@ -1,16 +1,21 @@ <header class="header"> <ol class="breadcrumb pull-right"> <%= if @conn.assigns.current_user do %> - <li>Hello <%= @conn.assigns.current_user.name %> </li> + <li>Hello <%= @conn.assigns.current_user.name %></li> + <button + id="logout_button" + phx-click={JS.navigate("/logout")} + class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700" + > + <.icon name="hero-arrow-left-solid" class="h-3 w-3" /> Logout + </button> <% else %> - <% end %> </ol> <span class="logo"></span> </header> + + <main class="px-4 py-20 sm:px-6 lg:px-8"> - <div class="mx-auto max-w-2xl"> - <.flash_group flash={@flash} /> - <%= @inner_content %> - </div> -</main> \ No newline at end of file + <div class="mx-auto max-w-2xl"><.flash_group flash={@flash} /> <%= @inner_content %></div> +</main> diff --git a/lib/proptrackr_web/controllers/login_controller.ex b/lib/proptrackr_web/controllers/login_controller.ex index a1d39b566502c2125347cb12c2edcc442f851b30..6f2fd28afbcc7c71383d43f242e5d7834651f7f5 100644 --- a/lib/proptrackr_web/controllers/login_controller.ex +++ b/lib/proptrackr_web/controllers/login_controller.ex @@ -3,10 +3,6 @@ defmodule PropTrackrWeb.LoginController do import PropTrackr.Authentication - defmodule LoginProps do - defstruct [:email, :password] - end - def index(conn, _params) do current_user = get_session(conn, :user_id) if current_user do diff --git a/lib/proptrackr_web/controllers/logout_controller.ex b/lib/proptrackr_web/controllers/logout_controller.ex new file mode 100644 index 0000000000000000000000000000000000000000..7afb2f64b4bb62a92a23cba8d06176f3c49c9649 --- /dev/null +++ b/lib/proptrackr_web/controllers/logout_controller.ex @@ -0,0 +1,21 @@ +defmodule PropTrackrWeb.LogoutController do + use PropTrackrWeb, :controller + + alias PropTrackr.Authentication + + def index(conn, _params) do + current_user = get_session(conn, :user_id) + + if current_user do + + conn + |> Authentication.logout() + |> put_flash(:info, "You are successfully logged out") + |> redirect(to: "/") + else + conn + |> put_flash(:info, "You are not logged in") + |> redirect(to: "/") + end + end +end diff --git a/lib/proptrackr_web/plugs/authentication.ex b/lib/proptrackr_web/plugs/authentication.ex index 359749752f55cae872af2d54b8ebec9ac677ad96..3b2feb2a6038c05dc3b7d689b0f713c1a1034979 100644 --- a/lib/proptrackr_web/plugs/authentication.ex +++ b/lib/proptrackr_web/plugs/authentication.ex @@ -1,5 +1,6 @@ defmodule PropTrackr.Authentication do import Plug.Conn + import Phoenix.Controller, only: [put_flash: 3] def init(opts) do opts[:repo] @@ -9,7 +10,11 @@ defmodule PropTrackr.Authentication do def call(conn, repo) do user_id = get_session(conn, :user_id) user = user_id && repo.get(PropTrackr.Accounts.User, user_id) - login(conn, user_id, user) + if user do + login(conn, user_id, user) + else + assign(conn, :current_user, nil) + end end def login(conn, user_id, user) do @@ -17,6 +22,12 @@ defmodule PropTrackr.Authentication do |> put_session(:user_id, user_id) end + def logout(conn) do + conn + |> assign(:current_user, nil) + |> delete_session(:user_id) + end + def validate_credentials(conn, email, password, [repo: repo]) do user = repo.get_by(PropTrackr.Accounts.User, email: email) cond do diff --git a/lib/proptrackr_web/router.ex b/lib/proptrackr_web/router.ex index 872edcab7660b48368e634c75cdb688d97af3430..2d2b78158e57b4c4a64b960be4f8d5877bd2eec0 100644 --- a/lib/proptrackr_web/router.ex +++ b/lib/proptrackr_web/router.ex @@ -22,6 +22,7 @@ defmodule PropTrackrWeb.Router do resources "/users", UserController, only: [:index] resources "/register", RegisterController, only: [:index, :create] resources "/login", LoginController, only: [:index, :create] + resources "/logout", LogoutController resources "/me/password", PasswordController, only: [:index, :create] end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index ae6baf57727d0981742afe15c5d0d5fd5f42fbf8..04b23610c2f0ed2e615bcc69f557c7998de736f6 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -1,11 +1,26 @@ -# Script for populating the database. You can run it as: -# -# mix run priv/repo/seeds.exs -# -# Inside the script, you can read and write to any of your -# repositories directly: -# -# PropTrackr.Repo.insert!(%PropTrackr.SomeSchema{}) -# -# We recommend using the bang functions (`insert!`, `update!` -# and so on) as they will fail if something goes wrong. +alias PropTrackr.{Repo, Accounts.User} + +[ + %{ + name: "Shyngys", + surname: "Satkan", + birth_date: "2003-09-04", + phone_number: "+77011234567", + bio: "Software engineer", + email: "shyngys.satkan@example.com", + password: "ssatkan", + confirm_password: "ssatkan" + }, + %{ + name: "Jotaro", + surname: "Kujo", + birth_date: "1984-04-03", + phone_number: "+1234567890", + bio: "Marine biologist and a stand user.", + email: "jotaro.kujo@example.com", + password: "oraoraora", + confirm_password: "oraoraora" + } +] +|> Enum.map(fn user_data -> User.changeset(%User{}, user_data) end) +|> Enum.each(fn changeset -> Repo.insert!(changeset) end) diff --git a/test/proptrackr_web/controllers/logout_controller_test.exs b/test/proptrackr_web/controllers/logout_controller_test.exs new file mode 100644 index 0000000000000000000000000000000000000000..223561f9135b7b7e693c7683be8593b59890e222 --- /dev/null +++ b/test/proptrackr_web/controllers/logout_controller_test.exs @@ -0,0 +1,47 @@ +defmodule PropTrackrWeb.LogoutControllerTest do + use PropTrackrWeb.ConnCase + alias PropTrackr.Accounts.User + alias PropTrackr.Repo + + setup do + user = %User{ + name: "Test", + surname: "User", + birth_date: "2000-01-01", + phone_number: "000", + bio: "Yo", + email: "test.user@gmail.com", + password: "testing", + confirm_password: "testing", + } + Repo.insert!(user) + + :ok + end + + test "Authenticated user should be able to logout and be redirected to the homepage", %{conn: conn} do + # First login the user + conn = conn + |> post("/login", %{"email" => "test.user@gmail.com", "password" => "testing"}) + + # Verify logged in + assert get_session(conn, :user_id) + + # Perform logout + conn = get(conn, "/logout") + assert redirected_to(conn) == "/" + conn = get(conn, redirected_to(conn)) + assert html_response(conn, 200) + assert get_flash(conn, :info) =~ ~r/You are successfully logged out/ + refute get_session(conn, :user_id) + end + + test "Unauthenticated user should be redirected to homepage with message", %{conn: conn} do + conn = get(conn, "/logout") + assert redirected_to(conn) == "/" + conn = get(conn, redirected_to(conn)) + assert html_response(conn, 200) + assert get_flash(conn, :info) =~ ~r/You are not logged in/ + end + +end