defmodule Aprsme.Accounts.UserTest do use Aprsme.DataCase, async: true alias Aprsme.Accounts.User describe "registration_changeset/2" do test "requires callsign" do changeset = User.registration_changeset(%User{}, %{}) errors = errors_on(changeset) assert Map.has_key?(errors, :callsign) assert "can't be blank" in errors.callsign end test "validates callsign format" do invalid_callsigns = ["123", "TOOLONGCALLSIGN", "NO SPACES", "nodigit"] for callsign <- invalid_callsigns do changeset = User.registration_changeset(%User{}, %{ email: "test@example.com", password: "valid_password123", callsign: callsign }) errors = errors_on(changeset) assert Map.has_key?(errors, :callsign), "Expected callsign error for: #{callsign}" assert "must be a valid amateur radio callsign" in errors.callsign end end test "validates empty callsign" do changeset = User.registration_changeset(%User{}, %{ email: "test@example.com", password: "valid_password123", callsign: "" }) errors = errors_on(changeset) assert Map.has_key?(errors, :callsign) assert "can't be blank" in errors.callsign end test "accepts valid callsigns" do valid_callsigns = ["K1ABC", "W2XYZ", "AA3BB", "KG4AAA", "N7PQR", "VE3ABC"] for callsign <- valid_callsigns do changeset = User.registration_changeset(%User{}, %{ email: "test@example.com", password: "valid_password123", callsign: callsign }) assert changeset.valid? end end test "converts callsign to uppercase" do changeset = User.registration_changeset(%User{}, %{ email: "test@example.com", password: "valid_password123", callsign: "k1abc" }) assert get_change(changeset, :callsign) == "K1ABC" end end describe "callsign_changeset/2" do test "requires callsign to change" do user = %User{callsign: "K1ABC"} changeset = User.callsign_changeset(user, %{callsign: "K1ABC"}) errors = errors_on(changeset) assert Map.has_key?(errors, :callsign) assert "did not change" in errors.callsign end test "validates new callsign format" do user = %User{callsign: "K1ABC"} changeset = User.callsign_changeset(user, %{callsign: "INVALID"}) errors = errors_on(changeset) assert Map.has_key?(errors, :callsign) assert "must be a valid amateur radio callsign" in errors.callsign end test "accepts valid callsign change" do user = %User{callsign: "K1ABC"} changeset = User.callsign_changeset(user, %{callsign: "W2XYZ"}) assert changeset.valid? assert get_change(changeset, :callsign) == "W2XYZ" end end describe "email_changeset/2" do test "requires email to change" do user = %User{email: "a@b.com"} changeset = User.email_changeset(user, %{email: "a@b.com"}) errors = errors_on(changeset) assert Map.has_key?(errors, :email) assert "did not change" in errors.email end test "validates format of the new email" do user = %User{email: "a@b.com"} changeset = User.email_changeset(user, %{email: "bad email"}) errors = errors_on(changeset) assert "must have the @ sign and no spaces" in errors.email end end describe "password_changeset/2" do test "requires matching confirmation" do user = %User{} changeset = User.password_changeset(user, %{ "password" => "abcdefghijkl", "password_confirmation" => "different" }) errors = errors_on(changeset) assert "does not match password" in errors.password_confirmation end test "hashes password when valid" do user = %User{} changeset = User.password_changeset(user, %{ "password" => "abcdefghijkl", "password_confirmation" => "abcdefghijkl" }) assert changeset.valid? assert byte_size(get_change(changeset, :hashed_password)) > 0 # Plaintext password is dropped after hashing. refute get_change(changeset, :password) end end describe "confirm_changeset/1" do test "sets confirmed_at to the current time truncated to seconds" do changeset = User.confirm_changeset(%User{}) confirmed_at = get_change(changeset, :confirmed_at) assert %NaiveDateTime{} = confirmed_at # No microseconds — truncated. assert confirmed_at.microsecond == {0, 0} end end describe "valid_password?/2" do test "returns false when the user has no hashed password" do refute User.valid_password?(%User{hashed_password: nil}, "any") end test "returns false when the provided password is empty" do user = %User{hashed_password: Bcrypt.hash_pwd_salt("hello world!")} refute User.valid_password?(user, "") end test "returns true for the correct password" do user = %User{hashed_password: Bcrypt.hash_pwd_salt("hello world!")} assert User.valid_password?(user, "hello world!") end test "returns false for the wrong password" do user = %User{hashed_password: Bcrypt.hash_pwd_salt("hello world!")} refute User.valid_password?(user, "wrong") end end describe "validate_current_password/2" do test "adds an error when the password is wrong" do user = %User{hashed_password: Bcrypt.hash_pwd_salt("hello world!")} changeset = Ecto.Changeset.change(user) changeset = User.validate_current_password(changeset, "wrong") errors = errors_on(changeset) assert "is not valid" in errors.current_password end test "does not add an error when the password is correct" do user = %User{hashed_password: Bcrypt.hash_pwd_salt("hello world!")} changeset = Ecto.Changeset.change(user) changeset = User.validate_current_password(changeset, "hello world!") assert changeset.valid? end end describe "Inspect protocol" do test "redacts the hashed and plaintext passwords" do user = %User{ email: "a@b.com", hashed_password: "super-secret-hash", password: "super-secret" } output = inspect(user) refute output =~ "super-secret-hash" refute output =~ "super-secret" # The email is non-redacted, so it should still appear. assert output =~ "a@b.com" changeset = User.password_changeset(%User{}, %{"password" => "newpass12345", "password_confirmation" => "newpass12345"}) assert changeset.valid? assert byte_size(get_change(changeset, :hashed_password)) > 0 end end end