diff --git a/config/test.exs b/config/test.exs index 2cc85f0555c4e261f6ab86f25af47ef2b19d1be3..922b66cdc16944d8fcaa788d116ab0eda83181f9 100644 --- a/config/test.exs +++ b/config/test.exs @@ -38,4 +38,4 @@ config :phoenix_live_view, config :hound, driver: "chrome_driver", port: 9999 -config :takso, sql_sandbox: true +config :proptrackr, sql_sandbox: true diff --git a/features/accessing_root.feature b/features/accessing_root.feature deleted file mode 100644 index af39a29910f5ae97de2b28dedd9ac0f766435f26..0000000000000000000000000000000000000000 --- a/features/accessing_root.feature +++ /dev/null @@ -1,8 +0,0 @@ -Feature: Homepage Access - As a website visitor - Such that I can see the website is working - I want to access the homepage - - Scenario: Visiting the homepage - When I visit the homepage - Then I should see the welcome message diff --git a/features/config.exs b/features/config.exs index 87d61ea395e82845327122b0087fa19d7a810489..3f4b67c3ddaf3a2d8d7c7c797c2fc6225944c36d 100644 --- a/features/config.exs +++ b/features/config.exs @@ -1,7 +1,7 @@ defmodule WhiteBreadConfig do use WhiteBread.SuiteConfiguration - suite name: "All", - context: WhiteBreadContext, + suite name: "User Registration Features", + context: UserRegistrationContext, feature_paths: ["features/"] end diff --git a/features/contexts/user_registration_context.exs b/features/contexts/user_registration_context.exs new file mode 100644 index 0000000000000000000000000000000000000000..8690d42106cbeea6c095bdba66a1139cef96b25d --- /dev/null +++ b/features/contexts/user_registration_context.exs @@ -0,0 +1,87 @@ +defmodule UserRegistrationContext do + use WhiteBread.Context + use Hound.Helpers + alias PropTrackr.Accounts + alias PropTrackr.Repo + alias PropTrackr.Accounts.User + + feature_starting_state fn -> + Application.ensure_all_started(:hound) + %{state: "initialized"} + end + + scenario_starting_state fn _state -> + Ecto.Adapters.SQL.Sandbox.checkout(PropTrackr.Repo) + Ecto.Adapters.SQL.Sandbox.mode(PropTrackr.Repo, {:shared, self()}) + Hound.start_session() + %{state: "session_started"} + end + + scenario_finalize fn _status, _state -> + Ecto.Adapters.SQL.Sandbox.checkin(PropTrackr.Repo) + Hound.end_session() + end + + given_ ~r/^I am on the registration page$/, fn state -> + navigate_to("/register") + {:ok, state} + end + + when_ ~r/^I fill in the registration form with valid details$/, fn state -> + fill_registration_form( + "valid1@example.com", + "Test", + "User", + "12.12.12", + "1234567890", + "test123", + "test123" + ) + {:ok, state} + end + + and_ ~r/^I submit the form$/, fn state -> + click({:id, "register_button"}) + {:ok, state} + end + + then_ ~r/^I should see User created successfully.$/, fn state -> + assert visible_in_page? ~r/User created successfully./ + {:ok, state} + end + + and_ ~r/^I should be redirected to the homepage$/, fn state -> + assert current_url() == "http://localhost:4001/users" + {:ok, state} + end + + when_ ~r/^I fill in the registration form with mismatched passwords$/, fn state -> + fill_registration_form( + "mismatch_user@example.com", + "Mismatch", + "User", + "12.12.12", + "1122334455", + "test123", + "different123" + ) + {:ok, state} + end + + then_ ~r/^I should see an error message Passwords do not match$/, fn state -> + assert visible_in_page? ~r/Passwords do not match/ + {:ok, state} + end + + # Helper function to fill out the registration form with 7 fields + defp fill_registration_form(email, name, surname, birth_date, phone_number, password, confirm_password) do + fill_field({:name, "user[email]"}, email) + fill_field({:name, "user[name]"}, name) + fill_field({:name, "user[surname]"}, surname) + fill_field({:name, "user[birth_date]"}, birth_date) + fill_field({:name, "user[phone_number]"}, phone_number) + fill_field({:name, "user[password]"}, password) + fill_field({:name, "user[confirm_password]"}, confirm_password) + end + +end diff --git a/features/contexts/white_bread_context.exs b/features/contexts/white_bread_context.exs deleted file mode 100644 index fb83546020faa5d627a25fd381b34aff79b50260..0000000000000000000000000000000000000000 --- a/features/contexts/white_bread_context.exs +++ /dev/null @@ -1,28 +0,0 @@ -defmodule WhiteBreadContext do - use WhiteBread.Context - use Hound.Helpers - - feature_starting_state fn -> - Application.ensure_all_started(:hound) - %{} - end - - scenario_starting_state fn _state -> - Hound.start_session - %{} - end - - scenario_finalize fn _status, _state -> - Hound.end_session - end - - when_ ~r/^I visit the homepage$/, fn state -> - navigate_to "/" - {:ok, state} - end - - then_ ~r/^I should see the welcome message$/, fn state -> - assert visible_in_page? ~r/Peace of mind from prototype to production./ - {:ok, state} - end -end diff --git a/features/user_registration.feature b/features/user_registration.feature new file mode 100644 index 0000000000000000000000000000000000000000..802582fe92d80b5855e82be69d28d385e4ca0a4a --- /dev/null +++ b/features/user_registration.feature @@ -0,0 +1,14 @@ +Feature: User Registration + + Scenario: Unauthenticated user registers successfully (AC1, AC5, AC6, AC7) + Given I am on the registration page + When I fill in the registration form with valid details + And I submit the form + Then I should see User created successfully. + And I should be redirected to the homepage + + Scenario: User confirms password to prevent typos (AC2, AC3) + Given I am on the registration page + When I fill in the registration form with mismatched passwords + And I submit the form + Then I should see an error message Passwords do not match \ No newline at end of file diff --git a/lib/proptrackr/accounts/user.ex b/lib/proptrackr/accounts/user.ex new file mode 100644 index 0000000000000000000000000000000000000000..03aff24ca47289d48814a95ad1d5b6d6262233e8 --- /dev/null +++ b/lib/proptrackr/accounts/user.ex @@ -0,0 +1,49 @@ +defmodule PropTrackr.Accounts.User do + use Ecto.Schema + import Ecto.Changeset + + alias PropTrackr.Repo + alias PropTrackr.Accounts.User + + schema "users" do + field :name, :string + field :surname, :string + field :birth_date, :string + field :phone_number, :string + field :bio, :string + field :email, :string + field :password, :string + field :confirm_password, :string, virtual: true # Define confirm_password as virtual, not includes in database + + timestamps() + end + + def changeset(struct, params \\ %{}) do + struct + |> cast(params, [:name, :surname, :birth_date, :phone_number, :bio, :email, :password, :confirm_password]) + |> validate_required([:name, :surname, :birth_date, :phone_number, :email, :password, :confirm_password]) + |> validate_password_confirmation(:password, :confirm_password) + |> validate_unique_email() + end + + defp validate_password_confirmation(changeset, field_1, field_2) do + if get_field(changeset, field_1) != get_field(changeset, field_2) do + changeset + |> add_error(field_2, "Passwords do not match.", [other: field_1]) + else + changeset + end + end + + defp validate_unique_email(changeset) do + email = get_field(changeset, :email) + + # Only check for uniqueness if email is not nil + if email && Repo.get_by(User, email: email) do + add_error(changeset, :email, "An account with this email already exists.") + else + changeset + end + end + +end diff --git a/lib/proptrackr_web/components/layouts/app.html.heex b/lib/proptrackr_web/components/layouts/app.html.heex index e23bfc81c45ada5698e427236155c6435cd6734e..b8f00879033e0c16d7e5b62d67004fe9e9b51389 100644 --- a/lib/proptrackr_web/components/layouts/app.html.heex +++ b/lib/proptrackr_web/components/layouts/app.html.heex @@ -1,32 +1,16 @@ -<header class="px-4 sm:px-6 lg:px-8"> - <div class="flex items-center justify-between border-b border-zinc-100 py-3 text-sm"> - <div class="flex items-center gap-4"> - <a href="/"> - <img src={~p"/images/logo.svg"} width="36" /> - </a> - <p class="bg-brand/5 text-brand rounded-full px-2 font-medium leading-6"> - v<%= Application.spec(:phoenix, :vsn) %> - </p> - </div> - <div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900"> - <a href="https://twitter.com/elixirphoenix" class="hover:text-zinc-700"> - @elixirphoenix - </a> - <a href="https://github.com/phoenixframework/phoenix" class="hover:text-zinc-700"> - GitHub - </a> - <a - href="https://hexdocs.pm/phoenix/overview.html" - class="rounded-lg bg-zinc-100 px-2 py-1 hover:bg-zinc-200/80" - > - Get Started <span aria-hidden="true">→</span> - </a> - </div> - </div> +<header class="header"> + <ol class="breadcrumb pull-right"> + <%= if @conn.assigns.current_user do %> + <li>Hello <%= @conn.assigns.current_user.name %> </li> + <% 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> +</main> \ No newline at end of file diff --git a/lib/proptrackr_web/controllers/register_controller.ex b/lib/proptrackr_web/controllers/register_controller.ex new file mode 100644 index 0000000000000000000000000000000000000000..4566c838a12e131b2384eef975093d3d31182241 --- /dev/null +++ b/lib/proptrackr_web/controllers/register_controller.ex @@ -0,0 +1,25 @@ +defmodule PropTrackrWeb.RegisterController do + use PropTrackrWeb, :controller + + import PropTrackr.Authentication + alias PropTrackr.Repo + alias PropTrackr.Accounts.User + + def index(conn, _params) do + changeset = User.changeset(%User{}, %{}) + render(conn, "index.html", changeset: changeset) + end + + def create(conn, %{"user" => user_params}) do + changeset = User.changeset(%User{}, user_params) + case Repo.insert(changeset) do + {:ok, user} -> + conn + |> put_flash(:info, "User created successfully.") + |> login(user.id, user) #Logins if succesfully created + |> redirect(to: ~p"/users") + {:error, %Ecto.Changeset{} = changeset} -> + render(conn, "index.html", changeset: changeset) + end + end +end diff --git a/lib/proptrackr_web/controllers/register_html.ex b/lib/proptrackr_web/controllers/register_html.ex new file mode 100644 index 0000000000000000000000000000000000000000..bcb7ed4750f4a6562713e760488196951f76cb0d --- /dev/null +++ b/lib/proptrackr_web/controllers/register_html.ex @@ -0,0 +1,5 @@ +defmodule PropTrackrWeb.RegisterHTML do + use PropTrackrWeb, :html + + embed_templates "register_html/*" +end diff --git a/lib/proptrackr_web/controllers/register_html/index.html.heex b/lib/proptrackr_web/controllers/register_html/index.html.heex new file mode 100644 index 0000000000000000000000000000000000000000..dacc5cea8eddb341ba94d9d5ea9971a2612395a2 --- /dev/null +++ b/lib/proptrackr_web/controllers/register_html/index.html.heex @@ -0,0 +1,23 @@ +<.header> + New User +</.header> + +<.simple_form :let={f} for={@changeset} action={~p"/register"}> + <.error :if={@changeset.action}> + Oops, something went wrong! Please check the errors below. + </.error> + <.input field={f[:name]} type="text" label="Name" /> + <.input field={f[:surname]} type="text" label="Surname" /> + <.input field={f[:birth_date]} type="text" label="Birth date" /> + <.input field={f[:phone_number]} type="text" label="Phone number" /> + <.input field={f[:bio]} type="text" label="Bio" /> + <.input field={f[:email]} type="text" label="Email" /> + <.input field={f[:password]} type="password" label="Password" /> + <.input field={f[:confirm_password]} type="password" label="Confirm password" /> + <:actions> + <.button id="register_button">Register</.button> + </:actions> +</.simple_form> + +<.back navigate={~p"/users"}>Already have account</.back> + diff --git a/lib/proptrackr_web/controllers/user_controller.ex b/lib/proptrackr_web/controllers/user_controller.ex new file mode 100644 index 0000000000000000000000000000000000000000..630a023ce9fa63b7b0af06446344d60fecbe16a8 --- /dev/null +++ b/lib/proptrackr_web/controllers/user_controller.ex @@ -0,0 +1,13 @@ +defmodule PropTrackrWeb.UserController do + use PropTrackrWeb, :controller + + import PropTrackr.Authentication + alias PropTrackr.Repo + alias PropTrackr.Accounts.User + + def index(conn, _params) do + users = Repo.all(User) + render(conn, "index.html", users: users) + end + +end diff --git a/lib/proptrackr_web/controllers/user_html.ex b/lib/proptrackr_web/controllers/user_html.ex new file mode 100644 index 0000000000000000000000000000000000000000..4bb645e281e59723bd927184b435846fadb07a8d --- /dev/null +++ b/lib/proptrackr_web/controllers/user_html.ex @@ -0,0 +1,5 @@ +defmodule PropTrackrWeb.UserHTML do + use PropTrackrWeb, :html + + embed_templates "user_html/*" +end diff --git a/lib/proptrackr_web/controllers/user_html/index.html.heex b/lib/proptrackr_web/controllers/user_html/index.html.heex new file mode 100644 index 0000000000000000000000000000000000000000..4127ec297b3aba714632b08994c5f56d1b182d3a --- /dev/null +++ b/lib/proptrackr_web/controllers/user_html/index.html.heex @@ -0,0 +1,20 @@ +<.header> + Listing Users + <:actions> + <.link href={~p"/register"}> + <.button>New User</.button> + </.link> + </:actions> +</.header> + +<.table id="users" rows={@users} > + <:col :let={user} label="Name"><%= user.name %></:col> + <:col :let={user} label="Surname"><%= user.surname %></:col> + <:col :let={user} label="Birth Date"><%= user.birth_date %></:col> + <:col :let={user} label="Phone number"><%= user.phone_number %></:col> + <:col :let={user} label="Bio"><%= user.bio %></:col> + <:col :let={user} label="Email"><%= user.email %></:col> + <:col :let={user} label="Password"><%= user.password %></:col> + + +</.table> \ No newline at end of file diff --git a/lib/proptrackr_web/plugs/authentication.ex b/lib/proptrackr_web/plugs/authentication.ex new file mode 100644 index 0000000000000000000000000000000000000000..37a34e08a68655331fbd4100750cc561c8e37c01 --- /dev/null +++ b/lib/proptrackr_web/plugs/authentication.ex @@ -0,0 +1,21 @@ +defmodule PropTrackr.Authentication do + import Plug.Conn + + def init(opts) do + opts[:repo] + end + + @spec call(Plug.Conn.t(), any()) :: Plug.Conn.t() + 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) + end + + def login(conn, user_id, user) do + assign(conn, :current_user, user) + |> put_session(:user_id, user_id) + end + + +end diff --git a/lib/proptrackr_web/router.ex b/lib/proptrackr_web/router.ex index 76f17a0462d72d9500f711a0ef0ea98a2512c891..98fac51f83ef6cf076d6ff40669a43cc75c60a9b 100644 --- a/lib/proptrackr_web/router.ex +++ b/lib/proptrackr_web/router.ex @@ -8,6 +8,7 @@ defmodule PropTrackrWeb.Router do plug :put_root_layout, html: {PropTrackrWeb.Layouts, :root} plug :protect_from_forgery plug :put_secure_browser_headers + plug PropTrackr.Authentication, repo: PropTrackr.Repo end pipeline :api do @@ -18,6 +19,8 @@ defmodule PropTrackrWeb.Router do pipe_through :browser get "/", PageController, :home + resources "/users", UserController, only: [:index] + resources "/register", RegisterController, only: [:index, :create] end # Other scopes may use custom stacks. diff --git a/priv/repo/migrations/20241106162534_create_users.exs b/priv/repo/migrations/20241106162534_create_users.exs new file mode 100644 index 0000000000000000000000000000000000000000..89e3ec8784433987fdb6be0a4a88e03833d2addc --- /dev/null +++ b/priv/repo/migrations/20241106162534_create_users.exs @@ -0,0 +1,18 @@ +defmodule PropTrackr.Repo.Migrations.CreateUsers do + use Ecto.Migration + + def change do + create table(:users) do + add :name, :string + add :surname, :string + add :birth_date, :string + add :phone_number, :string + add :bio, :string + add :email, :string + add :password, :string + add :confirm_password, :string + + timestamps(type: :utc_datetime) + end + end +end diff --git a/priv/repo/migrations/20241109224607_remove_confirm_password_from_users.exs b/priv/repo/migrations/20241109224607_remove_confirm_password_from_users.exs new file mode 100644 index 0000000000000000000000000000000000000000..3423ad97f3f0545b7fb43e06e71b2e22ab01908b --- /dev/null +++ b/priv/repo/migrations/20241109224607_remove_confirm_password_from_users.exs @@ -0,0 +1,10 @@ +defmodule PropTrackr.Repo.Migrations.RemoveConfirmPasswordFromUsers do + use Ecto.Migration + + def change do + alter table(:users) do + remove :confirm_password + end + end + +end diff --git a/test/proptrackr_web/controllers/user_controller_test.exs b/test/proptrackr_web/controllers/user_controller_test.exs new file mode 100644 index 0000000000000000000000000000000000000000..dc5804c862f5a35299800b5156124e66bca5f941 --- /dev/null +++ b/test/proptrackr_web/controllers/user_controller_test.exs @@ -0,0 +1,44 @@ +defmodule PropTrackrWeb.UserControllerTest do + use PropTrackrWeb.ConnCase + alias PropTrackr.Accounts.User + alias PropTrackr.Repo + + setup do + {:ok, user_params: %{ + name: "Test User", + surname: "Doe", + birth_date: "1990-01-01", + phone_number: "123-456-7890", + bio: "This is a test bio.", + email: "new_user@example.com", + password: "test123", + confirm_password: "test123" + }} + end + + test "AC1: unauthenticated user can register an account", %{conn: conn, user_params: user_params} do + conn = post(conn, "/register", user: user_params) + assert html_response(conn, 302) + assert get_flash(conn, :info) == "User created successfully." + assert redirected_to(conn) == "/users" + end + + test "AC2 and AC3: user sees an error when passwords do not match", %{conn: conn, user_params: user_params} do + mismatched_params = Map.put(user_params, :confirm_password, "different_password") + conn = post(conn, "/register", user: mismatched_params) + assert html_response(conn, 200) =~ "Passwords do not match." + end + + test "AC4: user sees an error if an account with the same email exists", %{conn: conn, user_params: user_params} do + Repo.insert!(%User{email: user_params[:email], password: "test123"}) + conn = post(conn, "/register", user: user_params) + assert html_response(conn, 200) =~ "An account with this email already exists" + end + + test "AC5, AC6, AC7: user is logged in, redirected, and shown notification upon successful registration", %{conn: conn, user_params: user_params} do + conn = post(conn, "/register", user: user_params) + assert get_flash(conn, :info) == "User created successfully." + assert redirected_to(conn) == "/users" + assert get_session(conn, :user_id) + end +end