- Add jump_credo_checks ~> 0.4 with all 20 checks enabled - Fix all standard Credo issues: 139 @spec (113 done, 26 remain), 4 refactoring, 3 alias usage, 9 System.cmd env, 5 unsafe_to_atom, 2 max line length, 9 assert_receive timeout - Fix 170+ jump_credo_checks warnings: - 117 TopLevelAliasImportRequire: move nested alias/import to module top - 32 UseObanProWorker: switch to Oban.Pro.Worker - 4 DoctestIExExamples: add doctests / create test file - ~20 WeakAssertion: strengthen type-check assertions - Various ConditionalAssertion, AssertReceiveTimeout fixes - Exclude vendor/ from Credo analysis - Remaining: 175 warnings (mostly opinionated WeakAssertion, AvoidSocketAssignsInTest), 26 @spec annotations
521 lines
18 KiB
Elixir
521 lines
18 KiB
Elixir
defmodule Microwaveprop.Accounts.UserTest do
|
|
use Microwaveprop.DataCase, async: true
|
|
|
|
import Microwaveprop.AccountsFixtures
|
|
|
|
alias Microwaveprop.Accounts.User
|
|
|
|
@valid_attrs %{
|
|
callsign: "W5ABC",
|
|
name: "Alice",
|
|
email: "alice@example.com",
|
|
password: "hunter2!!",
|
|
password_confirmation: "hunter2!!"
|
|
}
|
|
|
|
describe "admin_email/0" do
|
|
test "exposes the hard-coded admin email constant" do
|
|
assert User.admin_email() == "graham@mcintire.me"
|
|
end
|
|
end
|
|
|
|
describe "registration_changeset/3" do
|
|
test "valid attrs produce a valid changeset" do
|
|
changeset = User.registration_changeset(%User{}, @valid_attrs)
|
|
assert changeset.valid?
|
|
end
|
|
|
|
test "requires callsign, name, email, and password" do
|
|
changeset = User.registration_changeset(%User{}, %{})
|
|
errors = errors_on(changeset)
|
|
|
|
assert errors[:callsign]
|
|
assert errors[:name]
|
|
assert errors[:email]
|
|
assert errors[:password]
|
|
end
|
|
|
|
test "upcases callsign" do
|
|
attrs = Map.put(@valid_attrs, :callsign, "w5abc")
|
|
changeset = User.registration_changeset(%User{}, attrs)
|
|
assert changeset.valid?
|
|
assert Ecto.Changeset.get_change(changeset, :callsign) == "W5ABC"
|
|
end
|
|
|
|
test "rejects callsigns shorter than 3 chars" do
|
|
attrs = Map.put(@valid_attrs, :callsign, "AB")
|
|
changeset = User.registration_changeset(%User{}, attrs)
|
|
assert "must be 3-10 letters and digits" in errors_on(changeset).callsign
|
|
end
|
|
|
|
test "rejects callsigns longer than 10 chars" do
|
|
attrs = Map.put(@valid_attrs, :callsign, "ABCDEFGHIJK")
|
|
changeset = User.registration_changeset(%User{}, attrs)
|
|
assert "must be 3-10 letters and digits" in errors_on(changeset).callsign
|
|
end
|
|
|
|
test "rejects callsigns with non-alphanumeric chars" do
|
|
attrs = Map.put(@valid_attrs, :callsign, "W5-AB")
|
|
changeset = User.registration_changeset(%User{}, attrs)
|
|
assert "must be 3-10 letters and digits" in errors_on(changeset).callsign
|
|
end
|
|
|
|
test "rejects name longer than 100 chars" do
|
|
attrs = Map.put(@valid_attrs, :name, String.duplicate("a", 101))
|
|
changeset = User.registration_changeset(%User{}, attrs)
|
|
assert errors_on(changeset).name
|
|
end
|
|
|
|
test "rejects email without @ sign" do
|
|
attrs = Map.put(@valid_attrs, :email, "not-an-email")
|
|
changeset = User.registration_changeset(%User{}, attrs)
|
|
assert "must have the @ sign and no spaces" in errors_on(changeset).email
|
|
end
|
|
|
|
test "rejects email with whitespace" do
|
|
attrs = Map.put(@valid_attrs, :email, "bad @example.com")
|
|
changeset = User.registration_changeset(%User{}, attrs)
|
|
assert "must have the @ sign and no spaces" in errors_on(changeset).email
|
|
end
|
|
|
|
test "rejects email longer than 160 chars" do
|
|
long_email = String.duplicate("a", 161) <> "@example.com"
|
|
attrs = Map.put(@valid_attrs, :email, long_email)
|
|
changeset = User.registration_changeset(%User{}, attrs)
|
|
assert errors_on(changeset).email
|
|
end
|
|
|
|
test "rejects password shorter than 8 chars" do
|
|
attrs = %{@valid_attrs | password: "short", password_confirmation: "short"}
|
|
changeset = User.registration_changeset(%User{}, attrs)
|
|
assert errors_on(changeset).password
|
|
end
|
|
|
|
test "rejects password longer than 72 chars" do
|
|
long_password = String.duplicate("a", 73)
|
|
attrs = %{@valid_attrs | password: long_password, password_confirmation: long_password}
|
|
changeset = User.registration_changeset(%User{}, attrs)
|
|
assert errors_on(changeset).password
|
|
end
|
|
|
|
test "requires password confirmation to match" do
|
|
attrs = %{@valid_attrs | password_confirmation: "different!"}
|
|
changeset = User.registration_changeset(%User{}, attrs)
|
|
assert "does not match password" in errors_on(changeset).password_confirmation
|
|
end
|
|
|
|
test "hashes password by default and clears virtual field" do
|
|
changeset = User.registration_changeset(%User{}, @valid_attrs)
|
|
assert changeset.valid?
|
|
hashed = Ecto.Changeset.get_change(changeset, :hashed_password)
|
|
assert byte_size(hashed) > 0
|
|
assert String.starts_with?(hashed, "$2b$")
|
|
refute Ecto.Changeset.get_change(changeset, :password)
|
|
end
|
|
|
|
test "does not hash password when hash_password: false" do
|
|
changeset = User.registration_changeset(%User{}, @valid_attrs, hash_password: false)
|
|
assert changeset.valid?
|
|
refute Ecto.Changeset.get_change(changeset, :hashed_password)
|
|
assert Ecto.Changeset.get_change(changeset, :password) == @valid_attrs.password
|
|
end
|
|
|
|
test "final persisted struct contains hashed_password and no plaintext" do
|
|
{:ok, user} =
|
|
%User{}
|
|
|> User.registration_changeset(@valid_attrs)
|
|
|> Repo.insert()
|
|
|
|
assert byte_size(user.hashed_password) > 0
|
|
assert user.password == nil
|
|
end
|
|
|
|
test "enforces email uniqueness via unsafe_validate_unique" do
|
|
{:ok, _first} =
|
|
%User{}
|
|
|> User.registration_changeset(@valid_attrs)
|
|
|> Repo.insert()
|
|
|
|
duplicate = User.registration_changeset(%User{}, %{@valid_attrs | callsign: "W5XYZ"})
|
|
refute duplicate.valid?
|
|
assert "has already been taken" in errors_on(duplicate).email
|
|
end
|
|
|
|
test "enforces callsign uniqueness via unsafe_validate_unique" do
|
|
{:ok, _first} =
|
|
%User{}
|
|
|> User.registration_changeset(@valid_attrs)
|
|
|> Repo.insert()
|
|
|
|
duplicate = User.registration_changeset(%User{}, %{@valid_attrs | email: "other@example.com"})
|
|
refute duplicate.valid?
|
|
assert "has already been taken" in errors_on(duplicate).callsign
|
|
end
|
|
|
|
test "surfaces unique constraint error on email at insert time (default opts)" do
|
|
{:ok, _first} =
|
|
%User{}
|
|
|> User.registration_changeset(@valid_attrs)
|
|
|> Repo.insert()
|
|
|
|
# With the default opts, unique_constraint/3 is attached so the DB
|
|
# violation comes back as a changeset error rather than raising.
|
|
changeset = User.registration_changeset(%User{}, %{@valid_attrs | callsign: "W5XYZ"})
|
|
# unsafe_validate_unique already flags it at changeset-build time.
|
|
assert "has already been taken" in errors_on(changeset).email
|
|
end
|
|
|
|
test "validate_unique: false skips callsign and email uniqueness checks" do
|
|
{:ok, _first} =
|
|
%User{}
|
|
|> User.registration_changeset(@valid_attrs)
|
|
|> Repo.insert()
|
|
|
|
changeset = User.registration_changeset(%User{}, @valid_attrs, validate_unique: false)
|
|
# Uniqueness is not pre-validated, so the changeset is otherwise valid.
|
|
assert changeset.valid?
|
|
end
|
|
|
|
test "grants admin when email matches the admin_email constant (case-insensitive)" do
|
|
attrs = Map.put(@valid_attrs, :email, "GRAHAM@MCINTIRE.ME")
|
|
changeset = User.registration_changeset(%User{}, attrs)
|
|
assert changeset.valid?
|
|
assert Ecto.Changeset.get_change(changeset, :is_admin) == true
|
|
end
|
|
|
|
test "grants admin for exact admin email match" do
|
|
attrs = Map.put(@valid_attrs, :email, "graham@mcintire.me")
|
|
changeset = User.registration_changeset(%User{}, attrs)
|
|
assert Ecto.Changeset.get_change(changeset, :is_admin) == true
|
|
end
|
|
|
|
test "does not grant admin for other emails" do
|
|
changeset = User.registration_changeset(%User{}, @valid_attrs)
|
|
refute Ecto.Changeset.get_change(changeset, :is_admin)
|
|
end
|
|
|
|
test "does not set is_admin when email is missing" do
|
|
changeset = User.registration_changeset(%User{}, Map.delete(@valid_attrs, :email))
|
|
refute Ecto.Changeset.get_change(changeset, :is_admin)
|
|
end
|
|
end
|
|
|
|
describe "admin_changeset/2" do
|
|
setup do
|
|
{:ok, user} = Repo.insert(User.registration_changeset(%User{}, @valid_attrs))
|
|
%{user: user}
|
|
end
|
|
|
|
test "accepts valid updates and casts is_admin", %{user: user} do
|
|
changeset =
|
|
User.admin_changeset(user, %{
|
|
callsign: "K5NEW",
|
|
name: "New Name",
|
|
email: "new@example.com",
|
|
is_admin: true
|
|
})
|
|
|
|
assert changeset.valid?
|
|
assert Ecto.Changeset.get_change(changeset, :is_admin) == true
|
|
assert Ecto.Changeset.get_change(changeset, :callsign) == "K5NEW"
|
|
end
|
|
|
|
test "trims and upcases callsign", %{user: user} do
|
|
changeset = User.admin_changeset(user, %{callsign: " k5abc "})
|
|
assert Ecto.Changeset.get_change(changeset, :callsign) == "K5ABC"
|
|
end
|
|
|
|
test "requires callsign, name, and email", %{user: user} do
|
|
changeset =
|
|
User.admin_changeset(user, %{callsign: nil, name: nil, email: nil})
|
|
|
|
errors = errors_on(changeset)
|
|
assert errors[:callsign]
|
|
assert errors[:name]
|
|
assert errors[:email]
|
|
end
|
|
|
|
test "rejects invalid callsign format", %{user: user} do
|
|
changeset = User.admin_changeset(user, %{callsign: "bad-call"})
|
|
assert "must be 3-10 letters and digits" in errors_on(changeset).callsign
|
|
end
|
|
|
|
test "rejects invalid email", %{user: user} do
|
|
changeset = User.admin_changeset(user, %{email: "bad email"})
|
|
assert "must have the @ sign and no spaces" in errors_on(changeset).email
|
|
end
|
|
|
|
test "does not cast password", %{user: user} do
|
|
changeset = User.admin_changeset(user, %{password: "something1234"})
|
|
refute Ecto.Changeset.get_change(changeset, :password)
|
|
refute Ecto.Changeset.get_change(changeset, :hashed_password)
|
|
end
|
|
|
|
test "enforces email uniqueness", %{user: user} do
|
|
{:ok, _other} =
|
|
Repo.insert(
|
|
User.registration_changeset(%User{}, %{
|
|
@valid_attrs
|
|
| callsign: "K5OTH",
|
|
email: "other@example.com"
|
|
})
|
|
)
|
|
|
|
changeset = User.admin_changeset(user, %{email: "other@example.com"})
|
|
refute changeset.valid?
|
|
assert "has already been taken" in errors_on(changeset).email
|
|
end
|
|
|
|
test "enforces callsign uniqueness", %{user: user} do
|
|
{:ok, _other} =
|
|
Repo.insert(
|
|
User.registration_changeset(%User{}, %{
|
|
@valid_attrs
|
|
| callsign: "K5OTH",
|
|
email: "other@example.com"
|
|
})
|
|
)
|
|
|
|
changeset = User.admin_changeset(user, %{callsign: "K5OTH"})
|
|
refute changeset.valid?
|
|
assert "has already been taken" in errors_on(changeset).callsign
|
|
end
|
|
end
|
|
|
|
describe "email_changeset/3" do
|
|
setup do
|
|
{:ok, user} = Repo.insert(User.registration_changeset(%User{}, @valid_attrs))
|
|
%{user: user}
|
|
end
|
|
|
|
test "accepts a new valid email", %{user: user} do
|
|
changeset = User.email_changeset(user, %{email: "new-address@example.com"})
|
|
assert changeset.valid?
|
|
assert Ecto.Changeset.get_change(changeset, :email) == "new-address@example.com"
|
|
end
|
|
|
|
test "requires email", %{user: user} do
|
|
changeset = User.email_changeset(user, %{email: nil})
|
|
assert errors_on(changeset).email
|
|
end
|
|
|
|
test "rejects malformed email", %{user: user} do
|
|
changeset = User.email_changeset(user, %{email: "no-at-sign"})
|
|
assert "must have the @ sign and no spaces" in errors_on(changeset).email
|
|
end
|
|
|
|
test "rejects email longer than 160 chars", %{user: user} do
|
|
long = String.duplicate("a", 161) <> "@example.com"
|
|
changeset = User.email_changeset(user, %{email: long})
|
|
assert errors_on(changeset).email
|
|
end
|
|
|
|
test "errors if email did not change", %{user: user} do
|
|
changeset = User.email_changeset(user, %{email: user.email})
|
|
assert "did not change" in errors_on(changeset).email
|
|
end
|
|
|
|
test "enforces email uniqueness against existing users", %{user: user} do
|
|
{:ok, _other} =
|
|
Repo.insert(
|
|
User.registration_changeset(%User{}, %{
|
|
@valid_attrs
|
|
| callsign: "K5OTH",
|
|
email: "taken@example.com"
|
|
})
|
|
)
|
|
|
|
changeset = User.email_changeset(user, %{email: "taken@example.com"})
|
|
refute changeset.valid?
|
|
assert "has already been taken" in errors_on(changeset).email
|
|
end
|
|
|
|
test "validate_unique: false skips the uniqueness check", %{user: user} do
|
|
{:ok, _other} =
|
|
Repo.insert(
|
|
User.registration_changeset(%User{}, %{
|
|
@valid_attrs
|
|
| callsign: "K5OTH",
|
|
email: "taken@example.com"
|
|
})
|
|
)
|
|
|
|
changeset = User.email_changeset(user, %{email: "taken@example.com"}, validate_unique: false)
|
|
assert changeset.valid?
|
|
end
|
|
end
|
|
|
|
describe "password_changeset/3" do
|
|
setup do
|
|
{:ok, user} = Repo.insert(User.registration_changeset(%User{}, @valid_attrs))
|
|
%{user: user}
|
|
end
|
|
|
|
test "accepts a valid new password and hashes it by default", %{user: user} do
|
|
changeset =
|
|
User.password_changeset(user, %{
|
|
password: "new-password-123",
|
|
password_confirmation: "new-password-123"
|
|
})
|
|
|
|
assert changeset.valid?
|
|
hashed = Ecto.Changeset.get_change(changeset, :hashed_password)
|
|
assert byte_size(hashed) > 0
|
|
refute Ecto.Changeset.get_change(changeset, :password)
|
|
end
|
|
|
|
test "requires the password", %{user: user} do
|
|
changeset = User.password_changeset(user, %{})
|
|
assert errors_on(changeset).password
|
|
end
|
|
|
|
test "rejects password shorter than 8 chars", %{user: user} do
|
|
changeset =
|
|
User.password_changeset(user, %{password: "short", password_confirmation: "short"})
|
|
|
|
assert errors_on(changeset).password
|
|
end
|
|
|
|
test "rejects password longer than 72 chars", %{user: user} do
|
|
pw = String.duplicate("a", 73)
|
|
changeset = User.password_changeset(user, %{password: pw, password_confirmation: pw})
|
|
assert errors_on(changeset).password
|
|
end
|
|
|
|
test "requires password_confirmation to match", %{user: user} do
|
|
changeset =
|
|
User.password_changeset(user, %{
|
|
password: "new-password-123",
|
|
password_confirmation: "other-password-123"
|
|
})
|
|
|
|
assert "does not match password" in errors_on(changeset).password_confirmation
|
|
end
|
|
|
|
test "hash_password: false keeps plaintext and skips hashing", %{user: user} do
|
|
changeset =
|
|
User.password_changeset(
|
|
user,
|
|
%{password: "new-password-123", password_confirmation: "new-password-123"},
|
|
hash_password: false
|
|
)
|
|
|
|
assert changeset.valid?
|
|
refute Ecto.Changeset.get_change(changeset, :hashed_password)
|
|
assert Ecto.Changeset.get_change(changeset, :password) == "new-password-123"
|
|
end
|
|
end
|
|
|
|
describe "confirm_changeset/1" do
|
|
test "sets confirmed_at to the current UTC time (truncated to seconds)" do
|
|
user = %User{}
|
|
changeset = User.confirm_changeset(user)
|
|
confirmed_at = Ecto.Changeset.get_change(changeset, :confirmed_at)
|
|
|
|
assert %DateTime{} = confirmed_at
|
|
assert confirmed_at.microsecond == {0, 0}
|
|
assert DateTime.diff(DateTime.utc_now(), confirmed_at, :second) in 0..5
|
|
end
|
|
end
|
|
|
|
describe "valid_password?/2" do
|
|
test "returns true when the password matches the hash" do
|
|
user = user_fixture()
|
|
assert User.valid_password?(user, valid_user_password())
|
|
end
|
|
|
|
test "returns false for an incorrect password" do
|
|
user = user_fixture()
|
|
refute User.valid_password?(user, "wrong password")
|
|
end
|
|
|
|
test "returns false when hashed_password is nil" do
|
|
refute User.valid_password?(%User{hashed_password: nil}, "anything")
|
|
end
|
|
|
|
test "returns false for empty password input" do
|
|
user = user_fixture()
|
|
refute User.valid_password?(user, "")
|
|
end
|
|
|
|
test "returns false for a nil user-like value" do
|
|
refute User.valid_password?(nil, "anything")
|
|
end
|
|
end
|
|
|
|
describe "change_home_qth/2" do
|
|
test "derives lat/lon from a 6-char grid" do
|
|
changeset = User.change_home_qth(%User{}, %{"home_grid" => "EM13qc"})
|
|
assert changeset.valid?
|
|
lat = Ecto.Changeset.get_change(changeset, :home_lat)
|
|
lon = Ecto.Changeset.get_change(changeset, :home_lon)
|
|
assert_in_delta lat, 33.10, 0.05
|
|
assert_in_delta lon, -96.625, 0.05
|
|
assert Ecto.Changeset.get_change(changeset, :home_grid) == "EM13qc"
|
|
end
|
|
|
|
test "accepts explicit lat/lon without grid" do
|
|
changeset = User.change_home_qth(%User{}, %{"home_lat" => 33.0, "home_lon" => -96.7})
|
|
assert changeset.valid?
|
|
assert Ecto.Changeset.get_change(changeset, :home_lat) == 33.0
|
|
assert Ecto.Changeset.get_change(changeset, :home_lon) == -96.7
|
|
end
|
|
|
|
test "derives a 10-char home_grid when lat/lon are given without one" do
|
|
changeset = User.change_home_qth(%User{}, %{"home_lat" => 33.10, "home_lon" => -96.625})
|
|
assert changeset.valid?
|
|
grid = Ecto.Changeset.get_change(changeset, :home_grid)
|
|
assert byte_size(grid) > 0
|
|
assert String.length(grid) == 10
|
|
assert String.starts_with?(grid, "EM13qc")
|
|
end
|
|
|
|
test "preserves an explicit grid when both grid and lat/lon are provided" do
|
|
changeset =
|
|
User.change_home_qth(%User{}, %{
|
|
"home_grid" => "EM13qc",
|
|
"home_lat" => 33.10,
|
|
"home_lon" => -96.625
|
|
})
|
|
|
|
assert changeset.valid?
|
|
assert Ecto.Changeset.get_change(changeset, :home_grid) == "EM13qc"
|
|
end
|
|
|
|
test "rejects invalid grids" do
|
|
changeset = User.change_home_qth(%User{}, %{"home_grid" => "ZZ99zz"})
|
|
refute changeset.valid?
|
|
assert errors_on(changeset)[:home_grid]
|
|
end
|
|
|
|
test "requires either grid or lat/lon" do
|
|
changeset = User.change_home_qth(%User{}, %{})
|
|
refute changeset.valid?
|
|
assert errors_on(changeset)[:home_grid]
|
|
end
|
|
|
|
test "rejects out-of-range lat/lon" do
|
|
changeset = User.change_home_qth(%User{}, %{"home_lat" => 100.0, "home_lon" => 0.0})
|
|
refute changeset.valid?
|
|
assert errors_on(changeset)[:home_lat]
|
|
end
|
|
|
|
test "uppercase Field + lowercase subsquare normalisation" do
|
|
changeset = User.change_home_qth(%User{}, %{"home_grid" => "em13QC"})
|
|
assert changeset.valid?
|
|
assert Ecto.Changeset.get_change(changeset, :home_grid) == "EM13qc"
|
|
end
|
|
|
|
test "clears all home fields when home_grid is an empty string" do
|
|
user = %User{home_grid: "EM13qc", home_lat: 33.1, home_lon: -96.8, home_elevation_m: 200}
|
|
|
|
changeset =
|
|
User.change_home_qth(user, %{"home_grid" => "", "home_lat" => nil, "home_lon" => nil})
|
|
|
|
assert changeset.valid?
|
|
assert Ecto.Changeset.get_change(changeset, :home_grid) == nil
|
|
assert Ecto.Changeset.get_change(changeset, :home_lat) == nil
|
|
assert Ecto.Changeset.get_change(changeset, :home_lon) == nil
|
|
end
|
|
end
|
|
end
|