towerops/test/towerops/accounts/user_recovery_code_test.exs

143 lines
4.2 KiB
Elixir

defmodule Towerops.Accounts.UserRecoveryCodeTest do
use Towerops.DataCase, async: true
import Towerops.AccountsFixtures
alias Towerops.Accounts.UserRecoveryCode
# doctest UserRecoveryCode
describe "generate_code/0" do
test "generates a code in XXXX-XXXX format" do
code = UserRecoveryCode.generate_code()
assert code =~ ~r/^[A-Z2-9]{4}-[A-Z2-9]{4}$/
end
test "generates unique codes" do
codes = for _ <- 1..100, do: UserRecoveryCode.generate_code()
assert length(Enum.uniq(codes)) == 100
end
test "excludes ambiguous characters (0, O, I, 1)" do
# Generate many codes and check none contain ambiguous chars
codes = for _ <- 1..200, do: UserRecoveryCode.generate_code()
for code <- codes do
refute String.contains?(code, "0"), "Code #{code} contains '0'"
refute String.contains?(code, "O"), "Code #{code} contains 'O'"
refute String.contains?(code, "I"), "Code #{code} contains 'I'"
refute String.contains?(code, "1"), "Code #{code} contains '1'"
end
end
test "code is exactly 9 characters (8 + hyphen)" do
code = UserRecoveryCode.generate_code()
assert String.length(code) == 9
end
end
describe "hash_code/1" do
test "returns a SHA-256 hex string" do
hash = UserRecoveryCode.hash_code("ABCD-EFGH")
assert String.length(hash) == 64
assert hash =~ ~r/^[0-9a-f]{64}$/
end
test "same input produces same hash" do
hash1 = UserRecoveryCode.hash_code("TEST-CODE")
hash2 = UserRecoveryCode.hash_code("TEST-CODE")
assert hash1 == hash2
end
test "different inputs produce different hashes" do
hash1 = UserRecoveryCode.hash_code("AAAA-BBBB")
hash2 = UserRecoveryCode.hash_code("CCCC-DDDD")
refute hash1 == hash2
end
end
describe "changeset/2" do
test "valid changeset with required fields" do
user = user_fixture()
attrs = %{
code_hash: UserRecoveryCode.hash_code("TEST-CODE"),
user_id: user.id,
created_at: DateTime.utc_now()
}
changeset = UserRecoveryCode.changeset(%UserRecoveryCode{}, attrs)
assert changeset.valid?
end
test "invalid without code_hash" do
user = user_fixture()
attrs = %{user_id: user.id, created_at: DateTime.utc_now()}
changeset = UserRecoveryCode.changeset(%UserRecoveryCode{}, attrs)
refute changeset.valid?
assert %{code_hash: ["can't be blank"]} = errors_on(changeset)
end
test "invalid without user_id" do
attrs = %{
code_hash: UserRecoveryCode.hash_code("TEST-CODE"),
created_at: DateTime.utc_now()
}
changeset = UserRecoveryCode.changeset(%UserRecoveryCode{}, attrs)
refute changeset.valid?
assert %{user_id: ["can't be blank"]} = errors_on(changeset)
end
test "invalid without created_at" do
user = user_fixture()
attrs = %{
code_hash: UserRecoveryCode.hash_code("TEST-CODE"),
user_id: user.id
}
changeset = UserRecoveryCode.changeset(%UserRecoveryCode{}, attrs)
refute changeset.valid?
assert %{created_at: ["can't be blank"]} = errors_on(changeset)
end
test "enforces unique code_hash" do
user = user_fixture()
hash = UserRecoveryCode.hash_code("UNIQ-CODE")
attrs = %{code_hash: hash, user_id: user.id, created_at: DateTime.utc_now()}
{:ok, _} = %UserRecoveryCode{} |> UserRecoveryCode.changeset(attrs) |> Repo.insert()
assert {:error, changeset} =
%UserRecoveryCode{} |> UserRecoveryCode.changeset(attrs) |> Repo.insert()
assert %{code_hash: ["has already been taken"]} = errors_on(changeset)
end
end
describe "use_changeset/1" do
test "sets used_at to current time" do
user = user_fixture()
{:ok, code} =
%UserRecoveryCode{}
|> UserRecoveryCode.changeset(%{
code_hash: UserRecoveryCode.hash_code("USE-TEST1"),
user_id: user.id,
created_at: DateTime.utc_now()
})
|> Repo.insert()
assert code.used_at == nil
changeset = UserRecoveryCode.use_changeset(code)
assert changeset.changes.used_at
{:ok, used_code} = Repo.update(changeset)
assert used_code.used_at
end
end
end