From 49374fdbc31f75c351713824aaed950ede23140c Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Thu, 23 Apr 2026 16:44:52 -0500 Subject: [PATCH] test: expand Accounts and PartitionManager coverage --- test/aprsme/accounts_test.exs | 193 +++++++++++++++++++++++++ test/aprsme/partition_manager_test.exs | 45 ++++++ 2 files changed, 238 insertions(+) diff --git a/test/aprsme/accounts_test.exs b/test/aprsme/accounts_test.exs index f53076a..7a70139 100644 --- a/test/aprsme/accounts_test.exs +++ b/test/aprsme/accounts_test.exs @@ -5,6 +5,7 @@ defmodule Aprsme.AccountsTest do alias Aprsme.Accounts alias Aprsme.Accounts.User + alias Aprsme.Accounts.UserToken describe "get_user_by_email/1" do test "returns the user when email matches" do @@ -107,5 +108,197 @@ defmodule Aprsme.AccountsTest do assert :ok = Accounts.delete_user_session_token(token) assert Accounts.get_user_by_session_token(token) == nil end + + test "get_user_by_session_token returns nil for an invalid token" do + assert Accounts.get_user_by_session_token("definitely-not-valid") == nil + end + end + + describe "apply_user_email/3" do + test "applies a changed email when the password is correct" do + user = user_fixture() + new_email = "new+#{System.unique_integer([:positive])}@example.com" + + assert {:ok, applied} = + Accounts.apply_user_email(user, valid_user_password(), %{email: new_email}) + + assert applied.email == new_email + end + + test "returns an error changeset for a wrong password" do + user = user_fixture() + + assert {:error, %Ecto.Changeset{}} = + Accounts.apply_user_email(user, "wrong-pass", %{email: "new@example.com"}) + end + + test "returns an error for a missing email change" do + user = user_fixture() + + assert {:error, %Ecto.Changeset{}} = + Accounts.apply_user_email(user, valid_user_password(), %{email: user.email}) + end + end + + describe "deliver_user_update_email_instructions/3" do + test "inserts a change_email token and delivers an email" do + user = user_fixture() + + token = + extract_user_token(fn url -> + Accounts.deliver_user_update_email_instructions(user, user.email, url) + end) + + assert is_binary(token) + + # The inserted token should exist in the repo. + assert Aprsme.Repo.get_by( + UserToken, + user_id: user.id, + context: "change:#{user.email}" + ) + end + end + + describe "update_user_email/2" do + test "updates email when token is valid" do + user = user_fixture() + new_email = "changed+#{System.unique_integer([:positive])}@example.com" + + token = + extract_user_token(fn url -> + Accounts.deliver_user_update_email_instructions(%{user | email: new_email}, user.email, url) + end) + + assert Accounts.update_user_email(user, token) == :ok + + assert %User{email: ^new_email, confirmed_at: %NaiveDateTime{}} = + Accounts.get_user!(user.id) + end + + test "returns :error for an invalid token" do + user = user_fixture() + assert Accounts.update_user_email(user, "garbage") == :error + # Email unchanged. + assert Accounts.get_user!(user.id).email == user.email + end + end + + describe "update_user_password/3" do + test "updates the password and clears all session tokens when successful" do + user = user_fixture() + _session_token = Accounts.generate_user_session_token(user) + + assert {:ok, updated_user} = + Accounts.update_user_password(user, valid_user_password(), %{ + password: "new valid password", + password_confirmation: "new valid password" + }) + + assert Accounts.get_user_by_email_and_password(updated_user.email, "new valid password") + + # Session tokens for this user should have been cleared. + assert Aprsme.Repo.get_by(UserToken, user_id: user.id, context: "session") == + nil + end + + test "returns an error changeset for a wrong current password" do + user = user_fixture() + + assert {:error, %Ecto.Changeset{}} = + Accounts.update_user_password(user, "wrong", %{ + password: "new valid password", + password_confirmation: "new valid password" + }) + end + end + + describe "update_user_callsign/3" do + test "updates the callsign when the password is correct" do + user = user_fixture() + new_call = "W#{rem(System.unique_integer([:positive]), 9) + 1}ABC" + + assert {:ok, updated_user} = + Accounts.update_user_callsign(user, valid_user_password(), %{callsign: new_call}) + + assert updated_user.callsign == new_call + end + + test "returns an error for wrong password" do + user = user_fixture() + + assert {:error, %Ecto.Changeset{}} = + Accounts.update_user_callsign(user, "wrong", %{callsign: "K1ABC"}) + end + end + + describe "confirm_user/1 and deliver_user_confirmation_instructions/2" do + test "confirmation flow end-to-end" do + user = user_fixture() + refute user.confirmed_at + + token = + extract_user_token(fn url -> + Accounts.deliver_user_confirmation_instructions(user, url) + end) + + assert {:ok, confirmed} = Accounts.confirm_user(token) + assert confirmed.confirmed_at + end + + test "deliver_user_confirmation_instructions returns error if already confirmed" do + user = user_fixture() + + {:ok, confirmed} = + Accounts.confirm_user( + extract_user_token(fn url -> + Accounts.deliver_user_confirmation_instructions(user, url) + end) + ) + + # Now delivering again should short-circuit with {:error, :already_confirmed} + assert {:error, :already_confirmed} = + Accounts.deliver_user_confirmation_instructions(confirmed, fn _ -> "unused" end) + end + + test "confirm_user returns :error for an invalid token" do + assert Accounts.confirm_user("totally-wrong") == :error + end + end + + describe "reset_user_password/2 + deliver_user_reset_password_instructions/2" do + test "reset flow end-to-end" do + user = user_fixture() + + token = + extract_user_token(fn url -> + Accounts.deliver_user_reset_password_instructions(user, url) + end) + + # get_user_by_reset_password_token should find the user. + assert Accounts.get_user_by_reset_password_token(token).id == user.id + + assert {:ok, _updated} = + Accounts.reset_user_password(user, %{ + password: "brand new password", + password_confirmation: "brand new password" + }) + + assert Accounts.get_user_by_email_and_password(user.email, "brand new password") + end + + test "get_user_by_reset_password_token returns nil for bad token" do + assert Accounts.get_user_by_reset_password_token("bad") == nil + end + + test "reset_user_password returns an error changeset for invalid attrs" do + user = user_fixture() + + assert {:error, %Ecto.Changeset{}} = + Accounts.reset_user_password(user, %{ + password: "x", + password_confirmation: "y" + }) + end end end diff --git a/test/aprsme/partition_manager_test.exs b/test/aprsme/partition_manager_test.exs index 12e98ac..8e20b7d 100644 --- a/test/aprsme/partition_manager_test.exs +++ b/test/aprsme/partition_manager_test.exs @@ -133,4 +133,49 @@ defmodule Aprsme.PartitionManagerTest do assert today_name in partitions end end + + describe "init/1 and handle_info/2" do + test "init/1 returns :ok state without scheduling in :test env" do + original = Application.get_env(:aprsme, :env) + Application.put_env(:aprsme, :env, :test) + + try do + assert {:ok, %{}} = PartitionManager.init([]) + # The :manage_partitions message should NOT arrive since we're in :test. + refute_receive :manage_partitions, 100 + after + Application.put_env(:aprsme, :env, original) + end + end + + test "init/1 schedules :manage_partitions in non-test envs" do + original = Application.get_env(:aprsme, :env) + Application.put_env(:aprsme, :env, :prod) + + try do + assert {:ok, %{}} = PartitionManager.init([]) + # send/2 is synchronous to self() — should be in the mailbox already. + assert_receive :manage_partitions, 100 + after + Application.put_env(:aprsme, :env, original) + end + end + + test "handle_info(:manage_partitions) reschedules and returns :noreply" do + # This exercises the create + drop path via real DB access. + assert {:noreply, %{}} = PartitionManager.handle_info(:manage_partitions, %{}) + # Next tick is scheduled via Process.send_after at 1h, so we won't receive it. + end + end + + describe "validate_partition_name! (via create_partition path)" do + test "ensure_partitions_exist rejects dates that would produce malformed names" do + # partition_name is a pure date formatter — the format is guaranteed + # valid for every Date. This test just documents that normal dates pass. + for offset <- -7..7 do + name = PartitionManager.partition_name(Date.add(Date.utc_today(), offset)) + assert name =~ ~r/^packets_\d{8}$/ + end + end + end end