diff --git a/features/config.exs b/features/config.exs index 10fb29a1eb14eb46974f4dc3c00088bba806b1c7..a75437b245684d42990238f4d28456c080a2d360 100644 --- a/features/config.exs +++ b/features/config.exs @@ -3,9 +3,13 @@ defmodule WhiteBreadConfig do suite name: "User Registration Features", context: UserRegistrationContext, - feature_paths: ["features/"] + feature_paths: ["features/user_registration.feature"] suite name: "User Login Features", context: UserLoginContext, - feature_paths: ["features/"] + feature_paths: ["features/user_login.feature"] + + suite name: "Password Change Features", + context: PasswordChangeContext, + feature_paths: ["features/password_change.feature"] end diff --git a/features/contexts/password_change_context.exs b/features/contexts/password_change_context.exs new file mode 100644 index 0000000000000000000000000000000000000000..1d6100479797b8d504275193031f9432e7b2d807 --- /dev/null +++ b/features/contexts/password_change_context.exs @@ -0,0 +1,118 @@ +defmodule PasswordChangeContext do + use WhiteBread.Context + use Hound.Helpers + alias PropTrackr.Accounts + alias PropTrackr.Repo + alias PropTrackr.Accounts.User + import PropTrackr.Authentication + + 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) + + new_password = "new password cool" + + { + :ok, + state + |> Map.put(:email, existing_user[:email]) + |> Map.put(:password, existing_user[:password]) + |> Map.put(:new_password, new_password) + } + end + + and_ ~r/^I am logged in$/, fn state -> + setup_session(state[:email], state[:password]) + {:ok, state} + end + + and_ ~r/^I want to change my password$/, fn state -> + navigate_to("/me/password") + {:ok, state} + end + + and_ ~r/^I enter my current password$/, fn state -> + fill_field({:id, "current_password"}, state[:password]) + {:ok, state} + end + + and_ ~r/^I enter my current password invalid$/, fn state -> + fill_field({:id, "current_password"}, "obviously_invalid") + {:ok, state} + end + + and_ ~r/^I enter my new password and confirm it$/, fn state -> + fill_field({:id, "new_password"}, state[:new_password]) + fill_field({:id, "new_password_confirmation"}, state[:new_password]) + {:ok, state} + end + + and_ ~r/^I enter my new password and confirm it with a password that does not match$/, fn state -> + fill_field({:id, "new_password"}, state[:new_password]) + fill_field({:id, "new_password_confirmation"}, "obviously_invalid") + {:ok, state} + end + + and_ ~r/^I enter my new password and confirm it that match the current password$/, fn state -> + fill_field({:id, "new_password"}, state[:password]) + fill_field({:id, "new_password_confirmation"}, state[:password]) + {:ok, state} + end + + when_ ~r/^I click change password$/, fn state -> + click({:id, "change_password"}) + {:ok, state} + end + + then_ ~r/^I should receive a success notification$/, fn state -> + assert visible_in_page? ~r"Password changed successfully! Please log in again" + {:ok, state} + end + + then_ ~r/^I should receive an error message stating invalid current password$/, fn state -> + assert visible_in_page? ~r"Invalid current password!" + {:ok, state} + end + + then_ ~r/^I should receive an error message stating that the new passwords don't match$/, fn state -> + assert visible_in_page? ~r"New passwords don't match!" + {:ok, state} + end + + then_ ~r/^I should receive an error message stating that the new password is the same as the old password$/, fn state -> + assert visible_in_page? ~r"New password can't be the same as the old password!" + {:ok, state} + end + + and_ ~r/^I should be shown the change password form again$/, fn state -> + assert visible_in_page? ~r"Change your password" + {:ok, state} + end + + and_ ~r/^I should be redirected to the login page$/, fn state -> + assert current_path() == "/login" + {:ok, state} + end + + defp setup_session(email, password) do + navigate_to("/login") + fill_field({:id, "email"}, email) + fill_field({:id, "password"}, password) + click({:id, "login_button"}) + end +end diff --git a/features/password_change.feature b/features/password_change.feature new file mode 100644 index 0000000000000000000000000000000000000000..1d4a3054362fb59e3f904746d06c4d0bf9943234 --- /dev/null +++ b/features/password_change.feature @@ -0,0 +1,49 @@ +Feature: Password Change + + Scenario: Authenticated user can change their password + 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 + And I want to change my password + And I enter my current password + And I enter my new password and confirm it + When I click change password + Then I should receive a success notification + And I should be redirected to the login page + + Scenario: Authenticated user cannot change their password when invalid current password + 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 + And I want to change my password + And I enter my current password invalid + And I enter my new password and confirm it + When I click change password + Then I should receive an error message stating invalid current password + And I should be shown the change password form again + + Scenario: Authenticated user cannot change their password when new passwords don't match + 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 + And I want to change my password + And I enter my current password + And I enter my new password and confirm it with a password that does not match + When I click change password + Then I should receive an error message stating that the new passwords don't match + And I should be shown the change password form again + + Scenario: Authenticated user cannot change their password when new password is the same as the old password + 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 + And I want to change my password + And I enter my current password + And I enter my new password and confirm it that match the current password + When I click change password + Then I should receive an error message stating that the new password is the same as the old password + And I should be shown the change password form again diff --git a/lib/proptrackr_web/controllers/password_controller.ex b/lib/proptrackr_web/controllers/password_controller.ex new file mode 100644 index 0000000000000000000000000000000000000000..4bfcb299b72fba33aa8b431f14406146fcf1ebee --- /dev/null +++ b/lib/proptrackr_web/controllers/password_controller.ex @@ -0,0 +1,49 @@ +defmodule PropTrackrWeb.PasswordController do + use PropTrackrWeb, :controller + + alias PropTrackr.Repo + alias PropTrackr.Accounts.User + import Ecto.Query, only: [from: 2] + + def index(conn, _params) do + render conn, "index.html" + end + + def create(conn, %{"current_password" => current_password, "new_password" => new_password, "new_password_confirmation" => new_password_confirmation}) do + cond do + new_password != new_password_confirmation -> + conn + |> put_flash(:error, "New passwords don't match!") + |> render("index.html") + # TODO: Insecure, needs refactoring when taking security into account + new_password == current_password -> + conn + |> put_flash(:error, "New password can't be the same as the old password!") + |> render("index.html") + true -> + user_id = get_session(conn, :user_id) + user = Repo.get(User, user_id) + + if user.password == current_password do + {row_count, _} = from(u in User, where: u.id == ^user_id, select: u) + |> Repo.update_all(set: [password: new_password]) + + if row_count == 1 do + conn + |> delete_session(:user_id) + |> put_flash(:info, "Password changed successfully! Please log in again") + |> redirect(to: ~p"/login") + else + # Note: Adding this just in case the update fails for some odd reason + conn + |> put_flash(:error, "Unknown error has occurred!") + |> redirect(to: ~p"/") + end + else + conn + |> put_flash(:error, "Invalid current password!") + |> render("index.html") + end + end + end +end diff --git a/lib/proptrackr_web/controllers/password_html.ex b/lib/proptrackr_web/controllers/password_html.ex new file mode 100644 index 0000000000000000000000000000000000000000..eb00040f876d7933c46cc9a8447ec99004c7a2bd --- /dev/null +++ b/lib/proptrackr_web/controllers/password_html.ex @@ -0,0 +1,5 @@ +defmodule PropTrackrWeb.PasswordHTML do + use PropTrackrWeb, :html + + embed_templates "password_html/*" +end diff --git a/lib/proptrackr_web/controllers/password_html/index.html.heex b/lib/proptrackr_web/controllers/password_html/index.html.heex new file mode 100644 index 0000000000000000000000000000000000000000..2f24942621f38bdbf8577bfa105cf377350e4636 --- /dev/null +++ b/lib/proptrackr_web/controllers/password_html/index.html.heex @@ -0,0 +1,15 @@ +<.header> + Change your password +</.header> + +<.simple_form :let={f} for={} action={~p"/me/password"}> + <.input field={f[:current_password]} type="password" required label="Current password" /> + <.input field={f[:new_password]} type="password" required label="New password" /> + <.input field={f[:new_password_confirmation]} type="password" required label="Confirm password" /> + <:actions> + <.button id="change_password">Change password</.button> + </:actions> +</.simple_form> + + +<.back navigate={~p"/"}>Changed your mind?</.back> diff --git a/lib/proptrackr_web/router.ex b/lib/proptrackr_web/router.ex index 768dd5e0ffb0262c8a74885241658a8214001010..872edcab7660b48368e634c75cdb688d97af3430 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 "/me/password", PasswordController, only: [:index, :create] end # Other scopes may use custom stacks. diff --git a/test/proptrackr_web/controllers/password_controller_test.exs b/test/proptrackr_web/controllers/password_controller_test.exs new file mode 100644 index 0000000000000000000000000000000000000000..ae23a4cdf996f5c7aaba8e0983dcdba7ccf1ae2a --- /dev/null +++ b/test/proptrackr_web/controllers/password_controller_test.exs @@ -0,0 +1,79 @@ +defmodule PropTrackrWeb.PasswordControllerTest do + use PropTrackrWeb.ConnCase + alias PropTrackr.Accounts.User + alias PropTrackr.Repo + + @user_credentials %{email: "test.user@gmail.com", password: "testing", new_password: "new_password"} + + 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", + } + user = Repo.insert!(user) + + {:ok, %{user: user}} + end + + test "Authenticated user should be able to change their password with correct current credentials and matching new passwords", %{conn: conn, user: user} do + conn = conn |> setup_session(user) + conn = conn + |> post("/me/password", + current_password: @user_credentials[:password], + new_password: @user_credentials[:new_password], + new_password_confirmation: @user_credentials[:new_password]) + assert redirected_to(conn) == "/login" + conn = get conn, redirected_to(conn) + assert html_response(conn, 200) + assert get_flash(conn, :info) =~ ~r/Password changed successfully! Please log in again/ + assert get_session(conn, :user_id) == nil + + updated_user = Repo.get(User, user.id) + assert updated_user.password == @user_credentials[:new_password] + end + + test "Authenticated user should receive an error if the current password is invalid", %{conn: conn, user: user} do + conn = conn |> setup_session(user) + conn = conn + |> post("/me/password", + current_password: "obviously_invalid", + new_password: @user_credentials[:new_password], + new_password_confirmation: @user_credentials[:new_password]) + assert html_response(conn, 200) + assert get_flash(conn, :error) =~ ~r/Invalid current password!/ + end + + test "Authenticated user should receive an error if the new password does not match the new password confirmation", %{conn: conn, user: user} do + conn = conn |> setup_session(user) + conn = conn + |> post("/me/password", + current_password: @user_credentials[:password], + new_password: @user_credentials[:new_password], + new_password_confirmation: "obviously_does_not_match") + assert html_response(conn, 200) + assert get_flash(conn, :error) =~ ~r/New passwords don't match!/ + end + + test "Authenticated user should receive an error if the new password is the same as the old password", %{conn: conn, user: user} do + conn = conn |> setup_session(user) + conn = conn + |> post("/me/password", + current_password: @user_credentials[:password], + new_password: @user_credentials[:password], + new_password_confirmation: @user_credentials[:password]) + assert html_response(conn, 200) + assert get_flash(conn, :error) =~ ~r/New password can't be the same as the old password!/ + end + + defp setup_session(conn, user) do + conn = conn |> post("/login", email: user.email, password: user.password) + conn = get conn, redirected_to(conn) + conn + end +end