143 lines
4.2 KiB
Elixir
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
|