diff --git a/lib/simpleshop_theme_web/components/layouts/admin.html.heex b/lib/simpleshop_theme_web/components/layouts/admin.html.heex index f59a5eb..c4230db 100644 --- a/lib/simpleshop_theme_web/components/layouts/admin.html.heex +++ b/lib/simpleshop_theme_web/components/layouts/admin.html.heex @@ -61,14 +61,6 @@ <.icon name="hero-paint-brush" class="size-5" /> Theme -
  • - <.link - navigate={~p"/admin/providers"} - class={admin_nav_active?(@current_path, "/admin/providers")} - > - <.icon name="hero-cube" class="size-5" /> Providers - -
  • <.link navigate={~p"/admin/settings"} diff --git a/lib/simpleshop_theme_web/controllers/user_session_controller.ex b/lib/simpleshop_theme_web/controllers/user_session_controller.ex index 44507ed..ee0177f 100644 --- a/lib/simpleshop_theme_web/controllers/user_session_controller.ex +++ b/lib/simpleshop_theme_web/controllers/user_session_controller.ex @@ -55,7 +55,7 @@ defmodule SimpleshopThemeWeb.UserSessionController do UserAuth.disconnect_sessions(expired_tokens) conn - |> put_session(:user_return_to, ~p"/users/settings") + |> put_session(:user_return_to, ~p"/admin/settings") |> create(params, "Password updated successfully!") end diff --git a/lib/simpleshop_theme_web/live/admin/providers/form.ex b/lib/simpleshop_theme_web/live/admin/providers/form.ex index 1ed922b..626b24e 100644 --- a/lib/simpleshop_theme_web/live/admin/providers/form.ex +++ b/lib/simpleshop_theme_web/live/admin/providers/form.ex @@ -83,7 +83,7 @@ defmodule SimpleshopThemeWeb.Admin.Providers.Form do {:noreply, socket |> put_flash(:info, "Connected to Printify!") - |> push_navigate(to: ~p"/admin/providers")} + |> push_navigate(to: ~p"/admin/settings")} {:error, %Ecto.Changeset{} = changeset} -> {:noreply, assign(socket, form: to_form(changeset))} @@ -96,7 +96,7 @@ defmodule SimpleshopThemeWeb.Admin.Providers.Form do {:noreply, socket |> put_flash(:info, "Settings saved") - |> push_navigate(to: ~p"/admin/providers")} + |> push_navigate(to: ~p"/admin/settings")} {:error, %Ecto.Changeset{} = changeset} -> {:noreply, assign(socket, form: to_form(changeset))} diff --git a/lib/simpleshop_theme_web/live/admin/settings.ex b/lib/simpleshop_theme_web/live/admin/settings.ex index 6b37012..1d320b4 100644 --- a/lib/simpleshop_theme_web/live/admin/settings.ex +++ b/lib/simpleshop_theme_web/live/admin/settings.ex @@ -1,18 +1,26 @@ defmodule SimpleshopThemeWeb.Admin.Settings do use SimpleshopThemeWeb, :live_view + alias SimpleshopTheme.Accounts + alias SimpleshopTheme.Products alias SimpleshopTheme.Settings alias SimpleshopTheme.Stripe.Setup, as: StripeSetup @impl true def mount(_params, _session, socket) do + user = socket.assigns.current_scope.user + {:ok, socket |> assign(:page_title, "Settings") |> assign(:site_live, Settings.site_live?()) - |> assign_stripe_state()} + |> assign_stripe_state() + |> assign_products_state() + |> assign_account_state(user)} end + # -- Stripe assigns -- + defp assign_stripe_state(socket) do has_key = Settings.has_secret?("stripe_api_key") has_signing = Settings.has_secret?("stripe_signing_secret") @@ -32,11 +40,58 @@ defmodule SimpleshopThemeWeb.Admin.Settings do |> assign(:stripe_has_signing_secret, has_signing) |> assign(:connect_form, to_form(%{"api_key" => ""}, as: :stripe)) |> assign(:secret_form, to_form(%{"signing_secret" => ""}, as: :webhook)) - |> assign(:advanced_open, false) + |> assign(:stripe_advanced_open, false) |> assign(:connecting, false) end + # -- Products assigns -- + + defp assign_products_state(socket) do + connections = Products.list_provider_connections() + + connection_info = + case connections do + [conn | _] -> + product_count = Products.count_products_for_connection(conn.id) + %{connection: conn, product_count: product_count} + + [] -> + nil + end + + assign(socket, :printify, connection_info) + end + + # -- Account assigns -- + + defp assign_account_state(socket, user) do + email_changeset = Accounts.change_user_email(user, %{}, validate_unique: false) + password_changeset = Accounts.change_user_password(user, %{}, hash_password: false) + + socket + |> assign(:current_email, user.email) + |> assign(:email_form, to_form(email_changeset)) + |> assign(:password_form, to_form(password_changeset)) + |> assign(:trigger_submit, false) + end + + # -- Events: shop status -- + @impl true + def handle_event("toggle_site_live", _params, socket) do + new_value = !socket.assigns.site_live + {:ok, _} = Settings.set_site_live(new_value) + + message = if new_value, do: "Shop is now live", else: "Shop taken offline" + + {:noreply, + socket + |> assign(:site_live, new_value) + |> put_flash(:info, message)} + end + + # -- Events: Stripe -- + def handle_event("connect_stripe", %{"stripe" => %{"api_key" => api_key}}, socket) do if api_key == "" do {:noreply, put_flash(socket, :error, "Please enter your Stripe secret key")} @@ -100,42 +155,124 @@ defmodule SimpleshopThemeWeb.Admin.Settings do end end - def handle_event("toggle_site_live", _params, socket) do - new_value = !socket.assigns.site_live - {:ok, _} = Settings.set_site_live(new_value) + def handle_event("toggle_stripe_advanced", _params, socket) do + {:noreply, assign(socket, :stripe_advanced_open, !socket.assigns.stripe_advanced_open)} + end - message = if new_value, do: "Shop is now live", else: "Shop taken offline" + # -- Events: products -- + + def handle_event("sync", %{"id" => id}, socket) do + connection = Products.get_provider_connection!(id) + + case Products.enqueue_sync(connection) do + {:ok, _job} -> + {:noreply, + socket + |> assign_products_state() + |> put_flash(:info, "Sync started for #{connection.name}")} + + {:error, _reason} -> + {:noreply, put_flash(socket, :error, "Failed to start sync")} + end + end + + def handle_event("delete_connection", %{"id" => id}, socket) do + connection = Products.get_provider_connection!(id) + {:ok, _} = Products.delete_provider_connection(connection) {:noreply, socket - |> assign(:site_live, new_value) - |> put_flash(:info, message)} + |> assign_products_state() + |> put_flash(:info, "Provider connection deleted")} end - def handle_event("toggle_advanced", _params, socket) do - {:noreply, assign(socket, :advanced_open, !socket.assigns.advanced_open)} + # -- Events: account -- + + def handle_event("validate_email", %{"user" => user_params}, socket) do + email_form = + socket.assigns.current_scope.user + |> Accounts.change_user_email(user_params, validate_unique: false) + |> Map.put(:action, :validate) + |> to_form() + + {:noreply, assign(socket, email_form: email_form)} end + def handle_event("update_email", %{"user" => user_params}, socket) do + user = socket.assigns.current_scope.user + + unless Accounts.sudo_mode?(user) do + {:noreply, + socket + |> put_flash(:error, "Please log in again to change account settings.") + |> redirect(to: ~p"/users/log-in")} + else + case Accounts.change_user_email(user, user_params) do + %{valid?: true} = changeset -> + Accounts.deliver_user_update_email_instructions( + Ecto.Changeset.apply_action!(changeset, :insert), + user.email, + &url(~p"/users/settings/confirm-email/#{&1}") + ) + + info = "A link to confirm your email change has been sent to the new address." + {:noreply, put_flash(socket, :info, info)} + + changeset -> + {:noreply, assign(socket, :email_form, to_form(changeset, action: :insert))} + end + end + end + + def handle_event("validate_password", %{"user" => user_params}, socket) do + password_form = + socket.assigns.current_scope.user + |> Accounts.change_user_password(user_params, hash_password: false) + |> Map.put(:action, :validate) + |> to_form() + + {:noreply, assign(socket, password_form: password_form)} + end + + def handle_event("update_password", %{"user" => user_params}, socket) do + user = socket.assigns.current_scope.user + + unless Accounts.sudo_mode?(user) do + {:noreply, + socket + |> put_flash(:error, "Please log in again to change account settings.") + |> redirect(to: ~p"/users/log-in")} + else + case Accounts.change_user_password(user, user_params) do + %{valid?: true} = changeset -> + {:noreply, assign(socket, trigger_submit: true, password_form: to_form(changeset))} + + changeset -> + {:noreply, assign(socket, password_form: to_form(changeset, action: :insert))} + end + end + end + + # -- Render -- + @impl true def render(assigns) do ~H"""
    <.header> Settings - <:subtitle>Shop status, payment providers, and API keys + <%!-- Shop status --%>

    Shop status

    <%= if @site_live do %> - + <.status_pill color="green"> <.icon name="hero-check-circle-mini" class="size-3" /> Live - + <% else %> - - Offline - + <.status_pill color="zinc">Offline <% end %>

    @@ -165,22 +302,21 @@ defmodule SimpleshopThemeWeb.Admin.Settings do

    + <%!-- Payments --%>
    -

    Stripe

    +

    Payments

    <%= case @stripe_status do %> <% :connected -> %> - + <.status_pill color="green"> <.icon name="hero-check-circle-mini" class="size-3" /> Connected - + <% :connected_localhost -> %> - + <.status_pill color="amber"> <.icon name="hero-exclamation-triangle-mini" class="size-3" /> Dev mode - + <% :not_configured -> %> - - Not connected - + <.status_pill color="zinc">Not connected <% end %>
    @@ -194,10 +330,213 @@ defmodule SimpleshopThemeWeb.Admin.Settings do stripe_signing_secret_hint={@stripe_signing_secret_hint} stripe_has_signing_secret={@stripe_has_signing_secret} secret_form={@secret_form} - advanced_open={@advanced_open} + advanced_open={@stripe_advanced_open} /> <% end %>
    + + <%!-- Products --%> +
    +
    +

    Products

    + <%= if @printify do %> + <.status_pill color="green"> + <.icon name="hero-check-circle-mini" class="size-3" /> Connected + + <% else %> + <.status_pill color="zinc">Not connected + <% end %> +
    + + <%= if @printify do %> + <.printify_connected printify={@printify} /> + <% else %> +
    +

    + Connect a print-on-demand provider to import products into your shop. +

    +
    + <.link + navigate={~p"/admin/providers/new"} + class="inline-flex items-center gap-2 rounded-md bg-zinc-900 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-zinc-700" + > + <.icon name="hero-plus-mini" class="size-4" /> Connect to Printify + +
    +
    + <% end %> +
    + + <%!-- Account --%> +
    +

    Account

    + +
    + <.form + for={@email_form} + id="email_form" + phx-submit="update_email" + phx-change="validate_email" + > + <.input + field={@email_form[:email]} + type="email" + label="Email" + autocomplete="username" + required + /> +
    + <.button phx-disable-with="Saving...">Change email +
    + + +
    + <.form + for={@password_form} + id="password_form" + action={~p"/users/update-password"} + method="post" + phx-change="validate_password" + phx-submit="update_password" + phx-trigger-action={@trigger_submit} + > + + <.input + field={@password_form[:password]} + type="password" + label="New password" + autocomplete="new-password" + required + /> + <.input + field={@password_form[:password_confirmation]} + type="password" + label="Confirm new password" + autocomplete="new-password" + /> +
    + <.button phx-disable-with="Saving...">Change password +
    + +
    +
    +
    + + <%!-- Advanced --%> +
    +

    Advanced

    + +
    + <.link href={~p"/admin/dashboard"} class="text-sm text-zinc-600 hover:text-zinc-900"> + <.icon name="hero-chart-bar" class="size-4 inline" /> System dashboard + + <.link href={~p"/admin/errors"} class="text-sm text-zinc-600 hover:text-zinc-900"> + <.icon name="hero-bug-ant" class="size-4 inline" /> Error tracker + +
    +
    + + """ + end + + # -- Function components -- + + attr :color, :string, required: true + slot :inner_block, required: true + + defp status_pill(assigns) do + classes = + case assigns.color do + "green" -> "bg-green-50 text-green-700 ring-green-600/20" + "amber" -> "bg-amber-50 text-amber-700 ring-amber-600/20" + "zinc" -> "bg-zinc-50 text-zinc-600 ring-zinc-500/10" + _ -> "bg-zinc-50 text-zinc-600 ring-zinc-500/10" + end + + assigns = assign(assigns, :classes, classes) + + ~H""" + + {render_slot(@inner_block)} + + """ + end + + attr :printify, :map, required: true + + defp printify_connected(assigns) do + conn = assigns.printify.connection + + assigns = + assigns + |> assign(:connection, conn) + |> assign(:product_count, assigns.printify.product_count) + |> assign(:syncing, conn.sync_status == "syncing") + + ~H""" +
    +
    +
    +
    Provider
    +
    Printify
    +
    +
    +
    Shop
    +
    {@connection.name}
    +
    +
    +
    Products
    +
    {@product_count}
    +
    +
    +
    Last synced
    +
    + <%= if @connection.last_synced_at do %> + {format_relative_time(@connection.last_synced_at)} + <% else %> + Never + <% end %> +
    +
    +
    + +
    + + <.link + navigate={~p"/admin/providers/#{@connection.id}/edit"} + class="inline-flex items-center gap-1.5 rounded-md bg-zinc-100 px-3 py-1.5 text-sm font-medium text-zinc-700 hover:bg-zinc-200 ring-1 ring-zinc-300 ring-inset" + > + <.icon name="hero-cog-6-tooth" class="size-4" /> Settings + + +
    """ end @@ -291,7 +630,7 @@ defmodule SimpleshopThemeWeb.Admin.Settings do <% else %>
    """ end + + defp format_relative_time(datetime) do + diff = DateTime.diff(DateTime.utc_now(), datetime, :second) + + cond do + diff < 60 -> "just now" + diff < 3600 -> "#{div(diff, 60)} min ago" + diff < 86400 -> "#{div(diff, 3600)} hours ago" + true -> "#{div(diff, 86400)} days ago" + end + end end diff --git a/lib/simpleshop_theme_web/live/auth/settings.ex b/lib/simpleshop_theme_web/live/auth/settings.ex index 8707aa7..427297b 100644 --- a/lib/simpleshop_theme_web/live/auth/settings.ex +++ b/lib/simpleshop_theme_web/live/auth/settings.ex @@ -1,71 +1,9 @@ defmodule SimpleshopThemeWeb.Auth.Settings do use SimpleshopThemeWeb, :live_view - on_mount {SimpleshopThemeWeb.UserAuth, :require_sudo_mode} - alias SimpleshopTheme.Accounts - @impl true - def render(assigns) do - ~H""" - -
    - <.header> - Account Settings - <:subtitle>Manage your account email address and password settings - -
    - - <.form for={@email_form} id="email_form" phx-submit="update_email" phx-change="validate_email"> - <.input - field={@email_form[:email]} - type="email" - label="Email" - autocomplete="username" - required - /> - <.button variant="primary" phx-disable-with="Changing...">Change Email - - -
    - - <.form - for={@password_form} - id="password_form" - action={~p"/users/update-password"} - method="post" - phx-change="validate_password" - phx-submit="update_password" - phx-trigger-action={@trigger_submit} - > - - <.input - field={@password_form[:password]} - type="password" - label="New password" - autocomplete="new-password" - required - /> - <.input - field={@password_form[:password_confirmation]} - type="password" - label="Confirm new password" - autocomplete="new-password" - /> - <.button variant="primary" phx-disable-with="Saving..."> - Save Password - - - - """ - end - + # Confirm-email token: process it and redirect to admin settings @impl true def mount(%{"token" => token}, _session, socket) do socket = @@ -77,81 +15,16 @@ defmodule SimpleshopThemeWeb.Auth.Settings do put_flash(socket, :error, "Email change link is invalid or it has expired.") end - {:ok, push_navigate(socket, to: ~p"/users/settings")} + {:ok, redirect(socket, to: ~p"/admin/settings")} end + # Main mount: just redirect — account settings live in admin now def mount(_params, _session, socket) do - user = socket.assigns.current_scope.user - email_changeset = Accounts.change_user_email(user, %{}, validate_unique: false) - password_changeset = Accounts.change_user_password(user, %{}, hash_password: false) - - socket = - socket - |> assign(:current_email, user.email) - |> assign(:email_form, to_form(email_changeset)) - |> assign(:password_form, to_form(password_changeset)) - |> assign(:trigger_submit, false) - - {:ok, socket} + {:ok, redirect(socket, to: ~p"/admin/settings")} end @impl true - def handle_event("validate_email", params, socket) do - %{"user" => user_params} = params - - email_form = - socket.assigns.current_scope.user - |> Accounts.change_user_email(user_params, validate_unique: false) - |> Map.put(:action, :validate) - |> to_form() - - {:noreply, assign(socket, email_form: email_form)} - end - - def handle_event("update_email", params, socket) do - %{"user" => user_params} = params - user = socket.assigns.current_scope.user - true = Accounts.sudo_mode?(user) - - case Accounts.change_user_email(user, user_params) do - %{valid?: true} = changeset -> - Accounts.deliver_user_update_email_instructions( - Ecto.Changeset.apply_action!(changeset, :insert), - user.email, - &url(~p"/users/settings/confirm-email/#{&1}") - ) - - info = "A link to confirm your email change has been sent to the new address." - {:noreply, socket |> put_flash(:info, info)} - - changeset -> - {:noreply, assign(socket, :email_form, to_form(changeset, action: :insert))} - end - end - - def handle_event("validate_password", params, socket) do - %{"user" => user_params} = params - - password_form = - socket.assigns.current_scope.user - |> Accounts.change_user_password(user_params, hash_password: false) - |> Map.put(:action, :validate) - |> to_form() - - {:noreply, assign(socket, password_form: password_form)} - end - - def handle_event("update_password", params, socket) do - %{"user" => user_params} = params - user = socket.assigns.current_scope.user - true = Accounts.sudo_mode?(user) - - case Accounts.change_user_password(user, user_params) do - %{valid?: true} = changeset -> - {:noreply, assign(socket, trigger_submit: true, password_form: to_form(changeset))} - - changeset -> - {:noreply, assign(socket, password_form: to_form(changeset, action: :insert))} - end + def render(assigns) do + ~H"" end end diff --git a/lib/simpleshop_theme_web/user_auth.ex b/lib/simpleshop_theme_web/user_auth.ex index 13758d6..e81e0c4 100644 --- a/lib/simpleshop_theme_web/user_auth.ex +++ b/lib/simpleshop_theme_web/user_auth.ex @@ -257,9 +257,9 @@ defmodule SimpleshopThemeWeb.UserAuth do end @doc "Returns the path to redirect to after log in." - # the user was already logged in, redirect to settings + # the user was already logged in, redirect to admin settings def signed_in_path(%Plug.Conn{assigns: %{current_scope: %Scope{user: %Accounts.User{}}}}) do - ~p"/users/settings" + ~p"/admin/settings" end def signed_in_path(_), do: ~p"/" diff --git a/test/simpleshop_theme_web/controllers/user_session_controller_test.exs b/test/simpleshop_theme_web/controllers/user_session_controller_test.exs index d747da8..91ab3bd 100644 --- a/test/simpleshop_theme_web/controllers/user_session_controller_test.exs +++ b/test/simpleshop_theme_web/controllers/user_session_controller_test.exs @@ -21,7 +21,7 @@ defmodule SimpleshopThemeWeb.UserSessionControllerTest do assert redirected_to(conn) == ~p"/" # Now do a logged in request and assert on the page content - conn = get(conn, ~p"/users/settings") + conn = get(conn, ~p"/admin/settings") response = html_response(conn, 200) assert response =~ user.email end @@ -83,7 +83,7 @@ defmodule SimpleshopThemeWeb.UserSessionControllerTest do assert redirected_to(conn) == ~p"/" # Now do a logged in request and assert on the page content - conn = get(conn, ~p"/users/settings") + conn = get(conn, ~p"/admin/settings") response = html_response(conn, 200) assert response =~ user.email end @@ -105,7 +105,7 @@ defmodule SimpleshopThemeWeb.UserSessionControllerTest do assert Accounts.get_user!(user.id).confirmed_at # Now do a logged in request and assert on the page content - conn = get(conn, ~p"/users/settings") + conn = get(conn, ~p"/admin/settings") response = html_response(conn, 200) assert response =~ user.email end diff --git a/test/simpleshop_theme_web/live/admin/layout_test.exs b/test/simpleshop_theme_web/live/admin/layout_test.exs index dd99cc1..4607d9d 100644 --- a/test/simpleshop_theme_web/live/admin/layout_test.exs +++ b/test/simpleshop_theme_web/live/admin/layout_test.exs @@ -19,7 +19,6 @@ defmodule SimpleshopThemeWeb.Admin.LayoutTest do assert has_element?(view, ~s(a[href="/admin/orders"]), "Orders") assert has_element?(view, ~s(a[href="/admin/theme"]), "Theme") - assert has_element?(view, ~s(a[href="/admin/providers"]), "Providers") assert has_element?(view, ~s(a[href="/admin/settings"]), "Settings") end diff --git a/test/simpleshop_theme_web/live/admin/settings_test.exs b/test/simpleshop_theme_web/live/admin/settings_test.exs index 20fdc48..0ae650f 100644 --- a/test/simpleshop_theme_web/live/admin/settings_test.exs +++ b/test/simpleshop_theme_web/live/admin/settings_test.exs @@ -3,7 +3,9 @@ defmodule SimpleshopThemeWeb.Admin.SettingsTest do import Phoenix.LiveViewTest import SimpleshopTheme.AccountsFixtures + import SimpleshopTheme.ProductsFixtures + alias SimpleshopTheme.Accounts alias SimpleshopTheme.Settings setup do @@ -77,7 +79,7 @@ defmodule SimpleshopThemeWeb.Admin.SettingsTest do html = view - |> form("form", %{stripe: %{api_key: ""}}) + |> form(~s(form[phx-submit="connect_stripe"]), %{stripe: %{api_key: ""}}) |> render_submit() assert html =~ "Please enter your Stripe secret key" @@ -106,7 +108,9 @@ defmodule SimpleshopThemeWeb.Admin.SettingsTest do html = view - |> form("form", %{webhook: %{signing_secret: "whsec_test_manual_456"}}) + |> form(~s(form[phx-submit="save_signing_secret"]), %{ + webhook: %{signing_secret: "whsec_test_manual_456"} + }) |> render_submit() assert html =~ "Webhook signing secret saved" @@ -118,7 +122,7 @@ defmodule SimpleshopThemeWeb.Admin.SettingsTest do html = view - |> form("form", %{webhook: %{signing_secret: ""}}) + |> form(~s(form[phx-submit="save_signing_secret"]), %{webhook: %{signing_secret: ""}}) |> render_submit() assert html =~ "Please enter a signing secret" @@ -135,4 +139,118 @@ defmodule SimpleshopThemeWeb.Admin.SettingsTest do refute Settings.has_secret?("stripe_api_key") end end + + describe "products section" do + setup %{conn: conn, user: user} do + conn = log_in_user(conn, user) + %{conn: conn} + end + + test "shows connect button when no provider connected", %{conn: conn} do + {:ok, view, html} = live(conn, ~p"/admin/settings") + + assert html =~ "Products" + assert html =~ "Not connected" + assert has_element?(view, ~s(a[href="/admin/providers/new"]), "Connect to Printify") + end + + test "shows connection info when provider connected", %{conn: conn} do + conn_record = provider_connection_fixture(%{name: "Test Shop"}) + product_fixture(%{provider_connection: conn_record}) + + {:ok, _view, html} = live(conn, ~p"/admin/settings") + + assert html =~ "Connected" + assert html =~ "Test Shop" + assert html =~ "Sync products" + end + end + + describe "account section" do + setup %{conn: conn, user: user} do + conn = log_in_user(conn, user) + %{conn: conn, user: user} + end + + test "renders email and password forms", %{conn: conn, user: user} do + {:ok, view, html} = live(conn, ~p"/admin/settings") + + assert html =~ "Account" + assert html =~ user.email + assert has_element?(view, "#email_form") + assert has_element?(view, "#password_form") + end + + test "validates email change", %{conn: conn} do + {:ok, view, _html} = live(conn, ~p"/admin/settings") + + result = + view + |> element("#email_form") + |> render_change(%{"user" => %{"email" => "with spaces"}}) + + assert result =~ "must have the @ sign and no spaces" + end + + test "submits email change", %{conn: conn} do + {:ok, view, _html} = live(conn, ~p"/admin/settings") + + result = + view + |> form("#email_form", %{"user" => %{"email" => unique_user_email()}}) + |> render_submit() + + assert result =~ "A link to confirm your email" + end + + test "validates password", %{conn: conn} do + {:ok, view, _html} = live(conn, ~p"/admin/settings") + + result = + view + |> element("#password_form") + |> render_change(%{ + "user" => %{ + "password" => "short", + "password_confirmation" => "mismatch" + } + }) + + assert result =~ "should be at least 12 character(s)" + end + + test "submits valid password change", %{conn: conn, user: user} do + new_password = valid_user_password() + {:ok, view, _html} = live(conn, ~p"/admin/settings") + + form = + form(view, "#password_form", %{ + "user" => %{ + "email" => user.email, + "password" => new_password, + "password_confirmation" => new_password + } + }) + + render_submit(form) + new_password_conn = follow_trigger_action(form, conn) + + assert redirected_to(new_password_conn) == ~p"/admin/settings" + assert Accounts.get_user_by_email_and_password(user.email, new_password) + end + end + + describe "advanced section" do + setup %{conn: conn, user: user} do + conn = log_in_user(conn, user) + %{conn: conn} + end + + test "shows links to system tools", %{conn: conn} do + {:ok, view, _html} = live(conn, ~p"/admin/settings") + + assert has_element?(view, ~s(a[href="/admin/dashboard"]), "System dashboard") + assert has_element?(view, ~s(a[href="/admin/errors"]), "Error tracker") + end + end end diff --git a/test/simpleshop_theme_web/live/auth/settings_test.exs b/test/simpleshop_theme_web/live/auth/settings_test.exs index 8b95950..d028218 100644 --- a/test/simpleshop_theme_web/live/auth/settings_test.exs +++ b/test/simpleshop_theme_web/live/auth/settings_test.exs @@ -5,159 +5,18 @@ defmodule SimpleshopThemeWeb.Auth.SettingsTest do import Phoenix.LiveViewTest import SimpleshopTheme.AccountsFixtures - describe "Settings page" do - test "renders settings page", %{conn: conn} do - {:ok, _lv, html} = - conn - |> log_in_user(user_fixture()) - |> live(~p"/users/settings") - - assert html =~ "Change Email" - assert html =~ "Save Password" + describe "settings redirect" do + test "redirects to admin settings when logged in", %{conn: conn} do + conn = log_in_user(conn, user_fixture()) + assert {:error, {:redirect, %{to: "/admin/settings"}}} = live(conn, ~p"/users/settings") end - test "redirects if user is not logged in", %{conn: conn} do + test "redirects to login when not logged in", %{conn: conn} do assert {:error, redirect} = live(conn, ~p"/users/settings") - assert {:redirect, %{to: path, flash: flash}} = redirect assert path == ~p"/users/log-in" assert %{"error" => "You must log in to access this page."} = flash end - - test "redirects if user is not in sudo mode", %{conn: conn} do - {:ok, conn} = - conn - |> log_in_user(user_fixture(), - token_authenticated_at: DateTime.add(DateTime.utc_now(:second), -11, :minute) - ) - |> live(~p"/users/settings") - |> follow_redirect(conn, ~p"/users/log-in") - - assert conn.resp_body =~ "You must re-authenticate to access this page." - end - end - - describe "update email form" do - setup %{conn: conn} do - user = user_fixture() - %{conn: log_in_user(conn, user), user: user} - end - - test "updates the user email", %{conn: conn, user: user} do - new_email = unique_user_email() - - {:ok, lv, _html} = live(conn, ~p"/users/settings") - - result = - lv - |> form("#email_form", %{ - "user" => %{"email" => new_email} - }) - |> render_submit() - - assert result =~ "A link to confirm your email" - assert Accounts.get_user_by_email(user.email) - end - - test "renders errors with invalid data (phx-change)", %{conn: conn} do - {:ok, lv, _html} = live(conn, ~p"/users/settings") - - result = - lv - |> element("#email_form") - |> render_change(%{ - "action" => "update_email", - "user" => %{"email" => "with spaces"} - }) - - assert result =~ "Change Email" - assert result =~ "must have the @ sign and no spaces" - end - - test "renders errors with invalid data (phx-submit)", %{conn: conn, user: user} do - {:ok, lv, _html} = live(conn, ~p"/users/settings") - - result = - lv - |> form("#email_form", %{ - "user" => %{"email" => user.email} - }) - |> render_submit() - - assert result =~ "Change Email" - assert result =~ "did not change" - end - end - - describe "update password form" do - setup %{conn: conn} do - user = user_fixture() - %{conn: log_in_user(conn, user), user: user} - end - - test "updates the user password", %{conn: conn, user: user} do - new_password = valid_user_password() - - {:ok, lv, _html} = live(conn, ~p"/users/settings") - - form = - form(lv, "#password_form", %{ - "user" => %{ - "email" => user.email, - "password" => new_password, - "password_confirmation" => new_password - } - }) - - render_submit(form) - - new_password_conn = follow_trigger_action(form, conn) - - assert redirected_to(new_password_conn) == ~p"/users/settings" - - assert get_session(new_password_conn, :user_token) != get_session(conn, :user_token) - - assert Phoenix.Flash.get(new_password_conn.assigns.flash, :info) =~ - "Password updated successfully" - - assert Accounts.get_user_by_email_and_password(user.email, new_password) - end - - test "renders errors with invalid data (phx-change)", %{conn: conn} do - {:ok, lv, _html} = live(conn, ~p"/users/settings") - - result = - lv - |> element("#password_form") - |> render_change(%{ - "user" => %{ - "password" => "too short", - "password_confirmation" => "does not match" - } - }) - - assert result =~ "Save Password" - assert result =~ "should be at least 12 character(s)" - assert result =~ "does not match password" - end - - test "renders errors with invalid data (phx-submit)", %{conn: conn} do - {:ok, lv, _html} = live(conn, ~p"/users/settings") - - result = - lv - |> form("#password_form", %{ - "user" => %{ - "password" => "too short", - "password_confirmation" => "does not match" - } - }) - |> render_submit() - - assert result =~ "Save Password" - assert result =~ "should be at least 12 character(s)" - assert result =~ "does not match password" - end end describe "confirm email" do @@ -176,33 +35,30 @@ defmodule SimpleshopThemeWeb.Auth.SettingsTest do test "updates the user email once", %{conn: conn, user: user, token: token, email: email} do {:error, redirect} = live(conn, ~p"/users/settings/confirm-email/#{token}") - assert {:live_redirect, %{to: path, flash: flash}} = redirect - assert path == ~p"/users/settings" - assert %{"info" => message} = flash - assert message == "Email changed successfully." + assert {:redirect, %{to: "/admin/settings", flash: flash}} = redirect + assert %{"info" => "Email changed successfully."} = flash refute Accounts.get_user_by_email(user.email) assert Accounts.get_user_by_email(email) # use confirm token again {:error, redirect} = live(conn, ~p"/users/settings/confirm-email/#{token}") - assert {:live_redirect, %{to: path, flash: flash}} = redirect - assert path == ~p"/users/settings" - assert %{"error" => message} = flash - assert message == "Email change link is invalid or it has expired." + + assert {:redirect, %{to: "/admin/settings", flash: flash}} = redirect + assert %{"error" => "Email change link is invalid or it has expired."} = flash end test "does not update email with invalid token", %{conn: conn, user: user} do {:error, redirect} = live(conn, ~p"/users/settings/confirm-email/oops") - assert {:live_redirect, %{to: path, flash: flash}} = redirect - assert path == ~p"/users/settings" - assert %{"error" => message} = flash - assert message == "Email change link is invalid or it has expired." + + assert {:redirect, %{to: "/admin/settings", flash: flash}} = redirect + assert %{"error" => "Email change link is invalid or it has expired."} = flash assert Accounts.get_user_by_email(user.email) end test "redirects if user is not logged in", %{token: token} do conn = build_conn() {:error, redirect} = live(conn, ~p"/users/settings/confirm-email/#{token}") + assert {:redirect, %{to: path, flash: flash}} = redirect assert path == ~p"/users/log-in" assert %{"error" => message} = flash diff --git a/test/simpleshop_theme_web/user_auth_test.exs b/test/simpleshop_theme_web/user_auth_test.exs index d6306fe..08586a4 100644 --- a/test/simpleshop_theme_web/user_auth_test.exs +++ b/test/simpleshop_theme_web/user_auth_test.exs @@ -80,7 +80,7 @@ defmodule SimpleshopThemeWeb.UserAuthTest do |> assign(:current_scope, Scope.for_user(user)) |> UserAuth.log_in_user(user) - assert redirected_to(conn) == ~p"/users/settings" + assert redirected_to(conn) == ~p"/admin/settings" end test "writes a cookie if remember_me was set in previous session", %{conn: conn, user: user} do